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

Commit 0709dcb

Browse files
committed
Add error-exit-code to differentiate from failures
it can be helpful to know if RSpec fails because of an example or if it errors out because it couldn't load a spec file, or there was an issue in the a before(:suite) hook or etc i've named the setting error-exit-code, and tried to add it to all the relevant places, but i don't know rspec codebase well so there was some guessing. in particular i'm not sure what bisect should do. i have it fall back to the failure-exit-code before defaulting to 1 so there's no changes if people don't opt in to the setting. the specific use of this is our CI automatically retries failures using the persistence file, and assumes if they pass on that retry they were flaky. however, the persistence file isn't written to when there's an error outside of examples, so this could mean falsely passing builds. by checking for a different exit code, and then not running the retry we can avoid this issue.
1 parent eaaca6d commit 0709dcb

File tree

12 files changed

+197
-11
lines changed

12 files changed

+197
-11
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
Feature: error exit code
2+
3+
Use the `error_exit_code` option to set a custom exit code when RSpec fails outside an example.
4+
5+
```ruby
6+
RSpec.configure { |c| c.error_exit_code = 42 }
7+
```
8+
9+
Background:
10+
Given a file named "spec/spec_helper.rb" with:
11+
"""ruby
12+
RSpec.configure { |c| c.error_exit_code = 42 }
13+
"""
14+
15+
Scenario: A erroring spec with the default exit code
16+
Given a file named "spec/typo_spec.rb" with:
17+
"""ruby
18+
RSpec.escribe "something" do # intentional typo
19+
it "works" do
20+
true
21+
end
22+
end
23+
"""
24+
When I run `rspec spec/typo_spec.rb`
25+
Then the exit status should be 1
26+
27+
Scenario: A erroring spec with a custom exit code
28+
Given a file named "spec/typo_spec.rb" with:
29+
"""ruby
30+
require 'spec_helper'
31+
RSpec.escribe "something" do # intentional typo
32+
it "works" do
33+
true
34+
end
35+
end
36+
"""
37+
When I run `rspec spec/typo_spec.rb`
38+
And the exit status should be 42
39+
40+
41+
Scenario: Success running specs spec with a custom error exit code defined
42+
Given a file named "spec/example_spec.rb" with:
43+
"""ruby
44+
require 'spec_helper'
45+
RSpec.describe "something" do
46+
it "works" do
47+
true
48+
end
49+
end
50+
"""
51+
When I run `rspec spec/example_spec.rb`
52+
Then the exit status should be 0

features/configuration/failure_exit_code.feature

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,32 @@ Feature: failure exit code
3737
When I run `rspec spec/example_spec.rb`
3838
Then the exit status should be 42
3939

