Module: SimpleCov::SimulateCoverage

Defined in:
lib/simplecov/simulate_coverage.rb

Overview

Responsible for producing file coverage metrics.

Class Method Summary collapse

Class Method Details

.call(absolute_path) ⇒ Hash

Simulate a file coverage report for a file that was tracked but never required. Returns the same hash shape as ‘Coverage.result` (lines, branches, methods).

The line classification comes from ‘Coverage.line_stub` — the same classification the runtime would have produced if the file had been required — overlaid with SimpleCov’s ‘# :nocov:` toggles and `# simplecov:disable line` directive ranges, which `Coverage` doesn’t know about. This keeps “relevant lines” identical whether a file was loaded or just tracked, fixing the multi-line statement discrepancy in github.com/simplecov-ruby/simplecov/issues/654.

Branches and methods are enumerated by static analysis (via ‘StaticCoverageExtractor`, which uses Prism). Earlier behavior left both as empty hashes, which made unloaded files invisible to the branch/method denominators while their lines DID count — so a `track_files`/`cover` glob that picked up files without specs silently inflated branch% relative to line%. See github.com/simplecov-ruby/simplecov/issues/1059. When Prism isn’t loadable (Ruby < 3.3 without the prism gem) or the file can’t be parsed, fall back to the old empty hashes — old behavior, old tradeoff.

Returns:

  • (Hash)


38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/simplecov/simulate_coverage.rb', line 38

def call(absolute_path)
  source_lines = read_lines(absolute_path)
  lines = coverage_stub(absolute_path, source_lines) ||
          LinesClassifier.new.classify(source_lines)
  synthesized = StaticCoverageExtractor.call(source_lines.join) ||
                {"branches" => {}, "methods" => {}}

  {
    "lines" => lines,
    "branches" => synthesized["branches"],
    "methods" => synthesized["methods"]
  }
end

.coverage_stub(path, source_lines) ⇒ Object

Combine ‘Coverage.line_stub` (which gets multi-line statements right) with `LinesClassifier` (which knows about `# :nocov:` toggles and `# simplecov:disable line` ranges). Returns nil — and the caller falls back to `LinesClassifier` alone — when `Coverage` can’t read or parse the file, or when the runtime doesn’t expose ‘line_stub` (JRuby and TruffleRuby).



64
65
66
67
68
69
70
71
72
73
# File 'lib/simplecov/simulate_coverage.rb', line 64

def coverage_stub(path, source_lines)
  return nil unless Coverage.respond_to?(:line_stub)

  stub = Coverage.line_stub(path)
  classifier_output = LinesClassifier.new.classify(source_lines)
  stub.each_index { |idx| stub[idx] = nil if classifier_output[idx].nil? }
  stub
rescue Errno::ENOENT, SyntaxError
  nil
end

.read_lines(path) ⇒ Object



52
53
54
55
56
# File 'lib/simplecov/simulate_coverage.rb', line 52

def read_lines(path)
  File.readlines(path)
rescue Errno::ENOENT
  []
end