test-map 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +11 -0
- data/LICENSE +21 -0
- data/README.md +83 -0
- data/lib/test_map/config.rb +17 -0
- data/lib/test_map/errors.rb +22 -0
- data/lib/test_map/file_recorder.rb +28 -0
- data/lib/test_map/filter.rb +13 -0
- data/lib/test_map/mapping.rb +19 -0
- data/lib/test_map/natural_mapping.rb +50 -0
- data/lib/test_map/plugins/minitest.rb +37 -0
- data/lib/test_map/plugins/rspec.rb +19 -0
- data/lib/test_map/report.rb +21 -0
- data/lib/test_map/test_task.rb +62 -0
- data/lib/test_map/version.rb +5 -0
- data/lib/test_map.rb +20 -0
- metadata +65 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 0a6119ac2cd9cbbed7ab386e84db91d4af55abb17839e4eaed8e2db66aed8f10
|
4
|
+
data.tar.gz: 454388e46781b590ecc38d8b5bab9d6395f8b5db48c8b7f1241533fc519995c4
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8165ebf293a69de809d2c04cb496836d44b722ae52870bd13cb15688493066e65c7d9daa422bd62a86fd87cec871bfbe7acfec9ffdea26b44091e6d84b2d8936
|
7
|
+
data.tar.gz: 62f784cfb4a6968c8ccd481477cef3e4d5eec0dbcaa04bfec295514ea8b5ae1bc667c8f99d46d6aabf4c888bec3d23a4fce00a2bbbddd5b8cc6396a381121f68
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,11 @@
|
|
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
|
+
## [Unreleased] - yyyy-mm-dd
|
10
|
+
|
11
|
+
Initial release.
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
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.
|
data/README.md
ADDED
@@ -0,0 +1,83 @@
|
|
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
|
+
On demand you can adapt the configuration to your needs.
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
TestMap::Configure.configure do |config|
|
26
|
+
config.logger = Logger.new($stdout) # default logs to dev/null
|
27
|
+
config.out_file = 'my-test-map.yml' # default is .test-map.yml
|
28
|
+
# defaults to [%r{^(vendor|test|spec)/}] }
|
29
|
+
config.exclude_patterns = [%r{^(libraries|testsuite)/}]
|
30
|
+
# register a custom rule to match new files; must implement `call(file)`;
|
31
|
+
# defaults to nil
|
32
|
+
config.natural_mapping = ->(file) { file.sub(%r{^library/}, 'test/') }
|
33
|
+
end
|
34
|
+
```
|
35
|
+
|
36
|
+
### Minitest
|
37
|
+
|
38
|
+
Include test-map in your test helper. Typically you want to include it
|
39
|
+
conditionally so it only generates the test map when needed.
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
# filename: test/test_helper.rb
|
43
|
+
|
44
|
+
# Include test-map after minitest has been required
|
45
|
+
require 'test_map' if ENV['TEST_MAP']
|
46
|
+
```
|
47
|
+
|
48
|
+
Run your tests with the `TEST_MAP` environment variable set.
|
49
|
+
|
50
|
+
```sh
|
51
|
+
$ TEST_MAP=1 bundle exec ruby -Itest test/models/user_test.rb
|
52
|
+
# or
|
53
|
+
$ TEST_MAP=1 bundle exec rake test
|
54
|
+
```
|
55
|
+
|
56
|
+
### Rspec
|
57
|
+
|
58
|
+
Include test-map in your test helper. Typically you want to include it
|
59
|
+
conditionally so it only generates the test map when needed.
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
# filename: spec/spec_helper.rb
|
63
|
+
require 'test_map' if ENV['TEST_MAP']
|
64
|
+
```
|
65
|
+
|
66
|
+
Run your tests with the `TEST_MAP` environment variable set.
|
67
|
+
|
68
|
+
```sh
|
69
|
+
$ TEST_MAP=1 bundle exec rspec
|
70
|
+
```
|
71
|
+
|
72
|
+
## Development
|
73
|
+
|
74
|
+
```sh
|
75
|
+
$ bundle install # install dependencies
|
76
|
+
$ bundle exec rake # run testsuite
|
77
|
+
$ bundle exec rubocop # run linter
|
78
|
+
```
|
79
|
+
|
80
|
+
## Contributing
|
81
|
+
|
82
|
+
Bug reports and pull requests are welcome on
|
83
|
+
[GitHub](https://github.com/unused/test-map).
|
@@ -0,0 +1,17 @@
|
|
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
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,22 @@
|
|
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
|
@@ -0,0 +1,28 @@
|
|
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
|
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.tap(&:enable)
|
15
|
+
end
|
16
|
+
|
17
|
+
def stop = @trace&.disable
|
18
|
+
|
19
|
+
# TODO: also add custom filters, e.g. for vendor directories
|
20
|
+
def results
|
21
|
+
raise NotTracedError.default unless @trace
|
22
|
+
|
23
|
+
@files.filter { _1.start_with? Dir.pwd }
|
24
|
+
.map { _1.sub("#{Dir.pwd}/", '') }
|
25
|
+
.then { Filter.call _1 }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,13 @@
|
|
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
|
@@ -0,0 +1,19 @@
|
|
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
|
@@ -0,0 +1,50 @@
|
|
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
|
@@ -0,0 +1,37 @@
|
|
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
|
+
result = TestMap.reporter.to_yaml
|
11
|
+
File.write "#{Dir.pwd}/#{Config.config[:out_file]}", result
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def after_setup
|
16
|
+
@recorder = FileRecorder.new.tap(&:trace)
|
17
|
+
|
18
|
+
super
|
19
|
+
end
|
20
|
+
|
21
|
+
def before_teardown
|
22
|
+
super
|
23
|
+
|
24
|
+
@recorder.stop
|
25
|
+
TestMap.reporter.add @recorder.results
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
TestMap.logger.info 'Loading Minitest plugin'
|
32
|
+
|
33
|
+
if defined?(Rails)
|
34
|
+
ActiveSupport::TestCase.include TestMap::Plugins::Minitest
|
35
|
+
else
|
36
|
+
Minitest::Test.include TestMap::Plugins::Minitest
|
37
|
+
end
|
@@ -0,0 +1,19 @@
|
|
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.tap(&:trace)
|
9
|
+
example.run
|
10
|
+
ensure
|
11
|
+
recorder.stop
|
12
|
+
TestMap.reporter.add recorder.results
|
13
|
+
end
|
14
|
+
|
15
|
+
config.after(:suite) do
|
16
|
+
result = TestMap.reporter.to_yaml
|
17
|
+
File.write "#{Dir.pwd}/#{TestMap::Config.config[:out_file]}", result
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,21 @@
|
|
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 results = @results.transform_values { _1.to_a.sort }.sort.to_h
|
19
|
+
def to_yaml = results.to_yaml
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,62 @@
|
|
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 minitest test task.
|
19
|
+
class MinitestTask < Minitest::TestTask
|
20
|
+
def call = ruby(make_test_cmd, verbose: false)
|
21
|
+
|
22
|
+
def files=(test_files)
|
23
|
+
self.test_globs = test_files
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Adapter for rspec test task
|
28
|
+
class RSpecTask
|
29
|
+
attr_accessor :files
|
30
|
+
|
31
|
+
def call = `rspec #{files.join(' ')}`
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.create(name = :test) = new(name).define
|
35
|
+
|
36
|
+
def define
|
37
|
+
namespace @name do
|
38
|
+
desc 'Run tests for changed files'
|
39
|
+
task :changes do
|
40
|
+
out_file = "#{Dir.pwd}/.test-map.yml"
|
41
|
+
test_files = Mapping.new(out_file).lookup(*ARGV[1..])
|
42
|
+
|
43
|
+
# puts "Running tests #{test_files.join(' ')}"
|
44
|
+
test_task.files = test_files
|
45
|
+
test_task.call
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def test_task = @test_task ||= build_test_task
|
51
|
+
|
52
|
+
def build_test_task
|
53
|
+
if defined?(Minitest)
|
54
|
+
return MinitestTask.new
|
55
|
+
elsif defined?(RSpec)
|
56
|
+
return RSpecTask.new
|
57
|
+
end
|
58
|
+
|
59
|
+
raise UnknownAdapterError, 'No test task adapter found'
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
data/lib/test_map.rb
ADDED
@@ -0,0 +1,20 @@
|
|
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)
|
metadata
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: test-map
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Christoph Lipautz
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-09-22 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: |
|
14
|
+
Track files that are covered by test files to execute only the necessary
|
15
|
+
tests.
|
16
|
+
email:
|
17
|
+
- christoph@lipautz.org
|
18
|
+
executables: []
|
19
|
+
extensions: []
|
20
|
+
extra_rdoc_files:
|
21
|
+
- LICENSE
|
22
|
+
- README.md
|
23
|
+
files:
|
24
|
+
- CHANGELOG.md
|
25
|
+
- LICENSE
|
26
|
+
- README.md
|
27
|
+
- lib/test_map.rb
|
28
|
+
- lib/test_map/config.rb
|
29
|
+
- lib/test_map/errors.rb
|
30
|
+
- lib/test_map/file_recorder.rb
|
31
|
+
- lib/test_map/filter.rb
|
32
|
+
- lib/test_map/mapping.rb
|
33
|
+
- lib/test_map/natural_mapping.rb
|
34
|
+
- lib/test_map/plugins/minitest.rb
|
35
|
+
- lib/test_map/plugins/rspec.rb
|
36
|
+
- lib/test_map/report.rb
|
37
|
+
- lib/test_map/test_task.rb
|
38
|
+
- lib/test_map/version.rb
|
39
|
+
homepage: https://github.com/unused/test-map
|
40
|
+
licenses: []
|
41
|
+
metadata:
|
42
|
+
homepage_uri: https://github.com/unused/test-map
|
43
|
+
source_code_uri: https://github.com/unused/test-map
|
44
|
+
changelog_uri: https://github.com/unused/test-map/main/blob/main/CHANGELOG.md
|
45
|
+
rubygems_mfa_required: 'true'
|
46
|
+
post_install_message:
|
47
|
+
rdoc_options: []
|
48
|
+
require_paths:
|
49
|
+
- lib
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 3.0.0
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
60
|
+
requirements: []
|
61
|
+
rubygems_version: 3.5.16
|
62
|
+
signing_key:
|
63
|
+
specification_version: 4
|
64
|
+
summary: Track associated files of tests.
|
65
|
+
test_files: []
|