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

Commit d7ff6b3

Browse files
authored
Merge pull request #2316 from rspec/myron/handle-errors-in-suite-hooks
Handle errors in :suite hooks.
2 parents 2a6bff1 + bef9d61 commit d7ff6b3

15 files changed

+335
-22
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: 18 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,22 @@ 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+
1891+
# Do not run subsequent `before` hooks if one fails.
1892+
# But for `after` hooks, we run them all so that all
1893+
# cleanup bits get a chance to complete, minimizing the
1894+
# chance that resources get left behind.
1895+
break if hooks.equal?(@before_suite_hooks)
1896+
end
1897+
end
18851898
end
18861899

18871900
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/formatters/exception_presenter.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,8 @@ def encoded_string(string)
122122
end
123123

124124
def indent_lines(lines, failure_number)
125-
alignment_basis = "#{' ' * @indentation}#{failure_number}) "
125+
alignment_basis = ' ' * @indentation
126+
alignment_basis << "#{failure_number}) " if failure_number
126127
indentation = ' ' * alignment_basis.length
127128

128129
lines.each_with_index.map do |line, index|

lib/rspec/core/metadata.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ def description_separator(parent_part, child_part)
178178

179179
def build_description_from(parent_description=nil, my_description=nil)
180180
return parent_description.to_s unless my_description
181+
return my_description.to_s if parent_description.to_s == ''
181182
separator = description_separator(parent_description, my_description)
182183
(parent_description.to_s + separator) << my_description.to_s
183184
end

lib/rspec/core/notifications.rb

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,7 @@ def self.for(example)
5151
FailedExampleNotification
5252
end
5353

54-
exception_presenter = Formatters::ExceptionPresenter::Factory.new(example).build
55-
klass.new(example, exception_presenter)
54+
klass.new(example)
5655
end
5756

5857
private_class_method :new
@@ -202,7 +201,7 @@ def fully_formatted(failure_number, colorizer=::RSpec::Core::Formatters::Console
202201

203202
private
204203

205-
def initialize(example, exception_presenter)
204+
def initialize(example, exception_presenter=Formatters::ExceptionPresenter::Factory.new(example).build)
206205
@exception_presenter = exception_presenter
207206
super(example)
208207
end

lib/rspec/core/reporter.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,18 @@ def deprecation(hash)
151151
notify :deprecation, Notifications::DeprecationNotification.from_hash(hash)
152152
end
153153

154+
# @private
155+
# Provides a way to notify of an exception that is not tied to any
156+
# particular example (such as an exception encountered in a :suite hook).
157+
# Exceptions will be formatted the same way they normally are.
158+
def notify_non_example_exception(exception, context_description)
159+
@configuration.world.non_example_failure = true
160+
161+
example = Example.new(AnonymousExampleGroup, context_description, {})
162+
presenter = Formatters::ExceptionPresenter.new(exception, example, :indentation => 0)
163+
message presenter.fully_formatted(nil)
164+
end
165+
154166
# @private
155167
def finish
156168
close_after do

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: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
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 `block in (root)'" }
12+
elsif RSpec::Support::Ruby.jruby?
13+
let(:spec_line_suffix) { ":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 an `after(:suite)` hook.
114+
Failure/Error: c.after(:suite) { raise 'after 2' }
115+
116+
RuntimeError:
117+
after 2
118+
# ./the_spec.rb:6#{spec_line_suffix}
119+
120+
An error occurred in an `after(:suite)` hook.
121+
Failure/Error: c.after(:suite) { raise 'after 1' }
122+
123+
RuntimeError:
124+
after 1
125+
# ./the_spec.rb:5#{spec_line_suffix}
126+
127+
128+
Finished in n.nnnn seconds (files took n.nnnn seconds to load)
129+
0 examples, 0 failures
130+
EOS
131+
end
132+
end

spec/rspec/core/formatters/exception_presenter_spec.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,18 @@ module RSpec::Core
5151
EOS
5252
end
5353

54+
it "prints no identifier when no number argument is given" do
55+
expect(presenter.fully_formatted(nil)).to eq(<<-EOS.gsub(/^ +\|/, ''))
56+
|
57+
| Example
58+
| Failure/Error: # The failure happened here!#{ encoding_check }
59+
|
60+
| Boom
61+
| Bam
62+
| # ./spec/rspec/core/formatters/exception_presenter_spec.rb:#{line_num}
63+
EOS
64+
end
65+
5466
it "allows the caller to specify additional indentation" do
5567
the_presenter = Formatters::ExceptionPresenter.new(exception, example, :indentation => 4)
5668

spec/rspec/core/hooks_spec.rb

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,64 @@ def hook_collection_for(position, scope)
1818
end
1919
end
2020

21+
[:example, :context, :suite].each do |scope|
22+
describe "#before(#{scope})" do
23+
it "stops running subsequent hooks of the same type when an error is encountered" do
24+
sequence = []
25+
26+
RSpec.configure do |c|
27+
c.output_stream = StringIO.new
28+
29+
c.before(scope) do
30+
sequence << :hook_1
31+
raise "boom"
32+
end
33+
34+
c.before(scope) do
35+
sequence << :hook_2
36+
raise "boom"
37+
end
38+
end
39+
40+
RSpec.configuration.with_suite_hooks do
41+
RSpec.describe do
42+
example { sequence << :example }
43+
end.run
44+
end
45+
46+
expect(sequence).to eq [:hook_1]
47+
end
48+
end
49+
50+
describe "#after(#{scope})" do
51+
it "runs subsequent hooks of the same type when an error is encountered so all cleanup can complete" do
52+
sequence = []
53+
54+
RSpec.configure do |c|
55+
c.output_stream = StringIO.new
56+
57+
c.after(scope) do
58+
sequence << :hook_2
59+
raise "boom"
60+
end
61+
62+
c.after(scope) do
63+
sequence << :hook_1
64+
raise "boom"
65+
end
66+
end
67+
68+
RSpec.configuration.with_suite_hooks do
69+
RSpec.describe do
70+
example { sequence << :example }
71+
end.run
72+
end
73+
74+
expect(sequence).to eq [:example, :hook_1, :hook_2]
75+
end
76+
end
77+
end
78+
2179
[:before, :after, :around].each do |type|
2280
[:example, :context].each do |scope|
2381
next if type == :around && scope == :context

0 commit comments

Comments
 (0)