Module: Moult::Coverage
- Defined in:
- lib/moult/coverage.rb,
lib/moult/coverage/resolver.rb
Overview
Ingests line-keyed code coverage from a LOCAL FILE and normalises it into one
Moult-owned value object (Dataset) the Resolver can read. This is the
runtime-layer analogue of Index: external formats (SimpleCov, stdlib
Coverage) come in, only Moult types go out, so the input is swappable.
Two on-disk formats are understood (auto-detected, or forced via format:):
:simplecov— SimpleCov'scoverage/.resultset.json: {command => {"coverage" => {abs_path => {"lines" => [...]}}, "timestamp" => epoch}}. Multiple command runs are merged element-wise.:coverage— a JSON dump of stdlib Coverage.result(lines: true): {abs_path => {"lines" => [...]}} or the legacy bare {abs_path => [...]}.
Line arrays are 0-indexed (index 0 = line 1) with the shared convention:
nil = non-executable, 0 = executable but never run, N = hit count.
oneshot_lines is intentionally unsupported: it cannot distinguish 0 from
nil, so runtime-cold could not be detected.
Defined Under Namespace
Modules: Resolver Classes: Dataset, Source
Class Method Summary collapse
-
.canonicalize(p) ⇒ Object
realpath resolves /tmp -> /private/tmp style symlinks so coverage paths line up with rubydex's canonical paths; falls back when the file is absent locally (coverage collected on another machine).
-
.detect_format(raw) ⇒ Object
SimpleCov nests file coverage under a command name and a "coverage" key; stdlib dumps key files at the top level.
-
.extract_lines(data) ⇒ Object
Accepts both the wrapped (=> [...]) and legacy bare-array forms; ignores sibling :methods/:branches data.
-
.from_coverage(raw, path) ⇒ [Hash{String=>Array}, Source]
Abs-path line arrays + provenance.
-
.from_simplecov(raw, _path) ⇒ [Hash{String=>Array}, Source]
Abs-path line arrays + provenance.
- .load(path, root:, format: :auto) ⇒ Dataset
-
.merge_lines(a, b) ⇒ Object
Element-wise merge of two coverage runs: a value is hit if hit in either run (max of the non-nil values), non-executable only if nil in both.
-
.relativize(abs_entries, root) ⇒ Object
Map absolute coverage paths to the root-relative paths Phase 2 emits, so the join lands on the same symbol_id components.
Class Method Details
.canonicalize(p) ⇒ Object
realpath resolves /tmp -> /private/tmp style symlinks so coverage paths line up with rubydex's canonical paths; falls back when the file is absent locally (coverage collected on another machine).
168 169 170 171 172 |
# File 'lib/moult/coverage.rb', line 168 def canonicalize(p) File.realpath(p) rescue File.(p) end |
.detect_format(raw) ⇒ Object
SimpleCov nests file coverage under a command name and a "coverage" key; stdlib dumps key files at the top level. The presence of "coverage" on the first value is the unambiguous discriminator.
75 76 77 78 79 80 81 82 83 84 85 |
# File 'lib/moult/coverage.rb', line 75 def detect_format(raw) raise Moult::Error, "coverage file is not a JSON object" unless raw.is_a?(Hash) sample = raw.values.first if sample.is_a?(Hash) && sample.key?("coverage") :simplecov elsif sample.is_a?(Array) || (sample.is_a?(Hash) && sample.key?("lines")) :coverage else raise Moult::Error, "could not auto-detect coverage format; pass --coverage-format simplecov|coverage" end end |
.extract_lines(data) ⇒ Object
Accepts both the wrapped (=> [...]) and legacy bare-array forms; ignores sibling :methods/:branches data.
126 127 128 129 130 131 |
# File 'lib/moult/coverage.rb', line 126 def extract_lines(data) case data when Array then data when Hash then data["lines"] end end |
.from_coverage(raw, path) ⇒ [Hash{String=>Array}, Source]
Returns abs-path line arrays + provenance.
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
# File 'lib/moult/coverage.rb', line 108 def from_coverage(raw, path) entries = {} raw.each do |file, data| lines = extract_lines(data) entries[file] = lines if lines end # The raw dump carries no timestamp, so the file mtime is the best-effort # collected_at (noted as a fallback; only matters for deferred staleness). source = Source.new( backend: "coverage", version: RUBY_VERSION, collected_at: File.mtime(path).utc.iso8601 ) [entries, source] end |
.from_simplecov(raw, _path) ⇒ [Hash{String=>Array}, Source]
Returns abs-path line arrays + provenance.
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/moult/coverage.rb', line 88 def from_simplecov(raw, _path) merged = {} = [] raw.each_value do |run| next unless run.is_a?(Hash) << run["timestamp"] if run["timestamp"] (run["coverage"] || {}).each do |file, data| merged[file] = merge_lines(merged[file], extract_lines(data)) end end collected = .compact.max source = Source.new( backend: "simplecov", version: nil, # not recorded in the resultset collected_at: collected && Time.at(collected).utc.iso8601 ) [merged, source] end |
.load(path, root:, format: :auto) ⇒ Dataset
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
# File 'lib/moult/coverage.rb', line 56 def load(path, root:, format: :auto) raw = JSON.parse(File.read(path)) fmt = (format == :auto) ? detect_format(raw) : format abs_entries, source = case fmt when :simplecov then from_simplecov(raw, path) when :coverage then from_coverage(raw, path) else raise Moult::Error, "unknown coverage format: #{fmt}" end entries, unmatched = relativize(abs_entries, root) Dataset.new(entries: entries, source: source, unmatched_count: unmatched) rescue JSON::ParserError => e raise Moult::Error, "could not parse coverage file #{path}: #{e.}" rescue Errno::ENOENT raise Moult::Error, "no such coverage file: #{path}" end |
.merge_lines(a, b) ⇒ Object
Element-wise merge of two coverage runs: a value is hit if hit in either run (max of the non-nil values), non-executable only if nil in both.
135 136 137 138 139 140 141 142 143 144 145 |
# File 'lib/moult/coverage.rb', line 135 def merge_lines(a, b) return b if a.nil? return a if b.nil? Array.new([a.length, b.length].max) do |i| x, y = a[i], b[i] if x.nil? then y elsif y.nil? then x else [x, y].max end end end |
.relativize(abs_entries, root) ⇒ Object
Map absolute coverage paths to the root-relative paths Phase 2 emits, so the join lands on the same symbol_id components. Files outside the root are dropped and counted (a different checkout layout, vendored code, etc.).
150 151 152 153 154 155 156 157 158 159 160 161 162 163 |
# File 'lib/moult/coverage.rb', line 150 def relativize(abs_entries, root) real_root = canonicalize(root) entries = {} unmatched = 0 abs_entries.each do |abs, lines| full = canonicalize(abs) if full == real_root || full.start_with?(real_root + File::SEPARATOR) entries[SymbolId.relative_path(full, real_root)] = lines else unmatched += 1 end end [entries, unmatched] end |