henitai 0.1.0 → 0.1.2
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 +30 -1
- data/lib/henitai/coverage_bootstrapper.rb +0 -1
- data/lib/henitai/coverage_formatter.rb +4 -4
- data/lib/henitai/integration.rb +75 -14
- data/lib/henitai/minitest_simplecov.rb +15 -0
- data/lib/henitai/mutant/activator.rb +78 -42
- data/lib/henitai/rspec_coverage_formatter.rb +10 -0
- data/lib/henitai/static_filter.rb +43 -4
- data/lib/henitai/version.rb +1 -1
- data/sig/henitai.rbs +14 -1
- metadata +18 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d11d8d5361258fac7bddc006b8fd8485d1d085c3eaf294afc5dc97c6eed9ee7c
|
|
4
|
+
data.tar.gz: c3620c79cd2b1e59ec43abb143775e0b5edcbc2ed89b3384caa39703e7b9759a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eed2ea62f262d95b79bae130a0d19437749c07b8873d532bd029755a72d9600335131167da004fe01dcb9d83427fb6155de1d841df4ea8dac0e2b4651de33d26
|
|
7
|
+
data.tar.gz: a4709f5c4804af79ea9599a62dfce714487c6d947bc8f85459cebebe206c4f0f7df965bff8c0aedacca227dceb67b32876733e9bfb450dc96357e2ef879f07c4
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.2] - 2026-04-07
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- `Henitai::Mutant::Activator` now rewrites heredoc-backed method bodies from
|
|
14
|
+
source slices instead of unparsing the whole body, eliminating timeouts on
|
|
15
|
+
HTML reporter mutants
|
|
16
|
+
- `henitai run -v` now stops before the run pipeline starts
|
|
17
|
+
|
|
18
|
+
## [0.1.1] - 2026-04-03
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- Minitest integration for Rails projects: injects SimpleCov for coverage
|
|
22
|
+
collection, sets `RAILS_ENV=test` and `PARALLEL_WORKERS=1` in the baseline
|
|
23
|
+
subprocess, preloads `config/environment.rb` before mutant activation, adds
|
|
24
|
+
`test/` to `$LOAD_PATH` before forking, and excludes `test/system/` by default
|
|
25
|
+
- `simplecov` runtime dependency (required by the Minitest integration)
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- `rspec/core` was unconditionally required at load time, causing a `LoadError`
|
|
29
|
+
in projects that do not have RSpec installed — now loaded lazily only when the
|
|
30
|
+
RSpec integration is used
|
|
31
|
+
- Coverage path normalisation now uses `File.realpath` so symlinked temp
|
|
32
|
+
directories on macOS no longer cause false no-coverage results
|
|
33
|
+
|
|
34
|
+
## [0.1.0] - 2026-03-01
|
|
35
|
+
|
|
10
36
|
### Added
|
|
11
37
|
- Initial gem scaffold with Ruby 4.0.2 support
|
|
12
38
|
- Dev Container configuration (official `ruby:4.0.2-alpine` base image, Codex CLI preinstalled)
|
|
@@ -16,4 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
16
42
|
- CLI critical path: `henitai run` now executes the full pipeline, supports `--since`, returns CI-friendly exit codes, and `henitai version` prints `Henitai::VERSION`
|
|
17
43
|
- RSpec per-test coverage output: `henitai/coverage_formatter` now writes `coverage/henitai_per_test.json`
|
|
18
44
|
|
|
19
|
-
[Unreleased]: https://github.com/martinotten/henitai/
|
|
45
|
+
[Unreleased]: https://github.com/martinotten/henitai/compare/v0.1.2...HEAD
|
|
46
|
+
[0.1.2]: https://github.com/martinotten/henitai/compare/v0.1.1...v0.1.2
|
|
47
|
+
[0.1.1]: https://github.com/martinotten/henitai/compare/v0.1.0...v0.1.1
|
|
48
|
+
[0.1.0]: https://github.com/martinotten/henitai/releases/tag/v0.1.0
|
|
@@ -10,8 +10,6 @@ module Henitai
|
|
|
10
10
|
REPORT_DIR_ENV = "HENITAI_REPORTS_DIR"
|
|
11
11
|
REPORT_FILE_NAME = "henitai_per_test.json"
|
|
12
12
|
|
|
13
|
-
RSpec::Core::Formatters.register self, :example_finished, :dump_summary
|
|
14
|
-
|
|
15
13
|
def initialize(_output)
|
|
16
14
|
@coverage_by_test = Hash.new do |hash, test_file|
|
|
17
15
|
hash[test_file] = Hash.new { |nested, source_file| nested[source_file] = [] }
|
|
@@ -91,7 +89,7 @@ module Henitai
|
|
|
91
89
|
def line_counts_for(file_coverage)
|
|
92
90
|
case file_coverage
|
|
93
91
|
when Hash
|
|
94
|
-
Array(file_coverage["lines"])
|
|
92
|
+
Array(file_coverage[:lines] || file_coverage["lines"])
|
|
95
93
|
else
|
|
96
94
|
Array(file_coverage)
|
|
97
95
|
end
|
|
@@ -105,7 +103,9 @@ module Henitai
|
|
|
105
103
|
|
|
106
104
|
def serializable_report
|
|
107
105
|
@coverage_by_test.transform_values do |source_map|
|
|
108
|
-
source_map.
|
|
106
|
+
source_map.to_h do |source_file, lines|
|
|
107
|
+
[File.expand_path(source_file), lines.uniq.sort]
|
|
108
|
+
end
|
|
109
109
|
end
|
|
110
110
|
end
|
|
111
111
|
end
|
data/lib/henitai/integration.rb
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
require "minitest"
|
|
5
|
-
require "rspec/core"
|
|
6
5
|
|
|
7
6
|
module Henitai
|
|
8
7
|
# Namespace for test-framework integrations.
|
|
@@ -57,8 +56,8 @@ module Henitai
|
|
|
57
56
|
|
|
58
57
|
def build_child_output_files(log_paths)
|
|
59
58
|
{
|
|
60
|
-
original_stdout:
|
|
61
|
-
original_stderr:
|
|
59
|
+
original_stdout: stdout_stream.dup,
|
|
60
|
+
original_stderr: stderr_stream.dup,
|
|
62
61
|
stdout_file: File.new(log_paths[:stdout_path], "w"),
|
|
63
62
|
stderr_file: File.new(log_paths[:stderr_path], "w")
|
|
64
63
|
}
|
|
@@ -70,13 +69,17 @@ module Henitai
|
|
|
70
69
|
end
|
|
71
70
|
|
|
72
71
|
def redirect_child_output(output_files)
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
reopen_child_output_stream(stdout_stream, output_files[:stdout_file])
|
|
73
|
+
reopen_child_output_stream(stderr_stream, output_files[:stderr_file])
|
|
74
|
+
$stdout = stdout_stream
|
|
75
|
+
$stderr = stderr_stream
|
|
75
76
|
end
|
|
76
77
|
|
|
77
78
|
def restore_child_output(output_files)
|
|
78
|
-
reopen_child_output_stream(
|
|
79
|
-
reopen_child_output_stream(
|
|
79
|
+
reopen_child_output_stream(stdout_stream, output_files[:original_stdout])
|
|
80
|
+
reopen_child_output_stream(stderr_stream, output_files[:original_stderr])
|
|
81
|
+
$stdout = output_files[:original_stdout]
|
|
82
|
+
$stderr = output_files[:original_stderr]
|
|
80
83
|
end
|
|
81
84
|
|
|
82
85
|
def reopen_child_output_stream(stream, original_stream)
|
|
@@ -95,6 +98,14 @@ module Henitai
|
|
|
95
98
|
reports_dir = ENV.fetch("HENITAI_REPORTS_DIR", "reports")
|
|
96
99
|
File.join(reports_dir, "mutation-coverage", mutant_id.to_s)
|
|
97
100
|
end
|
|
101
|
+
|
|
102
|
+
def stdout_stream
|
|
103
|
+
@stdout_stream ||= IO.for_fd(1)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def stderr_stream
|
|
107
|
+
@stderr_stream ||= IO.for_fd(2)
|
|
108
|
+
end
|
|
98
109
|
end
|
|
99
110
|
|
|
100
111
|
# Integration adapter for RSpec.
|
|
@@ -106,7 +117,12 @@ module Henitai
|
|
|
106
117
|
def self.for(name)
|
|
107
118
|
const_get(name.capitalize)
|
|
108
119
|
rescue NameError
|
|
109
|
-
|
|
120
|
+
available = constants.filter_map do |constant_name|
|
|
121
|
+
integration = const_get(constant_name)
|
|
122
|
+
constant_name.to_s.downcase if integration.is_a?(Class) && integration < Base
|
|
123
|
+
end.sort.join(", ")
|
|
124
|
+
|
|
125
|
+
raise ArgumentError, "Unknown integration: #{name}. Available: #{available}"
|
|
110
126
|
end
|
|
111
127
|
|
|
112
128
|
# Base class for all integrations.
|
|
@@ -131,6 +147,12 @@ module Henitai
|
|
|
131
147
|
def run_mutant(mutant:, test_files:, timeout:)
|
|
132
148
|
raise NotImplementedError
|
|
133
149
|
end
|
|
150
|
+
|
|
151
|
+
private
|
|
152
|
+
|
|
153
|
+
def pause(seconds)
|
|
154
|
+
sleep(seconds)
|
|
155
|
+
end
|
|
134
156
|
end
|
|
135
157
|
|
|
136
158
|
# RSpec integration adapter.
|
|
@@ -186,6 +208,7 @@ module Henitai
|
|
|
186
208
|
private
|
|
187
209
|
|
|
188
210
|
def run_in_child(mutant:, test_files:, log_paths:)
|
|
211
|
+
Thread.report_on_exception = false
|
|
189
212
|
scenario_log_support.with_coverage_dir(mutant.id) do
|
|
190
213
|
scenario_log_support.capture_child_output(log_paths) do
|
|
191
214
|
return 2 if Mutant::Activator.activate!(mutant) == :compile_error
|
|
@@ -224,6 +247,7 @@ module Henitai
|
|
|
224
247
|
end
|
|
225
248
|
|
|
226
249
|
def run_tests(test_files)
|
|
250
|
+
require "rspec/core"
|
|
227
251
|
status = RSpec::Core::Runner.run(test_files + rspec_options)
|
|
228
252
|
return status if status.is_a?(Integer)
|
|
229
253
|
|
|
@@ -231,11 +255,7 @@ module Henitai
|
|
|
231
255
|
end
|
|
232
256
|
|
|
233
257
|
def rspec_options
|
|
234
|
-
["--require", "henitai/
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
def pause(seconds)
|
|
238
|
-
sleep(seconds)
|
|
258
|
+
["--require", "henitai/rspec_coverage_formatter"]
|
|
239
259
|
end
|
|
240
260
|
|
|
241
261
|
def scenario_log_support
|
|
@@ -391,10 +411,33 @@ module Henitai
|
|
|
391
411
|
# runner. Minitest shares selection and execution semantics, but per-test
|
|
392
412
|
# coverage collection is not yet wired into this path.
|
|
393
413
|
class Minitest < Rspec
|
|
414
|
+
def run_mutant(mutant:, test_files:, timeout:)
|
|
415
|
+
setup_load_path
|
|
416
|
+
super
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
def run_in_child(mutant:, test_files:, log_paths:)
|
|
420
|
+
ENV["RAILS_ENV"] = "test" unless ENV["RAILS_ENV"] == "test"
|
|
421
|
+
preload_environment
|
|
422
|
+
super
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
def run_suite(test_files, timeout: DEFAULT_SUITE_TIMEOUT)
|
|
426
|
+
log_paths = scenario_log_paths("baseline")
|
|
427
|
+
FileUtils.mkdir_p(File.dirname(log_paths[:stdout_path]))
|
|
428
|
+
pid = File.open(log_paths[:stdout_path], "w") do |stdout_file|
|
|
429
|
+
File.open(log_paths[:stderr_path], "w") do |stderr_file|
|
|
430
|
+
Process.spawn(subprocess_env, *suite_command(test_files), out: stdout_file, err: stderr_file)
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
build_result(wait_with_timeout(pid, timeout), log_paths)
|
|
434
|
+
end
|
|
435
|
+
|
|
394
436
|
private
|
|
395
437
|
|
|
396
438
|
def suite_command(test_files)
|
|
397
439
|
["bundle", "exec", "ruby", "-I", "test",
|
|
440
|
+
"-r", "henitai/minitest_simplecov",
|
|
398
441
|
"-e", "ARGV.each { |f| require File.expand_path(f) }",
|
|
399
442
|
*test_files]
|
|
400
443
|
end
|
|
@@ -409,8 +452,26 @@ module Henitai
|
|
|
409
452
|
status == true ? 0 : 1
|
|
410
453
|
end
|
|
411
454
|
|
|
455
|
+
def preload_environment
|
|
456
|
+
env_file = File.expand_path("config/environment.rb")
|
|
457
|
+
require env_file if File.exist?(env_file)
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def setup_load_path
|
|
461
|
+
test_dir = File.expand_path("test")
|
|
462
|
+
$LOAD_PATH.unshift(test_dir) unless $LOAD_PATH.include?(test_dir)
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def subprocess_env
|
|
466
|
+
env = {} # : Hash[String, String]
|
|
467
|
+
env["RAILS_ENV"] = "test" unless ENV["RAILS_ENV"] == "test"
|
|
468
|
+
env["PARALLEL_WORKERS"] = "1"
|
|
469
|
+
env
|
|
470
|
+
end
|
|
471
|
+
|
|
412
472
|
def spec_files
|
|
413
|
-
Dir.glob("test/**/*_test.rb") + Dir.glob("test/**/*_spec.rb")
|
|
473
|
+
(Dir.glob("test/**/*_test.rb") + Dir.glob("test/**/*_spec.rb"))
|
|
474
|
+
.reject { |f| f.start_with?("test/system/") }
|
|
414
475
|
end
|
|
415
476
|
end
|
|
416
477
|
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Injected by henitai into the Minitest baseline subprocess to collect
|
|
4
|
+
# line coverage and write it as a SimpleCov-compatible .resultset.json.
|
|
5
|
+
#
|
|
6
|
+
# Must be required before any application code is loaded so that Coverage
|
|
7
|
+
# tracking is active from the first line.
|
|
8
|
+
|
|
9
|
+
require "coverage"
|
|
10
|
+
Coverage.start(lines: true, branches: true, methods: true)
|
|
11
|
+
|
|
12
|
+
require "simplecov"
|
|
13
|
+
|
|
14
|
+
SimpleCov.coverage_dir(ENV.fetch("HENITAI_COVERAGE_DIR", "coverage"))
|
|
15
|
+
SimpleCov.start
|
|
@@ -7,6 +7,22 @@ module Henitai
|
|
|
7
7
|
class Mutant
|
|
8
8
|
# Activates a mutant inside the forked child process.
|
|
9
9
|
class Activator
|
|
10
|
+
# Filters "already initialized constant" C-level warnings that fire when
|
|
11
|
+
# a source file is loaded into a process that already has the constant
|
|
12
|
+
# defined via require. Uses a thread-local flag so the filter is active
|
|
13
|
+
# only during load_source_file, leaving all other warnings untouched.
|
|
14
|
+
module ConstantRedefinitionFilter
|
|
15
|
+
PATTERN = /already initialized constant|previous definition of/
|
|
16
|
+
private_constant :PATTERN
|
|
17
|
+
|
|
18
|
+
def warn(msg, **kwargs)
|
|
19
|
+
return if Thread.current[:henitai_filter_const_warnings] && PATTERN.match?(msg.to_s)
|
|
20
|
+
|
|
21
|
+
super
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
Warning.singleton_class.prepend(ConstantRedefinitionFilter)
|
|
25
|
+
|
|
10
26
|
SERIALIZER_METHODS = {
|
|
11
27
|
arg: :argument_parameter_fragment,
|
|
12
28
|
optarg: :optional_parameter_fragment,
|
|
@@ -26,8 +42,9 @@ module Henitai
|
|
|
26
42
|
subject = mutant.subject
|
|
27
43
|
raise ArgumentError, "Cannot activate wildcard subjects" if subject.method_name.nil?
|
|
28
44
|
|
|
45
|
+
target = target_for(subject)
|
|
29
46
|
Henitai::WarningSilencer.silence do
|
|
30
|
-
|
|
47
|
+
target.class_eval(method_source(mutant), __FILE__, __LINE__ + 1)
|
|
31
48
|
nil
|
|
32
49
|
end
|
|
33
50
|
rescue Unparser::UnsupportedNodeError
|
|
@@ -57,44 +74,28 @@ module Henitai
|
|
|
57
74
|
subject_node = mutant.subject.ast_node
|
|
58
75
|
return compile_safe_unparse(mutant.mutated_node) unless subject_node
|
|
59
76
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
mutant.original_node,
|
|
63
|
-
mutant.mutated_node
|
|
64
|
-
)
|
|
65
|
-
body = method_body(mutated_subject) || Parser::AST::Node.new(:nil, [])
|
|
66
|
-
compile_safe_unparse(body)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def replace_node(node, original_node, mutated_node)
|
|
70
|
-
return mutated_node if same_node?(node, original_node)
|
|
71
|
-
return node unless node.is_a?(Parser::AST::Node)
|
|
77
|
+
body = method_body(subject_node)
|
|
78
|
+
return compile_safe_unparse(Parser::AST::Node.new(:nil, [])) unless body
|
|
72
79
|
|
|
73
|
-
|
|
74
|
-
replace_child(child, original_node, mutated_node)
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
return node if updated_children == node.children
|
|
78
|
-
|
|
79
|
-
Parser::AST::Node.new(node.type, updated_children)
|
|
80
|
+
body_source_for_mutant(body, mutant)
|
|
80
81
|
end
|
|
81
82
|
|
|
82
|
-
def
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return
|
|
83
|
+
def body_source_for_mutant(body, mutant)
|
|
84
|
+
original_range = mutant.original_node.location&.expression
|
|
85
|
+
location = body.location
|
|
86
|
+
return source_body(location, body) unless original_range && location
|
|
86
87
|
|
|
87
|
-
|
|
88
|
+
replacement = compile_safe_unparse(mutant.mutated_node)
|
|
89
|
+
body_source_for_location(location, original_range, replacement, body)
|
|
88
90
|
end
|
|
89
91
|
|
|
90
|
-
def
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
when Array
|
|
95
|
-
child.map { |item| replace_child(item, original_node, mutated_node) }
|
|
92
|
+
def body_source_for_location(location, original_range, replacement, body)
|
|
93
|
+
if heredoc_location?(location)
|
|
94
|
+
heredoc_body_source(location, original_range, replacement) ||
|
|
95
|
+
source_body(location, body)
|
|
96
96
|
else
|
|
97
|
-
|
|
97
|
+
expression_source(location, original_range, replacement) ||
|
|
98
|
+
source_body(location, body)
|
|
98
99
|
end
|
|
99
100
|
end
|
|
100
101
|
|
|
@@ -184,6 +185,38 @@ module Henitai
|
|
|
184
185
|
subject_node.children[1]
|
|
185
186
|
end
|
|
186
187
|
|
|
188
|
+
def heredoc_location?(location)
|
|
189
|
+
location.respond_to?(:heredoc_body) && location.heredoc_body
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def heredoc_body_source(location, original_range, replacement)
|
|
193
|
+
body_source = replace_source_fragment(
|
|
194
|
+
location.heredoc_body,
|
|
195
|
+
original_range,
|
|
196
|
+
replacement
|
|
197
|
+
)
|
|
198
|
+
return unless body_source
|
|
199
|
+
|
|
200
|
+
"#{location.expression.source}\n#{body_source}#{location.heredoc_end.source}"
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def source_body(location, body)
|
|
204
|
+
return compile_safe_unparse(body) unless location
|
|
205
|
+
|
|
206
|
+
if heredoc_location?(location)
|
|
207
|
+
"#{location.expression.source}\n#{location.heredoc_body.source}#{location.heredoc_end.source}"
|
|
208
|
+
else
|
|
209
|
+
location.expression.source
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def expression_source(location, original_range, replacement)
|
|
214
|
+
source_range = location.expression
|
|
215
|
+
return unless source_range
|
|
216
|
+
|
|
217
|
+
replace_source_fragment(source_range, original_range, replacement)
|
|
218
|
+
end
|
|
219
|
+
|
|
187
220
|
def load_target(subject)
|
|
188
221
|
Object.const_get(subject.namespace.delete_prefix("::"))
|
|
189
222
|
rescue NameError
|
|
@@ -195,7 +228,10 @@ module Henitai
|
|
|
195
228
|
source_file = subject.source_file || source_file_from_ast(subject)
|
|
196
229
|
return unless source_file && File.file?(source_file)
|
|
197
230
|
|
|
231
|
+
Thread.current[:henitai_filter_const_warnings] = true
|
|
198
232
|
load(source_file)
|
|
233
|
+
ensure
|
|
234
|
+
Thread.current[:henitai_filter_const_warnings] = false
|
|
199
235
|
end
|
|
200
236
|
|
|
201
237
|
def source_file_from_ast(subject)
|
|
@@ -211,17 +247,17 @@ module Henitai
|
|
|
211
247
|
expression.source_buffer.name
|
|
212
248
|
end
|
|
213
249
|
|
|
214
|
-
def
|
|
215
|
-
|
|
216
|
-
|
|
250
|
+
def replace_source_fragment(source_range, original_range, replacement)
|
|
251
|
+
source = source_range.source
|
|
252
|
+
start = original_range.begin_pos - source_range.begin_pos
|
|
253
|
+
stop = original_range.end_pos - source_range.begin_pos
|
|
254
|
+
return unless start >= 0 && stop <= source.bytesize && start <= stop
|
|
255
|
+
|
|
256
|
+
prefix = source.byteslice(0, start)
|
|
257
|
+
suffix = source.byteslice(stop, source.bytesize - stop)
|
|
258
|
+
return unless prefix && suffix
|
|
217
259
|
|
|
218
|
-
|
|
219
|
-
expression.source_buffer.name,
|
|
220
|
-
expression.line,
|
|
221
|
-
expression.column,
|
|
222
|
-
expression.last_line,
|
|
223
|
-
expression.last_column
|
|
224
|
-
]
|
|
260
|
+
prefix + replacement + suffix
|
|
225
261
|
end
|
|
226
262
|
|
|
227
263
|
def compile_safe_unparse(node)
|
|
@@ -32,6 +32,7 @@ module Henitai
|
|
|
32
32
|
per_test_coverage_report_path = per_test_coverage_report_path(config)
|
|
33
33
|
|
|
34
34
|
coverage_lines = coverage_lines_by_file(coverage_report_path)
|
|
35
|
+
coverage_lines = merge_method_coverage(coverage_lines, coverage_report_path)
|
|
35
36
|
return coverage_lines unless coverage_lines.empty?
|
|
36
37
|
|
|
37
38
|
coverage_lines_from_test_lines(
|
|
@@ -97,9 +98,10 @@ module Henitai
|
|
|
97
98
|
|
|
98
99
|
def covered?(mutant, coverage_lines)
|
|
99
100
|
file = normalize_path(mutant.location[:file])
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
covered = Array(coverage_lines[file])
|
|
102
|
+
(mutant.location[:start_line]..mutant.location[:end_line]).any? do |line|
|
|
103
|
+
covered.include?(line)
|
|
104
|
+
end
|
|
103
105
|
end
|
|
104
106
|
|
|
105
107
|
def source_for(mutant)
|
|
@@ -115,6 +117,40 @@ module Henitai
|
|
|
115
117
|
@compiled_ignore_patterns[patterns] ||= patterns.map { |pattern| Regexp.new(pattern) }
|
|
116
118
|
end
|
|
117
119
|
|
|
120
|
+
def merge_method_coverage(coverage_lines, path)
|
|
121
|
+
return coverage_lines unless File.exist?(path)
|
|
122
|
+
|
|
123
|
+
JSON.parse(File.read(path)).each_value do |suite|
|
|
124
|
+
suite.fetch("coverage", {}).each do |file, file_coverage|
|
|
125
|
+
merge_file_method_coverage(coverage_lines, file, file_coverage)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
coverage_lines.transform_values(&:sort)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def merge_file_method_coverage(coverage_lines, file, file_coverage)
|
|
133
|
+
methods = file_coverage["methods"]
|
|
134
|
+
return unless methods.is_a?(Hash)
|
|
135
|
+
|
|
136
|
+
normalized = normalize_path(file)
|
|
137
|
+
methods.each do |key, count|
|
|
138
|
+
next unless count.to_i.positive?
|
|
139
|
+
|
|
140
|
+
range = method_line_range(key)
|
|
141
|
+
next unless range
|
|
142
|
+
|
|
143
|
+
coverage_lines[normalized] = Array(coverage_lines[normalized]) | range.to_a
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def method_line_range(key)
|
|
148
|
+
m = key.match(/(\d+), \d+, (\d+), \d+\]\z/)
|
|
149
|
+
return unless m
|
|
150
|
+
|
|
151
|
+
(m.captures.first.to_i..m.captures.last.to_i)
|
|
152
|
+
end
|
|
153
|
+
|
|
118
154
|
def covered_lines(file_coverage)
|
|
119
155
|
Array(file_coverage["lines"]).each_with_index.filter_map do |count, index|
|
|
120
156
|
index + 1 if count.to_i.positive?
|
|
@@ -147,7 +183,10 @@ module Henitai
|
|
|
147
183
|
end
|
|
148
184
|
|
|
149
185
|
def normalize_path(path)
|
|
150
|
-
File.expand_path(path)
|
|
186
|
+
expanded = File.expand_path(path)
|
|
187
|
+
File.realpath(expanded)
|
|
188
|
+
rescue Errno::ENOENT, Errno::ENOTDIR
|
|
189
|
+
expanded
|
|
151
190
|
end
|
|
152
191
|
|
|
153
192
|
def equivalence_detector
|
data/lib/henitai/version.rb
CHANGED
data/sig/henitai.rbs
CHANGED
|
@@ -223,6 +223,10 @@ module Henitai
|
|
|
223
223
|
def test_files: () -> Array[String]
|
|
224
224
|
def run_mutant: (mutant: Mutant, test_files: Array[String], timeout: Float) -> ScenarioExecutionResult
|
|
225
225
|
def run_suite: (Array[String], ?timeout: Float) -> ScenarioExecutionResult
|
|
226
|
+
|
|
227
|
+
private
|
|
228
|
+
|
|
229
|
+
def pause: (Float) -> void
|
|
226
230
|
end
|
|
227
231
|
|
|
228
232
|
class ScenarioLogSupport
|
|
@@ -240,6 +244,8 @@ module Henitai
|
|
|
240
244
|
def reopen_child_output_stream: (untyped, untyped) -> void
|
|
241
245
|
def close_child_output_files: (Hash[Symbol, untyped]) -> void
|
|
242
246
|
def mutation_coverage_dir: (String) -> String
|
|
247
|
+
def stdout_stream: () -> IO
|
|
248
|
+
def stderr_stream: () -> IO
|
|
243
249
|
end
|
|
244
250
|
|
|
245
251
|
class Rspec < Base
|
|
@@ -265,7 +271,6 @@ module Henitai
|
|
|
265
271
|
def combined_log: (String, String) -> String
|
|
266
272
|
def scenario_log_paths: (String) -> Hash[Symbol, String]
|
|
267
273
|
def run_tests: (Array[String]) -> Integer
|
|
268
|
-
def pause: (Float) -> void
|
|
269
274
|
def scenario_log_support: () -> ScenarioLogSupport
|
|
270
275
|
def rspec_options: () -> Array[String]
|
|
271
276
|
def spec_files: () -> Array[String]
|
|
@@ -282,9 +287,17 @@ module Henitai
|
|
|
282
287
|
end
|
|
283
288
|
|
|
284
289
|
class Minitest < Rspec
|
|
290
|
+
def run_mutant: (mutant: Mutant, test_files: Array[String], timeout: Float) -> ScenarioExecutionResult
|
|
291
|
+
def run_suite: (Array[String], ?timeout: Float) -> ScenarioExecutionResult
|
|
292
|
+
|
|
285
293
|
private
|
|
286
294
|
|
|
295
|
+
def run_in_child: (mutant: Mutant, test_files: Array[String], log_paths: Hash[Symbol, String]) -> Integer
|
|
296
|
+
def suite_command: (Array[String]) -> Array[String]
|
|
287
297
|
def run_tests: (Array[String]) -> Integer
|
|
298
|
+
def preload_environment: () -> void
|
|
299
|
+
def setup_load_path: () -> void
|
|
300
|
+
def subprocess_env: () -> Hash[String, String]
|
|
288
301
|
def spec_files: () -> Array[String]
|
|
289
302
|
end
|
|
290
303
|
end
|
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.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Martin Otten
|
|
@@ -23,6 +23,20 @@ dependencies:
|
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '1.5'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: simplecov
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.22'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.22'
|
|
26
40
|
- !ruby/object:Gem::Dependency
|
|
27
41
|
name: sqlite3
|
|
28
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -83,6 +97,7 @@ files:
|
|
|
83
97
|
- lib/henitai/execution_engine.rb
|
|
84
98
|
- lib/henitai/git_diff_analyzer.rb
|
|
85
99
|
- lib/henitai/integration.rb
|
|
100
|
+
- lib/henitai/minitest_simplecov.rb
|
|
86
101
|
- lib/henitai/mutant.rb
|
|
87
102
|
- lib/henitai/mutant/activator.rb
|
|
88
103
|
- lib/henitai/mutant_generator.rb
|
|
@@ -107,6 +122,7 @@ files:
|
|
|
107
122
|
- lib/henitai/parser_current.rb
|
|
108
123
|
- lib/henitai/reporter.rb
|
|
109
124
|
- lib/henitai/result.rb
|
|
125
|
+
- lib/henitai/rspec_coverage_formatter.rb
|
|
110
126
|
- lib/henitai/runner.rb
|
|
111
127
|
- lib/henitai/sampling_strategy.rb
|
|
112
128
|
- lib/henitai/scenario_execution_result.rb
|
|
@@ -131,7 +147,7 @@ metadata:
|
|
|
131
147
|
changelog_uri: https://github.com/martinotten/henitai/blob/main/CHANGELOG.md
|
|
132
148
|
documentation_uri: https://github.com/martinotten/henitai/blob/main/README.md
|
|
133
149
|
homepage_uri: https://github.com/martinotten/henitai
|
|
134
|
-
source_code_uri: https://github.com/martinotten/henitai/tree/v0.1.
|
|
150
|
+
source_code_uri: https://github.com/martinotten/henitai/tree/v0.1.2
|
|
135
151
|
rubygems_mfa_required: 'true'
|
|
136
152
|
rdoc_options: []
|
|
137
153
|
require_paths:
|