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

Commit 8492f12

Browse files
authored
Merge pull request #2503 from rspec/myron/bisect-forker-prep-refactorings
Bisect forker prep refactorings
2 parents 88bd0c9 + 1488497 commit 8492f12

19 files changed

+431
-293
lines changed

lib/rspec/core/bisect/coordinator.rb

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
RSpec::Support.require_rspec_core "bisect/server"
2-
RSpec::Support.require_rspec_core "bisect/runner"
1+
RSpec::Support.require_rspec_core "bisect/shell_command"
2+
RSpec::Support.require_rspec_core "bisect/shell_runner"
33
RSpec::Support.require_rspec_core "bisect/example_minimizer"
4+
RSpec::Support.require_rspec_core "bisect/utilities"
45
RSpec::Support.require_rspec_core "formatters/bisect_progress_formatter"
56

67
module RSpec
78
module Core
89
module Bisect
910
# @private
1011
# The main entry point into the bisect logic. Coordinates among:
11-
# - Bisect::Server: Receives suite results.
12-
# - Bisect::Runner: Runs a set of examples and directs the results
13-
# to the server.
12+
# - Bisect::ShellCommand: Generates shell commands to run spec subsets
13+
# - Bisect::ShellRunner: Runs a set of examples and returns the results.
1414
# - Bisect::ExampleMinimizer: Contains the core bisect logic.
1515
# - Formatters::BisectProgressFormatter: provides progress updates
1616
# to the user.
@@ -20,18 +20,17 @@ def self.bisect_with(original_cli_args, configuration, formatter)
2020
end
2121

2222
def initialize(original_cli_args, configuration, formatter)
23-
@original_cli_args = original_cli_args
24-
@configuration = configuration
25-
@formatter = formatter
23+
@shell_command = ShellCommand.new(original_cli_args)
24+
@configuration = configuration
25+
@formatter = formatter
2626
end
2727

2828
def bisect
2929
@configuration.add_formatter @formatter
3030

3131
reporter.close_after do
32-
repro = Server.run do |server|
33-
runner = Runner.new(server, @original_cli_args)
34-
minimizer = ExampleMinimizer.new(runner, reporter)
32+
repro = ShellRunner.start(@shell_command) do |runner|
33+
minimizer = ExampleMinimizer.new(@shell_command, runner, reporter)
3534

3635
gracefully_abort_on_sigint(minimizer)
3736
minimizer.find_minimal_repro

lib/rspec/core/bisect/example_minimizer.rb

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
1+
RSpec::Support.require_rspec_core "bisect/utilities"
2+
13
module RSpec
24
module Core
35
module Bisect
46
# @private
57
# Contains the core bisect logic. Searches for examples we can ignore by
68
# repeatedly running different subsets of the suite.
79
class ExampleMinimizer
8-
attr_reader :runner, :reporter, :all_example_ids, :failed_example_ids
10+
attr_reader :shell_command, :runner, :reporter, :all_example_ids, :failed_example_ids
911
attr_accessor :remaining_ids
1012

11-
def initialize(runner, reporter)
12-
@runner = runner
13-
@reporter = reporter
13+
def initialize(shell_command, runner, reporter)
14+
@shell_command = shell_command
15+
@runner = runner
16+
@reporter = reporter
1417
end
1518

1619
def find_minimal_repro
@@ -82,7 +85,7 @@ def currently_needed_ids
8285
end
8386

8487
def repro_command_for_currently_needed_ids
85-
return runner.repro_command_from(currently_needed_ids) if remaining_ids
88+
return shell_command.repro_command_from(currently_needed_ids) if remaining_ids
8689
"(Not yet enough information to provide any repro command)"
8790
end
8891

@@ -108,7 +111,7 @@ def example_range(ids)
108111
end
109112

110113
def prep
111-
notify(:bisect_starting, :original_cli_args => runner.original_cli_args)
114+
notify(:bisect_starting, :original_cli_args => shell_command.original_cli_args)
112115

113116
_, duration = track_duration do
114117
original_results = runner.original_results
@@ -135,7 +138,7 @@ def get_expected_failures_for?(ids)
135138
ids_to_run = ids + failed_example_ids
136139
notify(
137140
:bisect_individual_run_start,
138-
:command => runner.repro_command_from(ids_to_run),
141+
:command => shell_command.repro_command_from(ids_to_run),
139142
:ids_to_run => ids_to_run
140143
)
141144

lib/rspec/core/bisect/server.rb

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
require 'drb/drb'
22
require 'drb/acl'
3+
RSpec::Support.require_rspec_core "bisect/utilities"
34

45
module RSpec
56
module Core
67
# @private
78
module Bisect
8-
# @private
9-
BisectFailedError = Class.new(StandardError)
10-
119
# @private
1210
# A DRb server that receives run results from a separate RSpec process
1311
# started by the bisect process.
@@ -27,7 +25,7 @@ def capture_run_results(files_or_directories_to_run=[], expected_failures=[])
2725
run_output = yield
2826

