├── .github └── workflows │ ├── main.yml │ └── release.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── .ruby-version ├── .simplecov ├── .test-map.yml ├── CHANGELOG.md ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── examples ├── minitest │ ├── .test-map.yml │ ├── .test-map.yml.sha1 │ ├── Gemfile │ ├── Gemfile.lock │ ├── Rakefile │ ├── animal.rb │ ├── cat.rb │ ├── dog.rb │ └── test │ │ ├── animal_test.rb │ │ ├── cat_test.rb │ │ ├── dog_test.rb │ │ └── test_helper.rb └── rspec │ ├── .rspec │ ├── .test-map.yml │ ├── .test-map.yml.sha1 │ ├── Gemfile │ ├── Gemfile.lock │ ├── Rakefile │ ├── animal.rb │ ├── cat.rb │ ├── dog.rb │ └── spec │ ├── animal_spec.rb │ ├── cat_spec.rb │ ├── dog_spec.rb │ └── spec_helper.rb ├── lib ├── test_map.rb └── test_map │ ├── config.rb │ ├── errors.rb │ ├── file_recorder.rb │ ├── filter.rb │ ├── mapping.rb │ ├── natural_mapping.rb │ ├── plugins │ ├── minitest.rb │ └── rspec.rb │ ├── report.rb │ ├── test_task.rb │ └── version.rb ├── test-map.gemspec └── test ├── fixtures ├── sample.rb └── test-map.yml ├── test_helper.rb ├── test_map ├── config_test.rb ├── errors_test.rb ├── file_recorder_test.rb ├── filter_test.rb ├── mapping_test.rb ├── natural_mapping_test.rb └── report_test.rb └── test_map_test.rb /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: Main Pipeline 4 | 5 | on: 6 | push: 7 | branches: 8 | - main 9 | pull_request: 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-latest 14 | name: Rubocop 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Set up Ruby 18 | uses: ruby/setup-ruby@v1 19 | with: 20 | bundler-cache: true 21 | - name: Run rubocop 22 | run: bundle exec rubocop 23 | 24 | test: 25 | runs-on: ubuntu-latest 26 | name: Ruby ${{ matrix.ruby }} 27 | strategy: 28 | matrix: 29 | ruby: 30 | - '3.2' 31 | - '3.3' 32 | steps: 33 | - uses: actions/checkout@v4 34 | - name: Set up Ruby 35 | uses: ruby/setup-ruby@v1 36 | with: 37 | ruby-version: ${{ matrix.ruby }} 38 | bundler-cache: true 39 | - name: Run testsuite 40 | run: bundle exec rake 41 | 42 | verify: 43 | runs-on: ubuntu-latest 44 | name: Verify 45 | strategy: 46 | matrix: 47 | plugin: 48 | - minitest 49 | - rspec 50 | steps: 51 | - uses: actions/checkout@v4 52 | - name: Set up Ruby 53 | uses: ruby/setup-ruby@v1 54 | - name: Run example testsuite 55 | run: | 56 | cd examples/${{ matrix.plugin }} 57 | rm -f .test-map.yml 58 | bundle install 59 | bundle exec rake 60 | sha1sum --check .test-map.yml.sha1 61 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: Release Pipeline 4 | 5 | on: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | release: 10 | if: github.ref == 'refs/heads/main' 11 | # needs: [lint, test, verify] 12 | name: Push gem to RubyGems.org 13 | runs-on: ubuntu-latest 14 | permissions: 15 | id-token: write 16 | contents: write 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Set up Ruby 20 | uses: ruby/setup-ruby@v1 21 | with: 22 | bundler-cache: true 23 | ruby-version: ruby 24 | - uses: rubygems/release-gem@v1 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /pkg/ 3 | /coverage/ 4 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | --- 2 | require: 3 | - rubocop-minitest 4 | - rubocop-rake 5 | 6 | AllCops: 7 | NewCops: enable 8 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.2.2 2 | -------------------------------------------------------------------------------- /.simplecov: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | SimpleCov.start do 4 | add_filter '/test/' 5 | enable_coverage :branch 6 | end 7 | -------------------------------------------------------------------------------- /.test-map.yml: -------------------------------------------------------------------------------- 1 | --- 2 | lib/test_map/config.rb: 3 | - test/test_map/config_test.rb 4 | - test/test_map/file_recorder_test.rb 5 | - test/test_map/filter_test.rb 6 | - test/test_map/mapping_test.rb 7 | lib/test_map/errors.rb: 8 | - test/test_map/errors_test.rb 9 | - test/test_map/file_recorder_test.rb 10 | lib/test_map/file_recorder.rb: 11 | - test/test_map/config_test.rb 12 | - test/test_map/errors_test.rb 13 | - test/test_map/file_recorder_test.rb 14 | - test/test_map/filter_test.rb 15 | - test/test_map/mapping_test.rb 16 | - test/test_map/natural_mapping_test.rb 17 | - test/test_map/report_test.rb 18 | - test/test_map_test.rb 19 | lib/test_map/filter.rb: 20 | - test/test_map/file_recorder_test.rb 21 | - test/test_map/filter_test.rb 22 | lib/test_map/mapping.rb: 23 | - test/test_map/mapping_test.rb 24 | lib/test_map/natural_mapping.rb: 25 | - test/test_map/mapping_test.rb 26 | - test/test_map/natural_mapping_test.rb 27 | lib/test_map/plugins/minitest.rb: 28 | - test/test_map/config_test.rb 29 | - test/test_map/errors_test.rb 30 | - test/test_map/file_recorder_test.rb 31 | - test/test_map/filter_test.rb 32 | - test/test_map/mapping_test.rb 33 | - test/test_map/natural_mapping_test.rb 34 | - test/test_map/report_test.rb 35 | - test/test_map_test.rb 36 | lib/test_map/report.rb: 37 | - test/test_map/report_test.rb 38 | test/fixtures/sample.rb: 39 | - test/test_map/file_recorder_test.rb 40 | test/test_map/config_test.rb: 41 | - test/test_map/config_test.rb 42 | test/test_map/file_recorder_test.rb: 43 | - test/test_map/file_recorder_test.rb 44 | test/test_map/filter_test.rb: 45 | - test/test_map/filter_test.rb 46 | test/test_map/mapping_test.rb: 47 | - test/test_map/mapping_test.rb 48 | test/test_map/natural_mapping_test.rb: 49 | - test/test_map/natural_mapping_test.rb 50 | test/test_map/report_test.rb: 51 | - test/test_map/report_test.rb 52 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # Changelog 3 | 4 | All notable changes to this project will be documented in this file. 5 | 6 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 7 | and this project adheres to [Semantic Versioning](http://semver.org/). 8 | 9 | ## 0.2.0 - 2024-10-25 10 | 11 | Provide explicit Test Task via Rake for Minitest, Rspec, and Rails. Extend 12 | documentation. 13 | 14 | ## 0.1.0 - 2024-09-22 15 | 16 | Initial release. 17 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Specify your gem's dependencies in test-map.gemspec 6 | gemspec 7 | 8 | gem 'minitest', '~> 5.25' 9 | gem 'pry', '~> 0.14.2' 10 | gem 'rake', '~> 13.0' 11 | gem 'rubocop', '~> 1.66' 12 | gem 'rubocop-minitest', '~> 0.36.0' 13 | gem 'rubocop-rake', '~> 0.6.0' 14 | gem 'simplecov', '~> 0.22.0', require: false 15 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | test-map (0.2.1) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | ast (2.4.2) 10 | coderay (1.1.3) 11 | docile (1.4.1) 12 | json (2.7.2) 13 | language_server-protocol (3.17.0.3) 14 | method_source (1.1.0) 15 | minitest (5.25.1) 16 | parallel (1.26.3) 17 | parser (3.3.5.0) 18 | ast (~> 2.4.1) 19 | racc 20 | pry (0.14.2) 21 | coderay (~> 1.1) 22 | method_source (~> 1.0) 23 | racc (1.8.1) 24 | rainbow (3.1.1) 25 | rake (13.2.1) 26 | regexp_parser (2.9.2) 27 | rubocop (1.66.1) 28 | json (~> 2.3) 29 | language_server-protocol (>= 3.17.0) 30 | parallel (~> 1.10) 31 | parser (>= 3.3.0.2) 32 | rainbow (>= 2.2.2, < 4.0) 33 | regexp_parser (>= 2.4, < 3.0) 34 | rubocop-ast (>= 1.32.2, < 2.0) 35 | ruby-progressbar (~> 1.7) 36 | unicode-display_width (>= 2.4.0, < 3.0) 37 | rubocop-ast (1.32.3) 38 | parser (>= 3.3.1.0) 39 | rubocop-minitest (0.36.0) 40 | rubocop (>= 1.61, < 2.0) 41 | rubocop-ast (>= 1.31.1, < 2.0) 42 | rubocop-rake (0.6.0) 43 | rubocop (~> 1.0) 44 | ruby-progressbar (1.13.0) 45 | simplecov (0.22.0) 46 | docile (~> 1.1) 47 | simplecov-html (~> 0.11) 48 | simplecov_json_formatter (~> 0.1) 49 | simplecov-html (0.13.1) 50 | simplecov_json_formatter (0.1.4) 51 | unicode-display_width (2.6.0) 52 | 53 | PLATFORMS 54 | ruby 55 | x86_64-linux 56 | 57 | DEPENDENCIES 58 | minitest (~> 5.25) 59 | pry (~> 0.14.2) 60 | rake (~> 13.0) 61 | rubocop (~> 1.66) 62 | rubocop-minitest (~> 0.36.0) 63 | rubocop-rake (~> 0.6.0) 64 | simplecov (~> 0.22.0) 65 | test-map! 66 | 67 | BUNDLED WITH 68 | 2.5.9 69 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Christoph Lipautz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Test-Map 3 | 4 | Track associated files of executed tests to optimize test execution on file 5 | changes. 6 | 7 | Test-Map results in a file that maps test files to the files they depend on. 8 | You can use this file to run only the tests that are affected by a file change. 9 | This is useful when you have a large test suite and want to optimize the time 10 | spent running tests. Submit a change request and only run tests that depend on 11 | what you changed. Optimizing in such way, the time spent waiting for CI to 12 | verify can be reduced to seconds. 13 | 14 | ## Usage 15 | 16 | Add test-map to your Gemfile. 17 | 18 | ```sh 19 | $ bundle add test-map 20 | ``` 21 | 22 | ### Minitest 23 | 24 | Include test-map in your test helper. Typically you want to include it 25 | conditionally so it only generates the test map when needed. 26 | 27 | ```ruby 28 | # filename: test/test_helper.rb 29 | 30 | # Include test-map after minitest has been required 31 | require 'test_map' if ENV['TEST_MAP'] 32 | ``` 33 | 34 | Run your tests with the `TEST_MAP` environment variable set. 35 | 36 | ```sh 37 | $ TEST_MAP=1 bundle exec ruby -Itest test/models/user_test.rb 38 | # or 39 | $ TEST_MAP=1 bundle exec rake test 40 | ``` 41 | 42 | Using the a dedicated rake task you can connect a file watcher and trigger 43 | tests on file changes. 44 | 45 | ```ruby 46 | # filename: Rakefile 47 | require 'test_map/test_task' 48 | 49 | TestMap::TestTask.create 50 | ``` 51 | 52 | Using [entr](https://eradman.com/entrproject/) as example file watcher. 53 | 54 | ```sh 55 | # find all ruby files | watch them, postpone first execution, clear screen 56 | # with every run and on file change run test suite for the changed file 57 | # (placeholder /_). 58 | $ find . -name "*.rb" | entr -cp bundle exec rake test:changes /_ 59 | ``` 60 | 61 | ### Rspec 62 | 63 | Include test-map in your test helper. Typically you want to include it 64 | conditionally so it only generates the test map when needed. 65 | 66 | ```ruby 67 | # filename: spec/spec_helper.rb 68 | require 'test_map' if ENV['TEST_MAP'] 69 | ``` 70 | 71 | Run your tests with the `TEST_MAP` environment variable set. 72 | 73 | ```sh 74 | $ TEST_MAP=1 bundle exec rspec 75 | ``` 76 | 77 | ## Configuration 78 | 79 | On demand you can adapt the configuration to your needs. 80 | 81 | ```ruby 82 | TestMap::Config.configure do |config| 83 | config[:logger] = Logger.new($stdout) # default logs to dev/null 84 | config[:merge] = false # merge results (e.g. with multiple testsuites) 85 | config[:out_file] = 'my-test-map.yml' # default is .test-map.yml 86 | # defaults to [%r{^(vendor)/}] } 87 | config[:exclude_patterns] = [%r{^(vendor|other_libraries)/}] 88 | # register a custom rule to match new files; must implement `call(file)`; 89 | # defaults to nil 90 | config[:natural_mapping] = ->(file) { file.sub(%r{^library/}, 'test/') } 91 | end 92 | ``` 93 | 94 | ## Development 95 | 96 | Open list of features: 97 | 98 | - [x] Configure file exclude list (e.g. test files are not needed). 99 | - [ ] Auto-handle packs, packs with subdirectories. 100 | - [x] Demonstrate usage with file watchers. 101 | - [ ] Demonstrate CI pipelines with GitHub actions and GitLab CI. 102 | - [x] Merge results. 103 | 104 | ```sh 105 | $ bundle install # install dependencies 106 | $ bundle exec rake # run testsuite 107 | $ bundle exec rubocop # run linter 108 | ``` 109 | 110 | ## Contributing 111 | 112 | Bug reports and pull requests are very welcome on 113 | [GitHub](https://github.com/unused/test-map). 114 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | require 'minitest/test_task' 5 | require_relative 'lib/test_map/test_task' 6 | 7 | Minitest::TestTask.create 8 | TestMap::TestTask.create 9 | 10 | task default: :test 11 | -------------------------------------------------------------------------------- /examples/minitest/.test-map.yml: -------------------------------------------------------------------------------- 1 | --- 2 | animal.rb: 3 | - test/animal_test.rb 4 | - test/cat_test.rb 5 | - test/dog_test.rb 6 | cat.rb: 7 | - test/cat_test.rb 8 | dog.rb: 9 | - test/dog_test.rb 10 | -------------------------------------------------------------------------------- /examples/minitest/.test-map.yml.sha1: -------------------------------------------------------------------------------- 1 | 59c4886762069184127e6714dd94938250d87bf6 .test-map.yml 2 | -------------------------------------------------------------------------------- /examples/minitest/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gem 'minitest', '~> 5.25' 6 | gem 'rake', '~> 13.2' 7 | gem 'test-map', path: '../../' 8 | -------------------------------------------------------------------------------- /examples/minitest/Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: ../.. 3 | specs: 4 | test-map (0.1.0) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | minitest (5.25.1) 10 | rake (13.2.1) 11 | 12 | PLATFORMS 13 | ruby 14 | x86_64-linux 15 | 16 | DEPENDENCIES 17 | minitest (~> 5.25) 18 | rake (~> 13.2) 19 | test-map! 20 | 21 | BUNDLED WITH 22 | 2.5.9 23 | -------------------------------------------------------------------------------- /examples/minitest/Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'minitest/test_task' 4 | require 'test_map/test_task' 5 | 6 | # require 'rake/testtask' 7 | # Rake::TestTask.new do |t| 8 | # t.libs << 'test' 9 | # t.test_files = FileList['test/**/*_test.rb'] 10 | # end 11 | Minitest::TestTask.create 12 | TestMap::TestTask.create 13 | 14 | task default: :test 15 | -------------------------------------------------------------------------------- /examples/minitest/animal.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Animal class 4 | class Animal 5 | def speak = raise(NotImplementedError, 'You must implement the speak method') 6 | def kind = String(self.class).downcase 7 | end 8 | -------------------------------------------------------------------------------- /examples/minitest/cat.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Cat class 4 | class Cat < Animal 5 | def speak = 'Miau' 6 | end 7 | -------------------------------------------------------------------------------- /examples/minitest/dog.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Dog class 4 | class Dog < Animal 5 | def speak = 'Wuff' 6 | end 7 | -------------------------------------------------------------------------------- /examples/minitest/test/animal_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | # Test for Animal class 6 | class AnimalTest < Minitest::Test 7 | def test_speak 8 | assert_raises(NotImplementedError) { Animal.new.speak } 9 | end 10 | 11 | def test_kind 12 | assert_equal 'animal', Animal.new.kind 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /examples/minitest/test/cat_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | # Test for Animal class 6 | class CatTest < Minitest::Test 7 | def test_speak 8 | assert_equal 'Miau', Cat.new.speak 9 | end 10 | 11 | def test_kind 12 | assert_equal 'cat', Cat.new.kind 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /examples/minitest/test/dog_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | # Test for Animal class 6 | class DogTest < Minitest::Test 7 | def test_speak 8 | assert_equal 'Wuff', Dog.new.speak 9 | end 10 | 11 | def test_kind 12 | assert_equal 'dog', Dog.new.kind 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /examples/minitest/test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'minitest/autorun' 4 | require 'test_map' # if ENV['TEST_MAP'] 5 | 6 | require_relative '../animal' 7 | require_relative '../cat' 8 | require_relative '../dog' 9 | 10 | # Output log messages to the console on demand. 11 | TestMap::Config.configure do |config| 12 | config[:logger] = Logger.new($stdout, level: :info) 13 | end 14 | 15 | # Run tests with `ruby -Itest test/*_test.rb`. 16 | -------------------------------------------------------------------------------- /examples/rspec/.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | -------------------------------------------------------------------------------- /examples/rspec/.test-map.yml: -------------------------------------------------------------------------------- 1 | --- 2 | animal.rb: 3 | - spec/animal_spec.rb 4 | - spec/cat_spec.rb 5 | - spec/dog_spec.rb 6 | cat.rb: 7 | - spec/cat_spec.rb 8 | dog.rb: 9 | - spec/dog_spec.rb 10 | -------------------------------------------------------------------------------- /examples/rspec/.test-map.yml.sha1: -------------------------------------------------------------------------------- 1 | bb867c45a71aa87cc5fa98d5c3e8e7fa86a51c5e .test-map.yml 2 | -------------------------------------------------------------------------------- /examples/rspec/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gem 'rake', '~> 13.2' 6 | gem 'rspec', '~> 3.13' 7 | gem 'test-map', path: '../../' 8 | -------------------------------------------------------------------------------- /examples/rspec/Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: ../.. 3 | specs: 4 | test-map (0.1.0) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | diff-lcs (1.5.1) 10 | rake (13.2.1) 11 | rspec (3.13.0) 12 | rspec-core (~> 3.13.0) 13 | rspec-expectations (~> 3.13.0) 14 | rspec-mocks (~> 3.13.0) 15 | rspec-core (3.13.1) 16 | rspec-support (~> 3.13.0) 17 | rspec-expectations (3.13.3) 18 | diff-lcs (>= 1.2.0, < 2.0) 19 | rspec-support (~> 3.13.0) 20 | rspec-mocks (3.13.1) 21 | diff-lcs (>= 1.2.0, < 2.0) 22 | rspec-support (~> 3.13.0) 23 | rspec-support (3.13.1) 24 | 25 | PLATFORMS 26 | ruby 27 | x86_64-linux 28 | 29 | DEPENDENCIES 30 | rake (~> 13.2) 31 | rspec (~> 3.13) 32 | test-map! 33 | 34 | BUNDLED WITH 35 | 2.5.9 36 | -------------------------------------------------------------------------------- /examples/rspec/Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rspec/core/rake_task' 4 | 5 | RSpec::Core::RakeTask.new do |t| 6 | t.pattern = 'spec/**/*.rb' 7 | end 8 | 9 | task default: :spec 10 | -------------------------------------------------------------------------------- /examples/rspec/animal.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Animal class 4 | class Animal 5 | def speak = raise(NotImplementedError, 'You must implement the speak method') 6 | def kind = String(self.class).downcase 7 | end 8 | -------------------------------------------------------------------------------- /examples/rspec/cat.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Cat class 4 | class Cat < Animal 5 | def speak = 'Miau' 6 | end 7 | -------------------------------------------------------------------------------- /examples/rspec/dog.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Dog class 4 | class Dog < Animal 5 | def speak = 'Wuff' 6 | end 7 | -------------------------------------------------------------------------------- /examples/rspec/spec/animal_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Test for Animal class 4 | describe Animal do 5 | subject(:animal) { described_class.new } 6 | 7 | it 'throws error on speak' do 8 | expect { animal.speak }.to raise_error NotImplementedError 9 | end 10 | 11 | it 'has a kind' do 12 | expect(animal.kind).to eq 'animal' 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /examples/rspec/spec/cat_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Test for Cat class 4 | describe Cat do 5 | subject(:cat) { described_class.new } 6 | 7 | it 'throws error on speak' do 8 | expect(cat.speak).to eq 'Miau' 9 | end 10 | 11 | it 'has a kind' do 12 | expect(cat.kind).to eq 'cat' 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /examples/rspec/spec/dog_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Dog do 4 | subject(:dog) { described_class.new } 5 | 6 | it 'throws error on speak' do 7 | expect(dog.speak).to eq 'Wuff' 8 | end 9 | 10 | it 'has a kind' do 11 | expect(dog.kind).to eq 'dog' 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /examples/rspec/spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_map' # if ENV['TEST_MAP'] 4 | 5 | require_relative '../animal' 6 | require_relative '../cat' 7 | require_relative '../dog' 8 | 9 | # Output log messages to the console on demand. 10 | TestMap::Config.configure do |config| 11 | config[:logger] = Logger.new($stdout, level: :info) 12 | end 13 | 14 | # See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 15 | # RSpec.configure do |config| 16 | # end 17 | -------------------------------------------------------------------------------- /lib/test_map.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'test_map/config' 4 | require_relative 'test_map/version' 5 | require_relative 'test_map/errors' 6 | require_relative 'test_map/filter' 7 | require_relative 'test_map/report' 8 | require_relative 'test_map/file_recorder' 9 | require_relative 'test_map/natural_mapping' 10 | require_relative 'test_map/mapping' 11 | 12 | # TestMap records associated files to test execution. 13 | module TestMap 14 | def self.reporter = @reporter ||= Report.new 15 | def self.logger = Config.config[:logger] 16 | end 17 | 18 | # Load plugins for supported test frameworks. 19 | require_relative 'test_map/plugins/minitest' if defined?(Minitest) 20 | require_relative 'test_map/plugins/rspec' if defined?(RSpec) 21 | -------------------------------------------------------------------------------- /lib/test_map/config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'logger' 4 | 5 | module TestMap 6 | # Configuration for TestMap 7 | class Config 8 | def self.[](key) = config[key] 9 | def self.config = @config ||= default_config 10 | def self.configure = yield(config) 11 | 12 | def self.default_config 13 | { logger: Logger.new('/dev/null'), out_file: '.test-map.yml', 14 | exclude_patterns: [%r{^(vendor)/}], natural_mapping: nil, 15 | skip_files: [%r{^(test/)}], merge: false } 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/test_map/errors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module TestMap 4 | # TraceInUseError is raised when a trace is already in use. 5 | class TraceInUseError < StandardError 6 | def self.default 7 | new <<~MSG 8 | Trace is already in use. Find for a second send of `#trace` and ensure 9 | you only use one. Use `#results` to get the results. 10 | MSG 11 | end 12 | end 13 | 14 | # NotTracedError is raised when a trace has not been started. 15 | class NotTracedError < StandardError 16 | def self.default 17 | new <<~MSG 18 | Trace has not been started. Use `#trace` to start tracing. 19 | MSG 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/test_map/file_recorder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module TestMap 4 | # FileRecorder records files accessed during test execution. 5 | class FileRecorder 6 | def initialize = @files = [] 7 | 8 | def trace(&block) 9 | raise TraceInUseError.default if @trace&.enabled? 10 | 11 | @trace = TracePoint.new(:call) do |tp| 12 | TestMap.logger.debug "#{tp.path}:#{tp.lineno}" 13 | @files << tp.path 14 | end 15 | 16 | if block_given? 17 | @trace.enable { block.call } 18 | else 19 | @trace.enable 20 | end 21 | end 22 | 23 | def stop = @trace&.disable 24 | 25 | # TODO: also add custom filters, e.g. for vendor directories 26 | def results 27 | raise NotTracedError.default unless @trace 28 | 29 | @files.filter { _1.start_with? Dir.pwd } 30 | .map { _1.sub("#{Dir.pwd}/", '') } 31 | .then { Filter.call _1 } 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/test_map/filter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module TestMap 4 | # Filter skips files that are not part of the project. 5 | class Filter 6 | attr_writer :exclude_patterns 7 | 8 | def self.call(files) = new.call(files) 9 | def call(files) = files.reject { exclude? _1 } 10 | def exclude_patterns = @exclude_patterns ||= Config[:exclude_patterns] 11 | def exclude?(file) = exclude_patterns.any? { file.match? _1 } 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/test_map/mapping.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'yaml' 4 | 5 | module TestMap 6 | # Mapping looksup test files for changed files. 7 | Mapping = Data.define(:map_file) do 8 | def map = YAML.safe_load_file(map_file) 9 | 10 | def lookup(*changed_files) 11 | new_files = apply_natural_mapping(changed_files - map.keys) 12 | map.values_at(*changed_files).concat(new_files).flatten.compact.uniq 13 | end 14 | 15 | def apply_natural_mapping(files) 16 | files.map { |file| NaturalMapping.new(file).test_files } 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/test_map/natural_mapping.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'yaml' 4 | 5 | module TestMap 6 | # Natural mapping determines the test file for a given source file by 7 | # applying common and configurable rules and transformation. 8 | class NaturalMapping 9 | CommonRule = Struct.new(:file) do 10 | def self.call(file) = new(file).call 11 | 12 | def call 13 | if File.exist?('test') 14 | transform('test') 15 | elsif File.exist?('spec') 16 | transform('spec') 17 | end 18 | end 19 | 20 | def transform(type) 21 | test_file = "#{File.basename(file, '.rb')}_#{type}.rb" 22 | test_path = File.dirname(file).sub('app/', '') 23 | test_path = nil if test_path == '.' 24 | [type, test_path, test_file].compact.join('/') 25 | end 26 | end 27 | 28 | attr_reader :file 29 | 30 | def initialize(file) = @file = file 31 | def test_files = Array(transform(file)) 32 | 33 | def transform(file) 34 | self.class.registered_rules.each do |rule| 35 | test_files = rule.call(file) 36 | 37 | return test_files if test_files 38 | end 39 | 40 | nil 41 | end 42 | 43 | def self.registered_rules 44 | @registered_rules ||= [ 45 | Config.config[:natural_mapping], 46 | CommonRule 47 | ].compact 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/test_map/plugins/minitest.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module TestMap 4 | module Plugins 5 | # Minitest plugin for TestMap. 6 | module Minitest 7 | def self.included(_base) 8 | TestMap.logger.info 'Registering hooks for Minitest' 9 | ::Minitest.after_run do 10 | TestMap.reporter.write "#{Dir.pwd}/#{Config.config[:out_file]}" 11 | end 12 | end 13 | 14 | def after_setup 15 | @recorder = FileRecorder.new.tap(&:trace) 16 | 17 | super 18 | end 19 | 20 | def before_teardown 21 | super 22 | 23 | @recorder.stop 24 | TestMap.reporter.add @recorder.results 25 | end 26 | end 27 | end 28 | end 29 | 30 | TestMap.logger.info 'Loading Minitest plugin' 31 | 32 | if defined?(Rails) 33 | ActiveSupport::TestCase.include TestMap::Plugins::Minitest 34 | else 35 | Minitest::Test.include TestMap::Plugins::Minitest 36 | end 37 | -------------------------------------------------------------------------------- /lib/test_map/plugins/rspec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | TestMap.logger.info 'Loading RSpec plugin' 4 | 5 | RSpec.configure do |config| 6 | config.around(:example) do |example| 7 | # path = example.metadata[:example_group][:file_path] 8 | recorder = TestMap::FileRecorder.new 9 | recorder.trace { example.run } 10 | TestMap.reporter.add recorder.results 11 | end 12 | 13 | config.after(:suite) do 14 | TestMap.reporter.write "#{Dir.pwd}/#{TestMap::Config.config[:out_file]}" 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/test_map/report.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'yaml' 4 | 5 | module TestMap 6 | # Report keeps track of associated files to test execution. 7 | class Report 8 | def initialize = @results = Hash.new { Set.new } 9 | 10 | def add(files) 11 | test_file, *associated_files = files 12 | TestMap.logger.info "Adding #{test_file} with #{associated_files}" 13 | associated_files.each do |file| 14 | @results[file] = @results[file] << test_file 15 | end 16 | end 17 | 18 | def write(file) 19 | content = if File.exist?(file) && Config.config[:merge] 20 | merge(results, YAML.safe_load_file(file)).to_yaml 21 | else 22 | to_yaml 23 | end 24 | File.write file, content 25 | end 26 | 27 | def results = @results.transform_values { _1.to_a.uniq.sort }.sort.to_h 28 | def to_yaml = results.to_yaml 29 | 30 | def merge(result, current) 31 | current.merge(result) do |_key, oldval, newval| 32 | (oldval + newval).uniq.sort 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/test_map/test_task.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'mapping' 4 | require 'rake/testtask' 5 | require 'minitest' 6 | require 'minitest/unit' 7 | 8 | module TestMap 9 | # TestTask is a rake helper class. 10 | class TestTask < Rake::TaskLib 11 | # Error for unknown test task adapter. 12 | class UnknownAdapterError < StandardError; end 13 | 14 | def initialize(name) # rubocop:disable Lint/MissingSuper 15 | @name = name 16 | end 17 | 18 | # Adapter for rspec test task 19 | class RailsTestTask 20 | attr_accessor :files 21 | 22 | def call = Rails::TestUnit::Runner.run_from_rake('test', files) 23 | end 24 | 25 | # Adapter for minitest test task. 26 | class MinitestTask < Minitest::TestTask 27 | def call = ruby(make_test_cmd, verbose: false) 28 | 29 | def files=(test_files) 30 | self.test_globs = test_files 31 | end 32 | end 33 | 34 | # Adapter for rspec test task 35 | class RSpecTask 36 | attr_accessor :files 37 | 38 | def call = `rspec #{files.join(' ')}` 39 | end 40 | 41 | def self.create(name = :test) = new(name).define 42 | 43 | def define 44 | namespace @name do 45 | desc 'Run tests for changed files' 46 | task :changes do 47 | out_file = "#{Dir.pwd}/.test-map.yml" 48 | args = defined?(Rails) ? ENV['TEST']&.split : ARGV[1..] 49 | test_files = Mapping.new(out_file).lookup(*args) 50 | 51 | # puts "Running tests #{test_files.join(' ')}" 52 | test_task.files = test_files 53 | test_task.call 54 | end 55 | end 56 | end 57 | 58 | def test_task = @test_task ||= build_test_task 59 | 60 | def build_test_task 61 | if defined?(Rails) 62 | return RailsTestTask.new 63 | elsif defined?(Minitest) 64 | require 'minitest/test_task' 65 | return MinitestTask.new 66 | elsif defined?(RSpec) 67 | return RSpecTask.new 68 | end 69 | 70 | raise UnknownAdapterError, 'No test task adapter found' 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/test_map/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module TestMap 4 | VERSION = '0.2.1' 5 | end 6 | -------------------------------------------------------------------------------- /test-map.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'lib/test_map/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'test-map' 7 | spec.version = TestMap::VERSION 8 | spec.authors = ['Christoph Lipautz'] 9 | spec.email = ['christoph@lipautz.org'] 10 | 11 | spec.summary = 'Track associated files of tests.' 12 | spec.description = <<~MSG 13 | Track files that are covered by test files to execute only the necessary 14 | tests. 15 | MSG 16 | spec.homepage = 'https://github.com/unused/test-map' 17 | spec.required_ruby_version = '>= 3.0.0' 18 | 19 | spec.metadata['homepage_uri'] = spec.homepage 20 | spec.metadata['source_code_uri'] = spec.homepage 21 | spec.metadata['changelog_uri'] = "#{spec.homepage}/main/blob/main/CHANGELOG.md" 22 | 23 | # Specify which files should be added to the gem when it is released. The 24 | # `git ls-files -z` loads the files in the RubyGem that have been added into 25 | # git. 26 | spec.files = Dir['LICENSE', 'CHANGELOG.md', 'README.md', 'lib/**/*'] 27 | spec.extra_rdoc_files = ['LICENSE.txt', 'README.md'] 28 | spec.require_paths = ['lib'] 29 | spec.metadata['rubygems_mfa_required'] = 'true' 30 | end 31 | -------------------------------------------------------------------------------- /test/fixtures/sample.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # A sample Ruby class 4 | class Sample 5 | def hello = 'Hello, World!' 6 | end 7 | -------------------------------------------------------------------------------- /test/fixtures/test-map.yml: -------------------------------------------------------------------------------- 1 | --- 2 | animal.rb: 3 | - test/animal_test.rb 4 | - test/cat_test.rb 5 | - test/dog_test.rb 6 | cat.rb: 7 | - test/cat_test.rb 8 | dog.rb: 9 | - test/dog_test.rb 10 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'simplecov' 4 | 5 | require 'minitest/autorun' 6 | require 'test_map' 7 | -------------------------------------------------------------------------------- /test/test_map/config_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require './test/test_helper' 4 | 5 | # Tests for configuration. 6 | class ConfigTest < Minitest::Test 7 | def test_respond_to_confgiure 8 | assert_respond_to subject, :configure 9 | end 10 | 11 | def test_default_configuration 12 | assert_equal '.test-map.yml', subject.config[:out_file] 13 | assert_kind_of Logger, subject.config[:logger] 14 | end 15 | 16 | def test_ensure_default_keys 17 | expected_keys = %i[logger out_file exclude_patterns natural_mapping 18 | skip_files merge] 19 | 20 | assert_equal expected_keys, subject.config.keys 21 | end 22 | 23 | def test_shorthand_access 24 | assert_equal '.test-map.yml', subject[:out_file] 25 | end 26 | 27 | private 28 | 29 | def subject = TestMap::Config 30 | end 31 | -------------------------------------------------------------------------------- /test/test_map/errors_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require './test/test_helper' 4 | 5 | # Tests for trace in use error. 6 | class TraceInUseErrorTest < Minitest::Test 7 | def test_descriptive_error_message 8 | assert_includes TestMap::TraceInUseError.default.message, 9 | 'Trace is already in use.' 10 | end 11 | end 12 | 13 | # Tests for not traced error. 14 | class NotTracedErrorTest < Minitest::Test 15 | def test_descriptive_error_message 16 | assert_includes TestMap::NotTracedError.default.message, 17 | 'Trace has not been started.' 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/test_map/file_recorder_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require './test/test_helper' 4 | require './test/fixtures/sample' 5 | 6 | # Tests for file recorder. 7 | class FileRecorderTest < Minitest::Test 8 | def test_detects_double_trace 9 | assert_raises TestMap::TraceInUseError do 10 | 2.times { subject.trace } 11 | end 12 | end 13 | 14 | def test_warn_on_no_trace 15 | assert_raises TestMap::NotTracedError do 16 | subject.results 17 | end 18 | end 19 | 20 | def test_tracing 21 | subject.trace 22 | subject.stop 23 | 24 | assert_equal 'file_recorder.rb', File.basename(subject.results.last) 25 | end 26 | 27 | def test_tracing_sample 28 | subject.trace { Sample.new.hello } 29 | 30 | assert_equal 'sample.rb', File.basename(subject.results.first) 31 | end 32 | 33 | def test_tracing_in_block_mode 34 | subject.trace { Sample.new.hello } 35 | 36 | refute_predicate subject.instance_variable_get(:@trace), :enabled? 37 | end 38 | 39 | private 40 | 41 | def subject = @subject ||= TestMap::FileRecorder.new 42 | end 43 | -------------------------------------------------------------------------------- /test/test_map/filter_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require './test/test_helper' 4 | 5 | # Tests for filter class 6 | class FilterTest < Minitest::Test 7 | def test_call 8 | files = %w[dog.rb cat.rb horse.rb] 9 | 10 | assert_equal files, subject.call(files) 11 | end 12 | 13 | def test_call_with_exclude_patterns 14 | files = %w[cat.rb dog.rb vendor/gem/horse.rb] 15 | 16 | assert_equal %w[cat.rb dog.rb], subject.call(files) 17 | end 18 | 19 | def test_set_custom_patterns 20 | filter = subject.new 21 | files = %w[cat.rb dog.rb horse.rb] 22 | filter.exclude_patterns = [/dog/, /cat/] 23 | 24 | assert_equal %w[horse.rb], filter.call(files) 25 | end 26 | 27 | private 28 | 29 | def subject = @subject ||= TestMap::Filter 30 | end 31 | -------------------------------------------------------------------------------- /test/test_map/mapping_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require './test/test_helper' 4 | 5 | # Tests for mapping. 6 | class MappingTest < Minitest::Test 7 | def test_mapping 8 | expected_result = %w[animal cat dog].map { "test/#{_1}_test.rb" } 9 | 10 | assert_equal expected_result, subject.lookup('animal.rb') 11 | end 12 | 13 | def test_mapping_applies_natural_mapping 14 | expected_result = %w[cat horse].map { "test/#{_1}_test.rb" } 15 | 16 | assert_equal expected_result, subject.lookup(*%w[cat.rb horse.rb]) 17 | end 18 | 19 | def subject = @subject ||= TestMap::Mapping.new(fixture_file) 20 | def fixture_file = "#{Dir.pwd}/test/fixtures/test-map.yml" 21 | end 22 | -------------------------------------------------------------------------------- /test/test_map/natural_mapping_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require './test/test_helper' 4 | 5 | # Tests for mapping. 6 | class NaturalMappingTest < Minitest::Test 7 | def test_mapping 8 | mapping = subject.new('lib/animal.rb') 9 | 10 | assert_equal ['test/lib/animal_test.rb'], mapping.test_files 11 | end 12 | 13 | def test_rails_mapping 14 | mapping = subject.new('app/model/animal.rb') 15 | 16 | assert_equal ['test/model/animal_test.rb'], mapping.test_files 17 | end 18 | 19 | def test_registered_rules 20 | refute_empty subject.registered_rules 21 | end 22 | 23 | def test_specs 24 | File.stub(:exist?, ->(type) { type == 'spec' }) do 25 | mapping = subject.new('app/model/animal.rb') 26 | 27 | assert_equal ['spec/model/animal_spec.rb'], mapping.test_files 28 | end 29 | end 30 | 31 | def test_no_known_test_lib 32 | File.stub(:exist?, ->(_) { false }) do 33 | mapping = subject.new('lib/animal.rb') 34 | puts mapping.test_files 35 | 36 | assert_empty mapping.test_files 37 | end 38 | end 39 | 40 | def test_has_registered_default_rules 41 | assert_predicate subject.registered_rules, :any? 42 | end 43 | 44 | private 45 | 46 | def subject = TestMap::NaturalMapping 47 | end 48 | -------------------------------------------------------------------------------- /test/test_map/report_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require './test/test_helper' 4 | 5 | # Tests for trace in use error. 6 | class ReportTest < Minitest::Test 7 | def test_empty_results 8 | assert_empty described_class.new.results 9 | end 10 | 11 | def test_merges_two_hashes 12 | report = described_class.new 13 | result = report.merge({ a: [1, 2, 3], b: [4, 5, 6] }, 14 | { a: [3, 4, 5], c: [6, 7, 8] }) 15 | expected_result = { a: [1, 2, 3, 4, 5], b: [4, 5, 6], c: [6, 7, 8] } 16 | 17 | assert_equal expected_result, result 18 | end 19 | 20 | private 21 | 22 | def described_class = TestMap::Report 23 | end 24 | -------------------------------------------------------------------------------- /test/test_map_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class TestMapTest < Minitest::Test 6 | def test_presence_of_version_number 7 | refute_nil TestMap::VERSION 8 | end 9 | end 10 | --------------------------------------------------------------------------------