gemika 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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