Class: Mutineer::CoverageMap

Inherits:
Object
  • Object
show all
Defined in:
lib/mutineer/coverage_map.rb

Overview

Maps (source_file, line) -> [test_files] so each mutant runs only against the tests that actually exercise its line. Built once (Phase A), then queried per mutant (Phase B via #tests_for). Persisted to .mutineer/coverage.json with a content-based digest that rebuilds the map whenever any tracked file changes.

Keys are "file:line" strings (relative to project_root) everywhere — in memory and on disk — so load/save needs no key transformation (KTD4).

Constant Summary collapse

DEFAULT_CAPTURE_TIMEOUT =

seconds, per coverage subprocess (R3)

120

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(source_paths:, test_paths:, cache_dir: ".mutineer", load_paths: ["lib"], project_root: Dir.pwd, capture_timeout: DEFAULT_CAPTURE_TIMEOUT, boot_path: nil, framework: "minitest", verbose: false) ⇒ CoverageMap

Returns a new instance of CoverageMap.



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/mutineer/coverage_map.rb', line 26

def initialize(source_paths:, test_paths:, cache_dir: ".mutineer",
               load_paths: ["lib"], project_root: Dir.pwd,
               capture_timeout: DEFAULT_CAPTURE_TIMEOUT, boot_path: nil,
               framework: "minitest", verbose: false)
  @source_paths = Array(source_paths)
  @test_paths   = Array(test_paths)
  @cache_dir    = cache_dir
  @load_paths   = Array(load_paths)
  @project_root = project_root
  @capture_timeout = capture_timeout
  @boot_path    = boot_path
  @framework    = framework || "minitest"
  @verbose      = verbose
  @map          = {}
  @failed_test_files = []
  @phase_a_ran  = false
end

Instance Attribute Details

#failed_test_filesObject (readonly)

Returns the value of attribute failed_test_files.



24
25
26
# File 'lib/mutineer/coverage_map.rb', line 24

def failed_test_files
  @failed_test_files
end

#phase_a_ranObject (readonly)

Returns the value of attribute phase_a_ran.



24
25
26
# File 'lib/mutineer/coverage_map.rb', line 24

def phase_a_ran
  @phase_a_ran
end

#project_rootObject (readonly)

Returns the value of attribute project_root.



24
25
26
# File 'lib/mutineer/coverage_map.rb', line 24

def project_root
  @project_root
end

Instance Method Details

#build_or_loadObject

Phase A entry point (standalone): load the cached map when the content digest matches, otherwise rebuild from subprocesses and overwrite the cache.



46
47
48
49
# File 'lib/mutineer/coverage_map.rb', line 46

def build_or_load
  warn_external_sources
  cached_or { run_phase_a }
end

#build_via_fork(rails: false) ⇒ Object

Boot-mode Phase A: Coverage is already running in the parent (started before the app booted, so booted source lines are instrumented). A clean ruby subprocess has no booted env, so per-test coverage is captured by FORKING the booted parent instead. Inverts into the same map #tests_for reads, and reuses the digest cache (the digest mixes in the boot file so a boot cache never collides with a standalone one).



57
58
59
60
# File 'lib/mutineer/coverage_map.rb', line 57

def build_via_fork(rails: false)
  warn_external_sources
  cached_or { run_phase_a_via_fork(rails: rails) }
end

#tests_for(file, line) ⇒ Object

Phase B lookup: the test files that cover file:line, or [] when none do. ponytail: per-file granularity; upgrade to per-method when throughput warrants (requires Minitest method isolation + finer Coverage tracking).



65
66
67
# File 'lib/mutineer/coverage_map.rb', line 65

def tests_for(file, line)
  @map["#{relativize(file)}:#{line}"] || []
end

#uncapturable_source?(file) ⇒ Boolean

#9: is this source file's empty coverage the result of an errored capture rather than a genuine coverage gap? True iff (KTD-2) some capture failed this run AND this file got zero coverage from any successful capture AND a failed test file maps to it by the standard _test/_spec naming convention. Derived purely from already-persisted state (@map keys + @failed_test_files); no rerun, no new cached field, no digest change.

ponytail: file-level, convention-based attribution. A line covered only by a failed test in an otherwise-covered file stays no_coverage (condition 2), and a source with no naming-convention test match is never tainted. Upgrade path: persist per-file coverage per successful run and diff against the failed set, or record test->source targets explicitly. Not needed for the #8/#9 cases.

Returns:

  • (Boolean)


81
82
83
84
85
86
87
88
# File 'lib/mutineer/coverage_map.rb', line 81

def uncapturable_source?(file)
  return false if @failed_test_files.empty?

  rel = relativize(absolute(file))
  return false if covered_source_files.include?(rel)

  failed_test_targets.include?(File.basename(rel, ".rb"))
end