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

Commit b0d0843

Browse files
authored
Merge pull request #2749 from robotdana/error_exit_code
Add error-exit-code to differentiate from failures
2 parents eaaca6d + 0709dcb commit b0d0843

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)