Module: Rutidy::Render

Defined in:
lib/rutidy/render.rb

Overview

Walks the leaves Formatter (or a real rspec --format json report extended with a hierarchy array) produced and renders them as a nested RSpec -fd-style tree, the same dedupe-shared-prefix walk gorderly's render.go and xctidy's Engine.swift use, just over each example's real hierarchy array instead of a /-split or comma-disambiguated string. Four styles (:classic/:fd/:fs/:fv) share one closing footer, except :fv, which closes with Vitest's own Test Files/ Tests/Duration shape.

Unlike gorderly, there's no fake "package path" label above the tree -- RSpec has no equivalent concept, so the first top-level describe is just the first real node. A blank line still goes before every top-level group, including the first -- that's not a gorderly-style workaround here, it's genuine RSpec -fd behavior (its own DocumentationFormatter#example_group_started does `output.puts if

Defined Under Namespace

Classes: FailureEntry

Constant Summary collapse

ANSI_PREFIX =
"\e["
COLORS =

vitest_unit: real Vitest's own unit-suffix color (#b9e4b4); no ANSI-16 entry matches it.

{
  red: "31",
  green: "32",
  yellow: "33",
  cyan: "36",
  gray: "90",
  vitest_unit: "38;2;185;228;180"
}.freeze

Class Method Summary collapse

Class Method Details

.call(examples, style:, out:, color: true) ⇒ Integer

Returns the number of failed examples (0 means a clean run).

Returns:

  • (Integer)

    the number of failed examples (0 means a clean run)



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/rutidy/render.rb', line 37

def call(examples, style:, out:, color: true)
  colorize = ->(name, text) { color ? "#{ANSI_PREFIX}#{COLORS.fetch(name)}m#{text}#{ANSI_PREFIX}0m" : text }

  total = 0
  pending = 0
  failed_count = 0
  total_elapsed = 0.0
  failures = []
  files_total = {}
  files_failed = {}
  prev_path = []

  examples.each do |example|
    hierarchy = example.fetch(:hierarchy)
    path = hierarchy[0..-2]
    leaf_name = hierarchy.last

    shared = 0
    shared += 1 while shared < prev_path.length && shared < path.length && prev_path[shared] == path[shared]

    out.puts if shared.zero?
    (shared...path.length).each { |i| out.puts("#{"  " * i}#{path[i]}") }

    total += 1
    total_elapsed += example[:run_time]
    indent = "  " * path.length
    file = example[:file_path]
    files_total[file] = true if file

    case example[:status]
    when "failed"
      failed_count += 1
      files_failed[file] = true if file
      n = failures.length + 1
      failures << FailureEntry.new(n, hierarchy, format_backtrace(example))
      out.puts("#{indent}#{colorize_fail(style, leaf_name, example, n, colorize)}")
    when "pending"
      pending += 1
      out.puts("#{indent}#{colorize_pending(style, leaf_name, example, colorize)}")
    else
      out.puts("#{indent}#{colorize_pass(style, leaf_name, example, colorize)}")
    end

    prev_path = path
  end
  out.puts

  if failures.any?
    out.puts("Failures:")
    failures.each do |f|
      out.puts
      out.puts("  #{f.n}) #{f.full.join(" ")}")
      f.output.each { |line| out.puts("     #{line}") }
    end
    out.puts
  end

  if style == :fv
    write_vitest_footer(
      out, colorize,
      total: total, failed_count: failed_count, pending: pending, total_elapsed: total_elapsed,
      files_total: files_total.size, files_failed: files_failed.size
    )
  else
    write_shared_footer(
      out, colorize,
      total: total, failed_count: failed_count, pending: pending, total_elapsed: total_elapsed
    )
  end

  failed_count
end

.colorize_fail(style, name, example, n, colorize) ⇒ Object



152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/rutidy/render.rb', line 152

def colorize_fail(style, name, example, n, colorize)
  case style
  when :classic
    "#{colorize.call(:red, "")} #{name} (FAILED - #{n}) (#{colorize.call(:red, format_seconds(example[:run_time]))} seconds)"
  when :fs
    colorize.call(:red, "#{name} (FAILED - #{n})")
  when :fv
    colorize.call(:red, "× #{name} #{format_vitest_duration(example[:run_time])}")
  else # :fd
    colorize.call(:red, "#{name} (FAILED - #{n})")
  end
end

.colorize_pass(style, name, example, colorize) ⇒ Object



138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/rutidy/render.rb', line 138

