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
109 110 111 112 113 114 115 116 |
# File 'lib/simplecov/cli/patch.rb', line 109 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.
89 90 91 92 93 94 |
# File 'lib/simplecov/cli/patch.rb', line 89 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.
74 75 76 77 78 79 80 81 82 83 84 85 |
# File 'lib/simplecov/cli/patch.rb', line 74 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.
153 154 155 156 157 158 159 160 161 |
# File 'lib/simplecov/cli/patch.rb', line 153 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.
141 142 143 144 145 146 147 148 149 150 |
# File 'lib/simplecov/cli/patch.rb', line 141 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.
121 122 123 124 125 126 127 128 129 130 131 132 133 134 |
# File 'lib/simplecov/cli/patch.rb', line 121 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
42 43 44 45 46 47 48 49 50 51 52 53 54 |
# File 'lib/simplecov/cli/patch.rb', line 42 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) do |parser, | parser.on("--base REF") { |v| [:base] = v } parser.on("--minimum N", Float) { |v| [:minimum] = v } parser.on("--find-renames") { [:find_renames] = true } end return unless positional_ok?(rest, stderr) 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.
59 60 61 62 63 64 |
# File 'lib/simplecov/cli/patch.rb', line 59 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
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
# File 'lib/simplecov/cli/patch.rb', line 26 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) Output.emit(stdout, rows, opts) Output.gate(rows, opts.fetch(:minimum)) end |
#scored?(row) ⇒ Boolean
163 164 165 166 |
# File 'lib/simplecov/cli/patch.rb', line 163 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.
100 101 102 103 104 105 106 107 |
# File 'lib/simplecov/cli/patch.rb', line 100 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 |