Skip to content

Commit 13b64dd

Browse files
refactor: test summary output
1 parent 505a7cb commit 13b64dd

File tree

5 files changed

+220
-59
lines changed

5 files changed

+220
-59
lines changed

ruby/lib/rspec/queue.rb

Lines changed: 75 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
require 'ci/queue'
66
require 'rspec/queue/build_status_recorder'
77
require 'rspec/queue/order_recorder'
8+
require 'rspec/queue/error_report'
89

910
module RSpec
1011
module Queue
@@ -291,58 +292,56 @@ def call(options, stdout, stderr)
291292
end
292293
end
293294

294-
errors = supervisor.build.error_reports.sort_by(&:first).map(&:last)
295+
errors = supervisor.build.error_reports.sort_by(&:first).map do |_, error_data|
296+
RSpec::Queue::ErrorReport.load(error_data)
297+
end
295298
if errors.empty?
296299
step(green('No errors found'))
297300
0
298301
else
299302
message = errors.size == 1 ? "1 error found" : "#{errors.size} errors found"
300303
step(red(message), collapsed: false)
301304

302-
# Extract test file paths for summary
303-
test_paths = []
304-
errors.each do |error_output|
305-
# Look for the rspec rerun command (should always be present thanks to BuildStatusRecorder)
306-
# The rspec command appears on its own line, possibly after whitespace
307-
if match = error_output.match(/^\s*rspec\s+([^\s]+)(?::\d+)?/m)
308-
# Extract just the file path, removing line number if present
309-
file_path = match[1].split(':').first
310-
test_paths << file_path
311-
end
312-
end
313-
314-
# Print summary section FIRST, before any details
315-
if test_paths.any?
316-
# Count failures per file
317-
file_counts = test_paths.each_with_object(Hash.new(0)) { |path, counts| counts[path] += 1 }
318-
319-
puts "\n" + "=" * 80
320-
puts "FAILED TESTS SUMMARY:"
321-
puts "=" * 80
322-
file_counts.sort_by { |path, _| path }.each do |path, count|
323-
if count == 1
324-
puts " #{path}"
325-
else
326-
puts " #{path} (#{count} failures)"
327-
end
328-
end
329-
puts "=" * 80
330-
end
331-
332-
# Print full output of errors after the summary
333-
puts "\n" + "=" * 80
334-
puts "DETAILED ERROR INFORMATION:"
335-
puts "=" * 80
336-
337-
errors.each_with_index do |error, index|
338-
puts "\n" + "-" * 80
339-
puts "Error #{index + 1} of #{errors.size}"
340-
puts "-" * 80
341-
puts error
342-
end
343-
344-
puts "=" * 80
305+
pretty_print_summary(errors)
306+
pretty_print_failures(errors)
345307
1
308+
# Example output
309+
#
310+
# FAILED TESTS SUMMARY:
311+
# =================================================================================
312+
# ./spec/dummy_spec.rb
313+
# ./spec/dummy_spec_2.rb (2 failures)
314+
# ./spec/dummy_spec_3.rb (3 failures)
315+
# =================================================================================
316+
#
317+
# --------------------------------------------------------------------------------
318+
# Error 1 of 3
319+
# --------------------------------------------------------------------------------
320+
#
321+
# Object doesn't work on first try
322+
# Failure/Error: expect(1 + 1).to be == 42
323+
#
324+
# expected: == 42
325+
# got: 2
326+
#
327+
# --- stacktrace will be here ---
328+
# --- rerun command will be here ---
329+
#
330+
# --------------------------------------------------------------------------------
331+
# Error 2 of 3
332+
# --------------------------------------------------------------------------------
333+
#
334+
# Object doesn't work on first try
335+
# Failure/Error: expect(1 + 1).to be == 42
336+
#
337+
# expected: == 42
338+
# got: 2
339+
#
340+
# --- stacktrace will be here ---
341+
# --- rerun command will be here ---
342+
#
343+
# ... etc
344+
# =================================================================================
346345
end
347346
end
348347

@@ -362,6 +361,38 @@ def setup(options, out, err)
362361
invalid_usage!('Missing --queue parameter') unless queue_url
363362
invalid_usage!('Missing --build parameter') unless RSpec::Queue.config.build_id
364363
end
364+
365+
private
366+
367+
def pretty_print_summary(errors)
368+
test_paths = errors.map(&:test_file).compact
369+
return unless test_paths.any?
370+
371+
file_counts = test_paths.each_with_object(Hash.new(0)) { |path, counts| counts[path] += 1 }
372+
373+
puts "\n" + "=" * 80
374+
puts "FAILED TESTS SUMMARY:"
375+
puts "=" * 80
376+
file_counts.sort_by { |path, _| path }.each do |path, count|
377+
if count == 1
378+
puts " #{path}"
379+
else
380+
puts " #{path} (#{count} failures)"
381+
end
382+
end
383+
puts "=" * 80
384+
end
385+
386+
def pretty_print_failures(errors)
387+
errors.each_with_index do |error, index|
388+
puts "\n" + "-" * 80
389+
puts "Error #{index + 1} of #{errors.size}"
390+
puts "-" * 80
391+
puts error.to_s
392+
end
393+
394+
puts "=" * 80
395+
end
365396
end
366397

367398
class QueueReporter < SimpleDelegator

