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, then queried per
mutant 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.
Constant Summary collapse
- DEFAULT_CAPTURE_TIMEOUT =
Seconds per coverage subprocess before the parent kills it.
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
Standalone entry: 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 build: 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
Per-method taint.
-
#tests_for(file, line) ⇒ Object
Lookup: the test files that cover
file:line, or [] when none do. -
#uncapturable_source?(file) ⇒ Boolean
Is this source file's empty coverage the result of an errored capture rather than a genuine coverage gap? True iff 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.
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
# File 'lib/mutineer/coverage_map.rb', line 45 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.
25 26 27 |
# File 'lib/mutineer/coverage_map.rb', line 25 def failed_test_files @failed_test_files end |
#map ⇒ Object (readonly)
Returns the value of attribute map.
25 26 27 |
# File 'lib/mutineer/coverage_map.rb', line 25 def map @map end |
#phase_a_ran ⇒ Object (readonly)
Returns the value of attribute phase_a_ran.
25 26 27 |
# File 'lib/mutineer/coverage_map.rb', line 25 def phase_a_ran @phase_a_ran end |
#project_root ⇒ Object (readonly)
Returns the value of attribute project_root.
25 26 27 |
# File 'lib/mutineer/coverage_map.rb', line 25 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.
37 38 39 40 41 42 43 |
# File 'lib/mutineer/coverage_map.rb', line 37 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
Standalone entry: load the cached map when the content digest matches, otherwise rebuild from subprocesses and overwrite the cache.
65 66 67 68 |
# File 'lib/mutineer/coverage_map.rb', line 65 def build_or_load warn_external_sources cached_or { run_phase_a } end |
#build_via_fork(after_fork: nil) ⇒ Object
Boot-mode build: 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).
76 77 78 79 |
# File 'lib/mutineer/coverage_map.rb', line 76 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
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).
121 122 123 124 125 126 127 128 |
# File 'lib/mutineer/coverage_map.rb', line 121 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
Lookup: the test files that cover file:line, or [] when none do.
Per-file granularity; upgrade to per-method when throughput warrants
(requires Minitest method isolation + finer Coverage tracking).
84 85 86 |
# File 'lib/mutineer/coverage_map.rb', line 84 def tests_for(file, line) @map["#{relativize(file)}:#{line}"] || [] end |
#uncapturable_source?(file) ⇒ Boolean
Is this source file's empty coverage the result of an errored capture rather than a genuine coverage gap? True iff 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.
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.
100 101 102 103 104 105 106 107 |
# File 'lib/mutineer/coverage_map.rb', line 100 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 |