Class: Mutineer::CoverageMap
- Inherits:
-
Object
- Object
- Mutineer::CoverageMap
- 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
-
#failed_test_files ⇒ Object
readonly
Returns the value of attribute failed_test_files.
-
#map ⇒ Object
readonly
Returns the value of attribute map.
-
#phase_a_ran ⇒ Object
readonly
Returns the value of attribute phase_a_ran.
-
#project_root ⇒ Object
readonly
Returns the value of attribute project_root.
Class Method Summary collapse
-
.from_data(map:, failed_test_files:, project_root:) ⇒ Mutineer::CoverageMap
Build a QUERY-ONLY map from data captured elsewhere (the daemon builds the map app-side and ships
map+failed_test_filesover IPC; the tool reconstructs it here for per-mutant selection).
Instance Method Summary collapse
-
#build_or_load ⇒ Object
Phase A entry point (standalone): load the cached map when the content digest matches, otherwise rebuild from subprocesses and overwrite the cache.
-
#build_via_fork(after_fork: nil) ⇒ Object
Boot-mode Phase A: Coverage is already running in the parent (started before the app booted, so booted source lines are instrumented).
-
#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
constructor
A new instance of CoverageMap.
-
#method_uncapturable?(file, line_range) ⇒ Boolean
#25: per-METHOD taint.
-
#tests_for(file, line) ⇒ Object
Phase B lookup: the test files that cover
file:line, or [] when none do. -
#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.
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.
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
# File 'lib/mutineer/coverage_map.rb', line 43 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_files ⇒ Object (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 |
#map ⇒ Object (readonly)
Returns the value of attribute map.
24 25 26 |
# File 'lib/mutineer/coverage_map.rb', line 24 def map @map end |
#phase_a_ran ⇒ Object (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_root ⇒ Object (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 |
Class Method Details
.from_data(map:, failed_test_files:, project_root:) ⇒ Mutineer::CoverageMap
Build a QUERY-ONLY map from data captured elsewhere (the daemon builds the map
app-side and ships map + failed_test_files over IPC; the tool reconstructs it
here for per-mutant selection). Skips the capture machinery entirely — only the
three fields #tests_for / #method_uncapturable? read are set (U7).
35 36 37 38 39 40 41 |
# File 'lib/mutineer/coverage_map.rb', line 35 def self.from_data(map:, failed_test_files:, project_root:) instance = allocate instance.instance_variable_set(:@map, map || {}) instance.instance_variable_set(:@failed_test_files, failed_test_files || []) instance.instance_variable_set(:@project_root, project_root) instance end |
Instance Method Details
#build_or_load ⇒ Object
Phase A entry point (standalone): load the cached map when the content digest matches, otherwise rebuild from subprocesses and overwrite the cache.
63 64 65 66 |
# File 'lib/mutineer/coverage_map.rb', line 63 def build_or_load warn_external_sources cached_or { run_phase_a } end |
#build_via_fork(after_fork: nil) ⇒ 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).
74 75 76 77 |
# File 'lib/mutineer/coverage_map.rb', line 74 def build_via_fork(after_fork: nil) warn_external_sources cached_or { run_phase_a_via_fork(after_fork: after_fork) } end |
#method_uncapturable?(file, line_range) ⇒ Boolean
#25: per-METHOD taint. A mutant on a line whose enclosing method got zero successful coverage, in a file a failed sibling test targets, is :uncapturable (the capture that would have covered it errored) — NOT a genuine gap. A method with any covered line means its uncovered lines are a real :no_coverage. A failed capture emits no coverage, so per-line intent is unknowable; method-range + successful coverage is the finest derivable signal. Fully-failed files behave exactly as uncapturable_source? did (every method range has zero coverage), so #8/#9/#19 behavior is unchanged.
119 120 121 122 123 124 125 126 |
# File 'lib/mutineer/coverage_map.rb', line 119 def method_uncapturable?(file, line_range) return false if @failed_test_files.empty? rel = relativize(absolute(file)) return false unless failed_test_targets.include?(File.basename(rel, ".rb")) line_range.none? { |ln| @map.key?("#{rel}:#{ln}") } 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).
82 83 84 |
# File 'lib/mutineer/coverage_map.rb', line 82 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.
98 99 100 101 102 103 104 105 |
# File 'lib/mutineer/coverage_map.rb', line 98 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 |