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

Commit 9094876

Browse files
committed
Improve handling of additional around/after exceptions.
Now that we have nice formatting logic for multiple exception errors, let’s leverage that to improve the output for this case. Fixes #1966.
1 parent c561ca2 commit 9094876

File tree

4 files changed

+101
-99
lines changed

4 files changed

+101
-99
lines changed

features/expectation_framework_integration/aggregating_failures.feature

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Feature: Aggregating Failures
1313
Response = Struct.new(:status, :headers, :body)
1414
1515
class Client
16-
def self.make_request
16+
def self.make_request(url='/')
1717
Response.new(404, { "Content-Type" => "text/plain" }, "Not Found")
1818
end
1919
end
@@ -25,6 +25,17 @@ Feature: Aggregating Failures
2525
require 'client'
2626
2727
RSpec.describe Client do
28+
after do
29+
# this should be appended to failure list
30+
expect(false).to be(true), "after hook failure"
31+
end
32+
33+
around do |ex|
34+
ex.run
35+
# this should also be appended to failure list
36+
expect(false).to be(true), "around hook failure"
37+
end
38+
2839
it "returns a successful response" do
2940
response = Client.make_request
3041
@@ -42,32 +53,44 @@ Feature: Aggregating Failures
4253
Failures:
4354
4455
1) Client returns a successful response
45-
Got 3 failures from failure aggregation block "testing reponse".
46-
# ./spec/use_block_form_spec.rb:7
56+
Got 3 failures:
4757
48-
1.1) Failure/Error: expect(response.status).to eq(200)
58+
1.1) Got 3 failures from failure aggregation block "testing reponse".
59+
# ./spec/use_block_form_spec.rb:18:in `block (2 levels) in <top (required)>'
60+
# ./spec/use_block_form_spec.rb:10:in `block (2 levels) in <top (required)>'
4961
50-
expected: 200
51-
got: 404
62+
1.1.1) Failure/Error: expect(response.status).to eq(200)
5263
53-
(compared using ==)
54-
# ./spec/use_block_form_spec.rb:8
64+
expected: 200
65+
got: 404
5566
56-
1.2) Failure/Error: expect(response.headers).to include("Content-Type" => "application/json")
57-
expected {"Content-Type" => "text/plain"} to include {"Content-Type" => "application/json"}
58-
Diff:
59-
@@ -1,2 +1,2 @@
60-
-[{"Content-Type"=>"application/json"}]
61-
+"Content-Type" => "text/plain",
62-
# ./spec/use_block_form_spec.rb:9
67+
(compared using ==)
68+
# ./spec/use_block_form_spec.rb:19:in `block (3 levels) in <top (required)>'
6369
64-
1.3) Failure/Error: expect(response.body).to eq('{"message":"Success"}')
70+
1.1.2) Failure/Error: expect(response.headers).to include("Content-Type" => "application/json")
71+
expected {"Content-Type" => "text/plain"} to include {"Content-Type" => "application/json"}
72+
Diff:
73+
@@ -1,2 +1,2 @@
74+
-[{"Content-Type"=>"application/json"}]
75+
+"Content-Type" => "text/plain",
76+
# ./spec/use_block_form_spec.rb:20:in `block (3 levels) in <top (required)>'
6577
66-
expected: "{\"message\":\"Success\"}"
67-
got: "Not Found"
78+
1.1.3) Failure/Error: expect(response.body).to eq('{"message":"Success"}')
6879
69-
(compared using ==)
70-
# ./spec/use_block_form_spec.rb:10
80+
expected: "{\"message\":\"Success\"}"
81+
got: "Not Found"
82+
83+
(compared using ==)
84+
# ./spec/use_block_form_spec.rb:21:in `block (3 levels) in <top (required)>'
85+
86+
1.2) Failure/Error: expect(false).to be(true), "after hook failure"
87+
after hook failure
88+
# ./spec/use_block_form_spec.rb:6:in `block (2 levels) in <top (required)>'
89+
# ./spec/use_block_form_spec.rb:10:in `block (2 levels) in <top (required)>'
90+
91+
1.3) Failure/Error: expect(false).to be(true), "around hook failure"
92+
around hook failure
93+
# ./spec/use_block_form_spec.rb:12:in `block (2 levels) in <top (required)>'
7194
"""
7295

7396
Scenario: Use `:aggregate_failures` metadata

lib/rspec/core/example.rb

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -306,24 +306,17 @@ def inspect
306306
#
307307
# Used internally to set an exception in an after hook, which
308308
# captures the exception but doesn't raise it.
309-
def set_exception(exception, context=nil)
309+
def set_exception(exception)
310310
if pending? && !(Pending::PendingExampleFixedError === exception)
311311
execution_result.pending_exception = exception
312-
else
313-
if @exception
314-
# An error has already been set; we don't want to override it,
315-
# but we also don't want silence the error, so let's print it.
316-
msg = <<-EOS
317-
318-
An error occurred #{context}
319-
#{exception.class}: #{exception.message}
320-
occurred at #{exception.backtrace.first}
321-
322-
EOS
323-
RSpec.configuration.reporter.message(msg)
312+
elsif @exception
313+
unless RSpec::Core::MultipleExceptionError === @exception
314+
@exception = RSpec::Core::MultipleExceptionError.new(@exception)
324315
end
325316

326-
@exception ||= exception
317+
@exception.add exception
318+
else
319+
@exception = exception
327320
end
328321
end
329322

@@ -348,10 +341,10 @@ def skip_with_exception(reporter, exception)
348341
end
349342

350343
# @private
351-
def instance_exec_with_rescue(context, &block)
344+
def instance_exec_with_rescue(&block)
352345
@example_group_instance.instance_exec(self, &block)
353346
rescue Exception => e
354-
set_exception(e, context)
347+
set_exception(e)
355348
end
356349

357350
# @private
@@ -368,7 +361,7 @@ def hooks
368361
def with_around_example_hooks
369362
hooks.run(:around, :example, self) { yield }
370363
rescue Exception => e
371-
set_exception(e, "in an `around(:example)` hook")
364+
set_exception(e)
372365
end
373366

374367
def start(reporter)
@@ -548,7 +541,7 @@ def initialize
548541
end
549542

550543
# To ensure we don't silence errors.
551-
def set_exception(exception, _context=nil)
544+
def set_exception(exception)
552545
raise exception
553546
end
554547
end

lib/rspec/core/hooks.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@ def run(example)
361361
# @private
362362
class AfterHook < Hook
363363
def run(example)
364-
example.instance_exec_with_rescue("in an after hook", &block)
364+
example.instance_exec_with_rescue(&block)
365365
end
366366
end
367367

spec/rspec/core/example_spec.rb

Lines changed: 46 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,8 @@ def metadata_hash(*args)
3131
end
3232

3333
describe "#exception" do
34-
it "supplies the first exception raised, if any" do
35-
RSpec.configuration.output_stream = StringIO.new
36-
34+
it "supplies the exception raised, if there is one" do
3735
example = example_group.example { raise "first" }
38-
example_group.after { raise "second" }
3936
example_group.run
4037
expect(example.exception.message).to eq("first")
4138
end
@@ -45,6 +42,25 @@ def metadata_hash(*args)
4542
example_group.run
4643
expect(example.exception).to be_nil
4744
end
45+
46+
it 'provides a `MultipleExceptionError` if there are multiple exceptions (e.g. from `it`, `around` and `after`)' do
47+
the_example = nil
48+
49+
after_ex = StandardError.new("after")
50+
around_ex = StandardError.new("around")
51+
example_ex = StandardError.new("example")
52+
53+
RSpec.describe do
54+
the_example = example { raise example_ex }
55+
after { raise after_ex }
56+
around { |ex| ex.run; raise around_ex }
57+
end.run
58+
59+
expect(the_example.exception).to have_attributes(
60+
:class => RSpec::Core::MultipleExceptionError,
61+
:all_exceptions => [example_ex, after_ex, around_ex]
62+
)
63+
end
4864
end
4965

5066
describe "when there is an explicit description" do
@@ -401,65 +417,20 @@ def expect_gc(opts)
401417
end
402418
end
403419

404-
context 'when the example raises an error' do
405-
def run_and_capture_reported_message(group)
406-
reported_msg = nil
407-
# We can't use should_receive(:message).with(/.../) here,
408-
# because if that fails, it would fail within our example-under-test,
409-
# and since there's already two errors, it would just be reported again.
410-
allow(RSpec.configuration.reporter).to receive(:message) { |msg| reported_msg = msg }
411-
group.run
412-
reported_msg
413-
end
420+
it "leaves raised exceptions unmodified (GH-1103)" do
421+
# set the backtrace, otherwise MRI will build a whole new object,
422+
# and thus mess with our expectations. Rubinius and JRuby are not
423+
# affected.
424+
exception = StandardError.new
425+
exception.set_backtrace([])
414426

415-
it "prints any around hook errors rather than silencing them" do
416-
group = RSpec.describe do
417-
around(:each) { |e| e.run; raise "around" }
418-
example("e") { raise "example" }
419-
end
420-
421-
message = run_and_capture_reported_message(group)
422-
expect(message).to match(/An error occurred in an `around.* hook/i)
423-
end
424-
425-
it "prints any after hook errors rather than silencing them" do
426-
group = RSpec.describe do
427-
after(:each) { raise "after" }
428-
example("e") { raise "example" }
429-
end
430-
431-
message = run_and_capture_reported_message(group)
432-
expect(message).to match(/An error occurred in an after.* hook/i)
433-
end
434-
435-
it "does not print mock expectation errors" do
436-
group = RSpec.describe do
437-
example do
438-
foo = double
439-
expect(foo).to receive(:bar)
440-
raise "boom"
441-
end
442-
end
443-
444-
message = run_and_capture_reported_message(group)
445-
expect(message).to be_nil
427+
group = RSpec.describe do
428+
example { raise exception.freeze }
446429
end
430+
group.run
447431

448-
it "leaves a raised exception unmodified (GH-1103)" do
449-
# set the backtrace, otherwise MRI will build a whole new object,
450-
# and thus mess with our expectations. Rubinius and JRuby are not
451-
# affected.
452-
exception = StandardError.new
453-
exception.set_backtrace([])
454-
455-
group = RSpec.describe do
456-
example { raise exception.freeze }
457-
end
458-
group.run
459-
460-
actual = group.examples.first.execution_result.exception
461-
expect(actual.__id__).to eq(exception.__id__)
462-
end
432+
actual = group.examples.first.execution_result.exception
433+
expect(actual.__id__).to eq(exception.__id__)
463434
end
464435

465436
context "with --dry-run" do
@@ -752,6 +723,21 @@ def expect_pending_result(example)
752723
expect(ex).to fail_with(RSpec::Mocks::MockExpectationError)
753724
end
754725

726+
it 'skips mock verification if the example has already failed' do
727+
ex = nil
728+
boom = StandardError.new("boom")
729+
730+
RSpec.describe do
731+
ex = example do
732+
dbl = double
733+
expect(dbl).to receive(:Foo)
734+
raise boom
735+
end
736+
end.run
737+
738+
expect(ex.exception).to be boom
739+
end
740+
755741
it 'allows `after(:example)` hooks to satisfy mock expectations, since examples are not complete until their `after` hooks run' do
756742
ex = nil
757743

0 commit comments

Comments
 (0)