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

Commit bd2a9e2

Browse files
Set alternative RSpec invocations as callables in OptionParser
RSpec can be invoked with a number of CLI flags that result in substantially different code paths. As well as the default runner, there is: * `--init` - initializes the project directory and exits * `--help` - prints CLI usage and exits * `--version` - prints the version and exits * `--drb` - runs examples via a DRb connection * `--bisect` - starts a bisection process Previously some of these paths were invoked directly in the OptionParser (--init, --help and --version), while others were invoked by the main Runner.run call. This necessitated a lot of complexity in Runner.run, and a reasonable amount of stubbing of exit calls (which can be error-prone, and lead to a prematurely-terminated test run). This commit refactors the different invocations into callables, which the OptionParser sets as the `:runner` option. If an alternative invocation is found in Runner.run, it is used, falling back to the default of instantiating a Runner and starting it. The invocation callables do not handle exiting themselves, but are required to return an exit code instead. They must all accept the following arguments: @param options [RSpec::Core::ConfigurationOptions] @param err [IO] @param out [IO] At the moment the invocations are tested via the OptionParser specs - in future work we intend to extract these to their own module and test them independently.
1 parent 29efb0c commit bd2a9e2

File tree

5 files changed

+253
-104
lines changed

5 files changed

+253
-104
lines changed

lib/rspec/core/configuration_options.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ def configure_filter_manager(filter_manager)
3535
# @return [Hash] the final merged options, drawn from all external sources
3636
attr_reader :options
3737

38+
# @return [Array<String>] the original command-line arguments
39+
attr_reader :args
40+
3841
private
3942

4043
def organize_options

lib/rspec/core/option_parser.rb

Lines changed: 60 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def parser(options)
6969
parser.on('--bisect[=verbose]', 'Repeatedly runs the suite in order to isolate the failures to the ',
7070
' smallest reproducible case.') do |argument|
7171
options[:bisect] = argument || true
72+
options[:runner] = Callables.bisect
7273
end
7374

7475
parser.on('--[no-]fail-fast[=COUNT]', 'Abort the run after a certain number of failures (1 by default).') do |argument|
@@ -96,16 +97,17 @@ def parser(options)
9697
options[:dry_run] = true
9798
end
9899

99-
parser.on('-X', '--[no-]drb', 'Run examples via DRb.') do |o|
100-
options[:drb] = o
100+
parser.on('-X', '--[no-]drb', 'Run examples via DRb.') do |use_drb|
101+
options[:drb] = use_drb
102+
options[:runner] = Callables.drb_with_fallback if use_drb
101103
end
102104

103105
parser.on('--drb-port PORT', 'Port to connect to the DRb server.') do |o|
104106
options[:drb_port] = o.to_i
105107
end
106108

107109
parser.on('--init', 'Initialize your project with RSpec.') do |_cmd|
108-
initialize_project_and_exit
110+
options[:runner] = Callables.initialize_project
109111
end
110112

111113
parser.separator("\n **** Output ****\n\n")
@@ -242,7 +244,7 @@ def parser(options)
242244
parser.separator("\n **** Utility ****\n\n")
243245

244246
parser.on('-v', '--version', 'Display the version.') do
245-
print_version_and_exit
247+
options[:runner] = Callables.print_version
246248
end
247249

248250
# These options would otherwise be confusing to users, so we forcibly
@@ -254,7 +256,7 @@ def parser(options)
254256
invalid_options = %w[-d --I]
255257

256258
parser.on_tail('-h', '--help', "You're looking at it.") do
257-
print_help_and_exit(parser, invalid_options)
259+
options[:runner] = Callables.print_help(parser, invalid_options)
258260
end
259261

260262
# This prevents usage of the invalid_options.
@@ -282,21 +284,61 @@ def configure_only_failures(options)
282284
add_tag_filter(options, :inclusion_filter, :last_run_status, 'failed')
283285
end
284286

285-
def initialize_project_and_exit
286-
RSpec::Support.require_rspec_core "project_initializer"
287-
ProjectInitializer.new.run
288-
exit
289-
end
287+
# @private
288+
module Callables
289+
def self.initialize_project
290+
lambda do |*_args|
291+
RSpec::Support.require_rspec_core "project_initializer"
292+
ProjectInitializer.new.run
293+
0
294+
end
295+
end
290296

291-
def print_version_and_exit
292-
puts RSpec::Core::Version::STRING
293-
exit
294-
end
297+
def self.drb_with_fallback
298+
lambda do |options, err, out|
299+
require 'rspec/core/drb'
300+
begin
301+
return DRbRunner.new(options).run(err, out)
302+
rescue DRb::DRbConnError
303+
err.puts "No DRb server is running. Running in local process instead ..."
304+
end
305+
RSpec::Core::Runner.new(options).run(err, out)
306+
end
307+
end
308+
309+
def self.bisect
310+
lambda do |options, _err, _out|
311+
RSpec::Support.require_rspec_core "bisect/coordinator"
295312

