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) ⇒ MutationScanner

Returns a new instance of MutationScanner.

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.



72
73
74
75
76
77
78
79
80
81
# File 'lib/rigor/protection/mutation_scanner.rb', line 72

def initialize(configuration:, environment:, project_scan:, limit: nil, seed: 1, oracle: nil,
               site_selector: :biteable)
  @environment = environment
  @limit = limit
  @seed = seed
  @site_selector = site_selector
  @oracle = oracle || DiagnosticOracle.new(
    configuration: configuration, environment: environment, project_scan: project_scan
  )
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:



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/rigor/protection/mutation_scanner.rb', line 86

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



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

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
  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
      # :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)
end