ruby/lib/rspec/queue/build_status_recorder.rb

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
# frozen_string_literal: true
2+
require 'rspec/queue/failure_formatter'
3+
require 'rspec/queue/error_report'
4+
25
module RSpec
36
module Queue
47
class BuildStatusRecorder
58
::RSpec::Core::Formatters.register self, :example_passed, :example_failed
69

710
class << self
811
attr_accessor :build
12+
attr_accessor :failure_formatter
913
end
14+
self.failure_formatter = FailureFormatter
1015

1116
def initialize(*)
1217
end
@@ -18,17 +23,13 @@ def example_passed(notification)
1823

1924
def example_failed(notification)
2025
example = notification.example
21-
build.record_error(example.id, [
22-
notification.fully_formatted(nil),
23-
colorized_rerun_command(example),
24-
].join("\n"))
26+
build.record_error(example.id, dump(notification))
2527
end
2628

2729
private
2830

29-
def colorized_rerun_command(example, colorizer=::RSpec::Core::Formatters::ConsoleCodes)
30-
colorizer.wrap("rspec #{example.location_rerun_argument}", RSpec.configuration.failure_color) + " " +
31-
colorizer.wrap("# #{example.full_description}", RSpec.configuration.detail_color)
31+
def dump(notification)
32+
ErrorReport.new(self.class.failure_formatter.new(notification).to_h).dump
3233
end
3334

3435
def build
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# frozen_string_literal: true
2+
module RSpec
3+
module Queue
4+
class ErrorReport
5+
class << self
6+
attr_accessor :coder
7+
8+
def load(payload)
9+
new(coder.load(payload))
10+
end
11+
end
12+
13+
# Default to Marshal
14+
self.coder = Marshal
15+
16+
# Try to use SnappyPack if available from consumer's bundle
17+
begin
18+
require 'snappy'
19+
require 'msgpack'
20+
require 'stringio'
21+
22+
module SnappyPack
23+
extend self
24+
25+
MSGPACK = MessagePack::Factory.new
26+
MSGPACK.register_type(0x00, Symbol)
27+
28+
def load(payload)
29+
io = StringIO.new(Snappy.inflate(payload))
30+
MSGPACK.unpacker(io).unpack
31+
end
32+
33+
def dump(object)
34+
io = StringIO.new
35+
packer = MSGPACK.packer(io)
36+
packer.pack(object)
37+
packer.flush
38+
io.rewind
39+
Snappy.deflate(io.string).force_encoding(Encoding::UTF_8)
40+
end
41+
end
42+
43+
self.coder = SnappyPack
44+
rescue LoadError
45+
end
46+
47+
def initialize(data)
48+
@data = data
49+
end
50+
51+
def dump
52+
self.class.coder.dump(@data)
53+
end
54+
55+
def test_name
56+
@data[:test_name]
57+
end
58+
59+
def error_class
60+
@data[:error_class]
61+
end
62+
63+
def test_and_module_name
64+
@data[:test_and_module_name]
65+
end
66+
67+
def test_suite
68+
@data[:test_suite]
69+
end
70+
71+
def test_file
72+
@data[:test_file]
73+
end
74+
75+
def test_line
76+
@data[:test_line]
77+
end
78+
79+
def to_h
80+
@data
81+
end
82+
83+
def to_s
84+
output
85+
end
86+
87+
def output
88+
@data[:output]
89+
end
90+
end
91+
end
92+
end
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# frozen_string_literal: true
2+
require 'delegate'
3+
require 'ci/queue/output_helpers'
4+
5+
module RSpec
6+
module Queue
7+
class FailureFormatter < SimpleDelegator
8+
include ::CI::Queue::OutputHelpers
9+
10+
def initialize(notification)
11+
@notification = notification
12+
super
13+
end
14+
15+
def to_s
16+
[
17+
@notification.fully_formatted(nil),
18+
colorized_rerun_command(@notification.example)
19+
].join("\n")
20+
end
21+
22+
def to_h
23+
example = @notification.example
24+
{
25+
test_file: example.file_path,
26+
test_line: example.metadata[:line_number],
27+
test_and_module_name: example.id,
28+
test_name: example.description,
29+
test_suite: example.example_group.description,
30+
error_class: @notification.exception.class.name,
31+
output: to_s,
32+
}
33+
end
34+
35+
private
36+
37+
attr_reader :notification
38+
39+
def colorized_rerun_command(example, colorizer=::RSpec::Core::Formatters::ConsoleCodes)
40+
colorizer.wrap("rspec #{example.location_rerun_argument}", RSpec.configuration.failure_color) + " " +
41+
colorizer.wrap("# #{example.full_description}", RSpec.configuration.detail_color)
42+
end
43+
end
44+
end
45+
end

ruby/test/integration/rspec_redis_test.rb

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -195,10 +195,6 @@ def test_retry_report
195195
./spec/dummy_spec.rb
196196
================================================================================
197197
198-
================================================================================
199-
DETAILED ERROR INFORMATION:
200-
================================================================================
201-
202198
--------------------------------------------------------------------------------
203199
Error 1 of 1
204200
--------------------------------------------------------------------------------
@@ -384,10 +380,6 @@ def test_report
384380
./spec/dummy_spec.rb
385381
================================================================================
386382
387-
================================================================================
388-
DETAILED ERROR INFORMATION:
389-
================================================================================
390-
391383
--------------------------------------------------------------------------------
392384
Error 1 of 1
393385
--------------------------------------------------------------------------------

0 commit comments

Comments
 (0)