2927
if latest_run_results.nil? || latest_run_results.all_example_ids.empty?
30-
raise_bisect_failed(run_output)
28+
raise BisectFailedError.for_failed_spec_run(run_output)
3129
end
3230

3331
latest_run_results
@@ -49,21 +47,14 @@ def drb_port
4947
@drb_port ||= Integer(@drb.uri[/\d+$/])
5048
end
5149

52-
# Fetched via DRb by the BisectFormatter to determine when to abort.
50+
# Fetched via DRb by the BisectDRbFormatter to determine when to abort.
5351
attr_accessor :expected_failures
5452

55-
# Set via DRb by the BisectFormatter with the results of the run.
53+
# Set via DRb by the BisectDRbFormatter with the results of the run.
5654
attr_accessor :latest_run_results
5755

5856
# Fetched via DRb to tell clients which files to run
5957
attr_accessor :files_or_directories_to_run
60-
61-
private
62-
63-
def raise_bisect_failed(run_output)
64-
raise BisectFailedError, "Failed to get results from the spec " \
65-
"run. Spec run output:\n\n#{run_output}"
66-
end
6758
end
6859
end
6960
end

lib/rspec/core/bisect/runner.rb renamed to lib/rspec/core/bisect/shell_command.rb

Lines changed: 25 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,27 @@
11
RSpec::Support.require_rspec_core "shell_escape"
2-
require 'open3'
32
require 'shellwords'
43

54
module RSpec
65
module Core
76
module Bisect
8-
# Provides an API to run the suite for a set of locations, using
9-
# the given bisect server to capture the results.
7+
# Provides an API to generate shell commands to run the suite for a
8+
# set of locations, using the given bisect server to capture the results.
109
# @private
11-
class Runner
10+
class ShellCommand
1211
attr_reader :original_cli_args
1312

14-
def initialize(server, original_cli_args)
15-
@server = server
13+
def initialize(original_cli_args)
1614
@original_cli_args = original_cli_args.reject { |arg| arg.start_with?("--bisect") }
1715
end
1816

19-
def run(locations)
20-
run_locations(locations, original_results.failed_example_ids)
21-
end
22-
23-
def command_for(locations)
17+
def command_for(locations, server)
2418
parts = []
2519

2620
parts << RUBY << load_path
2721
parts << open3_safe_escape(RSpec::Core.path_to_executable)
2822

29-
parts << "--format" << "bisect"
30-
parts << "--drb-port" << @server.drb_port
23+
parts << "--format" << "bisect-drb"
24+
parts << "--drb-port" << server.drb_port
3125

3226
parts.concat(reusable_cli_options)
3327
parts.concat(locations.map { |l| open3_safe_escape(l) })
@@ -46,8 +40,24 @@ def repro_command_from(locations)
4640
parts.join(" ")
4741
end
4842

49-
def original_results
50-
@original_results ||= run_locations(original_locations)
43+
def original_locations
44+
parsed_original_cli_options.fetch(:files_or_directories_to_run)
45+
end
46+
47+
def bisect_environment_hash
48+
if ENV.key?('SPEC_OPTS')
49+
{ 'SPEC_OPTS' => spec_opts_without_bisect }
50+
else
51+
{}
52+
end
53+
end
54+
55+
def spec_opts_without_bisect
56+
Shellwords.join(
57+
Shellwords.split(ENV.fetch('SPEC_OPTS', '')).reject do |arg|
58+
arg =~ /^--bisect/
59+
end
60+
)
5161
end
5262

5363
private
@@ -63,61 +73,12 @@ def original_results
6373
alias open3_safe_escape escape
6474
end
6575

