Skip to content
This repository was archived by the owner on Nov 30, 2024. It is now read-only.

Commit 355b438

Browse files
committed
Merge pull request #1977 from rspec/failure-aggregation-fixes
Failure aggregation fixes
2 parents 5375dcd + 59a30e8 commit 355b438

File tree

3 files changed

+263
-109
lines changed

3 files changed

+263
-109
lines changed

lib/rspec/core/formatters/exception_presenter.rb

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,144 @@ def formatted_message_and_backtrace(colorizer, indentation)
146146

147147
formatted
148148
end
149+
150+
# @private
151+
# Configuring the `ExceptionPresenter` with the right set of options to handle
152+
# pending vs failed vs skipped and aggregated (or not) failures is not simple.
153+
# This class takes care of building an appropriate `ExceptionPresenter` for the
154+
# provided example.
155+
class Factory
156+
def build
157+
ExceptionPresenter.new(@exception, @example, options)
158+
end
159+
160+
private
161+
162+
def initialize(example)
163+
@example = example
164+
@execution_result = example.execution_result
165+
@exception = if @execution_result.status == :pending
166+
@execution_result.pending_exception
167+
else
168+
@execution_result.exception
169+
end
170+
end
171+
172+
def options
173+
with_multiple_error_options_as_needed(@exception, pending_options || {})
174+
end
175+
176+
def pending_options
177+
if @execution_result.pending_fixed?
178+
{
179+
:description_formatter => Proc.new { "#{@example.full_description} FIXED" },
180+
:message_color => RSpec.configuration.fixed_color,
181+
:failure_lines => [
182+
"Expected pending '#{@execution_result.pending_message}' to fail. No Error was raised."
183+
]
184+
}
185+
elsif @execution_result.status == :pending
186+
{
187+
:message_color => RSpec.configuration.pending_color,
188+
:detail_formatter => PENDING_DETAIL_FORMATTER
189+
}
190+
end
191+
end
192+
193+
def with_multiple_error_options_as_needed(exception, options)
194+
return options unless multiple_exceptions_not_met_error?(exception)
195+
196+
options = options.merge(
197+
:failure_lines => [],
198+
:extra_detail_formatter => sub_failure_list_formatter(exception, options[:message_color]),
199+
:detail_formatter => multiple_failure_sumarizer(exception,
200+
options[:detail_formatter],
201+
options[:message_color])
202+
)
203+
204+
options[:description_formatter] &&= Proc.new {}
205+
206+
return options unless exception.aggregation_metadata[:from_around_hook]
207+
options[:backtrace_formatter] = EmptyBacktraceFormatter
208+
options
209+
end
210+
211+
def multiple_exceptions_not_met_error?(exception)
212+
return false unless defined?(RSpec::Expectations::MultipleExpectationsNotMetError)
213+
RSpec::Expectations::MultipleExpectationsNotMetError === exception
214+
end
215+
216+
def multiple_failure_sumarizer(exception, prior_detail_formatter, color)
217+
lambda do |example, colorizer, indentation|
218+
summary = if exception.aggregation_metadata[:from_around_hook]
219+
"Got #{exception.exception_count_description}:"
220+
else
221+
"#{exception.summary}."
222+
end
223+
224+
summary = colorizer.wrap(summary, color || RSpec.configuration.failure_color)
225+
return summary unless prior_detail_formatter
226+
"#{prior_detail_formatter.call(example, colorizer, indentation)}\n#{indentation}#{summary}"
227+
end
228+
end
229+
230+
def sub_failure_list_formatter(exception, message_color)
231+
common_backtrace_truncater = CommonBacktraceTruncater.new(exception)
232+
233+
lambda do |failure_number, colorizer, indentation|
234+
exception.all_exceptions.each_with_index.map do |failure, index|
235+
options = with_multiple_error_options_as_needed(
236+
failure,
237+
:description_formatter => :failure_slash_error_line.to_proc,
238+
:indentation => indentation.length,
239+
:message_color => message_color || RSpec.configuration.failure_color,
240+
:skip_shared_group_trace => true
241+
)
242+
243+
failure = common_backtrace_truncater.with_truncated_backtrace(failure)
244+
presenter = ExceptionPresenter.new(failure, @example, options)
245+
presenter.fully_formatted("#{failure_number}.#{index + 1}", colorizer)
246+
end.join
247+
end
248+
end
249+
250+
# @private
251+
# Used to prevent a confusing backtrace from showing up from the `aggregate_failures`
252+
# block declared for `:aggregate_failures` metadata.
253+
module EmptyBacktraceFormatter
254+
def self.format_backtrace(*)
255+
[]
256+
end
257+
end
258+
259+
# @private
260+
class CommonBacktraceTruncater
261+
def initialize(parent)
262+
@parent = parent
263+
end
264+
265+
def with_truncated_backtrace(child)
266+
child_bt = child.backtrace
267+
parent_bt = @parent.backtrace
268+
return child if child_bt.nil? || child_bt.empty? || parent_bt.nil?
269+
270+
index_before_first_common_frame = -1.downto(-child_bt.size).find do |index|
271+
parent_bt[index] != child_bt[index]
272+
end
273+
274+
return child if index_before_first_common_frame == -1
275+
276+
child = child.dup
277+
child.set_backtrace(child_bt[0..index_before_first_common_frame])
278+
child
279+
end
280+
end
281+
end
282+
283+
# @private
284+
PENDING_DETAIL_FORMATTER = Proc.new do |example, colorizer|
285+
colorizer.wrap("# #{example.execution_result.pending_message}", :detail)
286+
end
149287
end
150288
end
151289
end

