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

Commit 29efb0c

Browse files
Support --bisect in SPEC_OPTS
Currently if RSpec is invoked with SPEC_OPTS="--bisect", an infinite number of child processes is spawned. This is because the bisect feature works by spawning child processes for each bisection run. In the normal cli usage case, the --bisect argument is removed before the child processes are invoked. It is not removed from the SPEC_OPTS variable, however, meaning each child process believes it's starting a new bisection. This commit allows Bisect::Runner to invoke its child processes with a modified environment, removing --bisect if present. In modern rubies we can use Open3.capture2e's optional environment argument - in older rubies and JRuby we have to fall back on modifying the parent process's environment, then restoring it after the child process has run. Previously the Bisect::Runner was invoked during options parsing. However, because SPEC_OPTS and the cli args are parsed by separate instances of the parser, invoking the bisection during SPEC_OPTS parsing meant the cli args were lost. To address this I've made the OptionParser set a :bisect option, which is then picked up in the main Runner to invoke the actual bisection. This has increased the complexity of Runner.run, and there are other invocations remaining in the OptionParser (e.g. --init and --help). These will be refactored in a follow-up commit, so that the OptionParser sets a callable in each case that can be called in the main Runner.
1 parent bc14826 commit 29efb0c

File tree

5 files changed

+101
-26
lines changed

5 files changed

+101
-26
lines changed

lib/rspec/core/bisect/runner.rb

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
RSpec::Support.require_rspec_core "shell_escape"
22
require 'open3'
3+
require 'shellwords'
34

45
module RSpec
56
module Core
@@ -71,24 +72,45 @@ def run_locations(*capture_args)
7172
# https://github.com/jruby/jruby/issues/2766
7273
if Open3.respond_to?(:capture2e) && !RSpec::Support::Ruby.jruby?
7374
def run_command(cmd)
74-
Open3.capture2e(cmd).first
75+
Open3.capture2e(bisect_environment_hash, cmd).first
7576
end
7677
else # for 1.8.7
7778
# :nocov:
7879
def run_command(cmd)
7980
out = err = nil
8081

82+
original_spec_opts = ENV['SPEC_OPTS']
83+
ENV['SPEC_OPTS'] = spec_opts_without_bisect
84+
8185
Open3.popen3(cmd) do |_, stdout, stderr|
8286
# Reading the streams blocks until the process is complete
8387
out = stdout.read
8488
err = stderr.read
8589
end
8690

8791
"Stdout:\n#{out}\n\nStderr:\n#{err}"
92+
ensure
93+
ENV['SPEC_OPTS'] = original_spec_opts
8894
end
8995
# :nocov:
9096
end
9197

98+
def bisect_environment_hash
99+
if ENV.key?('SPEC_OPTS')
100+
{ 'SPEC_OPTS' => spec_opts_without_bisect }
101+
else
102+
{}
103+
end
104+
end
105+
106+
def spec_opts_without_bisect
107+
Shellwords.join(
108+
Shellwords.split(ENV.fetch('SPEC_OPTS', '')).reject do |arg|
109+
arg =~ /^--bisect/
110+
end
111+
)
112+
end
113+
92114
def reusable_cli_options
93115
@reusable_cli_options ||= begin
94116
opts = original_cli_args_without_locations

lib/rspec/core/option_parser.rb

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def parser(options)
6868

6969
parser.on('--bisect[=verbose]', 'Repeatedly runs the suite in order to isolate the failures to the ',
7070
' smallest reproducible case.') do |argument|
71-
bisect_and_exit(argument)
71+
options[:bisect] = argument || true
7272
end
7373

7474
parser.on('--[no-]fail-fast[=COUNT]', 'Abort the run after a certain number of failures (1 by default).') do |argument|
@@ -288,23 +288,6 @@ def initialize_project_and_exit
288288
exit
289289
end
290290

291-
def bisect_and_exit(argument)
292-
RSpec::Support.require_rspec_core "bisect/coordinator"
293-
294-
success = Bisect::Coordinator.bisect_with(
295-
original_args,
296-
RSpec.configuration,
297-
bisect_formatter_for(argument)
298-
)
299-
300-
exit(success ? 0 : 1)
301-
end
302-
303-
def bisect_formatter_for(argument)
304-
return Formatters::BisectDebugFormatter if argument == "verbose"
305-
Formatters::BisectProgressFormatter
306-
end
307-
308291
def print_version_and_exit
309292
puts RSpec::Core::Version::STRING
310293
exit

