Skip to content

Commit a4e22bf

Browse files
authored
Merge pull request rspec#2271 from urbanautomaton/support-bisect-in-spec-opts
Support bisect in spec opts
2 parents 315f0fa + 590ffac commit a4e22bf

File tree

12 files changed

+415
-133
lines changed

12 files changed

+415
-133
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ rvm:
2020
- 1.9.3
2121
- 2.0.0
2222
- 2.1
23-
- 2.2
23+
- 2.2.5
2424
- 2.3.1
2525
- ruby-head
2626
- ree

lib/rspec/core.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
option_parser
3232
configuration_options
3333
runner
34+
invocations
3435
example
3536
shared_example_group
3637
example_group

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/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/invocations.rb

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
module RSpec
2+
module Core
3+
# @private
4+
module Invocations
5+
# @private
6+
class InitializeProject
7+
def call(*_args)
8+
RSpec::Support.require_rspec_core "project_initializer"
9+
ProjectInitializer.new.run
10+
0
11+
end
12+
end
13+
14+
# @private
15+
class DRbWithFallback
16+
def call(options, err, out)
17+
require 'rspec/core/drb'
18+
begin
19+
return DRbRunner.new(options).run(err, out)
20+
rescue DRb::DRbConnError
21+
err.puts "No DRb server is running. Running in local process instead ..."
22+
end
23+
RSpec::Core::Runner.new(options).run(err, out)
24+
end
25+
end
26+
27+
# @private
28+
class Bisect
29+
def call(options, _err, _out)
30+
RSpec::Support.require_rspec_core "bisect/coordinator"
31+
32+
success = RSpec::Core::Bisect::Coordinator.bisect_with(
33+
options.args,
34+
RSpec.configuration,
35+
bisect_formatter_for(options.options[:bisect])
36+
)
37+
38+
success ? 0 : 1
39+
end
40+
41+
private
42+
43+
def bisect_formatter_for(argument)
44+
return Formatters::BisectDebugFormatter if argument == "verbose"
45+
Formatters::BisectProgressFormatter
46+
end
47+
end
48+
49+
# @private
50+
class PrintVersion
51+
def call(_options, _err, out)
52+
out.puts RSpec::Core::Version::STRING
53+
0
54+
end
55+
end
56+
57+
# @private
58+
PrintHelp = Struct.new(:parser, :invalid_options) do
59+
def call(_options, _err, out)
60+
# Removing the blank invalid options from the output.
61+
out.puts parser.to_s.gsub(/^\s+(#{invalid_options.join('|')})\s*$\n/, '')
62+
0
63+
end
64+
end
65+
end
66+
end
67+
end

lib/rspec/core/option_parser.rb

Lines changed: 10 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ def parse(source=nil)
3636
# rubocop:disable MethodLength
3737
# rubocop:disable Metrics/AbcSize
3838
# rubocop:disable CyclomaticComplexity
39+
# rubocop:disable PerceivedComplexity
3940
def parser(options)
4041
OptionParser.new do |parser|
4142
parser.banner = "Usage: rspec [options] [files or directories]\n\n"
@@ -68,7 +69,8 @@ def parser(options)
6869

6970
parser.on('--bisect[=verbose]', 'Repeatedly runs the suite in order to isolate the failures to the ',
7071
' smallest reproducible case.') do |argument|
71-
bisect_and_exit(argument)
72+
options[:bisect] = argument || true
73+
options[:runner] = RSpec::Core::Invocations::Bisect.new
7274
end
7375

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

99-
parser.on('-X', '--[no-]drb', 'Run examples via DRb.') do |o|
100-
options[:drb] = o
101+
parser.on('-X', '--[no-]drb', 'Run examples via DRb.') do |use_drb|
102+
options[:drb] = use_drb
103+
options[:runner] = RSpec::Core::Invocations::DRbWithFallback.new if use_drb
101104
end
102105

103106
parser.on('--drb-port PORT', 'Port to connect to the DRb server.') do |o|
104107
options[:drb_port] = o.to_i
105108
end
106109

107110
parser.on('--init', 'Initialize your project with RSpec.') do |_cmd|
108-
initialize_project_and_exit
111+
options[:runner] = RSpec::Core::Invocations::InitializeProject.new
109112
end
110113

111114
parser.separator("\n **** Output ****\n\n")
@@ -242,7 +245,7 @@ def parser(options)
242245
parser.separator("\n **** Utility ****\n\n")
243246

244247
parser.on('-v', '--version', 'Display the version.') do
245-
print_version_and_exit
248+
options[:runner] = RSpec::Core::Invocations::PrintVersion.new
246249
end
247250

248251
# These options would otherwise be confusing to users, so we forcibly
@@ -254,7 +257,7 @@ def parser(options)
254257
invalid_options = %w[-d --I]
255258

256259
parser.on_tail('-h', '--help', "You're looking at it.") do
257-
print_help_and_exit(parser, invalid_options)
260+
options[:runner] = RSpec::Core::Invocations::PrintHelp.new(parser, invalid_options)
258261
end
259262

260263
# This prevents usage of the invalid_options.
@@ -268,6 +271,7 @@ def parser(options)
268271
# rubocop:enable Metrics/AbcSize
269272
# rubocop:enable MethodLength
270273
# rubocop:enable CyclomaticComplexity
274+
# rubocop:enable PerceivedComplexity
271275

272276
def add_tag_filter(options, filter_type, tag_name, value=true)
273277
(options[filter_type] ||= {})[tag_name] = value
@@ -281,39 +285,5 @@ def configure_only_failures(options)
281285
options[:only_failures] = true
282286
add_tag_filter(options, :inclusion_filter, :last_run_status, 'failed')
283287
end
284-
285-
def initialize_project_and_exit
286-
RSpec::Support.require_rspec_core "project_initializer"
287-
ProjectInitializer.new.run
288-
exit
289-
end
290-
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-
308-
def print_version_and_exit
309-
puts RSpec::Core::Version::STRING
310-
exit
311-
end
312-
313-
def print_help_and_exit(parser, invalid_options)
314-
# Removing the blank invalid options from the output.
315-
puts parser.to_s.gsub(/^\s+(#{invalid_options.join('|')})\s*$\n/, '')
316-
exit
317-
end
318288
end
319289
end

lib/rspec/core/runner.rb

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -65,17 +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
68+
if options.options[:runner]
69+
options.options[:runner].call(options, err, out)
70+
else
71+
new(options).run(err, out)
7672
end
77-
78-
new(options).run(err, out)
7973
end
8074

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

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 :invoke_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 :invoke_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 invoke_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 invoke_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 invoke_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)