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

Commit 3dc041c

Browse files
committed
Allow different bisect runners to be configured and used.
1 parent c43a8cc commit 3dc041c

File tree

13 files changed

+141
-41
lines changed

13 files changed

+141
-41
lines changed

features/command_line/bisect.feature

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ Feature: Bisect
9494
When I run `rspec --seed 1234 --bisect=verbose`
9595
Then bisect should succeed with output like:
9696
"""
97-
Bisect started using options: "--seed 1234"
97+
Bisect started using options: "--seed 1234" and bisect runner: RSpec::Core::Bisect::ForkRunner
9898
Running suite to find failures... (0.16528 seconds)
9999
- Failing examples (1):
100100
- ./spec/calculator_1_spec.rb[1:1]
@@ -156,3 +156,23 @@ Feature: Bisect
156156
"""
157157
When I run `rspec ./spec/calculator_10_spec.rb[1:1] ./spec/calculator_1_spec.rb[1:1] --seed 1234`
158158
Then the output should contain "2 examples, 1 failure"
159+
160+
Scenario: Pick a bisect runner via a config option
161+
Given a file named "spec/spec_helper.rb" with:
162+
"""
163+
RSpec.configure do |c|
164+
c.bisect_runner = :shell
165+
end
166+
"""
167+
And a file named ".rspec" with:
168+
"""
169+
--require spec_helper
170+
"""
171+
When I run `rspec --seed 1234 --bisect=verbose`
172+
Then bisect should succeed with output like:
173+
"""
174+
Bisect started using options: "--seed 1234" and bisect runner: RSpec::Core::Bisect::ShellRunner
175+
# ...
176+
The minimal reproduction command is:
177+
rspec ./spec/calculator_10_spec.rb[1:1] ./spec/calculator_1_spec.rb[1:1] --seed 1234
178+
"""

features/step_definitions/additional_cli_steps.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,9 +180,14 @@
180180
"Output:\n\n#{last_process.stdout}"
181181

182182
expected = normalize_durations(expected_output)
183-
actual = normalize_durations(last_process.stdout)
183+
actual = normalize_durations(last_process.stdout).sub(/\n+\Z/, '')
184184

185-
expect(actual.sub(/\n+\Z/, '')).to eq(expected)
185+
if expected.include?("# ...")
186+
expected_start, expected_end = expected.split("# ...")
187+
expect(actual).to start_with(expected_start).and end_with(expected_end)
188+
else
189+
expect(actual).to eq(expected)
190+
end
186191
end
187192

