Class: Rigor::Protection::ClosureKillOracle

Inherits:
Object
  • Object
show all
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:

  1. the per-file parse (Runner#parse_source resolves through the binding);
  2. 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);
  3. 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

Constructor Details

#initialize(configuration:, environment:, project_scan:, paths:, dependents:, seed_bundles:, discovery_seed: nil) ⇒ ClosureKillOracle

Returns a new instance of ClosureKillOracle.

Parameters:

  • configuration (Rigor::Configuration)
  • environment (Rigor::Environment)

    built once by the caller.

  • project_scan (Rigor::Analysis::ProjectScan)

    built once by the caller; adopted per analysis through prebuilt:, exactly as DiagnosticOracle does.

  • paths (Array<String>)

    the measured file set, in canonical order (the seed's span: a class declared outside it stays unknown, as it does for Tier 1's seed and for DiscoverySeed).

  • dependents (Hash{String => Array<String>})

    DependencyClosure map, restricted to paths.

  • seed_bundles (Hash{String => Hash})

    DiscoverySeed.bundles over the same paths.

  • discovery_seed (Hash, nil) (defaults to: nil)

    the discovery-seeded-mutation-sites seed when that feature is also adopted, nil otherwise. It goes to the delegated DiagnosticOracle verbatim, so the mutated file's verdict is byte-for-byte the verdict that feature combination produces without this one; its param_inferred_types slot additionally rides the per-mutant closure seed, so an admitted site is judged with the knowledge that admitted it (issue #260's amended decision). The table is not refreshed per mutant — the collector is a whole-project pre-pass, and one mutated method body does not justify re-running it thousands of times.



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).

Returns:

  • (Boolean)


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