henitai 0.1.3 → 0.1.4
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 +24 -1
- data/lib/henitai/coverage_bootstrapper.rb +19 -6
- data/lib/henitai/integration.rb +3 -20
- data/lib/henitai/operators/string_literal.rb +2 -1
- data/lib/henitai/reporter.rb +14 -2
- data/lib/henitai/scenario_execution_result.rb +31 -2
- data/lib/henitai/version.rb +1 -1
- data/sig/henitai.rbs +7 -0
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2380a5f3e3144cd94e68967bbf73ff7a078608e3004df80222d22646170b28b6
|
|
4
|
+
data.tar.gz: 06ee93b8a55d6dd72b12007e66929b2de22e8da984db3f033472868fa6380d92
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dc9efdf5285729c07f0ebaa6d487a1fc764589522172584468d4fb200ab2a35df706085f933f40a588447c9c599b6939db999ef2bceec42a014a2fe84d336b13
|
|
7
|
+
data.tar.gz: 83bc413591f334c60143ee7e8936d5fb0fc42b3e9865f470e20d9b9e85e26c18a9a03e8b3bbac2707fc385e3dd6b2e17dcc4d0599cbce2b5dd11a94704935f54
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.4] - 2026-04-14
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- `StringLiteral` operator no longer generates no-op mutations where the
|
|
14
|
+
replacement equals the original value (e.g. the spurious `"" → ""` mutant
|
|
15
|
+
that was emitted for methods already returning an empty string literal)
|
|
16
|
+
- Terminal diff output now uses `display_unparse` for string literal nodes,
|
|
17
|
+
making whitespace-only mutations unambiguous in the report
|
|
18
|
+
(e.g. `""`, `" "`, and `"\n"` are now visually distinct)
|
|
19
|
+
- Targeted coverage bootstrap (`--since` / explicit subjects) now correctly
|
|
20
|
+
retriggers a full suite run when the scoped bootstrap does not produce
|
|
21
|
+
coverage for all configured source files; previously the run could raise
|
|
22
|
+
`CoverageError` even though a fallback was available
|
|
23
|
+
- Coverage formatter specs now honor `HENITAI_REPORTS_DIR`, so the baseline
|
|
24
|
+
coverage bootstrap no longer fails when the suite runs under the mutation
|
|
25
|
+
runner's configured reports directory
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
- `ScenarioExecutionResult.build` factory method consolidates status and
|
|
29
|
+
exit-status derivation that was previously spread across `Integration`,
|
|
30
|
+
reducing the mutation surface of the value object
|
|
31
|
+
|
|
10
32
|
## [0.1.3] - 2026-04-13
|
|
11
33
|
|
|
12
34
|
### Added
|
|
@@ -122,7 +144,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
122
144
|
- CLI critical path: `henitai run` now executes the full pipeline, supports `--since`, returns CI-friendly exit codes, and `henitai version` prints `Henitai::VERSION`
|
|
123
145
|
- RSpec per-test coverage output: `henitai/coverage_formatter` now writes `coverage/henitai_per_test.json`
|
|
124
146
|
|
|
125
|
-
[Unreleased]: https://github.com/martinotten/henitai/compare/v0.1.
|
|
147
|
+
[Unreleased]: https://github.com/martinotten/henitai/compare/v0.1.4...HEAD
|
|
148
|
+
[0.1.4]: https://github.com/martinotten/henitai/compare/v0.1.3...v0.1.4
|
|
126
149
|
[0.1.3]: https://github.com/martinotten/henitai/compare/v0.1.2...v0.1.3
|
|
127
150
|
[0.1.2]: https://github.com/martinotten/henitai/compare/v0.1.1...v0.1.2
|
|
128
151
|
[0.1.1]: https://github.com/martinotten/henitai/compare/v0.1.0...v0.1.1
|
|
@@ -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 == existing_sources
|
|
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)
|
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
|
|
@@ -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)
|
|
@@ -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
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
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.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Martin Otten
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: prism
|
|
@@ -158,7 +158,7 @@ metadata:
|
|
|
158
158
|
changelog_uri: https://github.com/martinotten/henitai/blob/main/CHANGELOG.md
|
|
159
159
|
documentation_uri: https://github.com/martinotten/henitai/blob/main/README.md
|
|
160
160
|
homepage_uri: https://github.com/martinotten/henitai
|
|
161
|
-
source_code_uri: https://github.com/martinotten/henitai/tree/v0.1.
|
|
161
|
+
source_code_uri: https://github.com/martinotten/henitai/tree/v0.1.4
|
|
162
162
|
rubygems_mfa_required: 'true'
|
|
163
163
|
post_install_message:
|
|
164
164
|
rdoc_options: []
|