lib/rspec/core/runner.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,26 @@ def self.run(args, err=$stderr, out=$stdout)
7373
rescue DRb::DRbConnError
7474
err.puts "No DRb server is running. Running in local process instead ..."
7575
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
7686
end
7787

7888
new(options).run(err, out)
7989
end
8090

91+
def self.bisect_formatter_for(argument)
92+
return Formatters::BisectDebugFormatter if argument == "verbose"
93+
Formatters::BisectProgressFormatter
94+
end
95+
8196
def initialize(options, configuration=RSpec.configuration, world=RSpec.world)
8297
@options = options
8398
@configuration = configuration

spec/integration/bisect_spec.rb

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,10 @@ module RSpec::Core
1010

1111
def bisect(cli_args, expected_status=nil)
1212
RSpec.configuration.output_stream = formatter_output
13-
parser = Parser.new(cli_args + ["--bisect"])
14-
expect(parser).to receive(:exit).with(expected_status) if expected_status
1513

1614
expect {
17-
parser.parse
15+
status = RSpec::Core::Runner.run(cli_args + ["--bisect"])
16+
expect(status).to eq(expected_status) if expected_status
1817
}.to avoid_outputting.to_stdout_from_any_process.and avoid_outputting.to_stderr_from_any_process
1918

2019
normalize_durations(formatter_output.string)

spec/rspec/core/bisect/runner_spec.rb

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -213,19 +213,75 @@ def repro_command_from(ids)
213213
open3_method = Open3.respond_to?(:capture2e) ? :capture2e : :popen3
214214
open3_method = :popen3 if RSpec::Support::Ruby.jruby?
215215

216+
def called_environment
217+
@called_environment
218+
end
219+
220+
if open3_method == :capture2e
221+
RSpec::Matchers.define :have_invoked_command_with_env do |command, environment|
222+
match do |block|
223+
block.call
224+
225+
expect(Open3).to have_received(open3_method).with(environment, command)
226+
end
227+
228+
supports_block_expectations
229+
end
230+
elsif open3_method == :popen3
231+
RSpec::Matchers.define :have_invoked_command_with_env do |command, environment|
232+
match do |block|
233+
block.call
234+
235+
expect(Open3).to have_received(open3_method).with(command)
236+
expect(called_environment).to include(environment)
237+
end
238+
239+
supports_block_expectations
240+
end
241+
end
242+
216243
before do
217-
allow(Open3).to receive(open3_method).and_return(
244+
allow(Open3).to receive(open3_method) do
245+
@called_environment = ENV.to_hash.dup
218246
[double("Exit Status"), double("Stdout/err")]
219-
)
247+
end
248+
220249
allow(server).to receive(:capture_run_results) do |&block|
221250
block.call
222251
"the results"
223252
end
224253
end
225254

226255
it "runs the suite with the original CLI options" do
227-
runner.original_results
228-
expect(Open3).to have_received(open3_method).with(a_string_including("--seed 1234"))
256+
expect {
257+
runner.original_results
258+
}.to have_invoked_command_with_env(a_string_including("--seed 1234"), {})
259+
end
260+
261+
context 'when --bisect is present in SPEC_OPTS' do
262+
it "runs the suite with --bisect removed from the environment" do
263+
expect {
264+
with_env_vars 'SPEC_OPTS' => '--bisect --fail-fast' do
265+
runner.original_results
266+
end
267+
}.to have_invoked_command_with_env(
268+
a_string_including("--seed 1234"),
269+
{ 'SPEC_OPTS' => '--fail-fast' }
270+
)
271+
end
272+
end
273+
274+
context 'when --bisect=verbose is present in SPEC_OPTS' do
275+
it "runs the suite with --bisect removed from the environment" do
276+
expect {
277+
with_env_vars 'SPEC_OPTS' => '--bisect=verbose --fail-fast' do
278+
runner.original_results
279+
end
280+
}.to have_invoked_command_with_env(
281+
a_string_including("--seed 1234"),
282+
{ 'SPEC_OPTS' => '--fail-fast' }
283+
)
284+
end
229285
end
230286

231287
it 'returns the run results' do

0 commit comments

Comments
 (0)