Class: Rigor::Protection::ClosureKillOracle
- Inherits:
-
Object
- Object
- Rigor::Protection::ClosureKillOracle
- Defined in:
- lib/rigor/protection/closure_kill_oracle.rb
Overview
Issue #254 — the ADR-69 Seam 1 kill oracle that judges a mutant by the WHOLE dependent closure.
DiagnosticOracle re-analyses the mutated file alone, so the most valuable catch Rigor delivers is
scored as a miss: change what a method returns and the diagnostic lands in its callers, which is
exactly the cross-file reach the analyzer exists for. This oracle counts a kill when a new diagnostic
(against the clean baseline of the same file set) appears anywhere in {mutated} ∪ dependents[mutated]
— the ADR-46 reverse edge, supplied by DependencyClosure.
It is strictly additive, by construction. The mutated file's verdict is delegated to a real
DiagnosticOracle, built with exactly the knowledge the shipped oracle would have had (the
discovery-seeded-mutation-sites seed when that feature is adopted, nothing when it is not), and only
when that says "survived" is the closure consulted. So this feature moves killed in ONE direction and
cannot silently re-decide a mutant the current oracle already kills. That separation is deliberate: what
the oracle KNOWS is #253/#260's axis, and mixing the two here made the measurement uninterpretable —
on redmine app/models an early build lost 11 kills to the richer knowledge while gaining none from the
closure, and the two effects were indistinguishable in the total.
The mutant's bytes are never on the measured file's disk. For the closure half they are written to a process-private temp file and bound to the measured path through Analysis::BufferBinding — the #146 editor seam, whose whole purpose is "analyse THESE bytes at THAT logical path". The binding reaches three places that would otherwise read the file as it sits on disk:
- the per-file parse (
Runner#parse_sourceresolves through the binding); - the discovery tables the dependents resolve the mutated
defs through (DiscoverySeed.tables_for_buffer — the change-detection half: the mutated path's digest is the mutant's, so its bundle is invalidated and re-walked); - diagnostic locations, which stay on the LOGICAL path, so a signature computed against a mutant is comparable with the baseline's.
Miss any of them and every dependent reads the clean bytes, no diagnostic ever appears outside the mutated file, and the run reports a plausible number that measured nothing new.
Cost. The closure is consulted only for the mutants the mutated file did not already kill (≈30% of them
on Rigor's own lib), and each costs one analysis per dependent (mean 1.85 there) plus a ≈15ms seed
re-fold. Every per-mutant analysis keeps cache_store: nil: a --threshold CI gate must never be
handed a stale clean hit.
Defined Under Namespace
Classes: Baseline
Instance Method Summary collapse
-
#baseline(source:, path:) ⇒ Object
The clean baselines a mutant must add a diagnostic to: the mutated file's (the shipped oracle's own, unchanged) and the dependents'.
-
#closure_for(path) ⇒ Object
The file set a kill is looked for in: the mutated file plus its measured dependents.
-
#initialize(configuration:, environment:, project_scan:, paths:, dependents:, seed_bundles:, discovery_seed: nil) ⇒ ClosureKillOracle
constructor
A new instance of ClosureKillOracle.
-
#killed?(mutant_source:, path:, baseline:) ⇒ Boolean
Killed iff the mutant introduces a diagnostic the baseline did not carry — in the mutated file (the shipped verdict), or, failing that, in any dependent of it (what this feature adds).
Constructor Details
#initialize(configuration:, environment:, project_scan:, paths:, dependents:, seed_bundles:, discovery_seed: nil) ⇒ ClosureKillOracle
Returns a new instance of ClosureKillOracle.
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
# File 'lib/rigor/protection/closure_kill_oracle.rb', line 71 def initialize(configuration:, environment:, project_scan:, paths:, dependents:, seed_bundles:, discovery_seed: nil) @configuration = configuration @environment = environment @project_scan = project_scan @paths = paths @dependents = dependents @seed_bundles = seed_bundles @discovery_seed = discovery_seed @param_inferred_types = discovery_seed && discovery_seed[:param_inferred_types] @single = DiagnosticOracle.new( configuration: configuration, environment: environment, project_scan: project_scan, discovery_seed: discovery_seed ) end |
Instance Method Details
#baseline(source:, path:) ⇒ Object
The clean baselines a mutant must add a diagnostic to: the mutated file's (the shipped oracle's own, unchanged) and the dependents'. Computed once per measured file by the caller (MutationScanner), never per mutant. The dependents' half is bound through the same buffer machinery a mutant is, so the clean and mutant runs of the closure differ in exactly one input — the bytes.
91 92 93 94 95 96 |
# File 'lib/rigor/protection/closure_kill_oracle.rb', line 91 def baseline(source:, path:) Baseline.new( own: @single.baseline(source: source, path: path), dependents: dependents_signatures(source, path) ) end |
#closure_for(path) ⇒ Object
The file set a kill is looked for in: the mutated file plus its measured dependents.
110 111 112 |
# File 'lib/rigor/protection/closure_kill_oracle.rb', line 110 def closure_for(path) [path, *(@dependents[path] || [])] end |
#killed?(mutant_source:, path:, baseline:) ⇒ Boolean
Killed iff the mutant introduces a diagnostic the baseline did not carry — in the mutated file (the shipped verdict), or, failing that, in any dependent of it (what this feature adds).
100 101 102 103 104 105 106 107 |
# File 'lib/rigor/protection/closure_kill_oracle.rb', line 100 def killed?(mutant_source:, path:, baseline:) return true if @single.killed?(mutant_source: mutant_source, path: path, baseline: baseline.own) dependents = @dependents[path] || [] return false if dependents.empty? dependents_signatures(mutant_source, path).any? { |sig| !baseline.dependents.include?(sig) } end |