Skip to content

Commit de58d21

Browse files
authored
Merge pull request #361 from Shopify/ss-rspec-report
Improve test summary output for `rspec` and `minitest`
2 parents bd0c774 + ba04ffb commit de58d21

File tree

7 files changed

+315
-12
lines changed

7 files changed

+315
-12
lines changed

ruby/lib/minitest/queue/build_status_reporter.rb

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,10 @@ def report
153153
puts
154154

155155
errors = error_reports
156-
puts errors
156+
if errors.any?
157+
pretty_print_summary(errors)
158+
pretty_print_failures(errors)
159+
end
157160

158161
build.worker_errors.to_a.sort.each do |worker_id, error|
159162
puts red("Worker #{worker_id } crashed")
@@ -224,6 +227,37 @@ def write_flaky_tests_file(file)
224227

225228
attr_reader :build, :supervisor
226229

230+
def pretty_print_summary(errors)
231+
test_paths = errors.map(&:test_file).compact
232+
return unless test_paths.any?
233+
234+
file_counts = test_paths.each_with_object(Hash.new(0)) { |path, counts| counts[path] += 1 }
235+
236+
puts "\n" + "=" * 80
237+
puts "FAILED TESTS SUMMARY:"
238+
puts "=" * 80
239+
file_counts.sort_by { |path, _| path }.each do |path, count|
240+
relative_path = Minitest::Queue.relative_path(path)
241+
if count == 1
242+
puts " #{relative_path}"
243+
else
244+
puts " #{relative_path} (#{count} failures)"
245+
end
246+
end
247+
puts "=" * 80
248+
end
249+
250+
def pretty_print_failures(errors)
251+
errors.each_with_index do |error, index|
252+
puts "\n" + "-" * 80
253+
puts "Error #{index + 1} of #{errors.size}"
254+
puts "-" * 80
255+
puts error
256+
end
257+
258+
puts "=" * 80
259+
end
260+
227261
def timed_out?
228262
supervisor.time_left.to_i <= 0
229263
end

ruby/lib/rspec/queue.rb

Lines changed: 88 additions & 4 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,16 +292,56 @@ def call(options, stdout, stderr)
291292
end
292293
end
293294

294-
# TODO: better reporting
295-
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
296298
if errors.empty?
297299
step(green('No errors found'))
298300
0
299301
else
300302
message = errors.size == 1 ? "1 error found" : "#{errors.size} errors found"
301303
step(red(message), collapsed: false)
302-
puts errors
304+
305+
pretty_print_summary(errors)
306+
pretty_print_failures(errors)
303307
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+
# =================================================================================
304345
end
305346
end
306347

@@ -320,6 +361,38 @@ def setup(options, out, err)
320361
invalid_usage!('Missing --queue parameter') unless queue_url
321362
invalid_usage!('Missing --build parameter') unless RSpec::Queue.config.build_id
322363
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
323396
end
324397

325398
class QueueReporter < SimpleDelegator
@@ -362,7 +435,18 @@ def setup(err, out)
362435
invalid_usage!('Missing --queue parameter') unless queue_url
363436
invalid_usage!('Missing --build parameter') unless RSpec::Queue.config.build_id
364437
invalid_usage!('Missing --worker parameter') unless RSpec::Queue.config.worker_id
365-
RSpec.configuration.backtrace_formatter.filter_gem('ci-queue')
438+
RSpec.configure do |config|
439+
config.backtrace_exclusion_patterns = [
440+
# Filter bundler paths
441+
%r{/tmp/bundle/},
442+
# RSpec internals
443+
%r{/gems/rspec-},
444+
# ci-queue and rspec-queue internals
445+
%r{exe/rspec-queue},
446+
%r{lib/ci/queue/},
447+
%r{rspec/queue}
448+
]
449+
end
366450
end
367451

368452
def run_specs(example_groups)

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/minitest_redis_test.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -954,15 +954,40 @@ def test_redis_reporter
954954
Ran 7 tests, 8 assertions, 2 failures, 1 errors, 1 skips, 4 requeues in X.XXs (aggregated)
955955
956956
957+
958+
================================================================================
959+
FAILED TESTS SUMMARY:
960+
================================================================================
961+
test/dummy_test.rb (3 failures)
962+
================================================================================
963+
964+
--------------------------------------------------------------------------------
965+
Error 1 of 3
966+
--------------------------------------------------------------------------------
957967
FAIL ATest#test_bar
958968
Expected false to be truthy.
959969
test/dummy_test.rb:10:in `test_bar'
960970
971+
972+
--------------------------------------------------------------------------------
973+
Error 2 of 3
974+
--------------------------------------------------------------------------------
961975
FAIL ATest#test_flaky_fails_retry
962976
Expected false to be truthy.
963977
test/dummy_test.rb:23:in `test_flaky_fails_retry'
964978
979+
980+
--------------------------------------------------------------------------------
981+
Error 3 of 3
982+
--------------------------------------------------------------------------------
965983
ERROR BTest#test_bar
984+
Minitest::UnexpectedError: TypeError: String can't be coerced into Integer
985+
test/dummy_test.rb:37:in `+'
986+
test/dummy_test.rb:37:in `test_bar'
987+
test/dummy_test.rb:37:in `+'
988+
test/dummy_test.rb:37:in `test_bar'
989+
990+
================================================================================
966991
END
967992
assert_includes output, expected_output
968993
end

0 commit comments

Comments
 (0)