Module: SimpleCov::CLI::History::Output

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

Overview

Renders the trend: Unicode sparklines scaled to each series' own range, with the numbers beside them carrying the absolute scale.

Constant Summary collapse

BARS =
%w[▁ ▂ ▃ ▄ ▅ ▆ ▇ █].freeze
CRITERIA_ORDER =
%w[line branch method].freeze

Instance Method Summary collapse

Instance Method Details

#bar(value, min, span) ⇒ Object



88
89
90
# File 'lib/simplecov/cli/history/output.rb', line 88

def bar(value, min, span)
  span.zero? ? BARS.fetch(3) : BARS.fetch(((value - min) / span * (BARS.length - 1)).round)
end

#branch_of(entry) ⇒ Object



62
63
64
# File 'lib/simplecov/cli/history/output.rb', line 62

def branch_of(entry)
  entry["branch"] || "-"
end

#emit(stdout, opts, entries, color:) ⇒ Object



15
16
17
18
19
20
21
22
23
24
# File 'lib/simplecov/cli/history/output.rb', line 15

def emit(stdout, opts, entries, color:)
  return emit_json(stdout, opts, entries) if opts.fetch(:json)
  return stdout.puts("simplecov history: no recorded runs in #{opts.fetch(:input)}") if entries.empty?

  if opts.fetch(:file)
    emit_file(stdout, opts, entries, color)
  else
    emit_totals(stdout, opts, entries, color)
  end
end

#emit_criteria_views(stdout, entries, color, *path) ⇒ Object



39
40
41
42
43
44
# File 'lib/simplecov/cli/history/output.rb', line 39

def emit_criteria_views(stdout, entries, color, *path)
  criteria = measured_criteria(entries, path)
  emit_sparklines(stdout, entries, criteria, color, path)
  stdout.puts
  emit_rows(stdout, entries) { |entry| percents_cell(entry, criteria, path) }
end

#emit_file(stdout, opts, entries, color) ⇒ Object



32
33
34
35
36
37
# File 'lib/simplecov/cli/history/output.rb', line 32

def emit_file(stdout, opts, entries, color)
  file = opts.fetch(:file)
  stdout.puts("Coverage history for #{file} (#{pluralize(entries.length, "run")})")
  stdout.puts
  emit_criteria_views(stdout, entries, color, "files", file)
end

#emit_json(stdout, opts, entries) ⇒ Object



102
103
104
105
106
107
108
109
110
111
# File 'lib/simplecov/cli/history/output.rb', line 102

def emit_json(stdout, opts, entries)
  return stdout.puts(JSON.generate(entries)) unless opts.fetch(:file)

  rows = entries.map do |entry|
    percents = entry.dig("files", opts.fetch(:file))
    {created_at: entry["created_at"], branch: entry["branch"], commit: entry["commit"],
     percents: (percents if percents.instance_of?(Hash))}
  end
  stdout.puts(JSON.generate(rows))
end

#emit_rows(stdout, entries) ⇒ Object



54
55
56
57
58
59
60
# File 'lib/simplecov/cli/history/output.rb', line 54

def emit_rows(stdout, entries)
  width = entries.map { |entry| branch_of(entry).length }.max
  entries.each do |entry|
    commit = (entry["commit"] || "-")[0, 7].ljust(7)
    stdout.puts("  #{entry["created_at"]}  #{branch_of(entry).ljust(width)}  #{commit}  #{yield(entry)}")
  end
end

#emit_sparklines(stdout, entries, criteria, color, path) ⇒ Object



46
47
48
49
50
51
52
# File 'lib/simplecov/cli/history/output.rb', line 46

def emit_sparklines(stdout, entries, criteria, color, path)
  width = criteria.map(&:length).max
  criteria.each do |criterion|
    series = entries.map { |entry| numeric(entry.dig(*path, criterion)) }
    stdout.puts("  #{criterion.ljust(width)}  #{sparkline(series)}  #{trend(series, color)}")
  end
end

#emit_totals(stdout, opts, entries, color) ⇒ Object



26
27
28
29
30
# File 'lib/simplecov/cli/history/output.rb', line 26

def emit_totals(stdout, opts, entries, color)
  stdout.puts("Coverage history: #{opts.fetch(:input)} (#{pluralize(entries.length, "run")})")
  stdout.puts
  emit_criteria_views(stdout, entries, color, "totals")
end

#measured_criteria(entries, path) ⇒ Object

Criteria some run recorded a number for, in canonical order, so a criterion enabled midway still gets its sparkline. One no run recorded is left out: an all-gaps sparkline has no trend to read off it.



116
117
118
119
120
121
122
123
# File 'lib/simplecov/cli/history/output.rb', line 116

def measured_criteria(entries, path)
  CRITERIA_ORDER.select do |criterion|
    entries.any? do |entry|
      percents = entry.dig(*path)
      percents.instance_of?(Hash) && numeric(percents[criterion])
    end
  end
end

#numeric(value) ⇒ Object



125
126
127
# File 'lib/simplecov/cli/history/output.rb', line 125

def numeric(value)
  value if value.is_a?(Numeric)
end

#percents_cell(entry, criteria, path) ⇒ Object



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

def percents_cell(entry, criteria, path)
  cell = criteria.filter_map do |criterion|
    value = numeric(entry.dig(*path, criterion))
    "#{criterion} #{value}%" if value
  end.join("  ")
  cell.empty? ? "-" : cell
end

#pluralize(number, noun) ⇒ Object



129
130
131
# File 'lib/simplecov/cli/history/output.rb', line 129

def pluralize(number, noun)
  "#{number} #{noun}#{"s" unless number.eql?(1)}"
end

#sparkline(series) ⇒ Object

Min-max scaled block characters, one per run, a space where the series has no value for that run. A flat series renders at mid height: direction of travel is the message, and a flat line says it plainly. A series with no numbers at all is all gaps, and answering that outright keeps the scale off a range it doesn't have.



79
80
81
82
83
84
85
86
# File 'lib/simplecov/cli/history/output.rb', line 79

def sparkline(series)
  numeric = series.compact
  return " " * series.length if numeric.empty?

  min = numeric.min
  span = numeric.max - min.to_f
  series.map { |value| value.nil? ? " " : bar(value, min, span) }.join
end

#trend(series, color) ⇒ Object



92
93
94
95
96
97
98
99
100
# File 'lib/simplecov/cli/history/output.rb', line 92

def trend(series, color)
  numeric = series.compact
  first = numeric.first
  last = numeric.last
  delta = (last - first).round(2)
  sign = "+" unless delta.negative?
  delta_text = Color.colorize("(#{sign}#{delta})", delta.negative? ? :red : :green, enabled: color)
  "#{first}% → #{last}%  #{delta_text}"
end