Module: SimpleCov::CLI::Patch::Output

Extended by:
Output
Included in:
Output
Defined in:
lib/simplecov/cli/patch/output.rb

Constant Summary collapse

OPTIONAL_CRITERIA =
%i[branch method].freeze
CRITERIA =
%i[line branch method].freeze

Instance Method Summary collapse

Instance Method Details

#annotate(stdout, rows, kind) ⇒ Object

One diagnostic per contiguous missed range per measured criterion, in the CI host's own annotation form. Totals and the empty-change notice stay off stdout: the exit status under --minimum is the verdict.



19
20
21
22
23
24
25
26
27
# File 'lib/simplecov/cli/patch/output.rb', line 19

def annotate(stdout, rows, kind)
  diagnostics = rows.flat_map do |row|
    CRITERIA.flat_map do |criterion|
      stats = row.fetch(criterion)
      stats ? Annotations.diagnostics(row.fetch(:file), stats.fetch(:missing), criterion) : []
    end
  end
  Annotations.emit(stdout, kind, diagnostics)
end

#criterion_cell(label, stats, color) ⇒ Object

A "lines" cell always, "branches" and "methods" cells only when the touched lines actually carried one: a hollow 0/0 cell is noise.



60
61
62
63
64
# File 'lib/simplecov/cli/patch/output.rb', line 60

def criterion_cell(label, stats, color)
  percent = pct(stats)
  cell = Color.colorize(format("%6.2f%%", percent), (percent >= 100) ? :green : :red, enabled: color)
  "#{cell} (#{stats.fetch(:covered)}/#{stats.fetch(:relevant)}) #{label}"
end

#criterion_cells(row, color) ⇒ Object



51
52
53
54
55
56
# File 'lib/simplecov/cli/patch/output.rb', line 51

def criterion_cells(row, color)
  cells = [criterion_cell("lines", row.fetch(:line), color)]
  cells << criterion_cell("branches", row.fetch(:branch), color) if measured?(row.fetch(:branch))
  cells << criterion_cell("methods", row.fetch(:method), color) if measured?(row.fetch(:method))
  cells
end

#emit(stdout, rows, opts) ⇒ Object



29
30
31
32
33
34
35
# File 'lib/simplecov/cli/patch/output.rb', line 29

def emit(stdout, rows, opts)
  if opts.fetch(:json)
    stdout.puts(JSON.pretty_generate(json_rows(rows)))
  else
    emit_text(stdout, rows, CLI.color_enabled?(opts, stdout))
  end
end

#emit_text(stdout, rows, color) ⇒ Object



37
38
39
40
41
42
43
# File 'lib/simplecov/cli/patch/output.rb', line 37

def emit_text(stdout, rows, color)
  return stdout.puts("simplecov patch: no coverable lines changed") if rows.empty?

  rows.sort_by! { |row| [pct(row.fetch(:line)), row.fetch(:file)] }
  rows.each { |row| stdout.puts(format_row(row, color)) }
  stdout.puts(format_total(rows, color))
end

#format_row(row, color) ⇒ Object



45
46
47
48
49
# File 'lib/simplecov/cli/patch/output.rb', line 45

def format_row(row, color)
  line = "  #{criterion_cells(row, color).join("  ")}  #{row.fetch(:file)}"
  note = missing_note(row)
  note.empty? ? line : "#{line}  #{note}"
end

#format_total(rows, color) ⇒ Object



76
77
78
79
# File 'lib/simplecov/cli/patch/output.rb', line 76

def format_total(rows, color)
  totals = {line: sum_stats(rows, :line), branch: sum_stats(rows, :branch), method: sum_stats(rows, :method)}
  "  Patch coverage: #{criterion_cells(totals, color).join(", ")}"
end

#gate(rows, minimum) ⇒ Object

No --minimum is report-only. With one, every measured criterion must clear the floor, and short? never fails an unmeasured one.



131
132
133
134
135
136
# File 'lib/simplecov/cli/patch/output.rb', line 131

