Module: SimpleCov::CLI::Patch
- Extended by:
- CommandHelpers, Patch
- Included in:
- Patch
- Defined in:
- lib/simplecov/cli/patch.rb,
lib/simplecov/cli/patch/output.rb,
lib/simplecov/cli/patch/changed_lines.rb
Overview
simplecov patch [--base REF]: line coverage over only the lines a
change touched. Where diff asks "did the number move" and needs a
baseline artifact, patch asks the question a reviewer actually asks: is
the code in this change tested? A project sitting at 40% cannot move its
global number in one pull request, but it can insist that every line it
adds is covered.
Only files the report already carries are scored: a changed file
SimpleCov never tracked is out of scope, and a line LinesClassifier
considers never relevant stays out of the denominator the same way it
stays out of the file total.
Defined Under Namespace
Modules: ChangedLines, Output
Constant Summary
Constants included from CommandHelpers
CommandHelpers::STATS_ROW_FORMAT
Instance Method Summary collapse
- #build_row(path, payload, changed) ⇒ Object
-
#changed_for(lines, payload) ⇒ Object
An untracked file appears in no diff, so
:allstands in for its line numbers: every line the report knows is this change's work. -
#compute_rows(coverage, diffed, stderr) ⇒ Object
Diff paths are exact root-relative names, so they resolve exactly against the report.
-
#each_touched(entries, changed) ⇒ Object
"ignored" (nocov) entries and entries off the change are skipped.
-
#entry_stats(entries, changed) ⇒ Object
Branches and methods share the report shape (a reported line and an integer hit count), so one scorer serves both.
-
#line_stats(hits, changed) ⇒ Object
A line counts only where the report gives it an Integer hit count; a never-relevant line (nil) or a
:nocov:line ("ignored") stays out of the denominator, the same rule the file total uses. - #parse(args, stderr) ⇒ Object
-
#positional_ok?(rest, stderr) ⇒ Boolean
A stray positional looks exactly like a ref, so a forgotten
--base(simplecov patch feature-x) would otherwise diff against the default and gate the wrong change in silence. - #run(args, stdout:, stderr:) ⇒ Object
- #scored?(row) ⇒ Boolean
-
#warn_stale(path, payload, changed, stderr) ⇒ Object
A changed line past the end of the report's lines array reads as never-relevant and silently drops out of the denominator, which is right for a fresh report and wrong for a stale one, so say which is likelier out loud instead of letting a
--minimumgate pass vacuously.
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
#build_row(path, payload, changed) ⇒ Object
115 116 117 118 119 120 121 122 |
# File 'lib/simplecov/cli/patch.rb', line 115 def build_row(path, payload, changed) { file: path, line: line_stats(payload["lines"], changed), branch: entry_stats(payload["branches"], changed), method: entry_stats(payload["methods"], changed) } end |
#changed_for(lines, payload) ⇒ Object
An untracked file appears in no diff, so :all stands in for its line
numbers: every line the report knows is this change's work.
95 96 97 98 99 100 |
# File 'lib/simplecov/cli/patch.rb', line 95 def changed_for(lines, payload) return lines.uniq unless lines.equal?(:all) hits = payload["lines"] hits.instance_of?(Array) ? (1..hits.length).to_a : [] end |
#compute_rows(coverage, diffed, stderr) ⇒ Object
Diff paths are exact root-relative names, so they resolve exactly against
the report. The suffix fallback CoverageFile.lookup offers interactive
commands could only ever bind a changed file the report doesn't carry to
some other file's hits and score the wrong entry.
80 81 82 83 84 85 86 87 88 89 90 91 |
# File 'lib/simplecov/cli/patch.rb', line 80 def compute_rows(coverage, diffed, stderr) index = CoverageFile.exact_index(coverage) diffed.fetch(:changes).filter_map do |path, lines| payload = index[File.(path, diffed.fetch(:root))] || index[path] next unless payload.instance_of?(Hash) # file the report doesn't carry -> out of scope changed = changed_for(lines, payload) warn_stale(path, payload, changed, stderr) row = build_row(path, payload, changed) row if scored?(row) # nothing coverable changed in this file end end |
#each_touched(entries, changed) ⇒ Object
"ignored" (nocov) entries and entries off the change are skipped.
159 160 161 162 163 164 165 166 167 |
# File 'lib/simplecov/cli/patch.rb', line 159 def each_touched(entries, changed) entries.each do |entry| next unless entry.instance_of?(Hash) line = entry["report_line"] || entry["start_line"] hits = entry["coverage"] yield line, hits if changed.include?(line) && hits.instance_of?(Integer) end end |
#entry_stats(entries, changed) ⇒ Object
Branches and methods share the report shape (a reported line and an integer hit count), so one scorer serves both. nil when the report carries no data for the criterion, so the output and gate skip it rather than reporting a hollow 0/0. A miss is recorded at the reported line so the note points where the source is.
147 148 149 150 151 152 153 154 155 156 |
# File 'lib/simplecov/cli/patch.rb', line 147 def entry_stats(entries, changed) return nil unless entries.instance_of?(Array) covered = 0 missing = [] #: Array[Integer] each_touched(entries, changed) do |line, hits| hits.positive? ? (covered += 1) : (missing << line) end {covered: covered, relevant: covered + missing.size, missing: missing.uniq.sort} end |
#line_stats(hits, changed) ⇒ Object
A line counts only where the report gives it an Integer hit count; a
never-relevant line (nil) or a :nocov: line ("ignored") stays out of
the denominator, the same rule the file total uses.
127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
# File 'lib/simplecov/cli/patch.rb', line 127 def line_stats(hits, changed) covered = 0 missing = [] #: Array[Integer] return {covered: covered, relevant: 0, missing: missing} unless hits.instance_of?(Array) changed.each do |number| hit = hits.at(number - 1) # nil / "ignored" (nocov) / any non-Integer -> never relevant, skipped. next unless hit.instance_of?(Integer) hit.positive? ? (covered += 1) : (missing << number) end {covered: covered, relevant: covered + missing.size, missing: missing.sort} end |
#parse(args, stderr) ⇒ Object
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
# File 'lib/simplecov/cli/patch.rb', line 44 def parse(args, stderr) # No `base:` default: the run fills it in from the repository when the # option is left out. opts, rest = parse_common(args, find_renames: false, minimum: nil, annotate: nil) do |parser, | parser.on("--base REF") { |v| [:base] = v } parser.on("--minimum N", Float) { |v| [:minimum] = v } parser.on("--find-renames") { [:find_renames] = true } parser.on("--annotate KIND") { |v| [:annotate] = v } end return unless positional_ok?(rest, stderr) issue = Annotations.issue(opts) return error_nil(stderr, issue) if issue opts[:coverage] = CoverageFile.load_coverage(opts.fetch(:input), command: "patch", stderr: stderr) or return nil opts end |
#positional_ok?(rest, stderr) ⇒ Boolean
A stray positional looks exactly like a ref, so a forgotten --base
(simplecov patch feature-x) would otherwise diff against the default
and gate the wrong change in silence.
65 66 67 68 69 70 |
# File 'lib/simplecov/cli/patch.rb', line 65 def positional_ok?(rest, stderr) return true if rest.empty? error(stderr, "unexpected argument #{rest.first.inspect} (did you mean `--base #{rest.first}`?)") false end |
#run(args, stdout:, stderr:) ⇒ Object
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
# File 'lib/simplecov/cli/patch.rb', line 27 def run(args, stdout:, stderr:, **) opts = parse(args, stderr) return 1 unless opts # An omitted --base resolves through origin's HEAD, so master and trunk # repositories work bare; in CI, pass the pull request's target branch # explicitly. opts[:base] ||= Git.default_base diffed = ChangedLines.call(opts.fetch(:base), find_renames: opts.fetch(:find_renames), stderr: stderr) return 1 unless diffed rows = compute_rows(opts.fetch(:coverage), diffed, stderr) kind = opts.fetch(:annotate) kind ? Output.annotate(stdout, rows, kind) : Output.emit(stdout, rows, opts) Output.gate(rows, opts.fetch(:minimum)) end |
#scored?(row) ⇒ Boolean
169 170 171 172 |
# File 'lib/simplecov/cli/patch.rb', line 169 def scored?(row) row.fetch(:line).fetch(:relevant).positive? || Output.measured?(row.fetch(:branch)) || Output.measured?(row.fetch(:method)) end |
#warn_stale(path, payload, changed, stderr) ⇒ Object
A changed line past the end of the report's lines array reads as
never-relevant and silently drops out of the denominator, which is right
for a fresh report and wrong for a stale one, so say which is likelier
out loud instead of letting a --minimum gate pass vacuously.
106 107 108 109 110 111 112 113 |
# File 'lib/simplecov/cli/patch.rb', line 106 def warn_stale(path, payload, changed, stderr) hits = payload["lines"] return unless hits.instance_of?(Array) return if changed.empty? || changed.max <= hits.length stderr.puts("simplecov patch: #{path} changed beyond the #{hits.length}-line entry in the " \ "report (is the report stale? regenerate it and rerun)") end |