Module: SimpleCov::CLI::Diff

Extended by:
CommandHelpers, Diff
Included in:
Diff
Defined in:
lib/simplecov/cli/diff.rb,
lib/simplecov/cli/diff/output.rb

Overview

simplecov diff <baseline>: per-file coverage deltas between coverage.json and a baseline coverage.json checked in alongside the suite. Only files whose coverage moved are listed, and --fail-on-drop exits non-zero when any file regressed, so this composes as a "coverage of this PR didn't drop" gate.

Defined Under Namespace

Modules: Output

Constant Summary collapse

EPSILON =

Tolerance below which a delta is considered noise.

0.005

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

#compute_deltas(current_payload, baseline_payload) ⇒ Object



86
87
88
89
90
# File 'lib/simplecov/cli/diff.rb', line 86

def compute_deltas(current_payload, baseline_payload)
  CoverageFile::CRITERIA.transform_values do |fields|
    pct_for(fields, current_payload) - pct_for(fields, baseline_payload)
  end
end

#compute_row(fname, current_payload, baseline_payload, threshold) ⇒ Object

The threshold is inclusive, so a file that moved exactly N% is listed under --threshold N, matching the "at least N%" the usage text promises.



77
78
79
80
81
82
83
84
# File 'lib/simplecov/cli/diff.rb', line 77

def compute_row(fname, current_payload, baseline_payload, threshold)
  deltas = compute_deltas(current_payload, baseline_payload)
  floor = threshold.abs
  return nil unless deltas.values.any? { |delta| delta.abs > EPSILON && delta.abs >= floor }

  {file: fname, status: status_for(current_payload, baseline_payload),
   line_delta: deltas.fetch(:line), branch_delta: deltas.fetch(:branch), method_delta: deltas.fetch(:method)}
end

#compute_rows(current, baseline, threshold) ⇒ Object



70
71
72
73
# File 'lib/simplecov/cli/diff.rb', line 70

def compute_rows(current, baseline, threshold)
  files = current.keys | baseline.keys
  files.filter_map { |fname| compute_row(fname, current[fname], baseline[fname], threshold) }
end

#coverage_drop?(rows) ⇒ Boolean

A removed file's deltas are all -baseline%, but deleting a covered file is not a coverage regression, so removed rows never trip the gate. Drops are gated on EPSILON like row inclusion and display, so a row listed for a gain in one criterion cannot fail the run over float noise in another.

Returns:

  • (Boolean)


109
110
111
112
# File 'lib/simplecov/cli/diff.rb', line 109

def coverage_drop?(rows)
  rows.reject { |row| row.fetch(:status).eql?("removed") }
    .any? { |row| row.fetch_values(:line_delta, :branch_delta, :method_delta).min < -EPSILON }
end

#load_coverage(path, stderr) ⇒ Object



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

def load_coverage(path, stderr)
  coverage = CoverageFile.load_coverage(path, command: "diff", stderr: stderr)
  return unless coverage

  normalize_keys(coverage)
end

#normalize_keys(coverage) ⇒ Object

Strips a leading slash so coverage.json files written before the project_filename change still diff cleanly against newer reports.



66
67
68
# File 'lib/simplecov/cli/diff.rb', line 66

def normalize_keys(coverage)
  coverage.transform_keys { |key| key.delete_prefix("/") }
end

#parse(args, stderr) ⇒ Object



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

def parse(args, stderr)
  opts = parse_flags(args)
  if opts.fetch(:rest).empty?
    stderr.puts("simplecov diff: missing baseline argument")
    return nil
  end

  opts[:baseline] = load_coverage(opts.fetch(:rest).first, stderr) or return nil
  opts[:current] = load_coverage(opts.fetch(:input), stderr) or return nil
  opts
end

#parse_flags(args) ⇒ Object



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

def parse_flags(args)
  opts, rest = parse_common(args, fail_on_drop: false, threshold: 0.0) do |o, options|
    o.on("--fail-on-drop") { options[:fail_on_drop] = true }
    o.on("--threshold N", Float) { |v| options[:threshold] = v }
  end
  opts.merge(rest: rest)
end

#pct_for(fields, payload) ⇒ Object



99
100
101
102
103
# File 'lib/simplecov/cli/diff.rb', line 99

def pct_for(fields, payload)
  return 0.0 unless payload.instance_of?(Hash) && payload[fields.fetch(:total)].to_i.positive?

  payload[fields.fetch(:percent)].to_f
end

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



23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/simplecov/cli/diff.rb', line 23

def run(args, stdout:, stderr:, **)
  opts = parse(args, stderr)
  return 1 unless opts

  rows = compute_rows(opts.fetch(:current), opts.fetch(:baseline), opts.fetch(:threshold))
  rows.sort_by! { |row| row.fetch(:line_delta) }
  if opts.fetch(:json)
    stdout.puts(JSON.pretty_generate(rows))
  else
    Output.emit_text(stdout, rows, CLI.color_enabled?(opts, stdout))
  end
  (opts.fetch(:fail_on_drop) && coverage_drop?(rows)) ? 1 : 0
end

#status_for(current_payload, baseline_payload) ⇒ Object



92
93
94
95
96
97
# File 'lib/simplecov/cli/diff.rb', line 92

def status_for(current_payload, baseline_payload)
  return "added" if baseline_payload.nil?
  return "removed" if current_payload.nil?

  "changed"
end