henitai 0.1.7 → 0.1.8
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 +17 -1
- data/lib/henitai/coverage_formatter.rb +6 -95
- data/lib/henitai/eager_load.rb +1 -1
- data/lib/henitai/equivalence_detector.rb +55 -1
- data/lib/henitai/integration.rb +24 -2
- data/lib/henitai/minitest_coverage_hook.rb +17 -0
- data/lib/henitai/minitest_coverage_reporter.rb +36 -0
- data/lib/henitai/per_test_coverage_collector.rb +119 -0
- data/lib/henitai/version.rb +1 -1
- data/lib/henitai.rb +2 -0
- data/sig/henitai.rbs +41 -8
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 93624e520ff6014d8b5e69ee8b3ac0939d490697130e4acb996cce369c331b78
|
|
4
|
+
data.tar.gz: 02f3116c9e5031df714e08b8d394cd0b90c914c57f1ffe9639540ca4251b4013
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8823368e6bdb4fd73ce08253983a63088037a488f8e8a423d36ba495be44f157bb5e1d0a77d3ee1105986eb7c965c670552a489d9b50c3657a8e6935d1774003
|
|
7
|
+
data.tar.gz: b49c42e37777ca6519436ab327a58fcc0a9ab012ad6ad9ffee226c9cacfe156612653bb6a3e794cdf9255d1cf84fa36c6b221ef12dfafc0523ddd884a7ff6e50
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.8] - 2026-04-16
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Minitest integration now supports per-test coverage: a `MinitestCoverageReporter`
|
|
14
|
+
snapshots `Coverage.peek_result` after each test and writes `henitai_per_test.json`,
|
|
15
|
+
enabling targeted mutation runs that only execute the tests covering the mutated lines —
|
|
16
|
+
the same efficiency that was previously available only to RSpec projects
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- RSpec integration now respects `--exclude-pattern` entries in `.rspec` so excluded
|
|
20
|
+
spec files are not passed to the runner during mutation runs
|
|
21
|
+
- Per-test source file filter corrected to check only the project-relative path prefix
|
|
22
|
+
rather than scanning the full absolute path, preventing false exclusions when the
|
|
23
|
+
project lives under a directory whose path contains `/spec/` or `/test/`
|
|
24
|
+
|
|
10
25
|
## [0.1.7] - 2026-04-14
|
|
11
26
|
|
|
12
27
|
### Added
|
|
@@ -181,7 +196,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
181
196
|
- CLI critical path: `henitai run` now executes the full pipeline, supports `--since`, returns CI-friendly exit codes, and `henitai version` prints `Henitai::VERSION`
|
|
182
197
|
- RSpec per-test coverage output: `henitai/coverage_formatter` now writes `coverage/henitai_per_test.json`
|
|
183
198
|
|
|
184
|
-
[Unreleased]: https://github.com/martinotten/henitai/compare/v0.1.
|
|
199
|
+
[Unreleased]: https://github.com/martinotten/henitai/compare/v0.1.8...HEAD
|
|
200
|
+
[0.1.8]: https://github.com/martinotten/henitai/compare/v0.1.7...v0.1.8
|
|
185
201
|
[0.1.7]: https://github.com/martinotten/henitai/compare/v0.1.6...v0.1.7
|
|
186
202
|
[0.1.6]: https://github.com/martinotten/henitai/compare/v0.1.5...v0.1.6
|
|
187
203
|
[0.1.5]: https://github.com/martinotten/henitai/compare/v0.1.4...v0.1.5
|
|
@@ -1,112 +1,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
4
|
-
require "fileutils"
|
|
5
|
-
require "json"
|
|
3
|
+
require "henitai/per_test_coverage_collector"
|
|
6
4
|
|
|
7
5
|
module Henitai
|
|
8
6
|
# Collects per-test coverage data for static filtering heuristics.
|
|
9
7
|
class CoverageFormatter
|
|
10
|
-
REPORT_DIR_ENV =
|
|
11
|
-
REPORT_FILE_NAME =
|
|
8
|
+
REPORT_DIR_ENV = PerTestCoverageCollector::REPORT_DIR_ENV
|
|
9
|
+
REPORT_FILE_NAME = PerTestCoverageCollector::REPORT_FILE_NAME
|
|
12
10
|
|
|
13
11
|
def initialize(_output)
|
|
14
|
-
@
|
|
15
|
-
hash[test_file] = Hash.new { |nested, source_file| nested[source_file] = [] }
|
|
16
|
-
end
|
|
17
|
-
@previous_snapshot = {}
|
|
18
|
-
@warned_missing_coverage = false
|
|
12
|
+
@collector = PerTestCoverageCollector.new
|
|
19
13
|
end
|
|
20
14
|
|
|
21
15
|
def example_finished(notification)
|
|
22
|
-
|
|
23
|
-
return warn_missing_coverage unless snapshot
|
|
24
|
-
|
|
25
|
-
test_file = notification.example.metadata[:file_path]
|
|
26
|
-
new_lines(snapshot).each do |source_file, lines|
|
|
27
|
-
@coverage_by_test[test_file][source_file].concat(lines)
|
|
28
|
-
@coverage_by_test[test_file][source_file].uniq!
|
|
29
|
-
@coverage_by_test[test_file][source_file].sort!
|
|
30
|
-
end
|
|
31
|
-
@previous_snapshot = snapshot
|
|
16
|
+
@collector.record_test(notification.example.metadata[:file_path])
|
|
32
17
|
end
|
|
33
18
|
|
|
34
19
|
def dump_summary(_summary)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
FileUtils.mkdir_p(File.dirname(report_path))
|
|
38
|
-
File.write(report_path, JSON.pretty_generate(serializable_report))
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
private
|
|
42
|
-
|
|
43
|
-
def report_path
|
|
44
|
-
File.join(reports_dir, REPORT_FILE_NAME)
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def reports_dir
|
|
48
|
-
ENV.fetch(REPORT_DIR_ENV, "coverage")
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def current_snapshot
|
|
52
|
-
Coverage.peek_result
|
|
53
|
-
rescue StandardError
|
|
54
|
-
nil
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def warn_missing_coverage
|
|
58
|
-
return if @warned_missing_coverage
|
|
59
|
-
|
|
60
|
-
warn "Per-test coverage unavailable; skipping coverage formatter output"
|
|
61
|
-
@warned_missing_coverage = true
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def new_lines(snapshot)
|
|
65
|
-
snapshot.each_with_object({}) do |(source_file, file_coverage), result|
|
|
66
|
-
next unless source_file?(source_file)
|
|
67
|
-
|
|
68
|
-
lines = new_line_numbers(
|
|
69
|
-
file_coverage,
|
|
70
|
-
previous_line_counts(source_file)
|
|
71
|
-
)
|
|
72
|
-
result[source_file] = lines unless lines.empty?
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def new_line_numbers(file_coverage, previous_counts)
|
|
77
|
-
line_counts_for(file_coverage).each_with_index.filter_map do |count, index|
|
|
78
|
-
next unless count.to_i.positive?
|
|
79
|
-
next if previous_counts.fetch(index, 0).to_i.positive?
|
|
80
|
-
|
|
81
|
-
index + 1
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def previous_line_counts(source_file)
|
|
86
|
-
line_counts_for(@previous_snapshot.fetch(source_file, []))
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def line_counts_for(file_coverage)
|
|
90
|
-
case file_coverage
|
|
91
|
-
when Hash
|
|
92
|
-
Array(file_coverage[:lines] || file_coverage["lines"])
|
|
93
|
-
else
|
|
94
|
-
Array(file_coverage)
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def source_file?(path)
|
|
99
|
-
expanded = File.expand_path(path)
|
|
100
|
-
expanded.start_with?(Dir.pwd) &&
|
|
101
|
-
!expanded.include?("#{File::SEPARATOR}spec#{File::SEPARATOR}")
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def serializable_report
|
|
105
|
-
@coverage_by_test.transform_values do |source_map|
|
|
106
|
-
source_map.to_h do |source_file, lines|
|
|
107
|
-
[File.expand_path(source_file), lines.uniq.sort]
|
|
108
|
-
end
|
|
109
|
-
end
|
|
20
|
+
@collector.write_report
|
|
110
21
|
end
|
|
111
22
|
end
|
|
112
23
|
end
|
data/lib/henitai/eager_load.rb
CHANGED
|
@@ -4,7 +4,7 @@ require "henitai"
|
|
|
4
4
|
|
|
5
5
|
# Force all autoloaded constants to load so mutation testing tools
|
|
6
6
|
# (e.g. mutant) can discover subjects via ObjectSpace.
|
|
7
|
-
SIDE_EFFECT_FILES = %w[minitest_simplecov.rb rspec_coverage_formatter.rb].freeze
|
|
7
|
+
SIDE_EFFECT_FILES = %w[minitest_simplecov.rb minitest_coverage_hook.rb rspec_coverage_formatter.rb].freeze
|
|
8
8
|
|
|
9
9
|
Dir[File.join(__dir__, "**/*.rb")].each do |f|
|
|
10
10
|
require f unless SIDE_EFFECT_FILES.any? { |name| f.end_with?(name) }
|
|
@@ -21,7 +21,8 @@ module Henitai
|
|
|
21
21
|
def equivalent_mutation?(mutant)
|
|
22
22
|
equivalent_arithmetic_mutation?(mutant) ||
|
|
23
23
|
equivalent_logical_mutation?(mutant) ||
|
|
24
|
-
equivalent_singleton_equality_mutation?(mutant)
|
|
24
|
+
equivalent_singleton_equality_mutation?(mutant) ||
|
|
25
|
+
equivalent_string_eql_mutation?(mutant)
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
def equivalent_arithmetic_mutation?(mutant)
|
|
@@ -162,6 +163,59 @@ module Henitai
|
|
|
162
163
|
equality_operators?(original.children[1], mutated.children[1])
|
|
163
164
|
end
|
|
164
165
|
|
|
166
|
+
# Detects `lhs == rhs` mutated to `lhs.eql?(rhs)` (or the reverse) when at
|
|
167
|
+
# least one operand is a string literal.
|
|
168
|
+
#
|
|
169
|
+
# String#eql? is documented to compare both type and value. Since String#==
|
|
170
|
+
# also compares type and value (it returns false for any non-String argument
|
|
171
|
+
# without invoking the other object's #==), the two methods are equivalent
|
|
172
|
+
# for all possible inputs whenever at least one operand is statically known
|
|
173
|
+
# to be a String — proven here by the presence of a :str literal on the
|
|
174
|
+
# receiver or the argument side.
|
|
175
|
+
#
|
|
176
|
+
# When no operand is a string literal we conservatively leave the mutant
|
|
177
|
+
# pending: the receiver could be any object whose custom #== diverges from
|
|
178
|
+
# its #eql?.
|
|
179
|
+
def equivalent_string_eql_mutation?(mutant)
|
|
180
|
+
original = mutant.original_node
|
|
181
|
+
mutated = mutant.mutated_node
|
|
182
|
+
|
|
183
|
+
string_eql_send?(original) && string_eql_send?(mutated) &&
|
|
184
|
+
same_receiver?(original, mutated) &&
|
|
185
|
+
string_eql_operators?(original.children[1], mutated.children[1]) &&
|
|
186
|
+
same_rhs?(original, mutated) &&
|
|
187
|
+
string_operand?(original)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def string_eql_send?(node)
|
|
191
|
+
node.is_a?(Parser::AST::Node) &&
|
|
192
|
+
node.type == :send &&
|
|
193
|
+
node.children.size == 3 &&
|
|
194
|
+
string_eql_operator?(node.children[1])
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def string_eql_operator?(operator)
|
|
198
|
+
%i[== eql?].include?(operator)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def string_eql_operators?(op_a, op_b)
|
|
202
|
+
string_eql_operator?(op_a) && string_eql_operator?(op_b) && op_a != op_b
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def same_rhs?(original, mutated)
|
|
206
|
+
same_node?(original.children[2], mutated.children[2])
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Returns true when at least one operand is a string literal, giving static
|
|
210
|
+
# proof that the comparison is string-typed on at least one side.
|
|
211
|
+
def string_operand?(node)
|
|
212
|
+
string_literal?(node.children[0]) || string_literal?(node.children[2])
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def string_literal?(node)
|
|
216
|
+
node.is_a?(Parser::AST::Node) && node.type == :str
|
|
217
|
+
end
|
|
218
|
+
|
|
165
219
|
def singleton_rhs_match?(original, mutated)
|
|
166
220
|
rhs = original.children[2]
|
|
167
221
|
singleton_literal?(rhs) && same_node?(rhs, mutated.children[2])
|
data/lib/henitai/integration.rb
CHANGED
|
@@ -326,7 +326,8 @@ module Henitai
|
|
|
326
326
|
end
|
|
327
327
|
|
|
328
328
|
def spec_files
|
|
329
|
-
Dir.glob("spec/**/*_spec.rb")
|
|
329
|
+
paths = Dir.glob("spec/**/*_spec.rb")
|
|
330
|
+
paths - excluded_spec_files
|
|
330
331
|
end
|
|
331
332
|
|
|
332
333
|
def fallback_spec_files(subject)
|
|
@@ -341,6 +342,26 @@ module Henitai
|
|
|
341
342
|
matches.empty? ? spec_files : matches
|
|
342
343
|
end
|
|
343
344
|
|
|
345
|
+
def excluded_spec_files
|
|
346
|
+
rspec_exclude_patterns.flat_map { |pattern| Dir.glob(pattern) }.uniq
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def rspec_exclude_patterns
|
|
350
|
+
rspec_config_lines.filter_map do |line|
|
|
351
|
+
line[/\A--exclude-pattern\s+(.+)\z/, 1]
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def rspec_config_lines
|
|
356
|
+
return [] unless File.exist?(rspec_config_path)
|
|
357
|
+
|
|
358
|
+
File.readlines(rspec_config_path, chomp: true).map(&:strip)
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def rspec_config_path
|
|
362
|
+
".rspec"
|
|
363
|
+
end
|
|
364
|
+
|
|
344
365
|
def selection_patterns(subject)
|
|
345
366
|
[
|
|
346
367
|
subject.expression,
|
|
@@ -477,7 +498,7 @@ module Henitai
|
|
|
477
498
|
# coverage collection is not yet wired into this path.
|
|
478
499
|
class Minitest < Rspec
|
|
479
500
|
def per_test_coverage_supported?
|
|
480
|
-
|
|
501
|
+
true
|
|
481
502
|
end
|
|
482
503
|
|
|
483
504
|
def run_mutant(mutant:, test_files:, timeout:)
|
|
@@ -505,6 +526,7 @@ module Henitai
|
|
|
505
526
|
def suite_command(test_files)
|
|
506
527
|
["bundle", "exec", "ruby", "-I", "test",
|
|
507
528
|
"-r", "henitai/minitest_simplecov",
|
|
529
|
+
"-r", "henitai/minitest_coverage_hook",
|
|
508
530
|
"-e", "ARGV.each { |f| require File.expand_path(f) }",
|
|
509
531
|
*test_files]
|
|
510
532
|
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Injected by henitai into the Minitest baseline subprocess to collect
|
|
4
|
+
# per-test line coverage. Must be required after henitai/minitest_simplecov
|
|
5
|
+
# so that Coverage is already running when the reporter takes snapshots.
|
|
6
|
+
|
|
7
|
+
require "minitest"
|
|
8
|
+
require "henitai/minitest_coverage_reporter"
|
|
9
|
+
|
|
10
|
+
Minitest.extensions << "henitai_coverage"
|
|
11
|
+
|
|
12
|
+
# Henitai per-test coverage plugin for Minitest.
|
|
13
|
+
module Minitest
|
|
14
|
+
def self.plugin_henitai_coverage_init(_options)
|
|
15
|
+
reporter.reporters << Henitai::MinitestCoverageReporter.new
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest"
|
|
4
|
+
require "henitai/per_test_coverage_collector"
|
|
5
|
+
|
|
6
|
+
module Henitai
|
|
7
|
+
# Minitest reporter that collects per-test line coverage deltas.
|
|
8
|
+
#
|
|
9
|
+
# Added to Minitest's reporter chain by the henitai_coverage plugin
|
|
10
|
+
# (see minitest_coverage_hook.rb). Delegates accumulation and serialisation
|
|
11
|
+
# to PerTestCoverageCollector so the JSON output format is identical to the
|
|
12
|
+
# RSpec integration.
|
|
13
|
+
class MinitestCoverageReporter < Minitest::Reporter
|
|
14
|
+
def initialize(io = $stdout, options = {})
|
|
15
|
+
super
|
|
16
|
+
@collector = PerTestCoverageCollector.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def record(result)
|
|
20
|
+
super
|
|
21
|
+
@collector.record_test(relative_to_pwd(result.source_location.first))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def report
|
|
25
|
+
super
|
|
26
|
+
@collector.write_report
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def relative_to_pwd(path)
|
|
32
|
+
prefix = "#{Dir.pwd}#{File::SEPARATOR}"
|
|
33
|
+
path.start_with?(prefix) ? path.sub(prefix, "") : path
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "coverage"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module Henitai
|
|
8
|
+
# Accumulates per-test line coverage deltas across test examples.
|
|
9
|
+
#
|
|
10
|
+
# Framework-agnostic core used by both the RSpec and Minitest adapters.
|
|
11
|
+
# Callers invoke record_test after each test completes and write_report
|
|
12
|
+
# once the full suite has finished.
|
|
13
|
+
class PerTestCoverageCollector
|
|
14
|
+
REPORT_DIR_ENV = "HENITAI_REPORTS_DIR"
|
|
15
|
+
REPORT_FILE_NAME = "henitai_per_test.json"
|
|
16
|
+
|
|
17
|
+
def initialize
|
|
18
|
+
@coverage_by_test = Hash.new do |hash, test_file|
|
|
19
|
+
hash[test_file] = Hash.new { |nested, source_file| nested[source_file] = [] }
|
|
20
|
+
end
|
|
21
|
+
@previous_snapshot = {}
|
|
22
|
+
@warned_missing_coverage = false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def record_test(test_file)
|
|
26
|
+
snapshot = current_snapshot
|
|
27
|
+
return warn_missing_coverage unless snapshot
|
|
28
|
+
|
|
29
|
+
new_lines(snapshot).each do |source_file, lines|
|
|
30
|
+
@coverage_by_test[test_file][source_file].concat(lines)
|
|
31
|
+
@coverage_by_test[test_file][source_file].uniq!
|
|
32
|
+
@coverage_by_test[test_file][source_file].sort!
|
|
33
|
+
end
|
|
34
|
+
@previous_snapshot = snapshot
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def write_report
|
|
38
|
+
return if @coverage_by_test.empty?
|
|
39
|
+
|
|
40
|
+
FileUtils.mkdir_p(File.dirname(report_path))
|
|
41
|
+
File.write(report_path, JSON.pretty_generate(serializable_report))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def report_path
|
|
47
|
+
File.join(reports_dir, REPORT_FILE_NAME)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def reports_dir
|
|
51
|
+
ENV.fetch(REPORT_DIR_ENV, "coverage")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def current_snapshot
|
|
55
|
+
Coverage.peek_result
|
|
56
|
+
rescue StandardError
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def warn_missing_coverage
|
|
61
|
+
return if @warned_missing_coverage
|
|
62
|
+
|
|
63
|
+
warn "Per-test coverage unavailable; skipping coverage formatter output"
|
|
64
|
+
@warned_missing_coverage = true
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def new_lines(snapshot)
|
|
68
|
+
snapshot.each_with_object({}) do |(source_file, file_coverage), result|
|
|
69
|
+
next unless source_file?(source_file)
|
|
70
|
+
|
|
71
|
+
lines = new_line_numbers(
|
|
72
|
+
file_coverage,
|
|
73
|
+
previous_line_counts(source_file)
|
|
74
|
+
)
|
|
75
|
+
result[source_file] = lines unless lines.empty?
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def new_line_numbers(file_coverage, previous_counts)
|
|
80
|
+
line_counts_for(file_coverage).each_with_index.filter_map do |count, index|
|
|
81
|
+
next unless count.to_i.positive?
|
|
82
|
+
next if previous_counts.fetch(index, 0).to_i.positive?
|
|
83
|
+
|
|
84
|
+
index + 1
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def previous_line_counts(source_file)
|
|
89
|
+
line_counts_for(@previous_snapshot.fetch(source_file, []))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def line_counts_for(file_coverage)
|
|
93
|
+
case file_coverage
|
|
94
|
+
when Hash
|
|
95
|
+
Array(file_coverage[:lines] || file_coverage["lines"])
|
|
96
|
+
else
|
|
97
|
+
Array(file_coverage)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def source_file?(path)
|
|
102
|
+
expanded = File.expand_path(path)
|
|
103
|
+
prefix = "#{Dir.pwd}#{File::SEPARATOR}"
|
|
104
|
+
return false unless expanded.start_with?(prefix)
|
|
105
|
+
|
|
106
|
+
relative = expanded.sub(prefix, "")
|
|
107
|
+
!relative.start_with?("spec#{File::SEPARATOR}") &&
|
|
108
|
+
!relative.start_with?("test#{File::SEPARATOR}")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def serializable_report
|
|
112
|
+
@coverage_by_test.transform_values do |source_map|
|
|
113
|
+
source_map.to_h do |source_file, lines|
|
|
114
|
+
[File.expand_path(source_file), lines.uniq.sort]
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
data/lib/henitai/version.rb
CHANGED
data/lib/henitai.rb
CHANGED
|
@@ -41,6 +41,8 @@ module Henitai
|
|
|
41
41
|
autoload :StillbornFilter, "henitai/stillborn_filter"
|
|
42
42
|
autoload :ScenarioExecutionResult, "henitai/scenario_execution_result"
|
|
43
43
|
autoload :CoverageFormatter, "henitai/coverage_formatter"
|
|
44
|
+
autoload :MinitestCoverageReporter, "henitai/minitest_coverage_reporter"
|
|
45
|
+
autoload :PerTestCoverageCollector, "henitai/per_test_coverage_collector"
|
|
44
46
|
autoload :SyntaxValidator, "henitai/syntax_validator"
|
|
45
47
|
autoload :SamplingStrategy, "henitai/sampling_strategy"
|
|
46
48
|
autoload :TestPrioritizer, "henitai/test_prioritizer"
|
data/sig/henitai.rbs
CHANGED
|
@@ -39,6 +39,15 @@ end
|
|
|
39
39
|
|
|
40
40
|
module ::Minitest
|
|
41
41
|
def self.run: (Array[String]) -> bool
|
|
42
|
+
def self.extensions: () -> Array[String]
|
|
43
|
+
def self.reporter: () -> untyped
|
|
44
|
+
def self.plugin_henitai_coverage_init: (untyped) -> void
|
|
45
|
+
|
|
46
|
+
class Reporter
|
|
47
|
+
def initialize: (?IO, ?Hash[untyped, untyped]) -> void
|
|
48
|
+
def record: (untyped) -> void
|
|
49
|
+
def report: () -> void
|
|
50
|
+
end
|
|
42
51
|
end
|
|
43
52
|
|
|
44
53
|
module ::RSpec::Core::Formatters
|
|
@@ -177,25 +186,45 @@ module Henitai
|
|
|
177
186
|
def valid?: (Mutant) -> bool
|
|
178
187
|
end
|
|
179
188
|
|
|
180
|
-
class
|
|
189
|
+
class PerTestCoverageCollector
|
|
181
190
|
REPORT_DIR_ENV: String
|
|
182
191
|
REPORT_FILE_NAME: String
|
|
183
192
|
|
|
184
|
-
def initialize: (
|
|
185
|
-
def
|
|
186
|
-
def
|
|
193
|
+
def initialize: () -> void
|
|
194
|
+
def record_test: (String) -> void
|
|
195
|
+
def write_report: () -> void
|
|
187
196
|
|
|
188
197
|
private
|
|
189
198
|
|
|
190
199
|
def report_path: () -> String
|
|
191
200
|
def reports_dir: () -> String
|
|
192
201
|
def current_snapshot: () -> untyped
|
|
193
|
-
def
|
|
194
|
-
def
|
|
195
|
-
def
|
|
202
|
+
def warn_missing_coverage: () -> void
|
|
203
|
+
def new_lines: (untyped) -> Hash[String, Array[Integer]]
|
|
204
|
+
def new_line_numbers: (untyped, untyped) -> Array[Integer]
|
|
205
|
+
def previous_line_counts: (String) -> Array[untyped]
|
|
196
206
|
def line_counts_for: (untyped) -> Array[untyped]
|
|
197
207
|
def source_file?: (String) -> bool
|
|
198
|
-
def serializable_report: () ->
|
|
208
|
+
def serializable_report: () -> Hash[String, Hash[String, Array[Integer]]]
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
class CoverageFormatter
|
|
212
|
+
REPORT_DIR_ENV: String
|
|
213
|
+
REPORT_FILE_NAME: String
|
|
214
|
+
|
|
215
|
+
def initialize: (untyped) -> void
|
|
216
|
+
def example_finished: (untyped) -> void
|
|
217
|
+
def dump_summary: (untyped) -> void
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
class MinitestCoverageReporter < ::Minitest::Reporter
|
|
221
|
+
def initialize: (?IO, ?Hash[untyped, untyped]) -> void
|
|
222
|
+
def record: (untyped) -> void
|
|
223
|
+
def report: () -> void
|
|
224
|
+
|
|
225
|
+
private
|
|
226
|
+
|
|
227
|
+
def relative_to_pwd: (String) -> String
|
|
199
228
|
end
|
|
200
229
|
|
|
201
230
|
class SamplingStrategy
|
|
@@ -301,6 +330,10 @@ module Henitai
|
|
|
301
330
|
def with_subprocess_env: () { () -> untyped } -> untyped
|
|
302
331
|
def spec_files: () -> Array[String]
|
|
303
332
|
def fallback_spec_files: (Subject) -> Array[String]
|
|
333
|
+
def excluded_spec_files: () -> Array[String]
|
|
334
|
+
def rspec_exclude_patterns: () -> Array[String]
|
|
335
|
+
def rspec_config_lines: () -> Array[String]
|
|
336
|
+
def rspec_config_path: () -> String
|
|
304
337
|
def selection_patterns: (Subject) -> Array[String]
|
|
305
338
|
def requires_source_file?: (String, String) -> bool
|
|
306
339
|
def requires_source_file_transitively?: (String, String, ?Array[String]) -> bool
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: henitai
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Martin Otten
|
|
@@ -101,6 +101,8 @@ files:
|
|
|
101
101
|
- lib/henitai/git_diff_analyzer.rb
|
|
102
102
|
- lib/henitai/integration.rb
|
|
103
103
|
- lib/henitai/integration/rspec_process_runner.rb
|
|
104
|
+
- lib/henitai/minitest_coverage_hook.rb
|
|
105
|
+
- lib/henitai/minitest_coverage_reporter.rb
|
|
104
106
|
- lib/henitai/minitest_simplecov.rb
|
|
105
107
|
- lib/henitai/mutant.rb
|
|
106
108
|
- lib/henitai/mutant/activator.rb
|
|
@@ -129,6 +131,7 @@ files:
|
|
|
129
131
|
- lib/henitai/operators/update_operator.rb
|
|
130
132
|
- lib/henitai/parallel_execution_runner.rb
|
|
131
133
|
- lib/henitai/parser_current.rb
|
|
134
|
+
- lib/henitai/per_test_coverage_collector.rb
|
|
132
135
|
- lib/henitai/per_test_coverage_selector.rb
|
|
133
136
|
- lib/henitai/reporter.rb
|
|
134
137
|
- lib/henitai/result.rb
|
|
@@ -157,7 +160,7 @@ metadata:
|
|
|
157
160
|
changelog_uri: https://github.com/martinotten/henitai/blob/main/CHANGELOG.md
|
|
158
161
|
documentation_uri: https://github.com/martinotten/henitai/blob/main/README.md
|
|
159
162
|
homepage_uri: https://github.com/martinotten/henitai
|
|
160
|
-
source_code_uri: https://github.com/martinotten/henitai/tree/v0.1.
|
|
163
|
+
source_code_uri: https://github.com/martinotten/henitai/tree/v0.1.8
|
|
161
164
|
rubygems_mfa_required: 'true'
|
|
162
165
|
rdoc_options: []
|
|
163
166
|
require_paths:
|