Module: SimpleCov::SimulateCoverage

Extended by:
SimulateCoverage
Included in:
SimulateCoverage
Defined in:
lib/simplecov/simulate_coverage.rb

Instance Method Summary collapse

Instance Method Details

#call(absolute_path, synthesize: true, lines: true) ⇒ Object

Simulate a file coverage report for a file that was tracked but never required, in the same hash shape as Coverage.result.

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 (#654).

Branches and methods are enumerated by static analysis. Earlier behavior left both as empty hashes, which made unloaded files invisible to the branch/method denominators while their lines did count, so a glob that picked up files without specs silently inflated branch% relative to line% (#1059). When the file can't be parsed, fall back to the old empty hashes.

Pass synthesize: false to skip the static analysis. Callers use it when neither branch nor method coverage is enabled, since nothing will read the tuples and the Prism parse is about half the cost of simulating a file (#1250).

Pass lines: false to omit the "lines" key entirely, mirroring what Coverage.result reports for a file loaded under a branch-only or method-only run. Emitting zeroed lines there would make a simulated file indistinguishable from one a sibling process actually loaded.



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

def call(absolute_path, synthesize: true, lines: true)
  source_lines = read_lines(absolute_path)
  simulated = synthesized_tuples(source_lines, synthesize)
  return simulated unless lines

  classified = coverage_stub(absolute_path, source_lines) ||
    LinesClassifier.new.classify(source_lines)
  {"lines" => classified}.merge(simulated)
end

#coverage_stub(path, source_lines) ⇒ Object

Combines Coverage.line_stub (which gets multi-line statements right) with LinesClassifier (which knows about # :nocov: toggles and # simplecov:disable line ranges). Answers nil when Coverage can't read or parse the file, or the runtime doesn't expose line_stub.



70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/simplecov/simulate_coverage.rb', line 70

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

  # Paired off rather than indexed: the two lists describe the same file
  # line for line. A line the classifier has no verdict for keeps the nil
  # it would have been set to.
  relevance = LinesClassifier.new.classify(source_lines)
  Coverage.line_stub(path).zip(relevance).map { |hits, relevant| hits if relevant }
rescue SystemCallError, SyntaxError
  # SystemCallError for the same reason as read_lines: line_stub reads the
  # file itself, so EACCES/EISDIR surface here too.
  nil
end

#read_lines(path) ⇒ Object

SystemCallError, not just ENOENT: a track_files glob can sweep up an unreadable file or a directory named like a Ruby file, and simulation must degrade to "empty file" rather than crash the merge or report step.



60
61
62
63
64
# File 'lib/simplecov/simulate_coverage.rb', line 60

def read_lines(path)
  File.readlines(path)
rescue SystemCallError
  []
end

#synthesized_tuples(source_lines, synthesize) ⇒ Object

Empty hashes when the static analysis is skipped (nothing enabled reads them) or unavailable (no Prism, or the file doesn't parse).



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

def synthesized_tuples(source_lines, synthesize)
  empty = {"branches" => {}, "methods" => {}} #: Hash[String, Hash[untyped, untyped]]
  synthesized = (StaticCoverageExtractor.call(source_lines.join) if synthesize) || empty

  {"branches" => synthesized.fetch("branches"), "methods" => synthesized.fetch("methods")}
end