gemika 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,12 @@
1
1
  require 'yaml'
2
+ require 'active_record'
2
3
  require 'gemika/env'
4
+ require 'gemika/errors'
3
5
 
4
6
  module Gemika
7
+ ##
8
+ # Helpers for creating a test database.
9
+ #
5
10
  class Database
6
11
 
7
12
  class Error < StandardError; end
@@ -20,6 +25,9 @@ module Gemika
20
25
  @connected = false
21
26
  end
22
27
 
28
+ ##
29
+ # Connects ActiveRecord to the database configured in `spec/support/database.yml`.
30
+ #
23
31
  def connect
24
32
  unless @connected
25
33
  ActiveRecord::Base.establish_connection(adapter_config)
@@ -27,6 +35,9 @@ module Gemika
27
35
  end
28
36
  end
29
37
 
38
+ ##
39
+ # Drops all tables from the current database.
40
+ #
30
41
  def drop_tables!
31
42
  connect
32
43
  connection.tables.each do |table|
@@ -34,28 +45,52 @@ module Gemika
34
45
  end
35
46
  end
36
47
 
48
+ ##
49
+ # Runs the [ActiveRecord database migration](http://api.rubyonrails.org/classes/ActiveRecord/Migration.html) described in `block`.
50
+ #
51
+ # @example
52
+ # Gemika::Database.new.migrate do
53
+ # create_table :users do |t|
54
+ # t.string :name
55
+ # t.string :email
56
+ # t.string :city
57
+ # end
58
+ # end
37
59
  def migrate(&block)
38
60
  connect
39
61
  ActiveRecord::Migration.class_eval(&block)
40
62
  end
41
63
 
64
+ ##
65
+ # Drops all tables, then
66
+ # runs the [ActiveRecord database migration](http://api.rubyonrails.org/classes/ActiveRecord/Migration.html) described in `block`.
67
+ #
68
+ # @example
69
+ # Gemika::Database.new.rewrite_schema! do
70
+ # create_table :users do |t|
71
+ # t.string :name
72
+ # t.string :email
73
+ # t.string :city
74
+ # end
75
+ # end
42
76
  def rewrite_schema!(&block)
43
77
  connect
44
78
  drop_tables!
45
79
  migrate(&block)
46
80
  end
47
81
 
48
- private
49
-
82
+ ##
83
+ # Returns a hash of ActiveRecord adapter options for the currently activated database gem.
84
+ #
50
85
  def adapter_config
51
86
  default_config = {}
52
87
  default_config['database'] = guess_database_name
53
- if Env.pg?
88
+ if Env.gem?('pg')
54
89
  default_config['adapter'] = 'postgresql'
55
90
  default_config['username'] = 'postgres' if Env.travis?
56
91
  default_config['password'] = ''
57
92
  user_config = @yaml_config['postgresql'] || @yaml_config['postgres'] || @yaml_config['pg'] || {}
58
- elsif Env.mysql2?
93
+ elsif Env.gem?('mysql2')
59
94
  default_config['adapter'] = 'mysql2'
60
95
  default_config['username'] = 'travis' if Env.travis?
61
96
  default_config['encoding'] = 'utf8'
@@ -66,6 +101,8 @@ module Gemika
66
101
  default_config.merge(user_config)
67
102
  end
68
103
 
104
+ private
105
+
69
106
  def guess_database_name
70
107
  project_name = File.basename(Dir.pwd)
71
108
  "#{project_name}_test"
data/lib/gemika/env.rb CHANGED
@@ -1,84 +1,126 @@
1
+ require 'rubygems'
2
+ require 'gemika/errors'
3
+
1
4
  module Gemika
5
+ ##
6
+ # Version switches to write code that works with different versions of
7
+ # Ruby and gem dependencies.
8
+ #
2
9
  module Env
3
10
 
4
- class Error < StandardError; end
5
- class Unknown < Error; end
11
+ VERSION_PATTERN = /(?:\d+\.)*\d+/
6
12
 
