henitai 0.1.3 → 0.1.5
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 +39 -1
- data/README.md +4 -1
- data/lib/henitai/cli.rb +62 -0
- data/lib/henitai/coverage_bootstrapper.rb +19 -6
- data/lib/henitai/equivalence_detector.rb +59 -1
- data/lib/henitai/integration.rb +7 -20
- data/lib/henitai/operators/string_literal.rb +2 -1
- data/lib/henitai/reporter.rb +14 -2
- data/lib/henitai/runner.rb +3 -46
- data/lib/henitai/scenario_execution_result.rb +31 -2
- data/lib/henitai/version.rb +1 -1
- data/sig/henitai.rbs +8 -5
- metadata +4 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 28b31872e8c6d86e806caa179862bb2af3eda92425e2c9f0690fd4b5d287d573
|
|
4
|
+
data.tar.gz: d7b0d36289b8fbbe11c0a690f1b501f857e738fb997790626282846df5450724
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ed8eab942e02c4723a2a7b77f0d4b713698e61a914e1d9ab8bb3e4b1429514737dfef8160cc279a17d5d6428f6b02910f0d0258cd7e0a61342982d4a2f18e8cc
|
|
7
|
+
data.tar.gz: 4e4994d17183eecf751848d6877a300352f0e366fd715f557bc27fbdeede4e4f9164d51861bf3d1cca475fe2e789d2c80c9e2ff25331f8c34a2e957a59fbe5b9
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.5] - 2026-04-14
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Steep type errors in `Runner` after removing targeted-run bootstrap scoping:
|
|
14
|
+
updated `bootstrap_mutants` RBS signature to match the new single-argument
|
|
15
|
+
form and removed stale signatures for `refresh_coverage_for_targeted_run`,
|
|
16
|
+
`scoped_bootstrap_test_files`, `targeted_run?`, and `retry_full_bootstrap?`
|
|
17
|
+
- `RSpec/ExampleLength` offense in `coverage_bootstrapper_spec.rb` — extracted
|
|
18
|
+
workspace setup and report writing into helper methods
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- Targeted-run coverage bootstrap no longer scopes the initial run to the
|
|
22
|
+
subject's test files; the full suite is always used for the baseline,
|
|
23
|
+
trading a minor performance optimisation for reliability
|
|
24
|
+
|
|
25
|
+
## [0.1.4] - 2026-04-14
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- `StringLiteral` operator no longer generates no-op mutations where the
|
|
29
|
+
replacement equals the original value (e.g. the spurious `"" → ""` mutant
|
|
30
|
+
that was emitted for methods already returning an empty string literal)
|
|
31
|
+
- Terminal diff output now uses `display_unparse` for string literal nodes,
|
|
32
|
+
making whitespace-only mutations unambiguous in the report
|
|
33
|
+
(e.g. `""`, `" "`, and `"\n"` are now visually distinct)
|
|
34
|
+
- Targeted coverage bootstrap (`--since` / explicit subjects) now correctly
|
|
35
|
+
retriggers a full suite run when the scoped bootstrap does not produce
|
|
36
|
+
coverage for all configured source files; previously the run could raise
|
|
37
|
+
`CoverageError` even though a fallback was available
|
|
38
|
+
- Coverage formatter specs now honor `HENITAI_REPORTS_DIR`, so the baseline
|
|
39
|
+
coverage bootstrap no longer fails when the suite runs under the mutation
|
|
40
|
+
runner's configured reports directory
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
- `ScenarioExecutionResult.build` factory method consolidates status and
|
|
44
|
+
exit-status derivation that was previously spread across `Integration`,
|
|
45
|
+
reducing the mutation surface of the value object
|
|
46
|
+
|
|
10
47
|
## [0.1.3] - 2026-04-13
|
|
11
48
|
|
|
12
49
|
### Added
|
|
@@ -122,7 +159,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
122
159
|
- CLI critical path: `henitai run` now executes the full pipeline, supports `--since`, returns CI-friendly exit codes, and `henitai version` prints `Henitai::VERSION`
|
|
123
160
|
- RSpec per-test coverage output: `henitai/coverage_formatter` now writes `coverage/henitai_per_test.json`
|
|
124
161
|
|
|
125
|
-
[Unreleased]: https://github.com/martinotten/henitai/compare/v0.1.
|
|
162
|
+
[Unreleased]: https://github.com/martinotten/henitai/compare/v0.1.4...HEAD
|
|
163
|
+
[0.1.4]: https://github.com/martinotten/henitai/compare/v0.1.3...v0.1.4
|
|
126
164
|
[0.1.3]: https://github.com/martinotten/henitai/compare/v0.1.2...v0.1.3
|
|
127
165
|
[0.1.2]: https://github.com/martinotten/henitai/compare/v0.1.1...v0.1.2
|
|
128
166
|
[0.1.1]: https://github.com/martinotten/henitai/compare/v0.1.0...v0.1.1
|
data/README.md
CHANGED
|
@@ -163,10 +163,13 @@ cd henitai
|
|
|
163
163
|
bundle install
|
|
164
164
|
bundle exec rspec # run tests
|
|
165
165
|
bundle exec rubocop # lint
|
|
166
|
+
bundle exec henitai clean # remove stale generated report artifacts
|
|
166
167
|
bundle exec henitai run # dogfood
|
|
167
168
|
```
|
|
168
169
|
|
|
169
|
-
A Dev Container configuration is included (`.devcontainer/`) for VS Code with the official `ruby:4
|
|
170
|
+
A Dev Container configuration is included (`.devcontainer/`) for VS Code with the official `ruby:4`
|
|
171
|
+
image, the Codex CLI, and RTK preinstalled. Codex support is bootstrapped with
|
|
172
|
+
`rtk init -g --codex --auto-patch` during container creation.
|
|
170
173
|
|
|
171
174
|
## Architecture
|
|
172
175
|
|
data/lib/henitai/cli.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "fileutils"
|
|
3
4
|
require "optparse"
|
|
4
5
|
module Henitai
|
|
5
6
|
# Command-line interface entry point.
|
|
@@ -35,6 +36,15 @@ module Henitai
|
|
|
35
36
|
" low: 60"
|
|
36
37
|
].freeze
|
|
37
38
|
|
|
39
|
+
REPORT_CLEANUP_PATHS = [
|
|
40
|
+
%w[mutation-logs baseline.log],
|
|
41
|
+
%w[mutation-logs baseline.stdout.log],
|
|
42
|
+
%w[mutation-logs baseline.stderr.log],
|
|
43
|
+
%w[coverage .resultset.json],
|
|
44
|
+
%w[coverage .last_run.json],
|
|
45
|
+
["henitai_per_test.json"]
|
|
46
|
+
].freeze
|
|
47
|
+
|
|
38
48
|
OPERATOR_METADATA = {
|
|
39
49
|
"ArithmeticOperator" => ["Arithmetic operators", "a + b -> a - b"],
|
|
40
50
|
"EqualityOperator" => ["Comparison operators", "a == b -> a != b"],
|
|
@@ -69,6 +79,7 @@ module Henitai
|
|
|
69
79
|
command = @argv.shift
|
|
70
80
|
case command
|
|
71
81
|
when "run" then run_command
|
|
82
|
+
when "clean" then clean_command
|
|
72
83
|
when "version" then puts Henitai::VERSION
|
|
73
84
|
when "init" then init_command
|
|
74
85
|
when "operator" then operator_command
|
|
@@ -94,6 +105,18 @@ module Henitai
|
|
|
94
105
|
handle_run_error(e)
|
|
95
106
|
end
|
|
96
107
|
|
|
108
|
+
def clean_command
|
|
109
|
+
@command_halted = false
|
|
110
|
+
options = parse_clean_options
|
|
111
|
+
return if @command_halted
|
|
112
|
+
|
|
113
|
+
config = load_config(options)
|
|
114
|
+
removed_paths = cleanup_report_artifacts(config)
|
|
115
|
+
puts clean_summary(removed_paths)
|
|
116
|
+
rescue StandardError => e
|
|
117
|
+
handle_run_error(e)
|
|
118
|
+
end
|
|
119
|
+
|
|
97
120
|
def parse_run_options
|
|
98
121
|
options = {}
|
|
99
122
|
build_run_option_parser(options).parse!(@argv)
|
|
@@ -142,6 +165,21 @@ module Henitai
|
|
|
142
165
|
end
|
|
143
166
|
end
|
|
144
167
|
|
|
168
|
+
def parse_clean_options
|
|
169
|
+
options = {}
|
|
170
|
+
build_clean_option_parser(options).parse!(@argv)
|
|
171
|
+
options
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def build_clean_option_parser(options)
|
|
175
|
+
OptionParser.new do |opts|
|
|
176
|
+
opts.banner = "Usage: henitai clean [options]"
|
|
177
|
+
add_config_option(opts, options)
|
|
178
|
+
add_help_option(opts)
|
|
179
|
+
add_version_option(opts)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
145
183
|
def add_since_option(opts, options)
|
|
146
184
|
opts.on("--since GIT_REF", "Only mutate subjects changed since GIT_REF") do |ref|
|
|
147
185
|
options[:since] = ref
|
|
@@ -198,6 +236,7 @@ module Henitai
|
|
|
198
236
|
|
|
199
237
|
Usage:
|
|
200
238
|
henitai run [options] [SUBJECT_PATTERN...]
|
|
239
|
+
henitai clean [options]
|
|
201
240
|
henitai version
|
|
202
241
|
henitai init [PATH]
|
|
203
242
|
henitai operator list
|
|
@@ -207,6 +246,7 @@ module Henitai
|
|
|
207
246
|
bundle exec henitai run --since origin/main
|
|
208
247
|
bundle exec henitai run 'Foo::Bar#my_method'
|
|
209
248
|
bundle exec henitai run 'MyNamespace*' --operators full
|
|
249
|
+
bundle exec henitai clean
|
|
210
250
|
bundle exec henitai init
|
|
211
251
|
bundle exec henitai operator list
|
|
212
252
|
|
|
@@ -239,6 +279,28 @@ module Henitai
|
|
|
239
279
|
exit 2
|
|
240
280
|
end
|
|
241
281
|
|
|
282
|
+
def clean_summary(removed_paths)
|
|
283
|
+
return "No generated report artifacts to clean" if removed_paths.empty?
|
|
284
|
+
|
|
285
|
+
format(
|
|
286
|
+
"Removed %<count>s generated report artifact%<plural>s",
|
|
287
|
+
count: removed_paths.length,
|
|
288
|
+
plural: removed_paths.length == 1 ? "" : "s"
|
|
289
|
+
)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def cleanup_report_artifacts(config)
|
|
293
|
+
removed_paths = report_cleanup_paths(config).select { |path| File.exist?(path) }
|
|
294
|
+
removed_paths.each { |path| FileUtils.rm_f(path) }
|
|
295
|
+
removed_paths
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def report_cleanup_paths(config)
|
|
299
|
+
REPORT_CLEANUP_PATHS.map do |relative_path|
|
|
300
|
+
File.join(config.reports_dir, *relative_path)
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
242
304
|
def exit_status_for(result, config)
|
|
243
305
|
result.mutation_score.to_i >= config.thresholds.fetch(:low, 60) ? 0 : 1
|
|
244
306
|
end
|
|
@@ -27,7 +27,7 @@ module Henitai
|
|
|
27
27
|
bootstrap_coverage(integration, config, test_files)
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
return if coverage_available?(source_files, config)
|
|
30
|
+
return if coverage_available?(source_files, config, test_files)
|
|
31
31
|
|
|
32
32
|
raise CoverageError,
|
|
33
33
|
"Coverage data is unavailable for the configured source files"
|
|
@@ -37,24 +37,37 @@ module Henitai
|
|
|
37
37
|
|
|
38
38
|
attr_reader :static_filter
|
|
39
39
|
|
|
40
|
-
def coverage_available?(source_files, config)
|
|
40
|
+
def coverage_available?(source_files, config, test_files)
|
|
41
41
|
coverage_lines = static_filter.coverage_lines_for(config)
|
|
42
|
+
covered_sources = covered_source_files(source_files, coverage_lines)
|
|
43
|
+
existing_sources = existing_source_file_paths(source_files)
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
return covered_sources.any? if existing_sources.empty?
|
|
46
|
+
return covered_sources.any? if test_files
|
|
47
|
+
|
|
48
|
+
covered_sources.any?
|
|
46
49
|
end
|
|
47
50
|
|
|
48
51
|
def coverage_ready?(source_files, config, integration, test_files)
|
|
49
52
|
coverage_fresh?(source_files, config, integration, test_files) &&
|
|
50
|
-
coverage_available?(source_files, config) &&
|
|
53
|
+
coverage_available?(source_files, config, test_files) &&
|
|
51
54
|
per_test_coverage_ready?(source_files, config, integration, test_files)
|
|
52
55
|
end
|
|
53
56
|
|
|
57
|
+
def covered_source_files(source_files, coverage_lines)
|
|
58
|
+
source_file_paths(source_files).select do |path|
|
|
59
|
+
Array(coverage_lines[path]).any?
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
54
63
|
def source_file_paths(source_files)
|
|
55
64
|
Array(source_files).map { |path| File.expand_path(path) }
|
|
56
65
|
end
|
|
57
66
|
|
|
67
|
+
def existing_source_file_paths(source_files)
|
|
68
|
+
source_file_paths(source_files).select { |path| File.exist?(path) }
|
|
69
|
+
end
|
|
70
|
+
|
|
58
71
|
# Returns true when a coverage report already exists and is newer than
|
|
59
72
|
# every watched source and test file. Stale or absent reports return false.
|
|
60
73
|
def coverage_fresh?(source_files, config, integration, test_files)
|
|
@@ -19,7 +19,9 @@ module Henitai
|
|
|
19
19
|
private
|
|
20
20
|
|
|
21
21
|
def equivalent_mutation?(mutant)
|
|
22
|
-
equivalent_arithmetic_mutation?(mutant) ||
|
|
22
|
+
equivalent_arithmetic_mutation?(mutant) ||
|
|
23
|
+
equivalent_logical_mutation?(mutant) ||
|
|
24
|
+
equivalent_singleton_equality_mutation?(mutant)
|
|
23
25
|
end
|
|
24
26
|
|
|
25
27
|
def equivalent_arithmetic_mutation?(mutant)
|
|
@@ -137,6 +139,62 @@ module Henitai
|
|
|
137
139
|
end
|
|
138
140
|
end
|
|
139
141
|
|
|
142
|
+
# Detects `lhs == <singleton>` mutated to `lhs.equal?(<singleton>)` (or the
|
|
143
|
+
# reverse), but only when the receiver is itself a singleton literal.
|
|
144
|
+
#
|
|
145
|
+
# Both sides must be singleton literals so that we can prove, without
|
|
146
|
+
# runtime type information, that `==` reduces to identity comparison.
|
|
147
|
+
# A variable or arbitrary expression as the receiver is unsafe: for example
|
|
148
|
+
# `1.0 == 1` is true while `1.0.equal?(1)` is false, and any object with a
|
|
149
|
+
# custom `#==` can exhibit the same divergence.
|
|
150
|
+
#
|
|
151
|
+
# Singleton types accepted on both receiver and RHS:
|
|
152
|
+
# Symbol – interned; only one instance of :foo ever exists in a process.
|
|
153
|
+
# nil/true/false – singletons by language specification.
|
|
154
|
+
def equivalent_singleton_equality_mutation?(mutant)
|
|
155
|
+
original = mutant.original_node
|
|
156
|
+
mutated = mutant.mutated_node
|
|
157
|
+
|
|
158
|
+
equality_send?(original) && equality_send?(mutated) &&
|
|
159
|
+
same_receiver?(original, mutated) &&
|
|
160
|
+
singleton_literal?(original.children[0]) &&
|
|
161
|
+
singleton_rhs_match?(original, mutated) &&
|
|
162
|
+
equality_operators?(original.children[1], mutated.children[1])
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def singleton_rhs_match?(original, mutated)
|
|
166
|
+
rhs = original.children[2]
|
|
167
|
+
singleton_literal?(rhs) && same_node?(rhs, mutated.children[2])
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def equality_send?(node)
|
|
171
|
+
node.is_a?(Parser::AST::Node) &&
|
|
172
|
+
node.type == :send &&
|
|
173
|
+
node.children.size == 3 &&
|
|
174
|
+
equality_operator?(node.children[1])
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def equality_operator?(operator)
|
|
178
|
+
%i[== equal?].include?(operator)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def equality_operators?(op_a, op_b)
|
|
182
|
+
equality_operator?(op_a) && equality_operator?(op_b) && op_a != op_b
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Returns true for AST nodes that represent Ruby singleton values:
|
|
186
|
+
# symbols, nil, true, and false.
|
|
187
|
+
def singleton_literal?(node)
|
|
188
|
+
return false unless node.is_a?(Parser::AST::Node)
|
|
189
|
+
|
|
190
|
+
# rubocop:disable Lint/BooleanSymbol
|
|
191
|
+
case node.type
|
|
192
|
+
when :sym, :nil, :true, :false then true
|
|
193
|
+
else false
|
|
194
|
+
end
|
|
195
|
+
# rubocop:enable Lint/BooleanSymbol
|
|
196
|
+
end
|
|
197
|
+
|
|
140
198
|
def same_node?(left, right)
|
|
141
199
|
left == right
|
|
142
200
|
end
|
data/lib/henitai/integration.rb
CHANGED
|
@@ -314,35 +314,18 @@ module Henitai
|
|
|
314
314
|
end
|
|
315
315
|
|
|
316
316
|
def build_result(wait_result, log_paths)
|
|
317
|
-
status = scenario_status(wait_result)
|
|
318
317
|
stdout = read_log_file(log_paths[:stdout_path])
|
|
319
318
|
stderr = read_log_file(log_paths[:stderr_path])
|
|
320
319
|
write_combined_log(log_paths[:log_path], stdout, stderr)
|
|
321
320
|
|
|
322
|
-
ScenarioExecutionResult.
|
|
323
|
-
|
|
321
|
+
ScenarioExecutionResult.build(
|
|
322
|
+
wait_result:,
|
|
324
323
|
stdout:,
|
|
325
324
|
stderr:,
|
|
326
|
-
log_path: log_paths[:log_path]
|
|
327
|
-
exit_status: exit_status_for(wait_result)
|
|
325
|
+
log_path: log_paths[:log_path]
|
|
328
326
|
)
|
|
329
327
|
end
|
|
330
328
|
|
|
331
|
-
def scenario_status(wait_result)
|
|
332
|
-
return :timeout if wait_result == :timeout
|
|
333
|
-
return :compile_error if exit_status_for(wait_result) == 2
|
|
334
|
-
return :survived if wait_result.respond_to?(:success?) && wait_result.success?
|
|
335
|
-
|
|
336
|
-
:killed
|
|
337
|
-
end
|
|
338
|
-
|
|
339
|
-
def exit_status_for(wait_result)
|
|
340
|
-
return nil if wait_result == :timeout
|
|
341
|
-
return nil unless wait_result.respond_to?(:exitstatus)
|
|
342
|
-
|
|
343
|
-
wait_result.exitstatus
|
|
344
|
-
end
|
|
345
|
-
|
|
346
329
|
def spec_files
|
|
347
330
|
Dir.glob("spec/**/*_spec.rb")
|
|
348
331
|
end
|
|
@@ -494,6 +477,10 @@ module Henitai
|
|
|
494
477
|
# runner. Minitest shares selection and execution semantics, but per-test
|
|
495
478
|
# coverage collection is not yet wired into this path.
|
|
496
479
|
class Minitest < Rspec
|
|
480
|
+
def per_test_coverage_supported?
|
|
481
|
+
false
|
|
482
|
+
end
|
|
483
|
+
|
|
497
484
|
def run_mutant(mutant:, test_files:, timeout:)
|
|
498
485
|
setup_load_path
|
|
499
486
|
super
|
|
@@ -27,7 +27,8 @@ module Henitai
|
|
|
27
27
|
private
|
|
28
28
|
|
|
29
29
|
def mutate_plain_string(node, subject:)
|
|
30
|
-
|
|
30
|
+
original_value = node.children.first
|
|
31
|
+
REPLACEMENTS.reject { |r| r == original_value }.map do |replacement|
|
|
31
32
|
build_mutant(
|
|
32
33
|
subject:,
|
|
33
34
|
original_node: node,
|
data/lib/henitai/reporter.rb
CHANGED
|
@@ -126,11 +126,23 @@ module Henitai
|
|
|
126
126
|
end
|
|
127
127
|
|
|
128
128
|
def original_line(mutant)
|
|
129
|
-
format("- %s",
|
|
129
|
+
format("- %s", display_unparse(mutant.original_node))
|
|
130
130
|
end
|
|
131
131
|
|
|
132
132
|
def mutated_line(mutant)
|
|
133
|
-
format("+ %s",
|
|
133
|
+
format("+ %s", display_unparse(mutant.mutated_node))
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Like safe_unparse but makes invisible characters visible in terminal
|
|
137
|
+
# output. For string literal nodes the inner value is shown via #inspect
|
|
138
|
+
# so that e.g. "" vs " " vs "\n" are unambiguous. Other nodes unparse
|
|
139
|
+
# normally.
|
|
140
|
+
def display_unparse(node)
|
|
141
|
+
if node.respond_to?(:type) && node.respond_to?(:children) && node.type == :str
|
|
142
|
+
node.children.first.inspect
|
|
143
|
+
else
|
|
144
|
+
safe_unparse(node)
|
|
145
|
+
end
|
|
134
146
|
end
|
|
135
147
|
|
|
136
148
|
def score_line(result)
|
data/lib/henitai/runner.rb
CHANGED
|
@@ -41,10 +41,6 @@ module Henitai
|
|
|
41
41
|
# The thread is joined before Gate 3 (static filtering), which is the first
|
|
42
42
|
# phase that requires coverage data.
|
|
43
43
|
#
|
|
44
|
-
# For targeted runs (`subjects:` provided), the bootstrap is further scoped
|
|
45
|
-
# to the spec files that cover the requested subjects rather than the full
|
|
46
|
-
# suite, reducing the baseline run time proportionally.
|
|
47
|
-
#
|
|
48
44
|
# @return [Result]
|
|
49
45
|
def run
|
|
50
46
|
started_at = Time.now
|
|
@@ -76,26 +72,15 @@ module Henitai
|
|
|
76
72
|
end
|
|
77
73
|
|
|
78
74
|
def mutants_for(subjects, source_files)
|
|
79
|
-
bootstrap_thread = bootstrap_mutants(source_files
|
|
75
|
+
bootstrap_thread = bootstrap_mutants(source_files)
|
|
80
76
|
mutants = generate_mutants(subjects)
|
|
81
77
|
bootstrap_thread.value
|
|
82
78
|
|
|
83
|
-
filtered_mutants = filter_mutants(mutants)
|
|
84
|
-
return filtered_mutants unless targeted_run?
|
|
85
|
-
|
|
86
|
-
refresh_coverage_for_targeted_run(filtered_mutants, source_files)
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def refresh_coverage_for_targeted_run(mutants, source_files)
|
|
90
|
-
return mutants unless retry_full_bootstrap?(mutants)
|
|
91
|
-
|
|
92
|
-
bootstrap_coverage(source_files)
|
|
93
79
|
filter_mutants(mutants)
|
|
94
80
|
end
|
|
95
81
|
|
|
96
|
-
def bootstrap_mutants(source_files
|
|
97
|
-
|
|
98
|
-
Thread.new { bootstrap_coverage(source_files, scoped_tests) }
|
|
82
|
+
def bootstrap_mutants(source_files)
|
|
83
|
+
Thread.new { bootstrap_coverage(source_files) }
|
|
99
84
|
end
|
|
100
85
|
|
|
101
86
|
def execute_mutants(mutants)
|
|
@@ -131,21 +116,6 @@ module Henitai
|
|
|
131
116
|
@result
|
|
132
117
|
end
|
|
133
118
|
|
|
134
|
-
# Returns the spec files to use for the coverage bootstrap.
|
|
135
|
-
#
|
|
136
|
-
# For full runs (no subject pattern given), returns nil so the bootstrapper
|
|
137
|
-
# falls back to the integration's full test-file list.
|
|
138
|
-
#
|
|
139
|
-
# For targeted runs, returns the union of test files selected for each
|
|
140
|
-
# resolved subject. Falls back to nil (all tests) if the selection is empty,
|
|
141
|
-
# so the bootstrapper always has a non-empty file list.
|
|
142
|
-
def scoped_bootstrap_test_files(subjects)
|
|
143
|
-
return nil if pattern_subjects.empty?
|
|
144
|
-
|
|
145
|
-
files = subjects.flat_map { |subject| integration.select_tests(subject) }.uniq
|
|
146
|
-
files.empty? ? nil : files
|
|
147
|
-
end
|
|
148
|
-
|
|
149
119
|
def bootstrap_coverage(source_files, test_files = nil)
|
|
150
120
|
coverage_bootstrapper.ensure!(source_files:, config:, integration:, test_files:)
|
|
151
121
|
end
|
|
@@ -217,19 +187,6 @@ module Henitai
|
|
|
217
187
|
Array(@subjects)
|
|
218
188
|
end
|
|
219
189
|
|
|
220
|
-
def targeted_run?
|
|
221
|
-
!pattern_subjects.empty?
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
def retry_full_bootstrap?(mutants)
|
|
225
|
-
executable_mutants = Array(mutants).reject do |mutant|
|
|
226
|
-
%i[ignored compile_error equivalent].include?(mutant.status)
|
|
227
|
-
end
|
|
228
|
-
return false if executable_mutants.empty?
|
|
229
|
-
|
|
230
|
-
executable_mutants.all? { |mutant| mutant.status == :no_coverage }
|
|
231
|
-
end
|
|
232
|
-
|
|
233
190
|
def unique_subjects(subjects)
|
|
234
191
|
subjects.uniq { |subject| [subject.expression, subject.source_file] }
|
|
235
192
|
end
|
|
@@ -5,6 +5,16 @@ module Henitai
|
|
|
5
5
|
class ScenarioExecutionResult
|
|
6
6
|
attr_reader :status, :stdout, :stderr, :exit_status, :log_path
|
|
7
7
|
|
|
8
|
+
def self.build(wait_result:, stdout:, stderr:, log_path:)
|
|
9
|
+
new(
|
|
10
|
+
status: status_for(wait_result),
|
|
11
|
+
stdout: stdout,
|
|
12
|
+
stderr: stderr,
|
|
13
|
+
log_path: log_path,
|
|
14
|
+
exit_status: exit_status_for(wait_result)
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
8
18
|
def initialize(status:, stdout:, stderr:, log_path:, exit_status: nil)
|
|
9
19
|
@status = status
|
|
10
20
|
@stdout = stdout.to_s
|
|
@@ -51,11 +61,11 @@ module Henitai
|
|
|
51
61
|
log_text.lines.last(lines).join
|
|
52
62
|
end
|
|
53
63
|
|
|
54
|
-
def should_show_logs?(all_logs:
|
|
64
|
+
def should_show_logs?(all_logs: nil)
|
|
55
65
|
all_logs || timeout?
|
|
56
66
|
end
|
|
57
67
|
|
|
58
|
-
def failure_tail(all_logs:
|
|
68
|
+
def failure_tail(all_logs: nil, lines: 12)
|
|
59
69
|
return combined_output if all_logs
|
|
60
70
|
return "" unless should_show_logs?(all_logs:)
|
|
61
71
|
|
|
@@ -64,6 +74,25 @@ module Henitai
|
|
|
64
74
|
|
|
65
75
|
private
|
|
66
76
|
|
|
77
|
+
class << self
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def status_for(wait_result)
|
|
81
|
+
return :timeout if wait_result == :timeout
|
|
82
|
+
return :compile_error if exit_status_for(wait_result) == 2
|
|
83
|
+
return :survived if wait_result.respond_to?(:success?) && wait_result.success?
|
|
84
|
+
|
|
85
|
+
:killed
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def exit_status_for(wait_result)
|
|
89
|
+
return nil if wait_result == :timeout
|
|
90
|
+
return nil unless wait_result.respond_to?(:exitstatus)
|
|
91
|
+
|
|
92
|
+
wait_result.exitstatus
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
67
96
|
def stream_section(name, content)
|
|
68
97
|
"#{name}:\n#{content}"
|
|
69
98
|
end
|
data/lib/henitai/version.rb
CHANGED
data/sig/henitai.rbs
CHANGED
|
@@ -112,6 +112,7 @@ module Henitai
|
|
|
112
112
|
attr_reader exit_status: Integer?
|
|
113
113
|
attr_reader log_path: String
|
|
114
114
|
|
|
115
|
+
def self.build: (wait_result: untyped, stdout: String?, stderr: String?, log_path: String) -> ScenarioExecutionResult
|
|
115
116
|
def initialize: (status: Symbol, stdout: String?, stderr: String?, log_path: String, ?exit_status: Integer?) -> void
|
|
116
117
|
def survived?: () -> bool
|
|
117
118
|
def killed?: () -> bool
|
|
@@ -121,6 +122,11 @@ module Henitai
|
|
|
121
122
|
def tail: (?Integer) -> String
|
|
122
123
|
def should_show_logs?: (?all_logs: bool) -> bool
|
|
123
124
|
def failure_tail: (?all_logs: bool, ?lines: Integer) -> String
|
|
125
|
+
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
def self.status_for: (untyped) -> Symbol
|
|
129
|
+
def self.exit_status_for: (untyped) -> Integer?
|
|
124
130
|
end
|
|
125
131
|
|
|
126
132
|
class Subject
|
|
@@ -474,6 +480,7 @@ module Henitai
|
|
|
474
480
|
def survived_mutant_header: (Mutant) -> String
|
|
475
481
|
def original_line: (Mutant) -> String
|
|
476
482
|
def mutated_line: (Mutant) -> String
|
|
483
|
+
def display_unparse: (untyped) -> String
|
|
477
484
|
def score_line: (Result) -> String
|
|
478
485
|
def format_row: (String, untyped) -> String
|
|
479
486
|
def count_status: (Result, Symbol) -> Integer
|
|
@@ -606,13 +613,9 @@ module Henitai
|
|
|
606
613
|
|
|
607
614
|
def build_result: (Array[Mutant], Time, Time) -> Result
|
|
608
615
|
def persist_history: (Result, Time) -> void
|
|
609
|
-
def refresh_coverage_for_targeted_run: (Array[Mutant], Array[String]) -> Array[Mutant]
|
|
610
616
|
def bootstrap_coverage: (Array[String], ?Array[String]?) -> void
|
|
611
|
-
def bootstrap_mutants: (Array[String]
|
|
617
|
+
def bootstrap_mutants: (Array[String]) -> Thread
|
|
612
618
|
def mutants_for: (Array[Subject], Array[String]) -> Array[Mutant]
|
|
613
|
-
def scoped_bootstrap_test_files: (Array[Subject]) -> Array[String]?
|
|
614
|
-
def targeted_run?: () -> bool
|
|
615
|
-
def retry_full_bootstrap?: (Array[Mutant]) -> bool
|
|
616
619
|
def with_reports_dir: () { () -> untyped } -> untyped
|
|
617
620
|
def result_thresholds: () -> Hash[Symbol, Integer]?
|
|
618
621
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
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.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Martin Otten
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: prism
|
|
@@ -158,9 +157,8 @@ metadata:
|
|
|
158
157
|
changelog_uri: https://github.com/martinotten/henitai/blob/main/CHANGELOG.md
|
|
159
158
|
documentation_uri: https://github.com/martinotten/henitai/blob/main/README.md
|
|
160
159
|
homepage_uri: https://github.com/martinotten/henitai
|
|
161
|
-
source_code_uri: https://github.com/martinotten/henitai/tree/v0.1.
|
|
160
|
+
source_code_uri: https://github.com/martinotten/henitai/tree/v0.1.4
|
|
162
161
|
rubygems_mfa_required: 'true'
|
|
163
|
-
post_install_message:
|
|
164
162
|
rdoc_options: []
|
|
165
163
|
require_paths:
|
|
166
164
|
- lib
|
|
@@ -175,8 +173,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
175
173
|
- !ruby/object:Gem::Version
|
|
176
174
|
version: '0'
|
|
177
175
|
requirements: []
|
|
178
|
-
rubygems_version:
|
|
179
|
-
signing_key:
|
|
176
|
+
rubygems_version: 4.0.6
|
|
180
177
|
specification_version: 4
|
|
181
178
|
summary: Mutation testing for Ruby
|
|
182
179
|
test_files: []
|