test-map 0.1.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 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TestMap
4
+ VERSION = '0.1.0'
5
+ 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: []