test-map 0.2.1 → 0.4.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 +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +61 -40
- data/lib/test_map/cache.rb +84 -0
- data/lib/test_map/config.rb +1 -0
- data/lib/test_map/event.rb +48 -0
- data/lib/test_map/file_recorder.rb +3 -2
- data/lib/test_map/plugins/minitest/cache_reporter.rb +53 -0
- data/lib/test_map/plugins/minitest.rb +62 -4
- data/lib/test_map/plugins/rspec/cache_formatter.rb +62 -0
- data/lib/test_map/plugins/rspec.rb +48 -7
- data/lib/test_map/version.rb +1 -1
- data/lib/test_map.rb +13 -0
- metadata +11 -8
- data/lib/test_map/test_task.rb +0 -73
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 502e7e183acdd6b0f2576ad014d920a1c00b4233f08cdfaf57243ed4814b1c1b
|
|
4
|
+
data.tar.gz: 4d50c652f8ffa46c485ab2da15c5ddd5da022cd856aac394375679f07ea083b8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 60480addcb32f9528c65895488190f4a7c01623b437e9818ff81e85f081b4e884e8fc3afb5684b4105b2eb52010ae1f1e182e2fbae0574a690c2404b86af3314
|
|
7
|
+
data.tar.gz: 61ce5a7bb1bd602f018e6e64b141bdfddbf52bbd61c72794796823d04b8b0a225f1748a6d66afe1d0540bfd85e31d34ed3cc3dd9c368ad543a2ecacfe0f898a5
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,17 @@ 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.4.0 - 2026-03-07
|
|
10
|
+
|
|
11
|
+
Extend reporting and cache handling for minitest and rspec. Add caches at
|
|
12
|
+
processing and profiling events. Optimize execution by reducing excessive
|
|
13
|
+
calls and calculations.
|
|
14
|
+
|
|
15
|
+
## 0.3.0 - 2026-03-01
|
|
16
|
+
|
|
17
|
+
Introduce test cache and remove changes runner. Removed `test:changes` Rake
|
|
18
|
+
task as it is no longer needed.
|
|
19
|
+
|
|
9
20
|
## 0.2.0 - 2024-10-25
|
|
10
21
|
|
|
11
22
|
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
|
|
8
|
-
|
|
9
|
-
This is useful when you have a large test suite and want
|
|
10
|
-
|
|
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
|
|
|
@@ -19,59 +17,89 @@ Add test-map to your Gemfile.
|
|
|
19
17
|
$ bundle add test-map
|
|
20
18
|
```
|
|
21
19
|
|
|
22
|
-
|
|
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.
|
|
20
|
+
Require test-map in your test helper or spec helper.
|
|
26
21
|
|
|
27
22
|
```ruby
|
|
28
23
|
# filename: test/test_helper.rb
|
|
29
|
-
|
|
30
|
-
# Include test-map after minitest has been required
|
|
31
|
-
require 'test_map' if ENV['TEST_MAP']
|
|
24
|
+
require 'test_map'
|
|
32
25
|
```
|
|
33
26
|
|
|
34
|
-
|
|
27
|
+
## Example Run
|
|
28
|
+
|
|
29
|
+
Running the testsuite, test-map creates a mapping of tests to their code files,
|
|
30
|
+
as well as a test-file result cache. Running the testsuite again, all
|
|
31
|
+
successfully run tests are cached and skipped. Chaning a file, **only tests
|
|
32
|
+
that need to be run are executed**.
|
|
35
33
|
|
|
36
34
|
```sh
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
# Running the testsuite for the first time, all tests are executed and mapped
|
|
36
|
+
> be rake
|
|
37
|
+
Run options: --seed 10112
|
|
38
|
+
|
|
39
|
+
# Running:
|
|
40
|
+
|
|
41
|
+
.......................................................
|
|
42
|
+
|
|
43
|
+
Finished in 0.042190s, 1303.6402 runs/s, 1730.2861 assertions/s.
|
|
44
|
+
55 runs, 73 assertions, 0 failures, 0 errors, 0 skips, 0 cached
|
|
45
|
+
|
|
46
|
+
# Running again without changes, all tests are cached and skipped
|
|
47
|
+
> be rake
|
|
48
|
+
Run options: --seed 40581
|
|
49
|
+
|
|
50
|
+
# Running:
|
|
51
|
+
|
|
52
|
+
CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
|
|
53
|
+
|
|
54
|
+
Finished in 0.007902s, 6960.1126 runs/s, 0.0000 assertions/s.
|
|
55
|
+
55 runs, 0 assertions, 0 failures, 0 errors, 0 skips, 55 cached
|
|
56
|
+
|
|
57
|
+
# Change a file and rerun the testsuite
|
|
58
|
+
> be rake
|
|
59
|
+
Run options: --seed 47682
|
|
60
|
+
|
|
61
|
+
# Running:
|
|
62
|
+
|
|
63
|
+
CCCCCCCCCC...CCCCCCCCCCCC.....CCCCCCCCCCCCCCCCCCCCCCCCC
|
|
64
|
+
|
|
65
|
+
Finished in 0.014029s, 3920.3764 runs/s, 570.2366 assertions/s.
|
|
66
|
+
55 runs, 8 assertions, 0 failures, 0 errors, 0 skips, 47 cached
|
|
40
67
|
```
|
|
41
68
|
|
|
42
|
-
|
|
43
|
-
|
|
69
|
+
### Minitest
|
|
70
|
+
|
|
71
|
+
Include test-map in your test helper.
|
|
44
72
|
|
|
45
73
|
```ruby
|
|
46
|
-
# filename:
|
|
47
|
-
require 'test_map/test_task'
|
|
74
|
+
# filename: test/test_helper.rb
|
|
48
75
|
|
|
49
|
-
|
|
76
|
+
# Include test-map after minitest has been required
|
|
77
|
+
require 'test_map'
|
|
50
78
|
```
|
|
51
79
|
|
|
52
|
-
|
|
80
|
+
Run your tests. On the first run test-map records file dependencies into
|
|
81
|
+
`.test-map.yml` and checksums into `.test-cache.yml`. On subsequent runs,
|
|
82
|
+
tests whose source files haven't changed are automatically skipped.
|
|
53
83
|
|
|
54
84
|
```sh
|
|
55
|
-
|
|
56
|
-
#
|
|
57
|
-
|
|
58
|
-
$ find . -name "*.rb" | entr -cp bundle exec rake test:changes /_
|
|
85
|
+
$ bundle exec ruby -Itest test/models/user_test.rb
|
|
86
|
+
# or
|
|
87
|
+
$ bundle exec rake test
|
|
59
88
|
```
|
|
60
89
|
|
|
61
90
|
### Rspec
|
|
62
91
|
|
|
63
|
-
Include test-map in your
|
|
64
|
-
conditionally so it only generates the test map when needed.
|
|
92
|
+
Include test-map in your spec helper.
|
|
65
93
|
|
|
66
94
|
```ruby
|
|
67
95
|
# filename: spec/spec_helper.rb
|
|
68
|
-
require 'test_map'
|
|
96
|
+
require 'test_map'
|
|
69
97
|
```
|
|
70
98
|
|
|
71
|
-
Run your tests
|
|
99
|
+
Run your tests. Caching works the same as with Minitest.
|
|
72
100
|
|
|
73
101
|
```sh
|
|
74
|
-
$
|
|
102
|
+
$ bundle exec rspec
|
|
75
103
|
```
|
|
76
104
|
|
|
77
105
|
## Configuration
|
|
@@ -83,6 +111,7 @@ TestMap::Config.configure do |config|
|
|
|
83
111
|
config[:logger] = Logger.new($stdout) # default logs to dev/null
|
|
84
112
|
config[:merge] = false # merge results (e.g. with multiple testsuites)
|
|
85
113
|
config[:out_file] = 'my-test-map.yml' # default is .test-map.yml
|
|
114
|
+
config[:cache_file] = 'my-test-cache.yml' # default is .test-cache.yml
|
|
86
115
|
# defaults to [%r{^(vendor)/}] }
|
|
87
116
|
config[:exclude_patterns] = [%r{^(vendor|other_libraries)/}]
|
|
88
117
|
# register a custom rule to match new files; must implement `call(file)`;
|
|
@@ -93,14 +122,6 @@ end
|
|
|
93
122
|
|
|
94
123
|
## Development
|
|
95
124
|
|
|
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
125
|
```sh
|
|
105
126
|
$ bundle install # install dependencies
|
|
106
127
|
$ bundle exec rake # run testsuite
|
|
@@ -0,0 +1,84 @@
|
|
|
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
|
+
@global_files_changed = nil
|
|
16
|
+
@current_checksums = {}
|
|
17
|
+
@file_exists_cache = {}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def fresh?(test_file)
|
|
21
|
+
return false unless cached_checksums
|
|
22
|
+
return false if global_files_changed?
|
|
23
|
+
|
|
24
|
+
files_to_check = [test_file].concat(source_files_for(test_file))
|
|
25
|
+
files_to_check.all? { |f| file_exist?(f) && current_checksum(f) == cached_checksums[f] }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def write(results)
|
|
29
|
+
all_files = collect_tracked_files(results)
|
|
30
|
+
checksums = all_files.each_with_object({}) do |file, hash|
|
|
31
|
+
hash[file] = current_checksum(file) if file_exist?(file)
|
|
32
|
+
end
|
|
33
|
+
File.write(@cache_file, checksums.sort.to_h.to_yaml)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def cached_checksums
|
|
39
|
+
@cached_checksums ||= File.exist?(@cache_file) && YAML.safe_load_file(@cache_file)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def global_files_changed?
|
|
43
|
+
return @global_files_changed unless @global_files_changed.nil?
|
|
44
|
+
|
|
45
|
+
@global_files_changed = GLOBAL_FILES.any? do |f|
|
|
46
|
+
file_exist?(f) && current_checksum(f) != cached_checksums[f]
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def inverted_map = @inverted_map ||= build_inverted_map
|
|
51
|
+
|
|
52
|
+
def build_inverted_map
|
|
53
|
+
return {} unless File.exist?(@map_file)
|
|
54
|
+
|
|
55
|
+
map = YAML.safe_load_file(@map_file)
|
|
56
|
+
inverted = Hash.new { |h, k| h[k] = [] }
|
|
57
|
+
map.each do |source, tests|
|
|
58
|
+
tests.each { |t| inverted[t] << source }
|
|
59
|
+
end
|
|
60
|
+
inverted
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def source_files_for(test_file)
|
|
64
|
+
inverted_map[test_file] || []
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def current_checksum(file)
|
|
68
|
+
@current_checksums[file] ||= Digest::SHA256.file(File.join(@root, file)).hexdigest
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def file_exist?(file)
|
|
72
|
+
return @file_exists_cache[file] if @file_exists_cache.key?(file)
|
|
73
|
+
|
|
74
|
+
@file_exists_cache[file] = File.exist?(File.join(@root, file))
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def collect_tracked_files(results)
|
|
78
|
+
sources = results.keys
|
|
79
|
+
tests = results.values.flatten.uniq
|
|
80
|
+
all = (sources + tests + GLOBAL_FILES).uniq
|
|
81
|
+
all.select { |f| file_exist?(f) }
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
data/lib/test_map/config.rb
CHANGED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TestMap
|
|
4
|
+
# Publish/subscribe event bus. No overhead when no subscribers.
|
|
5
|
+
#
|
|
6
|
+
# Topics use `type.action` format, e.g. `cache.create`, `map.create`.
|
|
7
|
+
#
|
|
8
|
+
# Block form measures elapsed time and passes it to subscribers:
|
|
9
|
+
# Event.publish('cache.create') { write_cache }
|
|
10
|
+
#
|
|
11
|
+
# Fire-and-forget form for simple notifications:
|
|
12
|
+
# Event.publish('cache.found', test_file:)
|
|
13
|
+
module Event
|
|
14
|
+
@subscribers = {}
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def subscribe(topic, &block)
|
|
18
|
+
(@subscribers[topic] ||= []) << block
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def publish(topic, **payload, &block)
|
|
22
|
+
listeners = @subscribers[topic]
|
|
23
|
+
return notify(listeners, topic, **payload) unless block
|
|
24
|
+
return yield unless listeners
|
|
25
|
+
|
|
26
|
+
publish_with_timing(listeners, topic, **payload, &block)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def reset
|
|
30
|
+
@subscribers = {}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def notify(listeners, topic, **payload)
|
|
36
|
+
listeners&.each { |cb| cb.call(topic, **payload) }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def publish_with_timing(listeners, topic, **payload)
|
|
40
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
41
|
+
result = yield
|
|
42
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
|
|
43
|
+
listeners.each { |cb| cb.call(topic, elapsed:, **payload) }
|
|
44
|
+
result
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -26,8 +26,9 @@ module TestMap
|
|
|
26
26
|
def results
|
|
27
27
|
raise NotTracedError.default unless @trace
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
cwd = "#{Dir.pwd}/"
|
|
30
|
+
@files.filter { _1.start_with? cwd }
|
|
31
|
+
.map { _1.sub(cwd, '') }
|
|
31
32
|
.then { Filter.call _1 }
|
|
32
33
|
end
|
|
33
34
|
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TestMap
|
|
4
|
+
module Plugins
|
|
5
|
+
module Minitest
|
|
6
|
+
# Reporter that tracks cached test count and appends it to the summary.
|
|
7
|
+
class CacheReporter < ::Minitest::StatisticsReporter
|
|
8
|
+
attr_accessor :cached, :composite
|
|
9
|
+
|
|
10
|
+
def initialize(io = $stdout, options = {}, composite: nil)
|
|
11
|
+
super(io, options)
|
|
12
|
+
self.cached = 0
|
|
13
|
+
self.composite = composite
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def record(result)
|
|
17
|
+
super
|
|
18
|
+
self.cached += 1 if result.failure.is_a?(TestMap::CachedSkip)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def report
|
|
22
|
+
super
|
|
23
|
+
return unless composite
|
|
24
|
+
|
|
25
|
+
summary_reporter = composite.reporters.find { |r| r.is_a?(::Minitest::SummaryReporter) }
|
|
26
|
+
return unless summary_reporter
|
|
27
|
+
|
|
28
|
+
summary_reporter.results.reject! { |r| r.failure.is_a?(TestMap::CachedSkip) }
|
|
29
|
+
summary_reporter.skips -= cached if summary_reporter.skips
|
|
30
|
+
patch_summary(summary_reporter)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def patch_summary(summary_reporter)
|
|
36
|
+
cached_count = cached
|
|
37
|
+
original_summary = summary_reporter.method(:summary)
|
|
38
|
+
|
|
39
|
+
summary_reporter.define_singleton_method(:summary) do
|
|
40
|
+
"#{original_summary.call}, #{cached_count} cached"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
module Minitest # :nodoc:
|
|
49
|
+
def self.plugin_test_map_cache_init(options)
|
|
50
|
+
cache_reporter = TestMap::Plugins::Minitest::CacheReporter.new(options[:io], options, composite: reporter)
|
|
51
|
+
reporter.reporters.unshift(cache_reporter)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -1,27 +1,85 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
module TestMap
|
|
4
|
+
# CachedSkip is raised to skip tests whose files haven't changed.
|
|
5
|
+
# Subclasses Minitest::Skip so Minitest treats it as a skip, but the
|
|
6
|
+
# custom reporter can distinguish it and show `C` instead of `S`.
|
|
7
|
+
class CachedSkip < Minitest::Skip
|
|
8
|
+
def initialize(msg = 'test-map: cached')
|
|
9
|
+
super
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def result_label
|
|
13
|
+
'Cached'
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
require_relative 'minitest/cache_reporter'
|
|
19
|
+
|
|
3
20
|
module TestMap
|
|
4
21
|
module Plugins
|
|
5
22
|
# Minitest plugin for TestMap.
|
|
6
23
|
module Minitest
|
|
7
24
|
def self.included(_base)
|
|
8
25
|
TestMap.logger.info 'Registering hooks for Minitest'
|
|
9
|
-
|
|
10
|
-
|
|
26
|
+
TestMap.suite_passed = true
|
|
27
|
+
|
|
28
|
+
::Minitest.after_run { write_results }
|
|
29
|
+
install_cache_reporter
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.install_cache_reporter
|
|
33
|
+
::Minitest.extensions << 'test_map_cache'
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.write_results
|
|
37
|
+
out_file = "#{Dir.pwd}/#{Config.config[:out_file]}"
|
|
38
|
+
reporter_results = TestMap.reporter.results
|
|
39
|
+
|
|
40
|
+
# All tests were cache-skipped, existing files are still valid
|
|
41
|
+
return if reporter_results.empty?
|
|
42
|
+
|
|
43
|
+
full_results = merge_results(out_file, reporter_results)
|
|
44
|
+
File.write(out_file, full_results.to_yaml)
|
|
45
|
+
TestMap.cache.write(full_results) if TestMap.suite_passed
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Merge with existing map to preserve mappings for cache-skipped tests
|
|
49
|
+
def self.merge_results(out_file, reporter_results)
|
|
50
|
+
if File.exist?(out_file)
|
|
51
|
+
TestMap.reporter.merge(reporter_results, YAML.safe_load_file(out_file))
|
|
52
|
+
else
|
|
53
|
+
reporter_results
|
|
11
54
|
end
|
|
12
55
|
end
|
|
13
56
|
|
|
14
|
-
def
|
|
15
|
-
|
|
57
|
+
def before_setup
|
|
58
|
+
test_file = resolve_test_file
|
|
59
|
+
raise TestMap::CachedSkip if test_file && TestMap.cache.fresh?(test_file)
|
|
16
60
|
|
|
61
|
+
@recorder = FileRecorder.new.tap(&:trace)
|
|
17
62
|
super
|
|
18
63
|
end
|
|
19
64
|
|
|
20
65
|
def before_teardown
|
|
21
66
|
super
|
|
22
67
|
|
|
68
|
+
return unless @recorder
|
|
69
|
+
|
|
23
70
|
@recorder.stop
|
|
24
71
|
TestMap.reporter.add @recorder.results
|
|
72
|
+
|
|
73
|
+
TestMap.suite_passed = false if !passed? && !skipped?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def resolve_test_file
|
|
79
|
+
file = method(name).source_location&.first
|
|
80
|
+
return unless file
|
|
81
|
+
|
|
82
|
+
file.sub("#{Dir.pwd}/", '')
|
|
25
83
|
end
|
|
26
84
|
end
|
|
27
85
|
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rspec/core/formatters/progress_formatter'
|
|
4
|
+
|
|
5
|
+
module TestMap
|
|
6
|
+
module Plugins
|
|
7
|
+
module RSpec
|
|
8
|
+
# Formatter that shows `C` for cached tests instead of `*` (pending).
|
|
9
|
+
# Extends ProgressFormatter so it can serve as the default formatter.
|
|
10
|
+
# Adjusts the summary to show cached count and filters cached tests
|
|
11
|
+
# from the pending output.
|
|
12
|
+
class CacheFormatter < ::RSpec::Core::Formatters::ProgressFormatter
|
|
13
|
+
CACHED_MESSAGE = 'test-map: cached'
|
|
14
|
+
|
|
15
|
+
::RSpec::Core::Formatters.register self,
|
|
16
|
+
:example_passed, :example_pending, :example_failed,
|
|
17
|
+
:start_dump, :dump_summary, :dump_pending
|
|
18
|
+
|
|
19
|
+
def initialize(output)
|
|
20
|
+
super
|
|
21
|
+
@cached_count = 0
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def example_pending(notification)
|
|
25
|
+
if cached?(notification.example)
|
|
26
|
+
@cached_count += 1
|
|
27
|
+
output.print ::RSpec::Core::Formatters::ConsoleCodes.wrap('c', :cyan)
|
|
28
|
+
else
|
|
29
|
+
super
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def dump_pending(notification)
|
|
34
|
+
examples = notification.pending_examples
|
|
35
|
+
cached = examples.select { |ex| cached?(ex) }
|
|
36
|
+
return if examples.size == cached.size
|
|
37
|
+
|
|
38
|
+
cached.each { |ex| examples.delete(ex) }
|
|
39
|
+
super
|
|
40
|
+
cached.each { |ex| examples.push(ex) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def dump_summary(summary)
|
|
44
|
+
if @cached_count.positive?
|
|
45
|
+
cached_count = @cached_count
|
|
46
|
+
original_totals_line = summary.method(:totals_line)
|
|
47
|
+
summary.define_singleton_method(:totals_line) do
|
|
48
|
+
"#{original_totals_line.call}, #{cached_count} cached"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
super
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def cached?(example)
|
|
57
|
+
example.execution_result.pending_message == CACHED_MESSAGE
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -1,16 +1,57 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'rspec/cache_formatter'
|
|
4
|
+
|
|
3
5
|
TestMap.logger.info 'Loading RSpec plugin'
|
|
6
|
+
TestMap.suite_passed = true
|
|
7
|
+
|
|
8
|
+
module TestMap
|
|
9
|
+
module Plugins
|
|
10
|
+
# RSpec integration for TestMap.
|
|
11
|
+
module RSpec
|
|
12
|
+
def self.write_results
|
|
13
|
+
out_file = "#{Dir.pwd}/#{Config.config[:out_file]}"
|
|
14
|
+
reporter_results = TestMap.reporter.results
|
|
15
|
+
|
|
16
|
+
# All tests were cache-skipped, existing files are still valid
|
|
17
|
+
return if reporter_results.empty?
|
|
18
|
+
|
|
19
|
+
full_results = merge_results(out_file, reporter_results)
|
|
20
|
+
File.write(out_file, full_results.to_yaml)
|
|
21
|
+
TestMap.cache.write(full_results) if TestMap.suite_passed
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Merge with existing map to preserve mappings for cache-skipped tests
|
|
25
|
+
def self.merge_results(out_file, reporter_results)
|
|
26
|
+
if File.exist?(out_file)
|
|
27
|
+
TestMap.reporter.merge(reporter_results, YAML.safe_load_file(out_file))
|
|
28
|
+
else
|
|
29
|
+
reporter_results
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
4
35
|
|
|
5
36
|
RSpec.configure do |config|
|
|
6
|
-
config.
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
TestMap.
|
|
37
|
+
config.default_formatter = TestMap::Plugins::RSpec::CacheFormatter
|
|
38
|
+
|
|
39
|
+
config.before(:context) do
|
|
40
|
+
test_file = self.class.metadata[:file_path].sub("#{Dir.pwd}/", '').sub(%r{^\./}, '')
|
|
41
|
+
skip TestMap::Plugins::RSpec::CacheFormatter::CACHED_MESSAGE if TestMap.cache.fresh?(test_file)
|
|
11
42
|
end
|
|
12
43
|
|
|
13
|
-
config.
|
|
14
|
-
|
|
44
|
+
config.around(:example) do |example|
|
|
45
|
+
if example.metadata[:skip]
|
|
46
|
+
example.run
|
|
47
|
+
else
|
|
48
|
+
recorder = TestMap::FileRecorder.new
|
|
49
|
+
recorder.trace { example.run }
|
|
50
|
+
TestMap.reporter.add recorder.results
|
|
51
|
+
|
|
52
|
+
TestMap.suite_passed = false if example.exception && !example.skipped?
|
|
53
|
+
end
|
|
15
54
|
end
|
|
55
|
+
|
|
56
|
+
config.after(:suite) { TestMap::Plugins::RSpec.write_results }
|
|
16
57
|
end
|
data/lib/test_map/version.rb
CHANGED
data/lib/test_map.rb
CHANGED
|
@@ -8,11 +8,24 @@ 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'
|
|
12
|
+
require_relative 'test_map/event'
|
|
11
13
|
|
|
12
14
|
# TestMap records associated files to test execution.
|
|
13
15
|
module TestMap
|
|
14
16
|
def self.reporter = @reporter ||= Report.new
|
|
15
17
|
def self.logger = Config.config[:logger]
|
|
18
|
+
|
|
19
|
+
def self.cache
|
|
20
|
+
@cache ||= Cache.new(
|
|
21
|
+
"#{Dir.pwd}/#{Config[:cache_file]}",
|
|
22
|
+
"#{Dir.pwd}/#{Config[:out_file]}"
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
attr_accessor :suite_passed
|
|
28
|
+
end
|
|
16
29
|
end
|
|
17
30
|
|
|
18
31
|
# 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.
|
|
4
|
+
version: 0.4.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:
|
|
11
|
+
date: 2026-03-07 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,16 +25,19 @@ 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
|
|
31
|
+
- lib/test_map/event.rb
|
|
30
32
|
- lib/test_map/file_recorder.rb
|
|
31
33
|
- lib/test_map/filter.rb
|
|
32
34
|
- lib/test_map/mapping.rb
|
|
33
35
|
- lib/test_map/natural_mapping.rb
|
|
34
36
|
- lib/test_map/plugins/minitest.rb
|
|
37
|
+
- lib/test_map/plugins/minitest/cache_reporter.rb
|
|
35
38
|
- lib/test_map/plugins/rspec.rb
|
|
39
|
+
- lib/test_map/plugins/rspec/cache_formatter.rb
|
|
36
40
|
- lib/test_map/report.rb
|
|
37
|
-
- lib/test_map/test_task.rb
|
|
38
41
|
- lib/test_map/version.rb
|
|
39
42
|
homepage: https://github.com/unused/test-map
|
|
40
43
|
licenses: []
|
|
@@ -43,7 +46,7 @@ metadata:
|
|
|
43
46
|
source_code_uri: https://github.com/unused/test-map
|
|
44
47
|
changelog_uri: https://github.com/unused/test-map/main/blob/main/CHANGELOG.md
|
|
45
48
|
rubygems_mfa_required: 'true'
|
|
46
|
-
post_install_message:
|
|
49
|
+
post_install_message:
|
|
47
50
|
rdoc_options: []
|
|
48
51
|
require_paths:
|
|
49
52
|
- lib
|
|
@@ -51,15 +54,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
51
54
|
requirements:
|
|
52
55
|
- - ">="
|
|
53
56
|
- !ruby/object:Gem::Version
|
|
54
|
-
version: 3.
|
|
57
|
+
version: 3.2.0
|
|
55
58
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
56
59
|
requirements:
|
|
57
60
|
- - ">="
|
|
58
61
|
- !ruby/object:Gem::Version
|
|
59
62
|
version: '0'
|
|
60
63
|
requirements: []
|
|
61
|
-
rubygems_version: 3.
|
|
62
|
-
signing_key:
|
|
64
|
+
rubygems_version: 3.4.10
|
|
65
|
+
signing_key:
|
|
63
66
|
specification_version: 4
|
|
64
67
|
summary: Track associated files of tests.
|
|
65
68
|
test_files: []
|
data/lib/test_map/test_task.rb
DELETED
|
@@ -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
|