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

Commit 6c3b4c6

Browse files
committed
Handle errors in :suite hooks.
Previously, we just allowed the error to propagate to the user, which was a subpar experience for a few reasons: - The error would cause RSpec to crash, leading to a long stack trace containing lots of extraneous info the user did not see. - The error was not formatted nicely like other errors that happen while RSpec runs. - If the error happened in an `after(:suite)` hook, the test suite had finished running all specs but the summary of the results would not get printed since RSpec had crashed. Now, we handle errors in :suite hooks and format the output the same way failures and errors are normally printed.
1 parent 5ae27f2 commit 6c3b4c6

File tree

9 files changed

+199
-15
lines changed

9 files changed

+199
-15
lines changed

Changelog.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ Bug Fixes:
1919
conflicting keys if the value in the host group was inherited from
2020
a parent group instead of being specified at that level.
2121
(Myron Marston, #2307)
22+
* Handle errors in `:suite` hooks and provided the same nicely formatted
23+
output as errors that happen in examples. (Myron Marston, #2316)
2224

2325
### 3.5.2 / 2016-07-28
2426
[Full Changelog](http://github.com/rspec/rspec-core/compare/v3.5.1...v3.5.2)

lib/rspec/core/configuration.rb

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1835,12 +1835,11 @@ def around(scope=nil, *meta, &block)
18351835
def with_suite_hooks
18361836
return yield if dry_run?
18371837

1838-
hook_context = SuiteHookContext.new
18391838
begin
1840-
run_hooks_with(@before_suite_hooks, hook_context)
1839+
run_suite_hooks("a `before(:suite)` hook", @before_suite_hooks)
18411840
yield
18421841
ensure
1843-
run_hooks_with(@after_suite_hooks, hook_context)
1842+
run_suite_hooks("an `after(:suite)` hook", @after_suite_hooks)
18441843
end
18451844
end
18461845

@@ -1880,8 +1879,16 @@ def handle_suite_hook(scope, meta)
18801879
yield
18811880
end
18821881

1883-
def run_hooks_with(hooks, hook_context)
1884-
hooks.each { |h| h.run(hook_context) }
1882+
def run_suite_hooks(hook_description, hooks)
1883+
context = SuiteHookContext.new(hook_description, reporter)
1884+
1885+
hooks.each do |hook|
1886+
begin
1887+
hook.run(context)
1888+
rescue Support::AllExceptionsExceptOnesWeMustNotRescue => ex
1889+
context.set_exception(ex)
1890+
end
1891+
end
18851892
end
18861893

18871894
def get_files_to_run(paths)

lib/rspec/core/example.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -632,16 +632,16 @@ def issue_deprecation(_method_name, *_args)
632632
# @private
633633
# Provides an execution context for before/after :suite hooks.
634634
class SuiteHookContext < Example
635-
def initialize
636-
super(AnonymousExampleGroup, "", {})
635+
def initialize(hook_description, reporter)
636+
super(AnonymousExampleGroup, hook_description, {})
637637
@example_group_instance = AnonymousExampleGroup.new
638+
@reporter = reporter
638639
end
639640

640641
# rubocop:disable Style/AccessorMethodName
641-
642-
# To ensure we don't silence errors.
643642
def set_exception(exception)
644-
raise exception
643+
reporter.notify_non_example_exception(exception, "An error occurred in #{description}.")
644+
RSpec.world.wants_to_quit = true
645645
end
646646
# rubocop:enable Style/AccessorMethodName
647647
end

lib/rspec/core/reporter.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@ def deprecation(hash)
156156
# particular exception (such as an exception encountered in a :suite hook).
157157
# Exceptions will be formatted the same way they normally are.
158158
def notify_non_example_exception(exception, context_description)
159+
@configuration.world.non_example_failure = true
160+
159161
example = Example.new(AnonymousExampleGroup, context_description, {})
160162
presenter = Formatters::ExceptionPresenter.new(exception, example, :indentation => 0)
161163
message presenter.fully_formatted(nil)

lib/rspec/core/runner.rb

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,14 +109,17 @@ def setup(err, out)
109109
# failed.
110110
def run_specs(example_groups)
111111
examples_count = @world.example_count(example_groups)
112-
@configuration.reporter.report(examples_count) do |reporter|
112+
success = @configuration.reporter.report(examples_count) do |reporter|
113113
@configuration.with_suite_hooks do
114114
if examples_count == 0 && @configuration.fail_if_no_examples
115115
return @configuration.failure_exit_code
116116
end
117-
example_groups.map { |g| g.run(reporter) }.all? ? 0 : @configuration.failure_exit_code
117+
118+
example_groups.map { |g| g.run(reporter) }.all?
118119
end
119-
end
120+
end && !@world.non_example_failure
121+
122+
success ? 0 : @configuration.failure_exit_code
120123
end
121124

122125
private

lib/rspec/core/world.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ class World
1010
# Used internally to determine what to do when a SIGINT is received.
1111
attr_accessor :wants_to_quit
1212

13+
# Used internally to signal that a failure outside of an example
14+
# has occurred, and that therefore the exit status should indicate
15+
# the run failed.
16+
# @private
17+
attr_accessor :non_example_failure
18+
1319
def initialize(configuration=RSpec.configuration)
1420
@configuration = configuration
1521
configuration.world = self
@@ -224,6 +230,9 @@ def fail_if_config_and_cli_options_invalid
224230
# @private
225231
# Provides a null implementation for initial use by configuration.
226232
module Null
233+
def self.non_example_failure; end
234+
def self.non_example_failure=(_); end
235+
227236
def self.registered_example_group_files
228237
[]
229238
end
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
require 'support/aruba_support'
2+
require 'support/formatter_support'
3+
4+
RSpec.describe 'Suite hook errors' do
5+
include_context "aruba support"
6+
include FormatterSupport
7+
8+
let(:failure_exit_code) { rand(97) + 2 } # 2..99
9+
10+
if RSpec::Support::Ruby.jruby_9000?
11+
let(:spec_line_suffix) { ":in `(root)'" }
12+
elsif RSpec::Support::Ruby.jruby?
13+
let(:spec_line_suffix) { ":in `block in (root)'" }
14+
elsif RUBY_VERSION == "1.8.7"
15+
let(:spec_line_suffix) { "" }
16+
else
17+
let(:spec_line_suffix) { ":in `block (2 levels) in <top (required)>'" }
18+
end
19+
20+
before do
21+
# get out of `aruba` sub-dir so that `filter_gems_from_backtrace 'aruba'`
22+
# below does not filter out our spec file.
23+
expect(dirs.pop).to eq "aruba"
24+
25+
clean_current_dir
26+
27+
RSpec.configure do |c|
28+
c.filter_gems_from_backtrace "aruba"
29+
c.backtrace_exclusion_patterns << %r{/rspec-core/spec/} << %r{rspec_with_simplecov}
30+
c.failure_exit_code = failure_exit_code
31+
end
32+
end
33+
34+
def run_spec_expecting_non_zero(before_or_after)
35+
write_file "the_spec.rb", "
36+
RSpec.configure do |c|
37+
c.#{before_or_after}(:suite) do
38+
raise 'boom'
39+
end
40+
end
41+
42+
RSpec.describe do
43+
it { }
44+
end
45+
"
46+
47+
run_command "the_spec.rb"
48+
expect(last_cmd_exit_status).to eq(failure_exit_code)
49+
normalize_durations(last_cmd_stdout)
50+
end
51+
52+
it 'nicely formats errors in `before(:suite)` hooks and exits with non-zero' do
53+
output = run_spec_expecting_non_zero(:before)
54+
expect(output).to eq unindent(<<-EOS)
55+
56+
An error occurred in a `before(:suite)` hook.
57+
Failure/Error: raise 'boom'
58+
59+
RuntimeError:
60+
boom
61+
# ./the_spec.rb:4#{spec_line_suffix}
62+
63+
64+
Finished in n.nnnn seconds (files took n.nnnn seconds to load)
65+
0 examples, 0 failures
66+
EOS
67+
end
68+
69+
it 'nicely formats errors in `after(:suite)` hooks and exits with non-zero' do
70+
output = run_spec_expecting_non_zero(:after)
71+
expect(output).to eq unindent(<<-EOS)
72+
.
73+
An error occurred in an `after(:suite)` hook.
74+
Failure/Error: raise 'boom'
75+
76+
RuntimeError:
77+
boom
78+
# ./the_spec.rb:4#{spec_line_suffix}
79+
80+
81+
Finished in n.nnnn seconds (files took n.nnnn seconds to load)
82+
1 example, 0 failures
83+
EOS
84+
end
85+
86+
it 'nicely formats errors from multiple :suite hooks of both types and exits with non-zero' do
87+
write_file "the_spec.rb", "
88+
RSpec.configure do |c|
89+
c.before(:suite) { raise 'before 1' }
90+
c.before(:suite) { raise 'before 2' }
91+
c.after(:suite) { raise 'after 1' }
92+
c.after(:suite) { raise 'after 2' }
93+
end
94+
95+
RSpec.describe do
96+
it { }
97+
end
98+
"
99+
100+
run_command "the_spec.rb"
101+
expect(last_cmd_exit_status).to eq(failure_exit_code)
102+
output = normalize_durations(last_cmd_stdout)
103+
104+
expect(output).to eq unindent(<<-EOS)
105+
106+
An error occurred in a `before(:suite)` hook.
107+
Failure/Error: c.before(:suite) { raise 'before 1' }
108+
109+
RuntimeError:
110+
before 1
111+
# ./the_spec.rb:3#{spec_line_suffix}
112+
113+
An error occurred in a `before(:suite)` hook.
114+
Failure/Error: c.before(:suite) { raise 'before 2' }
115+
116+
RuntimeError:
117+
before 2
118+
# ./the_spec.rb:4#{spec_line_suffix}
119+
120+
An error occurred in an `after(:suite)` hook.
121+
Failure/Error: c.after(:suite) { raise 'after 2' }
122+
123+
RuntimeError:
124+
after 2
125+
# ./the_spec.rb:6#{spec_line_suffix}
126+
127+
An error occurred in an `after(:suite)` hook.
128+
Failure/Error: c.after(:suite) { raise 'after 1' }
129+
130+
RuntimeError:
131+
after 1
132+
# ./the_spec.rb:5#{spec_line_suffix}
133+
134+
135+
Finished in n.nnnn seconds (files took n.nnnn seconds to load)
136+
0 examples, 0 failures
137+
EOS
138+
end
139+
end

spec/rspec/core/reporter_spec.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module RSpec::Core
33
include FormatterSupport
44

55
let(:config) { Configuration.new }
6+
let(:world) { World.new(config) }
67
let(:reporter) { Reporter.new config }
78
let(:start_time) { Time.now }
89
let(:example) { super() }
@@ -283,6 +284,10 @@ module RSpec::Core
283284

284285
describe "#notify_non_example_exception" do
285286
it "sends a `message` notification that contains the formatted exception details" do
287+
if RSpec::Support::Ruby.jruby_9000?
288+
pending "RSpec gets `Unable to find matching line from backtrace` on JRuby 9000"
289+
end
290+
286291
formatter_out = StringIO.new
287292
formatter = Formatters::ProgressFormatter.new(formatter_out)
288293
reporter.register_listener formatter, :message
@@ -301,6 +306,12 @@ module RSpec::Core
301306
|# #{Metadata.relative_path(__FILE__)}:#{line}
302307
EOS
303308
end
309+
310+
it "records the fact that a non example failure has occurred" do
311+
expect {
312+
reporter.notify_non_example_exception(Exception.new, "NonExample Context")
313+
}.to change(world, :non_example_failure).from(a_falsey_value).to(true)
314+
end
304315
end
305316
end
306317
end

spec/rspec/core/suite_hooks_spec.rb

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,23 @@ module RSpec::Core
1515
}.not_to yield_control
1616
end
1717

18-
it 'allows errors in the hook to propagate to the user' do
18+
it 'notifies about errors in the hook' do
1919
RSpec.configuration.__send__(registration_method, :suite) { 1 / 0 }
2020

21+
expect(RSpec.configuration.reporter).to receive(:notify_non_example_exception).with(
22+
ZeroDivisionError, /suite\)` hook/
23+
)
24+
25+
RSpec.configuration.with_suite_hooks { }
26+
end
27+
28+
it 'sets `wants_to_quit` when an error occurs so that the suite does not get run' do
29+
RSpec.configuration.__send__(registration_method, :suite) { 1 / 0 }
30+
allow(RSpec.configuration.reporter).to receive(:notify_non_example_exception)
31+
2132
expect {
2233
RSpec.configuration.with_suite_hooks { }
23-
}.to raise_error(ZeroDivisionError)
34+
}.to change(RSpec.world, :wants_to_quit).from(a_falsey_value).to(true)
2435
end
2536

2637
it 'runs in the context of an example group' do

0 commit comments

Comments
 (0)