test-map 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cd8d77a4693229a3bdc64882a2771830090f7b04ae977df9eec5386e3ffc16e3
4
- data.tar.gz: dc04407d47b77d1b5a6418072c8e380a327417a82fe27a21f31e1020d7b6b2aa
3
+ metadata.gz: 1ce3e91a8ab0098bedae3516d60ec4591513a5bd31507b143373627e3bd96ad3
4
+ data.tar.gz: 736396db3f44850d11fdeb3a0a0bca2bed9ecdb5f551439eed75aad59613b79c
5
5
  SHA512:
6
- metadata.gz: d80247e944cecd9b3c06b48b63bae9052589f97713496f674dc9ad23524ebfe6e8bb0720d38a90d0ba9b0874cb33c6a9422d6cd9bf3c49c2f8db252c26dde011
7
- data.tar.gz: 1690f86016ea54148386896023ba40e22210e2559c8d7dd175c7b728c458a9e2abdfdaa7f3a73997cbb4da446186e51033313a35df8c3e7e158a1a8cd8359f5a
6
+ metadata.gz: d80393696fdf9dfad334535bd560ddf4e93731d99ba77993a31c3243d8902f7cf30001cf56c2fe37b4ffa6ba5ad450329dd466ef2aa4119316e3344c0ce0c834
7
+ data.tar.gz: b0a20a539dec75bf6c27e47dc50c47dabd70cf68096fa83730950ede937c1445ddd8ed763814bf02b4040c2bcd47e32431379c9c78cde4e7ebc03ce2b7c912dc
data/CHANGELOG.md CHANGED
@@ -6,6 +6,11 @@ All notable changes to this project will be documented in this file.
6
6
  The format is based on [Keep a Changelog](http://keepachangelog.com/)
7
7
  and this project adheres to [Semantic Versioning](http://semver.org/).
8
8
 
9
+ ## 0.3.0 - 2026-03-01
10
+
11
+ Introduce test cache and remove changes runner. Removed `test:changes` Rake
12
+ task as it is no longer needed.
13
+
9
14
  ## 0.2.0 - 2024-10-25
10
15
 
11
16
  Provide explicit Test Task via Rake for Minitest, Rspec, and Rails. Extend
data/README.md CHANGED
@@ -4,12 +4,10 @@
4
4
  Track associated files of executed tests to optimize test execution on file
5
5
  changes.
6
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.
7
+ Test-Map records which source files each test touches and caches their
8
+ checksums. On subsequent runs, tests whose dependencies haven't changed are
9
+ automatically skipped. This is useful when you have a large test suite and want
10
+ to optimize the time spent running tests locally or in CI.
13
11
 
14
12
  ## Usage
15
13
 
@@ -21,57 +19,38 @@ $ bundle add test-map
21
19
 
22
20
  ### Minitest
23
21
 
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.
22
+ Include test-map in your test helper.
26
23
 
27
24
  ```ruby
28
25
  # filename: test/test_helper.rb
29
26
 
30
27
  # Include test-map after minitest has been required
31
- require 'test_map' if ENV['TEST_MAP']
28
+ require 'test_map'
32
29
  ```
33
30
 
34
- Run your tests with the `TEST_MAP` environment variable set.
31
+ Run your tests. On the first run test-map records file dependencies into
32
+ `.test-map.yml` and checksums into `.test-cache.yml`. On subsequent runs,
33
+ tests whose source files haven't changed are automatically skipped.
35
34
 
36
35
  ```sh
37
- $ TEST_MAP=1 bundle exec ruby -Itest test/models/user_test.rb
36
+ $ bundle exec ruby -Itest test/models/user_test.rb
38
37
  # 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 /_
38
+ $ bundle exec rake test
59
39
  ```
60
40
 
61
41
  ### Rspec
62
42
 
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.
43
+ Include test-map in your spec helper.
65
44
 
66
45
  ```ruby
67
46
  # filename: spec/spec_helper.rb
68
- require 'test_map' if ENV['TEST_MAP']
47
+ require 'test_map'
69
48
  ```
70
49
 
71
- Run your tests with the `TEST_MAP` environment variable set.
50
+ Run your tests. Caching works the same as with Minitest.
72
51
 
73
52
  ```sh
74
- $ TEST_MAP=1 bundle exec rspec
53
+ $ bundle exec rspec
75
54
  ```
76
55
 
77
56
  ## Configuration
@@ -83,6 +62,7 @@ TestMap::Config.configure do |config|
83
62
  config[:logger] = Logger.new($stdout) # default logs to dev/null
84
63
  config[:merge] = false # merge results (e.g. with multiple testsuites)
85
64
  config[:out_file] = 'my-test-map.yml' # default is .test-map.yml
65
+ config[:cache_file] = 'my-test-cache.yml' # default is .test-cache.yml
86
66
  # defaults to [%r{^(vendor)/}] }
87
67
  config[:exclude_patterns] = [%r{^(vendor|other_libraries)/}]
88
68
  # register a custom rule to match new files; must implement `call(file)`;
@@ -93,14 +73,6 @@ end
93
73
 
94
74
  ## Development
95
75
 
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
76
  ```sh
105
77
  $ bundle install # install dependencies
106
78
  $ bundle exec rake # run testsuite
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+ require 'yaml'
5
+
6
+ module TestMap
7
+ # Cache tracks file checksums to skip unchanged tests.
8
+ class Cache
9
+ GLOBAL_FILES = %w[Gemfile.lock .ruby-version].freeze
10
+
11
+ def initialize(cache_file, map_file, root: Dir.pwd)
12
+ @cache_file = cache_file
13
+ @map_file = map_file
14
+ @root = root
15
+ end
16
+
17
+ def fresh?(test_file)
18
+ return false unless cached_checksums
19
+ return false if global_files_changed?
20
+
21
+ files_to_check = [test_file] + source_files_for(test_file)
22
+ files_to_check.all? { |f| file_exist?(f) && current_checksum(f) == cached_checksums[f] }
23
+ end
24
+
25
+ def write(results)
26
+ all_files = collect_tracked_files(results)
27
+ checksums = all_files.each_with_object({}) do |file, hash|
28
+ hash[file] = current_checksum(file) if file_exist?(file)
29
+ end
30
+ File.write(@cache_file, checksums.sort.to_h.to_yaml)
31
+ end
32
+
33
+ private
34
+
35
+ def cached_checksums
36
+ @cached_checksums ||= File.exist?(@cache_file) && YAML.safe_load_file(@cache_file)
37
+ end
38
+
39
+ def global_files_changed?
40
+ GLOBAL_FILES.any? do |f|
41
+ file_exist?(f) && current_checksum(f) != cached_checksums[f]
42
+ end
43
+ end
44
+
45
+ def inverted_map = @inverted_map ||= build_inverted_map
46
+
47
+ def build_inverted_map
48
+ return {} unless File.exist?(@map_file)
49
+
50
+ map = YAML.safe_load_file(@map_file)
51
+ inverted = Hash.new { |h, k| h[k] = [] }
52
+ map.each do |source, tests|
53
+ tests.each { |t| inverted[t] << source }
54
+ end
55
+ inverted
56
+ end
57
+
58
+ def source_files_for(test_file)
59
+ inverted_map[test_file] || []
60
+ end
61
+
62
+ def current_checksum(file)
63
+ Digest::SHA256.file(File.join(@root, file)).hexdigest
64
+ end
65
+
66
+ def file_exist?(file) = File.exist?(File.join(@root, file))
67
+
68
+ def collect_tracked_files(results)
69
+ sources = results.keys
70
+ tests = results.values.flatten.uniq
71
+ all = (sources + tests + GLOBAL_FILES).uniq
72
+ all.select { |f| file_exist?(f) }
73
+ end
74
+ end
75
+ end
@@ -11,6 +11,7 @@ module TestMap
11
11
 
12
12
  def self.default_config
13
13
  { logger: Logger.new('/dev/null'), out_file: '.test-map.yml',
14
+ cache_file: '.test-cache.yml',
14
15
  exclude_patterns: [%r{^(vendor)/}], natural_mapping: nil,
15
16
  skip_files: [%r{^(test/)}], merge: false }
16
17
  end
@@ -6,13 +6,40 @@ module TestMap
6
6
  module Minitest
7
7
  def self.included(_base)
8
8
  TestMap.logger.info 'Registering hooks for Minitest'
9
- ::Minitest.after_run do
10
- TestMap.reporter.write "#{Dir.pwd}/#{Config.config[:out_file]}"
9
+ TestMap.suite_passed = true
10
+
11
+ ::Minitest.after_run { write_results }
12
+ end
13
+
14
+ def self.write_results
15
+ out_file = "#{Dir.pwd}/#{Config.config[:out_file]}"
16
+ reporter_results = TestMap.reporter.results
17
+
18
+ # All tests were cache-skipped, existing files are still valid
19
+ return if reporter_results.empty?
20
+
21
+ full_results = merge_results(out_file, reporter_results)
22
+ File.write(out_file, full_results.to_yaml)
23
+ TestMap.cache.write(full_results) if TestMap.suite_passed
24
+ end
25
+
26
+ # Merge with existing map to preserve mappings for cache-skipped tests
27
+ def self.merge_results(out_file, reporter_results)
28
+ if File.exist?(out_file)
29
+ TestMap.reporter.merge(reporter_results, YAML.safe_load_file(out_file))
30
+ else
31
+ reporter_results
11
32
  end
12
33
  end
13
34
 
14
35
  def after_setup
15
- @recorder = FileRecorder.new.tap(&:trace)
36
+ test_file = resolve_test_file
37
+ if test_file && TestMap.cache.fresh?(test_file)
38
+ @_test_map_skipped = true
39
+ skip 'test-map: cached'
40
+ else
41
+ @recorder = FileRecorder.new.tap(&:trace)
42
+ end
16
43
 
17
44
  super
18
45
  end
@@ -20,8 +47,21 @@ module TestMap
20
47
  def before_teardown
21
48
  super
22
49
 
50
+ return if @_test_map_skipped || !@recorder
51
+
23
52
  @recorder.stop
24
53
  TestMap.reporter.add @recorder.results
54
+
55
+ TestMap.suite_passed = false if !passed? && !skipped?
56
+ end
57
+
58
+ private
59
+
60
+ def resolve_test_file
61
+ file = method(name).source_location&.first
62
+ return unless file
63
+
64
+ file.sub("#{Dir.pwd}/", '')
25
65
  end
26
66
  end
27
67
  end
@@ -1,16 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  TestMap.logger.info 'Loading RSpec plugin'
4
+ TestMap.suite_passed = true
4
5
 
5
6
  RSpec.configure do |config|
6
7
  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
8
+ test_file = example.metadata[:file_path].sub("#{Dir.pwd}/", '').sub(%r{^\./}, '')
9
+
10
+ if TestMap.cache.fresh?(test_file)
11
+ skip 'test-map: cached'
12
+ else
13
+ recorder = TestMap::FileRecorder.new
14
+ recorder.trace { example.run }
15
+ TestMap.reporter.add recorder.results
16
+
17
+ TestMap.suite_passed = false if example.exception && !example.skipped?
18
+ end
11
19
  end
12
20
 
13
21
  config.after(:suite) do
14
- TestMap.reporter.write "#{Dir.pwd}/#{TestMap::Config.config[:out_file]}"
22
+ out_file = "#{Dir.pwd}/#{TestMap::Config.config[:out_file]}"
23
+ reporter_results = TestMap.reporter.results
24
+
25
+ # All tests were cache-skipped, existing files are still valid
26
+ next if reporter_results.empty?
27
+
28
+ # Merge with existing map to preserve mappings for cache-skipped tests
29
+ full_results = if File.exist?(out_file)
30
+ TestMap.reporter.merge(reporter_results, YAML.safe_load_file(out_file))
31
+ else
32
+ reporter_results
33
+ end
34
+
35
+ File.write(out_file, full_results.to_yaml)
36
+ TestMap.cache.write(full_results) if TestMap.suite_passed
15
37
  end
16
38
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TestMap
4
- VERSION = '0.2.1'
4
+ VERSION = '0.3.0'
5
5
  end
data/lib/test_map.rb CHANGED
@@ -8,11 +8,23 @@ require_relative 'test_map/report'
8
8
  require_relative 'test_map/file_recorder'
9
9
  require_relative 'test_map/natural_mapping'
10
10
  require_relative 'test_map/mapping'
11
+ require_relative 'test_map/cache'
11
12
 
12
13
  # TestMap records associated files to test execution.
13
14
  module TestMap
14
15
  def self.reporter = @reporter ||= Report.new
15
16
  def self.logger = Config.config[:logger]
17
+
18
+ def self.cache
19
+ @cache ||= Cache.new(
20
+ "#{Dir.pwd}/#{Config[:cache_file]}",
21
+ "#{Dir.pwd}/#{Config[:out_file]}"
22
+ )
23
+ end
24
+
25
+ class << self
26
+ attr_accessor :suite_passed
27
+ end
16
28
  end
17
29
 
18
30
  # Load plugins for supported test frameworks.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: test-map
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Christoph Lipautz
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-10-28 00:00:00.000000000 Z
11
+ date: 2026-03-01 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |
14
14
  Track files that are covered by test files to execute only the necessary
@@ -25,6 +25,7 @@ files:
25
25
  - LICENSE.txt
26
26
  - README.md
27
27
  - lib/test_map.rb
28
+ - lib/test_map/cache.rb
28
29
  - lib/test_map/config.rb
29
30
  - lib/test_map/errors.rb
30
31
  - lib/test_map/file_recorder.rb
@@ -34,7 +35,6 @@ files:
34
35
  - lib/test_map/plugins/minitest.rb
35
36
  - lib/test_map/plugins/rspec.rb
36
37
  - lib/test_map/report.rb
37
- - lib/test_map/test_task.rb
38
38
  - lib/test_map/version.rb
39
39
  homepage: https://github.com/unused/test-map
40
40
  licenses: []
@@ -43,7 +43,7 @@ metadata:
43
43
  source_code_uri: https://github.com/unused/test-map
44
44
  changelog_uri: https://github.com/unused/test-map/main/blob/main/CHANGELOG.md
45
45
  rubygems_mfa_required: 'true'
46
- post_install_message:
46
+ post_install_message:
47
47
  rdoc_options: []
48
48
  require_paths:
49
49
  - lib
@@ -51,15 +51,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: 3.0.0
54
+ version: 3.2.0
55
55
  required_rubygems_version: !ruby/object:Gem::Requirement
56
56
  requirements:
57
57
  - - ">="
58
58
  - !ruby/object:Gem::Version
59
59
  version: '0'
60
60
  requirements: []
61
- rubygems_version: 3.5.16
62
- signing_key:
61
+ rubygems_version: 3.4.10
62
+ signing_key:
63
63
  specification_version: 4
64
64
  summary: Track associated files of tests.
65
65
  test_files: []
@@ -1,73 +0,0 @@
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