def colorize_pass(style, name, example, colorize)
  case style
  when :classic
    "#{colorize.call(:green, "")} #{name} (#{colorize.call(:green, format_seconds(example[:run_time]))} seconds)"
  when :fs
    "#{colorize.call(:green, "")} #{colorize.call(:gray, name)}"
  when :fv
    number, unit = format_vitest_duration_parts(example[:run_time])
    "#{colorize.call(:green, "")} #{name} #{colorize.call(:green, number)}#{colorize.call(:vitest_unit, unit)}"
  else # :fd
    colorize.call(:green, name)
  end
end

.colorize_pending(style, name, example, colorize) ⇒ Object

RSpec's own vocabulary is "pending" -- not "skipped" the way Go/Ginkgo call it -- so every style says PENDING, matching what rspec's real -fd/--format documentation would say for the same example. Include the pending reason where RSpec itself would (-fd/-fs); :classic and :fv stay terse (glyph/name/time only), matching how gorderly's classic and Vitest's own tree both treat a skip.



171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/rutidy/render.rb', line 171

def colorize_pending(style, name, example, colorize)
  case style
  when :classic
    "#{colorize.call(:cyan, "")} #{name} (#{colorize.call(:cyan, format_seconds(example[:run_time]))} seconds)"
  when :fs
    colorize.call(:cyan, "- #{name} #{pending_suffix(example)}")
  when :fv
    colorize.call(:gray, "#{name}")
  else # :fd
    colorize.call(:yellow, "#{name} #{pending_suffix(example)}")
  end
end

.format_backtrace(example) ⇒ Object



189
190
191
192
193
194
# File 'lib/rutidy/render.rb', line 189

def format_backtrace(example)
  exception = example[:exception]
  return [] unless exception

  ["#{exception[:class]}: #{exception[:message]}"] + Array(exception[:backtrace])
end

.format_seconds(seconds) ⇒ Object



196
197
198
# File 'lib/rutidy/render.rb', line 196

def format_seconds(seconds)
  (seconds < 1) ? format("%.4f", seconds) : format("%.2f", seconds)
end

.format_vitest_duration(seconds) ⇒ Object



207
208
209
210
# File 'lib/rutidy/render.rb', line 207

def format_vitest_duration(seconds)
  number, unit = format_vitest_duration_parts(seconds)
  "#{number}#{unit}"
end

.format_vitest_duration_parts(seconds) ⇒ Object



200
201
202
203
204
205
# File 'lib/rutidy/render.rb', line 200

def format_vitest_duration_parts(seconds)
  ms = seconds * 1000
  return [format("%.2f", ms / 1000), "s"] if ms > 1000

  [ms.round.to_s, "ms"]
end

.pending_suffix(example) ⇒ Object



184
185
186
187
# File 'lib/rutidy/render.rb', line 184

def pending_suffix(example)
  reason = example[:pending_message]
  reason ? "(PENDING: #{reason})" : "(PENDING)"
end

.vitest_summary_line(label, colorize, failed:, passed:, pending:, total:) ⇒ Object



129
130
131
132
133
134
135
136
# File 'lib/rutidy/render.rb', line 129

def vitest_summary_line(label, colorize, failed:, passed:, pending:, total:)
  parts = []
  parts << colorize.call(:red, "#{failed} failed") if failed.positive?
  parts << colorize.call(:green, "#{passed} passed") if passed.positive?
  parts << colorize.call(:gray, "#{pending} pending") if pending.positive?
  parts << "0 passed" if parts.empty?
  format("%11s  %s (%d)", label, parts.join(" | "), total)
end


110
111
112
113
114
115
116
117
118
119
# File 'lib/rutidy/render.rb', line 110

def write_shared_footer(out, colorize, total:, failed_count:, pending:, total_elapsed:)
  verdict = failed_count.positive? ? "Test Failed" : "Test Succeeded"
  verdict_color = failed_count.positive? ? :red : :green
  out.puts(colorize.call(verdict_color, verdict))
  out.puts(colorize.call(
    verdict_color,
    format("Tests Passed: %d failed, %d pending, %d total (%s seconds)",
      failed_count, pending, total, format_seconds(total_elapsed))
  ))
end


121
122
123
124
125
126
127
# File 'lib/rutidy/render.rb', line 121

def write_vitest_footer(out, colorize, total:, failed_count:, pending:, total_elapsed:, files_total:, files_failed:)
  passed = total - failed_count - pending
  files_passed = files_total - files_failed
  out.puts(vitest_summary_line("Test Files", colorize, failed: files_failed, passed: files_passed, pending: 0, total: files_total))
  out.puts(vitest_summary_line("Tests", colorize, failed: failed_count, passed: passed, pending: pending, total: total))
  out.puts(format("%11s  %s", "Duration", format_vitest_duration(total_elapsed)))
end