henitai 0.5.3 → 0.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 +222 -1
- data/README.md +100 -13
- data/assets/schema/henitai.schema.json +20 -0
- data/lib/henitai/arid_node_filter.rb +4 -19
- data/lib/henitai/bootstrap_error_message.rb +20 -0
- data/lib/henitai/canonical_report_merger.rb +23 -2
- data/lib/henitai/cli/run_command.rb +25 -7
- data/lib/henitai/cli/run_options.rb +1 -1
- data/lib/henitai/cli.rb +4 -1
- data/lib/henitai/configuration.rb +4 -1
- data/lib/henitai/configuration_validator/rules.rb +8 -0
- data/lib/henitai/configuration_validator/scalars.rb +9 -0
- data/lib/henitai/configuration_validator.rb +2 -1
- data/lib/henitai/constant_path.rb +48 -0
- data/lib/henitai/control_run.rb +73 -0
- data/lib/henitai/coverage_bootstrapper.rb +36 -10
- data/lib/henitai/coverage_formatter.rb +26 -3
- data/lib/henitai/coverage_report_reader.rb +21 -0
- data/lib/henitai/equivalence_detector/operand_predicates.rb +16 -0
- data/lib/henitai/equivalence_detector.rb +46 -38
- data/lib/henitai/example_summary.rb +24 -0
- data/lib/henitai/execution_engine/fork_server_scope.rb +57 -0
- data/lib/henitai/execution_engine.rb +75 -60
- data/lib/henitai/fidelity_check.rb +173 -0
- data/lib/henitai/flaky_detection_baseline.rb +61 -0
- data/lib/henitai/flaky_mutant_retrier.rb +43 -0
- data/lib/henitai/flaky_retry_decision.rb +23 -0
- data/lib/henitai/flaky_test_registry.rb +63 -0
- data/lib/henitai/fork_server/client.rb +231 -0
- data/lib/henitai/fork_server/group_killer.rb +48 -0
- data/lib/henitai/fork_server/group_terminator.rb +47 -0
- data/lib/henitai/fork_server/launcher.rb +38 -0
- data/lib/henitai/fork_server/mutant_runner.rb +141 -0
- data/lib/henitai/fork_server/protocol.rb +73 -0
- data/lib/henitai/fork_server/reaper.rb +24 -0
- data/lib/henitai/fork_server/runtime.rb +109 -0
- data/lib/henitai/fork_server/server.rb +108 -0
- data/lib/henitai/fork_server/server_process.rb +73 -0
- data/lib/henitai/fork_server/status.rb +52 -0
- data/lib/henitai/fork_server.rb +43 -0
- data/lib/henitai/generated_artifacts.rb +3 -0
- data/lib/henitai/git_diff_analyzer.rb +28 -11
- data/lib/henitai/incremental_filter.rb +20 -10
- data/lib/henitai/integration/base.rb +72 -5
- data/lib/henitai/integration/capped_log_sink.rb +34 -0
- data/lib/henitai/integration/child_bootstrap.rb +7 -0
- data/lib/henitai/integration/child_report_store.rb +36 -0
- data/lib/henitai/integration/minitest.rb +7 -5
- data/lib/henitai/integration/minitest_autorun.rb +20 -0
- data/lib/henitai/integration/minitest_outcome_reporter.rb +28 -0
- data/lib/henitai/integration/minitest_preload.rb +30 -0
- data/lib/henitai/integration/minitest_runnable_filter.rb +61 -0
- data/lib/henitai/integration/minitest_test_runner.rb +32 -15
- data/lib/henitai/integration/mutant_run_support.rb +73 -13
- data/lib/henitai/integration/rspec_child_runner.rb +44 -9
- data/lib/henitai/integration/rspec_preload.rb +35 -0
- data/lib/henitai/integration/rspec_process_runner.rb +4 -1
- data/lib/henitai/integration/scenario_log_support.rb +31 -18
- data/lib/henitai/integration.rb +16 -3
- data/lib/henitai/minitest_coverage_reporter.rb +22 -6
- data/lib/henitai/minitest_result_file.rb +62 -0
- data/lib/henitai/mutant/activation.rb +32 -0
- data/lib/henitai/mutant/activator.rb +75 -55
- data/lib/henitai/mutant/def_injection.rb +136 -0
- data/lib/henitai/mutant/lexical_scope_check.rb +120 -0
- data/lib/henitai/mutant/lexical_target.rb +105 -0
- data/lib/henitai/mutant/replacement.rb +52 -0
- data/lib/henitai/mutant.rb +27 -8
- data/lib/henitai/mutant_generator.rb +62 -18
- data/lib/henitai/mutant_history_store/verdict_cache.rb +12 -11
- data/lib/henitai/mutant_history_store.rb +14 -4
- data/lib/henitai/mutant_identity.rb +5 -1
- data/lib/henitai/mutant_verdict.rb +71 -0
- data/lib/henitai/operators/array_declaration.rb +4 -0
- data/lib/henitai/operators/assignment_expression.rb +2 -0
- data/lib/henitai/operators/conditional_expression.rb +21 -1
- data/lib/henitai/operators/hash_literal.rb +7 -4
- data/lib/henitai/operators/method_expression.rb +14 -3
- data/lib/henitai/operators/regex_metacharacters.rb +89 -0
- data/lib/henitai/operators/regex_mutator.rb +23 -28
- data/lib/henitai/operators/return_value.rb +1 -1
- data/lib/henitai/operators.rb +1 -0
- data/lib/henitai/per_test_coverage.rb +11 -7
- data/lib/henitai/per_test_coverage_collector.rb +30 -31
- data/lib/henitai/per_test_coverage_delta.rb +60 -0
- data/lib/henitai/per_test_coverage_selector.rb +26 -13
- data/lib/henitai/process_wakeup.rb +8 -3
- data/lib/henitai/process_worker_runner.rb +19 -5
- data/lib/henitai/reporter.rb +15 -8
- data/lib/henitai/result.rb +32 -16
- data/lib/henitai/reuse_context.rb +50 -0
- data/lib/henitai/rspec_coverage_formatter.rb +4 -0
- data/lib/henitai/rspec_outcomes.rb +72 -0
- data/lib/henitai/runner.rb +58 -19
- data/lib/henitai/runner_dependencies.rb +22 -1
- data/lib/henitai/sampling_strategy.rb +36 -6
- data/lib/henitai/scenario_execution_result.rb +28 -5
- data/lib/henitai/signature_line.rb +21 -0
- data/lib/henitai/slot_scheduler/draining.rb +12 -2
- data/lib/henitai/slot_scheduler/process_control.rb +33 -0
- data/lib/henitai/slot_scheduler/retry_policy.rb +8 -6
- data/lib/henitai/slot_scheduler.rb +47 -8
- data/lib/henitai/source_file_selection.rb +1 -1
- data/lib/henitai/source_parser.rb +29 -9
- data/lib/henitai/static_filter.rb +45 -14
- data/lib/henitai/stillborn_filter.rb +70 -2
- data/lib/henitai/subject.rb +8 -3
- data/lib/henitai/subject_resolver.rb +65 -31
- data/lib/henitai/suite_boot_clock.rb +48 -0
- data/lib/henitai/survivor_activation_cache.rb +18 -11
- data/lib/henitai/survivor_rerun_strategy.rb +47 -43
- data/lib/henitai/test_file_outcomes.rb +79 -0
- data/lib/henitai/timeout_calibrator.rb +31 -9
- data/lib/henitai/timeout_ceiling.rb +37 -0
- data/lib/henitai/verdict_fingerprint.rb +29 -30
- data/lib/henitai/version.rb +1 -1
- data/lib/henitai.rb +17 -1
- data/sig/configuration_validator.rbs +1 -0
- data/sig/henitai.rbs +225 -45
- metadata +46 -3
- data/lib/henitai/survivor_test_filter.rb +0 -72
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4c9ef6e2d806e8d12acfd36adda062192c5cf4ba700ee140ce7d6ad848eff0c8
|
|
4
|
+
data.tar.gz: 800ba49ba72ce0ab6090e1ab9b40ea5146559738e59af5a81ceeb80923bc974e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 60acc4fc6df82c4d273c110302af6a73b7c4f7d6f3ebb2ae9fdcea06ca69e5cef2e61945c396b38810f978b8ace3db2b988d224b637d0f8f11c02ccb03eee368
|
|
7
|
+
data.tar.gz: 9fe40adf6368041348247af329f3153a0027e89bbbe60e9dcdef27a4a666512446ab7569188e891ec7eb2f5fe8419c33eeee2ed1778a5b034c97901ca94a095c
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,226 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.6.0] - 2026-10-20
|
|
11
|
+
|
|
12
|
+
Minor release with a deliberate score break: false kills and harness
|
|
13
|
+
failures no longer count as detected mutants, so most projects, including
|
|
14
|
+
Henitai's own dogfood run, will report lower Mutation Scores. Re-measure
|
|
15
|
+
before adjusting thresholds.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **Scores will drop, by design.** A mutant now counts as detected only when a
|
|
20
|
+
test failed because of its mutation (ADR-14). Harness failures that used to
|
|
21
|
+
be credited as kills are now reported as `CompileError` or `RuntimeError`,
|
|
22
|
+
with the reason in `statusReason`. On an assertion-free test suite, 0.5.3
|
|
23
|
+
reported 130 of 137 mutants as killed; this release reports none. Projects
|
|
24
|
+
should expect lower Mutation Scores. Re-measure before adjusting thresholds.
|
|
25
|
+
- **Flaky-test handling.** Survivors are no longer retried: a retry could only
|
|
26
|
+
turn a survivor into a kill and inflate the score. Kills are re-checked, up
|
|
27
|
+
to `max_flaky_retries`, only when every failing test file is known to be
|
|
28
|
+
flaky, as found by the new opt-in `mutation.flaky_detection` (baseline suite
|
|
29
|
+
runs twice; flaky files go to `henitai_flaky_tests.json`, removed by
|
|
30
|
+
`henitai clean`). Without it nothing is retried, so runtime on survivor-heavy
|
|
31
|
+
suites drops. The ">5% of mutants needed a retry" warning is replaced by a
|
|
32
|
+
report of flaky tests and re-checked kills. Child reports now carry
|
|
33
|
+
`failed_tests`. Failures outside an example (an `after(:context)` hook error,
|
|
34
|
+
a suite-level hook, a Minitest result without a file) are attributed to their
|
|
35
|
+
spec file when RSpec names it; otherwise the run is flagged
|
|
36
|
+
`unattributed_failures`, which makes a kill genuine (never retried) and makes
|
|
37
|
+
the flaky baseline reject the run. Baseline runs that fail on a file the two
|
|
38
|
+
runs cannot call flaky (for example one seen in a single run) now abort
|
|
39
|
+
instead of being accepted. Minitest outcomes are keyed to the file defining
|
|
40
|
+
the test class, not to a shared module that supplies the test method.
|
|
41
|
+
- Mutant runs are classified from an explicit report written by the child
|
|
42
|
+
process instead of from its exit status. An exception while activating the
|
|
43
|
+
mutant (for example a `NameError` from a superclass defined in another file)
|
|
44
|
+
is `CompileError`, not `Killed`. A child killed by a signal, or one that
|
|
45
|
+
exits without reporting, is `RuntimeError`, not `Killed`. A `SystemExit`
|
|
46
|
+
raised by the code under test is `RuntimeError`, not `Survived`.
|
|
47
|
+
- Mutants are activated only after the spec or test files have loaded, so a
|
|
48
|
+
subject whose superclass or constants live in other files can be tested.
|
|
49
|
+
Previously the child loaded the subject file on its own and failed with a
|
|
50
|
+
`NameError` (every `ApplicationRecord` model, for example).
|
|
51
|
+
- Mutants are activated by re-evaluating the subject's own `def` inside its
|
|
52
|
+
original module and class openers (ADR-13), not through `define_method`.
|
|
53
|
+
Constant lookup through enclosing modules, implicit `super`, `yield`,
|
|
54
|
+
`block_given?`, argument forwarding, default values, `__FILE__`/`__LINE__`,
|
|
55
|
+
`frozen_string_literal` and visibility now behave as in the original method.
|
|
56
|
+
Mutations in default values take effect, and splices no longer shift after
|
|
57
|
+
multibyte characters or leave heredoc bodies behind. Activation recipes from
|
|
58
|
+
earlier versions are ignored and regenerated.
|
|
59
|
+
|
|
60
|
+
### Added
|
|
61
|
+
|
|
62
|
+
- `mutation.sampling.seed` (integer >= 0, default `0`) and real stratified
|
|
63
|
+
sampling: per subject `max(1, ceil(size * ratio))` mutants, strata by
|
|
64
|
+
operator ordered by `SHA256(seed, stable_id)`, visited round-robin, original
|
|
65
|
+
order kept. The report records `"sampling": {strategy, ratio, seed}`.
|
|
66
|
+
- Report fields: top-level `completeFiles` (non-authoritative runs) and
|
|
67
|
+
per-file `sessionId` and `gitSha` provenance.
|
|
68
|
+
- `rake oracle:reuse` (run in CI): on a scratch copy of the oracle corpus, the
|
|
69
|
+
`--incremental` verdicts must equal a fresh full run after scripted edits to
|
|
70
|
+
source, tests, a helper and the configuration.
|
|
71
|
+
- **Fork server for Minitest and parallel runs.** `mutation.fork_server`
|
|
72
|
+
(opt-in, ADR-15) now supports Minitest and `jobs > 1`: the environment is
|
|
73
|
+
booted once and each mutant process is forked from the server. Falls back to
|
|
74
|
+
per-mutant boot with a warning if the server is unavailable.
|
|
75
|
+
- A fidelity check rejects, before execution, every mutant whose activated
|
|
76
|
+
code differs from the original method by more than the reported mutation.
|
|
77
|
+
Examples are a negated condition that lost its parentheses, keyword
|
|
78
|
+
arguments re-rendered as a positional hash, `|x|` re-rendered as `|x,|`, a
|
|
79
|
+
splice shifted by a multibyte character, and a removed assignment that turns
|
|
80
|
+
a later local-variable read into a method call. Such mutants are reported as
|
|
81
|
+
`CompileError` with the reason.
|
|
82
|
+
- `mutation.control_runs` (default `true`): after execution, every subject
|
|
83
|
+
with a detected verdict runs once more with its unmutated method injected.
|
|
84
|
+
If its tests fail, the subject's detected verdicts are reclassified as
|
|
85
|
+
`CompileError`, because the injection itself broke them.
|
|
86
|
+
- Methods defined in a constructor block assigned to a constant
|
|
87
|
+
(`Point = Struct.new(:x) do ... end`, likewise `Data.define` and
|
|
88
|
+
`Class.new`) and in `class << CONST` are now subjects. Top-level
|
|
89
|
+
`def Const.name` methods are subjects as well.
|
|
90
|
+
- `rake oracle` runs a corpus whose verdicts follow from its construction and
|
|
91
|
+
fails on false kills or false survivors. CI runs it.
|
|
92
|
+
|
|
93
|
+
### Fixed
|
|
94
|
+
|
|
95
|
+
- **Verdict reuse is bound to a reuse context.** `--incremental` reused stale
|
|
96
|
+
`Killed` verdicts after a spec helper, the configuration or the henitai
|
|
97
|
+
version changed. Every cached `Killed` and `Survived` verdict now stores a
|
|
98
|
+
`ReuseContext` fingerprint (test dependency files, henitai version,
|
|
99
|
+
integration, operators, `test_excludes`, `coverage_criteria`) and is reused
|
|
100
|
+
only under an identical one; rows recorded without it are never reused.
|
|
101
|
+
`rake oracle:reuse` reproduced 121 mismatches against fresh runs after a
|
|
102
|
+
helper edit before the fix and none after it.
|
|
103
|
+
- **`--survivors-from` no longer skips survivors on a git heuristic.** The
|
|
104
|
+
selected survivors go through the same content-fingerprint reuse rules as
|
|
105
|
+
`--incremental`, always: new or edited tests and helpers invalidate them,
|
|
106
|
+
reused survivors are reported with `fromCache`, and recipe stubs without a
|
|
107
|
+
source range are always re-executed. `SurvivorTestFilter` is removed.
|
|
108
|
+
The recipe fast path now refreshes coverage (Gate 0) so new or edited tests
|
|
109
|
+
are selected, recipe stubs also run their name-matched tests, and survivor
|
|
110
|
+
reruns never sample (sampling had dropped prior survivors before matching).
|
|
111
|
+
`--force` now also bypasses this reuse. Duplicate programs are collapsed
|
|
112
|
+
before sampling, so a sampled run keeps the same representative as an
|
|
113
|
+
unsampled survivor rerun and its survivors are found again.
|
|
114
|
+
- A survived verdict whose covering-test fingerprint cannot be recomputed keeps
|
|
115
|
+
stored fingerprints only from a survived row of the same subject source,
|
|
116
|
+
never from a killed row or an edited method.
|
|
117
|
+
- Gate 1 source selection has its own per-test coverage instance, so reuse and
|
|
118
|
+
history read the post-bootstrap per-test map.
|
|
119
|
+
- Recipe stubs keep their precomputed stable ID and persist the `replacement`
|
|
120
|
+
source in the history store.
|
|
121
|
+
- **Trend history only records authoritative runs.** `--since`,
|
|
122
|
+
subject-scoped and survivor-rerun results no longer add a `runs` row.
|
|
123
|
+
- **Canonical report merge.** A scoped run that mutated every subject of a file
|
|
124
|
+
replaces that file's prior entry wholesale (and removes it when it has no
|
|
125
|
+
mutants), so stale mutants of edited or deleted code no longer linger.
|
|
126
|
+
`--survivors-from <canonical report>` redirects to the session snapshot only
|
|
127
|
+
when every file entry carries the report's `sessionId`; a file whose every
|
|
128
|
+
prior mutant was re-run takes the new run's provenance.
|
|
129
|
+
- `--since` diffs against the merge base (`REF...HEAD`) instead of the ref's
|
|
130
|
+
tip, so commits that landed on the base branch after the fork point are not
|
|
131
|
+
treated as changes.
|
|
132
|
+
- The dashboard reporter uploads the merged canonical report for scoped runs
|
|
133
|
+
instead of the partial one.
|
|
134
|
+
- `Pending` mutants are excluded from the MS and MSI denominators; MSI is
|
|
135
|
+
`killed / (total - pending)`.
|
|
136
|
+
- **Equivalence detection only flags sound cases.** `false || x` and
|
|
137
|
+
`true && x` (left literal only); `n + 0`, `n * 1` and siblings only for
|
|
138
|
+
Integer/Float literal receivers (not `-0.0`); `==` <-> `eql?` only when both
|
|
139
|
+
operands are string literals; `==` <-> `equal?` for singleton literals.
|
|
140
|
+
Unsound rules (`x || false`, `x && true`, unknown receivers, a single string
|
|
141
|
+
literal) no longer mark mutants `Equivalent`.
|
|
142
|
+
- **`mutation.ignore_patterns` is line-scoped.** A pattern is matched against
|
|
143
|
+
the source line where the mutant's node starts, in `StaticFilter`; matching
|
|
144
|
+
mutants are reported as `Ignored` with an `ignore_reason` instead of being
|
|
145
|
+
dropped at generation time. An enclosing construct whose body matches is no
|
|
146
|
+
longer suppressed. The line is decoded honouring a magic encoding comment,
|
|
147
|
+
so a non-UTF-8 source file no longer aborts filtering.
|
|
148
|
+
- **Review follow-ups.** The slot scheduler no longer signals the process
|
|
149
|
+
group of a reaped pid that owns no mutant slot. Minitest results for tests
|
|
150
|
+
added by reopening a class in another file are attributed to the file that
|
|
151
|
+
defines the method, so coverage-first selection no longer misses them. With
|
|
152
|
+
the fork server active, calibrated timeouts keep the measured suite boot
|
|
153
|
+
(served mutants still run `before(:suite)` hooks). The `fork_server` schema
|
|
154
|
+
description and the remediation plan now state the real scope (RSpec and
|
|
155
|
+
Minitest, any `jobs`).
|
|
156
|
+
- **`--incremental` no longer reuses stale survived verdicts for def-line
|
|
157
|
+
mutants.** Their test selection is widened with name-matched tests that the
|
|
158
|
+
survivor fingerprint does not cover, so survived verdicts for mutants on a
|
|
159
|
+
method's def line always re-execute (killed reuse is unaffected: a kill by
|
|
160
|
+
an unchanged covering test remains valid).
|
|
161
|
+
- **Output capture no longer delays or races on lingering grandchildren.**
|
|
162
|
+
Closing a mutant child's captured stdout/stderr no longer waits up to 1 s
|
|
163
|
+
per stream for EOF and no longer kills the drain thread mid-write (which
|
|
164
|
+
could exceed the cap or write the truncation marker twice). The drain
|
|
165
|
+
thread is stopped cooperatively through a stop pipe and takes what is
|
|
166
|
+
already readable.
|
|
167
|
+
- **Fork-server lifecycle.** A server lost or stalled mid-run no longer turns
|
|
168
|
+
its in-flight mutants into `runtime_error`: they are rerun through the
|
|
169
|
+
per-mutant-boot fallback. Fallback runs use boot-inclusive timeouts. Mutant
|
|
170
|
+
processes forked by the server are cleaned up on timeout, loss and teardown
|
|
171
|
+
even before they lead a process group, and the connection and wake-pipe fds
|
|
172
|
+
are no longer inherited by mutant processes. The parallel event loop no
|
|
173
|
+
longer stalls on an exit event already buffered in the client, and the
|
|
174
|
+
SIGTERM drain window always runs its full length.
|
|
175
|
+
- The "negated condition" mutant now negates the whole condition
|
|
176
|
+
(`!(a > b)`), not just its left operand (`!a > b`).
|
|
177
|
+
- Source files are read as UTF-8 regardless of the process locale. With `LANG`
|
|
178
|
+
unset or `C`, any non-ASCII source file used to abort subject resolution.
|
|
179
|
+
- Per-test coverage now narrows RSpec test selection. RSpec records test files
|
|
180
|
+
as `./spec/x_spec.rb` while candidates arrive as `spec/x_spec.rb`, so the
|
|
181
|
+
lookup never matched and every mutant ran all candidate files.
|
|
182
|
+
- `--since` and the dirty-source and survivor checks now work for a project in
|
|
183
|
+
a sub-directory of its git repository and for non-ASCII file names.
|
|
184
|
+
- A corrupt or locked `mutation-history.sqlite3` no longer discards the run's
|
|
185
|
+
reports. History recording and export now warn instead.
|
|
186
|
+
- Forked mutant children reset the INT, TERM, HUP and CHLD handlers inherited
|
|
187
|
+
from the parent, so a draining SIGTERM is no longer ignored.
|
|
188
|
+
- A zero-example check no longer matches `10 examples, 0 failures`, nor a
|
|
189
|
+
`No examples found.` or `0 examples, 0 failures` quoted in failure output;
|
|
190
|
+
only the run's last summary line counts. Such kills were reported as
|
|
191
|
+
`CompileError`.
|
|
192
|
+
- A run in which harness errors (`CompileError`) left no mutant to score now
|
|
193
|
+
exits `1` with a warning instead of passing its threshold with nothing
|
|
194
|
+
tested.
|
|
195
|
+
- Mutating an `elsif` condition no longer rewrites the whole `if` chain; only
|
|
196
|
+
the condition changes.
|
|
197
|
+
- String parts of interpolated strings, symbols, backticks and regexps, and
|
|
198
|
+
elements of `%w` arrays, are no longer mutated as standalone strings. Their
|
|
199
|
+
replacement text changed the literal's syntax rather than its value.
|
|
200
|
+
- Operator replacements are parenthesised when spliced, so `a && b && c`
|
|
201
|
+
mutated to `a || b` no longer reads as `a || (b && c)`.
|
|
202
|
+
- Two mutants at the same position with the same description no longer share
|
|
203
|
+
one stable identity and history entry.
|
|
204
|
+
- `ReturnValue` mutates only the method's final expression, not every
|
|
205
|
+
expression that happens to equal it.
|
|
206
|
+
- Subject expressions for predicate, bang, setter and operator methods, such
|
|
207
|
+
as `Foo#empty?`, `Foo#save!`, `Foo#name=` and `Foo#==`, select their method
|
|
208
|
+
instead of nothing.
|
|
209
|
+
- Mutants from different operators that produce the same program run once.
|
|
210
|
+
- The implicit value of a shorthand hash pair (`build(config:)`) is no longer
|
|
211
|
+
mutated. Its replacement rewrote the key to `build(nil:)`.
|
|
212
|
+
- A mutant that removes the only assignment of a local variable read later in
|
|
213
|
+
the method is no longer generated. Ruby parses the later read as a method
|
|
214
|
+
call, so every such mutant failed with a `NameError` wherever it ran.
|
|
215
|
+
- Subjects are attributed to the right namespace. A `class ::Top` inside
|
|
216
|
+
`module Outer` is `Top`, not `Outer::Top`. A `def Other.name` inside `Outer`
|
|
217
|
+
belongs to `Other`, not `Outer`. Methods of `class << obj` or `def obj.name`
|
|
218
|
+
for a non-constant `obj` are no longer reported as class methods of the
|
|
219
|
+
enclosing class.
|
|
220
|
+
- "replaced method call with nil" on a call with a block replaces the call
|
|
221
|
+
together with its block. Replacing only the call left `nil { ... }`, which
|
|
222
|
+
does not parse, so these sites were never tested. Blocks attached to
|
|
223
|
+
logger, output and debug calls stay arid.
|
|
224
|
+
- `RegexMutator` no longer drops the interpolations and later lines of a
|
|
225
|
+
dynamic or multi-line regexp, which it now leaves unmutated. It skips
|
|
226
|
+
escaped characters and the contents of character classes, so `[^0-9]` is
|
|
227
|
+
no longer reported as a removed `^` anchor and `[+-]` no longer as a
|
|
228
|
+
quantifier swap.
|
|
229
|
+
|
|
10
230
|
## [0.5.3] - 2026-08-26
|
|
11
231
|
|
|
12
232
|
### Fixed
|
|
@@ -588,7 +808,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
588
808
|
- CLI critical path: `henitai run` now executes the full pipeline, supports `--since`, returns CI-friendly exit codes, and `henitai version` prints `Henitai::VERSION`
|
|
589
809
|
- RSpec per-test coverage output: `henitai/coverage_formatter` now writes `coverage/henitai_per_test.json`
|
|
590
810
|
|
|
591
|
-
[Unreleased]: https://github.com/martinotten/henitai/compare/v0.
|
|
811
|
+
[Unreleased]: https://github.com/martinotten/henitai/compare/v0.6.0...HEAD
|
|
812
|
+
[0.6.0]: https://github.com/martinotten/henitai/compare/v0.5.3...v0.6.0
|
|
592
813
|
[0.5.3]: https://github.com/martinotten/henitai/compare/v0.5.2...v0.5.3
|
|
593
814
|
[0.5.2]: https://github.com/martinotten/henitai/compare/v0.5.1...v0.5.2
|
|
594
815
|
[0.5.1]: https://github.com/martinotten/henitai/compare/v0.5.0...v0.5.1
|
data/README.md
CHANGED
|
@@ -82,12 +82,17 @@ test_excludes:
|
|
|
82
82
|
mutation:
|
|
83
83
|
operators: light # light | full
|
|
84
84
|
timeout: 10.0
|
|
85
|
+
timeout_multiplier: 3.0
|
|
85
86
|
max_flaky_retries: 3
|
|
87
|
+
flaky_detection: false # run the baseline twice to find flaky test files
|
|
86
88
|
max_log_bytes: 5000000
|
|
87
89
|
max_timeout: 30.0
|
|
90
|
+
control_runs: true # re-run detected subjects with the unmutated method
|
|
91
|
+
fork_server: false # opt-in: boot the RSpec/Minitest environment once, fork each mutant run from it (any jobs; falls back to per-mutant boot with a warning)
|
|
88
92
|
sampling:
|
|
89
93
|
ratio: 0.05
|
|
90
94
|
strategy: stratified
|
|
95
|
+
seed: 0 # integer >= 0; selects which mutants of each stratum are kept
|
|
91
96
|
reports_dir: reports
|
|
92
97
|
reporters:
|
|
93
98
|
- terminal
|
|
@@ -118,25 +123,79 @@ baseline suite has a fixed timeout. Lines covered only by excluded tests count
|
|
|
118
123
|
as uncovered and are not mutated. Excluding every test file aborts with
|
|
119
124
|
`Henitai::ConfigurationError`.
|
|
120
125
|
`mutation.max_log_bytes` is a positive per-stream byte cap for captured child
|
|
121
|
-
output. `mutation.
|
|
126
|
+
output. Without an explicit `mutation.timeout`, each mutant's timeout is
|
|
127
|
+
calibrated as `mutation.timeout_multiplier` (default `3.0`) × (the summed
|
|
128
|
+
durations of the selected test files + the measured suite boot time), with a
|
|
129
|
+
2 s floor. `mutation.max_timeout` (default `30.0`) caps the calibrated value,
|
|
130
|
+
but never below the measured baseline plus 2 s; when the baseline alone exceeds
|
|
131
|
+
`max_timeout`, Henitai warns once and uses baseline + 2 s. An explicit
|
|
122
132
|
`mutation.timeout` still takes precedence. When JSON or HTML reporting is
|
|
123
133
|
enabled, `reports.checkpoint*` controls periodic canonical-report checkpoints.
|
|
124
134
|
Each checkpoint updates `mutation-report.json`; when the HTML reporter is
|
|
125
135
|
configured, it also regenerates `mutation-report.html` from that merged JSON.
|
|
126
136
|
|
|
137
|
+
`mutation.sampling` keeps `ceil(size * ratio)` mutants per subject (at least
|
|
138
|
+
one). Mutants are stratified by operator: each operator's mutants are ordered
|
|
139
|
+
by a SHA256 of `seed` and the mutant's stable ID, and the strata are visited
|
|
140
|
+
round-robin, so every operator is represented before any contributes a second
|
|
141
|
+
mutant. The kept mutants retain their original order. The same seed selects the
|
|
142
|
+
same mutants across runs and line shifts; change `seed` to draw another sample.
|
|
143
|
+
The applied `{strategy, ratio, seed}` is recorded in the report under
|
|
144
|
+
`sampling`.
|
|
145
|
+
|
|
146
|
+
Scoped runs (`--since`, subject patterns, survivor reruns) are merged into the
|
|
147
|
+
existing `mutation-report.json` by `stableId`. A run that mutated every
|
|
148
|
+
subject of a file (a non-authoritative run without subject patterns, such as
|
|
149
|
+
`--since`) lists it under the top-level `completeFiles` and replaces that
|
|
150
|
+
file's prior entry wholesale, dropping it when it yields no mutants, so stale
|
|
151
|
+
mutants of edited or deleted code do not linger. Each file entry carries
|
|
152
|
+
`sessionId` and `gitSha` provenance of the run that produced it; a file merged
|
|
153
|
+
from several sessions has none. `--since` diffs against the merge base
|
|
154
|
+
(`REF...HEAD`), so commits that landed on the base branch after you forked do
|
|
155
|
+
not count as your changes.
|
|
156
|
+
|
|
127
157
|
Before mutation testing starts, Henitai checks whether the current coverage data
|
|
128
158
|
covers the configured source files. If not, Henitai runs the configured test
|
|
129
159
|
suite once to bootstrap a usable coverage baseline. If coverage is still
|
|
130
160
|
unavailable for the current sources, `henitai run` aborts with
|
|
131
161
|
`Henitai::CoverageError`.
|
|
132
162
|
|
|
133
|
-
|
|
134
|
-
|
|
163
|
+
Survivors are never retried: flakiness can only turn a survivor into a kill, so
|
|
164
|
+
a retry could only inflate the score. A killed mutant is retried, up to
|
|
165
|
+
`mutation.max_flaky_retries` times (default 3), only when every test file that
|
|
166
|
+
failed for it is a known flaky test; if any retry survives, the mutant is
|
|
167
|
+
reported as `Survived`.
|
|
168
|
+
|
|
169
|
+
Known flaky tests come from the opt-in `mutation.flaky_detection: true`. The
|
|
170
|
+
baseline suite then runs twice (existing coverage is not reused), and test files
|
|
171
|
+
whose pass/fail outcome differs between the runs are written to
|
|
172
|
+
`reports/henitai_flaky_tests.json`. Without `flaky_detection` there is no flaky
|
|
173
|
+
set and nothing is retried. At the end of the run Henitai reports the flaky
|
|
174
|
+
tests and how many kills were re-checked.
|
|
175
|
+
|
|
176
|
+
A mutant only counts as detected when a test failed because of the mutation.
|
|
177
|
+
Before a mutant runs, Henitai checks that its activated code differs from the
|
|
178
|
+
original method by exactly the reported mutation; a mutant that fails this
|
|
179
|
+
check is reported as `CompileError` with the reason in `statusReason` instead
|
|
180
|
+
of running. Each mutant child reports what it got to — a failed activation, a
|
|
181
|
+
finished test run, or a `SystemExit` raised by the code under test — and only
|
|
182
|
+
a finished test run yields `Killed` or `Survived`. A failed activation is a
|
|
183
|
+
`CompileError`; a child killed by a signal, one that exits without reporting,
|
|
184
|
+
and a `SystemExit` during the tests are `RuntimeError`. With
|
|
185
|
+
`mutation.control_runs` (on by default), every subject that has detected
|
|
186
|
+
mutants is run once more with its unmutated method injected; if its tests fail
|
|
187
|
+
then, the injection itself is broken for that subject and its detected
|
|
188
|
+
verdicts are reclassified as `CompileError`.
|
|
135
189
|
|
|
136
190
|
Per-test coverage reporting is currently wired through the RSpec child runner.
|
|
137
191
|
Minitest integration reuses the same selection and execution flow, but does not
|
|
138
192
|
yet enable the per-test coverage formatter.
|
|
139
193
|
|
|
194
|
+
Test selection is coverage first: the per-test coverage map is the primary source
|
|
195
|
+
of candidate tests for a mutant, and name matching is only the fallback for when
|
|
196
|
+
the map cannot decide (no map, no location, or no covering test left in the
|
|
197
|
+
integration).
|
|
198
|
+
|
|
140
199
|
Henitai currently defaults to linear mutant execution. Set `jobs` in
|
|
141
200
|
`.henitai.yml` or pass `--jobs N` to opt into parallel mutant execution.
|
|
142
201
|
|
|
@@ -161,6 +220,13 @@ for framework errors. With `--strict-exit-codes` (opt-in, additive) it also
|
|
|
161
220
|
exits `3` when one or more mutants timed out and `4` when runtime/compile
|
|
162
221
|
errors are present; precedence `2` > `3` > `4` > `1` > `0`. The timeout code
|
|
163
222
|
is informational — a run can pass its threshold and still exit `3`.
|
|
223
|
+
A run with no scoreable mutant passes its threshold only when there was
|
|
224
|
+
nothing to score (no changed code, everything ignored). When `CompileError`
|
|
225
|
+
harness errors emptied the score denominator, nothing was tested, and the run
|
|
226
|
+
exits `1`.
|
|
227
|
+
`Pending` mutants (left over from an interrupted run, without a verdict) are
|
|
228
|
+
excluded from both the MS and the MSI denominators; MSI is
|
|
229
|
+
`killed / (total - pending)`.
|
|
164
230
|
|
|
165
231
|
For pull-request feedback, add the `github` reporter to `reporters:` in
|
|
166
232
|
`.henitai.yml`: it prints one `::warning file=...,line=...` workflow command
|
|
@@ -174,10 +240,13 @@ and every covering test file are byte-identical to what was recorded. A
|
|
|
174
240
|
`Survived` verdict needs more proof, because a *new* test could now kill it:
|
|
175
241
|
it is reused only when, additionally, the live covering set derived from the
|
|
176
242
|
current per-test coverage map matches the recorded set exactly (no test
|
|
177
|
-
added, dropped, or changed)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
243
|
+
added, dropped, or changed). Both kinds additionally require the *reuse
|
|
244
|
+
context* to be unchanged: a fingerprint of the test dependency files (spec
|
|
245
|
+
helpers, `spec/support`, fixtures/factories, `Gemfile.lock`, `.henitai.yml`,
|
|
246
|
+
`.rspec`), the henitai version, the integration, the operator set,
|
|
247
|
+
`test_excludes` and `coverage_criteria`. Verdicts recorded without a reuse
|
|
248
|
+
context are never reused. When per-test coverage is unavailable, survivors
|
|
249
|
+
simply re-execute — reuse never happens on doubt (see ADR-11).
|
|
181
250
|
Reused mutants stay visible in the report (`fromCache: true` beside
|
|
182
251
|
`stableId`) and count toward MS/MSI; the terminal prints
|
|
183
252
|
`N of M verdicts reused from history (K killed, S survived)` plus an
|
|
@@ -186,7 +255,8 @@ results.
|
|
|
186
255
|
Generated mutants temporarily include `legacyStableId` so scoped runs replace
|
|
187
256
|
canonical entries written before site-offset IDs were introduced instead of
|
|
188
257
|
duplicating them.
|
|
189
|
-
Timeouts and errors always re-execute. `--force` bypasses reuse
|
|
258
|
+
Timeouts and errors always re-execute. `--force` bypasses reuse, including
|
|
259
|
+
the reuse applied to `--survivors-from` reruns.
|
|
190
260
|
`henitai run --dry-run` lists the post-filter mutant set without executing
|
|
191
261
|
mutants and always exits `0`. Gate 0 may still run the configured test suite to
|
|
192
262
|
refresh baseline coverage.
|
|
@@ -201,9 +271,20 @@ authoritative, and the file may be the only artifact a dry run creates.
|
|
|
201
271
|
|
|
202
272
|
`henitai run --survivors-from ...` performs a partial rerun: it reports only
|
|
203
273
|
the selected survivors, skips threshold-based exit checks, and does not update
|
|
204
|
-
the run trend history.
|
|
274
|
+
the run trend history. The selected survivors go through the same reuse rules
|
|
275
|
+
as `--incremental`, always, not only with that flag: a survivor is skipped
|
|
276
|
+
(reported with `fromCache: true`) only when its subject source, its covering
|
|
277
|
+
test set and the reuse context are unchanged, so new or edited tests and
|
|
278
|
+
helpers invalidate it. Dirty worktree changes are included, so you can edit
|
|
205
279
|
tests locally without committing first; if source files under `includes` are
|
|
206
|
-
dirty,
|
|
280
|
+
dirty, the activation-recipe fast path is not used and the survivors are
|
|
281
|
+
re-matched against freshly generated mutants. Survivors restored from
|
|
282
|
+
activation recipes have no source range to fingerprint and are always
|
|
283
|
+
re-executed. When `--survivors-from` names a canonical report, Henitai uses the
|
|
284
|
+
matching session snapshot only if every file entry carries that report's
|
|
285
|
+
`sessionId`; a report merged from several runs is used as is.
|
|
286
|
+
Only authoritative runs (no `--since`, no subject patterns, no survivor rerun)
|
|
287
|
+
add a row to the trend history.
|
|
207
288
|
|
|
208
289
|
### Skipping mutations inline
|
|
209
290
|
|
|
@@ -226,8 +307,11 @@ standalone comment in the contiguous comment block directly above a `def`
|
|
|
226
307
|
(a blank line breaks the association). Trailing text is allowed
|
|
227
308
|
(`# henitai:disable -- reviewed defensive branch`). Skipped mutants are
|
|
228
309
|
reported as `Ignored`, so they stay visible in the report instead of silently
|
|
229
|
-
vanishing.
|
|
230
|
-
|
|
310
|
+
vanishing. `mutation.ignore_patterns` is matched against the source line where
|
|
311
|
+
each mutant's node starts, so only mutants on a matching line are reported as
|
|
312
|
+
`Ignored`; an enclosing `if` or block whose body matches stays mutated. Use
|
|
313
|
+
`mutation.ignore_patterns` for repo-wide policy and `# henitai:disable` for
|
|
314
|
+
one-off, reviewed exclusions.
|
|
231
315
|
|
|
232
316
|
Directives can also target specific operators, carry a reason (serialized as
|
|
233
317
|
`statusReason`, visible in the HTML report), and cover regions:
|
|
@@ -307,7 +391,8 @@ dashboard:
|
|
|
307
391
|
Set `STRYKER_DASHBOARD_API_KEY` in your CI environment to publish reports. When
|
|
308
392
|
the key, project, and version are present, the `dashboard` reporter uploads the
|
|
309
393
|
Stryker-schema report to the dashboard REST API (default
|
|
310
|
-
`https://dashboard.stryker-mutator.io`); otherwise it is skipped silently.
|
|
394
|
+
`https://dashboard.stryker-mutator.io`); otherwise it is skipped silently.
|
|
395
|
+
Scoped runs upload the merged canonical report rather than their partial one. The
|
|
311
396
|
project defaults to the git remote and the version to the current branch or CI
|
|
312
397
|
ref (`GITHUB_REF_NAME`/`GITHUB_REF`/`GITHUB_SHA`).
|
|
313
398
|
|
|
@@ -323,6 +408,8 @@ bundle install
|
|
|
323
408
|
bundle exec rspec # run tests
|
|
324
409
|
bundle exec ruby bin/verify-process-free-specs # verify self-mutation-safe specs
|
|
325
410
|
bundle exec rake smoke:integration:all # run rspec/minitest integration smoke projects
|
|
411
|
+
bundle exec rake oracle # measure false kills/survivors on the oracle corpus
|
|
412
|
+
bundle exec rake oracle:reuse # check --incremental verdicts equal fresh runs
|
|
326
413
|
bundle exec rubocop # lint
|
|
327
414
|
bundle exec henitai clean # remove stale generated report artifacts
|
|
328
415
|
bundle exec henitai run # dogfood
|
|
@@ -65,6 +65,21 @@
|
|
|
65
65
|
"type": "number",
|
|
66
66
|
"exclusiveMinimum": 0
|
|
67
67
|
},
|
|
68
|
+
"flaky_detection": {
|
|
69
|
+
"type": "boolean",
|
|
70
|
+
"default": false,
|
|
71
|
+
"description": "Run the baseline suite twice and mark test files whose outcome differs between runs as flaky. Only killed mutants whose killing tests are all flaky are re-checked, up to max_flaky_retries times."
|
|
72
|
+
},
|
|
73
|
+
"fork_server": {
|
|
74
|
+
"type": "boolean",
|
|
75
|
+
"default": false,
|
|
76
|
+
"description": "Opt-in. Boot the test environment once in a long-lived fork server and fork each mutant run from it instead of re-booting per mutant. Supported for RSpec and Minitest with any jobs value; unsupported integrations and a server that fails to boot fall back to per-mutant boot with a warning."
|
|
77
|
+
},
|
|
78
|
+
"control_runs": {
|
|
79
|
+
"type": "boolean",
|
|
80
|
+
"default": true,
|
|
81
|
+
"description": "After execution, re-run each subject that has detected mutants with its unmutated method injected; if its tests fail, the subject's detections are reclassified as CompileError."
|
|
82
|
+
},
|
|
68
83
|
"ignore_patterns": {
|
|
69
84
|
"type": "array",
|
|
70
85
|
"items": {
|
|
@@ -83,6 +98,11 @@
|
|
|
83
98
|
},
|
|
84
99
|
"strategy": {
|
|
85
100
|
"enum": ["stratified"]
|
|
101
|
+
},
|
|
102
|
+
"seed": {
|
|
103
|
+
"type": "integer",
|
|
104
|
+
"minimum": 0,
|
|
105
|
+
"default": 0
|
|
86
106
|
}
|
|
87
107
|
}
|
|
88
108
|
}
|
|
@@ -10,32 +10,17 @@ module Henitai
|
|
|
10
10
|
INVARIANT_METHODS = %i[is_a? respond_to? kind_of?].freeze
|
|
11
11
|
DSL_METHODS = %i[let subject before after].freeze
|
|
12
12
|
|
|
13
|
-
def suppressed?(node
|
|
14
|
-
|
|
13
|
+
def suppressed?(node)
|
|
14
|
+
catalog_match?(node)
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
private
|
|
18
18
|
|
|
19
|
-
def custom_pattern_match?(node, config)
|
|
20
|
-
source = node.location&.expression&.source
|
|
21
|
-
return false unless source
|
|
22
|
-
|
|
23
|
-
compiled_ignore_patterns(config).any? do |pattern|
|
|
24
|
-
pattern.match?(source)
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def compiled_ignore_patterns(config)
|
|
29
|
-
patterns = Array(config&.ignore_patterns).dup.freeze
|
|
30
|
-
@compiled_ignore_patterns ||= {}
|
|
31
|
-
@compiled_ignore_patterns[patterns] ||= patterns.map { |pattern| Regexp.new(pattern) }
|
|
32
|
-
end
|
|
33
|
-
|
|
34
19
|
def catalog_match?(node)
|
|
35
20
|
case node.type
|
|
36
21
|
when :send
|
|
37
22
|
send_match?(node)
|
|
38
|
-
when :block
|
|
23
|
+
when :block, :numblock
|
|
39
24
|
block_match?(node)
|
|
40
25
|
when :or_asgn
|
|
41
26
|
# Memoization-style ||= is treated as arid on purpose. UpdateOperator
|
|
@@ -64,7 +49,7 @@ module Henitai
|
|
|
64
49
|
return false unless send_node&.type == :send
|
|
65
50
|
|
|
66
51
|
receiver, method_name, = send_node.children
|
|
67
|
-
receiver.nil? && DSL_METHODS.include?(method_name.to_sym)
|
|
52
|
+
(receiver.nil? && DSL_METHODS.include?(method_name.to_sym)) || send_match?(send_node)
|
|
68
53
|
end
|
|
69
54
|
|
|
70
55
|
def direct_output_call?(receiver, method_name)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Henitai
|
|
4
|
+
# Explains a failed baseline suite run, pointing at its log when one exists.
|
|
5
|
+
module BootstrapErrorMessage
|
|
6
|
+
GENERIC = "Configured test suite failed while generating coverage"
|
|
7
|
+
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def build(result)
|
|
11
|
+
return GENERIC unless result.respond_to?(:log_path)
|
|
12
|
+
|
|
13
|
+
tail = result.tail(12).strip
|
|
14
|
+
message = +GENERIC
|
|
15
|
+
message << " (see #{result.log_path})"
|
|
16
|
+
message << "\n#{tail}" unless tail.empty?
|
|
17
|
+
message
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -11,6 +11,11 @@ module Henitai
|
|
|
11
11
|
# falls back to the current run's schema by itself -- structurally never
|
|
12
12
|
# worse than the unconditional overwrite this replaces.
|
|
13
13
|
#
|
|
14
|
+
# Files the current run lists under `completeFiles` (it mutated every subject
|
|
15
|
+
# of them) are the exception: their prior entry is replaced wholesale, or
|
|
16
|
+
# removed when the run produced no mutants for them, so stale mutants of
|
|
17
|
+
# changed or deleted code cannot linger next to the new ones.
|
|
18
|
+
#
|
|
14
19
|
# Merges at mutant granularity, keyed by `stableId`, not file granularity:
|
|
15
20
|
# a `--survivors-from` rerun's schema only contains the re-verified
|
|
16
21
|
# survivors for a file, so replacing that file's whole entry would drop
|
|
@@ -37,10 +42,12 @@ module Henitai
|
|
|
37
42
|
end
|
|
38
43
|
|
|
39
44
|
def self.merge_files(current, prior, prune_missing:)
|
|
40
|
-
|
|
45
|
+
prior_files = deep_dup(prior["files"])
|
|
46
|
+
Array(current["completeFiles"]).each { |file| prior_files.delete(file) }
|
|
47
|
+
merged_files = overlay_current_mutants(prior_files, current)
|
|
41
48
|
merged_files.reject! { |_, file| file["mutants"].empty? }
|
|
42
49
|
prune_missing_source_files(merged_files) if prune_missing
|
|
43
|
-
current.merge("files" => merged_files)
|
|
50
|
+
current.except("completeFiles").merge("files" => merged_files)
|
|
44
51
|
end
|
|
45
52
|
private_class_method :merge_files
|
|
46
53
|
|
|
@@ -68,11 +75,25 @@ module Henitai
|
|
|
68
75
|
def self.append_current_mutants(merged_files, current)
|
|
69
76
|
current.fetch("files", {}).each do |file, entry|
|
|
70
77
|
target = merged_files[file] ||= { "mutants" => [] }
|
|
78
|
+
merge_provenance(target, entry)
|
|
71
79
|
target["source"] = entry["source"]
|
|
72
80
|
target["language"] = entry["language"]
|
|
73
81
|
target["mutants"] += entry["mutants"]
|
|
74
82
|
end
|
|
75
83
|
end
|
|
84
|
+
|
|
85
|
+
# A file merged from several sessions has no single snapshot holding all
|
|
86
|
+
# its mutants, so its provenance is dropped unless both sides agree.
|
|
87
|
+
def self.merge_provenance(target, entry)
|
|
88
|
+
provenance = entry.slice("sessionId", "gitSha")
|
|
89
|
+
# A fresh run with no prior mutants left in this file: the current
|
|
90
|
+
# entry's provenance covers every mutant in it.
|
|
91
|
+
fresh = target["mutants"].empty?
|
|
92
|
+
agree = fresh || target["sessionId"] == entry["sessionId"]
|
|
93
|
+
%w[sessionId gitSha].each { |key| target.delete(key) }
|
|
94
|
+
target.merge!(provenance) if agree
|
|
95
|
+
end
|
|
96
|
+
private_class_method :merge_provenance
|
|
76
97
|
private_class_method :append_current_mutants
|
|
77
98
|
|
|
78
99
|
def self.mutant_ids(schema)
|