Class: Rigor::Protection::MutationScanner

Inherits:
Object
  • Object
show all
Defined in:
lib/rigor/protection/mutation_scanner.rb

Overview

ADR-63 Tier 2 — the mutation effectiveness tier (the truth tier behind Tier 1's static Inference::ProtectionScanner proxy). For one file it answers the question Tier 1 only bounds: when a type-visible bug is introduced at a dispatch site, does Rigor actually catch it?

Mechanism (the ADR-62 warm loop, narrowed to per-file measurement): generate the type-visible mutations (Mutator), keep only those whose receiver Rigor holds a concrete type for (the type-aware filter — the FP-safe meaning-maker; an unresolved receiver is kept), then for each ask the kill oracle whether the mutant is caught. The oracle is the ADR-69 seam: #scan_file uses the DiagnosticOracle (a new Rigor diagnostic = a kill); #scan_file_fused additionally consults a TestSuiteOracle on the type-survivors (ADR-70 — the dynamic protection axis).

The expensive builds (RBS environment + the whole-project pre-pass scan) are paid ONCE by the caller and threaded into the DiagnosticOracle; each mutant reuses them through Runner.new(prebuilt:)#run_source (in-memory overlay, no disk write).

Defined Under Namespace

Classes: FileResult, FusedFileResult, FusedSite, SurvivingSite

Instance Method Summary collapse

Constructor Details

#initialize(configuration:, environment:, project_scan:, limit: nil, seed: 1, oracle: nil, site_selector: :biteable, base_scope: nil, discovery_seed: nil) ⇒ MutationScanner

rubocop:disable Metrics/ParameterLists -- every one is an independently-defaulted collaborator or knob; bundling them into an options object would only move the list behind a name.

Parameters:

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

    pre-built once by the caller

  • project_scan (Rigor::Analysis::ProjectScan)

    pre-built once

  • limit (Integer, nil) (defaults to: nil)

    optional per-file mutation cap (sampled with seed); nil analyses every type-relevant mutation (deterministic).

  • seed (Integer) (defaults to: 1)

    RNG seed for the optional sample.

  • oracle (#baseline, #killed?, nil) (defaults to: nil)

    the kill oracle (ADR-69 Seam 1); defaults to the DiagnosticOracle (the ADR-62/63 behaviour).

  • site_selector (:biteable, :all) (defaults to: :biteable)

    which sites to mutate (ADR-69 Seam 2). :biteable (default) keeps only concrete-type sites Rigor can bite; :all also mutates Dynamic-receiver dispatch sites — use only with a TestSuiteOracle (the fused overlay), never the diagnostic path.

  • base_scope (Rigor::Scope, nil) (defaults to: nil)

    the scope site selection judges anchor types against, built once by the caller (see Rigor::Protection::Mutator#anchor_base_scope). nil — the default — keeps the bare single-file empty scope. A caller that seeds cross-file discovery passes it here; this class stays free of Configuration and of bleeding-edge feature ids, which live one layer up in the CLI.

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

    issue #260 — the SAME cross-file table set base_scope was built from, threaded to the default DiagnosticOracle so an admitted cross-file site is one the oracle can also kill at. Pass both or neither: a base_scope without it measures sites no mutation can ever break. Ignored when oracle is supplied (that caller owns its oracle's knowledge).



95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/rigor/protection/mutation_scanner.rb', line 95

def initialize(configuration:, environment:, project_scan:, limit: nil, seed: 1, oracle: nil,
               site_selector: :biteable, base_scope: nil, discovery_seed: nil)
  # rubocop:enable Metrics/ParameterLists
  @environment = environment
  @limit = limit
  @seed = seed
  @site_selector = site_selector
  @base_scope = base_scope
  @oracle = oracle || DiagnosticOracle.new(
    configuration: configuration, environment: environment, project_scan: project_scan,
    discovery_seed: discovery_seed
  )
end

Instance Method Details

#scan_file(path, source: nil) ⇒ FileResult

Parameters:

  • path (String)

    the file to measure (used as the in-memory bind path)

  • source (String, nil) (defaults to: nil)

    the file's source; read from disk when nil

Returns:



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/rigor/protection/mutation_scanner.rb', line 112

def scan_file(path, source: nil)
  source ||= File.read(path, encoding: Encoding::UTF_8)
  kept = kept_mutations(source, path)
  return FileResult.new(path: path, killed: 0, survived: 0, sites: []) if kept.empty?

  baseline = @oracle.baseline(source: source, path: path)
  killed = 0
  harness_errors = 0
  sites = []
  kept.each do |mut|
    case classify(source, path, mut, baseline)
    when :killed then killed += 1
    when :survived then sites << surviving_site(mut)
    when :harness_error then harness_errors += 1
      # :invalid — a parse-broken mutant; not a measurement, skip it.
    end
  end
  FileResult.new(path: path, killed: killed, survived: sites.size, sites: sites, harness_errors: harness_errors)
end

#scan_file_fused(path, test_oracle:, source: nil) ⇒ FusedFileResult

ADR-70 — the fused static∪dynamic measurement. Runs the type pass (the DiagnosticOracle); for every mutant the type checker did not kill, asks test_oracle whether the project's test suite catches it. The expensive suite run is paid only for type-survivors (the gradual short-circuit), so the cost is proportional to the protection hole.

Parameters:

Returns:



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/rigor/protection/mutation_scanner.rb', line 138

def scan_file_fused(path, test_oracle:, source: nil)
  source ||= File.read(path, encoding: Encoding::UTF_8)
  kept = kept_mutations(source, path)
  return FusedFileResult.new(path: path, type_killed: 0, test_killed: 0, sites: []) if kept.empty?

  baseline = @oracle.baseline(source: source, path: path)
  type_killed = 0
  test_killed = 0
  harness_errors = 0
  sites = []
  kept.each do |mut|
    case classify(source, path, mut, baseline)
    when :killed then type_killed += 1
    when :survived
      if test_oracle.killed?(path: path, original: source, mutant_source: mut.apply(source))
        test_killed += 1
      else
        sites << fused_site(mut, :none)
      end
    when :harness_error then harness_errors += 1
      # :invalid — a parse-broken mutant; not a measurement, skip it.
    end
  end
  FusedFileResult.new(path: path, type_killed: type_killed, test_killed: test_killed, sites: sites,
                      harness_errors: harness_errors)
end