13
+ ##
14
+ # Returns the path to the gemfile for the current Ruby process.
15
+ #
7
16
  def gemfile
8
- ENV['BUNDLE_GEMFILE']
9
- end
10
-
11
- def gemfile=(path)
12
- ENV['BUNDLE_GEMFILE'] = path
17
+ if @gemfile_changed
18
+ @process_gemfile
19
+ else
20
+ ENV['BUNDLE_GEMFILE']
21
+ end
13
22
  end
14
23
 
24
+ ##
25
+ # Changes the gemfile to the given `path`, runs the given `block`, then resets
26
+ # the gemfile to its original path.
27
+ #
28
+ # @example
29
+ # Gemika::Env.with_gemfile('gemfiles/Gemfile.rails3') do
30
+ # system('rspec spec') or raise 'RSpec failed'
31
+ # end
32
+ #
15
33
  def with_gemfile(path, *args, &block)
16
- old_gemfile = gemfile
17
- self.gemfile = path
18
- block.call(*args)
34
+ # Make sure that if block calls #gemfile we still return the gemfile for this
35
+ # process, regardless of what's in ENV temporarily
36
+ @gemfile_changed = true
37
+ @process_gemfile = ENV['BUNDLE_GEMFILE']
38
+ Bundler.with_clean_env do
39
+ ENV['BUNDLE_GEMFILE'] = path
40
+ block.call(*args)
41
+ end
19
42
  ensure
20
- self.gemfile = old_gemfile
21
- end
22
-
23
- def ruby_1_8?
24
- RUBY_VERSION.start_with?('1.8.')
25
- end
26
-
27
- def pg?
28
- gem?('pg')
29
- end
30
-
31
- def mysql2?
32
- gem?('mysql2')
33
- end
34
-
35
- def active_record_2?
36
- gem?('activerecord', '< 3')
43
+ @gemfile_changed = false
44
+ ENV['BUNDLE_GEMFILE'] = @process_gemfile
45
+ end
46
+
47
+ ##
48
+ # Check if the given gem was activated by the current gemfile.
49
+ # It might or might not have been `require`d yet.
50
+ #
51
+ # @example
52
+ # Gemika::Env.gem?('activerecord')
53
+ # Gemika::Env.gem?('activerecord', '= 5.0.0')
54
+ # Gemika::Env.gem?('activerecord', '~> 4.2.0')
55
+ #
56
+ def gem?(*args)
57
+ options = args.last.is_a?(Hash) ? args.pop : {}
58
+ name, requirement_string = args
59
+ if options[:gemfile] && !process_gemfile?(options[:gemfile])
60
+ gem_in_gemfile?(options[:gemfile], name, requirement_string)
61
+ else
62
+ gem_activated?(name, requirement_string)
63
+ end
37
64
  end
38
65
 
39
- def active_record_3_plus?
40
- gem?('activerecord', '>= 3')
66
+ ##
67
+ # Returns the current version of Ruby.
68
+ #
69
+ def ruby
70
+ RUBY_VERSION
41
71
  end
42
72
 
43
- def rspec_1?
44
- gem?('rspec', '< 2')
73
+ ##
74
+ # Check if the current version of Ruby satisfies the given requirements.
75
+ #
76
+ # @example
77
+ # Gemika::Env.ruby?('>= 2.1.0')
78
+ #
79
+ def ruby?(requirement)
80
+ requirement_satisfied?(requirement, ruby)
45
81
  end
46
82
 
47
- def rspec_2_plus?
48
- gem?('rspec', '>= 2')
83
+ ##
84
+ # Returns whether this process is running within a TravisCI build.
85
+ #
86
+ def travis?
87
+ !!ENV['TRAVIS']
49
88
  end
50
89
 
51
- def rspec_binary
52
- if rspec_1?
53
- 'spec'
54
- elsif rspec_2_plus?
55
- 'rspec'
90
+ ##
91
+ # Creates an hash that enumerates entries in order of insertion.
92
+ #
93
+ # @!visibility private
94
+ #
95
+ def new_ordered_hash
96
+ # We use it when ActiveSupport is activated
97
+ if ruby?('>= 1.9')
98
+ {}
99
+ elsif gem?('activesupport')
100
+ require 'active_support/ordered_hash'
101
+ ActiveSupport::OrderedHash.new
56
102
  else
