Module: SimpleCov::CLI::Show

Extended by:
CommandHelpers, Show
Included in:
Show
Defined in:
lib/simplecov/cli/show.rb,
lib/simplecov/cli/show/sweep.rb,
lib/simplecov/cli/show/annotator.rb

Overview

simplecov show <path>: annotated source in the terminal, the way go tool cover and llvm-cov show print it. --uncovered-only collapses the answer to path:ranges, a form that greps, fits in a commit message, and hands a coding agent exactly the lines whose tests are missing.

Defined Under Namespace

Modules: Annotator, Sweep

Constant Summary

Constants included from CommandHelpers

CommandHelpers::STATS_ROW_FORMAT

Instance Method Summary collapse

Methods included from CommandHelpers

build_parser, command_name, common_options, error, error_nil, on_help, one?, parse_common, quiet_option, recorded_contexts, stats_row

Instance Method Details

#emit_json(entry, opts, stdout) ⇒ Object



90
91
92
93
94
95
96
# File 'lib/simplecov/cli/show.rb', line 90

def emit_json(entry, opts, stdout)
  stdout.puts(JSON.pretty_generate(
    path: opts.fetch(:path), missed: Annotator.missed_lines(entry),
    lines: relevant_lines(entry), markers: Annotator.markers_for(entry)
  ))
  0
end

#locate_or_report(coverage, opts, stderr) ⇒ Object

A line-less report has no per-line hits to put in a gutter, so it is reported like an unresolvable path is.



64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/simplecov/cli/show.rb', line 64

def locate_or_report(coverage, opts, stderr)
  path, input = opts.fetch_values(:path, :input)
  match = CoverageFile.lookup(coverage, path)
  return error_nil(stderr, CoverageFile.not_found_message(coverage, path, input)) unless match

  entry = match.last
  unless entry.instance_of?(Hash)
    return CoverageFile.report_invalid(stderr, "show", input, "entry for #{path} must be an object")
  end
  return match if entry["lines"].instance_of?(Array)

  error_nil(stderr, "no line coverage for #{path} in #{input}")
end

#options(parser, opts) ⇒ Object



55
56
57
58
59
60
# File 'lib/simplecov/cli/show.rb', line 55

def options(parser, opts)
  parser.on("--input PATH") { |v| opts[:input] = v }
  parser.on("--no-color") { opts[:no_color] = true }
  parser.on("--uncovered-only") { opts[:uncovered_only] = true }
  parser.on("--json") { opts[:json] = true }
end

#parse(args) ⇒ Object



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

def parse(args)
  opts = {input: CLI.default_input, no_color: false,
          uncovered_only: false, json: false} #: Hash[Symbol, untyped]
  rest = build_parser { |parser| options(parser, opts) }.parse(args)
  opts[:path] = rest.first
  opts
end

#relevant_lines(entry) ⇒ Object



98
99
100
101
102
# File 'lib/simplecov/cli/show.rb', line 98

def relevant_lines(entry)
  entry.fetch("lines").each_with_index.filter_map do |hit, index|
    {number: index + 1, hits: hit} if hit.instance_of?(Integer)
  end
end

#render(located, opts, stdout, stderr) ⇒ Object



78
79
80
81
82
83
84
85
86
87
88
# File 'lib/simplecov/cli/show.rb', line 78

def render(located, opts, stdout, stderr)
  filename, entry = located
  return emit_json(entry, opts, stdout) if opts.fetch(:json)
  return show_uncovered(entry, opts, stdout, stderr) if opts.fetch(:uncovered_only)

  source = source_for(filename, entry, opts, stderr)
  return 1 unless source

  Annotator.call(source, entry, stdout, color: CLI.color_enabled?(opts, stdout))
  0
end

#run(args, stdout:, stderr:) ⇒ Object



21
22
23
24
25
26
27
28
29
30
31
32
# File 'lib/simplecov/cli/show.rb', line 21

def run(args, stdout:, stderr:)
  opts = parse(args)
  return run_project(opts, stdout, stderr) unless opts.fetch(:path)

  coverage = CoverageFile.load_coverage(opts.fetch(:input), command: "show", stderr: stderr)
  return 1 unless coverage

  located = locate_or_report(coverage, opts, stderr)
  return 1 unless located

  render(located, opts, stdout, stderr)
end

#run_project(opts, stdout, stderr) ⇒ Object

No path sweeps the whole project, in the compact forms only. The annotated text form still wants one file.



36
37
38
39
40
41
42
43
44
45
# File 'lib/simplecov/cli/show.rb', line 36

def run_project(opts, stdout, stderr)
  unless opts.fetch(:uncovered_only) || opts.fetch(:json)
    return error(stderr, "missing path (annotating needs one file; --uncovered-only sweeps the whole project)")
  end

  coverage = CoverageFile.load_coverage(opts.fetch(:input), command: "show", stderr: stderr)
  return 1 unless coverage

  Sweep.emit(Sweep.misses(coverage), opts, stdout, stderr)
end

#show_uncovered(entry, opts, stdout, stderr) ⇒ Object



104
105
106
107
108
109
110
111
112
# File 'lib/simplecov/cli/show.rb', line 104

def show_uncovered(entry, opts, stdout, stderr)
  missed = Annotator.missed_lines(entry)
  if missed.empty?
    stderr.puts("simplecov show: nothing uncovered in #{opts.fetch(:path)}")
  else
    stdout.puts("#{opts.fetch(:path)}:#{Patch::Output.ranges(missed, ",")}")
  end
  0
end

#source_for(filename, entry, opts, stderr) ⇒ Object

The report's own source when it carries one; otherwise the file on disk, accepted only while its line count still matches the report's, since annotating drifted source would put hit counts on the wrong lines.



117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/simplecov/cli/show.rb', line 117

def source_for(filename, entry, opts, stderr)
  embedded = entry["source"]
  return embedded if embedded.instance_of?(Array) && embedded.all?(String)
  unless File.file?(filename)
    return error_nil(stderr, "no source for #{opts.fetch(:path)} in #{opts.fetch(:input)} and no file " \
                             "at #{filename}, regenerate the report with `source_in_json true`")
  end

  lines = File.readlines(filename, chomp: true)
  return lines if lines.size.eql?(entry.fetch("lines").size)

  error_nil(stderr, "#{filename} has changed since the report (#{lines.size} lines now, " \
                    "#{entry.fetch("lines").size} recorded), regenerate the report")
end