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
- #compute_deltas(current_payload, baseline_payload) ⇒ Object
-
#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. - #compute_rows(current, baseline, threshold) ⇒ Object
-
#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.
- #load_coverage(path, stderr) ⇒ Object
-
#normalize_keys(coverage) ⇒ Object
Strips a leading slash so coverage.json files written before the
project_filenamechange still diff cleanly against newer reports. - #parse(args, stderr) ⇒ Object
- #parse_flags(args) ⇒ Object
- #pct_for(fields, payload) ⇒ Object
- #run(args, stdout:, stderr:) ⇒ Object
- #status_for(current_payload, baseline_payload) ⇒ Object
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.
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, | o.on("--fail-on-drop") { [:fail_on_drop] = true } o.on("--threshold N", Float) { |v| [: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 |