296-
def print_help_and_exit(parser, invalid_options)
297-
# Removing the blank invalid options from the output.
298-
puts parser.to_s.gsub(/^\s+(#{invalid_options.join('|')})\s*$\n/, '')
299-
exit
313+
success = Bisect::Coordinator.bisect_with(
314+
options.args,
315+
RSpec.configuration,
316+
bisect_formatter_for(options.options[:bisect])
317+
)
318+
319+
success ? 0 : 1
320+
end
321+
end
322+
323+
def self.print_version
324+
lambda do |*_args|
325+
puts RSpec::Core::Version::STRING
326+
0
327+
end
328+
end
329+
330+
def self.print_help(parser, invalid_options)
331+
lambda do |*_args|
332+
# Removing the blank invalid options from the output.
333+
puts parser.to_s.gsub(/^\s+(#{invalid_options.join('|')})\s*$\n/, '')
334+
0
335+
end
336+
end
337+
338+
def self.bisect_formatter_for(argument)
339+
return Formatters::BisectDebugFormatter if argument == "verbose"
340+
Formatters::BisectProgressFormatter
341+
end
300342
end
301343
end
302344
end

lib/rspec/core/runner.rb

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -65,32 +65,11 @@ def self.run(args, err=$stderr, out=$stdout)
6565
trap_interrupt
6666
options = ConfigurationOptions.new(args)
6767

68-
if options.options[:drb]
69-
require 'rspec/core/drb'
70-
begin
71-
DRbRunner.new(options).run(err, out)
72-
return
73-
rescue DRb::DRbConnError
74-
err.puts "No DRb server is running. Running in local process instead ..."
75-
end
76-
elsif options.options[:bisect]
77-
RSpec::Support.require_rspec_core "bisect/coordinator"
78-
79-
success = Bisect::Coordinator.bisect_with(
80-
args,
81-
RSpec.configuration,
82-
bisect_formatter_for(options.options[:bisect])
83-
)
84-
85-
return success ? 0 : 1
68+
if options.options[:runner]
69+
options.options[:runner].call(options, err, out)
70+
else
71+
new(options).run(err, out)
8672
end
87-
88-
new(options).run(err, out)
89-
end
90-
91-
def self.bisect_formatter_for(argument)
92-
return Formatters::BisectDebugFormatter if argument == "verbose"
93-
Formatters::BisectProgressFormatter
9473
end
9574

9675
def initialize(options, configuration=RSpec.configuration, world=RSpec.world)

spec/rspec/core/option_parser_spec.rb

Lines changed: 165 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
require 'rspec/core/drb'
2+
require 'rspec/core/bisect/coordinator'
13
require 'rspec/core/project_initializer'
24

35
module RSpec::Core
@@ -70,8 +72,8 @@ module RSpec::Core
7072
it "won't display invalid options in the help output" do
7173
def generate_help_text
7274
parser = Parser.new(["--help"])
73-
allow(parser).to receive(:exit)
74-
parser.parse
75+
options = parser.parse
76+
options[:runner].call
7577
end
7678

7779
useless_lines = /^\s*--?\w+\s*$\n/
@@ -81,28 +83,101 @@ def generate_help_text
8183

8284
%w[ -v --version ].each do |option|
8385
describe option do
84-
it "prints the version and exits" do
86+
it "prints the version and returns a zero exit code" do
8587
parser = Parser.new([option])
86-
expect(parser).to receive(:exit)
8788

8889
expect {
89-
parser.parse
90+
options = parser.parse
91+
exit_code = options[:runner].call
92+
expect(exit_code).to eq(0)
9093
}.to output("#{RSpec::Core::Version::STRING}\n").to_stdout
9194
end
9295
end
9396
end
9497

98+
%w[ -X --drb ].each do |option|
99+
describe option do
100+
let(:parser) { Parser.new([option]) }
101+
let(:configuration_options) { double(:configuration_options) }
102+
let(:err) { StringIO.new }
103+
let(:out) { StringIO.new }
104+
105+
it 'sets the `:drb` option to true' do
106+
options = parser.parse
107+
108+
expect(options[:drb]).to be_truthy
109+
end
110+
111+
context 'when a DRb server is running' do
112+
it "builds a DRbRunner and runs the specs" do
113+
drb_proxy = instance_double(RSpec::Core::DRbRunner, :run => 0)
114+
allow(RSpec::Core::DRbRunner).to receive(:new).and_return(drb_proxy)
115+
116+
options = parser.parse
117+
exit_code = options[:runner].call(configuration_options, err, out)
118+
119+
expect(drb_proxy).to have_received(:run).with(err, out)
120+
expect(exit_code).to eq(0)
121+
end
122+
end
123+
124+
context 'when a DRb server is not running' do
125+
let(:runner) { instance_double(RSpec::Core::Runner, :run => 0) }
126+
127+
before(:each) do
128+
allow(RSpec::Core::Runner).to receive(:new).and_return(runner)
129+
allow(RSpec::Core::DRbRunner).to receive(:new).and_raise(DRb::DRbConnError)
130+
end
131+
132+
it "outputs a message" do
133+
options = parser.parse
134+
options[:runner].call(configuration_options, err, out)
135+
136+
expect(err.string).to include(
137+
"No DRb server is running. Running in local process instead ..."
138+
)
139+
end
140+
141+
it "builds a runner instance and runs the specs" do
142+
options = parser.parse
143+
options[:runner].call(configuration_options, err, out)
144+
145+
expect(RSpec::Core::Runner).to have_received(:new).with(configuration_options)
146+
expect(runner).to have_received(:run).with(err, out)
147+
end
148+
149+
if RSpec::Support::RubyFeatures.supports_exception_cause?
150+
it "prevents the DRb error from being listed as the cause of expectation failures" do
151+
allow(RSpec::Core::Runner).to receive(:new) do |configuration_options|
152+
raise RSpec::Expectations::ExpectationNotMetError
153+
end
154+
155+
expect {
156+
options = parser.parse
157+
options[:runner].call(configuration_options, err, out)
158+
159+
}.to raise_error(RSpec::Expectations::ExpectationNotMetError) do |e|
160+
expect(e.cause).to be_nil
161+
end
162+
end
163+
end
164+
end
165+
end
166+
end
167+
95168
describe "--init" do
96-
it "initializes a project and exits" do
169+
it "initializes a project and returns a 0 exit code" do
97170
project_init = instance_double(ProjectInitializer)
98171
allow(ProjectInitializer).to receive_messages(:new => project_init)
99172

100173
parser = Parser.new(["--init"])
101174

102175
expect(project_init).to receive(:run).ordered
103-
expect(parser).to receive(:exit).ordered
104176

105-
parser.parse
177+
options = parser.parse
178+
exit_code = options[:runner].call
179+
180+
expect(exit_code).to eq(0)
106181
end
107182
end
108183

@@ -317,6 +392,88 @@ def generate_help_text
317392
end
318393
end
319394

395+
describe "--bisect" do
396+
it "sets the `:bisect` option" do
397+
options = Parser.parse(%w[ --bisect ])
398+
399+
expect(options[:bisect]).to be_truthy
400+
end
401+
402+
it "sets a the `:runner` option with a callable" do
403+
options = Parser.parse(%w[ --bisect ])
404+
405+
expect(options[:runner]).to respond_to(:call)
406+
end
407+
408+
context "when the verbose option is specified" do
409+
it "records this in the options" do
410+
options = Parser.parse(%w[ --bisect=verbose ])
411+
412+
expect(options[:bisect]).to eq("verbose")
413+
end
414+
end
415+
416+
context 'when the runner is called' do
417+
let(:args) { double(:args) }
418+
let(:err) { double(:stderr) }
419+
let(:out) { double(:stdout) }
420+
let(:success) { true }
421+
let(:configuration_options) { double(:options, :args => args) }
422+
423+
before do
424+
allow(RSpec::Core::Bisect::Coordinator).to receive(:bisect_with).and_return(success)
425+
end
426+
427+
it "starts the bisection coordinator" do
428+
options = Parser.parse(%w[ --bisect ])
429+
allow(configuration_options).to receive(:options).and_return(options)
430+
options[:runner].call(configuration_options, err, out)
431+
432+
expect(RSpec::Core::Bisect::Coordinator).to have_received(:bisect_with).with(
433+
args,
434+
RSpec.configuration,
435+
Formatters::BisectProgressFormatter
436+
)
437+
end
438+
439+
context "when the bisection is successful" do
440+
it "returns 0" do
441+
options = Parser.parse(%w[ --bisect ])
442+
allow(configuration_options).to receive(:options).and_return(options)
443+
exit_code = options[:runner].call(configuration_options, err, out)
444+
445+
expect(exit_code).to eq(0)
446+
end
447+
end
448+
449+
context "when the bisection is unsuccessful" do
450+
let(:success) { false }
451+
452+
it "returns 1" do
453+
options = Parser.parse(%w[ --bisect ])
454+
allow(configuration_options).to receive(:options).and_return(options)
455+
exit_code = options[:runner].call(configuration_options, err, out)
456+
457+
expect(exit_code).to eq(1)
458+
end
459+
end
460+
461+
context "and the verbose option is specified" do
462+
it "starts the bisection coordinator with the debug formatter" do
463+
options = Parser.parse(%w[ --bisect=verbose ])
464+
allow(configuration_options).to receive(:options).and_return(options)
465+
options[:runner].call(configuration_options, err, out)
466+
467+
expect(RSpec::Core::Bisect::Coordinator).to have_received(:bisect_with).with(
468+
args,
469+
RSpec.configuration,
470+
Formatters::BisectDebugFormatter
471+
)
472+
end
473+
end
474+
end
475+
end
476+
320477
describe '--profile' do
321478
it 'sets profile_examples to true by default' do
322479
options = Parser.parse(%w[--profile])

0 commit comments

Comments
 (0)