188193
When(/^I run `([^`]+)` and abort in the middle with ctrl\-c$/) do |cmd|

lib/rspec/core/bisect/coordinator.rb

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,30 @@
11
RSpec::Support.require_rspec_core "bisect/shell_command"
2-
RSpec::Support.require_rspec_core "bisect/shell_runner"
32
RSpec::Support.require_rspec_core "bisect/example_minimizer"
43
RSpec::Support.require_rspec_core "bisect/utilities"
54
RSpec::Support.require_rspec_core "formatters/bisect_progress_formatter"
65

76
module RSpec
87
module Core
98
module Bisect
10-
# @private
119
# The main entry point into the bisect logic. Coordinates among:
1210
# - Bisect::ShellCommand: Generates shell commands to run spec subsets
13-
# - Bisect::ShellRunner: Runs a set of examples and returns the results.
1411
# - Bisect::ExampleMinimizer: Contains the core bisect logic.
15-
# - Formatters::BisectProgressFormatter: provides progress updates
16-
# to the user.
12+
# - A bisect runner: runs a set of examples and returns the results.
13+
# - A bisect formatter: provides progress updates to the user.
14+
# @private
1715
class Coordinator
18-
def self.bisect_with(original_cli_args, formatter)
19-
new(original_cli_args, formatter).bisect
16+
def self.bisect_with(spec_runner, original_cli_args, formatter)
17+
new(spec_runner, original_cli_args, formatter).bisect
2018
end
2119

22-
def initialize(original_cli_args, formatter)
20+
def initialize(spec_runner, original_cli_args, formatter)
21+
@spec_runner = spec_runner
2322
@shell_command = ShellCommand.new(original_cli_args)
2423
@notifier = Bisect::Notifier.new(formatter)
2524
end
2625

2726
def bisect
28-
repro = ShellRunner.start(@shell_command) do |runner|
27+
repro = start_bisect_runner do |runner|
2928
minimizer = ExampleMinimizer.new(@shell_command, runner, @notifier)
3029

3130
gracefully_abort_on_sigint(minimizer)
@@ -45,6 +44,11 @@ def bisect
4544

4645
private
4746

47+
def start_bisect_runner(&block)
48+
klass = @spec_runner.configuration.bisect_runner_class
49+
klass.start(@shell_command, @spec_runner, &block)
50+
end
51+
4852
def gracefully_abort_on_sigint(minimizer)
4953
trap('INT') do
5054
repro = minimizer.repro_command_for_currently_needed_ids

lib/rspec/core/bisect/example_minimizer.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,8 @@ def example_range(ids)
111111
end
112112

113113
def prep
114-
notify(:bisect_starting, :original_cli_args => shell_command.original_cli_args)
114+
notify(:bisect_starting, :original_cli_args => shell_command.original_cli_args,
115+
:bisect_runner => runner.class)
115116

116117
_, duration = track_duration do
117118
original_results = runner.original_results

lib/rspec/core/bisect/shell_runner.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ module Bisect
1010
# Sets of specs are run by shelling out.
1111
# @private
1212
class ShellRunner
13-
def self.start(shell_command, _spec_runner=nil)
13+
def self.start(shell_command, _spec_runner)
1414
Server.run do |server|
1515
yield new(server, shell_command)
1616
end

lib/rspec/core/configuration.rb

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,28 @@ def shared_context_metadata_behavior=(value)
413413
# return [Integer]
414414
add_setting :max_displayed_failure_line_count
415415

416+
# @macro add_setting
417+
# Determines which bisect runner implementation gets used to run subsets
418+
# of the suite during a bisection. Your choices are:
419+
#
420+
# - `:shell`: Performs a spec run by shelling out, booting RSpec and your
421+
# application environment each time. This runner is the most widely
422+
# compatible runner, but is not as fast. On platforms that do not
423+
# support forking, this is the default.
424+
# - `:fork`: Pre-boots RSpec and your application environment in a parent
425+
# process, and then forks a child process for each spec run. This runner
426+
# tends to be significantly faster than the `:shell` runner but cannot
427+
# be used in some situations. On platforms that support forking, this
428+
# is the default. If you use this runner, you should ensure that all
429+
# of your one-time setup logic goes in a `before(:suite)` hook instead
430+
# of getting run at the top-level of a file loaded by `--require`.
431+
#
432+
# @note This option will only be used by `--bisect` if you set it in a file
433+
# loaded via `--require`.
434+
#
435+
# @return [Symbol]
436+
add_setting :bisect_runner
437+
416438
# @private
417439
# @deprecated Use {#color_mode} = :on, instead of {#color} with {#tty}
418440
add_setting :tty
@@ -437,6 +459,8 @@ def initialize
437459
@extend_modules = FilterableItemRepository::QueryOptimized.new(:any?)
438460
@prepend_modules = FilterableItemRepository::QueryOptimized.new(:any?)
439461

462+
@bisect_runner = Process.respond_to?(:fork) ? :fork : :shell
463+
440464
@before_suite_hooks = []
441465
@after_suite_hooks = []
442466

@@ -1962,6 +1986,21 @@ def on_example_group_definition_callbacks
19621986
@on_example_group_definition_callbacks ||= []
19631987
end
19641988

1989+
# @private
1990+
def bisect_runner_class
1991+
case bisect_runner
1992+
when :fork
1993+
RSpec::Support.require_rspec_core 'bisect/fork_runner'
1994+
Bisect::ForkRunner
1995+
when :shell
1996+
RSpec::Support.require_rspec_core 'bisect/shell_runner'
1997+
Bisect::ShellRunner
1998+
else
1999+
raise "Unsupported value for `bisect_runner` (#{bisect_runner.inspect}). " \
2000+
"Only `:fork` and `:shell` are supported."
2001+
end
2002+
end
2003+
19652004
private
19662005

19672006
def load_file_handling_errors(method, file)

lib/rspec/core/formatters/bisect_progress_formatter.rb

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ module Formatters
88
class BisectProgressFormatter < BaseTextFormatter
99
def bisect_starting(notification)
1010
@round_count = 0
11-
options = notification.original_cli_args.join(' ')
12-
output.puts "Bisect started using options: #{options.inspect}"
11+
output.puts bisect_started_message(notification)
1312
output.print "Running suite to find failures..."
1413
end
1514

@@ -76,6 +75,13 @@ def bisect_aborted(notification)
7675
output.puts "\n\nBisect aborted!"
7776
output.puts "\nThe most minimal reproduction command discovered so far is:\n #{notification.repro}"
7877
end
78+
79+
private
80+
81+
def bisect_started_message(notification)
82+
options = notification.original_cli_args.join(' ')
83+
"Bisect started using options: #{options.inspect}"
84+
end
7985
end
8086

8187
# @private
@@ -126,6 +132,10 @@ def describe_ids(description, ids)
126132
formatted_ids = organized_ids.map { |id| " - #{id}" }.join("\n")
127133
"#{description} (#{ids.size}):\n#{formatted_ids}"
128134
end
135+
136+
def bisect_started_message(notification)
137+
"#{super} and bisect runner: #{notification.bisect_runner}"
138+
end
129139
end
130140
end
131141
end

lib/rspec/core/invocations.rb

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,27 +26,28 @@ def call(options, err, out)
2626

2727
# @private
2828
class Bisect
29-
def call(options, _err, _out)
29+
def call(options, err, out)
3030
RSpec::Support.require_rspec_core "bisect/coordinator"
3131

3232
success = RSpec::Core::Bisect::Coordinator.bisect_with(
33+
Runner.new(options).tap { |r| r.configure(err, out) },
3334
options.args,
34-
bisect_formatter_for(options.options[:bisect])
35+
bisect_formatter_for(options.options[:bisect], out)
3536
)
3637

3738
success ? 0 : 1
3839
end
3940

40-
private
41+
private
4142

42-
def bisect_formatter_for(argument)
43+
def bisect_formatter_for(argument, output)
4344
klass = if argument == "verbose"
4445
Formatters::BisectDebugFormatter
4546
else
4647
Formatters::BisectProgressFormatter
4748
end
4849

49-
klass.new(RSpec.configuration.output_stream)
50+
klass.new(output)
5051
end
5152
end
5253

spec/integration/bisect_spec.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ module RSpec::Core
99
end if ENV['APPVEYOR'] && RUBY_VERSION.to_f > 2.0
1010

1111
def bisect(cli_args, expected_status=nil)
12-
RSpec.configuration.output_stream = formatter_output
12+
options = ConfigurationOptions.new(cli_args)
1313

1414
expect {
15-
status = RSpec::Core::Runner.run(cli_args + ["--bisect"])
15+
status = Invocations::Bisect.new.call(options, formatter_output, formatter_output)
1616
expect(status).to eq(expected_status) if expected_status
1717
}.to avoid_outputting.to_stdout_from_any_process.and avoid_outputting.to_stderr_from_any_process
1818

spec/rspec/core/bisect/coordinator_spec.rb

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ module RSpec::Core
66
RSpec.describe Bisect::Coordinator, :simulate_shell_allowing_unquoted_ids do
77
include FormatterSupport
88

9-
let(:fake_runner) do
9+
let(:config) { instance_double(Configuration, :bisect_runner_class => fake_bisect_runner) }
10+
let(:spec_runner) { instance_double(RSpec::Core::Runner, :configuration => config) }
11+
let(:fake_bisect_runner) do
1012
FakeBisectRunner.new(
1113
1.upto(8).map { |i| "#{i}.rb[1:1]" },
1214
%w[ 2.rb[1:1] ],
@@ -15,12 +17,7 @@ module RSpec::Core
1517
end
1618

1719
def find_minimal_repro(output, formatter=Formatters::BisectProgressFormatter)
18-
allow(Bisect::Server).to receive(:run).and_yield(instance_double(Bisect::Server))
19-
allow(Bisect::ShellRunner).to receive(:new).and_return(fake_runner)
20-
21-
Bisect::Coordinator.bisect_with([], formatter.new(output))
22-
ensure
23-
RSpec.reset # so that RSpec.configuration.output_stream isn't closed
20+
Bisect::Coordinator.bisect_with(spec_runner, [], formatter.new(output))
2421
end
2522

2623
it 'notifies the bisect progress formatter of progress and closes the output' do
@@ -52,7 +49,7 @@ def find_minimal_repro(output, formatter=Formatters::BisectProgressFormatter)
5249
output = normalize_durations(output.string)
5350

5451
expect(output).to eq(<<-EOS.gsub(/^\s+\|/, ''))
55-
|Bisect started using options: ""
52+
|Bisect started using options: "" and bisect runner: FakeBisectRunner
5653
|Running suite to find failures... (n.nnnn seconds)
5754
| - Failing examples (2):
5855
| - 2.rb[1:1]
@@ -100,7 +97,7 @@ def find_minimal_repro(output, formatter=Formatters::BisectProgressFormatter)
10097

10198
context "with an order-independent failure" do
10299
it "detects the independent case and prints the minimal reproduction" do
103-
fake_runner.dependent_failures = {}
100+
fake_bisect_runner.dependent_failures = {}
104101
output = StringIO.new
105102
find_minimal_repro(output)
106103
output = normalize_durations(output.string)
@@ -120,13 +117,13 @@ def find_minimal_repro(output, formatter=Formatters::BisectProgressFormatter)
120117
end
121118

122119
it "can use the debug formatter for detailed output" do
123-
fake_runner.dependent_failures = {}
120+
fake_bisect_runner.dependent_failures = {}
124121
output = StringIO.new
125122
find_minimal_repro(output, Formatters::BisectDebugFormatter)
126123
output = normalize_durations(output.string)
127124

128125
expect(output).to eq(<<-EOS.gsub(/^\s+\|/, ''))
129-
|Bisect started using options: ""
126+
|Bisect started using options: "" and bisect runner: FakeBisectRunner
130127
|Running suite to find failures... (n.nnnn seconds)
131128
| - Failing examples (1):
132129
| - 2.rb[1:1]

spec/rspec/core/configuration_spec.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1443,6 +1443,25 @@ def metadata_hash(*args)
14431443
end
14441444
end
14451445

1446+
describe "#bisect_runner_class" do
1447+
it "returns `Bisect::ForkRunner` when `bisect_runner == :fork" do
1448+
config.bisect_runner = :fork
1449+
expect(config.bisect_runner_class).to be Bisect::ForkRunner
1450+
end
1451+
1452+
it "returns `Bisect::ShellRunner` when `bisect_runner == :shell" do
1453+
config.bisect_runner = :shell
1454+
expect(config.bisect_runner_class).to be Bisect::ShellRunner
1455+
end
1456+
1457+
it "raises a clear error when `bisect_runner` is configured to an unrecognized value" do
1458+
config.bisect_runner = :unknown
1459+
expect {
1460+
config.bisect_runner_class
1461+
}.to raise_error(/Unsupported value for `bisect_runner`/)
1462+
end
1463+
end
1464+
14461465
%w[formatter= add_formatter].each do |config_method|
14471466
describe "##{config_method}" do
14481467
it "delegates to formatters#add" do