57
- raise Unknown, 'Unknown rspec version'
103
+ # We give up
104
+ {}
58
105
  end
59
106
  end
60
107
 
61
- def rspec_1_in_gemfile?(gemfile)
62
- lockfile = "#{gemfile}.lock"
63
- contents = File.read(lockfile)
64
- contents =~ /\brspec \(1\./
108
+ private
109
+
110
+ def bundler?
111
+ !gemfile.nil? && gemfile != ''
65
112
  end
66
113
 
67
- def rspec_binary_for_gemfile(gemfile)
68
- if rspec_1_in_gemfile?(gemfile)
69
- 'spec'
70
- else
71
- 'rspec'
72
- end
114
+ def process_gemfile?(given_gemfile)
115
+ bundler? && File.expand_path(gemfile) == File.expand_path(given_gemfile)
73
116
  end
74
117
 
75
- def gem?(name, requirement_string = nil)
118
+ def gem_activated?(name, requirement)
76
119
  gem = Gem.loaded_specs[name]
77
120
  if gem
78
- if requirement_string
79
- requirement = Gem::Requirement.new(requirement_string)
121
+ if requirement
80
122
  version = gem.version
81
- gem_requirement_satisfied_by_version?(requirement, version)
123
+ requirement_satisfied?(requirement, version)
82
124
  else
83
125
  true
84
126
  end
@@ -87,29 +129,39 @@ module Gemika
87
129
  end
88
130
  end
89
131
 
90
- def travis?
91
- !!ENV['TRAVIS']
92
- end
93
-
94
- def new_ordered_hash
95
- if defined?(ActiveSupport::OrderedHash)
96
- ActiveSupport::OrderedHash.new
132
+ def gem_in_gemfile?(gemfile, name, requirement = nil)
133
+ lockfile = lockfile_contents(gemfile)
134
+ if lockfile =~ /\b#{Regexp.escape(name)}\s*\((#{VERSION_PATTERN})\)/
135
+ version = $1
136
+ if requirement
137
+ requirement_satisfied?(requirement, version)
138
+ else
139
+ true
140
+ end
97
141
  else
98
- {}
142
+ false
99
143
  end
100
144
  end
101
145
 
102
- private
103
-
104
- def gem_requirement_satisfied_by_version?(requirement, version)
105
- if Env.ruby_1_8?
146
+ def requirement_satisfied?(requirement, version)
147
+ requirement = Gem::Requirement.new(requirement) if requirement.is_a?(String)
148
+ version = Gem::Version.new(version) if version.is_a?(String)
149
+ if requirement.respond_to?(:satisfied_by?) # Ruby 1.9.3+
150
+ requirement.satisfied_by?(version)
151
+ else
106
152
  ops = Gem::Requirement::OPS
107
153
  requirement.requirements.all? { |op, rv| (ops[op] || ops["="]).call version, rv }
108
- else
109
- requirement.satisfied_by?(version)
110
154
  end
111
155
  end
112
156
 
157
+ def lockfile_contents(gemfile)
158
+ lockfile = "#{gemfile}.lock"
159
+ File.exists?(lockfile) or raise MissingLockfile, "Lockfile not found: #{lockfile}"
160
+ File.read(lockfile)
161
+ end
162
+
163
+ # Make all methods available as static module methods
113
164
  extend self
165
+
114
166
  end
115
167
  end
@@ -0,0 +1,9 @@
1
+ module Gemika
2
+ class Error < StandardError; end
3
+ class MissingGemfile < Error; end
4
+ class MissingLockfile < Error; end
5
+ class UnusableGemfile < Error; end
6
+ class UnsupportedRuby < Error; end
7
+ class MatrixFailed < Error; end
8
+ class RSpecFailed < Error; end
9
+ end
data/lib/gemika/matrix.rb CHANGED
@@ -1,14 +1,15 @@
1
1
  require 'yaml'
2
+ require 'gemika/errors'
2
3
  require 'gemika/env'
3
4
 
4
5
  module Gemika
5
6
  class Matrix
6
7
 
7
- class Error < StandardError; end
8
- class Invalid < Error; end
9
- class Failed < Error; end
10
- class Incompatible < Error; end
11
-
8
+ ##
9
+ # Load `.travis.yml` files.
10
+ #
11
+ # @!visibility private
12
+ #
12
13
  class TravisConfig
13
14
  class << self
14
15
 
@@ -37,6 +38,9 @@ module Gemika
37
38
  end
38
39
  end
39
40
 
41
+ ##
42
+ # A row in the test matrix
43
+ #
40
44
  class Row
41
45
 
42
46
  def initialize(attrs)
@@ -44,16 +48,32 @@ module Gemika
44
48
  @gemfile = attrs.fetch(:gemfile)
45
49
  end
46
50
 
47
- attr_reader :ruby, :gemfile
51
+ ##
52
+ # The Ruby version for the row.
53
+ #
54
+ attr_reader :ruby
55
+
56
+ ##
57
+ # The path to the gemfile for the row.
58
+ #
59
+ attr_reader :gemfile
48
60
 
49
- def compatible_with_ruby?(current_ruby)
61
+ ##
62
+ # Returns whether this row can be run with the given Ruby version.
63
+ #
64
+ def compatible_with_ruby?(current_ruby = Env.ruby)
50
65
  ruby == current_ruby
51
66
  end
52
67
 
68
+ ##
69
+ # Raises an error if this row is invalid.
70
+ #
71
+ # @!visibility private
72
+ #
53
73
  def validate!
54
- File.exists?(gemfile) or raise Invalid, "Gemfile not found: #{gemfile}"
74
+ File.exists?(gemfile) or raise MissingGemfile, "Gemfile not found: #{gemfile}"
55
75
  contents = File.read(gemfile)
56
- contents.include?('gemika') or raise Invalid, "Gemfile is missing gemika dependency: #{gemfile}"
76
+ contents.include?('gemika') or raise UnusableGemfile, "Gemfile is missing gemika dependency: #{gemfile}"
57
77
  end
58
78
 
59
79
  end
@@ -77,6 +97,13 @@ module Gemika
77
97
  @current_ruby = options.fetch(:current_ruby, RUBY_VERSION)
78
98
  end
79
99
 
100
+ ##
101
+ # Runs the given `block` for each matrix row that is compatible with the current Ruby.
102
+ #
103
+ # The row's gemfile will be set as an environment variable, so Bundler will use that gemfile if you shell out in `block`.
104
+ #
105
+ # At the end it will print a summary of which rows have passed, failed or were skipped (due to incompatible Ruby version).
106
+ #
80
107
  def each(&block)
81
108
  @all_passed = true
82
109
  rows.each do |row|
@@ -98,6 +125,12 @@ module Gemika
98
125
  print_summary
99
126
  end
100
127
 
128
+ ##
129
+ # Builds a {Matrix} from the given `.travis.yml` file.
130
+ #
131
+ # @param [Hash] options
132
+ # @option options [String] Path to the `.travis.yml` file.
133
+ #
101
134
  def self.from_travis_yml(options = {})
102
135
  rows = TravisConfig.load_rows(options)
103
136
  new(options.merge(:rows => rows))
@@ -143,7 +176,7 @@ module Gemika
143
176
  message = "No gemfiles were compatible with Ruby #{RUBY_VERSION}"
144
177
  puts tint(message, COLOR_FAILURE)
145
178
  puts
146
- raise Incompatible, message
179
+ raise UnsupportedRuby, message
147
180
  elsif @all_passed
148
181
  puts tint("All gemfiles succeeded for Ruby #{RUBY_VERSION}", COLOR_SUCCESS)
149
182
  puts
@@ -151,7 +184,7 @@ module Gemika
151
184
  message = 'Some gemfiles failed'
152
185
  puts tint(message, COLOR_FAILURE)
153
186
  puts
154
- raise Failed, message
187
+ raise MatrixFailed, message
155
188
  end
156
189
  end
157
190
 
data/lib/gemika/rspec.rb CHANGED
@@ -1,52 +1,95 @@
1
+ require 'gemika/errors'
2
+ require 'gemika/env'
3
+
1
4
  module Gemika
2
- class RSpec
3
- class << self
4
-
5
- def configure_transactional_examples
6
- if Env.rspec_1?
7
-
8
- Spec::Runner.configure do |config|
9
-
10
- config.before :each do
11
- # from ActiveRecord::Fixtures#setup_fixtures
12
- connection = ActiveRecord::Base.connection
13
- connection.increment_open_transactions
14
- connection.transaction_joinable = false
15
- connection.begin_db_transaction
16
- end
17
-
18
- config.after :each do
19
- # from ActiveRecord::Fixtures#teardown_fixtures
20
- connection = ActiveRecord::Base.connection
21
- if connection.open_transactions != 0
22
- connection.rollback_db_transaction
23
- connection.decrement_open_transactions
24
- end
25
- end
26
-
27
- end
28
-
29
- else
30
-
31
- ::RSpec.configure do |config|
32
- config.around do |example|
33
- if example.metadata.fetch(:transaction, example.metadata.fetch(:rollback, true))
34
- ActiveRecord::Base.transaction do
35
- begin
36
- example.run
37
- ensure
38
- raise ActiveRecord::Rollback
39
- end
40
- end
41
- else
42
- example.run
43
- end
44
- end
45
- end
5
+ module RSpec
6
+
7
+ ##
8
+ # Runs the RSpec binary.
9
+ #
10
+ def run_specs(options = nil)
11
+ options ||= {}
12
+ files = options.fetch(:files, 'spec')
13
+ rspec_options = options.fetch(:options, '--color')
14
+ # We need to override the gemfile explicitely, since we have a default Gemfile in the project root
15
+ gemfile = options.fetch(:gemfile, Gemika::Env.gemfile)
16
+ fatal = options.fetch(:fatal, true)
17
+ runner = binary(:gemfile => gemfile)
18
+ command = "bundle exec #{runner} #{rspec_options} #{files}"
19
+ result = shell_out(command)
20
+ if result
21
+ true
22
+ elsif fatal
23
+ raise RSpecFailed, "RSpec failed: #{command}"
24
+ else
25
+ false
26
+ end
27
+ end
28
+
29
+ ##
30
+ # Returns the binary name for the current RSpec version.
31
+ #
32
+ def binary(options = {})
33
+ if Env.gem?('rspec', '< 2', options)
34
+ 'spec'
35
+ else
36
+ 'rspec'
37
+ end
38
+ end
39
+
40
+ ##
41
+ # Configures RSpec.
42
+ #
43
+ # Works with both RSpec 1 and RSpec 2.
44
+ #
45
+ def configure(&block)
46
+ configurator.configure(&block)
47
+ end
48
+
49
+ ##
50
+ # Configures RSpec to clean out the database before each example.
51
+ #
52
+ # Requires the `database_cleaner` gem to be added to your development dependencies.
53
+ #
54
+ def configure_clean_database_before_example
55
+ require 'database_cleaner' # optional dependency
56
+ configure do |config|
57
+ config.before(:each) do
58
+ # Truncation works across most database adapters; I had issues with :deletion and pg
59
+ DatabaseCleaner.clean_with(:truncation)
60
+ end
61
+ end
62
+ end
46
63
 
64
+ ##
65
+ # Configures RSpec so it allows the `should` syntax that works across all RSpec versions.
66
+ #
67
+ def configure_should_syntax
68
+ if Env.gem?('rspec', '>= 2.11')
69
+ configure do |config|
70
+ config.expect_with(:rspec) { |c| c.syntax = [:should, :expect] }
71
+ config.mock_with(:rspec) { |c| c.syntax = [:should, :expect] }
47
72
  end
73
+ else
74
+ # We have an old RSpec that only understands should syntax
48
75
  end
76
+ end
77
+
78
+ private
49
79
 
80
+ def shell_out(command)
81
+ system(command)
50
82
  end
83
+
84
+ def configurator
85
+ if Env.gem?('rspec', '<2')
86
+ Spec::Runner
87
+ else
88
+ ::RSpec
89
+ end
90
+ end
91
+
92
+ extend self
93
+
51
94
  end
52
95
  end
@@ -1,46 +1,40 @@
1
+ require 'gemika/env'
1
2
  require 'gemika/matrix'
3
+ require 'gemika/rspec'
2
4
 
3
- module Gemika
4
- module Tasks
5
- RSPEC_ARGS = '--color spec'
6
- end
7
- end
8
-
5
+ ##
6
+ # Rake tasks to run commands for each compatible row in the test matrix.
7
+ #
9
8
  namespace :matrix do
10
9
 
11
10
  desc "Run specs for all Ruby #{RUBY_VERSION} gemfiles"
12
- task :spec do
11
+ task :spec, :files do |t, options|
13
12
  Gemika::Matrix.from_travis_yml.each do |row|
14
- rspec_binary = Gemika::Env.rspec_binary_for_gemfile(row.gemfile)
15
- args = Gemika::Tasks::RSPEC_ARGS
16
- system("bundle exec #{rspec_binary} #{args}")
13
+ options = options.to_hash.merge(:gemfile => row.gemfile, :fatal => false)
14
+ Gemika::RSpec.run_specs(options)
17
15
  end
18
16
  end
19
17
 
20
18
  desc "Install all Ruby #{RUBY_VERSION} gemfiles"
21
19
  task :install do
22
20
  Gemika::Matrix.from_travis_yml.each do |row|
21
+ puts "Calling `bundle install` with #{ENV['BUNDLE_GEMFILE']}"
23
22
  system('bundle install')
24
23
  end
25
24
  end
26
25
 
27
- desc "Update all Ruby #{RUBY_VERSION} gemfiles"
28
- task :update, :gems do |t, args|
26
+ desc "List dependencies for all Ruby #{RUBY_VERSION} gemfiles"
27
+ task :list do
29
28
  Gemika::Matrix.from_travis_yml.each do |row|
30
- system("bundle update #{args[:gems]}")
29
+ system('bundle list')
31
30
  end
32
31
  end
33
32
 
34
- end
35
-
36
- namespace :gemika do
37
-
38
- # Private task to pick the correct RSpec binary
39
- # (spec in RSpec 1, rspec in RSpec 2+)
40
- task :spec do
41
- rspec_binary = Gemika::Env.rspec_binary
42
- args = Gemika::Tasks::RSPEC_ARGS
43
- system("bundle exec #{rspec_binary} #{args}")
33
+ desc "Update all Ruby #{RUBY_VERSION} gemfiles"
34
+ task :update, :gems do |t, options|
35
+ Gemika::Matrix.from_travis_yml.each do |row|
36
+ system("bundle update #{options[:gems]}")
37
+ end
44
38
  end
45
39
 
46
40
  end
@@ -0,0 +1,9 @@
1
+ require 'gemika/rspec'
2
+
3
+ # Private task to pick the correct RSpec binary for the currently activated
4
+ # RSpec version (`spec` in RSpec 1, `rspec` in RSpec 2+)
5
+ desc 'Run specs with the current RSpec version'
6
+ task :current_rspec, :files do |t, options|
7
+ options = options.to_hash
8
+ Gemika::RSpec.run_specs(options)
9
+ end
data/lib/gemika/tasks.rb CHANGED
@@ -1,2 +1,2 @@
1
1
  require 'gemika/tasks/matrix'
2
- require 'gemika/tasks/database'
2
+ require 'gemika/tasks/rspec'
@@ -1,3 +1,3 @@
1
1
  module Gemika
2
- VERSION = '0.2.0'
2
+ VERSION = '0.3.0'
3
3
  end