mutation_tester 1.5.0 → 1.6.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 +12 -0
- data/Gemfile.lock +1 -1
- data/docs/ci.md +31 -0
- data/docs/execution-runners.md +23 -0
- data/docs/json-schema.md +44 -0
- data/examples/github_actions/ai_mutation_gate.yml +1 -1
- data/examples/github_actions/mutation_test.yml +1 -1
- data/examples/github_actions/redundant_tests.yml +77 -0
- data/exe/mutation_test +11 -1
- data/lib/mutation_tester/batch_runner.rb +3 -2
- data/lib/mutation_tester/configuration.rb +3 -1
- data/lib/mutation_tester/core.rb +37 -9
- data/lib/mutation_tester/fork_runner/worker.rb +4 -2
- data/lib/mutation_tester/fork_runner.rb +5 -4
- data/lib/mutation_tester/minitest_fail_fast.rb +3 -5
- data/lib/mutation_tester/minitest_load_hook.rb +35 -0
- data/lib/mutation_tester/mutation_runner.rb +50 -37
- data/lib/mutation_tester/progress_display.rb +134 -30
- data/lib/mutation_tester/reporters/base_reporter.rb +2 -1
- data/lib/mutation_tester/reporters/batch_json_reporter.rb +2 -1
- data/lib/mutation_tester/reporters/console_reporter.rb +1 -0
- data/lib/mutation_tester/reporters/json_reporter.rb +7 -0
- data/lib/mutation_tester/test_command.rb +60 -30
- data/lib/mutation_tester/test_recorder/minitest_hook.rb +55 -0
- data/lib/mutation_tester/test_recorder/rspec_hook.rb +62 -0
- data/lib/mutation_tester/test_recorder.rb +75 -0
- data/lib/mutation_tester/version.rb +1 -1
- data/lib/mutation_tester.rb +1 -0
- data/readme.md +421 -538
- metadata +7 -2
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../test_recorder'
|
|
4
|
+
|
|
5
|
+
module MutationTester
|
|
6
|
+
module TestRecorder
|
|
7
|
+
module RSpecHook
|
|
8
|
+
STATUSES = { passed: 'passed', failed: 'failed', pending: 'skipped' }.freeze
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def install
|
|
12
|
+
return if @installed
|
|
13
|
+
|
|
14
|
+
@installed = true
|
|
15
|
+
::RSpec.configure do |config|
|
|
16
|
+
config.after(:suite) { MutationTester::TestRecorder::RSpecHook.flush }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def flush
|
|
21
|
+
return unless TestRecorder.active?
|
|
22
|
+
|
|
23
|
+
TestRecorder.write(all_examples.filter_map { |example| entry_for(example) })
|
|
24
|
+
rescue StandardError => e
|
|
25
|
+
Kernel.warn "[MutationTester] The kill matrix could not record the rspec results: #{e.class}: #{e.message}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def all_examples
|
|
31
|
+
::RSpec.world.example_groups.flat_map(&:descendants).flat_map(&:examples)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def entry_for(example)
|
|
35
|
+
status = STATUSES[example.execution_result.status]
|
|
36
|
+
return nil unless status
|
|
37
|
+
|
|
38
|
+
metadata = example.metadata
|
|
39
|
+
{
|
|
40
|
+
id: id_for(metadata),
|
|
41
|
+
name: example.full_description,
|
|
42
|
+
line: metadata[:file_path] == rerun_path(metadata) ? metadata[:line_number] : nil,
|
|
43
|
+
status: status
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def id_for(metadata)
|
|
48
|
+
path = TestRecorder.relative_to_root(File.expand_path(rerun_path(metadata)))
|
|
49
|
+
return "#{path}[#{metadata[:scoped_id]}]" if metadata[:scoped_id]
|
|
50
|
+
|
|
51
|
+
"#{path}:#{metadata[:line_number]}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def rerun_path(metadata)
|
|
55
|
+
metadata[:rerun_file_path] || metadata[:file_path]
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
MutationTester::TestRecorder::RSpecHook.install
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'tempfile'
|
|
5
|
+
|
|
6
|
+
module MutationTester
|
|
7
|
+
module TestRecorder
|
|
8
|
+
LOG_ENV = 'MUTATION_TESTER_TEST_LOG'
|
|
9
|
+
SCOPE_ENV = 'MUTATION_TESTER_TEST_LOG_SCOPE'
|
|
10
|
+
ROOT_ENV = 'MUTATION_TESTER_TEST_LOG_ROOT'
|
|
11
|
+
FAILED = 'failed'
|
|
12
|
+
RSPEC_HOOK_PATH = File.expand_path('test_recorder/rspec_hook.rb', __dir__).freeze
|
|
13
|
+
MINITEST_HOOK_PATH = File.expand_path('test_recorder/minitest_hook.rb', __dir__).freeze
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
def capture(scope:, root:)
|
|
17
|
+
log = Tempfile.new(['mutation_tester_tests', '.jsonl'])
|
|
18
|
+
log.close
|
|
19
|
+
outcome = yield(LOG_ENV => log.path, SCOPE_ENV => scope.to_s, ROOT_ENV => root.to_s)
|
|
20
|
+
[outcome, read(log.path)]
|
|
21
|
+
ensure
|
|
22
|
+
log&.unlink
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def read(path)
|
|
26
|
+
return [] unless File.exist?(path)
|
|
27
|
+
|
|
28
|
+
File.readlines(path).filter_map do |line|
|
|
29
|
+
JSON.parse(line, symbolize_names: true)
|
|
30
|
+
rescue JSON::ParserError
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def failed_ids(entries)
|
|
36
|
+
Array(entries).select { |entry| entry[:status] == FAILED }.map { |entry| entry[:id] }.uniq.sort
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def active?
|
|
40
|
+
!ENV[LOG_ENV].to_s.empty?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def write(entries)
|
|
44
|
+
return unless active?
|
|
45
|
+
|
|
46
|
+
kept = ENV[SCOPE_ENV] == 'failures' ? entries.select { |entry| entry[:status] == FAILED } : entries
|
|
47
|
+
return if kept.empty?
|
|
48
|
+
|
|
49
|
+
File.open(ENV[LOG_ENV], 'a') do |file|
|
|
50
|
+
kept.each { |entry| file.puts(JSON.generate(entry)) }
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def relative_to_root(path)
|
|
55
|
+
root = ENV[ROOT_ENV].to_s
|
|
56
|
+
return path if root.empty?
|
|
57
|
+
|
|
58
|
+
prefixes = [root, resolved(root)].uniq.map { |candidate| "#{candidate.chomp('/')}/" }
|
|
59
|
+
[path, resolved(path)].uniq.each do |candidate|
|
|
60
|
+
prefix = prefixes.find { |value| candidate.start_with?(value) }
|
|
61
|
+
return candidate.delete_prefix(prefix) if prefix
|
|
62
|
+
end
|
|
63
|
+
path
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def resolved(path)
|
|
69
|
+
File.realpath(path)
|
|
70
|
+
rescue SystemCallError
|
|
71
|
+
path
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|