def gate(rows, minimum)
  return 0 unless minimum

  below = %i[line branch method].any? { |criterion| short?(sum_stats(rows, criterion), minimum) }
  below ? 1 : 0
end

#json_rows(rows) ⇒ Object



81
82
83
84
85
86
87
88
89
90
# File 'lib/simplecov/cli/patch/output.rb', line 81

def json_rows(rows)
  rows.map do |row|
    data = {file: row.fetch(:file), line: row.fetch(:line).merge(percent: pct(row.fetch(:line)))}
    OPTIONAL_CRITERIA.each do |criterion|
      stats = row.fetch(criterion)
      data[criterion] = stats.merge(percent: pct(stats)) if measured?(stats)
    end
    data
  end
end

#measured?(stats) ⇒ Boolean

Branch and method stats are nil when the report never measured them, and 0/0 when the change touched none.

Returns:

  • (Boolean)


94
95
96
# File 'lib/simplecov/cli/patch/output.rb', line 94

def measured?(stats)
  stats.is_a?(Hash) && stats.fetch(:relevant).positive?
end

#missing_note(row) ⇒ Object



66
67
68
69
70
71
72
73
74
# File 'lib/simplecov/cli/patch/output.rb', line 66

def missing_note(row)
  parts = [] #: Array[String]
  parts << "missing #{ranges(row.fetch(:line).fetch(:missing))}" if row.fetch(:line).fetch(:missing).any?
  OPTIONAL_CRITERIA.each do |criterion|
    stats = row.fetch(criterion)
    parts << "#{criterion} #{ranges(stats.fetch(:missing))}" if measured?(stats) && stats.fetch(:missing).any?
  end
  parts.join("  ")
end

#only(run) ⇒ Object

mutant:disable



109
110
111
# File 'lib/simplecov/cli/patch/output.rb', line 109

def only(run)
  format("%<only>d", only: run.first)
end

#pct(stats) ⇒ Object



122
123
124
125
126
127
# File 'lib/simplecov/cli/patch/output.rb', line 122

def pct(stats)
  relevant = stats.fetch(:relevant)
  return 100.0 if relevant.zero?

  (stats.fetch(:covered).to_f / relevant * 100).round(2)
end

#ranges(numbers, separator = ", ") ⇒ Object

Collapses a sorted line list into ranges: [41, 42, 43, 47] -> "41-43, 47". The show subcommand borrows this with a bare-comma separator for its greppable path:40,52-58,71 form.



101
102
103
104
105
106
# File 'lib/simplecov/cli/patch/output.rb', line 101

def ranges(numbers, separator = ", ")
  numbers.slice_when { |prev, curr| curr > prev + 1 }.each_with_object(+"") do |run, out|
    out << separator unless out.empty?
    out << (run.one? ? only(run) : span(run))
  end
end

#short?(stats, minimum) ⇒ Boolean

Cross-multiplied rather than compared as percentages so neither rounding nor float division can shift the verdict at the boundary: reading the displayed pct would round a 19_999/20_000 patch up to 100 and pass it against --minimum 100, while dividing makes 23/40 compute as 57.4999... and fail an exactly-57.5% patch. The minimum becomes an exact Rational from its own text, and covered * 100 is an integer, so the comparison is exact. A criterion with nothing relevant never falls short.

Returns:

  • (Boolean)


145
146
147
# File 'lib/simplecov/cli/patch/output.rb', line 145

def short?(stats, minimum)
  stats.fetch(:covered) * 100 < Rational(minimum.to_s) * stats.fetch(:relevant)
end

#span(run) ⇒ Object



113
114
115
# File 'lib/simplecov/cli/patch/output.rb', line 113

def span(run)
  format("%<from>d-%<to>d", from: run.first, to: run.last)
end

#sum_stats(rows, criterion) ⇒ Object



117
118
119
120
# File 'lib/simplecov/cli/patch/output.rb', line 117

def sum_stats(rows, criterion)
  stats = rows.filter_map { |row| row.fetch(criterion) }
  {covered: stats.sum { |stat| stat.fetch(:covered) }, relevant: stats.sum { |stat| stat.fetch(:relevant) }}
end