66-
def run_locations(*capture_args)
67-
@server.capture_run_results(*capture_args) do
68-
run_command command_for([])
69-
end
70-
end
71-
72-
# `Open3.capture2e` does not work on JRuby:
73-
# https://github.com/jruby/jruby/issues/2766
74-
if Open3.respond_to?(:capture2e) && !RSpec::Support::Ruby.jruby?
75-
def run_command(cmd)
76-
Open3.capture2e(bisect_environment_hash, cmd).first
77-
end
78-
else # for 1.8.7
79-
# :nocov:
80-
def run_command(cmd)
81-
out = err = nil
82-
83-
original_spec_opts = ENV['SPEC_OPTS']
84-
ENV['SPEC_OPTS'] = spec_opts_without_bisect
85-
86-
Open3.popen3(cmd) do |_, stdout, stderr|
87-
# Reading the streams blocks until the process is complete
88-
out = stdout.read
89-
err = stderr.read
90-
end
91-
92-
"Stdout:\n#{out}\n\nStderr:\n#{err}"
93-
ensure
94-
ENV['SPEC_OPTS'] = original_spec_opts
95-
end
96-
# :nocov:
97-
end
98-
99-
def bisect_environment_hash
100-
if ENV.key?('SPEC_OPTS')
101-
{ 'SPEC_OPTS' => spec_opts_without_bisect }
102-
else
103-
{}
104-
end
105-
end
106-
10776
def environment_repro_parts
10877
bisect_environment_hash.map do |k, v|
10978
%Q(#{k}="#{v}")
11079
end
11180
end
11281

113-
def spec_opts_without_bisect
114-
Shellwords.join(
115-
Shellwords.split(ENV.fetch('SPEC_OPTS', '')).reject do |arg|
116-
arg =~ /^--bisect/
117-
end
118-
)
119-
end
120-
12182
def reusable_cli_options
12283
@reusable_cli_options ||= begin
12384
opts = original_cli_args_without_locations
@@ -146,10 +107,6 @@ def parsed_original_cli_options
146107
@parsed_original_cli_options ||= Parser.parse(@original_cli_args)
147108
end
148109

149-
def original_locations
150-
parsed_original_cli_options.fetch(:files_or_directories_to_run)
151-
end
152-
153110
def load_path
154111
@load_path ||= "-I#{$LOAD_PATH.map { |p| open3_safe_escape(p) }.join(':')}"
155112
end

lib/rspec/core/bisect/shell_runner.rb

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
require 'open3'
2+
RSpec::Support.require_rspec_core "bisect/server"
3+
4+
module RSpec
5+
module Core
6+
module Bisect
7+
# Provides an API to run the suite for a set of locations, using
8+
# the given bisect server to capture the results.
9+
#
10+
# Sets of specs are run by shelling out.
11+
# @private
12+
class ShellRunner
13+
def self.start(shell_command)
14+
Server.run do |server|
15+
yield new(server, shell_command)
16+
end
17+
end
18+
19+
def initialize(server, shell_command)
20+
@server = server
21+
@shell_command = shell_command
22+
end
23+
24+
def run(locations)
25+
run_locations(locations, original_results.failed_example_ids)
26+
end
27+
28+
def original_results
29+
@original_results ||= run_locations(@shell_command.original_locations)
30+
end
31+
32+
private
33+
34+
def run_locations(*capture_args)
35+
@server.capture_run_results(*capture_args) do
36+
run_command @shell_command.command_for([], @server)
37+
end
38+
end
39+
40+
# `Open3.capture2e` does not work on JRuby:
41+
# https://github.com/jruby/jruby/issues/2766
42+
if Open3.respond_to?(:capture2e) && !RSpec::Support::Ruby.jruby?
43+
def run_command(cmd)
44+
Open3.capture2e(@shell_command.bisect_environment_hash, cmd).first
45+
end
46+
else # for 1.8.7
47+
# :nocov:
48+
def run_command(cmd)
49+
out = err = nil
50+
51+
original_spec_opts = ENV['SPEC_OPTS']
52+
ENV['SPEC_OPTS'] = @shell_command.spec_opts_without_bisect
53+
54+
Open3.popen3(cmd) do |_, stdout, stderr|
55+
# Reading the streams blocks until the process is complete
56+
out = stdout.read
57+
err = stderr.read
58+
end
59+
60+
"Stdout:\n#{out}\n\nStderr:\n#{err}"
61+
ensure
62+
ENV['SPEC_OPTS'] = original_spec_opts
63+
end
64+
# :nocov:
65+
end
66+
end
67+
end
68+
end
69+
end

lib/rspec/core/bisect/utilities.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
module RSpec
2+
module Core
3+
module Bisect
4+
# @private
5+
ExampleSetDescriptor = Struct.new(:all_example_ids, :failed_example_ids)
6+
7+
# @private
8+
class BisectFailedError < StandardError
9+
def self.for_failed_spec_run(spec_output)
10+
new("Failed to get results from the spec run. Spec run output:\n\n" +
11+
spec_output)
12+
end
13+
end
14+
end
15+
end
16+
end

lib/rspec/core/configuration.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -877,11 +877,11 @@ def full_description
877877
# @overload add_formatter(formatter)
878878
# @overload add_formatter(formatter, output)
879879
#
880-
# @param formatter [Class, String] formatter to use. Can be any of the
880+
# @param formatter [Class, String, Object] formatter to use. Can be any of the
881881
# string values supported from the CLI (`p`/`progress`,
882-
# `d`/`doc`/`documentation`, `h`/`html`, or `j`/`json`) or any
882+
# `d`/`doc`/`documentation`, `h`/`html`, or `j`/`json`), any
883883
# class that implements the formatter protocol and has registered
884-
# itself with RSpec as a formatter.
884+
# itself with RSpec as a formatter, or a formatter instance.
885885
# @param output [String, IO] where the formatter will write its output.
886886
# Can be an IO object or a string path to a file. If not provided,
887887
# the configured `output_stream` (`$stdout`, by default) will be used.

0 commit comments

Comments
 (0)