diff --git a/lib/rspec/core/bisect/server.rb b/lib/rspec/core/bisect/server.rb new file mode 100644 index 0000000000..a24771c677 --- /dev/null +++ b/lib/rspec/core/bisect/server.rb @@ -0,0 +1,46 @@ +require 'drb/drb' + +module RSpec + module Core + # @private + module Bisect + # @private + # A DRb server that receives run results from a separate RSpec process + # started by the bisect process. + class Server + def self.run + server = new + server.start + yield server + ensure + server.stop + end + + def capture_run_results(abort_after_example_id=nil) + self.abort_after_example_id = abort_after_example_id + yield + latest_run_results + end + + def start + # We pass `nil` as the first arg to allow it to pick a DRb port. + @drb = DRb.start_service(nil, self) + end + + def stop + @drb.stop_service + end + + def drb_port + @drb_port ||= Integer(@drb.uri[/\d+$/]) + end + + # Fetched via DRb by the BisectFormatter to determine when to abort. + attr_accessor :abort_after_example_id + + # Set via DRb by the BisectFormatter with the results of the run. + attr_accessor :latest_run_results + end + end + end +end diff --git a/lib/rspec/core/formatters.rb b/lib/rspec/core/formatters.rb index 9a6eb160d3..b27e74f3a7 100644 --- a/lib/rspec/core/formatters.rb +++ b/lib/rspec/core/formatters.rb @@ -71,6 +71,7 @@ module RSpec::Core::Formatters autoload :ProgressFormatter, 'rspec/core/formatters/progress_formatter' autoload :ProfileFormatter, 'rspec/core/formatters/profile_formatter' autoload :JsonFormatter, 'rspec/core/formatters/json_formatter' + autoload :BisectFormatter, 'rspec/core/formatters/bisect_formatter' # Register the formatter class # @param formatter_class [Class] formatter class to register @@ -186,6 +187,8 @@ def built_in_formatter(key) ProgressFormatter when 'j', 'json' JsonFormatter + when 'bisect' + BisectFormatter end end diff --git a/lib/rspec/core/formatters/bisect_formatter.rb b/lib/rspec/core/formatters/bisect_formatter.rb new file mode 100644 index 0000000000..71a7e922ed --- /dev/null +++ b/lib/rspec/core/formatters/bisect_formatter.rb @@ -0,0 +1,66 @@ +require 'drb/drb' + +module RSpec + module Core + module Formatters + # Used by `--bisect`. When it shells out and runs a portion of the suite, it uses + # this formatter as a means to have the status reported back to it, via DRb. + # + # Note that since DRb calls carry considerable overhead compared to normal + # method calls, we try to minimize the number of DRb calls for perf reasons, + # opting to communicate only at the start and the end of the run, rather than + # after each example. + # @private + class BisectFormatter + Formatters.register self, :start, :start_dump, :example_started, + :example_failed, :example_passed, :example_pending + + def initialize(_output) + port = RSpec.configuration.drb_port + drb_uri = "druby://127.0.0.1:#{port}" + @all_example_ids = [] + @failed_example_ids = [] + @bisect_server = DRbObject.new_with_uri(drb_uri) + @abort_after_id = nil + end + + def start(_notification) + @abort_after_id = @bisect_server.abort_after_example_id + end + + def example_started(notification) + @all_example_ids << notification.example.id + end + + def example_failed(notification) + @failed_example_ids << notification.example.id + example_finished(notification) + end + + def example_passed(notification) + example_finished(notification) + end + + def example_pending(notification) + example_finished(notification) + end + + def start_dump(_notification) + @bisect_server.latest_run_results = RunResults.new( + @all_example_ids, @failed_example_ids + ) + end + + RunResults = Struct.new(:all_example_ids_in_execution_order, + :failed_example_ids) + + private + + def example_finished(notification) + return unless notification.example.id == @abort_after_id + RSpec.world.wants_to_quit = true + end + end + end + end +end diff --git a/spec/rspec/core/bisect/server_spec.rb b/spec/rspec/core/bisect/server_spec.rb new file mode 100644 index 0000000000..fc94d81585 --- /dev/null +++ b/spec/rspec/core/bisect/server_spec.rb @@ -0,0 +1,90 @@ +require 'rspec/core/bisect/server' +require 'support/formatter_support' + +module RSpec::Core + RSpec.describe Bisect::Server do + RSpec::Matchers.define :have_running_server do + match do |drb| + begin + drb.current_server.alive? + rescue DRb::DRbServerNotFound + false + end + end + end + + it 'always stops the server, even if an error occurs while yielding' do + expect(DRb).not_to have_running_server + + expect { + Bisect::Server.run do + expect(DRb).to have_running_server + raise "boom" + end + }.to raise_error("boom") + + expect(DRb).not_to have_running_server + end + + context "when used in combination with the BisectFormatter", :slow do + include FormatterSupport + + attr_reader :server + + around do |ex| + Bisect::Server.run do |the_server| + @server = the_server + ex.run + end + end + + def run_formatter_specs + RSpec.configuration.drb_port = server.drb_port + run_example_specs_with_formatter("bisect") + end + + it 'receives suite results' do + results = server.capture_run_results do + run_formatter_specs + end + + expect(results).to have_attributes( + :all_example_ids_in_execution_order => %w[ + ./spec/rspec/core/resources/formatter_specs.rb[1:1] + ./spec/rspec/core/resources/formatter_specs.rb[2:1:1] + ./spec/rspec/core/resources/formatter_specs.rb[2:2:1] + ./spec/rspec/core/resources/formatter_specs.rb[3:1] + ./spec/rspec/core/resources/formatter_specs.rb[4:1] + ./spec/rspec/core/resources/formatter_specs.rb[5:1] + ./spec/rspec/core/resources/formatter_specs.rb[5:2] + ], + :failed_example_ids => %w[ + ./spec/rspec/core/resources/formatter_specs.rb[2:2:1] + ./spec/rspec/core/resources/formatter_specs.rb[4:1] + ./spec/rspec/core/resources/formatter_specs.rb[5:1] + ./spec/rspec/core/resources/formatter_specs.rb[5:2] + ] + ) + end + + it 'can abort the run early (e.g. when it is not interested in later examples)' do + results = server.capture_run_results("./spec/rspec/core/resources/formatter_specs.rb[2:2:1]") do + run_formatter_specs + end + + expect(results).to have_attributes( + :all_example_ids_in_execution_order => %w[ + ./spec/rspec/core/resources/formatter_specs.rb[1:1] + ./spec/rspec/core/resources/formatter_specs.rb[2:1:1] + ./spec/rspec/core/resources/formatter_specs.rb[2:2:1] + ], + :failed_example_ids => %w[ + ./spec/rspec/core/resources/formatter_specs.rb[2:2:1] + ] + ) + end + + # TODO: test aborting after pending vs failed vs passing example if we keep this feature. + end + end +end