Class: RSpecTurbo::Display

Inherits:
Object
  • Object
show all
Defined in:
lib/rspec_turbo/display.rb

Overview

Owns every line printed to the terminal: the live per-worker spinner (TTY), the plain CI worker roster, and the final report — failures plus the slowest folders/files (fed by slow_profile.rb in each worker log).

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(planner) ⇒ Display

Returns a new instance of Display.



8
9
10
# File 'lib/rspec_turbo/display.rb', line 8

def initialize(planner)
  @planner = planner
end

Class Method Details

.folder_for(unit) ⇒ Object



23
24
25
26
27
28
29
30
# File 'lib/rspec_turbo/display.rb', line 23

def self.folder_for(unit)
  path = unit.is_a?(Array) ? unit.first.split("[").first.delete_prefix("./spec/") : unit
  parts = path.split("/")

  return parts[0] if parts.size <= 1 || parts[1].end_with?("_spec.rb")

  "#{parts[0]}/#{parts[1]}"
end

.folder_labels(batch_units, with_counts: false, max_len: nil) ⇒ Object

Folder label string for a batch’s units, e.g. “models · requests”.



13
14
15
16
17
18
19
20
21
# File 'lib/rspec_turbo/display.rb', line 13

def self.folder_labels(batch_units, with_counts: false, max_len: nil)
  counts = Hash.new(0)
  batch_units.each { |unit| counts[folder_for(unit)] += 1 }
  label = counts.map { |folder, n| with_counts ? "#{folder}(#{n})" : folder }.join(" · ")

  return label unless max_len

  label.slice(0, max_len).then { |slice| (slice.length < label.length) ? "#{slice}" : slice }
end

Instance Method Details



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/rspec_turbo/display.rb', line 32

def print_plan
  total = @planner.counts.values.sum

  puts
  puts "  Found #{@planner.batches.flatten.size} spec files (#{total} examples) → #{@planner.batches.size} batches"
  puts

  @planner.batches.each_with_index do |batch, i|
    label = format("worker/%02d", i + 1)
    folders = Display.folder_labels(batch)

    puts format("  %-10s  %3d files  ~%-4d ex   %s", label, batch.size, @planner.example_count(batch), folders)
  end

  puts
end


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
# File 'lib/rspec_turbo/display.rb', line 49

def print_report(results, wall_total, n_workers)
  sum_total = results.sum { |r| r[:duration] }
  speedup = sum_total.positive? ? (sum_total.to_f / wall_total).round(2) : 0
  total_ex = results.sum { |r| @planner.example_count(r[:units]) }
  file_data = parse_profiler_data(results.map { |r| Config.log_path(r[:label]) })
  failed = results.select { |r| r[:status] == "FAIL" }

  puts
  puts Terminal::SEP_THICK
  puts "  RSpec Turbo Report"
  puts Terminal::SEP_THICK

  # On a TTY you watch failures scroll by live, so show them first. In CI
  # the log is read top-to-bottom afterwards, so push failures to the very
  # end where they sit right above the one-line summary — easy to find.
  if Config::TTY
    print_failures(failed)
    print_slowest(file_data)
  else
    print_slowest(file_data)
    print_failures(failed)
  end

  print_summary(failed, total_ex, wall_total, sum_total, speedup, n_workers)
end

Consolidated, sorted PASS/FAIL roster — printed once after every worker finishes (CI path) so the per-worker results form one clean block.



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/rspec_turbo/display.rb', line 77

def print_worker_summary(results)
  return if results.empty?

  puts
  puts Terminal::SEP_THICK
  puts "  Workers"
  puts Terminal::SEP_THICK
  puts

  results.sort_by { |r| r[:label] }.each do |r|
    icon = (r[:status] == "PASS") ? "" : ""
    code = (r[:status] == "PASS") ? "32" : "31"
    line = format("#{icon} %-10s  %-7s  %-6s   %s",
      r[:label], Terminal.fmt_duration(r[:duration]), r[:status], Display.folder_labels(r[:units]))

    puts Terminal.c(code, line)
  end
end

#spinner_line(state, frame) ⇒ Object



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/rspec_turbo/display.rb', line 96

def spinner_line(state, frame)
  folders = Display.folder_labels(state[:units], max_len: 55)

  case state[:status]
  when :pending
    "  \e[90m○ #{state[:label]}\e[0m"
  when :running
    elapsed = state[:started] ? (Process.clock_gettime(Process::CLOCK_MONOTONIC) - state[:started]).round : 0
    spin = Terminal::SPINNER_FRAMES[frame % Terminal::SPINNER_FRAMES.size]
    total = @planner.example_count(state[:units])

    "  \e[36m#{spin} #{state[:label]}\e[0m  ~#{total} ex  #{Terminal.fmt_duration(elapsed)}   #{folders}"
  when :done
    color = (state[:result] == "PASS") ? "\e[32m" : "\e[31m"
    icon = (state[:result] == "PASS") ? "" : ""

    "  #{color}#{icon} #{state[:label]}  #{Terminal.fmt_duration(state[:duration])}  #{state[:result]}   #{folders}\e[0m"
  end
end