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.
-
#phase_a_ran ⇒ Object
readonly
Returns the value of attribute phase_a_ran.
-
#project_root ⇒ Object
readonly
Returns the value of attribute project_root.
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(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).
-
#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.
-
#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.
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_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 |
#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 |
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.
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.
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 |