spec/rspec/core/invocations_spec.rb

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,21 +77,20 @@ def run_invocation
7777
end
7878

7979
describe Invocations::Bisect do
80-
let(:bisect) { nil }
81-
let(:options) { { :bisect => bisect } }
82-
let(:args) { double(:args) }
80+
let(:original_cli_args) { %w[--bisect --seed 1234] }
81+
let(:configuration_options) { ConfigurationOptions.new(original_cli_args) }
8382
let(:success) { true }
8483

8584
before do
86-
allow(configuration_options).to receive_messages(:args => args, :options => options)
8785
allow(RSpec::Core::Bisect::Coordinator).to receive(:bisect_with).and_return(success)
8886
end
8987

9088
it "starts the bisection coordinator" do
9189
run_invocation
9290

9391
expect(RSpec::Core::Bisect::Coordinator).to have_received(:bisect_with).with(
94-
args,
92+
an_instance_of(Runner),
93+
configuration_options.args,
9594
an_instance_of(Formatters::BisectProgressFormatter)
9695
)
9796
end
@@ -115,13 +114,14 @@ def run_invocation
115114
end
116115

117116
context "and the verbose option is specified" do
118-
let(:bisect) { "verbose" }
117+
let(:original_cli_args) { %w[--bisect=verbose --seed 1234] }
119118

120119
it "starts the bisection coordinator with the debug formatter" do
121120
run_invocation
122121

123122
expect(RSpec::Core::Bisect::Coordinator).to have_received(:bisect_with).with(
124-
args,
123+
an_instance_of(Runner),
124+
configuration_options.args,
125125
an_instance_of(Formatters::BisectDebugFormatter)
126126
)
127127
end

0 commit comments

Comments
 (0)