lib/rspec/core/notifications.rb

Lines changed: 13 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -43,110 +43,19 @@ def self.for(example)
4343
return SkippedExampleNotification.new(example) if execution_result.example_skipped?
4444
return new(example) unless execution_result.status == :pending || execution_result.status == :failed
4545

46-
klass = FailedExampleNotification
47-
exception = execution_result.exception
48-
ex_presenter_options = {}
49-
50-
if execution_result.pending_fixed?
51-
klass = PendingExampleFixedNotification
52-
ex_presenter_options = {
53-
:description_formatter => Proc.new { "#{example.full_description} FIXED" },
54-
:message_color => RSpec.configuration.fixed_color,
55-
:failure_lines => ["Expected pending '#{execution_result.pending_message}' to fail. No Error was raised."]
56-
}
57-
elsif execution_result.status == :pending
58-
klass = PendingExampleFailedAsExpectedNotification
59-
exception = example.execution_result.pending_exception
60-
ex_presenter_options = {
61-
:message_color => RSpec.configuration.pending_color,
62-
:detail_formatter => PENDING_DETAIL_FORMATTER
63-
}
64-
end
65-
66-
if multiple_exceptions_not_met_error?(exception)
67-
ex_presenter_options = exception_presenter_opts_for_multiple_error(
68-
exception, example, ex_presenter_options
69-
)
70-
end
71-
72-
ex_presenter = Formatters::ExceptionPresenter.new(exception, example, ex_presenter_options)
73-
klass.new(example, ex_presenter)
74-
end
75-
76-
def self.exception_presenter_opts_for_multiple_error(exception, example, options)
77-
ex_presenter_options = options.merge(
78-
:failure_lines => [],
79-
:extra_detail_formatter => sub_failure_list_formatter(exception, example,
80-
options[:message_color]),
81-
:detail_formatter => multiple_failure_sumarizer(exception,
82-
options[:detail_formatter],
83-
options[:message_color])
84-
)
85-
86-
ex_presenter_options[:description_formatter] &&= Proc.new {}
87-
88-
if exception.aggregation_metadata[:from_around_hook]
89-
ex_presenter_options[:backtrace_formatter] = EmptyBacktraceFormatter
90-
end
91-
92-
ex_presenter_options
93-
end
94-
95-
# @private
96-
# Used to prevent a confusing backtrace from showing up from the `aggregate_failures`
97-
# block declared for `:aggregate_failures` metadata.
98-
module EmptyBacktraceFormatter
99-
def self.format_backtrace(*)
100-
[]
101-
end
102-
end
46+
klass = if execution_result.pending_fixed?
47+
PendingExampleFixedNotification
48+
elsif execution_result.status == :pending
49+
PendingExampleFailedAsExpectedNotification
50+
else
51+
FailedExampleNotification
52+
end
10353

