├── .rspec ├── .ruby-version ├── Gemfile ├── Gemfile.lock ├── .yardopts ├── .gitignore ├── lib ├── gemika │ ├── version.rb │ ├── tasks.rb │ ├── errors.rb │ ├── tasks │ │ ├── rspec.rb │ │ └── matrix.rb │ ├── matrix │ │ └── github_actions_config.rb │ ├── rspec.rb │ ├── database.rb │ ├── env.rb │ └── matrix.rb └── gemika.rb ├── spec ├── support │ ├── models.rb │ ├── database.rb │ ├── database.sample.yml │ └── database.github.yml ├── fixtures │ ├── github_actions_yml │ │ ├── Gemfile_without_gemika │ │ ├── invalid.yml │ │ ├── missing_gemfile.yml │ │ ├── gemfile_without_gemika.yml │ │ ├── two_by_two.yml │ │ ├── excludes.yml │ │ ├── multiple_jobs.yml │ │ └── includes.yml │ └── gemfiles │ │ ├── Gemfile_with_activesupport_5 │ │ └── Gemfile_with_activesupport_5.lock ├── gemika │ ├── database_spec.rb │ ├── rspec_spec.rb │ ├── matrix │ │ └── row_spec.rb │ ├── env_spec.rb │ └── matrix_spec.rb └── spec_helper.rb ├── doc └── minidusen_test.png ├── Rakefile ├── Gemfile.5.2.mysql2 ├── Gemfile.5.2.sqlite3 ├── Gemfile.5.2.pg ├── Gemfile.8.0.pg ├── Gemfile.7.0.pg ├── Gemfile.6.1.pg ├── gemika.gemspec ├── LICENSE ├── Gemfile.5.2.mysql2.lock ├── Gemfile.5.2.pg.lock ├── Gemfile.5.2.sqlite3.lock ├── Gemfile.7.0.pg.lock ├── Gemfile.6.1.pg.lock ├── Gemfile.8.0.pg.lock ├── bin └── matrix ├── CHANGELOG.md ├── .github └── workflows │ └── test.yml └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.4.1 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | Gemfile.8.0.pg -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | Gemfile.8.0.pg.lock -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --markup=markdown --main=README.md 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | pkg 3 | /spec/support/database.yml 4 | -------------------------------------------------------------------------------- /lib/gemika/version.rb: -------------------------------------------------------------------------------- 1 | module Gemika 2 | VERSION = '1.0.0' 3 | end 4 | -------------------------------------------------------------------------------- /spec/support/models.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | 3 | end 4 | -------------------------------------------------------------------------------- /spec/fixtures/github_actions_yml/Gemfile_without_gemika: -------------------------------------------------------------------------------- 1 | gem 'some-gem' 2 | -------------------------------------------------------------------------------- /lib/gemika/tasks.rb: -------------------------------------------------------------------------------- 1 | require 'gemika/tasks/matrix' 2 | require 'gemika/tasks/rspec' 3 | -------------------------------------------------------------------------------- /doc/minidusen_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makandra/gemika/HEAD/doc/minidusen_test.png -------------------------------------------------------------------------------- /spec/fixtures/gemfiles/Gemfile_with_activesupport_5: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'activesupport', '~> 5.0.0' 4 | -------------------------------------------------------------------------------- /spec/support/database.rb: -------------------------------------------------------------------------------- 1 | Gemika::Database.new.rewrite_schema! do 2 | 3 | create_table :users do |t| 4 | t.string :name 5 | t.string :email 6 | end 7 | 8 | end 9 | -------------------------------------------------------------------------------- /spec/fixtures/github_actions_yml/invalid.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | first_job: 3 | strategy: 4 | matrix: 5 | ruby_version: 6 | - "2.1.8" 7 | gemfile: 8 | - gemfiles/Gemfile1 9 | -------------------------------------------------------------------------------- /spec/fixtures/github_actions_yml/missing_gemfile.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | first_job: 3 | strategy: 4 | matrix: 5 | ruby: 6 | - 2.1.8 7 | gemfile: 8 | - gemfiles/nonexisting_gemfile 9 | -------------------------------------------------------------------------------- /spec/support/database.sample.yml: -------------------------------------------------------------------------------- 1 | mysql: 2 | database: gemika_test 3 | host: localhost 4 | username: root 5 | password: secret 6 | 7 | postgresql: 8 | database: gemika_test 9 | user: 10 | password: 11 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'bundler/gem_tasks' 3 | begin 4 | require 'gemika/tasks' 5 | rescue LoadError 6 | puts 'Run `gem install gemika` for additional tasks' 7 | end 8 | 9 | task :default => 'matrix:spec' 10 | -------------------------------------------------------------------------------- /spec/fixtures/github_actions_yml/gemfile_without_gemika.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | first_job: 3 | strategy: 4 | matrix: 5 | ruby: 6 | - 2.1.8 7 | gemfile: 8 | - spec/fixtures/github_actions_yml/Gemfile_without_gemika 9 | -------------------------------------------------------------------------------- /spec/fixtures/github_actions_yml/two_by_two.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | first_job: 3 | strategy: 4 | matrix: 5 | ruby: 6 | - "2.1.8" 7 | - "2.3.1" 8 | gemfile: 9 | - gemfiles/Gemfile1 10 | - gemfiles/Gemfile2 11 | -------------------------------------------------------------------------------- /lib/gemika.rb: -------------------------------------------------------------------------------- 1 | require 'gemika/version' 2 | require 'gemika/errors' 3 | require 'gemika/env' 4 | require 'gemika/database' if Gemika::Env.gem?('activerecord') 5 | require 'gemika/matrix' 6 | require 'gemika/rspec' if Gemika::Env.gem?('rspec') 7 | 8 | # don't load tasks by default 9 | -------------------------------------------------------------------------------- /spec/support/database.github.yml: -------------------------------------------------------------------------------- 1 | mysql: 2 | database: test 3 | username: root 4 | password: password 5 | host: 127.0.0.1 6 | port: 3306 7 | 8 | postgresql: 9 | database: test 10 | host: localhost 11 | username: postgres 12 | password: postgres 13 | port: 5432 14 | -------------------------------------------------------------------------------- /Gemfile.5.2.mysql2: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Ruby 4 | ruby '>= 2.2' 5 | 6 | # Runtime dependencies 7 | gem 'activerecord', '~>5.2.0' 8 | gem 'rspec', '~>3.5' 9 | gem 'mysql2' 10 | 11 | # Development dependencies 12 | gem 'rake' 13 | gem 'database_cleaner' 14 | gem 'pry' 15 | 16 | # Gem under test 17 | gem 'gemika', :path => '.' 18 | -------------------------------------------------------------------------------- /Gemfile.5.2.sqlite3: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Ruby 4 | ruby '>= 2.2' 5 | 6 | # Runtime dependencies 7 | gem 'activerecord', '~>5.2.0' 8 | gem 'rspec', '~>3.5' 9 | gem 'sqlite3' 10 | 11 | # Development dependencies 12 | gem 'rake' 13 | gem 'database_cleaner' 14 | gem 'pry' 15 | 16 | # Gem under test 17 | gem 'gemika', :path => '.' 18 | -------------------------------------------------------------------------------- /Gemfile.5.2.pg: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Ruby 4 | ruby '>= 2.2' 5 | 6 | # Runtime dependencies 7 | gem 'activerecord', '~>5.2.0' 8 | gem 'rspec', '~>3.5' 9 | gem 'pg', '~>0.18.4' 10 | 11 | # Development dependencies 12 | gem 'rake' 13 | gem 'database_cleaner' 14 | gem 'pry' 15 | 16 | # Gem under test 17 | gem 'gemika', :path => '.' 18 | -------------------------------------------------------------------------------- /spec/fixtures/github_actions_yml/excludes.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | first_job: 3 | strategy: 4 | matrix: 5 | ruby: 6 | - "2.1.8" 7 | - "2.3.1" 8 | gemfile: 9 | - gemfiles/Gemfile1 10 | - gemfiles/Gemfile2 11 | exclude: 12 | - ruby: 2.1.8 13 | gemfile: gemfiles/Gemfile1 14 | -------------------------------------------------------------------------------- /spec/fixtures/github_actions_yml/multiple_jobs.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | first_job: 3 | strategy: 4 | matrix: 5 | ruby: 6 | - "2.1.8" 7 | gemfile: 8 | - gemfiles/Gemfile1 9 | 10 | second_job: 11 | strategy: 12 | matrix: 13 | ruby: 14 | - "2.3.1" 15 | gemfile: 16 | - gemfiles/Gemfile2 17 | -------------------------------------------------------------------------------- /Gemfile.8.0.pg: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Ruby 4 | ruby '>= 2.2' 5 | 6 | # Runtime dependencies 7 | gem 'activerecord', '~> 8.0.1' 8 | gem 'rspec', '~>3.5' 9 | gem 'pg', '~> 1.5.6' 10 | 11 | # Development dependencies 12 | gem 'rake' 13 | gem 'database_cleaner', '~> 2.0.2' 14 | gem 'pry', '~> 0.14.2' 15 | 16 | # Gem under test 17 | gem 'gemika', :path => '.' 18 | -------------------------------------------------------------------------------- /lib/gemika/errors.rb: -------------------------------------------------------------------------------- 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 | class MissingMatrixDefinition < Error; end 10 | class InvalidMatrixDefinition < Error; end 11 | end 12 | -------------------------------------------------------------------------------- /lib/gemika/tasks/rspec.rb: -------------------------------------------------------------------------------- 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.merge( 8 | :bundle_exec => false 9 | ) 10 | Gemika::RSpec.run_specs(options) 11 | end 12 | -------------------------------------------------------------------------------- /Gemfile.7.0.pg: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Ruby 4 | ruby '>= 2.2' 5 | 6 | # Former standard gems, see https://stdgems.org/ 7 | gem 'mutex_m' # Bundled gem since Ruby 3.4 8 | gem 'bigdecimal' # Bundled gem since Ruby 3.4 9 | 10 | # Runtime dependencies 11 | gem 'activerecord', '~> 7.0.1' 12 | gem 'rspec', '~>3.5' 13 | gem 'pg', '~> 1.3.5' 14 | 15 | # Development dependencies 16 | gem 'rake' 17 | gem 'database_cleaner' 18 | gem 'pry', '~> 0.14.2' 19 | 20 | # Gem under test 21 | gem 'gemika', :path => '.' 22 | -------------------------------------------------------------------------------- /spec/fixtures/github_actions_yml/includes.yml: -------------------------------------------------------------------------------- 1 | matrix: 2 | jobs: 3 | first_job: 4 | strategy: 5 | matrix: 6 | ruby: 7 | - "2.1.8" 8 | - "2.3.1" 9 | gemfile: 10 | - gemfiles/Gemfile1 11 | - gemfiles/Gemfile2 12 | include: 13 | - ruby: 2.6.3 14 | gemfile: gemfiles/Gemfile3 15 | second_job: 16 | strategy: 17 | matrix: 18 | include: 19 | - ruby: 2.7.1 20 | gemfile: gemfiles/Gemfile3 21 | -------------------------------------------------------------------------------- /spec/gemika/database_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' # can't move this to .rspec in RSpec 1 2 | 3 | describe Gemika::Database do 4 | 5 | it 'connects ActiveRecord to the database in database.yml, then migrates the schema' do 6 | user = User.create!(:email => 'foo@bar.com') 7 | database_user = User.first 8 | user.email.should == database_user.email 9 | end 10 | 11 | it 'wraps each example in a transaction that is rolled back when the transaction ends' do 12 | User.count.should == 0 13 | end 14 | 15 | end 16 | -------------------------------------------------------------------------------- /spec/fixtures/gemfiles/Gemfile_with_activesupport_5.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | activesupport (5.0.0.1) 5 | concurrent-ruby (~> 1.0, >= 1.0.2) 6 | i18n (~> 0.7) 7 | minitest (~> 5.1) 8 | tzinfo (~> 1.1) 9 | concurrent-ruby (1.0.2) 10 | i18n (0.7.0) 11 | minitest (5.9.1) 12 | thread_safe (0.3.5) 13 | tzinfo (1.2.2) 14 | thread_safe (~> 0.1) 15 | 16 | PLATFORMS 17 | ruby 18 | 19 | DEPENDENCIES 20 | activesupport (~> 5.0.0) 21 | 22 | BUNDLED WITH 23 | 1.12.5 24 | -------------------------------------------------------------------------------- /Gemfile.6.1.pg: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Ruby 4 | ruby '>= 2.2' 5 | 6 | # Former standard gems, see https://stdgems.org/ 7 | gem 'mutex_m' # Bundled gem since Ruby 3.4 8 | gem 'bigdecimal' # Bundled gem since Ruby 3.4 9 | gem 'base64' # Bundled gem since Ruby 3.4 10 | 11 | # Runtime dependencies 12 | gem 'activerecord', '~>6.1.0' 13 | gem 'rspec', '~>3.5' 14 | gem 'pg', '~> 1.3.5' 15 | 16 | # Development dependencies 17 | gem 'rake' 18 | gem 'database_cleaner' 19 | gem 'pry', '~> 0.14.2' 20 | 21 | # Gem under test 22 | gem 'gemika', :path => '.' 23 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $: << File.join(File.dirname(__FILE__), "/../../lib" ) 2 | 3 | require 'active_record' 4 | require 'gemika' 5 | require 'pry' 6 | 7 | if Gemika::Env.gem?('activerecord', '>= 7.0') 8 | ActiveRecord.default_timezone = :local 9 | else 10 | ActiveRecord::Base.default_timezone = :local 11 | end 12 | 13 | Dir["#{File.dirname(__FILE__)}/support/*.rb"].sort.each {|f| require f} 14 | Dir["#{File.dirname(__FILE__)}/shared_examples/*.rb"].sort.each {|f| require f} 15 | 16 | Gemika::RSpec.configure_clean_database_before_example 17 | Gemika::RSpec.configure_should_syntax 18 | -------------------------------------------------------------------------------- /gemika.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path("../lib", __FILE__) 2 | require "gemika/version" 3 | 4 | Gem::Specification.new do |s| 5 | s.name = 'gemika' 6 | s.version = Gemika::VERSION 7 | s.authors = ["Henning Koch"] 8 | s.email = 'henning.koch@makandra.de' 9 | s.homepage = 'https://github.com/makandra/gemika' 10 | s.summary = 'Helpers for testing Ruby gems' 11 | s.description = s.summary 12 | s.license = 'MIT' 13 | s.metadata = { 14 | 'source_code_uri' => s.homepage, 15 | 'bug_tracker_uri' => 'https://github.com/makandra/gemika/issues', 16 | 'changelog_uri' => 'https://github.com/makandra/gemika/blob/master/CHANGELOG.md', 17 | 'rubygems_mfa_required' => 'true', 18 | } 19 | 20 | s.files = `git ls-files`.split("\n").reject { |path| !File.exist?(path) || File.lstat(path).symlink? } 21 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n").reject { |path| !File.exist?(path) || File.lstat(path).symlink? } 22 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 23 | s.require_paths = ["lib"] 24 | end 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Henning Koch 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /lib/gemika/tasks/matrix.rb: -------------------------------------------------------------------------------- 1 | require 'gemika/env' 2 | require 'gemika/matrix' 3 | require 'gemika/rspec' 4 | 5 | ## 6 | # Rake tasks to run commands for each compatible row in the test matrix. 7 | # 8 | namespace :matrix do 9 | 10 | desc "Run specs for all Ruby #{RUBY_VERSION} gemfiles" 11 | task :spec, :files do |t, options| 12 | Gemika::Matrix.from_ci_config.each do |row| 13 | options = options.to_hash.merge( 14 | :gemfile => row.gemfile, 15 | :fatal => false, 16 | :bundle_exec => true 17 | ) 18 | Gemika::RSpec.run_specs(options) 19 | end 20 | end 21 | 22 | desc "Install all Ruby #{RUBY_VERSION} gemfiles" 23 | task :install do 24 | Gemika::Matrix.from_ci_config.each do |row| 25 | system('bundle install') 26 | end 27 | end 28 | 29 | desc "List dependencies for all Ruby #{RUBY_VERSION} gemfiles" 30 | task :list do 31 | Gemika::Matrix.from_ci_config.each do |row| 32 | system('bundle list') 33 | end 34 | end 35 | 36 | desc "Update all Ruby #{RUBY_VERSION} gemfiles" 37 | task :update, :gems do |t, options| 38 | Gemika::Matrix.from_ci_config.each do |row| 39 | system("bundle update #{options[:gems]}") 40 | end 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /Gemfile.5.2.mysql2.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | gemika (1.0.0) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | activemodel (5.2.3) 10 | activesupport (= 5.2.3) 11 | activerecord (5.2.3) 12 | activemodel (= 5.2.3) 13 | activesupport (= 5.2.3) 14 | arel (>= 9.0) 15 | activesupport (5.2.3) 16 | concurrent-ruby (~> 1.0, >= 1.0.2) 17 | i18n (>= 0.7, < 2) 18 | minitest (~> 5.1) 19 | tzinfo (~> 1.1) 20 | arel (9.0.0) 21 | coderay (1.1.2) 22 | concurrent-ruby (1.1.5) 23 | database_cleaner (1.7.0) 24 | diff-lcs (1.2.5) 25 | i18n (1.5.1) 26 | concurrent-ruby (~> 1.0) 27 | method_source (0.9.2) 28 | minitest (5.11.3) 29 | mysql2 (0.5.3) 30 | pry (0.12.2) 31 | coderay (~> 1.1.0) 32 | method_source (~> 0.9.0) 33 | rake (11.3.0) 34 | rspec (3.5.0) 35 | rspec-core (~> 3.5.0) 36 | rspec-expectations (~> 3.5.0) 37 | rspec-mocks (~> 3.5.0) 38 | rspec-core (3.5.3) 39 | rspec-support (~> 3.5.0) 40 | rspec-expectations (3.5.0) 41 | diff-lcs (>= 1.2.0, < 2.0) 42 | rspec-support (~> 3.5.0) 43 | rspec-mocks (3.5.0) 44 | diff-lcs (>= 1.2.0, < 2.0) 45 | rspec-support (~> 3.5.0) 46 | rspec-support (3.5.0) 47 | thread_safe (0.3.6) 48 | tzinfo (1.2.5) 49 | thread_safe (~> 0.1) 50 | 51 | PLATFORMS 52 | ruby 53 | 54 | DEPENDENCIES 55 | activerecord (~> 5.2.0) 56 | database_cleaner 57 | gemika! 58 | mysql2 59 | pry 60 | rake 61 | rspec (~> 3.5) 62 | 63 | RUBY VERSION 64 | ruby 2.2.4p230 65 | 66 | BUNDLED WITH 67 | 2.3.1 68 | -------------------------------------------------------------------------------- /Gemfile.5.2.pg.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | gemika (1.0.0) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | activemodel (5.2.3) 10 | activesupport (= 5.2.3) 11 | activerecord (5.2.3) 12 | activemodel (= 5.2.3) 13 | activesupport (= 5.2.3) 14 | arel (>= 9.0) 15 | activesupport (5.2.3) 16 | concurrent-ruby (~> 1.0, >= 1.0.2) 17 | i18n (>= 0.7, < 2) 18 | minitest (~> 5.1) 19 | tzinfo (~> 1.1) 20 | arel (9.0.0) 21 | coderay (1.1.2) 22 | concurrent-ruby (1.1.5) 23 | database_cleaner (1.7.0) 24 | diff-lcs (1.2.5) 25 | i18n (1.5.1) 26 | concurrent-ruby (~> 1.0) 27 | method_source (0.9.2) 28 | minitest (5.11.3) 29 | pg (0.18.4) 30 | pry (0.12.2) 31 | coderay (~> 1.1.0) 32 | method_source (~> 0.9.0) 33 | rake (11.3.0) 34 | rspec (3.5.0) 35 | rspec-core (~> 3.5.0) 36 | rspec-expectations (~> 3.5.0) 37 | rspec-mocks (~> 3.5.0) 38 | rspec-core (3.5.3) 39 | rspec-support (~> 3.5.0) 40 | rspec-expectations (3.5.0) 41 | diff-lcs (>= 1.2.0, < 2.0) 42 | rspec-support (~> 3.5.0) 43 | rspec-mocks (3.5.0) 44 | diff-lcs (>= 1.2.0, < 2.0) 45 | rspec-support (~> 3.5.0) 46 | rspec-support (3.5.0) 47 | thread_safe (0.3.6) 48 | tzinfo (1.2.5) 49 | thread_safe (~> 0.1) 50 | 51 | PLATFORMS 52 | ruby 53 | 54 | DEPENDENCIES 55 | activerecord (~> 5.2.0) 56 | database_cleaner 57 | gemika! 58 | pg (~> 0.18.4) 59 | pry 60 | rake 61 | rspec (~> 3.5) 62 | 63 | RUBY VERSION 64 | ruby 2.2.4p230 65 | 66 | BUNDLED WITH 67 | 2.3.1 68 | -------------------------------------------------------------------------------- /Gemfile.5.2.sqlite3.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | gemika (1.0.0) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | activemodel (5.2.3) 10 | activesupport (= 5.2.3) 11 | activerecord (5.2.3) 12 | activemodel (= 5.2.3) 13 | activesupport (= 5.2.3) 14 | arel (>= 9.0) 15 | activesupport (5.2.3) 16 | concurrent-ruby (~> 1.0, >= 1.0.2) 17 | i18n (>= 0.7, < 2) 18 | minitest (~> 5.1) 19 | tzinfo (~> 1.1) 20 | arel (9.0.0) 21 | coderay (1.1.2) 22 | concurrent-ruby (1.1.5) 23 | database_cleaner (1.7.0) 24 | diff-lcs (1.3) 25 | i18n (1.5.1) 26 | concurrent-ruby (~> 1.0) 27 | method_source (0.9.2) 28 | minitest (5.11.3) 29 | pry (0.12.2) 30 | coderay (~> 1.1.0) 31 | method_source (~> 0.9.0) 32 | rake (12.3.1) 33 | rspec (3.7.0) 34 | rspec-core (~> 3.7.0) 35 | rspec-expectations (~> 3.7.0) 36 | rspec-mocks (~> 3.7.0) 37 | rspec-core (3.7.1) 38 | rspec-support (~> 3.7.0) 39 | rspec-expectations (3.7.0) 40 | diff-lcs (>= 1.2.0, < 2.0) 41 | rspec-support (~> 3.7.0) 42 | rspec-mocks (3.7.0) 43 | diff-lcs (>= 1.2.0, < 2.0) 44 | rspec-support (~> 3.7.0) 45 | rspec-support (3.7.1) 46 | sqlite3 (1.3.13) 47 | thread_safe (0.3.6) 48 | tzinfo (1.2.5) 49 | thread_safe (~> 0.1) 50 | 51 | PLATFORMS 52 | ruby 53 | 54 | DEPENDENCIES 55 | activerecord (~> 5.2.0) 56 | database_cleaner 57 | gemika! 58 | pry 59 | rake 60 | rspec (~> 3.5) 61 | sqlite3 62 | 63 | RUBY VERSION 64 | ruby 2.2.4p230 65 | 66 | BUNDLED WITH 67 | 2.3.1 68 | -------------------------------------------------------------------------------- /Gemfile.7.0.pg.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | gemika (1.0.0) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | activemodel (7.0.4) 10 | activesupport (= 7.0.4) 11 | activerecord (7.0.4) 12 | activemodel (= 7.0.4) 13 | activesupport (= 7.0.4) 14 | activesupport (7.0.4) 15 | concurrent-ruby (~> 1.0, >= 1.0.2) 16 | i18n (>= 1.6, < 2) 17 | minitest (>= 5.1) 18 | tzinfo (~> 2.0) 19 | bigdecimal (3.1.8) 20 | coderay (1.1.3) 21 | concurrent-ruby (1.1.10) 22 | database_cleaner (1.7.0) 23 | diff-lcs (1.5.0) 24 | i18n (1.12.0) 25 | concurrent-ruby (~> 1.0) 26 | method_source (1.0.0) 27 | minitest (5.17.0) 28 | mutex_m (0.3.0) 29 | pg (1.3.5) 30 | pry (0.14.2) 31 | coderay (~> 1.1) 32 | method_source (~> 1.0) 33 | rake (12.3.3) 34 | rspec (3.8.0) 35 | rspec-core (~> 3.8.0) 36 | rspec-expectations (~> 3.8.0) 37 | rspec-mocks (~> 3.8.0) 38 | rspec-core (3.8.2) 39 | rspec-support (~> 3.8.0) 40 | rspec-expectations (3.8.6) 41 | diff-lcs (>= 1.2.0, < 2.0) 42 | rspec-support (~> 3.8.0) 43 | rspec-mocks (3.8.2) 44 | diff-lcs (>= 1.2.0, < 2.0) 45 | rspec-support (~> 3.8.0) 46 | rspec-support (3.8.3) 47 | tzinfo (2.0.5) 48 | concurrent-ruby (~> 1.0) 49 | 50 | PLATFORMS 51 | ruby 52 | 53 | DEPENDENCIES 54 | activerecord (~> 7.0.1) 55 | bigdecimal 56 | database_cleaner 57 | gemika! 58 | mutex_m 59 | pg (~> 1.3.5) 60 | pry (~> 0.14.2) 61 | rake 62 | rspec (~> 3.5) 63 | 64 | RUBY VERSION 65 | ruby 2.2.4p230 66 | 67 | BUNDLED WITH 68 | 2.3.1 69 | -------------------------------------------------------------------------------- /Gemfile.6.1.pg.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | gemika (1.0.0) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | activemodel (6.1.3.1) 10 | activesupport (= 6.1.3.1) 11 | activerecord (6.1.3.1) 12 | activemodel (= 6.1.3.1) 13 | activesupport (= 6.1.3.1) 14 | activesupport (6.1.3.1) 15 | concurrent-ruby (~> 1.0, >= 1.0.2) 16 | i18n (>= 1.6, < 2) 17 | minitest (>= 5.1) 18 | tzinfo (~> 2.0) 19 | zeitwerk (~> 2.3) 20 | base64 (0.2.0) 21 | bigdecimal (3.1.9) 22 | coderay (1.1.3) 23 | concurrent-ruby (1.1.8) 24 | database_cleaner (1.7.0) 25 | diff-lcs (1.3) 26 | i18n (1.8.10) 27 | concurrent-ruby (~> 1.0) 28 | method_source (1.0.0) 29 | minitest (5.14.4) 30 | mutex_m (0.3.0) 31 | pg (1.3.5) 32 | pry (0.14.2) 33 | coderay (~> 1.1) 34 | method_source (~> 1.0) 35 | rake (12.3.3) 36 | rspec (3.8.0) 37 | rspec-core (~> 3.8.0) 38 | rspec-expectations (~> 3.8.0) 39 | rspec-mocks (~> 3.8.0) 40 | rspec-core (3.8.2) 41 | rspec-support (~> 3.8.0) 42 | rspec-expectations (3.8.4) 43 | diff-lcs (>= 1.2.0, < 2.0) 44 | rspec-support (~> 3.8.0) 45 | rspec-mocks (3.8.1) 46 | diff-lcs (>= 1.2.0, < 2.0) 47 | rspec-support (~> 3.8.0) 48 | rspec-support (3.8.2) 49 | tzinfo (2.0.4) 50 | concurrent-ruby (~> 1.0) 51 | zeitwerk (2.4.2) 52 | 53 | PLATFORMS 54 | ruby 55 | 56 | DEPENDENCIES 57 | activerecord (~> 6.1.0) 58 | base64 59 | bigdecimal 60 | database_cleaner 61 | gemika! 62 | mutex_m 63 | pg (~> 1.3.5) 64 | pry (~> 0.14.2) 65 | rake 66 | rspec (~> 3.5) 67 | 68 | RUBY VERSION 69 | ruby 2.2.4p230 70 | 71 | BUNDLED WITH 72 | 2.3.1 73 | -------------------------------------------------------------------------------- /spec/gemika/rspec_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Gemika::RSpec do 4 | 5 | # before(:each) { puts "---", "RSpec example", "---" } 6 | 7 | subject { Gemika::RSpec } 8 | 9 | describe '.binary' do 10 | 11 | it 'returns "spec" for RSpec 1' do 12 | Gemika::Env.should_receive(:gem?).with('rspec', '< 2', {}).and_return(true) 13 | subject.binary.should == 'spec' 14 | end 15 | 16 | it 'returns "rspec" for RSpec 2+' do 17 | Gemika::Env.should_receive(:gem?).with('rspec', '< 2', {}).and_return(false) 18 | subject.binary.should == 'rspec' 19 | end 20 | 21 | end 22 | 23 | describe '.run_specs' do 24 | 25 | it 'shells out to the binary' do 26 | expected_command = %{#{subject.binary} --color spec} 27 | subject.should_receive(:shell_out).with(expected_command).and_return(true) 28 | subject.run_specs(:bundle_exec => false) 29 | end 30 | 31 | it 'allows to pass a :files option' do 32 | expected_command = %{#{subject.binary} --color spec/foo_spec.rb:23} 33 | subject.should_receive(:shell_out).with(expected_command).and_return(true) 34 | subject.run_specs( 35 | :bundle_exec => false, 36 | :files => 'spec/foo_spec.rb:23' 37 | ) 38 | end 39 | 40 | it 'calls the binary with `bundle exec` if :bundle_exec option is true' do 41 | expected_command = %{bundle exec #{subject.binary} --color spec} 42 | subject.should_receive(:shell_out).with(expected_command).and_return(true) 43 | subject.run_specs(:bundle_exec => true) 44 | end 45 | 46 | it 'raises an error if the call returns a non-zero error code' do 47 | subject.should_receive(:shell_out).with(anything).and_return(false) 48 | expect { subject.run_specs(:bundle_exec => false) }.to raise_error(Gemika::RSpecFailed) 49 | end 50 | 51 | end 52 | 53 | end 54 | -------------------------------------------------------------------------------- /lib/gemika/matrix/github_actions_config.rb: -------------------------------------------------------------------------------- 1 | module Gemika 2 | class Matrix 3 | 4 | ## 5 | # Load Github Action `.yml` files. 6 | # 7 | # @!visibility private 8 | # 9 | class GithubActionsConfig 10 | class << self 11 | 12 | def load_rows(options) 13 | path = options.fetch(:path, '.github/workflows/test.yml') 14 | raise MissingMatrixDefinition, "expected a #{path} file" unless File.exist?(path) 15 | 16 | workflow_yml = YAML.load_file(path) 17 | 18 | matrices = workflow_yml.fetch('jobs', {}).values.map do |job| 19 | job.fetch('strategy', {})['matrix'] 20 | end.reject(&:nil?) 21 | 22 | matrices.map do |matrix| 23 | matrix_to_rows(matrix) 24 | end.flatten(1) 25 | end 26 | 27 | private 28 | 29 | def matrix_to_rows(matrix) 30 | if (!matrix['ruby'] || !matrix['gemfile']) && (!matrix['include']) 31 | raise InvalidMatrixDefinition, 'matrix must use the keys "ruby" and "gemfile"' 32 | end 33 | 34 | rubies = matrix.fetch('ruby', []) 35 | gemfiles = matrix.fetch('gemfile', []) 36 | 37 | includes = matrix.fetch('include', []) 38 | excludes = matrix.fetch('exclude', []) 39 | 40 | rows = [] 41 | rubies.each do |ruby| 42 | gemfiles.each do |gemfile| 43 | row = { 'ruby' => ruby, 'gemfile' => gemfile } 44 | rows << row unless excludes.include?(row) 45 | end 46 | end 47 | 48 | rows = rows + includes 49 | rows.map { |row| convert_row(row) } 50 | end 51 | 52 | def convert_row(row_hash) 53 | if !row_hash['ruby'] || !row_hash['gemfile'] 54 | raise InvalidMatrixDefinition, 'matrix must use the keys "ruby" and "gemfile"' 55 | end 56 | Row.new(:ruby => row_hash['ruby'], :gemfile => row_hash['gemfile']) 57 | end 58 | 59 | end 60 | end 61 | 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /Gemfile.8.0.pg.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | gemika (1.0.0) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | activemodel (8.0.1) 10 | activesupport (= 8.0.1) 11 | activerecord (8.0.1) 12 | activemodel (= 8.0.1) 13 | activesupport (= 8.0.1) 14 | timeout (>= 0.4.0) 15 | activesupport (8.0.1) 16 | base64 17 | benchmark (>= 0.3) 18 | bigdecimal 19 | concurrent-ruby (~> 1.0, >= 1.3.1) 20 | connection_pool (>= 2.2.5) 21 | drb 22 | i18n (>= 1.6, < 2) 23 | logger (>= 1.4.2) 24 | minitest (>= 5.1) 25 | securerandom (>= 0.3) 26 | tzinfo (~> 2.0, >= 2.0.5) 27 | uri (>= 0.13.1) 28 | base64 (0.2.0) 29 | benchmark (0.4.0) 30 | bigdecimal (3.1.8) 31 | coderay (1.1.3) 32 | concurrent-ruby (1.3.4) 33 | connection_pool (2.4.1) 34 | database_cleaner (2.0.2) 35 | database_cleaner-active_record (>= 2, < 3) 36 | database_cleaner-active_record (2.2.0) 37 | activerecord (>= 5.a) 38 | database_cleaner-core (~> 2.0.0) 39 | database_cleaner-core (2.0.1) 40 | diff-lcs (1.5.0) 41 | drb (2.2.1) 42 | i18n (1.14.6) 43 | concurrent-ruby (~> 1.0) 44 | logger (1.6.4) 45 | method_source (1.0.0) 46 | minitest (5.25.4) 47 | pg (1.5.9) 48 | pry (0.14.2) 49 | coderay (~> 1.1) 50 | method_source (~> 1.0) 51 | rake (12.3.3) 52 | rspec (3.8.0) 53 | rspec-core (~> 3.8.0) 54 | rspec-expectations (~> 3.8.0) 55 | rspec-mocks (~> 3.8.0) 56 | rspec-core (3.8.2) 57 | rspec-support (~> 3.8.0) 58 | rspec-expectations (3.8.6) 59 | diff-lcs (>= 1.2.0, < 2.0) 60 | rspec-support (~> 3.8.0) 61 | rspec-mocks (3.8.2) 62 | diff-lcs (>= 1.2.0, < 2.0) 63 | rspec-support (~> 3.8.0) 64 | rspec-support (3.8.3) 65 | securerandom (0.4.1) 66 | timeout (0.4.3) 67 | tzinfo (2.0.6) 68 | concurrent-ruby (~> 1.0) 69 | uri (1.0.2) 70 | 71 | PLATFORMS 72 | ruby 73 | 74 | DEPENDENCIES 75 | activerecord (~> 8.0.1) 76 | database_cleaner (~> 2.0.2) 77 | gemika! 78 | pg (~> 1.5.6) 79 | pry (~> 0.14.2) 80 | rake 81 | rspec (~> 3.5) 82 | 83 | RUBY VERSION 84 | ruby 2.2.4p230 85 | 86 | BUNDLED WITH 87 | 2.3.1 88 | -------------------------------------------------------------------------------- /bin/matrix: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | # utility for comparing version strings 5 | # https://stackoverflow.com/questions/4023830/how-to-compare-two-strings-in-dot-separated-version-format-in-bash 6 | function version { echo "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }'; } 7 | 8 | # store current Ruby version and restore it on exit 9 | original_ruby_version=$(cat .ruby-version) 10 | trap "rbenv local $original_ruby_version" EXIT 11 | 12 | # determine employed Ruby versions (awk command == "strip whitespace", sed command == "delete_prefix") 13 | readarray -t versions < <(grep ruby: .github/workflows/test.yml | awk '{$1=$1};1' | sed 's/- ruby: //' | sort | uniq) 14 | echo "Detected Ruby versions:" 15 | for version in "${versions[@]}" 16 | do 17 | echo "- $version" 18 | done 19 | echo "" 20 | 21 | for version in "${versions[@]}" 22 | do 23 | # switch Ruby version 24 | rbenv local "$version" 25 | 26 | # determine actual versions 27 | ruby_version=$(ruby -v) 28 | rubygems_version=$(gem -v) 29 | bundler_version=$(bundler -v | sed 's/Bundler version //') 30 | 31 | # debug output 32 | echo "=====================" 33 | echo "Target Ruby version: $version" 34 | echo "" 35 | echo "Ruby: $ruby_version" 36 | echo "rubygems: $rubygems_version" 37 | echo "Bundler: $bundler_version" 38 | echo "=====================" 39 | echo "" 40 | 41 | # version checks (minimum versions to make 'BUNDLED WITH' in Gemfile.lock work correctly) 42 | if [ $(version $rubygems_version) -lt $(version "3.3.0") ]; then 43 | echo "Please ensure that your rubygems version is > 3.3.0 for Ruby $ruby_version!" 44 | echo "Install newest version:" 45 | echo "gem update --system" 46 | echo "Install specific version:" 47 | echo "gem update --system " 48 | exit 1 49 | fi 50 | 51 | if [ $(version $bundler_version) -lt $(version "2.3.0") ]; then 52 | echo "Please ensure that your Bundler version is > 2.3.0 for Ruby $ruby_version!" 53 | echo "Install newest version:" 54 | echo "gem install bundler" 55 | echo "Install specific version:" 56 | echo "gem install bundler:" 57 | exit 1 58 | fi 59 | 60 | # bundle and run specs 61 | rake matrix:install 62 | rake matrix:spec 63 | done 64 | 65 | exit 0 66 | -------------------------------------------------------------------------------- /lib/gemika/rspec.rb: -------------------------------------------------------------------------------- 1 | require 'gemika/errors' 2 | require 'gemika/env' 3 | 4 | module Gemika 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 | bundle_exec = options.fetch(:bundle_exec) ? 'bundle exec' : nil 19 | command = [bundle_exec, runner, rspec_options, files].compact.join(' ') 20 | result = shell_out(command) 21 | if result 22 | true 23 | elsif fatal 24 | raise RSpecFailed, "RSpec failed: #{command}" 25 | else 26 | false 27 | end 28 | end 29 | 30 | ## 31 | # Returns the binary name for the current RSpec version. 32 | # 33 | def binary(options = {}) 34 | if Env.gem?('rspec', '< 2', options) 35 | 'spec' 36 | else 37 | 'rspec' 38 | end 39 | end 40 | 41 | ## 42 | # Configures RSpec. 43 | # 44 | # Works with both RSpec 1 and RSpec 2. 45 | # 46 | def configure(&block) 47 | configurator.configure(&block) 48 | end 49 | 50 | ## 51 | # Configures RSpec to clean out the database before each example. 52 | # 53 | # Requires the `database_cleaner` gem to be added to your development dependencies. 54 | # 55 | def configure_clean_database_before_example 56 | require 'database_cleaner' # optional dependency 57 | configure do |config| 58 | config.before(:each) do 59 | # Truncation works across most database adapters; I had issues with :deletion and pg 60 | DatabaseCleaner.clean_with(:truncation) 61 | end 62 | end 63 | end 64 | 65 | ## 66 | # Configures RSpec so it allows the `should` syntax that works across all RSpec versions. 67 | # 68 | def configure_should_syntax 69 | if Env.gem?('rspec', '>= 2.11') 70 | configure do |config| 71 | config.expect_with(:rspec) { |c| c.syntax = [:should, :expect] } 72 | config.mock_with(:rspec) { |c| c.syntax = [:should, :expect] } 73 | end 74 | else 75 | # We have an old RSpec that only understands should syntax 76 | end 77 | end 78 | 79 | private 80 | 81 | def shell_out(command) 82 | system(command) 83 | end 84 | 85 | def configurator 86 | if Env.gem?('rspec', '<2') 87 | Spec::Runner 88 | else 89 | ::RSpec 90 | end 91 | end 92 | 93 | extend self 94 | 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | All notable changes to this project will be documented in this file. 2 | 3 | This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 4 | 5 | 6 | ## Unreleased 7 | 8 | ### Breaking changes 9 | 10 | - 11 | 12 | ### Compatible changes 13 | 14 | - 15 | 16 | ## 1.0.0 - 2025-06-16 17 | 18 | ### Breaking changes 19 | 20 | - Removed support for Travis CI 21 | - Removed migration from travis to github actions 22 | 23 | ## 0.8.4 - 2025-01-16 24 | 25 | - Add support for Ruby 3.4 26 | - Test against ActiveRecord 8.0 27 | 28 | ## 0.8.3 - 2024-01-04 29 | 30 | ### Compatible changes 31 | 32 | - handle missing rbenv-aliases plugin 33 | 34 | 35 | ## 0.8.2 - 2023-07-13 36 | 37 | ### Compatible changes 38 | 39 | - Update gem and bundler versions 40 | - Provide dev script `bin/matrix` and update README 41 | 42 | ## 0.8.1 - 2023-01-24 43 | 44 | ### Compatible changes 45 | 46 | - Fix specs 47 | 48 | ## 0.8.0 - 2023-01-24 49 | 50 | ### Compatible changes 51 | 52 | - Add support for Ruby 3.2 53 | - Test against Ruby 3.2 instead of Ruby 3.0 54 | 55 | ## 0.7.1 - 2022-03-16 56 | 57 | ### Compatible changes 58 | 59 | - Activate rubygems MFA 60 | 61 | ## 0.7.0 - 2022-01-19 62 | 63 | ### Breaking changes 64 | 65 | - Remove no longer supported ruby versions (2.3.8) 66 | 67 | ### Compatible changes 68 | 69 | - test against ActiveRecord 7.0 70 | - add support for rbenv aliases 71 | 72 | ## 0.6.1 - 2021-04-20 73 | 74 | ### Compatible changes 75 | 76 | - fix deprecation warning for Bundler.with_clean_env on Bundler >= 2 77 | 78 | ## 0.6.0 - 2021-04-20 79 | 80 | ### Compatible changes 81 | 82 | - add Ruby 3 compatibility 83 | - drop Ruby 2.2 support 84 | 85 | ## 0.5.0 - 2020-10-09 86 | 87 | ### Compatible changes 88 | 89 | - add support for github actions instead of travis 90 | - add method to migrate travis to github actions workflow 91 | 92 | ## 0.4.0 - 2019-08-07 93 | 94 | ### Compatible changes 95 | 96 | - Move gemfiles to project root 97 | - Added support to read the `include` option from the `travis.yml` file. All combinations defined in the include option 98 | are added to the existing matrix. If no matrix exist, these are the only ones that are run. 99 | 100 | Example: 101 | 102 | ``` 103 | rvm: 104 | - 2.1.8 105 | - 2.3.1 106 | 107 | gemfile: 108 | - gemfiles/Gemfile1 109 | - gemfiles/Gemfile2 110 | 111 | matrix: 112 | include: 113 | - rvm: 2.6.3 114 | gemfile: gemfiles/Gemfile3 115 | ``` 116 | 117 | ## 0.3.4 - 2018-08-29 118 | 119 | ### Compatible changes 120 | 121 | - Print a warning instead of crashing when `database.yml` is missing. 122 | 123 | 124 | ## 0.3.3 - 2018-08-01 125 | 126 | ### Compatible changes 127 | 128 | - Add support for sqlite3. 129 | 130 | 131 | ## 0.3.2 - 2016-09-28 132 | 133 | ### Compatible changes 134 | 135 | - Remove some debug output. 136 | 137 | 138 | ## 0.3.1 - 2016-09-28 139 | 140 | ### Compatible changes 141 | 142 | - `rake current_rspec` no longer does a second unnecessary `bundle exec` call 143 | 144 | 145 | ## Older releases 146 | 147 | Please check commits. 148 | -------------------------------------------------------------------------------- /spec/gemika/matrix/row_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Gemika::Matrix::Row do 4 | 5 | let(:subject) { described_class.new(ruby: '3.0.3', gemfile: nil) } 6 | 7 | describe '#compatible_with_ruby?' do 8 | context 'when no rbenv alias is present' do 9 | before { expect(subject).to receive(:rbenv_aliases).and_return('') } 10 | 11 | context 'when the requested ruby version is the current ruby version' do 12 | it 'returns true' do 13 | expect(subject.compatible_with_ruby?('3.0.3')).to eq(true) 14 | end 15 | end 16 | 17 | context 'when the requested ruby version is not the current ruby version' do 18 | it 'returns false' do 19 | expect(subject.compatible_with_ruby?('2.5.7')).to eq(false) 20 | end 21 | end 22 | end 23 | 24 | context 'when an rbenv alias is present' do 25 | context 'when the current ruby version is an rbenv alias of the requested version' do 26 | before { expect(subject).to receive(:rbenv_aliases).and_return('3.0.3 => 3.0.1') } 27 | 28 | it 'returns true and stores that alias in the @used_ruby variable' do 29 | expect(subject.compatible_with_ruby?('3.0.1')).to eq(true) 30 | expect(subject.used_ruby).to eq('3.0.1') 31 | expect(subject.ruby).to eq('3.0.3') 32 | end 33 | end 34 | 35 | context 'when the requested ruby version is not aliased by rbenv' do 36 | before { expect(subject).to receive(:rbenv_aliases).and_return('3.0.0 => 3.0.1') } 37 | 38 | it 'returns true when the requested ruby version is the current ruby version' do 39 | expect(subject.compatible_with_ruby?('3.0.3')).to eq(true) 40 | expect(subject.used_ruby).to eq('3.0.3') 41 | expect(subject.ruby).to eq('3.0.3') 42 | end 43 | 44 | it 'returns false when the requested ruby version is not the current ruby version' do 45 | expect(subject.compatible_with_ruby?('3.0.4')).to eq(false) 46 | expect(subject.used_ruby).to eq('3.0.3') 47 | expect(subject.ruby).to eq('3.0.3') 48 | end 49 | end 50 | end 51 | 52 | context 'when multiple rbenv aliases chained result in aliasing the requested ruby version' do 53 | before { expect(subject).to receive(:rbenv_aliases).and_return("3.0.3 => 3.0.2\n3.0.2 => 3.0.1\n3.0.1 => 3.0.0") } 54 | 55 | it 'returns true' do 56 | expect(subject.compatible_with_ruby?('3.0.0')).to eq(true) 57 | end 58 | end 59 | 60 | context 'when rbenv is installed without rbenv-aliases plugin' do 61 | before do 62 | allow(Open3).to receive(:capture2e).with('which', 'rbenv').and_return(['/path/to/rbenv', double(success?: true, exitstatus: 0)]) 63 | allow(Open3).to receive(:capture2e).with('rbenv', 'alias', '--list').and_return(['rbenv: no such command `alias', double(success?: false, exitstatus: 1)]) 64 | end 65 | 66 | it 'does not crash and does not print errors' do 67 | expect do 68 | expect(subject.compatible_with_ruby?('3.0.3')).to eq(true) 69 | end.to output('').to_stdout_from_any_process.and output('').to_stderr_from_any_process 70 | end 71 | end 72 | 73 | end 74 | 75 | end 76 | -------------------------------------------------------------------------------- /spec/gemika/env_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Gemika::Env do 4 | 5 | subject { Gemika::Env } 6 | 7 | describe '.gemfile' do 8 | 9 | it 'returns the path to the current gemfile' do 10 | subject.gemfile.should =~ /Gemfile/ 11 | end 12 | 13 | it 'returns the original gemfile of the process, even within blocks to .with_gemfile' do 14 | original_gemfile = subject.gemfile 15 | subject.with_gemfile('Foofile') do 16 | ENV['BUNDLE_GEMFILE'].should == 'Foofile' 17 | subject.gemfile.should == original_gemfile 18 | end 19 | end 20 | 21 | end 22 | 23 | describe '.with_gemfile' do 24 | 25 | it "changes ENV['BUNDLE_GEMFILE'] for the duration of the given block, then sets it back" do 26 | original_gemfile = ENV['BUNDLE_GEMFILE'] 27 | subject.with_gemfile('Foofile') do 28 | ENV['BUNDLE_GEMFILE'].should == 'Foofile' 29 | end 30 | ENV['BUNDLE_GEMFILE'].should == original_gemfile 31 | end 32 | 33 | end 34 | 35 | describe '.gem?' do 36 | 37 | it 'returns whether the given gem was activated by the current gemfile' do 38 | spec = Gem::Specification.new do |spec| 39 | spec.name = 'activated-gem' 40 | spec.version = '1.2.3' 41 | end 42 | Gem.should_receive(:loaded_specs).at_least(:once).and_return({ 'activated-gem' => spec}) 43 | subject.gem?('activated-gem').should == true 44 | subject.gem?('other-gem').should == false 45 | end 46 | 47 | it 'allows to pass a version constraint' do 48 | spec = Gem::Specification.new do |spec| 49 | spec.name = 'activated-gem' 50 | spec.version = '1.2.3' 51 | end 52 | Gem.should_receive(:loaded_specs).at_least(:once).and_return({ 'activated-gem' => spec}) 53 | subject.gem?('activated-gem', '=1.2.3').should == true 54 | subject.gem?('activated-gem', '=1.2.4').should == false 55 | subject.gem?('activated-gem', '>= 1').should == true 56 | subject.gem?('activated-gem', '< 1').should == false 57 | subject.gem?('activated-gem', '~> 1.2.0').should == true 58 | subject.gem?('activated-gem', '~> 1.1.0').should == false 59 | end 60 | 61 | it 'allows to query a gemfile that is not the current gemfile' do 62 | path = 'spec/fixtures/gemfiles/Gemfile_with_activesupport_5' 63 | subject.gem?('activesupport', :gemfile => path).should == true 64 | subject.gem?('activesupport', '>= 5', :gemfile => path).should == true 65 | subject.gem?('activesupport', '~> 5.0.0', :gemfile => path).should == true 66 | subject.gem?('activesupport', '< 5', :gemfile => path).should == false 67 | subject.gem?('consul', :gemfile => path).should == false 68 | subject.gem?('consul', '>= 0', :gemfile => path).should == false 69 | end 70 | 71 | end 72 | 73 | describe '.ruby?' do 74 | 75 | it 'returns whether the current Ruby version satisfies the given requirement' do 76 | subject.should_receive(:ruby).at_least(:once).and_return('2.1.8') 77 | subject.ruby?('=2.1.8').should == true 78 | subject.ruby?('=1.9.3').should == false 79 | subject.ruby?('>= 2').should == true 80 | subject.ruby?('< 2').should == false 81 | subject.ruby?('~> 2.1.0').should == true 82 | end 83 | 84 | end 85 | 86 | end 87 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Tests 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | jobs: 11 | test_mysql: 12 | runs-on: ubuntu-24.04 13 | services: 14 | mysql: 15 | image: mysql:5.6 16 | env: 17 | MYSQL_ROOT_PASSWORD: password 18 | options: --health-cmd "mysqladmin ping" --health-interval 10s --health-timeout 19 | 5s --health-retries 5 20 | ports: 21 | - 3306:3306 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | include: 26 | - ruby: 2.5.3 27 | gemfile: Gemfile.5.2.mysql2 28 | env: 29 | BUNDLE_GEMFILE: "${{ matrix.gemfile }}" 30 | steps: 31 | - uses: actions/checkout@v3 32 | - name: Install ruby 33 | uses: ruby/setup-ruby@v1 34 | with: 35 | ruby-version: "${{ matrix.ruby }}" 36 | - name: Setup database 37 | run: | 38 | mysql -e 'create database IF NOT EXISTS test;' -u root --password=password -P 3306 -h 127.0.0.1 39 | - name: Bundle 40 | run: | 41 | gem install bundler:2.3.1 42 | bundle install --no-deployment 43 | - name: Run tests 44 | run: bundle exec rspec 45 | test_pg: 46 | runs-on: ubuntu-24.04 47 | services: 48 | postgres: 49 | image: postgres 50 | env: 51 | POSTGRES_PASSWORD: postgres 52 | options: "--health-cmd pg_isready --health-interval 10s --health-timeout 5s 53 | --health-retries 5" 54 | ports: 55 | - 5432:5432 56 | strategy: 57 | fail-fast: false 58 | matrix: 59 | include: 60 | - ruby: 2.5.3 61 | gemfile: Gemfile.5.2.pg 62 | - ruby: 2.6.7 63 | gemfile: Gemfile.6.1.pg 64 | - ruby: 2.7.3 65 | gemfile: Gemfile.6.1.pg 66 | - ruby: 2.7.3 67 | gemfile: Gemfile.7.0.pg 68 | - ruby: 3.2.0 69 | gemfile: Gemfile.6.1.pg 70 | - ruby: 3.2.0 71 | gemfile: Gemfile.7.0.pg 72 | - ruby: 3.4.1 73 | gemfile: Gemfile.6.1.pg 74 | - ruby: 3.4.1 75 | gemfile: Gemfile.7.0.pg 76 | - ruby: 3.2.0 77 | gemfile: Gemfile.8.0.pg 78 | - ruby: 3.4.1 79 | gemfile: Gemfile.8.0.pg 80 | env: 81 | BUNDLE_GEMFILE: "${{ matrix.gemfile }}" 82 | steps: 83 | - uses: actions/checkout@v3 84 | - name: Install ruby 85 | uses: ruby/setup-ruby@v1 86 | with: 87 | ruby-version: "${{ matrix.ruby }}" 88 | - name: Setup database 89 | run: | 90 | sudo apt-get update 91 | sudo apt-get install -y postgresql-client 92 | PGPASSWORD=postgres psql -c 'create database test;' -U postgres -p 5432 -h localhost 93 | - name: Bundle 94 | run: | 95 | gem install bundler:2.3.1 96 | bundle install --no-deployment 97 | - name: Run tests 98 | run: bundle exec rspec 99 | test_sqlite: 100 | runs-on: ubuntu-24.04 101 | strategy: 102 | fail-fast: false 103 | matrix: 104 | include: 105 | - ruby: 2.5.3 106 | gemfile: Gemfile.5.2.sqlite3 107 | env: 108 | BUNDLE_GEMFILE: "${{ matrix.gemfile }}" 109 | steps: 110 | - uses: actions/checkout@v3 111 | - name: Install ruby 112 | uses: ruby/setup-ruby@v1 113 | with: 114 | ruby-version: "${{ matrix.ruby }}" 115 | - name: Bundle 116 | run: | 117 | gem install bundler:2.3.1 118 | bundle install --no-deployment 119 | - name: Run tests 120 | run: bundle exec rspec 121 | -------------------------------------------------------------------------------- /lib/gemika/database.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | require 'active_record' 3 | require 'gemika/env' 4 | require 'gemika/errors' 5 | 6 | module Gemika 7 | ## 8 | # Helpers for creating a test database. 9 | # 10 | class Database 11 | 12 | class Error < StandardError; end 13 | class UnknownAdapter < Error; end 14 | 15 | def initialize(options = {}) 16 | yaml_config_folder = options.fetch(:config_folder, 'spec/support') 17 | yaml_config_filename = if Env.github? 18 | 'database.github.yml' 19 | else 20 | 'database.yml' 21 | end 22 | yaml_config_path = File.join(yaml_config_folder, yaml_config_filename) 23 | if File.exist?(yaml_config_path) 24 | @yaml_config = YAML.load_file(yaml_config_path) 25 | else 26 | @yaml_config = {} 27 | warn "No database configuration in #{yaml_config_path}, using defaults: #{adapter_config.inspect}" 28 | end 29 | @connected = false 30 | end 31 | 32 | ## 33 | # Connects ActiveRecord to the database configured in `spec/support/database.yml`. 34 | # 35 | def connect 36 | unless @connected 37 | ActiveRecord::Base.establish_connection(**adapter_config) 38 | @connected = true 39 | end 40 | end 41 | 42 | ## 43 | # Drops all tables from the current database. 44 | # 45 | def drop_tables! 46 | connect 47 | connection.tables.each do |table| 48 | connection.drop_table table 49 | end 50 | end 51 | 52 | ## 53 | # Runs the [ActiveRecord database migration](http://api.rubyonrails.org/classes/ActiveRecord/Migration.html) described in `block`. 54 | # 55 | # @example 56 | # Gemika::Database.new.migrate do 57 | # create_table :users do |t| 58 | # t.string :name 59 | # t.string :email 60 | # t.string :city 61 | # end 62 | # end 63 | def migrate(&block) 64 | connect 65 | ActiveRecord::Migration.class_eval(&block) 66 | end 67 | 68 | ## 69 | # Drops all tables, then 70 | # runs the [ActiveRecord database migration](http://api.rubyonrails.org/classes/ActiveRecord/Migration.html) described in `block`. 71 | # 72 | # @example 73 | # Gemika::Database.new.rewrite_schema! do 74 | # create_table :users do |t| 75 | # t.string :name 76 | # t.string :email 77 | # t.string :city 78 | # end 79 | # end 80 | def rewrite_schema!(&block) 81 | connect 82 | drop_tables! 83 | migrate(&block) 84 | end 85 | 86 | ## 87 | # Returns a hash of ActiveRecord adapter options for the currently activated database gem. 88 | # 89 | def adapter_config 90 | default_config = {} 91 | default_config['database'] = guess_database_name 92 | if Env.gem?('pg') 93 | default_config['adapter'] = 'postgresql' 94 | default_config['password'] = '' 95 | user_config = @yaml_config['postgresql'] || @yaml_config['postgres'] || @yaml_config['pg'] || {} 96 | elsif Env.gem?('mysql2') 97 | default_config['adapter'] = 'mysql2' 98 | default_config['encoding'] = 'utf8' 99 | user_config = (@yaml_config['mysql'] || @yaml_config['mysql2']) || {} 100 | elsif Env.gem?('sqlite3') 101 | default_config['adapter'] = 'sqlite3' 102 | default_config['database'] = ':memory:' 103 | user_config = (@yaml_config['sqlite'] || @yaml_config['sqlite3']) || {} 104 | else 105 | raise UnknownAdapter, "Unknown database type. Either 'pg', 'mysql2', or 'sqlite3' gem should be in your current bundle." 106 | end 107 | default_config.merge(user_config).symbolize_keys 108 | end 109 | 110 | private 111 | 112 | def guess_database_name 113 | project_name = File.basename(Dir.pwd) 114 | "#{project_name}_test" 115 | end 116 | 117 | def connection 118 | ActiveRecord::Base.connection 119 | end 120 | 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /lib/gemika/env.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'gemika/errors' 3 | 4 | module Gemika 5 | ## 6 | # Version switches to write code that works with different versions of 7 | # Ruby and gem dependencies. 8 | # 9 | module Env 10 | 11 | VERSION_PATTERN = /(?:\d+\.)*\d+/ 12 | 13 | ## 14 | # Returns the path to the gemfile for the current Ruby process. 15 | # 16 | def gemfile 17 | if @gemfile_changed 18 | @process_gemfile 19 | else 20 | ENV['BUNDLE_GEMFILE'] 21 | end 22 | end 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 | # 33 | def with_gemfile(path, *args, &block) 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 | 39 | # .with_clean_env is deprecated since Bundler ~> 2. 40 | bundler_method = if Gemika::Env.gem?('bundler', '< 2') 41 | :with_clean_env 42 | else 43 | :with_unbundled_env 44 | end 45 | 46 | Bundler.send(bundler_method) do 47 | ENV['BUNDLE_GEMFILE'] = path 48 | block.call(*args) 49 | end 50 | ensure 51 | @gemfile_changed = false 52 | ENV['BUNDLE_GEMFILE'] = @process_gemfile 53 | end 54 | 55 | ## 56 | # Check if the given gem was activated by the current gemfile. 57 | # It might or might not have been `require`d yet. 58 | # 59 | # @example 60 | # Gemika::Env.gem?('activerecord') 61 | # Gemika::Env.gem?('activerecord', '= 5.0.0') 62 | # Gemika::Env.gem?('activerecord', '~> 4.2.0') 63 | # 64 | def gem?(*args) 65 | options = args.last.is_a?(Hash) ? args.pop : {} 66 | name, requirement_string = args 67 | if options[:gemfile] && !process_gemfile?(options[:gemfile]) 68 | gem_in_gemfile?(options[:gemfile], name, requirement_string) 69 | else 70 | gem_activated?(name, requirement_string) 71 | end 72 | end 73 | 74 | ## 75 | # Returns the current version of Ruby. 76 | # 77 | def ruby 78 | RUBY_VERSION 79 | end 80 | 81 | ## 82 | # Check if the current version of Ruby satisfies the given requirements. 83 | # 84 | # @example 85 | # Gemika::Env.ruby?('>= 2.1.0') 86 | # 87 | def ruby?(requirement) 88 | requirement_satisfied?(requirement, ruby) 89 | end 90 | 91 | ## 92 | # Return whether this process is running within a Github Actions build. 93 | def github? 94 | ENV.key?('GITHUB_WORKFLOW') 95 | end 96 | 97 | ## 98 | # Creates an hash that enumerates entries in order of insertion. 99 | # 100 | # @!visibility private 101 | # 102 | def new_ordered_hash 103 | # We use it when ActiveSupport is activated 104 | if ruby?('>= 1.9') 105 | {} 106 | elsif gem?('activesupport') 107 | require 'active_support/ordered_hash' 108 | ActiveSupport::OrderedHash.new 109 | else 110 | # We give up 111 | {} 112 | end 113 | end 114 | 115 | private 116 | 117 | def bundler? 118 | !gemfile.nil? && gemfile != '' 119 | end 120 | 121 | def process_gemfile?(given_gemfile) 122 | bundler? && File.expand_path(gemfile) == File.expand_path(given_gemfile) 123 | end 124 | 125 | def gem_activated?(name, requirement) 126 | gem = Gem.loaded_specs[name] 127 | if gem 128 | if requirement 129 | version = gem.version 130 | requirement_satisfied?(requirement, version) 131 | else 132 | true 133 | end 134 | else 135 | false 136 | end 137 | end 138 | 139 | def gem_in_gemfile?(gemfile, name, requirement = nil) 140 | lockfile = lockfile_contents(gemfile) 141 | if lockfile =~ /\b#{Regexp.escape(name)}\s*\((#{VERSION_PATTERN})\)/ 142 | version = $1 143 | if requirement 144 | requirement_satisfied?(requirement, version) 145 | else 146 | true 147 | end 148 | else 149 | false 150 | end 151 | end 152 | 153 | def requirement_satisfied?(requirement, version) 154 | requirement = Gem::Requirement.new(requirement) if requirement.is_a?(String) 155 | version = Gem::Version.new(version) if version.is_a?(String) 156 | if requirement.respond_to?(:satisfied_by?) # Ruby 1.9.3+ 157 | requirement.satisfied_by?(version) 158 | else 159 | ops = Gem::Requirement::OPS 160 | requirement.requirements.all? { |op, rv| (ops[op] || ops["="]).call version, rv } 161 | end 162 | end 163 | 164 | def lockfile_contents(gemfile) 165 | lockfile = "#{gemfile}.lock" 166 | File.exist?(lockfile) or raise MissingLockfile, "Lockfile not found: #{lockfile}" 167 | File.read(lockfile) 168 | end 169 | 170 | # Make all methods available as static module methods 171 | extend self 172 | 173 | end 174 | end 175 | -------------------------------------------------------------------------------- /lib/gemika/matrix.rb: -------------------------------------------------------------------------------- 1 | require 'open3' 2 | require 'yaml' 3 | require 'gemika/errors' 4 | require 'gemika/env' 5 | require 'gemika/matrix/github_actions_config' 6 | 7 | module Gemika 8 | class Matrix 9 | 10 | ## 11 | # A row in the test matrix 12 | # 13 | class Row 14 | 15 | def initialize(attrs) 16 | @ruby = attrs.fetch(:ruby) 17 | @gemfile = attrs.fetch(:gemfile) 18 | end 19 | 20 | ## 21 | # The Ruby version for the row. 22 | # 23 | attr_reader :ruby 24 | 25 | ## 26 | # The actually used Ruby version for the row. 27 | # 28 | attr_reader :used_ruby 29 | 30 | ## 31 | # The path to the gemfile for the row. 32 | # 33 | attr_reader :gemfile 34 | 35 | ## 36 | # Returns whether this row can be run with the given Ruby version. 37 | # 38 | def compatible_with_ruby?(current_ruby = Env.ruby) 39 | @used_ruby = aliased_ruby(ruby) 40 | 41 | @used_ruby == current_ruby 42 | end 43 | 44 | ## 45 | # Raises an error if this row is invalid. 46 | # 47 | # @!visibility private 48 | # 49 | def validate! 50 | File.exist?(gemfile) or raise MissingGemfile, "Gemfile not found: #{gemfile}" 51 | contents = File.read(gemfile) 52 | contents.include?('gemika') or raise UnusableGemfile, "Gemfile is missing gemika dependency: #{gemfile}" 53 | end 54 | 55 | private 56 | 57 | ## 58 | # Checks if the requested ruby version is aliased by rbenv to use another ruby version. 59 | # Returns the runnable ruby version. 60 | # 61 | def aliased_ruby(requested_version) 62 | ruby_aliases = rbenv_aliases 63 | 64 | aliased_versions = {} 65 | 66 | ruby_aliases.split("\n").each do |ruby_alias| 67 | split_pattern = /\A(.+) => (.+)\z/ 68 | alias_name, aliased_version = ruby_alias.match(split_pattern)&.captures 69 | aliased_versions[alias_name] = aliased_version 70 | end 71 | 72 | find_aliased_ruby(requested_version, aliased_versions) 73 | end 74 | 75 | ## 76 | # Recursively traverses aliases until the requested Ruby version is found. 77 | # Returns the requested version if no alias can be found for that version. 78 | # 79 | def find_aliased_ruby(requested_version, aliased_versions) 80 | found_version = aliased_versions[requested_version] 81 | 82 | if found_version == requested_version 83 | found_version 84 | elsif found_version 85 | find_aliased_ruby(found_version, aliased_versions) 86 | else 87 | requested_version 88 | end 89 | end 90 | 91 | ## 92 | # Returns the list of rbenv aliases, if rbenv is installed with rbenv-aliases plugin. 93 | # 94 | def rbenv_aliases 95 | _output, status = Open3.capture2e('which', 'rbenv') 96 | return '' unless status.success? 97 | 98 | output, status = Open3.capture2e('rbenv', 'alias', '--list') 99 | if status.success? 100 | output 101 | else 102 | '' 103 | end 104 | end 105 | 106 | end 107 | 108 | COLOR_HEAD = "\e[44;97m" 109 | COLOR_WARNING = "\e[33m" 110 | COLOR_SUCCESS = "\e[32m" 111 | COLOR_FAILURE = "\e[31m" 112 | COLOR_RESET = "\e[0m" 113 | 114 | def initialize(options) 115 | @rows = options.fetch(:rows) 116 | @silent = options.fetch(:silent, false) 117 | @io = options.fetch(:io, STDOUT) 118 | @color = options.fetch(:color, true) 119 | validate = options.fetch(:validate, true) 120 | @rows.each(&:validate!) if validate 121 | @results = Env.new_ordered_hash 122 | @compatible_count = 0 123 | @all_passed = nil 124 | @current_ruby = options.fetch(:current_ruby, RUBY_VERSION) 125 | @aliased_rubys = {} 126 | end 127 | 128 | ## 129 | # Runs the given `block` for each matrix row that is compatible with the current Ruby. 130 | # 131 | # The row's gemfile will be set as an environment variable, so Bundler will use that gemfile if you shell out in `block`. 132 | # 133 | # At the end it will print a summary of which rows have passed, failed or were skipped (due to incompatible Ruby version). 134 | # 135 | def each(&block) 136 | @all_passed = true 137 | rows.each do |row| 138 | gemfile = row.gemfile 139 | if row.compatible_with_ruby?(current_ruby) 140 | @compatible_count += 1 141 | 142 | @aliased_rubys[current_ruby] = row.ruby 143 | 144 | print_title gemfile 145 | gemfile_passed = Env.with_gemfile(gemfile, row, &block) 146 | @all_passed &= gemfile_passed 147 | if gemfile_passed 148 | @results[row] = tint('Success', COLOR_SUCCESS) 149 | else 150 | @results[row] = tint('Failed', COLOR_FAILURE) 151 | end 152 | else 153 | @results[row] = tint("Skipped", COLOR_WARNING) 154 | end 155 | end 156 | print_summary 157 | end 158 | 159 | 160 | class << self 161 | ## 162 | # Builds a {Matrix} from the given Github Action workflow definition 163 | # 164 | # @param [Hash] options 165 | # @option path [String] Path to the `.yml` file. 166 | # 167 | def from_github_actions_yml(options = {}) 168 | rows = GithubActionsConfig.load_rows(options) 169 | new(options.merge(rows: rows)) 170 | end 171 | 172 | alias from_ci_config from_github_actions_yml 173 | end 174 | 175 | attr_reader :rows, :current_ruby 176 | 177 | private 178 | 179 | def puts(*args) 180 | unless @silent 181 | @io.puts(*args) 182 | end 183 | end 184 | 185 | def tint(message, color) 186 | if @color 187 | color + message + COLOR_RESET 188 | else 189 | message 190 | end 191 | end 192 | 193 | def print_title(title) 194 | puts 195 | puts tint(title, COLOR_HEAD) 196 | puts 197 | end 198 | 199 | def print_summary 200 | print_title 'Summary' 201 | 202 | gemfile_size = @results.keys.map { |row| row.gemfile.size }.max 203 | ruby_size = @results.keys.map { |row| row.ruby.size }.max 204 | 205 | @results.each do |entry, result| 206 | puts "- #{entry.gemfile.ljust(gemfile_size)} Ruby #{entry.ruby.ljust(ruby_size)} #{result}" 207 | end 208 | 209 | puts 210 | 211 | if @compatible_count == 0 212 | message = "No gemfiles were compatible with Ruby #{@aliased_rubys[RUBY_VERSION]}" 213 | puts tint(message, COLOR_FAILURE) 214 | raise UnsupportedRuby, message 215 | elsif @all_passed 216 | puts tint("All gemfiles succeeded for Ruby #{@aliased_rubys[RUBY_VERSION]}", COLOR_SUCCESS) 217 | else 218 | message = 'Some gemfiles failed' 219 | puts tint(message, COLOR_FAILURE) 220 | puts 221 | raise MatrixFailed, message 222 | end 223 | 224 | print_aliases 225 | 226 | puts 227 | end 228 | 229 | def print_aliases 230 | @aliased_rubys.select { |used_version, alias_name| used_version != alias_name }.each do |used_version, alias_name| 231 | puts tint("Ruby #{alias_name} is an alias for Ruby #{used_version} in this environment.", COLOR_WARNING) 232 | end 233 | end 234 | 235 | end 236 | 237 | end 238 | -------------------------------------------------------------------------------- /spec/gemika/matrix_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' # can't move this to .rspec in RSpec 1 2 | 3 | describe Gemika::Matrix do 4 | 5 | before :each do 6 | @original_bundle_gemfile = ENV['BUNDLE_GEMFILE'] 7 | end 8 | 9 | after :each do 10 | # Make sure failing tests are not messing out our environment 11 | ENV['BUNDLE_GEMFILE'] = @original_bundle_gemfile 12 | end 13 | 14 | describe '#each' do 15 | 16 | it "calls the block with each matrix row, setting ENV['BUNDLE_GEMFILE'] to the respective gemfile" do 17 | current_ruby = '2.1.8' 18 | row1 = Gemika::Matrix::Row.new(:ruby => current_ruby, :gemfile => 'gemfiles/Gemfile1') 19 | row2 = Gemika::Matrix::Row.new(:ruby => current_ruby, :gemfile => 'gemfiles/Gemfile2') 20 | matrix = Gemika::Matrix.new(:rows =>[row1, row2], :validate => false, :current_ruby => current_ruby, :silent => true) 21 | spy = double('block') 22 | spy.should_receive(:observe_gemfile).with('gemfiles/Gemfile1') 23 | spy.should_receive(:observe_gemfile).with('gemfiles/Gemfile2') 24 | matrix.each do 25 | spy.observe_gemfile(ENV['BUNDLE_GEMFILE']) 26 | true 27 | end 28 | end 29 | 30 | it 'only calls the block with rows compatible with the current Ruby' do 31 | current_ruby = '2.1.8' 32 | other_ruby = '2.3.1' 33 | row1 = Gemika::Matrix::Row.new(:ruby => current_ruby, :gemfile => 'gemfiles/Gemfile1') 34 | row2 = Gemika::Matrix::Row.new(:ruby => other_ruby, :gemfile => 'gemfiles/Gemfile2') 35 | matrix = Gemika::Matrix.new(:rows =>[row1, row2], :validate => false, :current_ruby => current_ruby, :silent => true) 36 | spy = double('block') 37 | spy.should_receive(:observe_gemfile).with('gemfiles/Gemfile1') 38 | spy.should_not_receive(:observe_gemfile).with('gemfiles/Gemfile2') 39 | matrix.each do 40 | spy.observe_gemfile(ENV['BUNDLE_GEMFILE']) 41 | true 42 | end 43 | end 44 | 45 | it "resets ENV['BUNDLE GEMFILE'] to its initial value afterwards" do 46 | original_env = ENV['BUNDLE_GEMFILE'] 47 | original_env.should be_present 48 | current_ruby = '2.1.8' 49 | row = Gemika::Matrix::Row.new(:ruby => current_ruby, :gemfile => 'gemfiles/Gemfile1') 50 | matrix = Gemika::Matrix.new(:rows =>[row], :validate => false, :current_ruby => current_ruby, :silent => true) 51 | matrix.each { true } 52 | ENV['BUNDLE_GEMFILE'].should == original_env 53 | end 54 | 55 | it 'prints an overview of which gemfiles have passed, which have failed, which were skipped' do 56 | row1 = Gemika::Matrix::Row.new(:ruby => '2.1.8', :gemfile => 'gemfiles/GemfileAlpha') 57 | row2 = Gemika::Matrix::Row.new(:ruby => '2.1.8', :gemfile => 'gemfiles/GemfileBeta') 58 | row3 = Gemika::Matrix::Row.new(:ruby => '2.3.1', :gemfile => 'gemfiles/GemfileAlpha') 59 | require 'stringio' 60 | actual_output = '' 61 | io = StringIO.new(actual_output) 62 | matrix = Gemika::Matrix.new(:rows =>[row1, row2, row3], :validate => false, :current_ruby => '2.1.8', :io => io, :color => false) 63 | commands = [ 64 | lambda { io.puts 'Successful output'; true }, 65 | lambda { io.puts 'Failed output'; false }, 66 | lambda { io.puts 'Skipped output'; false } 67 | ] 68 | expect { matrix.each { commands.shift.call } }.to raise_error(Gemika::MatrixFailed) 69 | expected_output = < current_ruby, :gemfile => 'gemfiles/Gemfile') 92 | matrix = Gemika::Matrix.new(:rows =>[row], :validate => false, :current_ruby => current_ruby, :silent => true) 93 | expect { matrix.each { false } }.to raise_error(Gemika::MatrixFailed, /Some gemfiles failed/i) 94 | end 95 | 96 | it 'should raise an error if no row if compatible with the current Ruby' do 97 | current_ruby = '2.1.8' 98 | other_ruby = '2.3.1' 99 | row = Gemika::Matrix::Row.new(:ruby => other_ruby, :gemfile => 'gemfiles/Gemfile') 100 | matrix = Gemika::Matrix.new(:rows =>[row], :validate => false, :current_ruby => current_ruby, :silent => true) 101 | expect { matrix.each { false } }.to raise_error(Gemika::UnsupportedRuby, /No gemfiles were compatible/i) 102 | end 103 | 104 | end 105 | 106 | describe '.from_github_actions_yml' do 107 | 108 | it 'builds a matrix by combining Ruby versions and gemfiles from a Github Actions workflow configuration file' do 109 | path = 'spec/fixtures/github_actions_yml/two_by_two.yml' 110 | matrix = Gemika::Matrix.from_github_actions_yml(:path => path, :validate => false) 111 | matrix.rows.size.should == 4 112 | matrix.rows[0].ruby.should == '2.1.8' 113 | matrix.rows[0].gemfile.should == 'gemfiles/Gemfile1' 114 | matrix.rows[1].ruby.should == '2.1.8' 115 | matrix.rows[1].gemfile.should == 'gemfiles/Gemfile2' 116 | matrix.rows[2].ruby.should == '2.3.1' 117 | matrix.rows[2].gemfile.should == 'gemfiles/Gemfile1' 118 | matrix.rows[3].ruby.should == '2.3.1' 119 | matrix.rows[3].gemfile.should == 'gemfiles/Gemfile2' 120 | end 121 | 122 | it 'combines matrixes of multiple jobs' do 123 | path = 'spec/fixtures/github_actions_yml/multiple_jobs.yml' 124 | matrix = Gemika::Matrix.from_github_actions_yml(:path => path, :validate => false) 125 | matrix.rows.size.should == 2 126 | matrix.rows[0].ruby.should == '2.1.8' 127 | matrix.rows[0].gemfile.should == 'gemfiles/Gemfile1' 128 | matrix.rows[1].ruby.should == '2.3.1' 129 | matrix.rows[1].gemfile.should == 'gemfiles/Gemfile2' 130 | end 131 | 132 | it 'allows to exclude rows from the matrix' do 133 | path = 'spec/fixtures/github_actions_yml/excludes.yml' 134 | matrix = Gemika::Matrix.from_github_actions_yml(:path => path, :validate => false) 135 | matrix.rows.size.should == 3 136 | matrix.rows[0].ruby.should == '2.1.8' 137 | matrix.rows[0].gemfile.should == 'gemfiles/Gemfile2' 138 | matrix.rows[1].ruby.should == '2.3.1' 139 | matrix.rows[1].gemfile.should == 'gemfiles/Gemfile1' 140 | matrix.rows[2].ruby.should == '2.3.1' 141 | matrix.rows[2].gemfile.should == 'gemfiles/Gemfile2' 142 | end 143 | 144 | it 'allows to include rows to the matrix' do 145 | path = 'spec/fixtures/github_actions_yml/includes.yml' 146 | matrix = Gemika::Matrix.from_github_actions_yml(:path => path, :validate => false) 147 | matrix.rows.size.should == 6 148 | matrix.rows[0].ruby.should == '2.1.8' 149 | matrix.rows[0].gemfile.should == 'gemfiles/Gemfile1' 150 | matrix.rows[1].ruby.should == '2.1.8' 151 | matrix.rows[1].gemfile.should == 'gemfiles/Gemfile2' 152 | matrix.rows[2].ruby.should == '2.3.1' 153 | matrix.rows[2].gemfile.should == 'gemfiles/Gemfile1' 154 | matrix.rows[3].ruby.should == '2.3.1' 155 | matrix.rows[3].gemfile.should == 'gemfiles/Gemfile2' 156 | matrix.rows[4].ruby.should == '2.6.3' 157 | matrix.rows[4].gemfile.should == 'gemfiles/Gemfile3' 158 | matrix.rows[5].ruby.should == '2.7.1' 159 | matrix.rows[5].gemfile.should == 'gemfiles/Gemfile3' 160 | end 161 | 162 | it 'complains about missing keys' do 163 | path = 'spec/fixtures/github_actions_yml/invalid.yml' 164 | expect { Gemika::Matrix.from_github_actions_yml(:path => path) }.to raise_error(Gemika::InvalidMatrixDefinition) 165 | end 166 | 167 | it 'raises an error if a Gemfile does not exist' do 168 | path = 'spec/fixtures/github_actions_yml/missing_gemfile.yml' 169 | expect { Gemika::Matrix.from_github_actions_yml(:path => path) }.to raise_error(Gemika::MissingGemfile, /gemfile not found/i) 170 | end 171 | 172 | it 'raises an error if a Gemfile does not depend on "gemika"' do 173 | path = 'spec/fixtures/github_actions_yml/gemfile_without_gemika.yml' 174 | expect { Gemika::Matrix.from_github_actions_yml(:path => path) }.to raise_error(Gemika::UnusableGemfile, /missing gemika dependency/i) 175 | end 176 | 177 | it 'raises an error if no ci definition exists' do 178 | File.should_receive(:exist?).with('.github/workflows/test.yml').and_return(false) 179 | expect { Gemika::Matrix.from_ci_config }.to raise_error(Gemika::MissingMatrixDefinition) 180 | end 181 | end 182 | 183 | end 184 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gemika [![Tests](https://github.com/makandra/gemika/workflows/Tests/badge.svg)](https://github.com/makandra/gemika/actions) 2 | 3 | ## Test a Ruby gem against multiple versions of everything 4 | 5 | Gemika helps you test your gem against multiple versions of Ruby, gem dependencies and database types. 6 | 7 | ![Matrix task output](https://raw.githubusercontent.com/makandra/gemika/master/doc/minidusen_test.png) 8 | 9 | ## Features 10 | 11 | Here's what Gemika can give your test's development setup (all features are opt-in): 12 | 13 | - Test one codebase against multiple sets of runtime gem dependency sets (e.g. Rails 2.3, Rails 5.0). 14 | - Test one codebase against multiple Ruby versions (e.g. Ruby 1.8.7, Ruby 2.3.10). 15 | - Test one codebase against multiple database types (currently MySQL, PostgreSQL, or sqlite3). 16 | - Compute a matrix of all possible dependency permutations (Ruby, runtime gems, database type). Manually exclude incompatible dependency permutations (e.g. Rails 5.0 does not work with Ruby 2.1). 17 | - Let developers enter their local credentials for MySQL and PostgreSQL in a `database.yml` file. 18 | - Define default Ruby version, gem dependencies and database for developers who don't care about every possible permutation for everyday work. 19 | - Help configure a Github Actions build that tests every dependency permutation after each `git push`. 20 | - Share your Ruby / gem dependeny / database permutation between local development and Github Actions. 21 | - Define an [ActiveRecord database migration](http://api.rubyonrails.org/classes/ActiveRecord/Migration.html) that sets up your test database. 22 | - Automatically drop and re-create your test database before each run of your test suite. 23 | - Work around breaking changes in RSpec, Ruby and other gems 24 | 25 | 26 | ## Compatibility 27 | 28 | Gemika currently supports the following dependency versions: 29 | 30 | - Ruby: 2.5, 2.6, 2.7, 3.2, 3.4 31 | - RSpec: Versions 1, 2, 3 32 | - ActiveRecord: Versions 5.2, 6.1, 7.0, 8.0 33 | - Databases: PostgreSQL (with `pg` gem), MySQL or MariaDB (with `mysql2` gem), or sqlite3 (with `sqlite3` gem) 34 | 35 | Gemika also makes some assumption about your Gem: 36 | 37 | - You're testing with [RSpec](http://rspec.info/). 38 | - If you use any database-related features, you need `activerecord` as a development dependency 39 | 40 | 41 | ## Example directory structure 42 | 43 | Below you can see the directory of a gem with a completed Gemika testing setup. The next section describes how to get there: 44 | 45 | ```shell 46 | Gemfile.set1 # First dependency set. Should include development dependencies and gemika. 47 | Gemfile.set1.lock # Generated by `rake matrix:install` 48 | Gemfile.set2 # Second dependency set. Should include development dependencies and gemika. 49 | Gemfile.set2.lock # Generated by `rake matrix:install` 50 | Gemfile.set3 # Third dependency set. Should include development dependencies and gemika. 51 | Gemfile.set3.lock # Generated by `rake matrix:install` 52 | Gemfile -> Gemfile.set2 # Symlink to default Gemfile for development 53 | Gemfile.lock -> Gemfile.set2.lock # Symlink to default Gemfile.lock for development 54 | .github/workflows/test.yml # Configures all tested Ruby / gemfile combinations, for both local development and Github Actions 55 | .ruby-version # Default Ruby version for development 56 | .gitignore # Should ignore spec/support/database.yml 57 | my_gem.gemspec # Specification for your gem 58 | Rakefile # Should require 'gemika/tasks' 59 | README.md # README for your gem 60 | lib/my_gem.rb # Main file to require for your gem 61 | lib/my_gem/my_class.rb # Class delivered by your gem 62 | lib/my_gem/version.rb # Version definition for your gem 63 | spec/spec_helper.rb # Requires 'gemika' and all files in support folder 64 | spec/support/database.rb # Database schema for test database 65 | spec/support/database.yml # Database credentials for local development (not checked in) 66 | spec/support/database.sample.yml # Sample database credentials for new developers 67 | spec/support/database.github.yml # Database credentials for Github Actions 68 | spec/my_gem/my_class_spec.rb # Tests for your gem 69 | ``` 70 | 71 | For a live example of this setup, check the [makandra/minidusen](https://github.com/makandra/minidusen) repo. 72 | 73 | 74 | ## Step-by-step integration 75 | 76 | 77 | ### Have a standard gem setup 78 | 79 | Gemika expects a standard gem directory that looks roughly like this: 80 | 81 | ```shell 82 | my_gem.gemspec # Specification for your gem 83 | Rakefile # Rake tasks for your gem 84 | lib/my_gem.rb # Main file to require for your gem 85 | spec/my_gem_spec.rb # Tests for your gem 86 | ``` 87 | 88 | If you don't have a directory yet, you can [ask Bundler to create it for you](http://bundler.io/guides/rubygems.html): 89 | 90 | ``` 91 | bundle gem my_gem 92 | ``` 93 | 94 | ### Install Gemika 95 | 96 | Switch to your favorite Ruby version, then install Gemika: 97 | 98 | ```shell 99 | gem install gemika 100 | ``` 101 | 102 | Future contributors to your gem can install Gemika using the Gemfiles we will create next. 103 | 104 | 105 | ### Rake tasks 106 | 107 | Add this to your `Rakefile` to gain tasks `matrix:install`, `matrix:spec`, `matrix:update`. 108 | 109 | ```ruby 110 | begin 111 | require 'gemika/tasks' 112 | rescue LoadError 113 | puts 'Run `gem install gemika` for additional tasks' 114 | end 115 | ``` 116 | 117 | Check that the tasks appear with `rake -T`: 118 | 119 | ```shell 120 | rake current_rspec[files] # Run specs with the current RSpec version 121 | rake matrix:install # Install all Ruby 1.8.7 gemfiles 122 | rake matrix:list # List dependencies for all Ruby 1.8.7 gemfiles 123 | rake matrix:spec[files] # Run specs for all Ruby 1.8.7 gemfiles 124 | rake matrix:update[gems] # Update all Ruby 1.8.7 gemfiles 125 | ``` 126 | 127 | We also recommend to make `matrix:spec` the default task in your `Rakefile`: 128 | 129 | ```ruby 130 | task :default => 'matrix:spec' 131 | ``` 132 | 133 | ### Define multiple dependency sets 134 | 135 | For each combination of runtime and development dependencies (e.g. Rails + database), create a corresponding `Gemfile` in your project root. 136 | 137 | Each `Gemfile` should include: 138 | 139 | 1. The runtime dependencies you'd like to test against (e.g. Rails 5) 140 | 2. The development dependencies for that set (e.g. `rspec`) in a version that is compatible with these runtime dependencies. 141 | 3. The `gemika` gem 142 | 4. Your gem under test, from path `.` 143 | 144 | For instance, if one dependency set is Rails 4.2 with a MySQL database, we would create `./Gemfile.4.2.mysql2` with these contents: 145 | 146 | ```ruby 147 | source 'https://rubygems.org' 148 | 149 | # Runtime dependencies 150 | gem 'rails', '~>4.2.11' 151 | gem 'mysql2', '= 0.4.10' 152 | 153 | # Development dependencies 154 | gem 'rspec', '~> 3.4' 155 | gem 'rake' 156 | gem 'byebug' 157 | gem 'gemika' 158 | 159 | # Gem under test 160 | gem 'my_gem', :path => '.' 161 | ``` 162 | 163 | Repeat for other combinations (e.g. Rails 5.0 + PostgreSQL) 164 | 165 | 166 | ### Define Ruby/Gemfile Test Matrix 167 | 168 | Configure the test matrix in `.github/workflows/test.yml`, **even if you're not currently running tests on Github Actions**. 169 | 170 | Use [GitHub's matrix strategy:](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/running-variations-of-jobs-in-a-workflow) 171 | 172 | ```yaml 173 | jobs: 174 | my_job: 175 | strategy: 176 | matrix: 177 | ruby: 178 | - 2.1.8 179 | - 2.2.4 180 | - 2.3.1 181 | gemfile: 182 | - Gemfile.3.2.mysql2 183 | - Gemfile.4.2.mysql2 184 | - Gemfile.4.2.pg 185 | - Gemfile.5.0.mysql2 186 | - Gemfile.5.0.pg 187 | env: 188 | BUNDLE_GEMFILE: "${{ matrix.gemfile }}" 189 | steps: 190 | - uses: actions/checkout@v4 191 | - name: Install ruby 192 | uses: ruby/setup-ruby@v1 193 | with: 194 | ruby-version: "${{ matrix.ruby }}" 195 | - ... 196 | ``` 197 | 198 | There might be incompatible combinations of gemfiles and Rubies, e.g. Rails 5.0 does not work with Ruby 2.1 or lower. In this case, add an `exclude` key to your matrix in `.github/workflows/test.yml` 199 | 200 | ```yaml 201 | jobs: 202 | my_job: 203 | strategy: 204 | matrix: 205 | ruby: 206 | - 2.1.8 207 | - 2.3.1 208 | gemfile: 209 | - Gemfile.4.2.mysql2 210 | - Gemfile.4.2.pg 211 | - Gemfile.5.0.mysql2 212 | - Gemfile.5.0.pg 213 | exclude: 214 | - gemfile: Gemfile.5.0.mysql2 215 | ruby: 2.1.8 216 | - gemfile: Gemfile.5.0.pg 217 | ruby: 2.1.8 218 | ``` 219 | 220 | Alternatively, you can explicitly list all Ruby / Gemfile combinations with 221 | 222 | ``` 223 | matrix: 224 | include: 225 | - gemfile: Gemfile.5.0.mysql2 226 | ruby: 2.3.8 227 | - gemfile: Gemfile.5.2.mysql2 228 | ruby: 2.3.8 229 | ``` 230 | 231 | ### Generate lockfiles 232 | 233 | Generate lockfiles for each Gemfile: 234 | 235 | ```shell 236 | rake matrix:install 237 | ``` 238 | 239 | In this example, your project directory should now contain a lockfile for each gemfile: 240 | 241 | ``` 242 | Gemfile.4.2.mysql2 243 | Gemfile.4.2.mysql2.lock 244 | Gemfile.5.0.pg 245 | Gemfile.5.0.pg.lock 246 | ``` 247 | 248 | Gemfiles and lockfiles should be committed to your repo. 249 | 250 | Make sure to re-run `rake matrix:install` after each change to your gemfiles, and commit the generated changes. 251 | 252 | 253 | ### Default Ruby and default gemfile 254 | 255 | Your project will be more approachable if you're defining a default Ruby and dependency set. This way a developer can make changes and run code without knowing about the test matrix. 256 | 257 | Create a `.ruby-version` file with the default Ruby version: 258 | 259 | ``` 260 | 2.2.4 261 | ``` 262 | 263 | Choose a default dependency set and symlink both gemfile and lockfile to your project root: 264 | 265 | ``` 266 | ln -s Gemfile.4.2.mysql2 Gemfile 267 | ln -s Gemfile.4.2.mysql2.lock Gemfile.lock 268 | ``` 269 | 270 | Commit both `.ruby-version` and symlinks to your repo. 271 | 272 | ### Configure Test databases 273 | 274 | Create a local test database (e.g. `my_gem_test`) for MySQL/PostgreSQL/etc. 275 | 276 | Then add credentials to `spec/support/database.yml`: 277 | 278 | ```yaml 279 | mysql: 280 | database: my_gem_test 281 | host: localhost 282 | username: root 283 | password: secret 284 | 285 | postgresql: 286 | database: minidusen_test 287 | user: 288 | password: 289 | 290 | sqlite: 291 | database: ":memory:" 292 | ``` 293 | 294 | We don't want to commit our local credentials, so add a line to your `.gitignore`: 295 | 296 | ``` 297 | spec/support/database.yml 298 | ``` 299 | 300 | What we *will* commit is a `database.sample.yml` as a template for future contributors: 301 | 302 | ``` 303 | cp spec/support/database.yml spec/support/database.sample.yml 304 | ``` 305 | 306 | Remember to replace any private passwords in `database.sample.yml` with `secret` before committing. 307 | 308 | To have ActiveRecord connect to the database in `database.yml` before your tests, add a file `spec/support/database.rb` with the following content: 309 | 310 | ``` 311 | database = Gemika::Database.new 312 | database.connect 313 | ``` 314 | 315 | Now require Gemika and this support file from your `spec_helper.rb`. 316 | 317 | ``` 318 | require 'gemika' 319 | require 'spec/support/database' 320 | ``` 321 | 322 | Protip: Instead of requiring support files indidually, configure your `spec_helper.rb` to automatically `require` all files in the `spec/support` folder: 323 | 324 | ```ruby 325 | Dir["#{File.dirname(__FILE__)}/support/*.rb"].sort.each {|f| require f} 326 | ``` 327 | 328 | Now you have a great place for code snippets that need to run before specs (factories, VCR configuration, etc.). 329 | 330 | 331 | To have your database work with Github Actions, add a database file `spec/support/database.github.yml`. 332 | 333 | ``` 334 | mysql: 335 | database: test 336 | username: root 337 | password: password 338 | host: 127.0.0.1 339 | port: 3306 340 | 341 | postgresql: 342 | database: test 343 | host: localhost 344 | username: postgres 345 | password: postgres 346 | port: 5432 347 | ``` 348 | 349 | 350 | #### Test database schema 351 | 352 | If your gem is talking to the database, you probably need to create some example tables. 353 | 354 | Gemika lets you define an [ActiveRecord database migration](http://api.rubyonrails.org/classes/ActiveRecord/Migration.html) for that. Before your test suite runs, Gemika will drop *all* tables in your test database and recreate them using this migration. 355 | 356 | Add your migration to your `spec/support/database.rb` (created and required above): 357 | 358 | ```ruby 359 | 360 | database = Gemika::Database.new 361 | database.connect 362 | database.rewrite_schema! do 363 | 364 | create_table :users do |t| 365 | t.string :name 366 | t.string :email 367 | t.string :city 368 | end 369 | 370 | create_table :recipes do |t| 371 | t.string :name 372 | t.integer :category_id 373 | end 374 | 375 | create_table :recipe_ingredients do |t| 376 | t.string :name 377 | t.integer :recipe_id 378 | end 379 | 380 | create_table :recipe_categories do |t| 381 | t.string :name 382 | end 383 | 384 | end 385 | ``` 386 | 387 | #### Clean database before each test 388 | 389 | A very useful Rails default is to wrap every test in a transaction that is rolled back when the example ends. This way each example starts with a blank database. 390 | 391 | To get the same behavior in your gem tests, add `database_cleaner` as a development dependency to all your gemfiles: 392 | 393 | ```ruby 394 | gem 'database_cleaner' 395 | ``` 396 | 397 | If you don't want to configure `database_cleaner` manually, you can ask Gemika to clean the database before each example: 398 | 399 | ```ruby 400 | Gemika::RSpec.configure_clean_database_before_example 401 | ``` 402 | 403 | Note that you also need `require 'gemika'` in your `spec_helper.rb`. 404 | 405 | 406 | ### Try it out 407 | 408 | Check if you can install development dependencies for each row in the test matrix: 409 | 410 | ```shell 411 | bundle exec rake matrix:install 412 | ``` 413 | 414 | Check if you can run tests for each row in the test matrix: 415 | 416 | ```shell 417 | bundle exec rake matrix:spec 418 | ``` 419 | 420 | To only run some examples, put the list of files in square brackets (it's a Rake thing): 421 | 422 | ```shell 423 | bundle exec rake matrix:spec[spec/foo_spec.rb:1005] 424 | ``` 425 | 426 | You should see the command output for each row in the test matrix. Gemika will also print a summary at the end: 427 | 428 | ![Matrix task output](https://raw.githubusercontent.com/makandra/gemika/master/doc/minidusen_test.png) 429 | 430 | If you now discover compatibility issue with your library, see below how Gemika can help you [bridge incompatibilities between dependency sets](#bridging-incompatibilities-between-dependency-sets). 431 | 432 | 433 | ### Running specs in multiple Ruby versions 434 | 435 | Note that there is no task for automatically running all gemfiles in all Ruby versions. We had something like this in earlier versions of Gemika and it wasn't as practical as we thought. 436 | 437 | Instead you need to manually switch Ruby versions and re-run: 438 | 439 | ```shell 440 | rake matrix:install 441 | rake matrix:spec 442 | ``` 443 | 444 | Note that if your current Ruby version is *very* far away from your [default Ruby](#default-ruby-and-default-gemfile) in `.ruby-version`, you might need to run `rake` with a gemfile that has compatible dependencies: 445 | 446 | ```shell 447 | BUNDLE_GEMFILE=Gemfile.2.3 bundle exec rake matrix:install 448 | BUNDLE_GEMFILE=Gemfile.2.3 bundle exec rake matrix:spec 449 | ``` 450 | 451 | We recommend to setup Github Actions to check the entire test matrix after each push, including all Rubies. This way developers can stay on the [default Ruby and gemfile](#default-ruby-and-default-gemfile) most of the time while the pipeline checks make sure that nothing broken gets merged. 452 | 453 | 454 | ## Activate Github Actions 455 | 456 | We recommend to setup Github Actions to check the entire test matrix after each push. This will also show the test results on a pull request's page, helping maintainers decide whether a PR is safe to merge. 457 | 458 | 459 | ## Add development instructions to your README 460 | 461 | Your README should contain instructions how to run tests before making a PR. We recommend to add a section like the one below to your `README.md`: 462 | 463 | ```markdown 464 | ## Development 465 | 466 | There are tests in `spec`. We only accept PRs with tests. To run tests: 467 | 468 | - Install Ruby x.y.z 469 | - Create a local test database `my_gem_test` in both MySQL and PostgreSQL 470 | - Copy `spec/support/database.sample.yml` to `spec/support/database.yml` and enter your local credentials for the test databases 471 | - Install development dependencies using `bundle install` 472 | - Run tests using `bundle exec rspec` 473 | 474 | We recommend to test large changes against multiple versions of Ruby and multiple dependency sets. Supported combinations are configured in `.github/workflows/test.yml`. We provide some rake tasks to help with this: 475 | 476 | - Install development dependencies using `bundle matrix:install` 477 | - Run tests using `bundle matrix:spec` 478 | 479 | Note that we have configured Github Actions to automatically run tests in all supported Ruby versions and dependency sets after each push. We will only merge pull requests after a green build. 480 | ``` 481 | 482 | Adjust the first part to match what you chose as your [default Ruby and default gemfile](#default-ruby-and-dependency-set). 483 | 484 | 485 | Bridging incompatibilities between dependency sets 486 | --------------------------------------------------- 487 | 488 | Gemika can help you bridge incompatibilities or breaking changes between Ruby versions, gem versions, or RSpec. 489 | 490 | 491 | ### Version switches 492 | 493 | Check if a gem was activated by the current gemfile: 494 | 495 | ```ruby 496 | Gemika::Env.gem?('activesupport') 497 | ``` 498 | 499 | Check if a gem was activated and satisfies a version requirement: 500 | 501 | ```ruby 502 | Gemika::Env.gem?('activesupport', '>= 5') 503 | Gemika::Env.gem?('activesupport', '~> 5.0.0') 504 | Gemika::Env.gem?('activesupport', '< 5') 505 | ``` 506 | 507 | Check if the current Ruby version satisfies a version requirement: 508 | 509 | ```ruby 510 | Gemika::Env.ruby?('>= 2') 511 | Gemika::Env.ruby?('< 2') 512 | Gemika::Env.ruby?('~> 2.1.0') 513 | ``` 514 | 515 | Check if the process is running as a Github Actions test 516 | 517 | ```ruby 518 | Gemika::Env.github? 519 | ``` 520 | 521 | ### RSpec 1 vs. RSpec 2+ 522 | 523 | If you're testing gems against Rails 2.3 or Ruby 1.8.7 you might need to test with RSpec 1. There are a lot of differences between RSpec 1 and later versions, which Gemika helps to pave over. 524 | 525 | Configuring RSpec requires you to work on a different module in RSpec 1 (`Spec::Runner`) and RSpec 2 (just `RSpec`). The following works for all RSpec versions: 526 | 527 | ```ruby 528 | Gemika::RSpec.configure do |config| 529 | 530 | config.before(:each) do 531 | # runs before each example 532 | end 533 | 534 | end 535 | ``` 536 | 537 | When your tests need to run with RSpec 1, you need to use the old `should` syntax, which works across all RSpec versions. 538 | 539 | To enable this `should` syntax for later RSpecs: 540 | 541 | ```ruby 542 | Gemika::RSpec.configure_should_syntax 543 | ``` 544 | 545 | RSpec 1 has a binary `spec`, while later RSpecs use `rspec`. To call the correct binary for the current gemfile: 546 | 547 | ```shell 548 | rake current_rspec 549 | ``` 550 | 551 | 552 | Development 553 | ----------- 554 | 555 | Here are some hints when you try to make changes to Gemika itself: 556 | 557 | There are tests in `spec`. We only accept PRs with tests. If you create a PR, the tests will automatically run on 558 | GitHub actions on each push. We will only merge pull requests after a green GitHub actions run. 559 | 560 | To run tests locally for development, first setup your test databases: 561 | 562 | - Create a local test database `gemika_test` in both MySQL and PostgreSQL 563 | - Copy `spec/support/database.sample.yml` to `spec/support/database.yml` and enter your local credentials for the test databases 564 | 565 | Afterwards you have multiple options: 566 | 567 | 1. Run tests against the "main development" Ruby version (`.ruby-version`) and dependencies (`Gemfile`/`Gemfile.lock` symlinks): 568 | - Install the Ruby version specified in `.ruby-version` 569 | - Install development dependencies using `bundle install` 570 | - Run tests using `bundle exec rspec` 571 | 572 | 2. Run tests against a specific Ruby version (out of those mentioned in `.github/workflows/test.yml`) and all Gemfiles compatible with that version: 573 | - Install and switch to the Ruby version 574 | - Install development dependencies for all compatible Gemfiles using `rake matrix:install` 575 | - Run tests for all compatible Gemfiles using `rake matrix:spec` 576 | 577 | 3. Run tests against all compatible combinations of Ruby and Gemfile: 578 | - Install all Ruby versions mentioned in `.github/workflows/test.yml` 579 | - run `bin/matrix` (only supports `rbenv` for switching Ruby versions currently) 580 | 581 | Hints: 582 | - We recommend to have sufficiently new versions of bundler (> 2.3.0) and rubygems (> 3.3.0) installed for each Ruby version. 583 | - The script `bin/matrix` will warn you, if that is not the case. For all other methods you need to ensure that yourself. 584 | - Supported "Ruby <-> Gemfile" combinations are configured in `.github/workflows/test.yml`. 585 | 586 | Credits 587 | ------- 588 | 589 | Henning Koch from [makandra](http://makandra.com/) 590 | --------------------------------------------------------------------------------