Module: SimpleCov::CLI::CoverageFile

Extended by:
CoverageFile
Included in:
CoverageFile
Defined in:
lib/simplecov/cli/coverage_file.rb

Overview

Shared input boundary for commands that consume JSONFormatter's coverage.json. It validates only the stable outer shape, so older schema versions remain readable.

Constant Summary collapse

CRITERIA =
{
  line: {label: "Line", percent: "lines_covered_percent",
         covered: "covered_lines", missed: "missed_lines", total: "total_lines"},
  branch: {label: "Branch", percent: "branches_covered_percent",
           covered: "covered_branches", missed: "missed_branches", total: "total_branches"},
  method: {label: "Method", percent: "methods_covered_percent",
           covered: "covered_methods", missed: "missed_methods", total: "total_methods"}
}.freeze

Instance Method Summary collapse

Instance Method Details

#exact_index(coverage_hash) ⇒ Object

The report's entries under exact, realpath-normalized keys, for programmatic resolution where a suffix fallback could bind a path to the wrong entry. The realpath step keeps a symlinked project root from splitting a resolved path apart from the report's keys.



47
48
49
50
51
52
53
54
# File 'lib/simplecov/cli/coverage_file.rb', line 47

def exact_index(coverage_hash)
  index = {} #: Hash[String, untyped]
  coverage_hash.each do |key, payload|
    index[key] ||= payload
    index[normalize(key)] ||= payload
  end
  index
end

#load_coverage(path, command:, stderr:) ⇒ Object



83
84
85
86
87
88
89
90
91
92
# File 'lib/simplecov/cli/coverage_file.rb', line 83

def load_coverage(path, command:, stderr:)
  document = load_document(path, command: command, stderr: stderr)
  return unless document

  none = {} #: Hash[String, untyped]
  coverage = document.fetch("coverage", none)
  return coverage if coverage.is_a?(Hash)

  report_invalid(stderr, command, path, '"coverage" must be an object')
end

#load_document(path, command:, stderr:) ⇒ Object



73
74
75
76
77
78
79
80
81
# File 'lib/simplecov/cli/coverage_file.rb', line 73

def load_document(path, command:, stderr:)
  CoverageJSON.load(path)
rescue Errno::ENOENT
  report_missing(stderr, command, path)
rescue CoverageJSON::Error => e
  report_invalid(stderr, command, path, e.message)
rescue SystemCallError => e
  report_unreadable(stderr, command, path, e.message)
end

#lookup(coverage_hash, path) ⇒ Object

An exact match, the absolute path or the literal string passed, wins over a suffix match, so a nested "vendor/lib/foo.rb" can't shadow the real "lib/foo.rb" just by sitting earlier in the report. A subpath can end more than one key, and a collision is left as "not found" for the caller rather than scoring whichever key happens to sit first.



27
28
29
30
31
32
33
34
# File 'lib/simplecov/cli/coverage_file.rb', line 27

def lookup(coverage_hash, path)
  absolute = File.expand_path(path)
  return [absolute, coverage_hash.fetch(absolute)] if coverage_hash.key?(absolute)
  return [path, coverage_hash.fetch(path)] if coverage_hash.key?(path)

  matches = suffix_matches(coverage_hash, path)
  matches.first if matches.one?
end

#normalize(key) ⇒ Object

A key whose file no longer exists keeps its literal spelling.



57
58
59
60
61
# File 'lib/simplecov/cli/coverage_file.rb', line 57

def normalize(key)
  File.realdirpath(key)
rescue SystemCallError
  key
end

#not_found_message(coverage_hash, path, input) ⇒ Object

An ambiguous subpath names its candidates: "no entry" would send the user hunting for a typo in a path that exists twice.



65
66
67
68
69
70
71
# File 'lib/simplecov/cli/coverage_file.rb', line 65

def not_found_message(coverage_hash, path, input)
  matches = suffix_matches(coverage_hash, path).keys
  return "no entry for #{path} in #{input}" if matches.size < 2

  "#{path} matches #{matches.size} files in #{input}: #{matches.sort.join(", ")} " \
    "(use a longer path to pick one)"
end

#report_invalid(stderr, command, path, reason) ⇒ Object



94
95
96
97
# File 'lib/simplecov/cli/coverage_file.rb', line 94

def report_invalid(stderr, command, path, reason)
  detail = reason.lines.first.to_s.strip
  stderr.puts("simplecov #{command}: input file #{path.inspect} isn't valid JSON (#{detail})")
end

#report_missing(stderr, command, path) ⇒ Object



99
100
101
# File 'lib/simplecov/cli/coverage_file.rb', line 99

def report_missing(stderr, command, path)
  stderr.puts("simplecov #{command}: #{path} not found")
end

#report_unreadable(stderr, command, path, reason) ⇒ Object



103
104
105
106
# File 'lib/simplecov/cli/coverage_file.rb', line 103

def report_unreadable(stderr, command, path, reason)
  detail = reason.lines.first.to_s.strip
  stderr.puts("simplecov #{command}: cannot read #{path.inspect} (#{detail})")
end

#suffix_matches(coverage_hash, path) ⇒ Object

The suffix's leading slash keeps "foo.rb" from matching "barfoo.rb", and hoisting it keeps the scan from allocating a string per entry.



38
39
40
41
# File 'lib/simplecov/cli/coverage_file.rb', line 38

def suffix_matches(coverage_hash, path)
  suffix = "/#{path}"
  coverage_hash.select { |fname, _| fname.end_with?(suffix) }
end