40+
Scenario: An error running specs spec with a custom exit code
41+
Given a file named "spec/typo_spec.rb" with:
42+
"""ruby
43+
require 'spec_helper'
44+
RSpec.escribe "something" do # intentional typo
45+
it "works" do
46+
true
47+
end
48+
end
49+
"""
50+
When I run `rspec spec/typo_spec.rb`
51+
Then the exit status should be 42
52+
53+
Scenario: Success running specs spec with a custom exit code defined
54+
Given a file named "spec/example_spec.rb" with:
55+
"""ruby
56+
require 'spec_helper'
57+
RSpec.describe "something" do
58+
it "works" do
59+
true
60+
end
61+
end
62+
"""
63+
When I run `rspec spec/example_spec.rb`
64+
Then the exit status should be 0
65+
4066
Scenario: Exit with the default exit code when an `at_exit` hook is added upstream
4167
Given a file named "exit_at_spec.rb" with:
4268
"""ruby

lib/rspec/core/configuration.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,11 @@ def fail_fast=(value)
242242
# @return [Integer]
243243
add_setting :failure_exit_code
244244

245+
# @macro add_setting
246+
# The exit code to return if there are any errors outside examples (default: failure_exit_code)
247+
# @return [Integer]
248+
add_setting :error_exit_code
249+
245250
# @macro add_setting
246251
# Whether or not to fail when there are no RSpec examples (default: false).
247252
# @return [Boolean]
@@ -523,6 +528,7 @@ def initialize
523528
@pattern = '**{,/*/**}/*_spec.rb'
524529
@exclude_pattern = ''
525530
@failure_exit_code = 1
531+
@error_exit_code = nil # so it can be overridden by failure exit code
526532
@fail_if_no_examples = false
527533
@spec_files_loaded = false
528534

lib/rspec/core/drb.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ def options
5151
argv << "--order" << @submitted_options[:order] if @submitted_options[:order]
5252

5353
add_failure_exit_code(argv)
54+
add_error_exit_code(argv)
5455
add_full_description(argv)
5556
add_filter(argv, :inclusion, @filter_manager.inclusions)
5657
add_filter(argv, :exclusion, @filter_manager.exclusions)
@@ -67,6 +68,12 @@ def add_failure_exit_code(argv)
6768
argv << "--failure-exit-code" << @submitted_options[:failure_exit_code].to_s
6869
end
6970

71+
def add_error_exit_code(argv)
72+
return unless @submitted_options[:error_exit_code]
73+
74+
argv << "--error-exit-code" << @submitted_options[:error_exit_code].to_s
75+
end
76+
7077
def add_full_description(argv)
7178
return unless @submitted_options[:full_description]
7279

lib/rspec/core/invocations.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def call(options, err, out)
3737
runner, options.args, formatter
3838
)
3939

40-
success ? 0 : runner.configuration.failure_exit_code
40+
runner.exit_code(success)
4141
end
4242

4343
private

lib/rspec/core/option_parser.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@ def parser(options)
9595
options[:failure_exit_code] = code
9696
end
9797

98+
parser.on('--error-exit-code CODE', Integer,
99+
'Override the exit code used when there are errors loading or running specs outside of examples.') do |code|
100+
options[:error_exit_code] = code
101+
end
102+
98103
parser.on('-X', '--[no-]drb', 'Run examples via DRb.') do |use_drb|
99104
options[:drb] = use_drb
100105
options[:runner] = RSpec::Core::Invocations::DRbWithFallback.new if use_drb

lib/rspec/core/runner.rb

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ def initialize(options, configuration=RSpec.configuration, world=RSpec.world)
8484
# @param out [IO] output stream
8585
def run(err, out)
8686
setup(err, out)
87-
return @configuration.reporter.exit_early(@configuration.failure_exit_code) if RSpec.world.wants_to_quit
87+
return @configuration.reporter.exit_early(exit_code) if RSpec.world.wants_to_quit
8888

8989
run_specs(@world.ordered_example_groups).tap do
9090
persist_example_statuses
@@ -112,17 +112,17 @@ def setup(err, out)
112112
# failed.
113113
def run_specs(example_groups)
114114
examples_count = @world.example_count(example_groups)
115-
success = @configuration.reporter.report(examples_count) do |reporter|
115+
examples_passed = @configuration.reporter.report(examples_count) do |reporter|
116116
@configuration.with_suite_hooks do
117117
if examples_count == 0 && @configuration.fail_if_no_examples
118118
return @configuration.failure_exit_code
119119
end
120120

121121
example_groups.map { |g| g.run(reporter) }.all?
122122
end
123-
end && !@world.non_example_failure
123+
end
124124

125-
success ? 0 : @configuration.failure_exit_code
125+
exit_code(examples_passed)
126126
end
127127

128128
# @private
@@ -186,6 +186,14 @@ def self.handle_interrupt
186186
end
187187
end
188188

189+
# @private
190+
def exit_code(examples_passed=false)
191+
return @configuration.error_exit_code || @configuration.failure_exit_code if @world.non_example_failure
192+
return @configuration.failure_exit_code unless examples_passed
193+
194+
0
195+
end
196+
189197
private
190198

191199
def persist_example_statuses

spec/integration/spec_file_load_errors_spec.rb

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
include FormatterSupport
77

88
let(:failure_exit_code) { rand(97) + 2 } # 2..99
9+
let(:error_exit_code) { failure_exit_code + 1 } # 3..100
910

1011
if RSpec::Support::Ruby.jruby_9000?
1112
let(:spec_line_suffix) { ":in `<main>'" }
@@ -24,14 +25,15 @@
2425
c.filter_gems_from_backtrace "gems/aruba"
2526
c.backtrace_exclusion_patterns << %r{/rspec-core/spec/} << %r{rspec_with_simplecov}
2627
c.failure_exit_code = failure_exit_code
28+
c.error_exit_code = error_exit_code
2729
end
2830
end
2931

3032
it 'nicely handles load-time errors from --require files' do
3133
write_file_formatted "helper_with_error.rb", "raise 'boom'"
3234

3335
run_command "--require ./helper_with_error"
34-
expect(last_cmd_exit_status).to eq(failure_exit_code)
36+
expect(last_cmd_exit_status).to eq(error_exit_code)
3537
output = normalize_durations(last_cmd_stdout)
3638
expect(output).to eq unindent(<<-EOS)
3739
@@ -60,7 +62,7 @@
6062
"
6163

6264
run_command "--require ./helper_with_error 1_spec.rb"
63-
expect(last_cmd_exit_status).to eq(failure_exit_code)
65+
expect(last_cmd_exit_status).to eq(error_exit_code)
6466
output = normalize_durations(last_cmd_stdout)
6567
expect(output).to eq unindent(<<-EOS)
6668
@@ -109,7 +111,7 @@
109111
"
110112

111113
run_command "1_spec.rb 2_spec.rb 3_spec.rb"
112-
expect(last_cmd_exit_status).to eq(failure_exit_code)
114+
expect(last_cmd_exit_status).to eq(error_exit_code)
113115
output = normalize_durations(last_cmd_stdout)
114116
expect(output).to eq unindent(<<-EOS)
115117

spec/integration/suite_hooks_errors_spec.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
include FormatterSupport
77

88
let(:failure_exit_code) { rand(97) + 2 } # 2..99
9+
let(:error_exit_code) { failure_exit_code + 2 } # 4..101
910

1011
if RSpec::Support::Ruby.jruby_9000?
1112
let(:spec_line_suffix) { ":in `block in (root)'" }
@@ -24,6 +25,7 @@
2425
c.filter_gems_from_backtrace "gems/aruba"
2526
c.backtrace_exclusion_patterns << %r{/rspec-core/spec/} << %r{rspec_with_simplecov}
2627
c.failure_exit_code = failure_exit_code
28+
c.error_exit_code = error_exit_code
2729
end
2830
end
2931

@@ -41,7 +43,7 @@ def run_spec_expecting_non_zero(before_or_after)
4143
"
4244

4345
run_command "the_spec.rb"
44-
expect(last_cmd_exit_status).to eq(failure_exit_code)
46+
expect(last_cmd_exit_status).to eq(error_exit_code)
4547
normalize_durations(last_cmd_stdout)
4648
end
4749

@@ -96,7 +98,7 @@ def run_spec_expecting_non_zero(before_or_after)
9698
"
9799

98100
run_command "the_spec.rb"
99-
expect(last_cmd_exit_status).to eq(failure_exit_code)
101+
expect(last_cmd_exit_status).to eq(error_exit_code)
100102
output = normalize_durations(last_cmd_stdout)
101103

102104
expect(output).to eq unindent(<<-EOS)

spec/rspec/core/configuration_options_spec.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,18 @@
321321
end
322322
end
323323

324+
describe "--error-exit-code" do
325+
it "sets :error_exit_code" do
326+
expect(parse_options('--error-exit-code', '0')).to include(:error_exit_code => 0)
327+
expect(parse_options('--error-exit-code', '1')).to include(:error_exit_code => 1)
328+
expect(parse_options('--error-exit-code', '2')).to include(:error_exit_code => 2)
329+
end
330+
331+
it "overrides previous :error_exit_code" do
332+
expect(parse_options('--error-exit-code', '2', '--error-exit-code', '3')).to include(:error_exit_code => 3)
333+
end
334+
end
335+
324336
describe "--dry-run" do
325337
it "defaults to nil" do
326338
expect(parse_options[:dry_run]).to be(nil)

spec/rspec/core/configuration_spec.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2862,6 +2862,28 @@ def emulate_not_configured_expectation_framework
28622862
end
28632863
end
28642864

2865+
describe '#failure_exit_code' do
2866+
it 'defaults to 1' do
2867+
expect(config.failure_exit_code).to eq 1
2868+
end
2869+
2870+
it 'is configurable' do
2871+
config.failure_exit_code = 2
2872+
expect(config.failure_exit_code).to eq 2
2873+
end
2874+
end
2875+
2876+
describe '#error_exit_code' do
2877+
it 'defaults to nil' do
2878+
expect(config.error_exit_code).to eq nil
2879+
end
2880+
2881+
it 'is configurable' do
2882+
config.error_exit_code = 2
2883+
expect(config.error_exit_code).to eq 2
2884+
end
2885+
end
2886+
28652887
describe "#shared_context_metadata_behavior" do
28662888
it "defaults to :trigger_inclusion for backwards compatibility" do
28672889
expect(config.shared_context_metadata_behavior).to eq :trigger_inclusion

spec/rspec/core/runner_spec.rb

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,51 @@ def interrupt
232232
end
233233
end
234234

235+
describe '#exit_code' do
236+
let(:world) { World.new }
237+
let(:config) { Configuration.new }
238+
let(:runner) { Runner.new({}, config, world) }
239+
240+
it 'defaults to 1' do
241+
expect(runner.exit_code).to eq 1
242+
end
243+
244+
it 'is failure_exit_code by default' do
245+
config.failure_exit_code = 2
246+
expect(runner.exit_code).to eq 2
247+
end
248+
249+
it 'is failure_exit_code when world is errored by default' do
250+
world.non_example_failure = true
251+
config.failure_exit_code = 2
252+
expect(runner.exit_code).to eq 2
253+
end
254+
255+
it 'is error_exit_code when world is errored by and both are defined' do
256+
world.non_example_failure = true
257+
config.failure_exit_code = 2
258+
config.error_exit_code = 3
259+
expect(runner.exit_code).to eq 3
260+
end
261+
262+
it 'is error_exit_code when world is errored by and failure exit code is not defined' do
263+
world.non_example_failure = true
264+
config.error_exit_code = 3
265+
expect(runner.exit_code).to eq 3
266+
end
267+
268+
it 'can be given success' do
269+
config.error_exit_code = 3
270+
expect(runner.exit_code(true)).to eq 0
271+
end
272+
273+
it 'can be given success, but non_example_failure=true will still cause an error code' do
274+
world.non_example_failure = true
275+
config.error_exit_code = 3
276+
expect(runner.exit_code(true)).to eq 3
277+
end
278+
end
279+
235280
describe ".invoke" do
236281
let(:runner) { RSpec::Core::Runner }
237282

@@ -287,7 +332,6 @@ def interrupt
287332
expect(process_proxy).to have_received(:run).with(err, out)
288333
end
289334
end
290-
291335
end
292336

293337
context "when run" do

0 commit comments

Comments
 (0)