104-
def self.multiple_exceptions_not_met_error?(exception)
105-
return false unless defined?(RSpec::Expectations::MultipleExpectationsNotMetError)
106-
RSpec::Expectations::MultipleExpectationsNotMetError === exception
54+
exception_presenter = Formatters::ExceptionPresenter::Factory.new(example).build
55+
klass.new(example, exception_presenter)
10756
end
10857

109-
def self.multiple_failure_sumarizer(exception, prior_detail_formatter, color)
110-
lambda do |example, colorizer, indentation|
111-
summary = if exception.aggregation_metadata[:from_around_hook]
112-
"Got #{exception.exception_count_description}:"
113-
else
114-
"#{exception.summary}."
115-
end
116-
117-
summary = colorizer.wrap(summary, color || RSpec.configuration.failure_color)
118-
return summary unless prior_detail_formatter
119-
"#{prior_detail_formatter.call(example, colorizer, indentation)}\n#{indentation}#{summary}"
120-
end
121-
end
122-
123-
def self.sub_failure_list_formatter(exception, example, message_color)
124-
lambda do |failure_number, colorizer, indentation|
125-
exception.all_exceptions.each_with_index.map do |failure, index|
126-
options = {
127-
:description_formatter => :failure_slash_error_line.to_proc,
128-
:indentation => indentation.length,
129-
:message_color => message_color || RSpec.configuration.failure_color,
130-
:skip_shared_group_trace => true
131-
}
132-
133-
if multiple_exceptions_not_met_error?(failure)
134-
options = exception_presenter_opts_for_multiple_error(failure, example, options)
135-
end
136-
137-
failure = failure.dup
138-
failure.set_backtrace(failure.backtrace[0..-exception.backtrace.size])
139-
140-
Formatters::ExceptionPresenter.new(
141-
failure, example, options
142-
).fully_formatted("#{failure_number}.#{index + 1}", colorizer)
143-
end.join
144-
end
145-
end
146-
147-
private_class_method :new, :multiple_exceptions_not_met_error?,
148-
:multiple_failure_sumarizer, :sub_failure_list_formatter,
149-
:exception_presenter_opts_for_multiple_error
58+
private_class_method :new
15059
end
15160

15261
# The `ExamplesNotification` represents notifications sent by the reporter
@@ -305,11 +214,6 @@ class PendingExampleFixedNotification < FailedExampleNotification; end
305214
# @deprecated Use {FailedExampleNotification} instead.
306215
class PendingExampleFailedAsExpectedNotification < FailedExampleNotification; end
307216

308-
# @private
309-
PENDING_DETAIL_FORMATTER = Proc.new do |example, colorizer|
310-
colorizer.wrap("# #{example.execution_result.pending_message}", :detail)
311-
end
312-
313217
# The `SkippedExampleNotification` extends `ExampleNotification` with
314218
# things useful for specs that are skipped.
315219
#
@@ -322,9 +226,9 @@ class SkippedExampleNotification < ExampleNotification
322226
# RSpec's built-in formatters emit.
323227
def fully_formatted(pending_number, colorizer=::RSpec::Core::Formatters::ConsoleCodes)
324228
formatted_caller = RSpec.configuration.backtrace_formatter.backtrace_line(example.location)
325-
colorizer.wrap("\n #{pending_number}) #{example.full_description}", :pending) <<
326-
"\n " << PENDING_DETAIL_FORMATTER.call(example, colorizer) << "\n" <<
327-
colorizer.wrap(" # #{formatted_caller}\n", :detail)
229+
colorizer.wrap("\n #{pending_number}) #{example.full_description}", :pending) << "\n " <<
230+
Formatters::ExceptionPresenter::PENDING_DETAIL_FORMATTER.call(example, colorizer) <<
231+
"\n" << colorizer.wrap(" # #{formatted_caller}\n", :detail)
328232
end
329233
end
330234

0 commit comments

Comments
 (0)