From 67f933c444d4b3b17bc9bed85de280ef4bc40983 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Tue, 3 Feb 2015 08:08:32 -0800 Subject: [PATCH 001/258] Bump version to 3.3.0.pre --- lib/rspec/core/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rspec/core/version.rb b/lib/rspec/core/version.rb index 5fa13d465d..9d70567630 100644 --- a/lib/rspec/core/version.rb +++ b/lib/rspec/core/version.rb @@ -3,7 +3,7 @@ module Core # Version information for RSpec Core. module Version # Current version of RSpec Core, in semantic versioning format. - STRING = '3.2.0' + STRING = '3.3.0.pre' end end end From c6fef1afee5e2a4667c7da3c3c28827b70136f85 Mon Sep 17 00:00:00 2001 From: Raymond Sanchez <'raysanchez1979@gmail.com'> Date: Tue, 3 Feb 2015 17:07:44 -0600 Subject: [PATCH 002/258] Skip Core.path_to_executable for Windows OS only --- spec/rspec/core_spec.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/spec/rspec/core_spec.rb b/spec/rspec/core_spec.rb index a367e92bc8..742785e400 100644 --- a/spec/rspec/core_spec.rb +++ b/spec/rspec/core_spec.rb @@ -178,9 +178,13 @@ ).to eq(:slow => true) end end - + + # File.executable? always returns false in Windows + # In *nix operating systems, the permissions are checked against the executable bit, + # but in windows os we don't have the same permissions + # This project had the same issue https://projects.puppetlabs.com/issues/20302 describe "::Core.path_to_executable" do - it 'returns the absolute location of the exe/rspec file', :failing_on_appveyor do + it 'returns the absolute location of the exe/rspec file', :if => !RSpec::Support::OS.windows? do expect(File.exist? RSpec::Core.path_to_executable).to be_truthy expect(File.executable? RSpec::Core.path_to_executable).to be_truthy end From da20dd37f708bb487293192c6adb36070b2c4e5c Mon Sep 17 00:00:00 2001 From: raymond sanchez Date: Tue, 3 Feb 2015 22:43:47 -0600 Subject: [PATCH 003/258] Update spec for Core.path_to_executable This is to handle issue with File.executable? on Windows. --- spec/rspec/core_spec.rb | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/spec/rspec/core_spec.rb b/spec/rspec/core_spec.rb index 742785e400..915cb51de4 100644 --- a/spec/rspec/core_spec.rb +++ b/spec/rspec/core_spec.rb @@ -178,15 +178,12 @@ ).to eq(:slow => true) end end - - # File.executable? always returns false in Windows - # In *nix operating systems, the permissions are checked against the executable bit, - # but in windows os we don't have the same permissions - # This project had the same issue https://projects.puppetlabs.com/issues/20302 + describe "::Core.path_to_executable" do - it 'returns the absolute location of the exe/rspec file', :if => !RSpec::Support::OS.windows? do + it 'returns the absolute location of the exe/rspec file' do expect(File.exist? RSpec::Core.path_to_executable).to be_truthy - expect(File.executable? RSpec::Core.path_to_executable).to be_truthy + expect(File.read(RSpec::Core.path_to_executable)).to include("RSpec::Core::Runner.invoke") + expect(File.executable? RSpec::Core.path_to_executable).to be_truthy unless RSpec::Support::OS.windows? end end From a640e98a9ee84dfda1ef931af75b12da6463834d Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Tue, 3 Feb 2015 22:58:37 -0800 Subject: [PATCH 004/258] Remove unnecessary temporary mocks scope. --- spec/support/formatter_support.rb | 15 +++++---------- spec/support/sandboxing.rb | 9 +++------ 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/spec/support/formatter_support.rb b/spec/support/formatter_support.rb index 0d146ce0b9..875bd98988 100644 --- a/spec/support/formatter_support.rb +++ b/spec/support/formatter_support.rb @@ -48,9 +48,8 @@ def expected_summary_output_for_example_specs | (compared using ==) | # ./spec/rspec/core/resources/formatter_specs.rb:11 | # ./spec/support/formatter_support.rb:13:in `run_example_specs_with_formatter' - | # ./spec/support/sandboxing.rb:16 | # ./spec/support/sandboxing.rb:14 - | # ./spec/support/sandboxing.rb:8 + | # ./spec/support/sandboxing.rb:7 | |Failures: | @@ -67,9 +66,8 @@ def expected_summary_output_for_example_specs | (compared using ==) | # ./spec/rspec/core/resources/formatter_specs.rb:31 | # ./spec/support/formatter_support.rb:13:in `run_example_specs_with_formatter' - | # ./spec/support/sandboxing.rb:16 | # ./spec/support/sandboxing.rb:14 - | # ./spec/support/sandboxing.rb:8 + | # ./spec/support/sandboxing.rb:7 | | 3) a failing spec with odd backtraces fails with a backtrace that has no file | Failure/Error: Unable to find matching line from backtrace @@ -113,9 +111,8 @@ def expected_summary_output_for_example_specs | (compared using ==) | # ./spec/rspec/core/resources/formatter_specs.rb:11:in `block (3 levels) in ' | # ./spec/support/formatter_support.rb:13:in `run_example_specs_with_formatter' - | # ./spec/support/sandboxing.rb:16:in `block (4 levels) in ' | # ./spec/support/sandboxing.rb:14:in `block (3 levels) in ' - | # ./spec/support/sandboxing.rb:8:in `block (2 levels) in ' + | # ./spec/support/sandboxing.rb:7:in `block (2 levels) in ' | |Failures: | @@ -132,9 +129,8 @@ def expected_summary_output_for_example_specs | (compared using ==) | # ./spec/rspec/core/resources/formatter_specs.rb:31:in `block (2 levels) in ' | # ./spec/support/formatter_support.rb:13:in `run_example_specs_with_formatter' - | # ./spec/support/sandboxing.rb:16:in `block (4 levels) in ' | # ./spec/support/sandboxing.rb:14:in `block (3 levels) in ' - | # ./spec/support/sandboxing.rb:8:in `block (2 levels) in ' + | # ./spec/support/sandboxing.rb:7:in `block (2 levels) in ' | | 3) a failing spec with odd backtraces fails with a backtrace that has no file | Failure/Error: ERB.new("<%= raise 'foo' %>").result @@ -143,9 +139,8 @@ def expected_summary_output_for_example_specs | # (erb):1:in `
' | # ./spec/rspec/core/resources/formatter_specs.rb:39:in `block (2 levels) in ' | # ./spec/support/formatter_support.rb:13:in `run_example_specs_with_formatter' - | # ./spec/support/sandboxing.rb:16:in `block (4 levels) in ' | # ./spec/support/sandboxing.rb:14:in `block (3 levels) in ' - | # ./spec/support/sandboxing.rb:8:in `block (2 levels) in ' + | # ./spec/support/sandboxing.rb:7:in `block (2 levels) in ' | | 4) a failing spec with odd backtraces fails with a backtrace containing an erb file | Failure/Error: Unable to find matching line from backtrace diff --git a/spec/support/sandboxing.rb b/spec/support/sandboxing.rb index dc04bb0c97..90af13e71b 100644 --- a/spec/support/sandboxing.rb +++ b/spec/support/sandboxing.rb @@ -1,5 +1,4 @@ require 'rspec/core/sandbox' -require 'rspec/mocks' # Because testing RSpec with RSpec tries to modify the same global # objects, we sandbox every test. @@ -11,11 +10,9 @@ # something like `pending` config.before(:context) { RSpec.current_example = nil } - RSpec::Mocks.with_temporary_scope do - orig_load_path = $LOAD_PATH.dup - ex.run - $LOAD_PATH.replace(orig_load_path) - end + orig_load_path = $LOAD_PATH.dup + ex.run + $LOAD_PATH.replace(orig_load_path) end end end From 8fa14ae799da64885e630cdbf0d798788c4f3e0e Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Tue, 3 Feb 2015 23:12:58 -0800 Subject: [PATCH 005/258] Cleanup whitespace. --- spec/rspec/core_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/rspec/core_spec.rb b/spec/rspec/core_spec.rb index 915cb51de4..6073b420f8 100644 --- a/spec/rspec/core_spec.rb +++ b/spec/rspec/core_spec.rb @@ -178,12 +178,12 @@ ).to eq(:slow => true) end end - + describe "::Core.path_to_executable" do it 'returns the absolute location of the exe/rspec file' do expect(File.exist? RSpec::Core.path_to_executable).to be_truthy - expect(File.read(RSpec::Core.path_to_executable)).to include("RSpec::Core::Runner.invoke") - expect(File.executable? RSpec::Core.path_to_executable).to be_truthy unless RSpec::Support::OS.windows? + expect(File.read(RSpec::Core.path_to_executable)).to include("RSpec::Core::Runner.invoke") + expect(File.executable? RSpec::Core.path_to_executable).to be_truthy unless RSpec::Support::OS.windows? end end From dcbc98e5d5f7c29d594b6b37578af30b2cf0e1e6 Mon Sep 17 00:00:00 2001 From: Raymond Sanchez <'raysanchez1979@gmail.com'> Date: Wed, 4 Feb 2015 17:08:27 -0600 Subject: [PATCH 006/258] Update spec for format_backtrace In Windows, we can use '/' for file separator. Let's keep one spec for both Windows and non-windows systems. --- spec/rspec/core/backtrace_formatter_spec.rb | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/spec/rspec/core/backtrace_formatter_spec.rb b/spec/rspec/core/backtrace_formatter_spec.rb index 133f121719..e63460601c 100644 --- a/spec/rspec/core/backtrace_formatter_spec.rb +++ b/spec/rspec/core/backtrace_formatter_spec.rb @@ -101,7 +101,7 @@ def make_backtrace_formatter(exclusion_patterns=nil, inclusion_patterns=nil) end describe "#format_backtrace" do - it "excludes lines from rspec libs by default", :unless => RSpec::Support::OS.windows? do + it "excludes lines from rspec libs by default" do backtrace = [ "/path/to/rspec-expectations/lib/rspec/expectations/foo.rb:37", "/path/to/rspec-expectations/lib/rspec/matchers/foo.rb:37", @@ -112,19 +112,7 @@ def make_backtrace_formatter(exclusion_patterns=nil, inclusion_patterns=nil) expect(BacktraceFormatter.new.format_backtrace(backtrace)).to eq(["./my_spec.rb:5"]) end - - it "excludes lines from rspec libs by default", :failing_on_appveyor, :if => RSpec::Support::OS.windows? do - backtrace = [ - "\\path\\to\\rspec-expectations\\lib\\rspec\\expectations\\foo.rb:37", - "\\path\\to\\rspec-expectations\\lib\\rspec\\matchers\\foo.rb:37", - ".\\my_spec.rb:5", - "\\path\\to\\rspec-mocks\\lib\\rspec\\mocks\\foo.rb:37", - "\\path\\to\\rspec-core\\lib\\rspec\\core\\foo.rb:37" - ] - - expect(BacktraceFormatter.new.format_backtrace(backtrace)).to eq([".\\my_spec.rb:5"]) - end - + context "when every line is filtered out" do let(:backtrace) do [ From 038a183df0827ad2d7dafc4dcbd10b0778345a65 Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Thu, 5 Feb 2015 10:59:27 +1100 Subject: [PATCH 007/258] expose the reporter from a running example --- lib/rspec/core/example.rb | 5 +++++ spec/rspec/core/example_spec.rb | 15 +++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/lib/rspec/core/example.rb b/lib/rspec/core/example.rb index 8d02d6ef72..4f71e93a34 100644 --- a/lib/rspec/core/example.rb +++ b/lib/rspec/core/example.rb @@ -143,8 +143,12 @@ def initialize(example_group_class, description, user_metadata, example_block=ni @example_group_instance = @exception = nil @clock = RSpec::Core::Time + @reporter = RSpec::Core::NullReporter.new end + # @return [RSpec::Core::Reporter] the current reporter for the example + attr_reader :reporter + # Returns the example group class that provides the context for running # this example. def example_group @@ -160,6 +164,7 @@ def example_group # @param example_group_instance the instance of an ExampleGroup subclass def run(example_group_instance, reporter) @example_group_instance = example_group_instance + @reporter = reporter hooks.register_global_singleton_context_hooks(self, RSpec.configuration.hooks) RSpec.configuration.configure_example(self) RSpec.current_example = self diff --git a/spec/rspec/core/example_spec.rb b/spec/rspec/core/example_spec.rb index 7fa62cba8a..2d63a2f5b9 100644 --- a/spec/rspec/core/example_spec.rb +++ b/spec/rspec/core/example_spec.rb @@ -732,4 +732,19 @@ def expect_pending_result(example) expect(ex).to pass end end + + describe "exposing the examples reporter" do + it "returns a null reporter when the example hasnt run yet" do + example = RSpec.describe.example + expect(example.reporter).to be_a RSpec::Core::NullReporter + end + + it "returns the reporter used to run the example when executed" do + reporter = double(:reporter).as_null_object + group = RSpec.describe + example = group.example + example.run group.new, reporter + expect(example.reporter).to be reporter + end + end end From 44b36c7a64030422626e7f6135aaf5fa5288446b Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Thu, 5 Feb 2015 11:05:18 +1100 Subject: [PATCH 008/258] make Reporter#message a public api --- lib/rspec/core/reporter.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/rspec/core/reporter.rb b/lib/rspec/core/reporter.rb index 0405889355..1e5fc3697c 100644 --- a/lib/rspec/core/reporter.rb +++ b/lib/rspec/core/reporter.rb @@ -40,7 +40,6 @@ def registered_listeners(notification) @listeners[notification].to_a end - # @api # @overload report(count, &block) # @overload report(count, &block) # @param expected_example_count [Integer] the number of examples being run @@ -73,7 +72,9 @@ def start(expected_example_count, time=RSpec::Core::Time.now) notify :seed, Notifications::SeedNotification.new(@configuration.seed, seed_used?) end - # @private + # @param message [#to_s] A message object to send to formatters + # + # Send a custom message to supporting formatters. def message(message) notify :message, Notifications::MessageNotification.new(message) end From 41e8447e824f867fa64b4b8e8681b250904d7a82 Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Thu, 5 Feb 2015 11:08:39 +1100 Subject: [PATCH 009/258] changelog for #1866 [skip ci] --- Changelog.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Changelog.md b/Changelog.md index 68be1747e3..7f9df33034 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,11 @@ +### Development + +Enhancements: + +* Expose the reporter used to run examples via `RSpec::Core::Example#reporter`. + (Jon Rowe, #1866) +* Make `RSpec::Core::Reporter#message` a public supported API. (Jon Rowe, #1866) + ### 3.2.0 / 2015-02-03 [Full Changelog](http://github.com/rspec/rspec-core/compare/v3.1.7...v3.2.0) From 26331829661c5f5cf2fcbad4c988970929ccf0c6 Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Thu, 5 Feb 2015 14:45:41 +1100 Subject: [PATCH 010/258] switch NullReporter to not be an instance --- lib/rspec/core/example.rb | 2 +- lib/rspec/core/example_group.rb | 2 +- lib/rspec/core/reporter.rb | 5 ++--- spec/rspec/core/example_spec.rb | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/rspec/core/example.rb b/lib/rspec/core/example.rb index 4f71e93a34..c2b4f6ee90 100644 --- a/lib/rspec/core/example.rb +++ b/lib/rspec/core/example.rb @@ -143,7 +143,7 @@ def initialize(example_group_class, description, user_metadata, example_block=ni @example_group_instance = @exception = nil @clock = RSpec::Core::Time - @reporter = RSpec::Core::NullReporter.new + @reporter = RSpec::Core::NullReporter end # @return [RSpec::Core::Reporter] the current reporter for the example diff --git a/lib/rspec/core/example_group.rb b/lib/rspec/core/example_group.rb index 1738de33a9..7ebe8de395 100644 --- a/lib/rspec/core/example_group.rb +++ b/lib/rspec/core/example_group.rb @@ -499,7 +499,7 @@ def self.run_after_context_hooks(example_group_instance) end # Runs all the examples in this group. - def self.run(reporter=RSpec::Core::NullReporter.new) + def self.run(reporter=RSpec::Core::NullReporter) if RSpec.world.wants_to_quit RSpec.world.clear_remaining_example_groups if top_level? return diff --git a/lib/rspec/core/reporter.rb b/lib/rspec/core/reporter.rb index 1e5fc3697c..910c98bd00 100644 --- a/lib/rspec/core/reporter.rb +++ b/lib/rspec/core/reporter.rb @@ -164,10 +164,9 @@ def seed_used? # @private # # Used in place of a {Reporter} for situations where we don't want reporting output. class NullReporter - private - - def method_missing(*) + def self.method_missing(*) # ignore end + private_class_method :method_missing end end diff --git a/spec/rspec/core/example_spec.rb b/spec/rspec/core/example_spec.rb index 2d63a2f5b9..f2c3b7ec5d 100644 --- a/spec/rspec/core/example_spec.rb +++ b/spec/rspec/core/example_spec.rb @@ -736,7 +736,7 @@ def expect_pending_result(example) describe "exposing the examples reporter" do it "returns a null reporter when the example hasnt run yet" do example = RSpec.describe.example - expect(example.reporter).to be_a RSpec::Core::NullReporter + expect(example.reporter).to be RSpec::Core::NullReporter end it "returns the reporter used to run the example when executed" do From a7b1037022fb3234513cb865668a59f1bac63f07 Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Fri, 6 Feb 2015 11:58:57 +1100 Subject: [PATCH 011/258] allow the reporter to send custom events to registered formatters --- lib/rspec/core/notifications.rb | 14 ++++++++++++++ lib/rspec/core/reporter.rb | 23 +++++++++++++++++++++++ spec/rspec/core/reporter_spec.rb | 31 +++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+) diff --git a/lib/rspec/core/notifications.rb b/lib/rspec/core/notifications.rb index 50355d2077..93be6a4eb5 100644 --- a/lib/rspec/core/notifications.rb +++ b/lib/rspec/core/notifications.rb @@ -599,5 +599,19 @@ def self.from_hash(data) # currently require no information, but we may wish to extend in future. class NullNotification end + + # `CustomNotification` is used when sending custom events to formatters / + # other registered listeners, it creates attributes based on supplied hash + # of options. + class CustomNotification < Struct + # @param options [Hash] A hash of method / value pairs to create on this notification + # @return [CustomNotification] + # + # Build a custom notification based on the supplied option key / values. + def self.for(options={}) + return NullNotification if options.keys.empty? + new(*options.keys).new(*options.values) + end + end end end diff --git a/lib/rspec/core/reporter.rb b/lib/rspec/core/reporter.rb index 910c98bd00..7762507512 100644 --- a/lib/rspec/core/reporter.rb +++ b/lib/rspec/core/reporter.rb @@ -1,7 +1,17 @@ +require 'set' module RSpec::Core # A reporter will send notifications to listeners, usually formatters for the # spec suite run. class Reporter + # @private + RSPEC_NOTIFICATIONS = Set.new( + [ + :close, :deprecation, :deprecation_summary, :dump_failures, :dump_pending, + :dump_profile, :dump_summary, :example_failed, :example_group_finished, + :example_group_started, :example_passed, :example_pending, :example_started, + :message, :seed, :start, :start_dump, :stop + ]) + def initialize(configuration) @configuration = configuration @listeners = Hash.new { |h, k| h[k] = Set.new } @@ -79,6 +89,19 @@ def message(message) notify :message, Notifications::MessageNotification.new(message) end + # @param event [Symbol] Name of the custom event to trigger on formatters + # @param options [Hash] Hash of arguments to provide via `CustomNotification` + # + # Publish a custom event to supporting registered formatters. + # @see RSpec::Core::Notifications::CustomNotification + def publish(event, options={}) + if RSPEC_NOTIFICATIONS.include? event + raise "RSpec::Core::Reporter#publish is intended for sending custom " \ + "events not internal RSpec ones, please rename your custom event." + end + notify event, Notifications::CustomNotification.for(options) + end + # @private def example_group_started(group) notify :example_group_started, Notifications::GroupNotification.new(group) unless group.descendant_filtered_examples.empty? diff --git a/spec/rspec/core/reporter_spec.rb b/spec/rspec/core/reporter_spec.rb index 0c195a7db4..f60aa7dff3 100644 --- a/spec/rspec/core/reporter_spec.rb +++ b/spec/rspec/core/reporter_spec.rb @@ -172,6 +172,37 @@ module RSpec::Core end end + describe "#publish" do + let(:listener) { double("listener", :custom => nil) } + before do + reporter.register_listener listener, :custom, :start + end + + it 'will send custom events to registered listeners' do + expect(listener).to receive(:custom).with(RSpec::Core::Notifications::NullNotification) + reporter.publish :custom + end + + it 'will raise when encountering RSpec standard events' do + expect { reporter.publish :start }.to raise_error( + StandardError, + a_string_including("not internal RSpec ones") + ) + end + + it 'will ignore event names sent as strings' do + expect(listener).not_to receive(:custom) + reporter.publish "custom" + end + + it 'will provide a custom notification object based on the options hash' do + expect(listener).to receive(:custom).with( + an_object_having_attributes(:my_data => :value) + ) + reporter.publish :custom, :my_data => :value + end + end + describe "timing" do before do config.start_time = start_time From 4e906ebdc4d2c9908541e7b67e56478d59cd34aa Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Sun, 8 Feb 2015 13:36:34 +1100 Subject: [PATCH 012/258] enforce notify only sending internal notifications --- spec/support/formatter_support.rb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/spec/support/formatter_support.rb b/spec/support/formatter_support.rb index 875bd98988..b8edddc4a3 100644 --- a/spec/support/formatter_support.rb +++ b/spec/support/formatter_support.rb @@ -275,3 +275,20 @@ def profile_notification(duration, examples, number) end end + +if RSpec::Support::RubyFeatures.module_prepends_supported? + module RSpec::Core + class Reporter + module EnforceRSpecNotificationsListComplete + def notify(event, *args) + return super if caller_locations(1, 1).first.label =~ /publish/ + return super if RSPEC_NOTIFICATIONS.include?(event) + + raise "#{event.inspect} must be added to `RSPEC_NOTIFICATIONS`" + end + end + + prepend EnforceRSpecNotificationsListComplete + end + end +end From a52fc456f8dee9b57ea92f1625842a80f778b84b Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Sun, 8 Feb 2015 21:15:14 -0600 Subject: [PATCH 013/258] Skip specs with non-mri-compatible backtrace. --- features/core_standalone.feature | 2 ++ spec/rspec/core/formatters/documentation_formatter_spec.rb | 4 ++-- spec/rspec/core/formatters/progress_formatter_spec.rb | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/features/core_standalone.feature b/features/core_standalone.feature index a583563dae..53523d9abf 100644 --- a/features/core_standalone.feature +++ b/features/core_standalone.feature @@ -5,6 +5,8 @@ Feature: Use rspec-core without rspec-mocks or rspec-expectations available, but rspec-core can be used just fine without either of those gems installed. + # Rubinius stacktrace includes kernel/loader.rb etc. + @unsupported-on-rbx Scenario: Use only rspec-core when only it is installed Given only rspec-core is installed And a file named "core_only_spec.rb" with: diff --git a/spec/rspec/core/formatters/documentation_formatter_spec.rb b/spec/rspec/core/formatters/documentation_formatter_spec.rb index 43eac1beaa..2c14d8d2cc 100644 --- a/spec/rspec/core/formatters/documentation_formatter_spec.rb +++ b/spec/rspec/core/formatters/documentation_formatter_spec.rb @@ -77,8 +77,8 @@ def execution_result(values) ") end - # The backrace is slightly different on JRuby so we skip there. - it 'produces the expected full output', :unless => RUBY_PLATFORM == 'java' do + # The backtrace is slightly different on JRuby/Rubinius so we skip there. + it 'produces the expected full output', :if => RSpec::Support::Ruby.mri? do output = run_example_specs_with_formatter("doc") output.gsub!(/ +$/, '') # strip trailing whitespace diff --git a/spec/rspec/core/formatters/progress_formatter_spec.rb b/spec/rspec/core/formatters/progress_formatter_spec.rb index a92a9fdb0f..9386b6b1c4 100644 --- a/spec/rspec/core/formatters/progress_formatter_spec.rb +++ b/spec/rspec/core/formatters/progress_formatter_spec.rb @@ -40,8 +40,8 @@ expect(output.string).to eq("\n") end - # The backrace is slightly different on JRuby so we skip there. - it 'produces the expected full output', :unless => RUBY_PLATFORM == 'java' do + # The backtrace is slightly different on JRuby/Rubinius so we skip there. + it 'produces the expected full output', :if => RSpec::Support::Ruby.mri? do output = run_example_specs_with_formatter("progress") output.gsub!(/ +$/, '') # strip trailing whitespace From f00b298509e324120c39be0df37b0dd28021af84 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Sun, 8 Feb 2015 23:17:11 -0800 Subject: [PATCH 014/258] Add changelog entry for #1869. [ci skip] --- Changelog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Changelog.md b/Changelog.md index 7f9df33034..d36c728b8b 100644 --- a/Changelog.md +++ b/Changelog.md @@ -5,6 +5,8 @@ Enhancements: * Expose the reporter used to run examples via `RSpec::Core::Example#reporter`. (Jon Rowe, #1866) * Make `RSpec::Core::Reporter#message` a public supported API. (Jon Rowe, #1866) +* Allow custom formatter events to be published via + `RSpec::Core::Reporter#publish(hash)`. (Jon Rowe, #1869) ### 3.2.0 / 2015-02-03 [Full Changelog](http://github.com/rspec/rspec-core/compare/v3.1.7...v3.2.0) From c0960539ee720fb9366b4852f9dc95248cc78ee9 Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Mon, 9 Feb 2015 22:07:11 +1100 Subject: [PATCH 015/258] ammend changelog for #1869 [skip ci] --- Changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index d36c728b8b..fa818aba5f 100644 --- a/Changelog.md +++ b/Changelog.md @@ -6,7 +6,7 @@ Enhancements: (Jon Rowe, #1866) * Make `RSpec::Core::Reporter#message` a public supported API. (Jon Rowe, #1866) * Allow custom formatter events to be published via - `RSpec::Core::Reporter#publish(hash)`. (Jon Rowe, #1869) + `RSpec::Core::Reporter#publish(event_name, hash_of_attributes)`. (Jon Rowe, #1869) ### 3.2.0 / 2015-02-03 [Full Changelog](http://github.com/rspec/rspec-core/compare/v3.1.7...v3.2.0) From f2518949d01ddacbdfc8b6d48fe2bb0903561d48 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Tue, 10 Feb 2015 23:55:13 -0800 Subject: [PATCH 016/258] Remove unnecessary shift/unshift. --- lib/rspec/core/example_group.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/rspec/core/example_group.rb b/lib/rspec/core/example_group.rb index 7ebe8de395..d566093153 100644 --- a/lib/rspec/core/example_group.rb +++ b/lib/rspec/core/example_group.rb @@ -372,7 +372,7 @@ def self.subclass(parent, description, args, &example_group_block) end # @private - def self.set_it_up(*args, &example_group_block) + def self.set_it_up(description, *args, &example_group_block) # Ruby 1.9 has a bug that can lead to infinite recursion and a # SystemStackError if you include a module in a superclass after # including it in a subclass: https://gist.github.com/845896 @@ -383,12 +383,10 @@ def self.set_it_up(*args, &example_group_block) # here. ensure_example_groups_are_configured - description = args.shift user_metadata = Metadata.build_hash_from(args) - args.unshift(description) @metadata = Metadata::ExampleGroupHash.create( - superclass_metadata, user_metadata, *args, &example_group_block + superclass_metadata, user_metadata, description, *args, &example_group_block ) hooks.register_globals(self, RSpec.configuration.hooks) From 6525139b8b66144c2dfc01e97ea71adf8c6c23ee Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Sat, 14 Feb 2015 14:33:29 -0800 Subject: [PATCH 017/258] Beef up tests for when rspec-expectations is not available. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Specifically, in #1826, we changed the conditional for whether or not to assign a generated description from RSpec::Matchers for examples with no doc string. Before #1826, the conditional was: assign_generated_description if RSpec.configuration.expecting_with_rspec? In #1826 it changed to: assign_generated_description if defined?(::RSpec::Matchers) We didn’t update the spec meant for that case to match, but it continued to pass (as a false positive) due to rspec/rspec-mocks#874. @samphippen’s fix in rspec/rspec-mocks#884 surfaced the issue (as the spec now failed) so I decided to improve the tests. - The spec now simulates the `RSpec::Matchers` constant being undefined to simulate the correct condition. We also have to prevent it from being autoloaded. - The cukes for minitest/test-unit did not sufficiently cover this case, because the aforementioned autoload would autoload RSpec::Matchers, so we have to simulate rspec-expectations being completely uninstalled. Then the cukes properly fail if we break the `if defined?(::RSpec::Matchers)` conditional. --- .../configure_expectation_framework.feature | 9 ++++--- .../step_definitions/core_standalone_steps.rb | 4 ++++ spec/rspec/core/example_spec.rb | 24 +++++++++++++++---- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/features/expectation_framework_integration/configure_expectation_framework.feature b/features/expectation_framework_integration/configure_expectation_framework.feature index 6a28436aca..e63d3caf6a 100644 --- a/features/expectation_framework_integration/configure_expectation_framework.feature +++ b/features/expectation_framework_integration/configure_expectation_framework.feature @@ -45,7 +45,8 @@ Feature: configure expectation framework Then the examples should all pass Scenario: Configure test/unit assertions - Given a file named "example_spec.rb" with: + Given rspec-expectations is not installed + And a file named "example_spec.rb" with: """ruby RSpec.configure do |config| config.expect_with :test_unit @@ -72,7 +73,8 @@ Feature: configure expectation framework And the output should contain "3 examples, 1 failure" Scenario: Configure minitest assertions - Given a file named "example_spec.rb" with: + Given rspec-expectations is not installed + And a file named "example_spec.rb" with: """ruby RSpec.configure do |config| config.expect_with :minitest @@ -146,7 +148,8 @@ Feature: configure expectation framework Then the examples should all pass Scenario: Configure test/unit and minitest assertions - Given a file named "example_spec.rb" with: + Given rspec-expectations is not installed + And a file named "example_spec.rb" with: """ruby RSpec.configure do |config| config.expect_with :test_unit, :minitest diff --git a/features/step_definitions/core_standalone_steps.rb b/features/step_definitions/core_standalone_steps.rb index fd24c3aa34..f847cb8575 100644 --- a/features/step_definitions/core_standalone_steps.rb +++ b/features/step_definitions/core_standalone_steps.rb @@ -10,3 +10,7 @@ # rspec-expectations from the load path. set_env('REMOVE_OTHER_RSPEC_LIBS_FROM_LOAD_PATH', 'true') end + +Given(/^rspec-expectations is not installed$/) do + step "only rspec-core is installed" +end diff --git a/spec/rspec/core/example_spec.rb b/spec/rspec/core/example_spec.rb index f2c3b7ec5d..05d864ca48 100644 --- a/spec/rspec/core/example_spec.rb +++ b/spec/rspec/core/example_spec.rb @@ -187,12 +187,28 @@ def assert(val) end context "when `expect_with :stdlib` is configured" do - before(:each) { expect_with :stdlib } + around do |ex| + # Prevent RSpec::Matchers from being autoloaded. + orig_autoloads = RSpec::MODULES_TO_AUTOLOAD.dup + RSpec::MODULES_TO_AUTOLOAD.clear + ex.run + RSpec::MODULES_TO_AUTOLOAD.replace(orig_autoloads) + end + + before { expect_with :stdlib } - it "does not attempt to get the generated description from RSpec::Matchers" do - expect(RSpec::Matchers).not_to receive(:generated_description) - example_group.example { assert 5 == 5 } + it "does not attempt to get the generated description from RSpec::Matchers when not loaded" do + # Hide the constant while the example runs to simulate it being unloaded. + example_group.before { hide_const("RSpec::Matchers") } + + ex = example_group.example { assert 5 == 5 } example_group.run + + # We rescue errors that occur while generating the description and append it, + # so this ensures that no error mentioning `RSpec::Matchers` occurred while + # generating the description. + expect(ex.description).not_to include("RSpec::Matchers") + expect(ex).to pass end it "uses the file and line number" do From 07b4bea7d4de684431abd19e1b8d99ea7ce48ed8 Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Mon, 16 Feb 2015 13:59:56 +1100 Subject: [PATCH 018/258] Fix inconsistency in spec @cbliard pointed out on ca3d7fe via https://github.com/rspec/rspec-core/pull/1703/files#r24651173 that these specs don't actually assert what they are meant to so corrected --- spec/rspec/core/ruby_project_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/rspec/core/ruby_project_spec.rb b/spec/rspec/core/ruby_project_spec.rb index 985a721231..7e49fedb7d 100644 --- a/spec/rspec/core/ruby_project_spec.rb +++ b/spec/rspec/core/ruby_project_spec.rb @@ -31,11 +31,11 @@ def expect_ascend(source_path, *yielded_paths) end it "works with a normal path" do - expect_ascend("/var//ponies/", "/var/ponies", "/var", "/") + expect_ascend("/var/ponies", "/var/ponies", "/var", "/") end it "works with a path with a trailing slash" do - expect_ascend("/var//ponies/", "/var/ponies", "/var", "/") + expect_ascend("/var/ponies/", "/var/ponies", "/var", "/") end it "works with a path with double slashes" do From 7a3a4fdc858a487531a0fbafa0106a1c7f08781c Mon Sep 17 00:00:00 2001 From: Samuel Esposito Date: Mon, 16 Feb 2015 16:35:36 +0100 Subject: [PATCH 019/258] notify seed before notifying start --- lib/rspec/core/reporter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rspec/core/reporter.rb b/lib/rspec/core/reporter.rb index 7762507512..16ffaad855 100644 --- a/lib/rspec/core/reporter.rb +++ b/lib/rspec/core/reporter.rb @@ -78,8 +78,8 @@ def report(expected_example_count) def start(expected_example_count, time=RSpec::Core::Time.now) @start = time @load_time = (@start - @configuration.start_time).to_f - notify :start, Notifications::StartNotification.new(expected_example_count, @load_time) notify :seed, Notifications::SeedNotification.new(@configuration.seed, seed_used?) + notify :start, Notifications::StartNotification.new(expected_example_count, @load_time) end # @param message [#to_s] A message object to send to formatters From 2808ab1a06981dfb1b932ff10ac902d1744fb7bf Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 13 Feb 2015 22:28:33 -0800 Subject: [PATCH 020/258] Cleanup whitespace. --- spec/rspec/core/backtrace_formatter_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/rspec/core/backtrace_formatter_spec.rb b/spec/rspec/core/backtrace_formatter_spec.rb index e63460601c..7355243229 100644 --- a/spec/rspec/core/backtrace_formatter_spec.rb +++ b/spec/rspec/core/backtrace_formatter_spec.rb @@ -112,7 +112,7 @@ def make_backtrace_formatter(exclusion_patterns=nil, inclusion_patterns=nil) expect(BacktraceFormatter.new.format_backtrace(backtrace)).to eq(["./my_spec.rb:5"]) end - + context "when every line is filtered out" do let(:backtrace) do [ From ca3f0d03b5de3c70a4de0552cb25d5bccac0b882 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Sun, 15 Feb 2015 16:14:17 -0800 Subject: [PATCH 021/258] Remove unneeded require. --- lib/rspec/core.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/rspec/core.rb b/lib/rspec/core.rb index 77d93705e8..8b71cd37eb 100644 --- a/lib/rspec/core.rb +++ b/lib/rspec/core.rb @@ -2,8 +2,6 @@ $_rspec_core_load_started_at = Time.now # rubocop:enable Style/GlobalVars -require 'rbconfig' - require "rspec/support" RSpec::Support.require_rspec_support "caller_filter" From 3f8a079ce6329a366af56b66931d11cf0e2b0edf Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 13 Feb 2015 22:28:44 -0800 Subject: [PATCH 022/258] Update to new library-wide rspec-support checks. --- spec/rspec/core_spec.rb | 56 +++++++++++++++++-- spec/spec_helper.rb | 4 ++ .../minitest.rb => fake_libs/coderay.rb} | 0 spec/support/fake_libs/drb/drb.rb | 0 spec/support/fake_libs/erb.rb | 4 ++ spec/support/fake_libs/flexmock/rspec.rb | 4 ++ spec/support/fake_libs/json.rb | 0 spec/support/fake_libs/minitest.rb | 0 .../minitest/assertions.rb} | 0 spec/support/fake_libs/mocha/api.rb | 4 ++ spec/support/fake_libs/rake.rb | 0 spec/support/fake_libs/rake/tasklib.rb | 4 ++ spec/support/fake_libs/rr.rb | 10 ++++ spec/support/fake_libs/rspec/expectations.rb | 8 +++ spec/support/fake_libs/rspec/mocks.rb | 8 +++ .../test/unit/assertions.rb | 0 16 files changed, 96 insertions(+), 6 deletions(-) rename spec/support/{fake_minitest/minitest.rb => fake_libs/coderay.rb} (100%) create mode 100644 spec/support/fake_libs/drb/drb.rb create mode 100644 spec/support/fake_libs/erb.rb create mode 100644 spec/support/fake_libs/flexmock/rspec.rb create mode 100644 spec/support/fake_libs/json.rb create mode 100644 spec/support/fake_libs/minitest.rb rename spec/support/{fake_minitest/minitest/minitest_assertions.rb => fake_libs/minitest/assertions.rb} (100%) create mode 100644 spec/support/fake_libs/mocha/api.rb create mode 100644 spec/support/fake_libs/rake.rb create mode 100644 spec/support/fake_libs/rake/tasklib.rb create mode 100644 spec/support/fake_libs/rr.rb create mode 100644 spec/support/fake_libs/rspec/expectations.rb create mode 100644 spec/support/fake_libs/rspec/mocks.rb rename spec/support/{fake_minitest => fake_libs}/test/unit/assertions.rb (100%) diff --git a/spec/rspec/core_spec.rb b/spec/rspec/core_spec.rb index 6073b420f8..260887d58b 100644 --- a/spec/rspec/core_spec.rb +++ b/spec/rspec/core_spec.rb @@ -1,11 +1,55 @@ -require 'rspec/support/spec/prevent_load_time_warnings' +require 'rspec/support/spec/library_wide_checks' RSpec.describe RSpec do - fake_minitest = File.expand_path('../../support/fake_minitest', __FILE__) - it_behaves_like 'a library that issues no warnings when loaded', 'rspec-core', - # Loading minitest issues warnings, so we put our fake minitest on the load - # path to prevent the real minitest from being loaded. - "$LOAD_PATH.unshift '#{fake_minitest}'", 'require "rspec/core"', 'RSpec::Core::Runner.disable_autorun!' do + fake_libs = File.expand_path('../../support/fake_libs', __FILE__) + it_behaves_like 'library wide checks', 'rspec-core', + :preamble_for_lib => [ + # rspec-core loads a number of external libraries. We don't want them loaded + # as part of loading all of rspec-core for these specs, for a few reasons: + # + # * Some external libraries issue warnings, which we can't do anything about. + # Since we are trying to prevent _any_ warnings from loading RSpec, it's + # easiest to avoid loading those libraries entirely. + # * Some external libraries load many stdlibs. Here we allow a known set of + # directly loaded stdlibs, and we're not directly concerned with transitive + # dependencies. + # * We're really only concerned with these issues w.r.t. rspec-mocks and + # rspec-expectations from within their spec suites. Here we care only about + # rspec-core, so avoiding loading them helps keep the spec suites independent. + # * These are some of the slowest specs we have, and cutting out the loading + # of external libraries cuts down on how long these specs take. + # + # To facilitate the avoidance of loading certain libraries, we have a bunch + # of files in `support/fake_libs` that substitute for the real things when + # we put that directory on the load path. Here's the list: + # + # * coderay -- loaded by the HTML formatter if availble for syntax highlighting. + # * drb -- loaded when `--drb` is used. Loads other stdlibs (socket, thread, fcntl). + # * erb -- loaded by `ConfigurationOptions` so `.rspec` can use ERB. Loads other stdlibs (strscan, cgi/util). + # * flexmock -- loaded by our Flexmock mocking adapter. + # * json -- loaded by the JSON formatter, loads other stdlibs (ostruct, enc/utf_16le.bundle, etc). + # * minitest -- loaded by our Minitest assertions adapter. + # * mocha -- loaded by our Mocha mocking adapter. + # * rake -- loaded by our Rake task. Loads other stdlibs (fileutils, ostruct, thread, monitor, etc). + # * rr -- loaded by our RR mocking adapter. + # * rspec-mocks -- loaded by our RSpec mocking adapter. + # * rspec-expectations -- loaded by the generated `spec_helper` (defined in project_init). + # * test-unit -- loaded by our T::U assertions adapter. + # + "$LOAD_PATH.unshift '#{fake_libs}'", + # Many files assume this has already been loaded and will have errors if it has not. + 'require "rspec/core"', + # Prevent rspec/autorun from trying to run RSpec. + 'RSpec::Core::Runner.disable_autorun!' + ], :skip_spec_files => %r{/fake_libs/}, + :allowed_loaded_feature_regexps => [ + /optparse\.rb/, # Used by OptionParser. + /set\.rb/, # used in a few places but being removed in #1870. + /rbconfig\.rb/, # loaded by rspec-support for OS detection. + /shellwords\.rb/, # used by ConfigurationOptions and RakeTask. + /stringio/, # Used by BaseFormatter. + %r{/fake_libs/}, # ignore these, obviously + ] do pending_when = { '1.9.2' => { :description => "issues no warnings when loaded" }, diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 81bc9b3a65..485d1bac12 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -22,6 +22,10 @@ def self.new(*args, &block) end Dir['./spec/support/**/*.rb'].map do |file| + # fake libs aren't intended to be loaded except by some specific specs + # that shell out and run a new process. + next if file =~ /fake_libs/ + # Ensure requires are relative to `spec`, which is on the # load path. This helps prevent double requires on 1.8.7. require file.gsub("./spec/support", "support") diff --git a/spec/support/fake_minitest/minitest.rb b/spec/support/fake_libs/coderay.rb similarity index 100% rename from spec/support/fake_minitest/minitest.rb rename to spec/support/fake_libs/coderay.rb diff --git a/spec/support/fake_libs/drb/drb.rb b/spec/support/fake_libs/drb/drb.rb new file mode 100644 index 0000000000..e69de29bb2 diff --git a/spec/support/fake_libs/erb.rb b/spec/support/fake_libs/erb.rb new file mode 100644 index 0000000000..4529438d7c --- /dev/null +++ b/spec/support/fake_libs/erb.rb @@ -0,0 +1,4 @@ +module ERB + module Util + end +end diff --git a/spec/support/fake_libs/flexmock/rspec.rb b/spec/support/fake_libs/flexmock/rspec.rb new file mode 100644 index 0000000000..c5b6c9f337 --- /dev/null +++ b/spec/support/fake_libs/flexmock/rspec.rb @@ -0,0 +1,4 @@ +module FlexMock + module MockContainer + end +end diff --git a/spec/support/fake_libs/json.rb b/spec/support/fake_libs/json.rb new file mode 100644 index 0000000000..e69de29bb2 diff --git a/spec/support/fake_libs/minitest.rb b/spec/support/fake_libs/minitest.rb new file mode 100644 index 0000000000..e69de29bb2 diff --git a/spec/support/fake_minitest/minitest/minitest_assertions.rb b/spec/support/fake_libs/minitest/assertions.rb similarity index 100% rename from spec/support/fake_minitest/minitest/minitest_assertions.rb rename to spec/support/fake_libs/minitest/assertions.rb diff --git a/spec/support/fake_libs/mocha/api.rb b/spec/support/fake_libs/mocha/api.rb new file mode 100644 index 0000000000..a69f6f70ca --- /dev/null +++ b/spec/support/fake_libs/mocha/api.rb @@ -0,0 +1,4 @@ +module Mocha + module API + end +end diff --git a/spec/support/fake_libs/rake.rb b/spec/support/fake_libs/rake.rb new file mode 100644 index 0000000000..e69de29bb2 diff --git a/spec/support/fake_libs/rake/tasklib.rb b/spec/support/fake_libs/rake/tasklib.rb new file mode 100644 index 0000000000..d7c9fba492 --- /dev/null +++ b/spec/support/fake_libs/rake/tasklib.rb @@ -0,0 +1,4 @@ +module Rake + class TaskLib + end +end diff --git a/spec/support/fake_libs/rr.rb b/spec/support/fake_libs/rr.rb new file mode 100644 index 0000000000..f9cc74adbc --- /dev/null +++ b/spec/support/fake_libs/rr.rb @@ -0,0 +1,10 @@ +module RR + module Errors + BACKTRACE_IDENTIFIER = /doesn't matter/ + end + + module Extensions + module InstanceMethods + end + end +end diff --git a/spec/support/fake_libs/rspec/expectations.rb b/spec/support/fake_libs/rspec/expectations.rb new file mode 100644 index 0000000000..33af050df1 --- /dev/null +++ b/spec/support/fake_libs/rspec/expectations.rb @@ -0,0 +1,8 @@ +module RSpec + module Expectations + end + + module Matchers + def self.configuration; RSpec::Core::NullReporter; end + end +end diff --git a/spec/support/fake_libs/rspec/mocks.rb b/spec/support/fake_libs/rspec/mocks.rb new file mode 100644 index 0000000000..ac20109105 --- /dev/null +++ b/spec/support/fake_libs/rspec/mocks.rb @@ -0,0 +1,8 @@ +module RSpec + module Mocks + module ExampleMethods + end + + def self.configuration; RSpec::Core::NullReporter; end + end +end diff --git a/spec/support/fake_minitest/test/unit/assertions.rb b/spec/support/fake_libs/test/unit/assertions.rb similarity index 100% rename from spec/support/fake_minitest/test/unit/assertions.rb rename to spec/support/fake_libs/test/unit/assertions.rb From 08c4c7b165f68a14108cb82698fdf05019c013c5 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Tue, 17 Feb 2015 09:46:12 -0800 Subject: [PATCH 023/258] Update what we skip to account for recent changes. --- spec/rspec/core_spec.rb | 38 +++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/spec/rspec/core_spec.rb b/spec/rspec/core_spec.rb index 260887d58b..fd75740daa 100644 --- a/spec/rspec/core_spec.rb +++ b/spec/rspec/core_spec.rb @@ -2,6 +2,18 @@ RSpec.describe RSpec do fake_libs = File.expand_path('../../support/fake_libs', __FILE__) + allowed_loaded_features = [ + /optparse\.rb/, # Used by OptionParser. + /set\.rb/, # used in a few places but being removed in #1870. + /rbconfig\.rb/, # loaded by rspec-support for OS detection. + /shellwords\.rb/, # used by ConfigurationOptions and RakeTask. + /stringio/, # Used by BaseFormatter. + %r{/fake_libs/}, # ignore these, obviously + ] + + # JRuby appears to not respect `--disable=gem` so rubygems also gets loaded. + allowed_loaded_features << /rubygems/ if RSpec::Support::Ruby.jruby? + it_behaves_like 'library wide checks', 'rspec-core', :preamble_for_lib => [ # rspec-core loads a number of external libraries. We don't want them loaded @@ -41,29 +53,13 @@ 'require "rspec/core"', # Prevent rspec/autorun from trying to run RSpec. 'RSpec::Core::Runner.disable_autorun!' - ], :skip_spec_files => %r{/fake_libs/}, - :allowed_loaded_feature_regexps => [ - /optparse\.rb/, # Used by OptionParser. - /set\.rb/, # used in a few places but being removed in #1870. - /rbconfig\.rb/, # loaded by rspec-support for OS detection. - /shellwords\.rb/, # used by ConfigurationOptions and RakeTask. - /stringio/, # Used by BaseFormatter. - %r{/fake_libs/}, # ignore these, obviously - ] do - - pending_when = { - '1.9.2' => { :description => "issues no warnings when loaded" }, - '1.8.7' => { :description => "issues no warnings when the spec files are loaded" }, - '2.0.0' => { } - } - - if RUBY_VERSION == '1.9.2' || RUBY_VERSION == '1.8.7' - before(:example, pending_when.fetch(RUBY_VERSION)) do + ], :skip_spec_files => %r{/fake_libs/}, :allowed_loaded_feature_regexps => allowed_loaded_features do + if RUBY_VERSION == '1.8.7' + before(:example, :description => /(issues no warnings when the spec files are loaded|stdlibs)/) do pending "Not working on #{RUBY_DESCRIPTION}" end - end - if (RUBY_PLATFORM == 'java' && RUBY_VERSION == '2.0.0') - before(:example, pending_when.fetch(RUBY_VERSION)) do + elsif RUBY_VERSION == '2.0.0' && RSpec::Support::Ruby.jruby? + before(:example) do skip "Not reliably working on #{RUBY_DESCRIPTION}" end end From 529f83b61c5460c49e7b25c8db8b0e7d1d585867 Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Sun, 8 Feb 2015 21:07:48 -0600 Subject: [PATCH 024/258] Address String#split failures by using EncodedString Add spec for exception when failure_lines has a bad encoding --- lib/rspec/core/notifications.rb | 10 +++++++++- spec/rspec/core/notifications_spec.rb | 13 +++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/lib/rspec/core/notifications.rb b/lib/rspec/core/notifications.rb index 93be6a4eb5..9c7b3091e5 100644 --- a/lib/rspec/core/notifications.rb +++ b/lib/rspec/core/notifications.rb @@ -205,9 +205,17 @@ def fully_formatted(failure_number, colorizer=::RSpec::Core::Formatters::Console def encoding_of(string) string.encoding end + + def encoded_string(string) + RSpec::Support::EncodedString.new(string, Encoding.default_external) + end else def encoding_of(_string) end + + def encoded_string(string) + RSpec::Support::EncodedString.new(string) + end end def backtrace_formatter @@ -225,7 +233,7 @@ def failure_lines begin lines = ["Failure/Error: #{read_failed_line.strip}"] lines << "#{exception_class_name}:" unless exception_class_name =~ /RSpec/ - exception.message.to_s.split("\n").each do |line| + encoded_string(exception.message.to_s).split("\n").each do |line| lines << " #{line}" if exception.message end lines diff --git a/spec/rspec/core/notifications_spec.rb b/spec/rspec/core/notifications_spec.rb index 83428f5026..cbcf5c0ca4 100644 --- a/spec/rspec/core/notifications_spec.rb +++ b/spec/rspec/core/notifications_spec.rb @@ -100,5 +100,18 @@ expect(lines[0]).to match %r{\AFailure\/Error} expect(lines[1]).to match %r{\A\s*Test exception\z} end + + if String.method_defined?(:encoding) + it "returns failures_lines with invalid bytes replace by '?'" do + message_with_invalid_byte_sequence = + "\xEF \255 \xAD I have bad bytes".force_encoding(Encoding::UTF_8) + allow(exception).to receive(:message). + and_return(message_with_invalid_byte_sequence) + + lines = notification.message_lines + expect(lines[0]).to match %r{\AFailure\/Error} + expect(lines[1].strip).to eq("? ? ? I have bad bytes") + end + end end end From 62204c858fe5fb11e119af997f3fc3b81b2cb129 Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Tue, 17 Feb 2015 20:56:06 -0600 Subject: [PATCH 025/258] Remove unnecessary conditional "".split("\n") # => [] nil.to_s.split("\n") # => [] If exception.message is nil, the inner block will not be reached. If it is non-nil, then there's no need to check for nil inside the block. --- lib/rspec/core/notifications.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rspec/core/notifications.rb b/lib/rspec/core/notifications.rb index 9c7b3091e5..b88d54ade4 100644 --- a/lib/rspec/core/notifications.rb +++ b/lib/rspec/core/notifications.rb @@ -234,7 +234,7 @@ def failure_lines lines = ["Failure/Error: #{read_failed_line.strip}"] lines << "#{exception_class_name}:" unless exception_class_name =~ /RSpec/ encoded_string(exception.message.to_s).split("\n").each do |line| - lines << " #{line}" if exception.message + lines << " #{line}" end lines end From 4bed44a8c2431b291027a65030de3f001b699593 Mon Sep 17 00:00:00 2001 From: Samuel Esposito Date: Wed, 18 Feb 2015 08:32:22 +0100 Subject: [PATCH 026/258] test order of notifications --- spec/rspec/core/reporter_spec.rb | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/spec/rspec/core/reporter_spec.rb b/spec/rspec/core/reporter_spec.rb index f60aa7dff3..01bb34c253 100644 --- a/spec/rspec/core/reporter_spec.rb +++ b/spec/rspec/core/reporter_spec.rb @@ -38,6 +38,16 @@ module RSpec::Core describe 'start' do before { config.start_time = start_time } + it 'notifies formatters of the seed used' do + formatter = double("formatter") + reporter.register_listener formatter, :seed + + expect(formatter).to receive(:seed).with( + an_object_having_attributes(:seed => config.seed, :seed_used? => config.seed_used?) + ) + reporter.start 1 + end + it 'notifies the formatter of start with example count' do formatter = double("formatter") reporter.register_listener formatter, :start @@ -50,13 +60,12 @@ module RSpec::Core reporter.start 3, (start_time + 5) end - it 'notifies formatters of the seed used' do + it 'notifies the formatter of the seed before notifing of start' do formatter = double("formatter") reporter.register_listener formatter, :seed - - expect(formatter).to receive(:seed).with( - an_object_having_attributes(:seed => config.seed, :seed_used? => config.seed_used?) - ) + reporter.register_listener formatter, :start + expect(formatter).to receive(:seed).ordered + expect(formatter).to receive(:start).ordered reporter.start 1 end end From 26d0cb01c5c49bb3c6c1051c4e638d4285a69f32 Mon Sep 17 00:00:00 2001 From: Mark Swinson Date: Wed, 18 Feb 2015 20:20:03 +0000 Subject: [PATCH 027/258] add type and version information to json formatter - add type to allow clients to detect file format - add version to determine which version of rspec-core generated the file --- lib/rspec/core/formatters/json_formatter.rb | 5 ++++- spec/rspec/core/formatters/json_formatter_spec.rb | 7 ++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/rspec/core/formatters/json_formatter.rb b/lib/rspec/core/formatters/json_formatter.rb index 080c10568d..83876caf8b 100644 --- a/lib/rspec/core/formatters/json_formatter.rb +++ b/lib/rspec/core/formatters/json_formatter.rb @@ -12,7 +12,10 @@ class JsonFormatter < BaseFormatter def initialize(output) super - @output_hash = {} + @output_hash = { + :type => 'rspec-json', + :version => RSpec::Core::Version::STRING + } end def message(notification) diff --git a/spec/rspec/core/formatters/json_formatter_spec.rb b/spec/rspec/core/formatters/json_formatter_spec.rb index 4e42215be1..d6dcf5591c 100644 --- a/spec/rspec/core/formatters/json_formatter_spec.rb +++ b/spec/rspec/core/formatters/json_formatter_spec.rb @@ -35,6 +35,8 @@ this_file = relative_path(__FILE__) expected = { + :type => 'rspec-json', + :version => RSpec::Core::Version::STRING, :examples => [ { :description => "succeeds", @@ -89,7 +91,10 @@ it "outputs the results as a JSON string" do expect(output.string).to eq "" send_notification :close, null_notification - expect(output.string).to eq("{}") + expect(output.string).to eq({ + :type => 'rspec-json', + :version => RSpec::Core::Version::STRING + }.to_json) end end From a14f95c0210492c57abc5e521097f98596ccf0f7 Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Thu, 19 Feb 2015 09:28:51 +1100 Subject: [PATCH 028/258] changelog for #1760 [skip ci] --- Changelog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Changelog.md b/Changelog.md index fa818aba5f..b36843919c 100644 --- a/Changelog.md +++ b/Changelog.md @@ -8,6 +8,10 @@ Enhancements: * Allow custom formatter events to be published via `RSpec::Core::Reporter#publish(event_name, hash_of_attributes)`. (Jon Rowe, #1869) +Bugfixes: + +* Handle invalid UTF-8 strings within exception methods. (Benjamin Fleischer, #1760) + ### 3.2.0 / 2015-02-03 [Full Changelog](http://github.com/rspec/rspec-core/compare/v3.1.7...v3.2.0) From 72b66b08e0cc46248728edeea2c5c963691cc13a Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Sun, 8 Feb 2015 15:49:04 +1100 Subject: [PATCH 029/258] remove usage of set from rspec-core and replace with an internal lookupset which uses a hash --- lib/rspec/core.rb | 1 + lib/rspec/core/configuration.rb | 2 +- lib/rspec/core/configuration_options.rb | 7 ++-- lib/rspec/core/formatters.rb | 4 +- .../core/formatters/deprecation_formatter.rb | 3 +- lib/rspec/core/lookup_set.rb | 41 +++++++++++++++++++ lib/rspec/core/metadata_filter.rb | 4 +- lib/rspec/core/reporter.rb | 5 +-- .../formatters/base_text_formatter_spec.rb | 2 +- spec/rspec/core/lookup_set_spec.rb | 23 +++++++++++ spec/rspec/core_spec.rb | 1 - 11 files changed, 77 insertions(+), 16 deletions(-) create mode 100644 lib/rspec/core/lookup_set.rb create mode 100644 spec/rspec/core/lookup_set_spec.rb diff --git a/lib/rspec/core.rb b/lib/rspec/core.rb index 8b71cd37eb..6a44c165c5 100644 --- a/lib/rspec/core.rb +++ b/lib/rspec/core.rb @@ -11,6 +11,7 @@ version warnings + lookup_set flat_map filter_manager dsl diff --git a/lib/rspec/core/configuration.rb b/lib/rspec/core/configuration.rb index f5dd8f9543..788c396422 100644 --- a/lib/rspec/core/configuration.rb +++ b/lib/rspec/core/configuration.rb @@ -306,7 +306,7 @@ def initialize @mock_framework = nil @files_or_directories_to_run = [] - @loaded_spec_files = Set.new + @loaded_spec_files = LookupSet.new @color = false @pattern = '**{,/*/**}/*_spec.rb' @exclude_pattern = '' diff --git a/lib/rspec/core/configuration_options.rb b/lib/rspec/core/configuration_options.rb index 36b9f5d05b..03ef3c13de 100644 --- a/lib/rspec/core/configuration_options.rb +++ b/lib/rspec/core/configuration_options.rb @@ -1,6 +1,5 @@ require 'erb' require 'shellwords' -require 'set' module RSpec module Core @@ -53,12 +52,12 @@ def organize_options end end - UNFORCED_OPTIONS = [ + UNFORCED_OPTIONS = LookupSet.new([ :requires, :profile, :drb, :libs, :files_or_directories_to_run, :full_description, :full_backtrace, :tty - ].to_set + ]) - UNPROCESSABLE_OPTIONS = [:formatters].to_set + UNPROCESSABLE_OPTIONS = LookupSet.new([:formatters]) def force?(key) !UNFORCED_OPTIONS.include?(key) diff --git a/lib/rspec/core/formatters.rb b/lib/rspec/core/formatters.rb index 765c2e1f45..ac0ec1ccfa 100644 --- a/lib/rspec/core/formatters.rb +++ b/lib/rspec/core/formatters.rb @@ -196,8 +196,8 @@ def built_in_formatter(key) end def notifications_for(formatter_class) - formatter_class.ancestors.inject(Set.new) do |notifications, klass| - notifications + Loader.formatters.fetch(klass) { Set.new } + formatter_class.ancestors.inject(::RSpec::Core::LookupSet.new) do |notifications, klass| + notifications.merge Loader.formatters.fetch(klass) { ::RSpec::Core::LookupSet.new } end end diff --git a/lib/rspec/core/formatters/deprecation_formatter.rb b/lib/rspec/core/formatters/deprecation_formatter.rb index 118fe88761..8aa2ed306c 100644 --- a/lib/rspec/core/formatters/deprecation_formatter.rb +++ b/lib/rspec/core/formatters/deprecation_formatter.rb @@ -1,5 +1,4 @@ RSpec::Support.require_rspec_core "formatters/helpers" -require 'set' module RSpec module Core @@ -13,7 +12,7 @@ class DeprecationFormatter def initialize(deprecation_stream, summary_stream) @deprecation_stream = deprecation_stream @summary_stream = summary_stream - @seen_deprecations = Set.new + @seen_deprecations = LookupSet.new @count = 0 end alias :output :deprecation_stream diff --git a/lib/rspec/core/lookup_set.rb b/lib/rspec/core/lookup_set.rb new file mode 100644 index 0000000000..2e60cecac3 --- /dev/null +++ b/lib/rspec/core/lookup_set.rb @@ -0,0 +1,41 @@ +module RSpec + module Core + # @private + # + # We use this to replace `::Set` so we can have the advantage of + # constant time key lookups for unique arrays but without the + # potential to pollute a developers environment with an extra + # piece of the stdlib. This helps to prevent false positive + # builds. + # + class LookupSet + include Enumerable + + def initialize(array=[]) + @values = {} + merge(array) + end + + def <<(key) + @values[key] = true + self + end + + def each(&block) + @values.keys.each(&block) + self + end + + def include?(key) + @values.key?(key) + end + + def merge(values) + values.each do |key| + @values[key] = true + end + self + end + end + end +end diff --git a/lib/rspec/core/metadata_filter.rb b/lib/rspec/core/metadata_filter.rb index ffe8c86258..64a9b9df2c 100644 --- a/lib/rspec/core/metadata_filter.rb +++ b/lib/rspec/core/metadata_filter.rb @@ -147,8 +147,8 @@ class QueryOptimized < UpdateOptimized def initialize(applies_predicate) super - @applicable_keys = Set.new - @proc_keys = Set.new + @applicable_keys = LookupSet.new + @proc_keys = LookupSet.new @memoized_lookups = Hash.new do |hash, applicable_metadata| hash[applicable_metadata] = find_items_for(applicable_metadata) end diff --git a/lib/rspec/core/reporter.rb b/lib/rspec/core/reporter.rb index 7762507512..a69e12a45e 100644 --- a/lib/rspec/core/reporter.rb +++ b/lib/rspec/core/reporter.rb @@ -1,10 +1,9 @@ -require 'set' module RSpec::Core # A reporter will send notifications to listeners, usually formatters for the # spec suite run. class Reporter # @private - RSPEC_NOTIFICATIONS = Set.new( + RSPEC_NOTIFICATIONS = LookupSet.new( [ :close, :deprecation, :deprecation_summary, :dump_failures, :dump_pending, :dump_profile, :dump_summary, :example_failed, :example_group_finished, @@ -14,7 +13,7 @@ class Reporter def initialize(configuration) @configuration = configuration - @listeners = Hash.new { |h, k| h[k] = Set.new } + @listeners = Hash.new { |h, k| h[k] = LookupSet.new } @examples = [] @failed_examples = [] @pending_examples = [] diff --git a/spec/rspec/core/formatters/base_text_formatter_spec.rb b/spec/rspec/core/formatters/base_text_formatter_spec.rb index bd5d476137..5b7eb1c318 100644 --- a/spec/rspec/core/formatters/base_text_formatter_spec.rb +++ b/spec/rspec/core/formatters/base_text_formatter_spec.rb @@ -52,7 +52,7 @@ end def output_from_running(example_group) - allow(RSpec.configuration).to receive(:loaded_spec_files) { [File.expand_path(__FILE__)].to_set } + allow(RSpec.configuration).to receive(:loaded_spec_files) { RSpec::Core::LookupSet.new([File.expand_path(__FILE__)]) } example_group.run(reporter) examples = example_group.examples send_notification :dump_summary, summary_notification(1, examples, examples, [], 0) diff --git a/spec/rspec/core/lookup_set_spec.rb b/spec/rspec/core/lookup_set_spec.rb new file mode 100644 index 0000000000..f931b026ac --- /dev/null +++ b/spec/rspec/core/lookup_set_spec.rb @@ -0,0 +1,23 @@ +RSpec.describe 'RSpec::Core::LookupSet' do + + let(:set) { RSpec::Core::LookupSet.new([1, 2, 3]) } + + it 'takes an array of values' do + expect(set).to include(1, 2, 3) + end + + it 'can be appended to' do + set << 4 + expect(set).to include 4 + end + + it 'can have more values merged in' do + set.merge([4, 5]).merge([6]) + expect(set).to include(4, 5, 6) + end + + it 'is enumerable' do + expect(set).to be_an Enumerable + expect { |p| set.each(&p) }.to yield_successive_args(1, 2, 3) + end +end diff --git a/spec/rspec/core_spec.rb b/spec/rspec/core_spec.rb index fd75740daa..e061d6f5f6 100644 --- a/spec/rspec/core_spec.rb +++ b/spec/rspec/core_spec.rb @@ -4,7 +4,6 @@ fake_libs = File.expand_path('../../support/fake_libs', __FILE__) allowed_loaded_features = [ /optparse\.rb/, # Used by OptionParser. - /set\.rb/, # used in a few places but being removed in #1870. /rbconfig\.rb/, # loaded by rspec-support for OS detection. /shellwords\.rb/, # used by ConfigurationOptions and RakeTask. /stringio/, # Used by BaseFormatter. From 44beedeffc29996aabf5292429b5290dff5ae0cf Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Thu, 19 Feb 2015 09:40:11 +1100 Subject: [PATCH 030/258] rename LookupSet to Set --- lib/rspec/core.rb | 2 +- lib/rspec/core/configuration.rb | 2 +- lib/rspec/core/configuration_options.rb | 4 ++-- lib/rspec/core/formatters.rb | 4 ++-- lib/rspec/core/formatters/deprecation_formatter.rb | 2 +- lib/rspec/core/metadata_filter.rb | 4 ++-- lib/rspec/core/reporter.rb | 4 ++-- lib/rspec/core/{lookup_set.rb => set.rb} | 2 +- spec/rspec/core/formatters/base_text_formatter_spec.rb | 2 +- spec/rspec/core/{lookup_set_spec.rb => set_spec.rb} | 4 ++-- 10 files changed, 15 insertions(+), 15 deletions(-) rename lib/rspec/core/{lookup_set.rb => set.rb} (97%) rename spec/rspec/core/{lookup_set_spec.rb => set_spec.rb} (80%) diff --git a/lib/rspec/core.rb b/lib/rspec/core.rb index 6a44c165c5..b02ea729e9 100644 --- a/lib/rspec/core.rb +++ b/lib/rspec/core.rb @@ -11,7 +11,7 @@ version warnings - lookup_set + set flat_map filter_manager dsl diff --git a/lib/rspec/core/configuration.rb b/lib/rspec/core/configuration.rb index 788c396422..f5dd8f9543 100644 --- a/lib/rspec/core/configuration.rb +++ b/lib/rspec/core/configuration.rb @@ -306,7 +306,7 @@ def initialize @mock_framework = nil @files_or_directories_to_run = [] - @loaded_spec_files = LookupSet.new + @loaded_spec_files = Set.new @color = false @pattern = '**{,/*/**}/*_spec.rb' @exclude_pattern = '' diff --git a/lib/rspec/core/configuration_options.rb b/lib/rspec/core/configuration_options.rb index 03ef3c13de..4e8f219a2a 100644 --- a/lib/rspec/core/configuration_options.rb +++ b/lib/rspec/core/configuration_options.rb @@ -52,12 +52,12 @@ def organize_options end end - UNFORCED_OPTIONS = LookupSet.new([ + UNFORCED_OPTIONS = Set.new([ :requires, :profile, :drb, :libs, :files_or_directories_to_run, :full_description, :full_backtrace, :tty ]) - UNPROCESSABLE_OPTIONS = LookupSet.new([:formatters]) + UNPROCESSABLE_OPTIONS = Set.new([:formatters]) def force?(key) !UNFORCED_OPTIONS.include?(key) diff --git a/lib/rspec/core/formatters.rb b/lib/rspec/core/formatters.rb index ac0ec1ccfa..194da7957d 100644 --- a/lib/rspec/core/formatters.rb +++ b/lib/rspec/core/formatters.rb @@ -196,8 +196,8 @@ def built_in_formatter(key) end def notifications_for(formatter_class) - formatter_class.ancestors.inject(::RSpec::Core::LookupSet.new) do |notifications, klass| - notifications.merge Loader.formatters.fetch(klass) { ::RSpec::Core::LookupSet.new } + formatter_class.ancestors.inject(::RSpec::Core::Set.new) do |notifications, klass| + notifications.merge Loader.formatters.fetch(klass) { ::RSpec::Core::Set.new } end end diff --git a/lib/rspec/core/formatters/deprecation_formatter.rb b/lib/rspec/core/formatters/deprecation_formatter.rb index 8aa2ed306c..ab9096260e 100644 --- a/lib/rspec/core/formatters/deprecation_formatter.rb +++ b/lib/rspec/core/formatters/deprecation_formatter.rb @@ -12,7 +12,7 @@ class DeprecationFormatter def initialize(deprecation_stream, summary_stream) @deprecation_stream = deprecation_stream @summary_stream = summary_stream - @seen_deprecations = LookupSet.new + @seen_deprecations = Set.new @count = 0 end alias :output :deprecation_stream diff --git a/lib/rspec/core/metadata_filter.rb b/lib/rspec/core/metadata_filter.rb index 64a9b9df2c..ffe8c86258 100644 --- a/lib/rspec/core/metadata_filter.rb +++ b/lib/rspec/core/metadata_filter.rb @@ -147,8 +147,8 @@ class QueryOptimized < UpdateOptimized def initialize(applies_predicate) super - @applicable_keys = LookupSet.new - @proc_keys = LookupSet.new + @applicable_keys = Set.new + @proc_keys = Set.new @memoized_lookups = Hash.new do |hash, applicable_metadata| hash[applicable_metadata] = find_items_for(applicable_metadata) end diff --git a/lib/rspec/core/reporter.rb b/lib/rspec/core/reporter.rb index a69e12a45e..43681889b0 100644 --- a/lib/rspec/core/reporter.rb +++ b/lib/rspec/core/reporter.rb @@ -3,7 +3,7 @@ module RSpec::Core # spec suite run. class Reporter # @private - RSPEC_NOTIFICATIONS = LookupSet.new( + RSPEC_NOTIFICATIONS = Set.new( [ :close, :deprecation, :deprecation_summary, :dump_failures, :dump_pending, :dump_profile, :dump_summary, :example_failed, :example_group_finished, @@ -13,7 +13,7 @@ class Reporter def initialize(configuration) @configuration = configuration - @listeners = Hash.new { |h, k| h[k] = LookupSet.new } + @listeners = Hash.new { |h, k| h[k] = Set.new } @examples = [] @failed_examples = [] @pending_examples = [] diff --git a/lib/rspec/core/lookup_set.rb b/lib/rspec/core/set.rb similarity index 97% rename from lib/rspec/core/lookup_set.rb rename to lib/rspec/core/set.rb index 2e60cecac3..19af219669 100644 --- a/lib/rspec/core/lookup_set.rb +++ b/lib/rspec/core/set.rb @@ -8,7 +8,7 @@ module Core # piece of the stdlib. This helps to prevent false positive # builds. # - class LookupSet + class Set include Enumerable def initialize(array=[]) diff --git a/spec/rspec/core/formatters/base_text_formatter_spec.rb b/spec/rspec/core/formatters/base_text_formatter_spec.rb index 5b7eb1c318..6c05ee76ec 100644 --- a/spec/rspec/core/formatters/base_text_formatter_spec.rb +++ b/spec/rspec/core/formatters/base_text_formatter_spec.rb @@ -52,7 +52,7 @@ end def output_from_running(example_group) - allow(RSpec.configuration).to receive(:loaded_spec_files) { RSpec::Core::LookupSet.new([File.expand_path(__FILE__)]) } + allow(RSpec.configuration).to receive(:loaded_spec_files) { RSpec::Core::Set.new([File.expand_path(__FILE__)]) } example_group.run(reporter) examples = example_group.examples send_notification :dump_summary, summary_notification(1, examples, examples, [], 0) diff --git a/spec/rspec/core/lookup_set_spec.rb b/spec/rspec/core/set_spec.rb similarity index 80% rename from spec/rspec/core/lookup_set_spec.rb rename to spec/rspec/core/set_spec.rb index f931b026ac..36f46d135b 100644 --- a/spec/rspec/core/lookup_set_spec.rb +++ b/spec/rspec/core/set_spec.rb @@ -1,6 +1,6 @@ -RSpec.describe 'RSpec::Core::LookupSet' do +RSpec.describe 'RSpec::Core::Set' do - let(:set) { RSpec::Core::LookupSet.new([1, 2, 3]) } + let(:set) { RSpec::Core::Set.new([1, 2, 3]) } it 'takes an array of values' do expect(set).to include(1, 2, 3) From d31ca7e1d70bcb5987b66c8b399bff9dd762d4dd Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Fri, 20 Feb 2015 10:13:27 +1100 Subject: [PATCH 031/258] benchmark demonstrating that `#keys.each` performs marginally better than `#each_key` --- benchmarks/keys_each_vs_each_key.rb | 43 +++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 benchmarks/keys_each_vs_each_key.rb diff --git a/benchmarks/keys_each_vs_each_key.rb b/benchmarks/keys_each_vs_each_key.rb new file mode 100644 index 0000000000..794bb1b35c --- /dev/null +++ b/benchmarks/keys_each_vs_each_key.rb @@ -0,0 +1,43 @@ +require 'benchmark/ips' + +small_hash = { :key => true, :more_key => true, :other_key => true } +large_hash = (1...100).inject({}) { |hash, key| hash["key_#{key}"] = true; hash } + +Benchmark.ips do |x| + x.report('keys.each with small hash') do + small_hash.keys.each { |value| value == true } + end + + x.report('each_key with small hash') do + small_hash.each_key { |value| value == true } + end + + x.report('keys.each with large hash') do + large_hash.keys.each { |value| value == true } + end + + x.report('each_key with large hash') do + large_hash.each_key { |value| value == true } + end +end + +__END__ + +Calculating ------------------------------------- +keys.each with small hash + 105.581k i/100ms +each_key with small hash + 112.045k i/100ms +keys.each with large hash + 7.625k i/100ms +each_key with large hash + 6.959k i/100ms +------------------------------------------------- +keys.each with small hash + 2.953M (± 3.8%) i/s - 14.781M +each_key with small hash + 2.917M (± 4.0%) i/s - 14.678M +keys.each with large hash + 79.349k (± 2.5%) i/s - 396.500k +each_key with large hash + 72.080k (± 2.1%) i/s - 361.868k From 60332a4b14b6c3fe2753174f2398b97b1620786d Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Fri, 20 Feb 2015 11:39:49 +1100 Subject: [PATCH 032/258] Changelog for #1870 [skip ci] --- Changelog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Changelog.md b/Changelog.md index b36843919c..263536815a 100644 --- a/Changelog.md +++ b/Changelog.md @@ -7,6 +7,8 @@ Enhancements: * Make `RSpec::Core::Reporter#message` a public supported API. (Jon Rowe, #1866) * Allow custom formatter events to be published via `RSpec::Core::Reporter#publish(event_name, hash_of_attributes)`. (Jon Rowe, #1869) +* Remove dependency on the standard library `Set` and replace with `RSpec::Core::Set`. + (Jon Rowe, #1870) Bugfixes: From 37a29f43bae3691984fba21ac455120c0fda0b9c Mon Sep 17 00:00:00 2001 From: Samuel Esposito Date: Sat, 21 Feb 2015 21:06:48 +0100 Subject: [PATCH 033/258] cleanup specs --- spec/rspec/core/reporter_spec.rb | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/spec/rspec/core/reporter_spec.rb b/spec/rspec/core/reporter_spec.rb index 01bb34c253..8df63a050b 100644 --- a/spec/rspec/core/reporter_spec.rb +++ b/spec/rspec/core/reporter_spec.rb @@ -38,16 +38,6 @@ module RSpec::Core describe 'start' do before { config.start_time = start_time } - it 'notifies formatters of the seed used' do - formatter = double("formatter") - reporter.register_listener formatter, :seed - - expect(formatter).to receive(:seed).with( - an_object_having_attributes(:seed => config.seed, :seed_used? => config.seed_used?) - ) - reporter.start 1 - end - it 'notifies the formatter of start with example count' do formatter = double("formatter") reporter.register_listener formatter, :start @@ -60,11 +50,13 @@ module RSpec::Core reporter.start 3, (start_time + 5) end - it 'notifies the formatter of the seed before notifing of start' do + it 'notifies the formatter of the seed used before notifing of start' do formatter = double("formatter") reporter.register_listener formatter, :seed reporter.register_listener formatter, :start - expect(formatter).to receive(:seed).ordered + expect(formatter).to receive(:seed).ordered.with( + an_object_having_attributes(:seed => config.seed, :seed_used? => config.seed_used?) + ) expect(formatter).to receive(:start).ordered reporter.start 1 end From 2abde01ef86eb62e2c6c49ccf3a52d1486f09ca8 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Sun, 22 Feb 2015 00:04:16 -0800 Subject: [PATCH 034/258] Changelog for #1882. [ci skip] --- Changelog.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index 263536815a..d2211069e1 100644 --- a/Changelog.md +++ b/Changelog.md @@ -10,9 +10,11 @@ Enhancements: * Remove dependency on the standard library `Set` and replace with `RSpec::Core::Set`. (Jon Rowe, #1870) -Bugfixes: +Bug Fixes: * Handle invalid UTF-8 strings within exception methods. (Benjamin Fleischer, #1760) +* Notify start-of-run seed _before_ `start` notification rather than + _after_ so that formatters like Fuubar work properly. (Samuel Esposito, #1882). ### 3.2.0 / 2015-02-03 [Full Changelog](http://github.com/rspec/rspec-core/compare/v3.1.7...v3.2.0) From 835b7a9a3a35b559eadebaef846e86491b3183eb Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Sun, 22 Feb 2015 00:07:03 -0800 Subject: [PATCH 035/258] Remove excess period. [ci skip] --- Changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index d2211069e1..90a843b0df 100644 --- a/Changelog.md +++ b/Changelog.md @@ -14,7 +14,7 @@ Bug Fixes: * Handle invalid UTF-8 strings within exception methods. (Benjamin Fleischer, #1760) * Notify start-of-run seed _before_ `start` notification rather than - _after_ so that formatters like Fuubar work properly. (Samuel Esposito, #1882). + _after_ so that formatters like Fuubar work properly. (Samuel Esposito, #1882) ### 3.2.0 / 2015-02-03 [Full Changelog](http://github.com/rspec/rspec-core/compare/v3.1.7...v3.2.0) From 67d15343a28ba2d1cbcb63e932e6c48cfaf41c21 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Thu, 19 Feb 2015 09:01:51 -0800 Subject: [PATCH 036/258] Remove duplicate spec. This exact spec is duplicated on line 775. I suspect this was written for the old `--line-number` CLI flag and got updated to the `file_path:line_num` form when we dropped support for `--line-number`, creating the duplication. --- spec/rspec/core/configuration_spec.rb | 7 ------- 1 file changed, 7 deletions(-) diff --git a/spec/rspec/core/configuration_spec.rb b/spec/rspec/core/configuration_spec.rb index 331d734ee3..131d75f572 100644 --- a/spec/rspec/core/configuration_spec.rb +++ b/spec/rspec/core/configuration_spec.rb @@ -752,13 +752,6 @@ def specify_consistent_ordering_of_files_to_run end end - describe "path with line number" do - it "assigns the line number as a location filter" do - assign_files_or_directories_to_run "path/to/a_spec.rb:37" - expect(inclusion_filter).to eq({:locations => {File.expand_path("path/to/a_spec.rb") => [37]}}) - end - end - context "with full_description set" do it "overrides filters" do config.filter_run :focused => true From 057e729f087660a45dd9569f7888fca28d6380ae Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 6 Feb 2015 17:12:39 -0800 Subject: [PATCH 037/258] Add ids to examples and groups. This allows us to uniquely identify any example or group. --- lib/rspec/core/example.rb | 10 +++- lib/rspec/core/example_group.rb | 19 +++++++- lib/rspec/core/metadata.rb | 26 ++++++++-- lib/rspec/core/world.rb | 7 +++ spec/rspec/core/metadata_spec.rb | 84 ++++++++++++++++++++++++++++++++ 5 files changed, 139 insertions(+), 7 deletions(-) diff --git a/lib/rspec/core/example.rb b/lib/rspec/core/example.rb index c2b4f6ee90..04eb19aa58 100644 --- a/lib/rspec/core/example.rb +++ b/lib/rspec/core/example.rb @@ -101,6 +101,12 @@ def rerun_argument end end + # @return [String] the unique id of this example. Pass + # this at the command line to re-run this exact example. + def id + Metadata.id_from(metadata) + end + # @attr_reader # # Returns the first exception raised in the context of running this @@ -138,7 +144,9 @@ def initialize(example_group_class, description, user_metadata, example_block=ni @example_block = example_block @metadata = Metadata::ExampleHash.create( - @example_group_class.metadata, user_metadata, description, example_block + @example_group_class.metadata, user_metadata, + example_group_class.method(:next_runnable_index_for), + description, example_block ) @example_group_instance = @exception = nil diff --git a/lib/rspec/core/example_group.rb b/lib/rspec/core/example_group.rb index d566093153..de555e280a 100644 --- a/lib/rspec/core/example_group.rb +++ b/lib/rspec/core/example_group.rb @@ -386,7 +386,9 @@ def self.set_it_up(description, *args, &example_group_block) user_metadata = Metadata.build_hash_from(args) @metadata = Metadata::ExampleGroupHash.create( - superclass_metadata, user_metadata, description, *args, &example_group_block + superclass_metadata, user_metadata, + superclass.method(:next_runnable_index_for), + description, *args, &example_group_block ) hooks.register_globals(self, RSpec.configuration.hooks) @@ -414,6 +416,15 @@ def self.children @children ||= [] end + # @private + def self.next_runnable_index_for(file) + if self == ExampleGroup + RSpec.world.num_example_groups_defined_in(file) + else + children.count + examples.count + end + 1 + end + # @private def self.descendants @_descendants ||= [self] + FlatMap.flat_map(children, &:descendants) @@ -573,6 +584,12 @@ def self.declaration_line_numbers FlatMap.flat_map(children, &:declaration_line_numbers) end + # @return [String] the unique id of this example group. Pass + # this at the command line to re-run this exact example group. + def self.id + Metadata.id_from(metadata) + end + # @private def self.top_level_description parent_groups.last.description diff --git a/lib/rspec/core/metadata.rb b/lib/rspec/core/metadata.rb index 6f71c4bf09..d2e348ad13 100644 --- a/lib/rspec/core/metadata.rb +++ b/lib/rspec/core/metadata.rb @@ -105,15 +105,21 @@ def self.backtrace_from(block) [block.source_location.join(':')] end + # @private + def self.id_from(metadata) + "#{metadata[:rerun_file_path]}[#{metadata[:scoped_id]}]" + end + # @private # Used internally to populate metadata hashes with computed keys # managed by RSpec. class HashPopulator attr_reader :metadata, :user_metadata, :description_args, :block - def initialize(metadata, user_metadata, description_args, block) + def initialize(metadata, user_metadata, index_provider, description_args, block) @metadata = metadata @user_metadata = user_metadata + @index_provider = index_provider @description_args = description_args @block = block end @@ -151,6 +157,8 @@ def populate_location_attributes metadata[:line_number] = line_number.to_i metadata[:location] = "#{relative_file_path}:#{line_number}" metadata[:absolute_file_path] = File.expand_path(relative_file_path) + metadata[:rerun_file_path] ||= relative_file_path + metadata[:scoped_id] = build_scoped_id_for(relative_file_path) end def file_path_and_line_number_from(backtrace) @@ -173,6 +181,12 @@ def build_description_from(parent_description=nil, my_description=nil) (parent_description.to_s + separator) << my_description.to_s end + def build_scoped_id_for(file_path) + index = @index_provider.call(file_path).to_s + parent_scoped_id = metadata.fetch(:scoped_id) { return index } + "#{parent_scoped_id}:#{index}" + end + def ensure_valid_user_keys RESERVED_KEYS.each do |key| next unless user_metadata.key?(key) @@ -196,7 +210,7 @@ def ensure_valid_user_keys # @private class ExampleHash < HashPopulator - def self.create(group_metadata, user_metadata, description, block) + def self.create(group_metadata, user_metadata, index, description, block) example_metadata = group_metadata.dup group_metadata = Hash.new(&ExampleGroupHash.backwards_compatibility_default_proc do |hash| hash[:parent_example_group] @@ -208,7 +222,7 @@ def self.create(group_metadata, user_metadata, description, block) example_metadata.delete(:parent_example_group) description_args = description.nil? ? [] : [description] - hash = new(example_metadata, user_metadata, description_args, block) + hash = new(example_metadata, user_metadata, index, description_args, block) hash.populate hash.metadata end @@ -229,7 +243,7 @@ def full_description # @private class ExampleGroupHash < HashPopulator - def self.create(parent_group_metadata, user_metadata, *args, &block) + def self.create(parent_group_metadata, user_metadata, example_group_index, *args, &block) group_metadata = hash_with_backwards_compatibility_default_proc if parent_group_metadata @@ -237,7 +251,7 @@ def self.create(parent_group_metadata, user_metadata, *args, &block) group_metadata[:parent_example_group] = parent_group_metadata end - hash = new(group_metadata, user_metadata, args, block) + hash = new(group_metadata, user_metadata, example_group_index, args, block) hash.populate hash.metadata end @@ -313,9 +327,11 @@ def full_description :execution_result, :file_path, :absolute_file_path, + :rerun_file_path, :full_description, :line_number, :location, + :scoped_id, :block ] end diff --git a/lib/rspec/core/world.rb b/lib/rspec/core/world.rb index 307538eecc..0791c754dd 100644 --- a/lib/rspec/core/world.rb +++ b/lib/rspec/core/world.rb @@ -13,6 +13,7 @@ class World def initialize(configuration=RSpec.configuration) @configuration = configuration @example_groups = [] + @example_group_counts_by_spec_file = Hash.new(0) @filtered_examples = Hash.new do |hash, group| hash[group] = begin examples = group.examples.dup @@ -55,9 +56,15 @@ def filter_manager # Register an example group. def register(example_group) example_groups << example_group + @example_group_counts_by_spec_file[example_group.metadata[:file_path]] += 1 example_group end + # @private + def num_example_groups_defined_in(file) + @example_group_counts_by_spec_file[file] + end + # @private def shared_example_group_registry @shared_example_group_registry ||= SharedExampleGroup::Registry.new diff --git a/spec/rspec/core/metadata_spec.rb b/spec/rspec/core/metadata_spec.rb index b646a7dfc7..b58125200b 100644 --- a/spec/rspec/core/metadata_spec.rb +++ b/spec/rspec/core/metadata_spec.rb @@ -159,6 +159,90 @@ def metadata_for(*args) end end + describe ":id" do + define :have_id_with do |scoped_id| + expected_id = "#{Metadata.relative_path(__FILE__)}[#{scoped_id}]" + + match do |group_or_example| + group_or_example.metadata[:scoped_id] == scoped_id && + group_or_example.id == expected_id + end + + failure_message do |group_or_example| + "expected #{group_or_example.inspect}\n" \ + " to have id: #{expected_id}\n" \ + " but had id: #{group_or_example.id}\n" \ + " and have scoped id: #{scoped_id}\n" \ + " but had scoped id: #{group_or_example.metadata[:scoped_id]}" + end + end + + context "on a top-level group" do + it "is set to file[]" do + expect(RSpec.describe).to have_id_with("1") + expect(RSpec.describe).to have_id_with("2") + end + + it "starts the count at 1 for each file" do + instance_eval <<-EOS, "spec_1.rb", 1 + $group_1 = RSpec.describe + $group_2 = RSpec.describe + EOS + + instance_eval <<-EOS, "spec_2.rb", 1 + $group_3 = RSpec.describe + $group_4 = RSpec.describe + EOS + + expect($group_1.id).to end_with("spec_1.rb[1]") + expect($group_2.id).to end_with("spec_1.rb[2]") + expect($group_3.id).to end_with("spec_2.rb[1]") + expect($group_4.id).to end_with("spec_2.rb[2]") + end + end + + context "on a nested group" do + it "is set to file[:]" do + top_level_group = RSpec.describe + expect(top_level_group.describe).to have_id_with("1:1") + expect(top_level_group.describe).to have_id_with("1:2") + end + end + + context "on an example" do + it "is set to file[:]" do + group = RSpec.describe + expect(group.example).to have_id_with("1:1") + expect(group.example).to have_id_with("1:2") + end + end + + context "when examples are interleaved with example groups" do + it "counts both when assigning the index" do + group = RSpec.describe + expect(group.example ).to have_id_with("1:1") + expect(group.describe).to have_id_with("1:2") + expect(group.example ).to have_id_with("1:3") + expect(group.example ).to have_id_with("1:4") + expect(group.describe).to have_id_with("1:5") + end + end + + context "on an example defined in a shared group defined in a separate file" do + it "uses the host group's file name as the prefix" do + # Using eval in order to make ruby think this got defined in another file. + instance_eval <<-EOS, "some/external/file.rb", 1 + RSpec.shared_examples "shared" do + example { } + end + EOS + + group = RSpec.describe { include_examples "shared" } + expect(group.examples.first.id).to start_with(Metadata.relative_path(__FILE__)) + end + end + end + describe ":shared_group_inclusion_backtrace" do context "for an example group" do it "is not set since we do not yet need it internally (but we can add it in the future if needed)" do From 911601d71795f8daa90b956f555cc57f71b0e2de Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Tue, 17 Feb 2015 17:33:03 -0800 Subject: [PATCH 038/258] Add support to filter based on example/group ids. --- lib/rspec/core/configuration.rb | 3 + lib/rspec/core/filter_manager.rb | 25 ++++- lib/rspec/core/metadata_filter.rb | 9 ++ spec/integration/filtering_spec.rb | 53 +++++++++++ spec/rspec/core/configuration_spec.rb | 36 +++++++ spec/rspec/core/filter_manager_spec.rb | 121 ++++++++++++++++++++---- spec/rspec/core/metadata_filter_spec.rb | 31 ++++++ 7 files changed, 257 insertions(+), 21 deletions(-) diff --git a/lib/rspec/core/configuration.rb b/lib/rspec/core/configuration.rb index f5dd8f9543..7078506945 100644 --- a/lib/rspec/core/configuration.rb +++ b/lib/rspec/core/configuration.rb @@ -1613,6 +1613,9 @@ def extract_location(path) captures = match.captures path, lines = captures[0], captures[1][1..-1].split(":").map { |n| n.to_i } filter_manager.add_location path, lines + else + path, scoped_ids = path.split(/[\[\]]/) + filter_manager.add_ids(path, scoped_ids.split(/\s*,\s*/)) if scoped_ids end return [] if path == default_path diff --git a/lib/rspec/core/filter_manager.rb b/lib/rspec/core/filter_manager.rb index b3e3217e20..a90718f59c 100644 --- a/lib/rspec/core/filter_manager.rb +++ b/lib/rspec/core/filter_manager.rb @@ -16,9 +16,15 @@ def add_location(file_path, line_numbers) # locations is a hash of expanded paths to arrays of line # numbers to match against. e.g. # { "path/to/file.rb" => [37, 42] } - locations = inclusions.delete(:locations) || Hash.new { |h, k| h[k] = [] } - locations[File.expand_path(file_path)].push(*line_numbers) - inclusions.add(:locations => locations) + add_path_to_arrays_filter(:locations, File.expand_path(file_path), line_numbers) + end + + def add_ids(rerun_path, scoped_ids) + # ids is a hash of relative paths to arrays of ids + # to match against. e.g. + # { "./path/to/file.rb" => ["1:1", "2:4"] } + rerun_path = Metadata.relative_path(File.expand_path rerun_path) + add_path_to_arrays_filter(:ids, rerun_path, scoped_ids) end def empty? @@ -32,7 +38,9 @@ def prune(examples) examples.select { |e| include?(e) } else locations = inclusions.fetch(:locations) { Hash.new([]) } - examples.select { |e| priority_include?(e, locations) || (!exclude?(e) && include?(e)) } + ids = inclusions.fetch(:ids) { Hash.new([]) } + + examples.select { |e| priority_include?(e, ids, locations) || (!exclude?(e) && include?(e)) } end end @@ -62,6 +70,12 @@ def include_with_low_priority(*args) private + def add_path_to_arrays_filter(filter_key, path, values) + filter = inclusions.delete(filter_key) || Hash.new { |h, k| h[k] = [] } + filter[path].concat(values) + inclusions.add(filter_key => filter) + end + def exclude?(example) exclusions.include_example?(example) end @@ -82,7 +96,8 @@ def prune_conditionally_filtered_examples(examples) # and there is a `:slow => true` exclusion filter), but only for specs # defined in the same file as the location filters. Excluded specs in # other files should still be excluded. - def priority_include?(example, locations) + def priority_include?(example, ids, locations) + return true if MetadataFilter.filter_applies?(:ids, ids, example.metadata) return false if locations[example.metadata[:absolute_file_path]].empty? MetadataFilter.filter_applies?(:locations, locations, example.metadata) end diff --git a/lib/rspec/core/metadata_filter.rb b/lib/rspec/core/metadata_filter.rb index ffe8c86258..e529e3ae57 100644 --- a/lib/rspec/core/metadata_filter.rb +++ b/lib/rspec/core/metadata_filter.rb @@ -17,6 +17,7 @@ def filter_applies?(key, value, metadata) silence_metadata_example_group_deprecations do return filter_applies_to_any_value?(key, value, metadata) if Array === metadata[key] && !(Proc === value) return location_filter_applies?(value, metadata) if key == :locations + return id_filter_applies?(value, metadata) if key == :ids return filters_apply?(key, value, metadata) if Hash === value return false unless metadata.key?(key) @@ -42,6 +43,14 @@ def filter_applies_to_any_value?(key, value, metadata) metadata[key].any? { |v| filter_applies?(key, v, key => value) } end + def id_filter_applies?(rerun_paths_to_scoped_ids, metadata) + scoped_ids = rerun_paths_to_scoped_ids.fetch(metadata[:rerun_file_path]) { return false } + + Metadata.ascend(metadata).any? do |meta| + scoped_ids.include?(meta[:scoped_id]) + end + end + def location_filter_applies?(locations, metadata) line_numbers = example_group_declaration_lines(locations, metadata) line_numbers.empty? || line_number_filter_applies?(line_numbers, metadata) diff --git a/spec/integration/filtering_spec.rb b/spec/integration/filtering_spec.rb index 46feb427a7..b2238cf7eb 100644 --- a/spec/integration/filtering_spec.rb +++ b/spec/integration/filtering_spec.rb @@ -123,4 +123,57 @@ def run_rerun_command_for_failing_spec expect(last_cmd_stdout).not_to match(/fails/) end end + + context "passing example ids at the command line" do + it "selects matching examples" do + write_file_formatted "spec/file_1_spec.rb", """ + RSpec.describe 'File 1' do + 1.upto(3) do |i| + example('ex ' + i.to_s) { expect(i).to be_odd } + end + end + """ + + write_file_formatted "spec/file_2_spec.rb", """ + RSpec.describe 'File 2' do + 1.upto(3) do |i| + example('ex ' + i.to_s) { expect(i).to be_even } + end + end + """ + + # Using the form that Metadata.relative_path returns... + run_command "./spec/file_1_spec.rb[1:1,1:3] ./spec/file_2_spec.rb[1:2]" + expect(last_cmd_stdout).to match(/3 examples, 0 failures/) + + # Without the leading `.`... + run_command "spec/file_1_spec.rb[1:1,1:3] spec/file_2_spec.rb[1:2]" + expect(last_cmd_stdout).to match(/3 examples, 0 failures/) + + # Using absolute paths... + spec_root = in_current_dir { File.expand_path("spec") } + run_command "#{spec_root}/file_1_spec.rb[1:1,1:3] #{spec_root}/file_2_spec.rb[1:2]" + expect(last_cmd_stdout).to match(/3 examples, 0 failures/) + end + + it "selects matching example groups" do + write_file_formatted "spec/file_1_spec.rb", """ + RSpec.describe 'Group 1' do + example { fail } + + context 'nested 1' do + it { } + it { } + end + + context 'nested 2' do + example { fail } + end + end + """ + + run_command "./spec/file_1_spec.rb[1:2]" + expect(last_cmd_stdout).to match(/2 examples, 0 failures/) + end + end end diff --git a/spec/rspec/core/configuration_spec.rb b/spec/rspec/core/configuration_spec.rb index 131d75f572..a2e49d4f60 100644 --- a/spec/rspec/core/configuration_spec.rb +++ b/spec/rspec/core/configuration_spec.rb @@ -796,6 +796,42 @@ def specify_consistent_ordering_of_files_to_run end end + context "with an example id" do + it "assigns the file and id as an ids filter" do + assign_files_or_directories_to_run "./path/to/a_spec.rb[1:2]" + expect(inclusion_filter).to eq(:ids => { "./path/to/a_spec.rb" => ["1:2"] }) + end + end + + context "with a single file with multiple example ids" do + it "assigns the file and ids as an ids filter" do + assign_files_or_directories_to_run "./path/to/a_spec.rb[1:2,1:3]" + expect(inclusion_filter).to eq(:ids => { "./path/to/a_spec.rb" => ["1:2", "1:3"] }) + end + + it "ignores whitespace between scoped ids" do + assign_files_or_directories_to_run "./path/to/a_spec.rb[1:2 , 1:3]" + expect(inclusion_filter).to eq(:ids => { "./path/to/a_spec.rb" => ["1:2", "1:3"] }) + end + end + + context "with multiple files with ids" do + it "assigns all of them to the ids filter" do + assign_files_or_directories_to_run "./path/to/a_spec.rb[1:2,1:3]", "./path/to/b_spec.rb[1:4]" + expect(inclusion_filter).to eq(:ids => { + "./path/to/a_spec.rb" => ["1:2", "1:3"], + "./path/to/b_spec.rb" => ["1:4"] + }) + end + end + + context "with the same file specified multiple times with different scoped ids" do + it "unions all the ids" do + assign_files_or_directories_to_run "./path/to/a_spec.rb[1:2]", "./path/to/a_spec.rb[1:3]" + expect(inclusion_filter).to eq(:ids => { "./path/to/a_spec.rb" => ["1:2", "1:3"] }) + end + end + it "assigns the example name as the filter on description" do config.full_description = "foo" expect(inclusion_filter).to eq({:full_description => /foo/}) diff --git a/spec/rspec/core/filter_manager_spec.rb b/spec/rspec/core/filter_manager_spec.rb index 676a6ba3f4..49f693ef2d 100644 --- a/spec/rspec/core/filter_manager_spec.rb +++ b/spec/rspec/core/filter_manager_spec.rb @@ -105,24 +105,64 @@ def example_with(*args) RSpec.describe("group", *args).example("example") end - it "prefers location to exclusion filter" do - group = RSpec.describe("group") - included = group.example("include", :slow => true) {} - excluded = group.example("exclude") {} - filter_manager.add_location(__FILE__, [__LINE__ - 2]) - filter_manager.exclude_with_low_priority :slow => true - expect(filter_manager.prune([included, excluded])).to eq([included]) + shared_examples_for "example identification filter preference" do |type| + it "prefers #{type} filter to exclusion filter" do + group = RSpec.describe("group") + included = group.example("include", :slow => true) {}; line = __LINE__ + excluded = group.example("exclude") {} + + add_filter(:line_number => line, :scoped_id => "1:1") + filter_manager.exclude_with_low_priority :slow => true + + expect(filter_manager.prune([included, excluded])).to eq([included]) + end + + it "prefers #{type} on entire group to exclusion filter on a nested example" do + # We way want to change this behaviour in future, see: + # https://github.com/rspec/rspec-core/issues/779 + group = RSpec.describe("group"); line = __LINE__ + included = group.example("include", :slow => true) + excluded = RSpec.describe.example + + add_filter(:line_number => line, :scoped_id => "1") + filter_manager.exclude_with_low_priority :slow => true + + expect(filter_manager.prune([included, excluded])).to eq([included]) + end end - it "prefers location to exclusion filter on entire group" do - # We way want to change this behaviour in future, see: - # https://github.com/rspec/rspec-core/issues/779 - group = RSpec.describe("group") - included = group.example("include", :slow => true) {} - excluded = example_with - filter_manager.add_location(__FILE__, [__LINE__ - 3]) - filter_manager.exclude_with_low_priority :slow => true - expect(filter_manager.prune([included, excluded])).to eq([included]) + describe "location filtering" do + include_examples "example identification filter preference", :location do + def add_filter(options) + filter_manager.add_location(__FILE__, [options.fetch(:line_number)]) + end + end + end + + describe "id filtering" do + include_examples "example identification filter preference", :id do + def add_filter(options) + filter_manager.add_ids(__FILE__, [options.fetch(:scoped_id)]) + end + end + end + + context "with a location and an id filter" do + it 'takes the set union of matched examples' do + group = RSpec.describe("group") + + matches_id = group.example + matches_line_number = group.example; line_1 = __LINE__ + matches_both = group.example; line_2 = __LINE__ + matches_neither = group.example + + filter_manager.add_ids(__FILE__, ["1:1", "1:3"]) + filter_manager.add_location(__FILE__, [line_1, line_2]) + + expect(filter_manager.prune([ + matches_id, matches_line_number, matches_both, matches_neither + ])).to eq([matches_id, matches_line_number, matches_both]) + end end context "with examples from multiple spec source files" do @@ -211,6 +251,55 @@ def example_with(*args) expect(filter_manager.prune(examples)).to contain_exactly(included_1, included_2) end end + + context "with :id filters" do + it 'selects only the matched example when a single example id is given' do + ex_1 = ex_2 = nil + RSpec.describe do + ex_1 = example + ex_2 = example + end + + filter_manager.add_ids(Metadata.relative_path(__FILE__), %w[ 1:2 ]) + expect(filter_manager.prune([ex_1, ex_2])).to eq([ex_2]) + end + + it 'can work with absolute file paths' do + ex_1 = ex_2 = nil + RSpec.describe do + ex_1 = example + ex_2 = example + end + + filter_manager.add_ids(File.expand_path(__FILE__), %w[ 1:2 ]) + expect(filter_manager.prune([ex_1, ex_2])).to eq([ex_2]) + end + + it "can work with relative paths that lack the leading `.`" do + path = Metadata.relative_path(__FILE__).sub(/^\.\//, '') + + ex_1 = ex_2 = nil + RSpec.describe do + ex_1 = example + ex_2 = example + end + + filter_manager.add_ids(path, %w[ 1:2 ]) + expect(filter_manager.prune([ex_1, ex_2])).to eq([ex_2]) + end + + it 'can select groups' do + ex_1 = ex_2 = ex_3 = nil + RSpec.describe { ex_1 = example } + RSpec.describe do + ex_2 = example + ex_3 = example + end + + filter_manager.add_ids(Metadata.relative_path(__FILE__), %w[ 2 ]) + expect(filter_manager.prune([ex_1, ex_2, ex_3])).to eq([ex_2, ex_3]) + end + end end describe "#inclusions#description" do diff --git a/spec/rspec/core/metadata_filter_spec.rb b/spec/rspec/core/metadata_filter_spec.rb index 98b77b32fb..a35a2c61e9 100644 --- a/spec/rspec/core/metadata_filter_spec.rb +++ b/spec/rspec/core/metadata_filter_spec.rb @@ -107,6 +107,37 @@ def filter_applies?(key, value, metadata) }.to raise_error(ArgumentError) end + context "with an :ids filter" do + it 'matches examples with a matching id and rerun_file_path' do + metadata = { :scoped_id => "1:2", :rerun_file_path => "some/file" } + expect(filter_applies?(:ids, { "some/file" => ["1:2"] }, metadata)).to be true + end + + it 'does not match examples without a matching id' do + metadata = { :scoped_id => "1:2", :rerun_file_path => "some/file" } + expect(filter_applies?(:ids, { "some/file" => ["1:3"] }, metadata)).to be false + end + + it 'does not match examples without a matching rerun_file_path' do + metadata = { :scoped_id => "1:2", :rerun_file_path => "some/file" } + expect(filter_applies?(:ids, { "some/file_2" => ["1:2"] }, metadata)).to be false + end + + it 'matches the scoped id from a parent example group' do + metadata = { :scoped_id => "1:2", :rerun_file_path => "some/file", :example_group => { :scoped_id => "1" } } + expect(filter_applies?(:ids, { "some/file" => ["1"] }, metadata)).to be true + expect(filter_applies?(:ids, { "some/file" => ["2"] }, metadata)).to be false + end + + it 'matches only on entire id segments so (1 is not treated as a parent group of 11)' do + metadata = { :scoped_id => "1:2", :rerun_file_path => "some/file", :example_group => { :scoped_id => "1" } } + expect(filter_applies?(:ids, { "some/file" => ["1"] }, metadata)).to be true + + metadata = { :scoped_id => "11", :rerun_file_path => "some/file" } + expect(filter_applies?(:ids, { "some/file" => ["1"] }, metadata)).to be false + end + end + context "with a nested hash" do it 'matches when the nested entry matches' do metadata = { :foo => { :bar => "words" } } From ea3d536e30014808d0b522f24b66fdd6f3e8ae5e Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Sun, 22 Feb 2015 00:01:36 -0800 Subject: [PATCH 039/258] Use id in rerun command when the location identifies multiple examples. --- lib/rspec/core/example.rb | 21 +++++-- lib/rspec/core/notifications.rb | 21 ++++++- lib/rspec/core/world.rb | 6 ++ spec/integration/filtering_spec.rb | 9 +-- .../formatters/base_text_formatter_spec.rb | 55 ++++++++++++------- spec/rspec/core/world_spec.rb | 27 +++++++++ spec/support/formatter_support.rb | 2 +- 7 files changed, 110 insertions(+), 31 deletions(-) diff --git a/lib/rspec/core/example.rb b/lib/rspec/core/example.rb index 04eb19aa58..062076b8ee 100644 --- a/lib/rspec/core/example.rb +++ b/lib/rspec/core/example.rb @@ -92,15 +92,26 @@ def inspect_output inspect_output end - # Returns the argument that can be passed to the `rspec` command to rerun this example. - def rerun_argument - loaded_spec_files = RSpec.configuration.loaded_spec_files + # Returns the location-based argument that can be passed to the `rspec` command to rerun this example. + def location_rerun_argument + @location_rerun_argument ||= begin + loaded_spec_files = RSpec.configuration.loaded_spec_files - Metadata.ascending(metadata) do |meta| - return meta[:location] if loaded_spec_files.include?(meta[:absolute_file_path]) + Metadata.ascending(metadata) do |meta| + return meta[:location] if loaded_spec_files.include?(meta[:absolute_file_path]) + end end end + # Returns the location-based argument that can be passed to the `rspec` command to rerun this example. + # + # @deprecated Use {#location_rerun_argument} instead. + # @note If there are multiple examples identified by this location, they will use {#id} + # to rerun instead, but this method will still return the location (that's why it is deprecated!). + def rerun_argument + location_rerun_argument + end + # @return [String] the unique id of this example. Pass # this at the command line to re-run this exact example. def id diff --git a/lib/rspec/core/notifications.rb b/lib/rspec/core/notifications.rb index b88d54ade4..dbeea85751 100644 --- a/lib/rspec/core/notifications.rb +++ b/lib/rspec/core/notifications.rb @@ -485,7 +485,7 @@ def colorized_totals_line(colorizer=::RSpec::Core::Formatters::ConsoleCodes) def colorized_rerun_commands(colorizer=::RSpec::Core::Formatters::ConsoleCodes) "\nFailed examples:\n\n" + failed_examples.map do |example| - colorizer.wrap("rspec #{example.rerun_argument}", RSpec.configuration.failure_color) + " " + + colorizer.wrap("rspec #{rerun_argument_for(example)}", RSpec.configuration.failure_color) + " " + colorizer.wrap("# #{example.full_description}", RSpec.configuration.detail_color) end.join("\n") end @@ -515,6 +515,25 @@ def fully_formatted(colorizer=::RSpec::Core::Formatters::ConsoleCodes) formatted end + + private + + def rerun_argument_for(example) + location = example.location_rerun_argument + duplicate_rerun_locations.include?(location) ? example.id : location + end + + def duplicate_rerun_locations + @duplicate_rerun_locations ||= begin + locations = RSpec.world.all_examples.map(&:location_rerun_argument) + + Set.new.tap do |s| + locations.group_by { |l| l }.each do |l, ls| + s << l if ls.count > 1 + end + end + end + end end # The `ProfileNotification` holds information about the results of running a diff --git a/lib/rspec/core/world.rb b/lib/rspec/core/world.rb index 0791c754dd..0e95b421ef 100644 --- a/lib/rspec/core/world.rb +++ b/lib/rspec/core/world.rb @@ -88,6 +88,12 @@ def example_count(groups=example_groups) inject(0) { |a, e| a + e.filtered_examples.size } end + # @private + def all_examples + flattened_groups = FlatMap.flat_map(example_groups) { |g| g.descendants } + FlatMap.flat_map(flattened_groups) { |g| g.examples } + end + # @api private # # Find line number of previous declaration. diff --git a/spec/integration/filtering_spec.rb b/spec/integration/filtering_spec.rb index b2238cf7eb..ed1e2230ab 100644 --- a/spec/integration/filtering_spec.rb +++ b/spec/integration/filtering_spec.rb @@ -6,8 +6,9 @@ it 'prints a rerun command for shared examples in external files that works to rerun' do write_file "spec/support/shared_examples.rb", """ - RSpec.shared_examples 'a failing example' do - example { expect(1).to eq(2) } + RSpec.shared_examples 'with a failing example' do + example { expect(1).to eq(2) } # failing + example { expect(2).to eq(2) } # passing end """ @@ -15,7 +16,7 @@ load File.expand_path('../support/shared_examples.rb', __FILE__) RSpec.describe 'A group with shared examples' do - include_examples 'a failing example' + include_examples 'with a failing example' end RSpec.describe 'A group with a passing example' do @@ -24,7 +25,7 @@ """ run_command "" - expect(last_cmd_stdout).to include("2 examples, 1 failure") + expect(last_cmd_stdout).to include("3 examples, 1 failure") run_rerun_command_for_failing_spec expect(last_cmd_stdout).to include("1 example, 1 failure") # There was originally a bug when doing it again... diff --git a/spec/rspec/core/formatters/base_text_formatter_spec.rb b/spec/rspec/core/formatters/base_text_formatter_spec.rb index 6c05ee76ec..a5831a2aec 100644 --- a/spec/rspec/core/formatters/base_text_formatter_spec.rb +++ b/spec/rspec/core/formatters/base_text_formatter_spec.rb @@ -30,33 +30,48 @@ expect(output.string).to match("2 examples, 2 failures, 2 pending") end - it "includes command to re-run each failed example" do - example_group = RSpec.describe("example group") do - it("fails") { fail } + describe "rerun command for failed examples" do + it "uses the location to identify the example" do + example_group = RSpec.describe("example group") do + it("fails") { fail } + end + line = __LINE__ - 2 + + expect(output_from_running example_group).to include("rspec #{RSpec::Core::Metadata::relative_path("#{__FILE__}:#{line}")} # example group fails") end - line = __LINE__ - 2 - expect(output_from_running example_group).to include("rspec #{RSpec::Core::Metadata::relative_path("#{__FILE__}:#{line}")} # example group fails") - end + context "for an example defined in an file required by the user rather than loaded by rspec" do + it "looks through ancestor metadata to find a workable re-run command" do + line = __LINE__ + 1 + example_group = RSpec.describe("example group") do + # Using eval in order to make it think this got defined in an external file. + instance_eval "it('fails') { fail }", "some/external/file.rb", 1 + end - context "for an example defined in an file required by the user rather than loaded by rspec" do - it "looks through ancestor metadata to find a workable re-run command" do - line = __LINE__ + 1 - example_group = RSpec.describe("example group") do - # Using eval in order to make it think this got defined in an external file. - instance_eval "it('fails') { fail }", "some/external/file.rb", 1 + expect(output_from_running example_group).to include("rspec #{RSpec::Core::Metadata::relative_path("#{__FILE__}:#{line}")} # example group fails") end + end - expect(output_from_running example_group).to include("rspec #{RSpec::Core::Metadata::relative_path("#{__FILE__}:#{line}")} # example group fails") + context "for an example that is not uniquely identified by the location" do + it "uses the id instead" do + example_group = RSpec.describe("example group") do + 1.upto(2) do |i| + it("compares #{i} against 2") { expect(i).to eq(2) } + end + end + + expect(output_from_running example_group).to include("rspec #{RSpec::Core::Metadata::relative_path("#{__FILE__}[1:1]")} # example group compares 1 against 2") + end end - end - def output_from_running(example_group) - allow(RSpec.configuration).to receive(:loaded_spec_files) { RSpec::Core::Set.new([File.expand_path(__FILE__)]) } - example_group.run(reporter) - examples = example_group.examples - send_notification :dump_summary, summary_notification(1, examples, examples, [], 0) - output.string + def output_from_running(example_group) + allow(RSpec.configuration).to receive(:loaded_spec_files) { RSpec::Core::Set.new([File.expand_path(__FILE__)]) } + example_group.run(reporter) + examples = example_group.examples + failed = examples.select { |e| e.execution_result.status == :failed } + send_notification :dump_summary, summary_notification(1, examples, failed, [], 0) + output.string + end end end diff --git a/spec/rspec/core/world_spec.rb b/spec/rspec/core/world_spec.rb index a837a13c05..318f579a6d 100644 --- a/spec/rspec/core/world_spec.rb +++ b/spec/rspec/core/world_spec.rb @@ -23,6 +23,33 @@ module RSpec::Core end end + describe "#all_examples" do + it "contains all examples from all levels of nesting" do + RSpec.describe do + example("ex1") + + context "nested" do + example("ex2") + + context "nested" do + example("ex3") + example("ex4") + end + end + + example("ex5") + end + + RSpec.describe do + example("ex6") + end + + expect(RSpec.world.all_examples.map(&:description)).to match_array(%w[ + ex1 ex2 ex3 ex4 ex5 ex6 + ]) + end + end + describe "#preceding_declaration_line (again)" do let(:group) do RSpec.describe("group") do diff --git a/spec/support/formatter_support.rb b/spec/support/formatter_support.rb index b8edddc4a3..2b9dbe0840 100644 --- a/spec/support/formatter_support.rb +++ b/spec/support/formatter_support.rb @@ -214,7 +214,7 @@ def example :full_description => "Example", :execution_result => result, :location => "", - :rerun_argument => "", + :location_rerun_argument => "", :metadata => { :shared_group_inclusion_backtrace => [] } From b77d5bd0c56b8be06ffaf31408fc613448757d1c Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Sun, 22 Feb 2015 21:01:18 -0800 Subject: [PATCH 040/258] Add some missing keys to RESERVED_KEYS. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …and add a spec enforcing that it is always up-to-date. --- lib/rspec/core/metadata.rb | 5 ++++- spec/rspec/core/metadata_spec.rb | 9 +++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/rspec/core/metadata.rb b/lib/rspec/core/metadata.rb index d2e348ad13..98d810c094 100644 --- a/lib/rspec/core/metadata.rb +++ b/lib/rspec/core/metadata.rb @@ -322,6 +322,8 @@ def full_description # @private RESERVED_KEYS = [ :description, + :description_args, + :described_class, :example_group, :parent_example_group, :execution_result, @@ -332,7 +334,8 @@ def full_description :line_number, :location, :scoped_id, - :block + :block, + :shared_group_inclusion_backtrace ] end diff --git a/spec/rspec/core/metadata_spec.rb b/spec/rspec/core/metadata_spec.rb index b58125200b..606bd15575 100644 --- a/spec/rspec/core/metadata_spec.rb +++ b/spec/rspec/core/metadata_spec.rb @@ -31,6 +31,15 @@ module Core end + specify 'RESERVED_KEYS contains all keys assigned by RSpec (and vice versa)' do + group = RSpec.describe("group") + example = group.example("example") { } + nested_group = group.describe("nested") + + assigned_keys = group.metadata.keys | example.metadata.keys | nested_group.metadata.keys + expect(RSpec::Core::Metadata::RESERVED_KEYS).to match_array(assigned_keys) + end + context "when created" do Metadata::RESERVED_KEYS.each do |key| it "prohibits :#{key} as a hash key for an example group" do From 8b71ab914d87ad594e5251e6e853633ed5894cca Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Mon, 23 Feb 2015 09:10:54 -0800 Subject: [PATCH 041/258] =?UTF-8?q?Name=20the=20regexp=20to=20make=20it=20?= =?UTF-8?q?more=20clear=20what=20it=E2=80=99s=20for.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ..as per @samphipppen’s request. --- lib/rspec/core/configuration.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/rspec/core/configuration.rb b/lib/rspec/core/configuration.rb index 7078506945..fe49573a5f 100644 --- a/lib/rspec/core/configuration.rb +++ b/lib/rspec/core/configuration.rb @@ -1606,6 +1606,9 @@ def absolute_pattern?(pattern) end end + # @private + ON_SQUARE_BRACKETS = /[\[\]]/ + def extract_location(path) match = /^(.*?)((?:\:\d+)+)$/.match(path) @@ -1614,7 +1617,7 @@ def extract_location(path) path, lines = captures[0], captures[1][1..-1].split(":").map { |n| n.to_i } filter_manager.add_location path, lines else - path, scoped_ids = path.split(/[\[\]]/) + path, scoped_ids = path.split(ON_SQUARE_BRACKETS) filter_manager.add_ids(path, scoped_ids.split(/\s*,\s*/)) if scoped_ids end From 0f7c90b0c1355e8087aea03182ea45089554146e Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Mon, 23 Feb 2015 09:12:09 -0800 Subject: [PATCH 042/258] Standardize on setting `line` before rather than after. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ..as per @samphippen’s review request. --- spec/rspec/core/formatters/base_text_formatter_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/rspec/core/formatters/base_text_formatter_spec.rb b/spec/rspec/core/formatters/base_text_formatter_spec.rb index a5831a2aec..c416054be4 100644 --- a/spec/rspec/core/formatters/base_text_formatter_spec.rb +++ b/spec/rspec/core/formatters/base_text_formatter_spec.rb @@ -32,10 +32,10 @@ describe "rerun command for failed examples" do it "uses the location to identify the example" do + line = __LINE__ + 2 example_group = RSpec.describe("example group") do it("fails") { fail } end - line = __LINE__ - 2 expect(output_from_running example_group).to include("rspec #{RSpec::Core::Metadata::relative_path("#{__FILE__}:#{line}")} # example group fails") end From 3177337d9dcd76a0e0d0811d0dd20d7919e9759f Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Mon, 23 Feb 2015 18:33:59 -0800 Subject: [PATCH 043/258] =?UTF-8?q?Quote=20example=20ids=20in=20rerun=20co?= =?UTF-8?q?mmand=20unless=20we=20know=20it=E2=80=99s=20unneeded.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/rspec/core/notifications.rb | 31 ++++++++++++++++- spec/integration/filtering_spec.rb | 4 +++ .../formatters/base_text_formatter_spec.rb | 34 ++++++++++++++++--- spec/support/aruba_support.rb | 3 +- 4 files changed, 65 insertions(+), 7 deletions(-) diff --git a/lib/rspec/core/notifications.rb b/lib/rspec/core/notifications.rb index dbeea85751..e02c648a37 100644 --- a/lib/rspec/core/notifications.rb +++ b/lib/rspec/core/notifications.rb @@ -520,7 +520,8 @@ def fully_formatted(colorizer=::RSpec::Core::Formatters::ConsoleCodes) def rerun_argument_for(example) location = example.location_rerun_argument - duplicate_rerun_locations.include?(location) ? example.id : location + return location unless duplicate_rerun_locations.include?(location) + conditionally_quote(example.id) end def duplicate_rerun_locations @@ -534,6 +535,34 @@ def duplicate_rerun_locations end end end + + # Known shells that require quoting: zsh, csh, tcsh. + # + # Feel free to add other shells to this list that are known to + # allow `rspec ./some_spec.rb[1:1]` syntax without quoting the id. + # + # @private + SHELLS_ALLOWING_UNQUOTED_IDS = %w[ bash ksh ] + + def conditionally_quote(id) + return id if shell_allows_unquoted_ids? + "'#{id.gsub("'", "\\\\'")}'" + end + + def shell_allows_unquoted_ids? + return @shell_allows_unquoted_ids if defined?(@shell_allows_unquoted_ids) + + @shell_allows_unquoted_ids = SHELLS_ALLOWING_UNQUOTED_IDS.include?( + # Note: ENV['SHELL'] isn't necessarily the shell the user is currently running. + # According to http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html: + # "This variable shall represent a pathname of the user's preferred command language interpreter." + # + # It's the best we can easily do, though. We err on the side of safety (quoting + # the id when not actually needed) so it's not a big deal if the user is actually + # using a different shell. + ENV['SHELL'].to_s.split('/').last + ) + end end # The `ProfileNotification` holds information about the results of running a diff --git a/spec/integration/filtering_spec.rb b/spec/integration/filtering_spec.rb index ed1e2230ab..3ae80b08d1 100644 --- a/spec/integration/filtering_spec.rb +++ b/spec/integration/filtering_spec.rb @@ -147,6 +147,10 @@ def run_rerun_command_for_failing_spec run_command "./spec/file_1_spec.rb[1:1,1:3] ./spec/file_2_spec.rb[1:2]" expect(last_cmd_stdout).to match(/3 examples, 0 failures/) + # Using spaces between scoped ids, and quoting the whole thing... + run_command "'./spec/file_1_spec.rb[1:1, 1:3]' ./spec/file_2_spec.rb[1:2]" + expect(last_cmd_stdout).to match(/3 examples, 0 failures/) + # Without the leading `.`... run_command "spec/file_1_spec.rb[1:1,1:3] spec/file_2_spec.rb[1:2]" expect(last_cmd_stdout).to match(/3 examples, 0 failures/) diff --git a/spec/rspec/core/formatters/base_text_formatter_spec.rb b/spec/rspec/core/formatters/base_text_formatter_spec.rb index c416054be4..e0ddd835e9 100644 --- a/spec/rspec/core/formatters/base_text_formatter_spec.rb +++ b/spec/rspec/core/formatters/base_text_formatter_spec.rb @@ -53,14 +53,38 @@ end context "for an example that is not uniquely identified by the location" do - it "uses the id instead" do - example_group = RSpec.describe("example group") do - 1.upto(2) do |i| - it("compares #{i} against 2") { expect(i).to eq(2) } + let(:example_group_in_this_file) { example_group_defined_in(__FILE__) } + + def example_group_defined_in(file) + instance_eval <<-EOS, file, 1 + $group = RSpec.describe("example group") do + 1.upto(2) do |i| + it("compares \#{i} against 2") { expect(i).to eq(2) } + end end + EOS + $group + end + + let(:id) { "#{RSpec::Core::Metadata::relative_path("#{__FILE__}")}[1:1]" } + + it "uses the id instead" do + with_env_vars 'SHELL' => '/usr/local/bin/bash' do + expect(output_from_running example_group_in_this_file).to include("rspec #{id} # example group compares 1 against 2") + end + end + + context "on a shell that may not handle unquoted ids" do + around { |ex| with_env_vars('SHELL' => '/usr/local/bin/cash', &ex) } + + it 'quotes the id to be safe so the rerun command can be copied and pasted' do + expect(output_from_running example_group_in_this_file).to include("rspec '#{id}'") end - expect(output_from_running example_group).to include("rspec #{RSpec::Core::Metadata::relative_path("#{__FILE__}[1:1]")} # example group compares 1 against 2") + it 'correctly escapes file names that have quotes in them' do + group_in_other_file = example_group_defined_in("./path/with'quote_spec.rb") + expect(output_from_running group_in_other_file).to include("rspec './path/with\\'quote_spec.rb[1:1]'") + end end end diff --git a/spec/support/aruba_support.rb b/spec/support/aruba_support.rb index 662ddbb97d..c9df9c4b17 100644 --- a/spec/support/aruba_support.rb +++ b/spec/support/aruba_support.rb @@ -16,9 +16,10 @@ def run_command(cmd) temp_stdout = StringIO.new temp_stderr = StringIO.new RSpec::Core::Metadata.instance_variable_set(:@relative_path_regex, nil) + cmd_parts = Shellwords.split(cmd) in_current_dir do - RSpec::Core::Runner.run(cmd.split, temp_stderr, temp_stdout) + RSpec::Core::Runner.run(cmd_parts, temp_stderr, temp_stdout) end ensure RSpec.reset From e226831ab9c6d0a6ef92fd54f3593fa2a9af707b Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Mon, 23 Feb 2015 18:40:06 -0800 Subject: [PATCH 044/258] Update `rspec --help` to include more detail about filtering. - Clarify location filtering. - Add info about id filtering. --- .rubocop.yml | 2 +- lib/rspec/core/option_parser.rb | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 368bdfb4d4..449b65d10e 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -25,7 +25,7 @@ Lint/LiteralInInterpolation: # This should go down over time. MethodLength: - Max: 155 + Max: 40 # Exclude the default spec_helper to make it easier to uncomment out # default settings (for both users and the Cucumber suite). diff --git a/lib/rspec/core/option_parser.rb b/lib/rspec/core/option_parser.rb index 8ee17163c1..00c12bd15a 100644 --- a/lib/rspec/core/option_parser.rb +++ b/lib/rspec/core/option_parser.rb @@ -21,6 +21,7 @@ def parse(args) options end + # rubocop:disable MethodLength def parser(options) OptionParser.new do |parser| parser.banner = "Usage: rspec [options] [files or directories]\n\n" @@ -143,11 +144,15 @@ def parser(options) **** Filtering/tags **** - In addition to the following options for selecting specific files, groups, - or examples, you can select a single example by appending the line number to + In addition to the following options for selecting specific files, groups, or + examples, you can select individual examples by appending the line number(s) to the filename: - rspec path/to/a_spec.rb:37 + rspec path/to/a_spec.rb:37:87 + + You can also pass example ids enclosed in square brackets: + + rspec path/to/a_spec.rb[1:5,1:6] # run the 5th and 6th examples/groups defined in the 1st group FILTERING @@ -224,5 +229,6 @@ def parser(options) end end + # rubocop:enable MethodLength end end From 16bb9a75a3138492d86fd7d5d3f617bda2a80428 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Mon, 23 Feb 2015 18:50:06 -0800 Subject: [PATCH 045/258] Add changelog entries. --- Changelog.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Changelog.md b/Changelog.md index 90a843b0df..645a6b897c 100644 --- a/Changelog.md +++ b/Changelog.md @@ -9,6 +9,11 @@ Enhancements: `RSpec::Core::Reporter#publish(event_name, hash_of_attributes)`. (Jon Rowe, #1869) * Remove dependency on the standard library `Set` and replace with `RSpec::Core::Set`. (Jon Rowe, #1870) +* Assign a unique id to each example and group so that they can be + uniquely identified, even for shared examples (and similar situations) + where the location isn't unique. (Myron Marston, #1884) +* Use the example id in the rerun command printed for failed examples + when the location is not unique. (Myron Marston, #1884) Bug Fixes: From 43b464b6bfa3117e6c91d0028d272954229f8b00 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Mon, 23 Feb 2015 19:20:41 -0800 Subject: [PATCH 046/258] Forwardport 3.2.1 release notes. [ci skip] --- Changelog.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Changelog.md b/Changelog.md index 90a843b0df..926de327a4 100644 --- a/Changelog.md +++ b/Changelog.md @@ -13,6 +13,12 @@ Enhancements: Bug Fixes: * Handle invalid UTF-8 strings within exception methods. (Benjamin Fleischer, #1760) + +### 3.2.1 / 2015-02-23 +[Full Changelog](http://github.com/rspec/rspec-core/compare/v3.2.0...v3.2.1) + +Bug Fixes: + * Notify start-of-run seed _before_ `start` notification rather than _after_ so that formatters like Fuubar work properly. (Samuel Esposito, #1882) From dcbb8530faf7b8b38bbc7a5a2dfc79e6dce1f586 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Mon, 23 Feb 2015 20:11:56 -0800 Subject: [PATCH 047/258] Add fish to the list of shells that allow unquoted ids. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …according to @joshcheek’s comment: https://github.com/rspec/rspec-core/pull/1884#issuecomment-75696016 --- lib/rspec/core/notifications.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rspec/core/notifications.rb b/lib/rspec/core/notifications.rb index e02c648a37..15cfbbc37e 100644 --- a/lib/rspec/core/notifications.rb +++ b/lib/rspec/core/notifications.rb @@ -542,7 +542,7 @@ def duplicate_rerun_locations # allow `rspec ./some_spec.rb[1:1]` syntax without quoting the id. # # @private - SHELLS_ALLOWING_UNQUOTED_IDS = %w[ bash ksh ] + SHELLS_ALLOWING_UNQUOTED_IDS = %w[ bash ksh fish ] def conditionally_quote(id) return id if shell_allows_unquoted_ids? From a34c84d23f9efb26195e491a6d0c6a7634c6c122 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Tue, 24 Feb 2015 14:46:39 -0800 Subject: [PATCH 048/258] Updated travis build scripts (from rspec-dev) --- .rubocop_rspec_base.yml | 2 +- .travis.yml | 18 +++++++++++++----- appveyor.yml | 2 +- script/clone_all_rspec_repos | 2 +- script/functions.sh | 9 +++++---- script/predicate_functions.sh | 2 +- script/run_build | 2 +- script/travis_functions.sh | 2 +- 8 files changed, 24 insertions(+), 15 deletions(-) diff --git a/.rubocop_rspec_base.yml b/.rubocop_rspec_base.yml index f7bea1c203..9e52c97e36 100644 --- a/.rubocop_rspec_base.yml +++ b/.rubocop_rspec_base.yml @@ -1,4 +1,4 @@ -# This file was generated on 2015-01-07T22:08:46-08:00 from the rspec-dev repo. +# This file was generated on 2015-02-24T14:46:39-08:00 from the rspec-dev repo. # DO NOT modify it by hand as your changes will get lost the next time it is generated. # This file contains defaults for RSpec projects. Individual projects diff --git a/.travis.yml b/.travis.yml index ea4f1b0fcc..636f134028 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ -# This file was generated on 2015-01-07T22:08:46-08:00 from the rspec-dev repo. +# This file was generated on 2015-02-24T14:46:39-08:00 from the rspec-dev repo. # DO NOT modify it by hand as your changes will get lost the next time it is generated. language: ruby @@ -22,16 +22,24 @@ rvm: - 2.2 - ruby-head - ree - - jruby-18mode - - jruby - - jruby-head - rbx matrix: include: - rvm: jruby - env: JRUBY_OPTS='--2.0' + env: JRUBY_OPTS='--server -Xcompile.invokedynamic=false -Xcompat.version=2.0' + - rvm: jruby-head + env: JRUBY_OPTS='--server -Xcompile.invokedynamic=false' + - rvm: jruby-18mode + env: JRUBY_OPTS='--server -Xcompile.invokedynamic=false' + - rvm: jruby + env: JRUBY_OPTS='--server -Xcompile.invokedynamic=false' allow_failures: - rvm: jruby-head + env: JRUBY_OPTS='--server -Xcompile.invokedynamic=false' - rvm: ruby-head - rvm: rbx fast_finish: true +branches: + only: + - master + - /^\d+-\d+-maintenance$/ diff --git a/appveyor.yml b/appveyor.yml index 9e4f457e30..15461acc38 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,4 @@ -# This file was generated on 2015-01-07T22:08:46-08:00 from the rspec-dev repo. +# This file was generated on 2015-02-24T14:46:39-08:00 from the rspec-dev repo. # DO NOT modify it by hand as your changes will get lost the next time it is generated. version: "{build}" diff --git a/script/clone_all_rspec_repos b/script/clone_all_rspec_repos index f83d2e910f..c7fae3a8a1 100755 --- a/script/clone_all_rspec_repos +++ b/script/clone_all_rspec_repos @@ -1,5 +1,5 @@ #!/bin/bash -# This file was generated on 2015-01-07T22:08:46-08:00 from the rspec-dev repo. +# This file was generated on 2015-02-24T14:46:39-08:00 from the rspec-dev repo. # DO NOT modify it by hand as your changes will get lost the next time it is generated. set -e diff --git a/script/functions.sh b/script/functions.sh index a96a5c71b4..1faf04ad51 100644 --- a/script/functions.sh +++ b/script/functions.sh @@ -1,12 +1,13 @@ -# This file was generated on 2015-01-07T22:08:46-08:00 from the rspec-dev repo. +# This file was generated on 2015-02-24T14:46:39-08:00 from the rspec-dev repo. # DO NOT modify it by hand as your changes will get lost the next time it is generated. SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" source $SCRIPT_DIR/travis_functions.sh source $SCRIPT_DIR/predicate_functions.sh -# idea taken from: http://blog.headius.com/2010/03/jruby-startup-time-tips.html -export JRUBY_OPTS="${JRUBY_OPTS} -X-C" # disable JIT since these processes are so short lived +# If JRUBY_OPTS isn't set, use these. +# see http://docs.travis-ci.com/user/ci-environment/ +export JRUBY_OPTS=${JRUBY_OPTS:-"--server -Xcompile.invokedynamic=false"} SPECS_HAVE_RUN_FILE=specs.out MAINTENANCE_BRANCH=`cat maintenance-branch` @@ -112,7 +113,7 @@ function check_documentation_coverage { } function check_style_and_lint { - echo "bin/rubucop lib" + echo "bin/rubocop lib" bin/rubocop lib } diff --git a/script/predicate_functions.sh b/script/predicate_functions.sh index fc5d372c50..1c9275fbfb 100644 --- a/script/predicate_functions.sh +++ b/script/predicate_functions.sh @@ -1,4 +1,4 @@ -# This file was generated on 2015-01-07T22:08:46-08:00 from the rspec-dev repo. +# This file was generated on 2015-02-24T14:46:39-08:00 from the rspec-dev repo. # DO NOT modify it by hand as your changes will get lost the next time it is generated. function is_mri { diff --git a/script/run_build b/script/run_build index e1edcef3a9..e78cbd8435 100755 --- a/script/run_build +++ b/script/run_build @@ -1,5 +1,5 @@ #!/bin/bash -# This file was generated on 2015-01-07T22:08:46-08:00 from the rspec-dev repo. +# This file was generated on 2015-02-24T14:46:39-08:00 from the rspec-dev repo. # DO NOT modify it by hand as your changes will get lost the next time it is generated. set -e diff --git a/script/travis_functions.sh b/script/travis_functions.sh index 77829b3638..8e86947b78 100644 --- a/script/travis_functions.sh +++ b/script/travis_functions.sh @@ -1,4 +1,4 @@ -# This file was generated on 2015-01-07T22:08:46-08:00 from the rspec-dev repo. +# This file was generated on 2015-02-24T14:46:39-08:00 from the rspec-dev repo. # DO NOT modify it by hand as your changes will get lost the next time it is generated. # Taken from: From 30d3e0bc77424ebe061537972b057e83e6905412 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Tue, 24 Feb 2015 23:41:08 -0800 Subject: [PATCH 049/258] Lower JRuby coverage threshold. --- script/rspec_with_simplecov | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/rspec_with_simplecov b/script/rspec_with_simplecov index 5c78675675..94f32b85a3 100755 --- a/script/rspec_with_simplecov +++ b/script/rspec_with_simplecov @@ -31,7 +31,7 @@ begin add_filter "./bundle/" add_filter "./tmp/" add_filter "./spec/" - minimum_coverage(RUBY_PLATFORM == 'java' ? 94 : 97) + minimum_coverage(RUBY_PLATFORM == 'java' ? 93 : 97) end end rescue LoadError From 4a57af86b0c3bd277b4d87d26d77edf7415750b9 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Wed, 25 Feb 2015 18:05:39 -0800 Subject: [PATCH 050/258] Fix rake task arg quoting on Windows. --- Changelog.md | 2 ++ lib/rspec/core/rake_task.rb | 2 +- spec/rspec/core/rake_task_spec.rb | 7 +++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index 926de327a4..3628d52526 100644 --- a/Changelog.md +++ b/Changelog.md @@ -13,6 +13,8 @@ Enhancements: Bug Fixes: * Handle invalid UTF-8 strings within exception methods. (Benjamin Fleischer, #1760) +* Fix Rake Task quoting of file names with quotes to work properly on + Windows. (Myron Marston, #1887 ### 3.2.1 / 2015-02-23 [Full Changelog](http://github.com/rspec/rspec-core/compare/v3.2.0...v3.2.1) diff --git a/lib/rspec/core/rake_task.rb b/lib/rspec/core/rake_task.rb index baf1c441d6..94ebe99a7b 100644 --- a/lib/rspec/core/rake_task.rb +++ b/lib/rspec/core/rake_task.rb @@ -128,7 +128,7 @@ def file_inclusion_specification if RSpec::Support::OS.windows? def escape(shell_command) - "'#{shell_command.gsub("'", "\'")}'" + "'#{shell_command.gsub("'", "\\\\'")}'" end else require 'shellwords' diff --git a/spec/rspec/core/rake_task_spec.rb b/spec/rspec/core/rake_task_spec.rb index 772dea4162..94a0b5c0c6 100644 --- a/spec/rspec/core/rake_task_spec.rb +++ b/spec/rspec/core/rake_task_spec.rb @@ -47,6 +47,13 @@ def spec_command end end + context "on windows, with a quote in the name", :if => RSpec::Support::OS.windows? do + it "renders rspec quoted, with quote escaped" do + task.rspec_path = "/foo'bar/exe/rspec" + expect(spec_command).to include(%q|'/foo\'bar/exe/rspec'|) + end + end + context "with ruby options" do it "renders them before the rspec path" do task.ruby_opts = "-w" From 95609709de8c5df85867feda0b9573b5e53a14bd Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Wed, 25 Feb 2015 22:45:43 -0800 Subject: [PATCH 051/258] Add missing paren. [ci skip] --- Changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index 534c6e97fc..bdcfccb14e 100644 --- a/Changelog.md +++ b/Changelog.md @@ -19,7 +19,7 @@ Bug Fixes: * Handle invalid UTF-8 strings within exception methods. (Benjamin Fleischer, #1760) * Fix Rake Task quoting of file names with quotes to work properly on - Windows. (Myron Marston, #1887 + Windows. (Myron Marston, #1887) ### 3.2.1 / 2015-02-23 [Full Changelog](http://github.com/rspec/rspec-core/compare/v3.2.0...v3.2.1) From f06892da47cdb9e1ba87760fb4854eae3a66090d Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Tue, 3 Mar 2015 11:40:40 -0800 Subject: [PATCH 052/258] Fix example in README to be accurate. [ci skip] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d47f17b39c..ea37c8564e 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ Although you probably won't ever need this unless you are writing an extension, you can access it from an example like this: ```ruby -it "does something" do +it "does something" do |example| expect(example.metadata[:description]).to eq("does something") end ``` From b610d83657483854a5098a80f8ae4483d19c1eda Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Wed, 4 Mar 2015 09:39:20 +1100 Subject: [PATCH 053/258] simple readme fix [skip ci] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d47f17b39c..ea37c8564e 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ Although you probably won't ever need this unless you are writing an extension, you can access it from an example like this: ```ruby -it "does something" do +it "does something" do |example| expect(example.metadata[:description]).to eq("does something") end ``` From 4ac3b663505a975ba7b346faa4faaf3e229fe299 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Tue, 3 Mar 2015 22:40:04 -0800 Subject: [PATCH 054/258] Make diff coloring work properly in integration specs. --- spec/support/aruba_support.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/support/aruba_support.rb b/spec/support/aruba_support.rb index c9df9c4b17..c3368db940 100644 --- a/spec/support/aruba_support.rb +++ b/spec/support/aruba_support.rb @@ -13,6 +13,8 @@ module ArubaLoader attr_reader :last_cmd_stdout, :last_cmd_stderr def run_command(cmd) + RSpec.configuration.color = true + temp_stdout = StringIO.new temp_stderr = StringIO.new RSpec::Core::Metadata.instance_variable_set(:@relative_path_regex, nil) @@ -23,6 +25,7 @@ def run_command(cmd) end ensure RSpec.reset + RSpec.configuration.color = true RSpec::Core::Metadata.instance_variable_set(:@relative_path_regex, nil) # Ensure it gets cached with a proper value -- if we leave it set to nil, From e4a28972e743b519c495215b21f2bb22839c90bf Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Tue, 3 Mar 2015 17:54:43 -0800 Subject: [PATCH 055/258] =?UTF-8?q?Triple=20quotes=20aren=E2=80=99t=20real?= =?UTF-8?q?ly=20a=20thing.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ruby concatenates strings that are next to each other, though, so it’s a blank string concatenated with a longer multi-line string concatenated with another blank string. --- spec/integration/filtering_spec.rb | 40 +++++++++++++++--------------- spec/integration/order_spec.rb | 16 ++++++------ 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/spec/integration/filtering_spec.rb b/spec/integration/filtering_spec.rb index 3ae80b08d1..a8f3284fdc 100644 --- a/spec/integration/filtering_spec.rb +++ b/spec/integration/filtering_spec.rb @@ -5,14 +5,14 @@ before { clean_current_dir } it 'prints a rerun command for shared examples in external files that works to rerun' do - write_file "spec/support/shared_examples.rb", """ + write_file "spec/support/shared_examples.rb", " RSpec.shared_examples 'with a failing example' do example { expect(1).to eq(2) } # failing example { expect(2).to eq(2) } # passing end - """ + " - write_file "spec/host_group_spec.rb", """ + write_file "spec/host_group_spec.rb", " load File.expand_path('../support/shared_examples.rb', __FILE__) RSpec.describe 'A group with shared examples' do @@ -22,7 +22,7 @@ RSpec.describe 'A group with a passing example' do example { expect(1).to eq(1) } end - """ + " run_command "" expect(last_cmd_stdout).to include("3 examples, 1 failure") @@ -40,7 +40,7 @@ def run_rerun_command_for_failing_spec context "with a shared example containing a context in a separate file" do it "runs the example nested inside the shared" do - write_file_formatted 'spec/shared_example.rb', """ + write_file_formatted 'spec/shared_example.rb', " RSpec.shared_examples_for 'a shared example' do it 'succeeds' do end @@ -50,15 +50,15 @@ def run_rerun_command_for_failing_spec end end end - """ + " - write_file_formatted 'spec/simple_spec.rb', """ + write_file_formatted 'spec/simple_spec.rb', " require File.join(File.dirname(__FILE__), 'shared_example.rb') RSpec.describe 'top level' do it_behaves_like 'a shared example' end - """ + " run_command 'spec/simple_spec.rb:3 -fd' expect(last_cmd_stdout).to match(/2 examples, 0 failures/) @@ -67,7 +67,7 @@ def run_rerun_command_for_failing_spec context "passing a line-number filter" do it "trumps exclusions, except for :if/:unless (which are absolute exclusions)" do - write_file_formatted 'spec/a_spec.rb', """ + write_file_formatted 'spec/a_spec.rb', " RSpec.configure do |c| c.filter_run_excluding :slow end @@ -82,7 +82,7 @@ def run_rerun_command_for_failing_spec example('ex 4', :slow ) { } example('ex 5', :if => false) { } end - """ + " run_command "spec/a_spec.rb -fd" expect(last_cmd_stdout).to include("1 example, 0 failures", "ex 3").and exclude("ex 1", "ex 2", "ex 4", "ex 5") @@ -100,14 +100,14 @@ def run_rerun_command_for_failing_spec context "passing a line-number-filtered file and a non-filtered file" do it "applies the line number filtering only to the filtered file, running all specs in the non-filtered file except excluded ones" do - write_file_formatted "spec/file_1_spec.rb", """ + write_file_formatted "spec/file_1_spec.rb", " RSpec.describe 'File 1' do it('passes') { } it('fails') { fail } end - """ + " - write_file_formatted "spec/file_2_spec.rb", """ + write_file_formatted "spec/file_2_spec.rb", " RSpec.configure do |c| c.filter_run_excluding :exclude_me end @@ -117,7 +117,7 @@ def run_rerun_command_for_failing_spec it('passes') { } it('fails', :exclude_me) { fail } end - """ + " run_command "spec/file_1_spec.rb:2 spec/file_2_spec.rb -fd" expect(last_cmd_stdout).to match(/3 examples, 0 failures/) @@ -127,21 +127,21 @@ def run_rerun_command_for_failing_spec context "passing example ids at the command line" do it "selects matching examples" do - write_file_formatted "spec/file_1_spec.rb", """ + write_file_formatted "spec/file_1_spec.rb", " RSpec.describe 'File 1' do 1.upto(3) do |i| example('ex ' + i.to_s) { expect(i).to be_odd } end end - """ + " - write_file_formatted "spec/file_2_spec.rb", """ + write_file_formatted "spec/file_2_spec.rb", " RSpec.describe 'File 2' do 1.upto(3) do |i| example('ex ' + i.to_s) { expect(i).to be_even } end end - """ + " # Using the form that Metadata.relative_path returns... run_command "./spec/file_1_spec.rb[1:1,1:3] ./spec/file_2_spec.rb[1:2]" @@ -162,7 +162,7 @@ def run_rerun_command_for_failing_spec end it "selects matching example groups" do - write_file_formatted "spec/file_1_spec.rb", """ + write_file_formatted "spec/file_1_spec.rb", " RSpec.describe 'Group 1' do example { fail } @@ -175,7 +175,7 @@ def run_rerun_command_for_failing_spec example { fail } end end - """ + " run_command "./spec/file_1_spec.rb[1:2]" expect(last_cmd_stdout).to match(/2 examples, 0 failures/) diff --git a/spec/integration/order_spec.rb b/spec/integration/order_spec.rb index 5d5921e136..a7bac40e68 100644 --- a/spec/integration/order_spec.rb +++ b/spec/integration/order_spec.rb @@ -4,7 +4,7 @@ include_context "aruba support" before :all do - write_file 'spec/simple_spec.rb', """ + write_file 'spec/simple_spec.rb', " RSpec.describe 'group 1' do specify('group 1 example 1') {} specify('group 1 example 2') {} @@ -15,9 +15,9 @@ specify('group 1-1 example 3') {} end end - """ + " - write_file 'spec/simple_spec2.rb', """ + write_file 'spec/simple_spec2.rb', " RSpec.describe 'group 2' do specify('group 2 example 1') {} specify('group 2 example 2') {} @@ -28,9 +28,9 @@ specify('group 2-1 example 3') {} end end - """ + " - write_file 'spec/order_spec.rb', """ + write_file 'spec/order_spec.rb', " RSpec.describe 'group 1' do specify('group 1 example 1') {} specify('group 1 example 2') {} @@ -76,7 +76,7 @@ RSpec.describe('group 8') { specify('example') {} } RSpec.describe('group 9') { specify('example') {} } RSpec.describe('group 10') { specify('example') {} } - """ + " end describe '--order rand' do @@ -150,7 +150,7 @@ after { remove_file 'spec/custom_order_spec.rb' } before do - write_file 'spec/custom_order_spec.rb', """ + write_file 'spec/custom_order_spec.rb', " RSpec.configure do |config| config.register_ordering :global do |list| list.sort_by { |item| item.description } @@ -167,7 +167,7 @@ RSpec.describe 'group A' do specify('group A example 1') {} end - """ + " end it 'orders the groups and examples by the provided strategy' do From 4dd013d1aaa6f2acfe43e3c4ae4ed6ee18e1a49c Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Thu, 5 Mar 2015 21:59:02 -0800 Subject: [PATCH 056/258] Prefer capitalized section headings. --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index ea37c8564e..10fb433ad7 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ rspec-core provides the structure for writing executable examples of how your code should behave, and an `rspec` command with tools to constrain which examples get run and tailor the output. -## install +## Install gem install rspec # for rspec-core, rspec-expectations, rspec-mocks gem install rspec-core # for rspec-core only @@ -19,7 +19,7 @@ RSpec repos as well. Add the following to your `Gemfile`: end ``` -## basic structure +## Basic Structure RSpec uses the words "describe" and "it" so we can express concepts like a conversation: @@ -49,7 +49,7 @@ Under the hood, an example group is a class in which the block passed to `describe` is evaluated. The blocks passed to `it` are evaluated in the context of an _instance_ of that class. -## nested groups +## Nested Groups You can also declare nested nested groups using the `describe` or `context` methods: @@ -70,7 +70,7 @@ RSpec.describe Order do end ``` -## aliases +## Aliases You can declare example groups using either `describe` or `context`. For a top level example group, `describe` and `context` are available @@ -81,7 +81,7 @@ patching. You can declare examples within a group using any of `it`, `specify`, or `example`. -## shared examples and contexts +## Shared Examples and Contexts Declare a shared example group using `shared_examples`, and then include it in any group using `include_examples`. @@ -111,7 +111,7 @@ pretty much the same as `shared_examples` and `include_examples`, providing more accurate naming when you share hooks, `let` declarations, helper methods, etc, but no examples. -## metadata +## Metadata rspec-core stores a metadata hash with every example and group, which contains their descriptions, the locations at which they were @@ -162,26 +162,26 @@ RSpec.describe Hash do end ``` -## the `rspec` command +## The `rspec` Command When you install the rspec-core gem, it installs the `rspec` executable, which you'll use to run rspec. The `rspec` command comes with many useful options. Run `rspec --help` to see the complete list. -## store command line options `.rspec` +## Store Command Line Options `.rspec` You can store command line options in a `.rspec` file in the project's root directory, and the `rspec` command will read them as though you typed them on the command line. -## autotest integration +## Autotest Integration rspec-core no longer ships with an Autotest extension, if you require Autotest integration, please use the `rspec-autotest` gem and see [rspec/rspec-autotest](https://github.com/rspec/rspec-autotest) for details -## get started +## Get Started Start with a simple example of behavior you expect from your system. Do this before you write any implementation code: From dcc5e929e0fb28513d8184884edb99457b17d4b1 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Thu, 5 Mar 2015 22:00:02 -0800 Subject: [PATCH 057/258] Remove mention of removed feature. README space is limited so why waste it mentioning a feature we no longer support? --- README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.md b/README.md index 10fb433ad7..32dc01c012 100644 --- a/README.md +++ b/README.md @@ -175,12 +175,6 @@ You can store command line options in a `.rspec` file in the project's root directory, and the `rspec` command will read them as though you typed them on the command line. -## Autotest Integration - -rspec-core no longer ships with an Autotest extension, if you require Autotest -integration, please use the `rspec-autotest` gem and see [rspec/rspec-autotest](https://github.com/rspec/rspec-autotest) -for details - ## Get Started Start with a simple example of behavior you expect from your system. Do From 7e18d707cc24d2d84785e284623fde38abf47391 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 6 Mar 2015 07:50:18 -0800 Subject: [PATCH 058/258] Add rspec gem to list as well. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit While it’s not normally needed (it has no code!), some RSpec extensions depend directly on `rspec` and in such situations you’ll need it to have the newer version as well. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 32dc01c012..9bcdb491f9 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Want to run against the `master` branch? You'll need to include the dependent RSpec repos as well. Add the following to your `Gemfile`: ```ruby -%w[rspec-core rspec-expectations rspec-mocks rspec-support].each do |lib| +%w[rspec rspec-core rspec-expectations rspec-mocks rspec-support].each do |lib| gem lib, :git => "git://github.com/rspec/#{lib}.git", :branch => 'master' end ``` From 38b1601e5e6693bd0c227fae198de7e8407eb80f Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 6 Mar 2015 07:52:30 -0800 Subject: [PATCH 059/258] Add spacing. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 9bcdb491f9..ae5e68b228 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ RSpec uses the words "describe" and "it" so we can express concepts like a conve RSpec.describe Order do it "sums the prices of its line items" do order = Order.new + order.add_entry(LineItem.new(:item => Item.new( :price => Money.new(1.11, :USD) ))) @@ -37,6 +38,7 @@ RSpec.describe Order do :price => Money.new(2.22, :USD), :quantity => 2 ))) + expect(order.total).to eq(Money.new(5.55, :USD)) end end From de156c1554bddeac8ffe3f2d2b72a718dd3f4624 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 6 Mar 2015 07:54:36 -0800 Subject: [PATCH 060/258] Add note explaining that nested groups are subclasses. --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index ae5e68b228..31c580f8d3 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,9 @@ RSpec.describe Order do end ``` +Nested groups are subclasses of the outer example group class, providing +the inheritance semantics you'd want for free. + ## Aliases You can declare example groups using either `describe` or `context`. From 2c4ae5c9b3868e6fbcf43897b4e9355e1cbc2c1f Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 6 Mar 2015 08:03:20 -0800 Subject: [PATCH 061/258] =?UTF-8?q?Improve=20=E2=80=9CGet=20Started?= =?UTF-8?q?=E2=80=9D=20section=20of=20README.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It’s important that you see the expectation fail (it’s how you “test the test”, so to speak, and you can confirm it gives you a good failure message), so let’s not skip that step. --- README.md | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 31c580f8d3..3a2af20423 100644 --- a/README.md +++ b/README.md @@ -203,13 +203,12 @@ $ rspec spec/calculator_spec.rb ./spec/calculator_spec.rb:1: uninitialized constant Calculator ``` -Implement the simplest solution: +Address the failure by defining a skeleton of the `Calculator` class: ```ruby # in lib/calculator.rb class Calculator - def add(a,b) - a + b + def add(a, b) end end ``` @@ -222,6 +221,39 @@ Be sure to require the implementation file in the spec: require "calculator" ``` +Now run the spec again, and watch the expectation fail: + +``` +$ rspec spec/calculator_spec.rb +F + +Failures: + + 1) Calculator#add returns the sum of its arguments + Failure/Error: expect(Calculator.new.add(1, 2)).to eq(3) + + expected: 3 + got: nil + + (compared using ==) + # ./spec/calcalator_spec.rb:6:in `block (3 levels) in ' + +Finished in 0.00131 seconds (files took 0.10968 seconds to load) +1 example, 1 failure + +Failed examples: + +rspec ./spec/calcalator_spec.rb:5 # Calculator#add returns the sum of its arguments +``` + +Implement the simplest solution, by changing the definition of `Calculator#add` to: + +```ruby +def add(a, b) + a + b +end +``` + Now run the spec again, and watch it pass: ``` From 8112643c5d0c560cd4a49f0c82f71e76fc390d3f Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 6 Mar 2015 08:18:49 -0800 Subject: [PATCH 062/258] Add section describing scope. After reading http://therealadam.com/2015/03/01/its-not-your-fault-if-your-tools-confuse-you/, I realized we do a poor job documenting the way the scopes work so this will hopefully help. --- README.md | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/README.md b/README.md index 3a2af20423..d17adcf4eb 100644 --- a/README.md +++ b/README.md @@ -167,6 +167,78 @@ RSpec.describe Hash do end ``` +## A Word on Scope + +RSpec has two scopes: + +* **Example Group**: Example groups are defined by a `describe` or + `context` block, which is eagerly evaluated when the spec file is + loaded. The block is evaluated in the context of a subclass of + `RSpec::Core::ExampleGroup`, or a subclass of the parent example group + when you're nesting them. +* **Example**: Examples, and any other blocks with per-example semantics + (such as a `before(:example)` hook), are evaluated in the context of + an _instance_ of the example group class to which the example belongs. + Examples are _not_ executed when the spec file is loaded; instead, + RSpec waits to run any examples until all spec files have been loaded, + at which point it can apply filtering, randomization, etc. + +To make this more concrete, consider this code snippet: + +``` ruby +RSpec.describe "Using an array as a stack" do + def build_stack + [] + end + + before(:example) do + @stack = build_stack + end + + it 'is initially empty' do + expect(@stack).to be_empty + end + + context "after an item has been pushed" do + def build_stack + super.push :item + end + + it 'allows the pushed item to be popped' do + expect(@stack.pop).to eq(:item) + end + end +end +``` + +Under the covers, this is (roughly) equivalent to: + +``` ruby +class UsingAnArrayAsAStack < RSpec::Core::ExampleGroup + def build_stack + [] + end + + def before_example + @stack = build_stack + end + + def it_is_initially_empty + expect(@stack).to be_empty + end + + class AfterAnItemHasBeenPushed < self + def build_stack + super.push :item + end + + def it_allows_the_pushed_item_to_be_popped + expect(@stack.pop).to eq(:item) + end + end +end +``` + ## The `rspec` Command When you install the rspec-core gem, it installs the `rspec` executable, From 302bfbaee8095fa800bc80b426a56f35942b4952 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 6 Mar 2015 08:22:02 -0800 Subject: [PATCH 063/258] Clarify what an example is. [ci skip] --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d17adcf4eb..702b769a66 100644 --- a/README.md +++ b/README.md @@ -176,8 +176,9 @@ RSpec has two scopes: loaded. The block is evaluated in the context of a subclass of `RSpec::Core::ExampleGroup`, or a subclass of the parent example group when you're nesting them. -* **Example**: Examples, and any other blocks with per-example semantics - (such as a `before(:example)` hook), are evaluated in the context of +* **Example**: Examples -- typically defined by an `it` block -- and any other + blocks with per-example semantics -- such as a `before(:example)` hook -- are + evaluated in the context of an _instance_ of the example group class to which the example belongs. Examples are _not_ executed when the spec file is loaded; instead, RSpec waits to run any examples until all spec files have been loaded, From 241f6971fa7c3b53bc2bec996edb1f13cb08a230 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 6 Mar 2015 11:28:48 -0800 Subject: [PATCH 064/258] Remove use of helper method override w/o `super`. --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 702b769a66..0cf42bdda1 100644 --- a/README.md +++ b/README.md @@ -201,8 +201,8 @@ RSpec.describe "Using an array as a stack" do end context "after an item has been pushed" do - def build_stack - super.push :item + before(:example) do + @stack.push :item end it 'allows the pushed item to be popped' do @@ -220,7 +220,7 @@ class UsingAnArrayAsAStack < RSpec::Core::ExampleGroup [] end - def before_example + def before_example_1 @stack = build_stack end @@ -229,8 +229,8 @@ class UsingAnArrayAsAStack < RSpec::Core::ExampleGroup end class AfterAnItemHasBeenPushed < self - def build_stack - super.push :item + def before_example_2 + @stack.push :item end def it_allows_the_pushed_item_to_be_popped From 82bd95e59ef80001095f72dc6d80ed191edbf1a3 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 6 Mar 2015 11:32:57 -0800 Subject: [PATCH 065/258] Add snippet showing how the examples would get run. --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 0cf42bdda1..1045ca6068 100644 --- a/README.md +++ b/README.md @@ -240,6 +240,19 @@ class UsingAnArrayAsAStack < RSpec::Core::ExampleGroup end ``` +To run these examples, RSpec would (roughly) do the following: + +``` ruby +example_1 = UsingAnArrayAsAStack.new +example_1.before_example_1 +example_1.it_is_initially_empty + +example_2 = UsingAnArrayAsAStack::AfterAnItemHasBeenPushed.new +example_2.before_example_1 +example_2.before_example_2 +example_2.it_allows_the_pushed_item_to_be_popped +``` + ## The `rspec` Command When you install the rspec-core gem, it installs the `rspec` executable, From 0a4937b8e7986f6702d0750dcc2621284b2a687d Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Tue, 3 Mar 2015 18:03:46 -0800 Subject: [PATCH 066/258] Apply tag filters only to files not having an id or location filter. Fixes #1889. --- lib/rspec/core/filter_manager.rb | 43 +++++++++++++++----------- lib/rspec/core/metadata_filter.rb | 2 +- spec/integration/filtering_spec.rb | 22 +++++++++++++ spec/rspec/core/filter_manager_spec.rb | 37 ++++++++++++++++++++++ 4 files changed, 85 insertions(+), 19 deletions(-) diff --git a/lib/rspec/core/filter_manager.rb b/lib/rspec/core/filter_manager.rb index a90718f59c..72d63f1774 100644 --- a/lib/rspec/core/filter_manager.rb +++ b/lib/rspec/core/filter_manager.rb @@ -35,12 +35,15 @@ def prune(examples) examples = prune_conditionally_filtered_examples(examples) if inclusions.standalone? - examples.select { |e| include?(e) } + examples.select { |e| inclusions.include_example?(e) } else - locations = inclusions.fetch(:locations) { Hash.new([]) } - ids = inclusions.fetch(:ids) { Hash.new([]) } + locations, ids, non_scoped_inclusions = inclusions.split_file_scoped_rules - examples.select { |e| priority_include?(e, ids, locations) || (!exclude?(e) && include?(e)) } + examples.select do |ex| + file_scoped_include?(ex.metadata, ids, locations) do + !exclusions.include_example?(ex) && non_scoped_inclusions.include_example?(ex) + end + end end end @@ -76,14 +79,6 @@ def add_path_to_arrays_filter(filter_key, path, values) inclusions.add(filter_key => filter) end - def exclude?(example) - exclusions.include_example?(example) - end - - def include?(example) - inclusions.include_example?(example) - end - def prune_conditionally_filtered_examples(examples) examples.reject do |ex| meta = ex.metadata @@ -96,10 +91,14 @@ def prune_conditionally_filtered_examples(examples) # and there is a `:slow => true` exclusion filter), but only for specs # defined in the same file as the location filters. Excluded specs in # other files should still be excluded. - def priority_include?(example, ids, locations) - return true if MetadataFilter.filter_applies?(:ids, ids, example.metadata) - return false if locations[example.metadata[:absolute_file_path]].empty? - MetadataFilter.filter_applies?(:locations, locations, example.metadata) + def file_scoped_include?(ex_metadata, ids, locations) + no_location_filters = locations[ex_metadata[:absolute_file_path]].empty? + no_id_filters = ids[ex_metadata[:rerun_file_path]].empty? + + return yield if no_location_filters && no_id_filters + + MetadataFilter.filter_applies?(:ids, ids, ex_metadata) || + MetadataFilter.filter_applies?(:locations, locations, ex_metadata) end end @@ -119,8 +118,8 @@ def self.build [exclusions, inclusions] end - def initialize(*args, &block) - @rules = Hash.new(*args, &block) + def initialize(rules={}) + @rules = rules end def add(updated) @@ -196,6 +195,14 @@ def standalone? is_standalone_filter?(@rules) end + def split_file_scoped_rules + rules_dup = @rules.dup + locations = rules_dup.delete(:locations) { Hash.new([]) } + ids = rules_dup.delete(:ids) { Hash.new([]) } + + return locations, ids, self.class.new(rules_dup) + end + private def apply_standalone_filter(updated) diff --git a/lib/rspec/core/metadata_filter.rb b/lib/rspec/core/metadata_filter.rb index e529e3ae57..9c58d7f194 100644 --- a/lib/rspec/core/metadata_filter.rb +++ b/lib/rspec/core/metadata_filter.rb @@ -53,7 +53,7 @@ def id_filter_applies?(rerun_paths_to_scoped_ids, metadata) def location_filter_applies?(locations, metadata) line_numbers = example_group_declaration_lines(locations, metadata) - line_numbers.empty? || line_number_filter_applies?(line_numbers, metadata) + line_number_filter_applies?(line_numbers, metadata) end def line_number_filter_applies?(line_numbers, metadata) diff --git a/spec/integration/filtering_spec.rb b/spec/integration/filtering_spec.rb index a8f3284fdc..d9a8a1791d 100644 --- a/spec/integration/filtering_spec.rb +++ b/spec/integration/filtering_spec.rb @@ -123,6 +123,28 @@ def run_rerun_command_for_failing_spec expect(last_cmd_stdout).to match(/3 examples, 0 failures/) expect(last_cmd_stdout).not_to match(/fails/) end + + it 'applies command line tag filters only to files that lack a line number filter' do + write_file_formatted "spec/file_1_spec.rb", " + RSpec.describe 'File 1' do + it('is selected by line') { } + it('is not selected', :tag) { } + end + " + + write_file_formatted "spec/file_2_spec.rb", " + RSpec.describe 'File 2' do + it('is not selected') { } + it('is selected by tag', :tag) { } + end + " + + run_command "spec/file_1_spec.rb:2 spec/file_2_spec.rb --tag tag -fd" + expect(last_cmd_stdout).to include( + "2 examples, 0 failures", + "is selected by line", "is selected by tag" + ).and exclude("not selected") + end end context "passing example ids at the command line" do diff --git a/spec/rspec/core/filter_manager_spec.rb b/spec/rspec/core/filter_manager_spec.rb index 49f693ef2d..ab46d9cf0d 100644 --- a/spec/rspec/core/filter_manager_spec.rb +++ b/spec/rspec/core/filter_manager_spec.rb @@ -129,6 +129,25 @@ def example_with(*args) expect(filter_manager.prune([included, excluded])).to eq([included]) end + + it "still applies inclusion filters to examples from files with no #{type} filters" do + group = RSpec.describe("group") + included_via_loc_or_id = group.example("inc via #{type}"); line = __LINE__ + excluded_via_loc_or_id = group.example("exc via #{type}", :foo) + + included_via_tag, excluded_via_tag = instance_eval <<-EOS, "some/other_spec.rb", 1 + group = RSpec.describe("group") + [group.example("inc via tag", :foo), group.example("exc via tag")] + EOS + + add_filter(:line_number => line, :scoped_id => "1:1") + filter_manager.include_with_low_priority :foo => true + + expect(filter_manager.prune([ + included_via_loc_or_id, excluded_via_loc_or_id, + included_via_tag, excluded_via_tag + ]).map(&:description)).to eq([included_via_loc_or_id, included_via_tag].map(&:description)) + end end describe "location filtering" do @@ -299,6 +318,24 @@ def add_filter(options) filter_manager.add_ids(Metadata.relative_path(__FILE__), %w[ 2 ]) expect(filter_manager.prune([ex_1, ex_2, ex_3])).to eq([ex_2, ex_3]) end + + it 'uses the rerun file path when applying the id filter' do + ex_1, ex_2 = instance_eval <<-EOS, "./some/spec.rb", 1 + ex_1 = ex_2 = nil + + RSpec.shared_examples "shared" do + ex_1 = example("ex 1") + ex_2 = example("ex 2") + end + + [ex_1, ex_2] + EOS + + RSpec.describe { include_examples "shared" } + + filter_manager.add_ids(__FILE__, %w[ 1:1 ]) + expect(filter_manager.prune([ex_1, ex_2]).map(&:description)).to eq([ex_1].map(&:description)) + end end end From c62fcdefbce884d24cd92f01e1beb9a93f55d289 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Mon, 9 Mar 2015 21:42:27 -0700 Subject: [PATCH 067/258] Rename argument to reflect what it actually is. In my first pass I passed in the index but later changed to passing in an index provider and forgot to update the argument name. --- lib/rspec/core/metadata.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rspec/core/metadata.rb b/lib/rspec/core/metadata.rb index 98d810c094..d40c9747df 100644 --- a/lib/rspec/core/metadata.rb +++ b/lib/rspec/core/metadata.rb @@ -210,7 +210,7 @@ def ensure_valid_user_keys # @private class ExampleHash < HashPopulator - def self.create(group_metadata, user_metadata, index, description, block) + def self.create(group_metadata, user_metadata, index_provider, description, block) example_metadata = group_metadata.dup group_metadata = Hash.new(&ExampleGroupHash.backwards_compatibility_default_proc do |hash| hash[:parent_example_group] @@ -222,7 +222,7 @@ def self.create(group_metadata, user_metadata, index, description, block) example_metadata.delete(:parent_example_group) description_args = description.nil? ? [] : [description] - hash = new(example_metadata, user_metadata, index, description_args, block) + hash = new(example_metadata, user_metadata, index_provider, description_args, block) hash.populate hash.metadata end From 96c7590b8d1afed1a5cb8ad1b9963a0b1eec719c Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Thu, 26 Feb 2015 18:04:50 -0800 Subject: [PATCH 068/258] Cleanup formatter support a bit. - Stop memoizing `example`. It made it difficult to construct multiple different examples. - Explicitly create the examples where they are needed. --- spec/rspec/core/notifications_spec.rb | 2 ++ spec/rspec/core/reporter_spec.rb | 2 ++ spec/support/formatter_support.rb | 46 ++++++++++++--------------- 3 files changed, 24 insertions(+), 26 deletions(-) diff --git a/spec/rspec/core/notifications_spec.rb b/spec/rspec/core/notifications_spec.rb index cbcf5c0ca4..499980a4ab 100644 --- a/spec/rspec/core/notifications_spec.rb +++ b/spec/rspec/core/notifications_spec.rb @@ -4,9 +4,11 @@ RSpec.describe "FailedExampleNotification" do include FormatterSupport + let(:example) { new_example } let(:notification) { ::RSpec::Core::Notifications::FailedExampleNotification.new(example) } before do + allow(example.execution_result).to receive(:exception) { exception } example.metadata[:absolute_file_path] = __FILE__ end diff --git a/spec/rspec/core/reporter_spec.rb b/spec/rspec/core/reporter_spec.rb index 8df63a050b..fefd5fac09 100644 --- a/spec/rspec/core/reporter_spec.rb +++ b/spec/rspec/core/reporter_spec.rb @@ -66,6 +66,7 @@ module RSpec::Core it "passes messages to that formatter" do formatter = double("formatter", :example_started => nil) reporter.register_listener formatter, :example_started + example = new_example expect(formatter).to receive(:example_started) do |notification| expect(notification.example).to eq example @@ -121,6 +122,7 @@ module RSpec::Core context "given multiple formatters" do it "passes messages to all formatters" do formatters = (1..2).map { double("formatter", :example_started => nil) } + example = new_example formatters.each do |formatter| expect(formatter).to receive(:example_started) do |notification| diff --git a/spec/support/formatter_support.rb b/spec/support/formatter_support.rb index 2b9dbe0840..93e5335ddf 100644 --- a/spec/support/formatter_support.rb +++ b/spec/support/formatter_support.rb @@ -200,34 +200,28 @@ def formatter end end - def example - @example ||= - begin - result = instance_double(RSpec::Core::Example::ExecutionResult, - :pending_fixed? => false, - :example_skipped? => false, - :status => :passed - ) - allow(result).to receive(:exception) { exception } - instance_double(RSpec::Core::Example, - :description => "Example", - :full_description => "Example", - :execution_result => result, - :location => "", - :location_rerun_argument => "", - :metadata => { - :shared_group_inclusion_backtrace => [] - } - ) - end - end - - def exception - Exception.new + def new_example(metadata = {}) + result = instance_double(RSpec::Core::Example::ExecutionResult, + :pending_fixed? => false, + :example_skipped? => false, + :status => :passed, + :exception => Exception.new + ) + + instance_double(RSpec::Core::Example, + :description => "Example", + :full_description => "Example", + :execution_result => result, + :location => "", + :location_rerun_argument => "", + :metadata => { + :shared_group_inclusion_backtrace => [] + }.merge(metadata) + ) end def examples(n) - (1..n).map { example } + Array.new(n) { new_example } end def group @@ -242,7 +236,7 @@ def stop_notification ::RSpec::Core::Notifications::ExamplesNotification.new reporter end - def example_notification(specific_example = example) + def example_notification(specific_example = new_example) ::RSpec::Core::Notifications::ExampleNotification.for specific_example end From bd774ef40764d567ea385513e54a2ac673c2596c Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Sat, 7 Mar 2015 18:42:14 -0800 Subject: [PATCH 069/258] Use a real result object instead of a double. The result object is really just a value object, so why fake it out? --- spec/support/formatter_support.rb | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/spec/support/formatter_support.rb b/spec/support/formatter_support.rb index 93e5335ddf..95a8377689 100644 --- a/spec/support/formatter_support.rb +++ b/spec/support/formatter_support.rb @@ -201,12 +201,11 @@ def formatter end def new_example(metadata = {}) - result = instance_double(RSpec::Core::Example::ExecutionResult, - :pending_fixed? => false, - :example_skipped? => false, - :status => :passed, - :exception => Exception.new - ) + metadata = metadata.dup + result = RSpec::Core::Example::ExecutionResult.new + result.started_at = ::Time.now + result.record_finished(metadata.delete(:status) { :passed }, ::Time.now) + result.exception = Exception.new if result.status == :failed instance_double(RSpec::Core::Example, :description => "Example", From 0529b6eb0a6d37346fb6b8f84eb8bec663d15d52 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Thu, 5 Mar 2015 20:45:50 -0800 Subject: [PATCH 070/258] Implement dumper/parser for example statuses. --- lib/rspec/core/example_status_persister.rb | 103 ++++++++++++++++++ .../core/example_status_persister_spec.rb | 88 +++++++++++++++ 2 files changed, 191 insertions(+) create mode 100644 lib/rspec/core/example_status_persister.rb create mode 100644 spec/rspec/core/example_status_persister_spec.rb diff --git a/lib/rspec/core/example_status_persister.rb b/lib/rspec/core/example_status_persister.rb new file mode 100644 index 0000000000..518bfdc5bc --- /dev/null +++ b/lib/rspec/core/example_status_persister.rb @@ -0,0 +1,103 @@ +module RSpec + module Core + class ExampleStatusPersister + end + + # Dumps a list of hashes in a pretty, human readable format + # for later parsing. The hashes are expected to have symbol + # keys and string values, and each hash should have the same + # set of keys. + # @private + class ExampleStatusDumper + def self.dump(examples) + new(examples).dump + end + + def initialize(examples) + @examples = examples + end + + def dump + return nil if @examples.empty? + (formatted_header_rows + formatted_value_rows).join("\n") << "\n" + end + + private + + def formatted_header_rows + @formatted_header_rows ||= begin + dividers = column_widths.map { |w| "-" * w } + [formatted_row_from(headers.map(&:to_s)), formatted_row_from(dividers)] + end + end + + def formatted_value_rows + @foramtted_value_rows ||= rows.map do |row| + formatted_row_from(row) + end + end + + def rows + @rows ||= @examples.map { |ex| ex.values_at(*headers) } + end + + def formatted_row_from(row_values) + padded_values = row_values.each_with_index.map do |value, index| + value.ljust(column_widths[index]) + end + + padded_values.join(" | ") << " |" + end + + def headers + @headers ||= @examples.first.keys + end + + def column_widths + @column_widths ||= begin + value_sets = rows.transpose + + headers.each_with_index.map do |header, index| + values = value_sets[index] << header.to_s + values.map(&:length).max + end + end + end + end + + # Parses a string that has been previously dumped by ExampleStatusDumper. + # Note that this parser is a bit naive in that it does a simple split on + # "\n" and " | ", with no concern for handling escaping. For now, that's + # OK because the values we plan to persist (example id, status, and perhaps + # example duration) are highly unlikely to contain "\n" or " | " -- after + # all, who puts those in file names? + # @private + class ExampleStatusParser + def self.parse(string) + new(string).parse + end + + def initialize(string) + @header_line, _, *@row_lines = string.lines.to_a + end + + def parse + @row_lines.map { |line| parse_row(line) } + end + + private + + def parse_row(line) + Hash[headers.zip(split_line(line))] + end + + def headers + @headers ||= split_line(@header_line).map(&:to_sym) + end + + def split_line(line) + line.split(/\s+\|\s+/) + end + end + end +end diff --git a/spec/rspec/core/example_status_persister_spec.rb b/spec/rspec/core/example_status_persister_spec.rb new file mode 100644 index 0000000000..044626c229 --- /dev/null +++ b/spec/rspec/core/example_status_persister_spec.rb @@ -0,0 +1,88 @@ +require 'rspec/core/example_status_persister' + +module RSpec::Core + RSpec.describe "Example status serialization" do + it 'serializes the provided example statuses in a human readable format' do + examples = [ + { :example_id => "./spec/unit/foo_spec.rb[1:1]", :status => 'passed' }, + { :example_id => "./spec/unit/foo_spec.rb[1:2]", :status => 'pending' }, + { :example_id => "./spec/integration/foo_spec.rb[1:2]", :status => 'failed' } + ] + + produce_expected_output = eq(unindent(<<-EOS)) + example_id | status | + ----------------------------------- | ------- | + ./spec/unit/foo_spec.rb[1:1] | passed | + ./spec/unit/foo_spec.rb[1:2] | pending | + ./spec/integration/foo_spec.rb[1:2] | failed | + EOS + + if RUBY_VERSION == '1.8.7' # unordered hashes :(. + produce_expected_output |= eq(unindent(<<-EOS)) + status | example_id | + ------- | ----------------------------------- | + passed | ./spec/unit/foo_spec.rb[1:1] | + pending | ./spec/unit/foo_spec.rb[1:2] | + failed | ./spec/integration/foo_spec.rb[1:2] | + EOS + end + + expect(dump(examples)).to produce_expected_output + end + + it 'takes the column headers into account when sizing the columns' do + examples = [ + { :long_key => '12', :a => '20' }, + { :long_key => '120', :a => '2' } + ] + + produce_expected_output = eq(unindent(<<-EOS)) + long_key | a | + -------- | -- | + 12 | 20 | + 120 | 2 | + EOS + + if RUBY_VERSION == '1.8.7' # unordered hashes :(. + produce_expected_output |= eq(unindent(<<-EOS)) + a | long_key | + -- | -------- | + 20 | 12 | + 2 | 120 | + EOS + end + + expect(dump(examples)).to produce_expected_output + end + + it 'can round trip through the dumper and parser' do + examples = [ + { :example_id => "./spec/unit/foo_spec.rb[1:1]", :status => 'passed' }, + { :example_id => "./spec/unit/foo_spec.rb[1:2]", :status => 'pending' }, + { :example_id => "./spec/integration/foo_spec.rb[1:2]", :status => 'failed' } + ] + + round_tripped = parse(dump(examples)) + expect(round_tripped).to eq(examples) + end + + it 'produces nothing when given nothing' do + expect(dump([])).to eq(nil) + end + + # Intended for use with indented heredocs. + # taken from Ruby Tapas: + # https://rubytapas.dpdcart.com/subscriber/post?id=616#files + def unindent(s) + s.gsub(/^#{s.scan(/^[ \t]+(?=\S)/).min}/, "") + end + + def dump(examples) + ExampleStatusDumper.dump(examples) + end + + def parse(string) + ExampleStatusParser.parse(string) + end + end +end From ec07ce819de9dc412afc02af058a2502d4194895 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Thu, 5 Mar 2015 21:52:47 -0800 Subject: [PATCH 071/258] Implement merging algorithm. --- lib/rspec/core/example_status_persister.rb | 81 +++++++++++ .../core/example_status_persister_spec.rb | 131 ++++++++++++++++++ 2 files changed, 212 insertions(+) diff --git a/lib/rspec/core/example_status_persister.rb b/lib/rspec/core/example_status_persister.rb index 518bfdc5bc..e6180db7be 100644 --- a/lib/rspec/core/example_status_persister.rb +++ b/lib/rspec/core/example_status_persister.rb @@ -3,6 +3,87 @@ module Core class ExampleStatusPersister end + # Merges together a list of example statuses from this run + # and a list from previous runs (presumably loaded from disk). + # Each example status object is expected to be a hash with + # at least an `:example_id` and a `:status` key. Examples that + # were loaded but not executed (due to filtering, `--fail-fast` + # or whatever) should have a `:status` of `UNKNOWN_STATUS`. + # + # This willl produce a new list that: + # - Will be missing examples from previous runs that we know for sure + # no longer exist. + # - Will have the latest known status for any examples that either + # definitively do exist or may still exist. + # - Is sorted by file name and example definition order, so that + # the saved file is easily scannable if users want to inspect it. + # @private + class ExampleStatusMerger + def self.merge(this_run, from_previous_runs) + new(this_run, from_previous_runs).merge + end + + def initialize(this_run, from_previous_runs) + @this_run = hash_from(this_run) + @from_previous_runs = hash_from(from_previous_runs) + @file_exists_cache = Hash.new { |hash, file| hash[file] = File.exist?(file) } + end + + def merge + delete_previous_examples_that_no_longer_exist + + @this_run.merge(@from_previous_runs) do |_ex_id, new, old| + new.fetch(:status) == UNKNOWN_STATUS ? old : new + end.values.sort_by(&method(:sort_value_from)) + end + + UNKNOWN_STATUS = "unknown".freeze + + private + + def hash_from(example_list) + example_list.inject({}) do |hash, example| + hash[example.fetch(:example_id)] = example + hash + end + end + + def delete_previous_examples_that_no_longer_exist + @from_previous_runs.delete_if do |ex_id, _| + example_must_no_longer_exist?(ex_id) + end + end + + def example_must_no_longer_exist?(ex_id) + # Obviously, it exists if it was loaded for this spec run... + return false if @this_run.key?(ex_id) + + spec_file = spec_file_from(ex_id) + + # `this_run` includes examples that were loaded but not executed. + # Given that, if the spec file for this example was loaded, + # but the id does not still exist, it's safe to assume that + # the example must no longer exist. + return true if loaded_spec_files.include?(spec_file) + + # The example may still exist as long as the file exists... + !@file_exists_cache[spec_file] + end + + def loaded_spec_files + @loaded_spec_files ||= Set.new(@this_run.keys.map(&method(:spec_file_from))) + end + + def spec_file_from(ex_id) + ex_id.split("[").first + end + + def sort_value_from(example) + file, scoped_id = example.fetch(:example_id).split(Configuration::ON_SQUARE_BRACKETS) + [file, *scoped_id.split(":").map(&method(:Integer))] + end + end + # Dumps a list of hashes in a pretty, human readable format # for later parsing. The hashes are expected to have symbol # keys and string values, and each hash should have the same diff --git a/spec/rspec/core/example_status_persister_spec.rb b/spec/rspec/core/example_status_persister_spec.rb index 044626c229..fd7ec8f226 100644 --- a/spec/rspec/core/example_status_persister_spec.rb +++ b/spec/rspec/core/example_status_persister_spec.rb @@ -1,6 +1,137 @@ require 'rspec/core/example_status_persister' module RSpec::Core + RSpec.describe "Example status merging" do + let(:existing_spec_file) { Metadata.relative_path(__FILE__) } + + context "when no examples from this or previous runs are given" do + it "returns an empty array" do + merged = merge(:this_run => [], :from_previous_runs => []) + expect(merged).to eq([]) + end + end + + context "when there are no examples from previous runs" do + it "returns the examples from this run" do + this_run = [ + example(existing_spec_file, "1:1", "passed"), + example(existing_spec_file, "1:2", "failed") + ] + + merged = merge(:this_run => this_run, :from_previous_runs => []) + expect(merged).to match_array(this_run) + end + end + + context "when there are no examples from this run" do + it "returns the examples from the previous runs" do + from_previous_runs = [ + example(existing_spec_file, "1:1", "passed"), + example(existing_spec_file, "1:2", "failed") + ] + + merged = merge(:this_run => [], :from_previous_runs => from_previous_runs) + expect(merged).to match_array(from_previous_runs) + end + end + + context "for examples that are only in the set for this run" do + it "takes them indiscriminately, even if they did not execute" do + this_run = [ example(existing_spec_file, "1:1", ExampleStatusMerger::UNKNOWN_STATUS) ] + + merged = merge(:this_run => this_run, :from_previous_runs => []) + expect(merged).to match_array(this_run) + end + end + + context "for examples that are only in the set for previous runs" do + context "if there are other examples from this run for the same file " do + it "deletes them since the examples must no longer exist" do + this_run = [ example(existing_spec_file, "1:1", "passed") ] + from_previous_runs = [ example(existing_spec_file, "1:2", "failed") ] + + merged = merge(:this_run => this_run, :from_previous_runs => from_previous_runs) + expect(merged).to match_array(this_run) + end + end + + context "if there are no other examples from this run for the same file" do + it "deletes them if the file no longer exist" do + from_previous_runs = [ example("./some/deleted_path/foo_spec.rb", "1:2", "failed") ] + + merged = merge(:this_run => [], :from_previous_runs => from_previous_runs) + expect(merged).to eq([]) + end + + it "keeps them if the file exists because the examples may still exist" do + from_previous_runs = [ example(existing_spec_file, "1:2", "failed") ] + + merged = merge(:this_run => [], :from_previous_runs => from_previous_runs) + expect(merged).to eq(from_previous_runs) + end + end + end + + context "for examples that are in both sets" do + it "takes the status from this run as long as the example executed" do + this_run = [ example("foo_spec.rb", "1:1", "passed") ] + from_previous_runs = [ example("foo_spec.rb", "1:1", "failed") ] + + merged = merge(:this_run => this_run, :from_previous_runs => from_previous_runs) + expect(merged).to match_array(this_run) + end + + it "takes the status from previous runs if the example was loaded but did not execute" do + this_run = [ example("foo_spec.rb", "1:1", ExampleStatusMerger::UNKNOWN_STATUS) ] + from_previous_runs = [ example("foo_spec.rb", "1:1", "failed") ] + + merged = merge(:this_run => this_run, :from_previous_runs => from_previous_runs) + expect(merged).to match_array(from_previous_runs) + end + end + + it 'sorts the returned examples to make the saved file more easily scannable' do + this_run = [ + ex_c_1_1 = example("c_spec.rb", "1:1", "passed"), + ex_a_1_2 = example("a_spec.rb", "1:2", "failed"), + ex_a_1_10 = example("a_spec.rb", "1:10", "failed"), + ex_a_1_9 = example("a_spec.rb", "1:9", "failed"), + ] + + merged = merge(:this_run => this_run, :from_previous_runs => []) + expect(merged).to eq([ ex_a_1_2, ex_a_1_9, ex_a_1_10, ex_c_1_1 ]) + end + + it "preserves any extra attributes include in the example hashes" do + this_run = [ + example(existing_spec_file, "1:1", "passed", :foo => 23), + example(existing_spec_file, "1:2", "failed", :bar => 12) + ] + + from_previous_runs = [ + example(existing_spec_file, "1:1", "passed", :foo => -23), + example(existing_spec_file, "1:2", "failed", :bar => -12) + ] + + merged = merge(:this_run => this_run, :from_previous_runs => from_previous_runs) + expect(merged).to contain_exactly( + a_hash_including(:foo => 23), + a_hash_including(:bar => 12) + ) + end + + def example(file, scoped_id, status, extras = {}) + { :example_id => "#{file}[#{scoped_id}]", :status => status }.merge(extras) + end + + def merge(options) + ExampleStatusMerger.merge( + options.fetch(:this_run), + options.fetch(:from_previous_runs) + ) + end + end + RSpec.describe "Example status serialization" do it 'serializes the provided example statuses in a human readable format' do examples = [ From 6c9d438ee96e4e6690fd47e2d96af475e9c52547 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Sat, 7 Mar 2015 07:55:08 -0800 Subject: [PATCH 072/258] Implement ExampleStatusPersister. --- lib/rspec/core/example_status_persister.rb | 53 +++++++++ .../core/example_status_persister_spec.rb | 105 ++++++++++++++++++ 2 files changed, 158 insertions(+) diff --git a/lib/rspec/core/example_status_persister.rb b/lib/rspec/core/example_status_persister.rb index e6180db7be..2867b78d12 100644 --- a/lib/rspec/core/example_status_persister.rb +++ b/lib/rspec/core/example_status_persister.rb @@ -1,6 +1,59 @@ +RSpec::Support.require_rspec_support "directory_maker" + module RSpec module Core + # Persists example ids and their statuses so that we can filter + # to just the ones that failed the last time they ran. + # @private class ExampleStatusPersister + def self.load_from(file_name) + return [] unless File.exist?(file_name) + ExampleStatusParser.parse(File.read(file_name)) + end + + def self.persist(examples, file_name) + new(examples, file_name).persist + end + + def initialize(examples, file_name) + @examples = examples + @file_name = file_name + end + + def persist + write dumped_statuses + end + + private + + def write(statuses) + RSpec::Support::DirectoryMaker.mkdir_p(File.dirname(@file_name)) + File.open(@file_name, "w") { |f| f.write(statuses) } + end + + def dumped_statuses + ExampleStatusDumper.dump(merged_statuses) + end + + def merged_statuses + ExampleStatusMerger.merge(statuses_from_this_run, statuses_from_previous_runs) + end + + def statuses_from_this_run + @examples.map do |ex| + result = ex.execution_result + + { + :example_id => ex.id, + :status => result.status ? result.status.to_s : ExampleStatusMerger::UNKNOWN_STATUS, + :run_time => result.run_time ? Formatters::Helpers.format_duration(result.run_time) : "" + } + end + end + + def statuses_from_previous_runs + self.class.load_from(@file_name) + end end # Merges together a list of example statuses from this run diff --git a/spec/rspec/core/example_status_persister_spec.rb b/spec/rspec/core/example_status_persister_spec.rb index fd7ec8f226..692fdfe07d 100644 --- a/spec/rspec/core/example_status_persister_spec.rb +++ b/spec/rspec/core/example_status_persister_spec.rb @@ -1,6 +1,111 @@ require 'rspec/core/example_status_persister' +require 'tempfile' module RSpec::Core + RSpec.describe "Example status persisting" do + it 'can load a previously persisted set of example statuses from disk' do + examples = [ + { :example_id => "spec_1.rb[1:1]", :status => "passed" }, + { :example_id => "spec_1.rb[1:2]", :status => "failed" } + ] + + temp_file = Tempfile.new("example_statuses.txt") + temp_file.write(ExampleStatusDumper.dump(examples)) + temp_file.close + + loaded = ExampleStatusPersister.load_from(temp_file.path) + expect(loaded).to eq(examples) + end + + it 'returns `[]` from `load_from` when the named file does not exist' do + expect(ExampleStatusPersister.load_from("./some/missing/path.txt")).to eq([]) + end + + describe "persisting example statuses" do + include FormatterSupport + + def new_example(id, metadata = {}) + super(metadata).tap do |ex| + allow(ex).to receive_messages(:id => id) + end + end + + let(:file) { Tempfile.new("example_statuses.txt") } + let(:existing_spec_file) { Metadata.relative_path(__FILE__) } + + it 'writes the given example statuses to disk' do + ex_1 = new_example("spec_1.rb[1:1]", :status => :passed) + ex_2 = new_example("spec_1.rb[1:2]", :status => :failed) + + ExampleStatusPersister.persist([ex_1, ex_2], file.path) + loaded = ExampleStatusPersister.load_from(file.path) + + expect(loaded).to contain_exactly( + a_hash_including(:example_id => ex_1.id, :status => "passed"), + a_hash_including(:example_id => ex_2.id, :status => "failed") + ) + end + + it 'creates any necessary intermediary directories' do + path = File.join("#{file.path}-some", "subdirectory", "examples.txt") + ex_1 = new_example("spec_1.rb[1:1]", :status => :passed) + + ExampleStatusPersister.persist([ex_1], path) + loaded = ExampleStatusPersister.load_from(path) + + expect(loaded).to contain_exactly( + a_hash_including(:example_id => ex_1.id, :status => "passed") + ) + end + + it 'merges the example statuses with the existing records in the named file' do + ex_1 = new_example("#{existing_spec_file}[1:1]", :status => :passed) + ex_2 = new_example("spec_1.rb[1:1]", :status => :failed) + + ExampleStatusPersister.persist([ex_1], file.path) + ExampleStatusPersister.persist([ex_2], file.path) + loaded = ExampleStatusPersister.load_from(file.path) + + expect(loaded).to contain_exactly( + a_hash_including(:example_id => ex_1.id, :status => "passed"), + a_hash_including(:example_id => ex_2.id, :status => "failed") + ) + end + + it 'includes the spec run times so users can use it for their own purposes' do + ex_1 = new_example("spec_1.rb[1:1]", :status => :passed) + allow(ex_1.execution_result).to receive(:run_time) { 3.0 } + + ExampleStatusPersister.persist([ex_1], file.path) + loaded = ExampleStatusPersister.load_from(file.path) + + expect(loaded).to match [ a_hash_including(:run_time => "3 seconds") ] + end + + it "persists a loaded but unexecuted example with an #{ExampleStatusMerger::UNKNOWN_STATUS} status" do + ex_1 = RSpec.describe.example + + ExampleStatusPersister.persist([ex_1], file.path) + loaded = ExampleStatusPersister.load_from(file.path) + + expect(loaded).to match [ a_hash_including( + :example_id => ex_1.id, :status => ExampleStatusMerger::UNKNOWN_STATUS + ) ] + end + + it "persists a skipped example properly" do + group = RSpec.describe + ex_1 = group.example("foo", :skip) + group.run + + ExampleStatusPersister.persist([ex_1], file.path) + loaded = ExampleStatusPersister.load_from(file.path) + + expect(loaded).to match [ a_hash_including( :example_id => ex_1.id, :status => "pending") ] + end + end + end + RSpec.describe "Example status merging" do let(:existing_spec_file) { Metadata.relative_path(__FILE__) } From 597b9fd6d6c5b89d65c857de81e7386592a349fb Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Mon, 9 Mar 2015 08:10:10 -0700 Subject: [PATCH 073/258] Fix parser to handle blank values properly. --- lib/rspec/core/example_status_persister.rb | 4 ++-- spec/rspec/core/example_status_persister_spec.rb | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/rspec/core/example_status_persister.rb b/lib/rspec/core/example_status_persister.rb index 2867b78d12..5e9a110a65 100644 --- a/lib/rspec/core/example_status_persister.rb +++ b/lib/rspec/core/example_status_persister.rb @@ -226,11 +226,11 @@ def parse_row(line) end def headers - @headers ||= split_line(@header_line).map(&:to_sym) + @headers ||= split_line(@header_line).grep(/\S/).map(&:to_sym) end def split_line(line) - line.split(/\s+\|\s+/) + line.split(/\s+\|\s+?/, -1) end end end diff --git a/spec/rspec/core/example_status_persister_spec.rb b/spec/rspec/core/example_status_persister_spec.rb index 692fdfe07d..69655e8d1d 100644 --- a/spec/rspec/core/example_status_persister_spec.rb +++ b/spec/rspec/core/example_status_persister_spec.rb @@ -302,6 +302,16 @@ def merge(options) expect(round_tripped).to eq(examples) end + it 'can round trip blank values through the dumper and parser' do + examples = [ + { :example_id => "./spec/unit/foo_spec.rb[1:1]", :run_time => '1 second' }, + { :example_id => "./spec/unit/foo_spec.rb[1:2]", :run_time => '' } + ] + + round_tripped = parse(dump(examples)) + expect(round_tripped).to eq(examples) + end + it 'produces nothing when given nothing' do expect(dump([])).to eq(nil) end From ee490ccc8807abab205eaa7b00c38803f0d3a6a7 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Sun, 8 Mar 2015 21:48:10 -0700 Subject: [PATCH 074/258] Add config option for example status persistence file path. --- .gitignore | 1 + lib/rspec/core.rb | 2 ++ lib/rspec/core/configuration.rb | 4 ++++ lib/rspec/core/runner.rb | 15 ++++++++++++++- spec/rspec/core/runner_spec.rb | 30 ++++++++++++++++++++++++++++++ spec/spec_helper.rb | 2 ++ 6 files changed, 53 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 2d9a1eaf52..540b64d91d 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ Gemfile-custom .idea bundle .rspec-local +examples.txt diff --git a/lib/rspec/core.rb b/lib/rspec/core.rb index b02ea729e9..4fb631e023 100644 --- a/lib/rspec/core.rb +++ b/lib/rspec/core.rb @@ -146,6 +146,8 @@ def self.world # Namespace for the rspec-core code. module Core + autoload :ExampleStatusPersister, "rspec/core/example_status_persister" + # @private # This avoids issues with reporting time caused by examples that # change the value/meaning of Time.now without properly restoring diff --git a/lib/rspec/core/configuration.rb b/lib/rspec/core/configuration.rb index fe49573a5f..f256d1b008 100644 --- a/lib/rspec/core/configuration.rb +++ b/lib/rspec/core/configuration.rb @@ -154,6 +154,10 @@ def deprecation_stream=(value) end end + # @macro add_setting + # Sets a file path to use for persisting example statuses. + add_setting :example_status_persistence_file_path + # @macro add_setting # Clean up and exit after the first failure (default: `false`). add_setting :fail_fast diff --git a/lib/rspec/core/runner.rb b/lib/rspec/core/runner.rb index af5612b92c..fc0075eeb6 100644 --- a/lib/rspec/core/runner.rb +++ b/lib/rspec/core/runner.rb @@ -83,7 +83,9 @@ def initialize(options, configuration=RSpec.configuration, world=RSpec.world) # @param out [IO] output stream def run(err, out) setup(err, out) - run_specs(@world.ordered_example_groups) + run_specs(@world.ordered_example_groups).tap do + persist_example_statuses + end end # Wires together the various configuration objects and state holders. @@ -112,6 +114,17 @@ def run_specs(example_groups) end end + private + + def persist_example_statuses + return unless @configuration.example_status_persistence_file_path + + ExampleStatusPersister.persist( + @world.all_examples, + @configuration.example_status_persistence_file_path + ) + end + # @private def self.disable_autorun! @autorun_disabled = true diff --git a/spec/rspec/core/runner_spec.rb b/spec/rspec/core/runner_spec.rb index 82c917c9a4..167d7c4511 100644 --- a/spec/rspec/core/runner_spec.rb +++ b/spec/rspec/core/runner_spec.rb @@ -236,6 +236,36 @@ def run_specs end end + describe "persistence of example statuses" do + let(:all_examples) { [double("example")] } + + def run + allow(world).to receive(:all_examples).and_return(all_examples) + allow(config).to receive(:load_spec_files) + + class_spy(ExampleStatusPersister).as_stubbed_const + + runner = build_runner + runner.run(err, out) + end + + context "when `example_status_persistence_file_path` is configured" do + it 'persists the status of all loaded examples' do + config.example_status_persistence_file_path = "examples.txt" + run + expect(ExampleStatusPersister).to have_received(:persist).with(all_examples, "examples.txt") + end + end + + context "when `example_status_persistence_file_path` is not configured" do + it 'persists the status of all loaded examples' do + config.example_status_persistence_file_path = nil + run + expect(ExampleStatusPersister).not_to have_received(:persist) + end + end + end + context "running files" do include_context "spec files" diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 485d1bac12..1ef99dde6c 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -56,6 +56,8 @@ def without_env_vars(*vars) end RSpec.configure do |c| + c.example_status_persistence_file_path = "./examples.txt" + # structural c.alias_it_behaves_like_to 'it_has_behavior' c.include(RSpecHelpers) From 6215a7ab9f9e9b745a7c0023f5988313e886fd0e Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Mon, 9 Mar 2015 21:52:48 -0700 Subject: [PATCH 075/258] Add `:last_run_status` metadata to examples. --- lib/rspec/core/example.rb | 7 ++++++- lib/rspec/core/metadata.rb | 1 + lib/rspec/core/world.rb | 13 +++++++++++++ spec/rspec/core/metadata_spec.rb | 16 ++++++++++++++++ spec/rspec/core/runner_spec.rb | 2 +- spec/rspec/core/world_spec.rb | 33 ++++++++++++++++++++++++++++++++ 6 files changed, 70 insertions(+), 2 deletions(-) diff --git a/lib/rspec/core/example.rb b/lib/rspec/core/example.rb index 062076b8ee..a93472942f 100644 --- a/lib/rspec/core/example.rb +++ b/lib/rspec/core/example.rb @@ -115,7 +115,7 @@ def rerun_argument # @return [String] the unique id of this example. Pass # this at the command line to re-run this exact example. def id - Metadata.id_from(metadata) + @id ||= Metadata.id_from(metadata) end # @attr_reader @@ -160,6 +160,11 @@ def initialize(example_group_class, description, user_metadata, example_block=ni description, example_block ) + # This should perhaps be done in `Metadata::ExampleHash.create`, + # but the logic there has no knowledge of `RSpec.world` and we + # want to keep it that way. It's easier to just assign it here. + @metadata[:last_run_status] = RSpec.world.last_run_statuses[id] + @example_group_instance = @exception = nil @clock = RSpec::Core::Time @reporter = RSpec::Core::NullReporter diff --git a/lib/rspec/core/metadata.rb b/lib/rspec/core/metadata.rb index d40c9747df..c7350cadcc 100644 --- a/lib/rspec/core/metadata.rb +++ b/lib/rspec/core/metadata.rb @@ -327,6 +327,7 @@ def full_description :example_group, :parent_example_group, :execution_result, + :last_run_status, :file_path, :absolute_file_path, :rerun_file_path, diff --git a/lib/rspec/core/world.rb b/lib/rspec/core/world.rb index 0e95b421ef..65a8333a1c 100644 --- a/lib/rspec/core/world.rb +++ b/lib/rspec/core/world.rb @@ -108,6 +108,19 @@ def reporter @configuration.reporter end + # @private + def last_run_statuses + @last_run_statuses ||= + if (path = @configuration.example_status_persistence_file_path) + ExampleStatusPersister.load_from(path).inject({}) do |hash, example| + hash[example.fetch(:example_id)] = example.fetch(:status) + hash + end + else + {} + end + end + # @api private # # Notify reporter of filters. diff --git a/spec/rspec/core/metadata_spec.rb b/spec/rspec/core/metadata_spec.rb index 606bd15575..8cd5fdb0eb 100644 --- a/spec/rspec/core/metadata_spec.rb +++ b/spec/rspec/core/metadata_spec.rb @@ -168,6 +168,22 @@ def metadata_for(*args) end end + describe ":last_run_status" do + it 'assigns it by looking up world.last_run_statuses[id]' do + looked_up_ids = [] + last_run_statuses = Hash.new do |hash, id| + looked_up_ids << id + "some_status" + end + + allow(RSpec.world).to receive(:last_run_statuses).and_return(last_run_statuses) + example = RSpec.describe.example + + expect(example.metadata[:last_run_status]).to eq("some_status") + expect(looked_up_ids).to eq [example.id] + end + end + describe ":id" do define :have_id_with do |scoped_id| expected_id = "#{Metadata.relative_path(__FILE__)}[#{scoped_id}]" diff --git a/spec/rspec/core/runner_spec.rb b/spec/rspec/core/runner_spec.rb index 167d7c4511..17f5114c60 100644 --- a/spec/rspec/core/runner_spec.rb +++ b/spec/rspec/core/runner_spec.rb @@ -243,7 +243,7 @@ def run allow(world).to receive(:all_examples).and_return(all_examples) allow(config).to receive(:load_spec_files) - class_spy(ExampleStatusPersister).as_stubbed_const + class_spy(ExampleStatusPersister, :load_from => []).as_stubbed_const runner = build_runner runner.run(err, out) diff --git a/spec/rspec/core/world_spec.rb b/spec/rspec/core/world_spec.rb index 318f579a6d..837ded67b0 100644 --- a/spec/rspec/core/world_spec.rb +++ b/spec/rspec/core/world_spec.rb @@ -23,6 +23,39 @@ module RSpec::Core end end + describe "#last_run_statuses" do + context "when `example_status_persistence_file_path` is configured" do + it 'gets the last run statuses from the ExampleStatusPersister' do + configuration.example_status_persistence_file_path = "examples.txt" + persister = class_double(ExampleStatusPersister).as_stubbed_const + + allow(persister).to receive(:load_from).with("examples.txt").and_return([ + { :example_id => "id_1", :status => "passed" }, + { :example_id => "id_2", :status => "failed" } + ]) + + expect(world.last_run_statuses).to eq( + 'id_1' => 'passed', 'id_2' => 'failed' + ) + end + end + + context "when `example_status_persistence_file_path` is not configured" do + it 'returns a memoized value' do + expect(world.last_run_statuses).to be(world.last_run_statuses) + end + + it 'returns a blank hash without attempting to load the persisted statuses' do + configuration.example_status_persistence_file_path = nil + + persister = class_double(ExampleStatusPersister).as_stubbed_const + expect(persister).not_to receive(:load_from) + + expect(world.last_run_statuses).to eq({}) + end + end + end + describe "#all_examples" do it "contains all examples from all levels of nesting" do RSpec.describe do From 951e0839bdba6ecd725adbbe32e1c62eb0689d25 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Tue, 10 Mar 2015 00:14:26 -0700 Subject: [PATCH 076/258] Add `--only-failures` and `--next-failure` options. --- features/command_line/only_failures.feature | 94 +++++++++++++++++++ .../step_definitions/additional_cli_steps.rb | 14 +++ lib/rspec/core/configuration.rb | 3 +- lib/rspec/core/option_parser.rb | 56 ++++++++--- spec/rspec/core/option_parser_spec.rb | 18 ++++ 5 files changed, 169 insertions(+), 16 deletions(-) create mode 100644 features/command_line/only_failures.feature diff --git a/features/command_line/only_failures.feature b/features/command_line/only_failures.feature new file mode 100644 index 0000000000..368a5cd4bd --- /dev/null +++ b/features/command_line/only_failures.feature @@ -0,0 +1,94 @@ +Feature: Only Failures + + The `--only-failures` option filters what examples are run so that only those that failed the last time they ran are executed. To use this option, you first have to configure `config.example_status_persistence_file_path`, which RSpec will use to store the status of each example the last time it ran. + + There's also a `--next-failure` option, which is shorthand for `--only-failures --fail-fast --order defined`. It allows you to repeatedly focus on just one of the currently failing examples, then move on to the next failure, etc. + + Either of these options can be combined with another a directory or file name; RSpec will run just the failures from the set of loaded examples. + + Background: + Given a file named "spec/spec_helper.rb" with: + """ruby + RSpec.configure do |c| + c.example_status_persistence_file_path = "examples.txt" + end + """ + And a file named ".rspec" with: + """ + --require spec_helper + --order random + --format documentation + """ + And a file named "spec/array_spec.rb" with: + """ruby + RSpec.describe 'Array' do + it "checks for inclusion of 1" do + expect([1, 2]).to include(1) + end + + it "checks for inclusion of 2" do + expect([1, 2]).to include(2) + end + + it "checks for inclusion of 3" do + expect([1, 2]).to include(3) # failure + end + end + """ + And a file named "spec/string_spec.rb" with: + """ruby + RSpec.describe 'String' do + it "checks for inclusion of 'foo'" do + expect("food").to include('foo') + end + + it "checks for inclusion of 'bar'" do + expect("food").to include('bar') # failure + end + + it "checks for inclusion of 'baz'" do + expect("bazzy").to include('baz') + end + + it "checks for inclusion of 'foobar'" do + expect("food").to include('foobar') # failure + end + end + """ + And I have run `rspec` once, resulting in "7 examples, 3 failures" + + Scenario: Use just `--only-failures` + When I run `rspec --only-failures` + Then the output should contain "3 examples, 3 failures" + + Scenario: Combine `--only-failures` with a file name + When I run `rspec spec/array_spec.rb --only-failures` + Then the output should contain "1 example, 1 failure" + When I run `rspec spec/string_spec.rb --only-failures` + Then the output should contain "2 examples, 2 failures" + + Scenario: Use `--next-failure` to repeatedly run a single failure + When I run `rspec --next-failure` + Then the output should contain "1 example, 1 failure" + And the output should contain "checks for inclusion of 3" + + When I fix "spec/array_spec.rb" by replacing "to include(3)" with "not_to include(3)" + And I run `rspec --next-failure` + Then the output should contain "2 examples, 1 failure" + And the output should contain "checks for inclusion of 3" + And the output should contain "checks for inclusion of 'bar'" + + When I fix "spec/string_spec.rb" by replacing "to include('bar')" with "not_to include('bar')" + And I run `rspec --next-failure` + Then the output should contain "2 examples, 1 failure" + And the output should contain "checks for inclusion of 'bar'" + And the output should contain "checks for inclusion of 'foobar'" + + When I fix "spec/string_spec.rb" by replacing "to include('foobar')" with "not_to include('foobar')" + And I run `rspec --next-failure` + Then the output should contain "1 example, 0 failures" + And the output should contain "checks for inclusion of 'foobar'" + + When I run `rspec --next-failure` + Then the output should contain "All examples were filtered out" + diff --git a/features/step_definitions/additional_cli_steps.rb b/features/step_definitions/additional_cli_steps.rb index 5ecfcadea7..8100949383 100644 --- a/features/step_definitions/additional_cli_steps.rb +++ b/features/step_definitions/additional_cli_steps.rb @@ -124,3 +124,17 @@ When /^I create "([^"]*)" with the following content:$/ do |file_name, content| write_file(file_name, content) end + +Given(/^I have run `([^`]*)` once, resulting in "([^"]*)"$/) do |command, output_snippet| + step %Q{I run `#{command}`} + step %Q{the output from "#{command}" should contain "#{output_snippet}"} +end + +When(/^I fix "(.*?)" by replacing "(.*?)" with "(.*?)"$/) do |file_name, original, replacement| + in_current_dir do + contents = File.read(file_name) + expect(contents).to include(original) + fixed = contents.sub(original, replacement) + File.open(file_name, "w") { |f| f.write(fixed) } + end +end diff --git a/lib/rspec/core/configuration.rb b/lib/rspec/core/configuration.rb index f256d1b008..86e762f42f 100644 --- a/lib/rspec/core/configuration.rb +++ b/lib/rspec/core/configuration.rb @@ -155,7 +155,8 @@ def deprecation_stream=(value) end # @macro add_setting - # Sets a file path to use for persisting example statuses. + # Sets a file path to use for persisting example statuses. Necessary for the + # `--only-failures` and `--next-failures` CLI options. add_setting :example_status_persistence_file_path # @macro add_setting diff --git a/lib/rspec/core/option_parser.rb b/lib/rspec/core/option_parser.rb index 00c12bd15a..12e817f1b8 100644 --- a/lib/rspec/core/option_parser.rb +++ b/lib/rspec/core/option_parser.rb @@ -21,6 +21,8 @@ def parse(args) options end + private + # rubocop:disable MethodLength def parser(options) OptionParser.new do |parser| @@ -45,7 +47,7 @@ def parser(options) ' [rand] randomize the order of groups and examples', ' [random] alias for rand', ' [random:SEED] e.g. --order random:123') do |o| - options[:order] = o + set_order(options, o) end parser.on('--seed SEED', Integer, 'Equivalent of --order rand:SEED.') do |seed| @@ -53,11 +55,11 @@ def parser(options) end parser.on('--fail-fast', 'Abort the run on first failure.') do |_o| - options[:fail_fast] = true + set_fail_fast(options) end parser.on('--no-fail-fast', 'Do not abort the run on first failure.') do |_o| - options[:fail_fast] = false + set_fail_fast(options, false) end parser.on('--failure-exit-code CODE', Integer, @@ -156,6 +158,17 @@ def parser(options) FILTERING + parser.on('--only-failures', "Filter to just the examples that failed the last time they ran.") do + add_tag_filter(options, :inclusion_filter, :last_run_status, 'failed') + end + + parser.on("--next-failure", "Apply `--only-failures` and abort after one failure.", + " (Equivalent to `--only-failures --fail-fast --order defined`)") do + add_tag_filter(options, :inclusion_filter, :last_run_status, 'failed') + set_fail_fast(options) + set_order(options, "defined") + end + parser.on('-P', '--pattern PATTERN', 'Load files matching pattern (default: "spec/**/*_spec.rb").') do |o| options[:pattern] = o end @@ -180,18 +193,19 @@ def parser(options) name, value = tag.gsub(/^(~@|~|@)/, '').split(':', 2) name = name.to_sym - options[filter_type] ||= {} - options[filter_type][name] = case value - when nil then true # The default value for tags is true - when 'true' then true - when 'false' then false - when 'nil' then nil - when /^:/ then value[1..-1].to_sym - when /^\d+$/ then Integer(value) - when /^\d+.\d+$/ then Float(value) - else - value - end + parsed_value = case value + when nil then true # The default value for tags is true + when 'true' then true + when 'false' then false + when 'nil' then nil + when /^:/ then value[1..-1].to_sym + when /^\d+$/ then Integer(value) + when /^\d+.\d+$/ then Float(value) + else + value + end + + add_tag_filter(options, filter_type, name, parsed_value) end parser.on('--default-path PATH', 'Set the default path where RSpec looks for examples (can', @@ -230,5 +244,17 @@ def parser(options) end end # rubocop:enable MethodLength + + def add_tag_filter(options, filter_type, tag_name, value=true) + (options[filter_type] ||= {})[tag_name] = value + end + + def set_fail_fast(options, value=true) + options[:fail_fast] = value + end + + def set_order(options, value) + options[:order] = value + end end end diff --git a/spec/rspec/core/option_parser_spec.rb b/spec/rspec/core/option_parser_spec.rb index f387dbb3f6..6d9904c3cc 100644 --- a/spec/rspec/core/option_parser_spec.rb +++ b/spec/rspec/core/option_parser_spec.rb @@ -123,6 +123,24 @@ def generate_help_text end end + describe "--only-failures" do + it 'is equivalent to `--tag last_run_status:failed`' do + tag = Parser.parse(%w[ --tag last_run_status:failed ]) + only_failures = Parser.parse(%w[ --only-failures ]) + + expect(only_failures).to eq(tag) + end + end + + describe "--next-failure" do + it 'is equivalent to `--tag last_run_status:failed --fail-fast --order defined`' do + long_form = Parser.parse(%w[ --tag last_run_status:failed --fail-fast --order defined ]) + next_failure = Parser.parse(%w[ --next-failure ]) + + expect(next_failure).to eq(long_form) + end + end + %w[--example -e].each do |option| describe option do it "escapes the arg" do From 67c48b3f4ab788b7b33f56a0efe0d9d09cb8863b Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Tue, 10 Mar 2015 09:12:40 -0700 Subject: [PATCH 077/258] Combine `--fail-fast` and `--no-fail-fast` in help output. --- lib/rspec/core/option_parser.rb | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/rspec/core/option_parser.rb b/lib/rspec/core/option_parser.rb index 12e817f1b8..7c274b848a 100644 --- a/lib/rspec/core/option_parser.rb +++ b/lib/rspec/core/option_parser.rb @@ -54,12 +54,8 @@ def parser(options) options[:order] = "rand:#{seed}" end - parser.on('--fail-fast', 'Abort the run on first failure.') do |_o| - set_fail_fast(options) - end - - parser.on('--no-fail-fast', 'Do not abort the run on first failure.') do |_o| - set_fail_fast(options, false) + parser.on('--[no-]fail-fast', 'Abort the run on first failure.') do |value| + set_fail_fast(options, value) end parser.on('--failure-exit-code CODE', Integer, @@ -165,7 +161,7 @@ def parser(options) parser.on("--next-failure", "Apply `--only-failures` and abort after one failure.", " (Equivalent to `--only-failures --fail-fast --order defined`)") do add_tag_filter(options, :inclusion_filter, :last_run_status, 'failed') - set_fail_fast(options) + set_fail_fast(options, true) set_order(options, "defined") end @@ -249,7 +245,7 @@ def add_tag_filter(options, filter_type, tag_name, value=true) (options[filter_type] ||= {})[tag_name] = value end - def set_fail_fast(options, value=true) + def set_fail_fast(options, value) options[:fail_fast] = value end From 52d4f7c674626a0f70e21f9bc5184ea4b80d6644 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Tue, 10 Mar 2015 10:46:00 -0700 Subject: [PATCH 078/258] Add changelog entries for new features from this PR. --- Changelog.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Changelog.md b/Changelog.md index bdcfccb14e..a0b9194428 100644 --- a/Changelog.md +++ b/Changelog.md @@ -14,6 +14,15 @@ Enhancements: where the location isn't unique. (Myron Marston, #1884) * Use the example id in the rerun command printed for failed examples when the location is not unique. (Myron Marston, #1884) +* Add `config.example_status_persistence_file_path` option, which is + used to persist the last run status of each example. (Myron Marston, #1888) +* Add `:last_run_status` metadata to each example, which indicates what + happened the last time an example ran. (Myron Marston, #1888) +* Add `--only-failures` CLI option which filters to only the examples + that failed the last time they ran. (Myron Marston, #1888) +* Add `--next-failure` CLI option which allows you to repeatedly focus + on just one of the currently failing examples, then move on to the + next failure, etc. (Myron Marston, #1888) Bug Fixes: From 1e483f6de33a56bb93d8846644cf73e42d307596 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Wed, 11 Mar 2015 21:40:01 -0700 Subject: [PATCH 079/258] Forward port 3.2.2 release notes. [ci skip] --- Changelog.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Changelog.md b/Changelog.md index bdcfccb14e..fcc72e6112 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,4 +1,5 @@ ### Development +[Full Changelog](http://github.com/rspec/rspec-core/compare/v3.2.2...master) Enhancements: @@ -21,6 +22,16 @@ Bug Fixes: * Fix Rake Task quoting of file names with quotes to work properly on Windows. (Myron Marston, #1887) +### 3.2.2 / 2015-03-11 +[Full Changelog](http://github.com/rspec/rspec-core/compare/v3.2.1...v3.2.2) + +Bug Fixes: + +* Fix regression in 3.2.0 that allowed tag-filtered examples to + run even if there was a location filter applied to the spec + file that was intended to limit the file to other examples. + (#1894, Myron Marston) + ### 3.2.1 / 2015-02-23 [Full Changelog](http://github.com/rspec/rspec-core/compare/v3.2.0...v3.2.1) From ab1e8955de953b1735a8772b1c14e96a79e0c955 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Tue, 10 Mar 2015 10:05:33 -0700 Subject: [PATCH 080/258] Load only files with failures when using `rspec --only-failures`. Loading ALL spec files when only a handful have failures is inefficient and could make the run take significantly longer. Note that we only do this if no file or directory arg is passed to `rspec`. If the user passes anything, we load what they tell us to load. --- features/command_line/only_failures.feature | 18 ++++++-- lib/rspec/core/configuration.rb | 24 +++++++++- lib/rspec/core/configuration_options.rb | 2 +- lib/rspec/core/option_parser.rb | 9 +++- lib/rspec/core/world.rb | 12 +++++ spec/rspec/core/configuration_options_spec.rb | 19 ++++++++ spec/rspec/core/configuration_spec.rb | 46 +++++++++++++++++++ spec/rspec/core/option_parser_spec.rb | 4 +- spec/rspec/core/world_spec.rb | 45 ++++++++++++++++-- 9 files changed, 163 insertions(+), 16 deletions(-) diff --git a/features/command_line/only_failures.feature b/features/command_line/only_failures.feature index 368a5cd4bd..9b13cb7e86 100644 --- a/features/command_line/only_failures.feature +++ b/features/command_line/only_failures.feature @@ -55,11 +55,22 @@ Feature: Only Failures end end """ - And I have run `rspec` once, resulting in "7 examples, 3 failures" + And a file named "spec/passing_spec.rb" with: + """ruby + puts "Loading passing_spec.rb" + + RSpec.describe "A passing spec" do + it "passes" do + expect(1).to eq(1) + end + end + """ + And I have run `rspec` once, resulting in "8 examples, 3 failures" - Scenario: Use just `--only-failures` + Scenario: Running `rspec --only-failures` loads only spec files with failures and runs only the failures When I run `rspec --only-failures` - Then the output should contain "3 examples, 3 failures" + Then the output from "rspec --only-failures" should contain "3 examples, 3 failures" + And the output from "rspec --only-failures" should not contain "Loading passing_spec.rb" Scenario: Combine `--only-failures` with a file name When I run `rspec spec/array_spec.rb --only-failures` @@ -91,4 +102,3 @@ Feature: Only Failures When I run `rspec --next-failure` Then the output should contain "All examples were filtered out" - diff --git a/lib/rspec/core/configuration.rb b/lib/rspec/core/configuration.rb index 86e762f42f..4c30f53f04 100644 --- a/lib/rspec/core/configuration.rb +++ b/lib/rspec/core/configuration.rb @@ -159,6 +159,11 @@ def deprecation_stream=(value) # `--only-failures` and `--next-failures` CLI options. add_setting :example_status_persistence_file_path + # @macro define_reader + # Indicates if the `--only-failures` (or `--next-failure`) flag is being used. + define_reader :only_failures + alias_method :only_failures?, :only_failures + # @macro add_setting # Clean up and exit after the first failure (default: `false`). add_setting :fail_fast @@ -310,6 +315,7 @@ def initialize @after_suite_hooks = [] @mock_framework = nil + @files_or_directories_to_run_defaulted = false @files_or_directories_to_run = [] @loaded_spec_files = Set.new @color = false @@ -797,7 +803,14 @@ def profile_examples # @private def files_or_directories_to_run=(*files) files = files.flatten - files << default_path if (command == 'rspec' || Runner.running_in_drb?) && default_path && files.empty? + + if (command == 'rspec' || Runner.running_in_drb?) && default_path && files.empty? + @files_or_directories_to_run_defaulted = true + files << default_path + else + @files_or_directories_to_run_defaulted = false + end + @files_or_directories_to_run = files @files_to_run = nil end @@ -805,7 +818,14 @@ def files_or_directories_to_run=(*files) # The spec files RSpec will run. # @return [Array] specified files about to run def files_to_run - @files_to_run ||= get_files_to_run(@files_or_directories_to_run) + @files_to_run ||= begin + if @files_or_directories_to_run_defaulted && only_failures? + files_with_failures = RSpec.world.spec_files_with_failures.to_a + @files_or_directories_to_run = files_with_failures if files_with_failures.any? + end + + get_files_to_run(@files_or_directories_to_run) + end end # Creates a method that delegates to `example` including the submitted diff --git a/lib/rspec/core/configuration_options.rb b/lib/rspec/core/configuration_options.rb index 4e8f219a2a..7f42180c07 100644 --- a/lib/rspec/core/configuration_options.rb +++ b/lib/rspec/core/configuration_options.rb @@ -81,7 +81,7 @@ def order(keys) # `files_or_directories_to_run` uses `default_path` so it must be # set before it. - :default_path, + :default_path, :only_failures, # These must be set before `requires` to support checking # `config.files_to_run` from within `spec_helper.rb` when a diff --git a/lib/rspec/core/option_parser.rb b/lib/rspec/core/option_parser.rb index 7c274b848a..f76a248ca5 100644 --- a/lib/rspec/core/option_parser.rb +++ b/lib/rspec/core/option_parser.rb @@ -155,12 +155,12 @@ def parser(options) FILTERING parser.on('--only-failures', "Filter to just the examples that failed the last time they ran.") do - add_tag_filter(options, :inclusion_filter, :last_run_status, 'failed') + configure_only_failures(options) end parser.on("--next-failure", "Apply `--only-failures` and abort after one failure.", " (Equivalent to `--only-failures --fail-fast --order defined`)") do - add_tag_filter(options, :inclusion_filter, :last_run_status, 'failed') + configure_only_failures(options) set_fail_fast(options, true) set_order(options, "defined") end @@ -252,5 +252,10 @@ def set_fail_fast(options, value) def set_order(options, value) options[:order] = value end + + def configure_only_failures(options) + options[:only_failures] = true + add_tag_filter(options, :inclusion_filter, :last_run_status, 'failed') + end end end diff --git a/lib/rspec/core/world.rb b/lib/rspec/core/world.rb index 65a8333a1c..8183c96efa 100644 --- a/lib/rspec/core/world.rb +++ b/lib/rspec/core/world.rb @@ -121,6 +121,18 @@ def last_run_statuses end end + # @private + def spec_files_with_failures + @spec_files_with_failures ||= + last_run_statuses.inject(Set.new) do |files, (id, status)| + files << id.split(Configuration::ON_SQUARE_BRACKETS).first if status == FAILED_STATUS + files + end.to_a + end + + # @private + FAILED_STATUS = "failed".freeze + # @api private # # Notify reporter of filters. diff --git a/spec/rspec/core/configuration_options_spec.rb b/spec/rspec/core/configuration_options_spec.rb index f0a3794f36..f4887ff23e 100644 --- a/spec/rspec/core/configuration_options_spec.rb +++ b/spec/rspec/core/configuration_options_spec.rb @@ -106,6 +106,13 @@ opts.configure(config) end + it 'configures `only_failures` before `files_or_directories_to_run` since it affects loaded files' do + opts = config_options_object(*%w[ --only-failures ]) + expect(config).to receive(:force).with(:only_failures => true).ordered + expect(config).to receive(:files_or_directories_to_run=).ordered + opts.configure(config) + end + { "pattern" => :pattern, "exclude-pattern" => :exclude_pattern }.each do |flag, attr| it "sets #{attr} before `requires` so users can check `files_to_run` in a `spec_helper` loaded by `--require`" do opts = config_options_object(*%W[--require spec_helpe --#{flag} **/*.spec]) @@ -168,6 +175,18 @@ opts.configure(config) end end + + %w[ --only-failures --next-failure ].each do |option| + describe option do + it "changes `config.only_failures?` to true" do + opts = config_options_object(option) + + expect { + opts.configure(config) + }.to change(config, :only_failures?).from(a_falsey_value).to(true) + end + end + end end describe "-c, --color, and --colour" do diff --git a/spec/rspec/core/configuration_spec.rb b/spec/rspec/core/configuration_spec.rb index a2e49d4f60..e53e78dba7 100644 --- a/spec/rspec/core/configuration_spec.rb +++ b/spec/rspec/core/configuration_spec.rb @@ -488,6 +488,52 @@ def stub_expectation_adapters end end + context "when only_failures is set" do + around { |ex| Dir.chdir("spec/rspec/core", &ex) } + let(:default_path) { "resources" } + let(:files_with_failures) { ["resources/a_spec.rb"] } + let(:files_loaded_via_default_path) do + config = Configuration.new + config.default_path = default_path + config.files_or_directories_to_run = [] + config.files_to_run + end + + before do + expect(files_loaded_via_default_path).not_to eq(files_with_failures) + config.default_path = default_path + allow(RSpec.world).to receive_messages(:spec_files_with_failures => files_with_failures) + config.force(:only_failures => true) + end + + context "and no explicit paths have been set" do + it 'loads only the files that have failures' do + assign_files_or_directories_to_run + expect(config.files_to_run).to eq(files_with_failures) + end + + it 'loads the default path if there are no files with failures' do + allow(RSpec.world).to receive_messages(:spec_files_with_failures => []) + assign_files_or_directories_to_run + expect(config.files_to_run).to eq(files_loaded_via_default_path) + end + end + + context "and a path has been set" do + it "ignores the list of files with failures, loading the configured path instead" do + assign_files_or_directories_to_run "resources/acceptance" + expect(config.files_to_run).to contain_files("resources/acceptance/foo_spec.rb") + end + end + + context "and the default path has been explicitly set" do + it "ignores the list of files with failures, loading the configured path instead" do + assign_files_or_directories_to_run default_path + expect(config.files_to_run).to eq(files_loaded_via_default_path) + end + end + end + context "with default pattern" do it "loads files named _spec.rb" do assign_files_or_directories_to_run "spec/rspec/core/resources" diff --git a/spec/rspec/core/option_parser_spec.rb b/spec/rspec/core/option_parser_spec.rb index 6d9904c3cc..e78b521162 100644 --- a/spec/rspec/core/option_parser_spec.rb +++ b/spec/rspec/core/option_parser_spec.rb @@ -128,7 +128,7 @@ def generate_help_text tag = Parser.parse(%w[ --tag last_run_status:failed ]) only_failures = Parser.parse(%w[ --only-failures ]) - expect(only_failures).to eq(tag) + expect(only_failures).to include(tag) end end @@ -137,7 +137,7 @@ def generate_help_text long_form = Parser.parse(%w[ --tag last_run_status:failed --fail-fast --order defined ]) next_failure = Parser.parse(%w[ --next-failure ]) - expect(next_failure).to eq(long_form) + expect(next_failure).to include(long_form) end end diff --git a/spec/rspec/core/world_spec.rb b/spec/rspec/core/world_spec.rb index 837ded67b0..3e0304d96f 100644 --- a/spec/rspec/core/world_spec.rb +++ b/spec/rspec/core/world_spec.rb @@ -23,16 +23,20 @@ module RSpec::Core end end + def simulate_persisted_examples(*examples) + configuration.example_status_persistence_file_path = "examples.txt" + persister = class_double(ExampleStatusPersister).as_stubbed_const + + allow(persister).to receive(:load_from).with("examples.txt").and_return(examples) + end + describe "#last_run_statuses" do context "when `example_status_persistence_file_path` is configured" do it 'gets the last run statuses from the ExampleStatusPersister' do - configuration.example_status_persistence_file_path = "examples.txt" - persister = class_double(ExampleStatusPersister).as_stubbed_const - - allow(persister).to receive(:load_from).with("examples.txt").and_return([ + simulate_persisted_examples( { :example_id => "id_1", :status => "passed" }, { :example_id => "id_2", :status => "failed" } - ]) + ) expect(world.last_run_statuses).to eq( 'id_1' => 'passed', 'id_2' => 'failed' @@ -56,6 +60,37 @@ module RSpec::Core end end + describe "#spec_files_with_failures" do + context "when `example_status_persistence_file_path` is configured" do + it 'returns a memoized array of unique spec files that contain failed exaples' do + simulate_persisted_examples( + { :example_id => "./spec_1.rb[1:1]", :status => "failed" }, + { :example_id => "./spec_1.rb[1:2]", :status => "failed" }, + { :example_id => "./spec_2.rb[1:2]", :status => "passed" }, + { :example_id => "./spec_3.rb[1:2]", :status => "pending" }, + { :example_id => "./spec_4.rb[1:2]", :status => "unknown" }, + { :example_id => "./spec_5.rb[1:2]", :status => "failed" } + ) + + expect(world.spec_files_with_failures).to( + be_an(Array) & + be(world.spec_files_with_failures) & + contain_exactly("./spec_1.rb", "./spec_5.rb") + ) + end + end + + context "when `example_status_persistence_file_path1` is not configured" do + it "returns a memoized blank array" do + configuration.example_status_persistence_file_path = nil + + expect(world.spec_files_with_failures).to( + eq([]) & be(world.spec_files_with_failures) + ) + end + end + end + describe "#all_examples" do it "contains all examples from all levels of nesting" do RSpec.describe do From f56aba58580e89120f4778efaa5c6e91cc43c738 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Wed, 11 Mar 2015 21:49:55 -0700 Subject: [PATCH 081/258] Move `only_failures` config specs into their own file. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `configuration_spec.rb` is too huge. The API needs to remain large to support the configurability of RSpec, but we don’t need to keep all the specs in one file. I want to start breaking off different chunks into their own files. This is a first step towards that. --- .../only_failures_support_spec.rb | 128 ++++++++++++++++++ spec/rspec/core/configuration_spec.rb | 46 ------- spec/rspec/core/world_spec.rb | 68 ---------- 3 files changed, 128 insertions(+), 114 deletions(-) create mode 100644 spec/rspec/core/configuration/only_failures_support_spec.rb diff --git a/spec/rspec/core/configuration/only_failures_support_spec.rb b/spec/rspec/core/configuration/only_failures_support_spec.rb new file mode 100644 index 0000000000..6efc607831 --- /dev/null +++ b/spec/rspec/core/configuration/only_failures_support_spec.rb @@ -0,0 +1,128 @@ +module RSpec::Core + RSpec.describe Configuration, "--only-failures support" do + let(:config) { Configuration.new } + let(:world) { World.new(config) } + + def simulate_persisted_examples(*examples) + config.example_status_persistence_file_path = "examples.txt" + persister = class_double(ExampleStatusPersister).as_stubbed_const + + allow(persister).to receive(:load_from).with("examples.txt").and_return(examples) + end + + describe "#last_run_statuses" do + def last_run_statuses + world.last_run_statuses + end + + context "when `example_status_persistence_file_path` is configured" do + it 'gets the last run statuses from the ExampleStatusPersister' do + simulate_persisted_examples( + { :example_id => "id_1", :status => "passed" }, + { :example_id => "id_2", :status => "failed" } + ) + + expect(last_run_statuses).to eq( + 'id_1' => 'passed', 'id_2' => 'failed' + ) + end + end + + context "when `example_status_persistence_file_path` is not configured" do + it 'returns a memoized value' do + expect(last_run_statuses).to be(world.last_run_statuses) + end + + it 'returns a blank hash without attempting to load the persisted statuses' do + config.example_status_persistence_file_path = nil + + persister = class_double(ExampleStatusPersister).as_stubbed_const + expect(persister).not_to receive(:load_from) + + expect(last_run_statuses).to eq({}) + end + end + end + + describe "#spec_files_with_failures" do + def spec_files_with_failures + world.spec_files_with_failures + end + + context "when `example_status_persistence_file_path` is configured" do + it 'returns a memoized array of unique spec files that contain failed exaples' do + simulate_persisted_examples( + { :example_id => "./spec_1.rb[1:1]", :status => "failed" }, + { :example_id => "./spec_1.rb[1:2]", :status => "failed" }, + { :example_id => "./spec_2.rb[1:2]", :status => "passed" }, + { :example_id => "./spec_3.rb[1:2]", :status => "pending" }, + { :example_id => "./spec_4.rb[1:2]", :status => "unknown" }, + { :example_id => "./spec_5.rb[1:2]", :status => "failed" } + ) + + expect(spec_files_with_failures).to( + be_an(Array) & + be(world.spec_files_with_failures) & + contain_exactly("./spec_1.rb", "./spec_5.rb") + ) + end + end + + context "when `example_status_persistence_file_path` is not configured" do + it "returns a memoized blank array" do + config.example_status_persistence_file_path = nil + + expect(spec_files_with_failures).to( + eq([]) & be(world.spec_files_with_failures) + ) + end + end + end + + describe "#files_to_run, when `only_failures` is set" do + around { |ex| Dir.chdir("spec/rspec/core", &ex) } + let(:default_path) { "resources" } + let(:files_with_failures) { ["resources/a_spec.rb"] } + let(:files_loaded_via_default_path) do + configuration = Configuration.new + configuration.default_path = default_path + configuration.files_or_directories_to_run = [] + configuration.files_to_run + end + + before do + expect(files_loaded_via_default_path).not_to eq(files_with_failures) + config.default_path = default_path + allow(RSpec.world).to receive_messages(:spec_files_with_failures => files_with_failures) + config.force(:only_failures => true) + end + + context "and no explicit paths have been set" do + it 'loads only the files that have failures' do + config.files_or_directories_to_run = [] + expect(config.files_to_run).to eq(files_with_failures) + end + + it 'loads the default path if there are no files with failures' do + allow(RSpec.world).to receive_messages(:spec_files_with_failures => []) + config.files_or_directories_to_run = [] + expect(config.files_to_run).to eq(files_loaded_via_default_path) + end + end + + context "and a path has been set" do + it "ignores the list of files with failures, loading the configured path instead" do + config.files_or_directories_to_run = ["resources/acceptance"] + expect(config.files_to_run).to contain_files("resources/acceptance/foo_spec.rb") + end + end + + context "and the default path has been explicitly set" do + it "ignores the list of files with failures, loading the configured path instead" do + config.files_or_directories_to_run = [default_path] + expect(config.files_to_run).to eq(files_loaded_via_default_path) + end + end + end + end +end diff --git a/spec/rspec/core/configuration_spec.rb b/spec/rspec/core/configuration_spec.rb index e53e78dba7..a2e49d4f60 100644 --- a/spec/rspec/core/configuration_spec.rb +++ b/spec/rspec/core/configuration_spec.rb @@ -488,52 +488,6 @@ def stub_expectation_adapters end end - context "when only_failures is set" do - around { |ex| Dir.chdir("spec/rspec/core", &ex) } - let(:default_path) { "resources" } - let(:files_with_failures) { ["resources/a_spec.rb"] } - let(:files_loaded_via_default_path) do - config = Configuration.new - config.default_path = default_path - config.files_or_directories_to_run = [] - config.files_to_run - end - - before do - expect(files_loaded_via_default_path).not_to eq(files_with_failures) - config.default_path = default_path - allow(RSpec.world).to receive_messages(:spec_files_with_failures => files_with_failures) - config.force(:only_failures => true) - end - - context "and no explicit paths have been set" do - it 'loads only the files that have failures' do - assign_files_or_directories_to_run - expect(config.files_to_run).to eq(files_with_failures) - end - - it 'loads the default path if there are no files with failures' do - allow(RSpec.world).to receive_messages(:spec_files_with_failures => []) - assign_files_or_directories_to_run - expect(config.files_to_run).to eq(files_loaded_via_default_path) - end - end - - context "and a path has been set" do - it "ignores the list of files with failures, loading the configured path instead" do - assign_files_or_directories_to_run "resources/acceptance" - expect(config.files_to_run).to contain_files("resources/acceptance/foo_spec.rb") - end - end - - context "and the default path has been explicitly set" do - it "ignores the list of files with failures, loading the configured path instead" do - assign_files_or_directories_to_run default_path - expect(config.files_to_run).to eq(files_loaded_via_default_path) - end - end - end - context "with default pattern" do it "loads files named _spec.rb" do assign_files_or_directories_to_run "spec/rspec/core/resources" diff --git a/spec/rspec/core/world_spec.rb b/spec/rspec/core/world_spec.rb index 3e0304d96f..318f579a6d 100644 --- a/spec/rspec/core/world_spec.rb +++ b/spec/rspec/core/world_spec.rb @@ -23,74 +23,6 @@ module RSpec::Core end end - def simulate_persisted_examples(*examples) - configuration.example_status_persistence_file_path = "examples.txt" - persister = class_double(ExampleStatusPersister).as_stubbed_const - - allow(persister).to receive(:load_from).with("examples.txt").and_return(examples) - end - - describe "#last_run_statuses" do - context "when `example_status_persistence_file_path` is configured" do - it 'gets the last run statuses from the ExampleStatusPersister' do - simulate_persisted_examples( - { :example_id => "id_1", :status => "passed" }, - { :example_id => "id_2", :status => "failed" } - ) - - expect(world.last_run_statuses).to eq( - 'id_1' => 'passed', 'id_2' => 'failed' - ) - end - end - - context "when `example_status_persistence_file_path` is not configured" do - it 'returns a memoized value' do - expect(world.last_run_statuses).to be(world.last_run_statuses) - end - - it 'returns a blank hash without attempting to load the persisted statuses' do - configuration.example_status_persistence_file_path = nil - - persister = class_double(ExampleStatusPersister).as_stubbed_const - expect(persister).not_to receive(:load_from) - - expect(world.last_run_statuses).to eq({}) - end - end - end - - describe "#spec_files_with_failures" do - context "when `example_status_persistence_file_path` is configured" do - it 'returns a memoized array of unique spec files that contain failed exaples' do - simulate_persisted_examples( - { :example_id => "./spec_1.rb[1:1]", :status => "failed" }, - { :example_id => "./spec_1.rb[1:2]", :status => "failed" }, - { :example_id => "./spec_2.rb[1:2]", :status => "passed" }, - { :example_id => "./spec_3.rb[1:2]", :status => "pending" }, - { :example_id => "./spec_4.rb[1:2]", :status => "unknown" }, - { :example_id => "./spec_5.rb[1:2]", :status => "failed" } - ) - - expect(world.spec_files_with_failures).to( - be_an(Array) & - be(world.spec_files_with_failures) & - contain_exactly("./spec_1.rb", "./spec_5.rb") - ) - end - end - - context "when `example_status_persistence_file_path1` is not configured" do - it "returns a memoized blank array" do - configuration.example_status_persistence_file_path = nil - - expect(world.spec_files_with_failures).to( - eq([]) & be(world.spec_files_with_failures) - ) - end - end - end - describe "#all_examples" do it "contains all examples from all levels of nesting" do RSpec.describe do From cc34181ea16befb2e1b46026afed7553c07ebf72 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Wed, 11 Mar 2015 21:55:11 -0700 Subject: [PATCH 082/258] Ensure derived values are updated when the config setting updates. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It’s easier to make this work by moving the `spec_files_with_failures` and the `last_run_statuses` methods off of `world` and over to `configuration. --- lib/rspec/core/configuration.rb | 52 ++++++++++++-- lib/rspec/core/example.rb | 2 +- lib/rspec/core/world.rb | 25 ------- .../only_failures_support_spec.rb | 70 ++++++++++++++++--- spec/rspec/core/metadata_spec.rb | 4 +- 5 files changed, 112 insertions(+), 41 deletions(-) diff --git a/lib/rspec/core/configuration.rb b/lib/rspec/core/configuration.rb index 4c30f53f04..15e9f874bc 100644 --- a/lib/rspec/core/configuration.rb +++ b/lib/rspec/core/configuration.rb @@ -154,10 +154,22 @@ def deprecation_stream=(value) end end - # @macro add_setting - # Sets a file path to use for persisting example statuses. Necessary for the + # @macro define_reader + # The file path to use for persisting example statuses. Necessary for the + # `--only-failures` and `--next-failures` CLI options. + # + # @overload example_status_persistence_file_path + # @return [String] the file path + # @overload example_status_persistence_file_path=(value) + # @param value [String] the file path + define_reader :example_status_persistence_file_path + + # Sets the file path to use for persisting example statuses. Necessary for the # `--only-failures` and `--next-failures` CLI options. - add_setting :example_status_persistence_file_path + def example_status_persistence_file_path=(value) + @example_status_persistence_file_path = value + clear_values_derived_from_example_status_persistence_file_path + end # @macro define_reader # Indicates if the `--only-failures` (or `--next-failure`) flag is being used. @@ -353,6 +365,9 @@ def initialize def force(hash) ordering_manager.force(hash) @preferred_options.merge!(hash) + + return unless hash.key?(:example_status_persistence_file_path) + clear_values_derived_from_example_status_persistence_file_path end # @private @@ -820,7 +835,7 @@ def files_or_directories_to_run=(*files) def files_to_run @files_to_run ||= begin if @files_or_directories_to_run_defaulted && only_failures? - files_with_failures = RSpec.world.spec_files_with_failures.to_a + files_with_failures = spec_files_with_failures.to_a @files_or_directories_to_run = files_with_failures if files_with_failures.any? end @@ -828,6 +843,30 @@ def files_to_run end end + # @private + def last_run_statuses + @last_run_statuses ||= + if (path = example_status_persistence_file_path) + ExampleStatusPersister.load_from(path).inject({}) do |hash, example| + hash[example.fetch(:example_id)] = example.fetch(:status) + hash + end + else + {} + end + end + + # @private + FAILED_STATUS = "failed".freeze + + # @private + def spec_files_with_failures + @spec_files_with_failures ||= last_run_statuses.inject(Set.new) do |files, (id, status)| + files << id.split(ON_SQUARE_BRACKETS).first if status == FAILED_STATUS + files + end.to_a + end + # Creates a method that delegates to `example` including the submitted # `args`. Used internally to add variants of `example` like `pending`: # @param name [String] example name alias @@ -1703,6 +1742,11 @@ def update_pattern_attr(name, value) instance_variable_set(:"@#{name}", value) @files_to_run = nil end + + def clear_values_derived_from_example_status_persistence_file_path + @last_run_statuses = nil + @spec_files_with_failures = nil + end end # rubocop:enable Style/ClassLength end diff --git a/lib/rspec/core/example.rb b/lib/rspec/core/example.rb index a93472942f..6141f72e60 100644 --- a/lib/rspec/core/example.rb +++ b/lib/rspec/core/example.rb @@ -163,7 +163,7 @@ def initialize(example_group_class, description, user_metadata, example_block=ni # This should perhaps be done in `Metadata::ExampleHash.create`, # but the logic there has no knowledge of `RSpec.world` and we # want to keep it that way. It's easier to just assign it here. - @metadata[:last_run_status] = RSpec.world.last_run_statuses[id] + @metadata[:last_run_status] = RSpec.configuration.last_run_statuses[id] @example_group_instance = @exception = nil @clock = RSpec::Core::Time diff --git a/lib/rspec/core/world.rb b/lib/rspec/core/world.rb index 8183c96efa..0e95b421ef 100644 --- a/lib/rspec/core/world.rb +++ b/lib/rspec/core/world.rb @@ -108,31 +108,6 @@ def reporter @configuration.reporter end - # @private - def last_run_statuses - @last_run_statuses ||= - if (path = @configuration.example_status_persistence_file_path) - ExampleStatusPersister.load_from(path).inject({}) do |hash, example| - hash[example.fetch(:example_id)] = example.fetch(:status) - hash - end - else - {} - end - end - - # @private - def spec_files_with_failures - @spec_files_with_failures ||= - last_run_statuses.inject(Set.new) do |files, (id, status)| - files << id.split(Configuration::ON_SQUARE_BRACKETS).first if status == FAILED_STATUS - files - end.to_a - end - - # @private - FAILED_STATUS = "failed".freeze - # @api private # # Notify reporter of filters. diff --git a/spec/rspec/core/configuration/only_failures_support_spec.rb b/spec/rspec/core/configuration/only_failures_support_spec.rb index 6efc607831..52b3647ff5 100644 --- a/spec/rspec/core/configuration/only_failures_support_spec.rb +++ b/spec/rspec/core/configuration/only_failures_support_spec.rb @@ -1,7 +1,6 @@ module RSpec::Core RSpec.describe Configuration, "--only-failures support" do let(:config) { Configuration.new } - let(:world) { World.new(config) } def simulate_persisted_examples(*examples) config.example_status_persistence_file_path = "examples.txt" @@ -12,25 +11,31 @@ def simulate_persisted_examples(*examples) describe "#last_run_statuses" do def last_run_statuses - world.last_run_statuses + config.last_run_statuses end context "when `example_status_persistence_file_path` is configured" do - it 'gets the last run statuses from the ExampleStatusPersister' do + before do simulate_persisted_examples( { :example_id => "id_1", :status => "passed" }, { :example_id => "id_2", :status => "failed" } ) + end + it 'gets the last run statuses from the ExampleStatusPersister' do expect(last_run_statuses).to eq( 'id_1' => 'passed', 'id_2' => 'failed' ) end + + it 'returns a memoized value' do + expect(last_run_statuses).to be(last_run_statuses) + end end context "when `example_status_persistence_file_path` is not configured" do it 'returns a memoized value' do - expect(last_run_statuses).to be(world.last_run_statuses) + expect(last_run_statuses).to be(last_run_statuses) end it 'returns a blank hash without attempting to load the persisted statuses' do @@ -42,11 +47,36 @@ def last_run_statuses expect(last_run_statuses).to eq({}) end end + + def allows_value_to_change_when_updated + simulate_persisted_examples( + { :example_id => "id_1", :status => "passed" }, + { :example_id => "id_2", :status => "failed" } + ) + + config.example_status_persistence_file_path = nil + + expect { + yield + }.to change { last_run_statuses }.to('id_1' => 'passed', 'id_2' => 'failed') + end + + it 'allows the value to be updated when `example_status_persistence_file_path` is set after first access' do + allows_value_to_change_when_updated do + config.example_status_persistence_file_path = "examples.txt" + end + end + + it 'allows the value to be updated when `example_status_persistence_file_path` is forced after first access' do + allows_value_to_change_when_updated do + config.force(:example_status_persistence_file_path => "examples.txt") + end + end end describe "#spec_files_with_failures" do def spec_files_with_failures - world.spec_files_with_failures + config.spec_files_with_failures end context "when `example_status_persistence_file_path` is configured" do @@ -62,7 +92,7 @@ def spec_files_with_failures expect(spec_files_with_failures).to( be_an(Array) & - be(world.spec_files_with_failures) & + be(spec_files_with_failures) & contain_exactly("./spec_1.rb", "./spec_5.rb") ) end @@ -73,10 +103,32 @@ def spec_files_with_failures config.example_status_persistence_file_path = nil expect(spec_files_with_failures).to( - eq([]) & be(world.spec_files_with_failures) + eq([]) & be(spec_files_with_failures) ) end end + + def allows_value_to_change_when_updated + simulate_persisted_examples({ :example_id => "./spec_1.rb[1:1]", :status => "failed" }) + + config.example_status_persistence_file_path = nil + + expect { + yield + }.to change { spec_files_with_failures }.to(["./spec_1.rb"]) + end + + it 'allows the value to be updated when `example_status_persistence_file_path` is set after first access' do + allows_value_to_change_when_updated do + config.example_status_persistence_file_path = "examples.txt" + end + end + + it 'allows the value to be updated when `example_status_persistence_file_path` is forced after first access' do + allows_value_to_change_when_updated do + config.force(:example_status_persistence_file_path => "examples.txt") + end + end end describe "#files_to_run, when `only_failures` is set" do @@ -93,7 +145,7 @@ def spec_files_with_failures before do expect(files_loaded_via_default_path).not_to eq(files_with_failures) config.default_path = default_path - allow(RSpec.world).to receive_messages(:spec_files_with_failures => files_with_failures) + allow(config).to receive_messages(:spec_files_with_failures => files_with_failures) config.force(:only_failures => true) end @@ -104,7 +156,7 @@ def spec_files_with_failures end it 'loads the default path if there are no files with failures' do - allow(RSpec.world).to receive_messages(:spec_files_with_failures => []) + allow(config).to receive_messages(:spec_files_with_failures => []) config.files_or_directories_to_run = [] expect(config.files_to_run).to eq(files_loaded_via_default_path) end diff --git a/spec/rspec/core/metadata_spec.rb b/spec/rspec/core/metadata_spec.rb index 8cd5fdb0eb..7a6b2da1aa 100644 --- a/spec/rspec/core/metadata_spec.rb +++ b/spec/rspec/core/metadata_spec.rb @@ -169,14 +169,14 @@ def metadata_for(*args) end describe ":last_run_status" do - it 'assigns it by looking up world.last_run_statuses[id]' do + it 'assigns it by looking up configuration.last_run_statuses[id]' do looked_up_ids = [] last_run_statuses = Hash.new do |hash, id| looked_up_ids << id "some_status" end - allow(RSpec.world).to receive(:last_run_statuses).and_return(last_run_statuses) + allow(RSpec.configuration).to receive(:last_run_statuses).and_return(last_run_statuses) example = RSpec.describe.example expect(example.metadata[:last_run_status]).to eq("some_status") From 3ad1aeb46c4d61012596d0b0940c39ac8cf558d5 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Wed, 11 Mar 2015 22:42:23 -0700 Subject: [PATCH 083/258] Stop stubbing the object under test. It made more sense when these methods were on `world` as they were before but now they are on config so we want to avoid that. --- .../core/configuration/only_failures_support_spec.rb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/spec/rspec/core/configuration/only_failures_support_spec.rb b/spec/rspec/core/configuration/only_failures_support_spec.rb index 52b3647ff5..8e180e83e0 100644 --- a/spec/rspec/core/configuration/only_failures_support_spec.rb +++ b/spec/rspec/core/configuration/only_failures_support_spec.rb @@ -6,7 +6,7 @@ def simulate_persisted_examples(*examples) config.example_status_persistence_file_path = "examples.txt" persister = class_double(ExampleStatusPersister).as_stubbed_const - allow(persister).to receive(:load_from).with("examples.txt").and_return(examples) + allow(persister).to receive(:load_from).with("examples.txt").and_return(examples.flatten) end describe "#last_run_statuses" do @@ -145,7 +145,11 @@ def allows_value_to_change_when_updated before do expect(files_loaded_via_default_path).not_to eq(files_with_failures) config.default_path = default_path - allow(config).to receive_messages(:spec_files_with_failures => files_with_failures) + + simulate_persisted_examples(files_with_failures.map do |file| + { :example_id => "#{file}[1:1]", :status => "failed" } + end) + config.force(:only_failures => true) end @@ -156,7 +160,7 @@ def allows_value_to_change_when_updated end it 'loads the default path if there are no files with failures' do - allow(config).to receive_messages(:spec_files_with_failures => []) + simulate_persisted_examples([]) config.files_or_directories_to_run = [] expect(config.files_to_run).to eq(files_loaded_via_default_path) end From f8a5cd66840c6348115f8473b4e7619c63eee74a Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Wed, 11 Mar 2015 22:45:00 -0700 Subject: [PATCH 084/258] Use spec/examples.txt instead of examples.txt. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It’s more appropriate there. --- .gitignore | 2 +- spec/spec_helper.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 540b64d91d..d13d81b13b 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,4 @@ Gemfile-custom .idea bundle .rspec-local -examples.txt +spec/examples.txt diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 1ef99dde6c..b924d13535 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -56,7 +56,7 @@ def without_env_vars(*vars) end RSpec.configure do |c| - c.example_status_persistence_file_path = "./examples.txt" + c.example_status_persistence_file_path = "./spec/examples.txt" # structural c.alias_it_behaves_like_to 'it_has_behavior' From 500ff379f6b4dd7140da808d3a3e652f0ce7dd96 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Wed, 11 Mar 2015 23:01:42 -0700 Subject: [PATCH 085/258] Always limit loaded files when `--only-failures` is used. Before, we only did so when no file or directory arg was passed to `rspec`, but it's better to take the intersection of files matching the provided args and files with failures. --- lib/rspec/core/configuration.rb | 20 ++++++---------- .../only_failures_support_spec.rb | 23 +++++++++++-------- spec/spec_helper.rb | 10 ++++++++ spec/support/aruba_support.rb | 8 +++---- 4 files changed, 34 insertions(+), 27 deletions(-) diff --git a/lib/rspec/core/configuration.rb b/lib/rspec/core/configuration.rb index 15e9f874bc..5f78db3284 100644 --- a/lib/rspec/core/configuration.rb +++ b/lib/rspec/core/configuration.rb @@ -327,7 +327,6 @@ def initialize @after_suite_hooks = [] @mock_framework = nil - @files_or_directories_to_run_defaulted = false @files_or_directories_to_run = [] @loaded_spec_files = Set.new @color = false @@ -820,10 +819,7 @@ def files_or_directories_to_run=(*files) files = files.flatten if (command == 'rspec' || Runner.running_in_drb?) && default_path && files.empty? - @files_or_directories_to_run_defaulted = true files << default_path - else - @files_or_directories_to_run_defaulted = false end @files_or_directories_to_run = files @@ -833,14 +829,7 @@ def files_or_directories_to_run=(*files) # The spec files RSpec will run. # @return [Array] specified files about to run def files_to_run - @files_to_run ||= begin - if @files_or_directories_to_run_defaulted && only_failures? - files_with_failures = spec_files_with_failures.to_a - @files_or_directories_to_run = files_with_failures if files_with_failures.any? - end - - get_files_to_run(@files_or_directories_to_run) - end + @files_to_run ||= get_files_to_run(@files_or_directories_to_run) end # @private @@ -1624,10 +1613,15 @@ def run_hooks_with(hooks, hook_context) end def get_files_to_run(paths) - FlatMap.flat_map(paths_to_check(paths)) do |path| + files = FlatMap.flat_map(paths_to_check(paths)) do |path| path = path.gsub(File::ALT_SEPARATOR, File::SEPARATOR) if File::ALT_SEPARATOR File.directory?(path) ? gather_directories(path) : extract_location(path) end.sort.uniq + + return files unless only_failures? + relative_files = files.map { |f| Metadata.relative_path(File.expand_path f) } + intersection = (relative_files & spec_files_with_failures.to_a) + intersection.empty? ? files : intersection end def paths_to_check(paths) diff --git a/spec/rspec/core/configuration/only_failures_support_spec.rb b/spec/rspec/core/configuration/only_failures_support_spec.rb index 8e180e83e0..5a9da66418 100644 --- a/spec/rspec/core/configuration/only_failures_support_spec.rb +++ b/spec/rspec/core/configuration/only_failures_support_spec.rb @@ -132,9 +132,14 @@ def allows_value_to_change_when_updated end describe "#files_to_run, when `only_failures` is set" do - around { |ex| Dir.chdir("spec/rspec/core", &ex) } + around do |ex| + handle_current_dir_change do + Dir.chdir("spec/rspec/core", &ex) + end + end + let(:default_path) { "resources" } - let(:files_with_failures) { ["resources/a_spec.rb"] } + let(:files_with_failures) { ["./resources/a_spec.rb"] } let(:files_loaded_via_default_path) do configuration = Configuration.new configuration.default_path = default_path @@ -167,16 +172,14 @@ def allows_value_to_change_when_updated end context "and a path has been set" do - it "ignores the list of files with failures, loading the configured path instead" do - config.files_or_directories_to_run = ["resources/acceptance"] - expect(config.files_to_run).to contain_files("resources/acceptance/foo_spec.rb") + it "loads the intersection of files matching the path and files with failures" do + config.files_or_directories_to_run = ["resources"] + expect(config.files_to_run).to eq(files_with_failures) end - end - context "and the default path has been explicitly set" do - it "ignores the list of files with failures, loading the configured path instead" do - config.files_or_directories_to_run = [default_path] - expect(config.files_to_run).to eq(files_loaded_via_default_path) + it "loads all files matching the path when there are no intersecting files" do + config.files_or_directories_to_run = ["resources/acceptance"] + expect(config.files_to_run).to contain_files("resources/acceptance/foo_spec.rb") end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index b924d13535..6a314d2b5d 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -53,10 +53,20 @@ def without_env_vars(*vars) ENV.replace(original) end end + + def handle_current_dir_change + RSpec::Core::Metadata.instance_variable_set(:@relative_path_regex, nil) + yield + ensure + RSpec::Core::Metadata.instance_variable_set(:@relative_path_regex, nil) + end end RSpec.configure do |c| c.example_status_persistence_file_path = "./spec/examples.txt" + c.around(:example, :isolated_directory) do |ex| + handle_current_dir_change(&ex) + end # structural c.alias_it_behaves_like_to 'it_has_behavior' diff --git a/spec/support/aruba_support.rb b/spec/support/aruba_support.rb index c3368db940..28929775da 100644 --- a/spec/support/aruba_support.rb +++ b/spec/support/aruba_support.rb @@ -17,16 +17,16 @@ def run_command(cmd) temp_stdout = StringIO.new temp_stderr = StringIO.new - RSpec::Core::Metadata.instance_variable_set(:@relative_path_regex, nil) cmd_parts = Shellwords.split(cmd) - in_current_dir do - RSpec::Core::Runner.run(cmd_parts, temp_stderr, temp_stdout) + handle_current_dir_change do + in_current_dir do + RSpec::Core::Runner.run(cmd_parts, temp_stderr, temp_stdout) + end end ensure RSpec.reset RSpec.configuration.color = true - RSpec::Core::Metadata.instance_variable_set(:@relative_path_regex, nil) # Ensure it gets cached with a proper value -- if we leave it set to nil, # and the next spec operates in a different dir, it could get set to an From 94b4099bf20b5cc166f3272d03af96d41671cf17 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Thu, 12 Mar 2015 21:06:28 -0700 Subject: [PATCH 086/258] Use "unknown" for :last_run_status when we don't have a value. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This ensures that the value is consistent — before for an unknown case it could either be `nil` or `"unknown"`. We've also moved the constant into configuration, so that we can avoid the cost of loading `example_status_persister` (via constant access since it is setup to be autoloaded) if the user hasn't configured the feature. --- lib/rspec/core/configuration.rb | 10 ++++++---- lib/rspec/core/example_status_persister.rb | 6 ++---- .../configuration/only_failures_support_spec.rb | 16 ++++++++++++++-- spec/rspec/core/example_status_persister_spec.rb | 8 ++++---- 4 files changed, 26 insertions(+), 14 deletions(-) diff --git a/lib/rspec/core/configuration.rb b/lib/rspec/core/configuration.rb index 5f78db3284..95da2f5e37 100644 --- a/lib/rspec/core/configuration.rb +++ b/lib/rspec/core/configuration.rb @@ -834,17 +834,19 @@ def files_to_run # @private def last_run_statuses - @last_run_statuses ||= + @last_run_statuses ||= Hash.new(UNKNOWN_STATUS).tap do |statuses| if (path = example_status_persistence_file_path) - ExampleStatusPersister.load_from(path).inject({}) do |hash, example| + ExampleStatusPersister.load_from(path).inject(statuses) do |hash, example| hash[example.fetch(:example_id)] = example.fetch(:status) hash end - else - {} end + end end + # @private + UNKNOWN_STATUS = "unknown".freeze + # @private FAILED_STATUS = "failed".freeze diff --git a/lib/rspec/core/example_status_persister.rb b/lib/rspec/core/example_status_persister.rb index 5e9a110a65..0eb8a0ab52 100644 --- a/lib/rspec/core/example_status_persister.rb +++ b/lib/rspec/core/example_status_persister.rb @@ -45,7 +45,7 @@ def statuses_from_this_run { :example_id => ex.id, - :status => result.status ? result.status.to_s : ExampleStatusMerger::UNKNOWN_STATUS, + :status => result.status ? result.status.to_s : Configuration::UNKNOWN_STATUS, :run_time => result.run_time ? Formatters::Helpers.format_duration(result.run_time) : "" } end @@ -86,12 +86,10 @@ def merge delete_previous_examples_that_no_longer_exist @this_run.merge(@from_previous_runs) do |_ex_id, new, old| - new.fetch(:status) == UNKNOWN_STATUS ? old : new + new.fetch(:status) == Configuration::UNKNOWN_STATUS ? old : new end.values.sort_by(&method(:sort_value_from)) end - UNKNOWN_STATUS = "unknown".freeze - private def hash_from(example_list) diff --git a/spec/rspec/core/configuration/only_failures_support_spec.rb b/spec/rspec/core/configuration/only_failures_support_spec.rb index 5a9da66418..095fdb318e 100644 --- a/spec/rspec/core/configuration/only_failures_support_spec.rb +++ b/spec/rspec/core/configuration/only_failures_support_spec.rb @@ -31,21 +31,33 @@ def last_run_statuses it 'returns a memoized value' do expect(last_run_statuses).to be(last_run_statuses) end + + specify 'the hash returns `unknown` for unknown example ids for consistency' do + expect(last_run_statuses["foo"]).to eq(Configuration::UNKNOWN_STATUS) + expect(last_run_statuses["bar"]).to eq(Configuration::UNKNOWN_STATUS) + end end context "when `example_status_persistence_file_path` is not configured" do + before do + config.example_status_persistence_file_path = nil + end + it 'returns a memoized value' do expect(last_run_statuses).to be(last_run_statuses) end it 'returns a blank hash without attempting to load the persisted statuses' do - config.example_status_persistence_file_path = nil - persister = class_double(ExampleStatusPersister).as_stubbed_const expect(persister).not_to receive(:load_from) expect(last_run_statuses).to eq({}) end + + specify 'the hash returns `unknown` for all ids for consistency' do + expect(last_run_statuses["foo"]).to eq(Configuration::UNKNOWN_STATUS) + expect(last_run_statuses["bar"]).to eq(Configuration::UNKNOWN_STATUS) + end end def allows_value_to_change_when_updated diff --git a/spec/rspec/core/example_status_persister_spec.rb b/spec/rspec/core/example_status_persister_spec.rb index 69655e8d1d..9320780d08 100644 --- a/spec/rspec/core/example_status_persister_spec.rb +++ b/spec/rspec/core/example_status_persister_spec.rb @@ -82,14 +82,14 @@ def new_example(id, metadata = {}) expect(loaded).to match [ a_hash_including(:run_time => "3 seconds") ] end - it "persists a loaded but unexecuted example with an #{ExampleStatusMerger::UNKNOWN_STATUS} status" do + it "persists a loaded but unexecuted example with an #{Configuration::UNKNOWN_STATUS} status" do ex_1 = RSpec.describe.example ExampleStatusPersister.persist([ex_1], file.path) loaded = ExampleStatusPersister.load_from(file.path) expect(loaded).to match [ a_hash_including( - :example_id => ex_1.id, :status => ExampleStatusMerger::UNKNOWN_STATUS + :example_id => ex_1.id, :status => Configuration::UNKNOWN_STATUS ) ] end @@ -142,7 +142,7 @@ def new_example(id, metadata = {}) context "for examples that are only in the set for this run" do it "takes them indiscriminately, even if they did not execute" do - this_run = [ example(existing_spec_file, "1:1", ExampleStatusMerger::UNKNOWN_STATUS) ] + this_run = [ example(existing_spec_file, "1:1", Configuration::UNKNOWN_STATUS) ] merged = merge(:this_run => this_run, :from_previous_runs => []) expect(merged).to match_array(this_run) @@ -187,7 +187,7 @@ def new_example(id, metadata = {}) end it "takes the status from previous runs if the example was loaded but did not execute" do - this_run = [ example("foo_spec.rb", "1:1", ExampleStatusMerger::UNKNOWN_STATUS) ] + this_run = [ example("foo_spec.rb", "1:1", Configuration::UNKNOWN_STATUS) ] from_previous_runs = [ example("foo_spec.rb", "1:1", "failed") ] merged = merge(:this_run => this_run, :from_previous_runs => from_previous_runs) From 43da674a13d18d4db5ef68fcc805a5b6fea8d8f0 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 13 Mar 2015 23:30:52 -0700 Subject: [PATCH 087/258] Fail fast when we can't support `--only-failures` due to lack of config. --- features/command_line/only_failures.feature | 5 +++ .../step_definitions/additional_cli_steps.rb | 12 ++++++ lib/rspec/core/configuration.rb | 5 +++ .../core/formatters/base_text_formatter.rb | 1 + lib/rspec/core/reporter.rb | 13 +++++- lib/rspec/core/world.rb | 11 +++++ .../formatters/base_text_formatter_spec.rb | 14 ++++++- spec/rspec/core/reporter_spec.rb | 23 ++++++++++ spec/rspec/core/world_spec.rb | 42 ++++++++++++++++++- 9 files changed, 122 insertions(+), 4 deletions(-) diff --git a/features/command_line/only_failures.feature b/features/command_line/only_failures.feature index 9b13cb7e86..e974e874be 100644 --- a/features/command_line/only_failures.feature +++ b/features/command_line/only_failures.feature @@ -102,3 +102,8 @@ Feature: Only Failures When I run `rspec --next-failure` Then the output should contain "All examples were filtered out" + + Scenario: Clear error given when using `--only-failures` without configuring `example_status_persistence_file_path` + Given I have not configured `example_status_persistence_file_path` + When I run `rspec --only-failures` + Then it should fail with "To use `--only-failures`, you must first set `config.example_status_persistence_file_path`." diff --git a/features/step_definitions/additional_cli_steps.rb b/features/step_definitions/additional_cli_steps.rb index 8100949383..1f58a4e5a0 100644 --- a/features/step_definitions/additional_cli_steps.rb +++ b/features/step_definitions/additional_cli_steps.rb @@ -138,3 +138,15 @@ File.open(file_name, "w") { |f| f.write(fixed) } end end + +Then(/^it should fail with "(.*?)"$/) do |snippet| + assert_failing_with(snippet) +end + +Given(/^I have not configured `example_status_persistence_file_path`$/) do + in_current_dir do + return unless File.exist?("spec/spec_helper.rb") + return unless File.read("spec/spec_helper.rb").include?("example_status_persistence_file_path") + File.open("spec/spec_helper.rb", "w") { |f| f.write("") } + end +end diff --git a/lib/rspec/core/configuration.rb b/lib/rspec/core/configuration.rb index 95da2f5e37..4fe02fdbb8 100644 --- a/lib/rspec/core/configuration.rb +++ b/lib/rspec/core/configuration.rb @@ -176,6 +176,11 @@ def example_status_persistence_file_path=(value) define_reader :only_failures alias_method :only_failures?, :only_failures + # @private + def only_failures_but_not_configured? + only_failures? && !example_status_persistence_file_path + end + # @macro add_setting # Clean up and exit after the first failure (default: `false`). add_setting :fail_fast diff --git a/lib/rspec/core/formatters/base_text_formatter.rb b/lib/rspec/core/formatters/base_text_formatter.rb index 03b79fb18a..262a8d3dda 100644 --- a/lib/rspec/core/formatters/base_text_formatter.rb +++ b/lib/rspec/core/formatters/base_text_formatter.rb @@ -68,6 +68,7 @@ def close(_notification) output.puts + output.flush output.close unless output == $stdout end end diff --git a/lib/rspec/core/reporter.rb b/lib/rspec/core/reporter.rb index c4bf54696f..5c430cfa3c 100644 --- a/lib/rspec/core/reporter.rb +++ b/lib/rspec/core/reporter.rb @@ -154,7 +154,7 @@ def finish @pending_examples, @load_time) notify :seed, Notifications::SeedNotification.new(@configuration.seed, seed_used?) ensure - notify :close, Notifications::NullNotification + close end # @private @@ -170,8 +170,19 @@ def notify(event, notification) end end + # @private + def abort_with(msg, exit_status) + message(msg) + close + exit!(exit_status) + end + private + def close + notify :close, Notifications::NullNotification + end + def mute_profile_output? # Don't print out profiled info if there are failures and `--fail-fast` is # used, it just clutters the output. diff --git a/lib/rspec/core/world.rb b/lib/rspec/core/world.rb index 0e95b421ef..48c22b2dd4 100644 --- a/lib/rspec/core/world.rb +++ b/lib/rspec/core/world.rb @@ -112,6 +112,7 @@ def reporter # # Notify reporter of filters. def announce_filters + fail_if_config_and_cli_options_invalid filter_announcements = [] announce_inclusion_filter filter_announcements @@ -175,6 +176,16 @@ def announce_exclusion_filter(announcements) def declaration_line_numbers @declaration_line_numbers ||= FlatMap.flat_map(example_groups, &:declaration_line_numbers) end + + def fail_if_config_and_cli_options_invalid + return unless @configuration.only_failures_but_not_configured? + + reporter.abort_with( + "\nTo use `--only-failures`, you must first set " \ + "`config.example_status_persistence_file_path`.", + 1 # exit code + ) + end end end end diff --git a/spec/rspec/core/formatters/base_text_formatter_spec.rb b/spec/rspec/core/formatters/base_text_formatter_spec.rb index e0ddd835e9..43159cbe55 100644 --- a/spec/rspec/core/formatters/base_text_formatter_spec.rb +++ b/spec/rspec/core/formatters/base_text_formatter_spec.rb @@ -5,13 +5,23 @@ include FormatterSupport context "when closing the formatter", :isolated_directory => true do + let(:output_to_close) { File.new("./output_to_close", "w") } + let(:formatter) { described_class.new(output_to_close) } + it 'does not close an already closed output stream' do - output_to_close = File.new("./output_to_close", "w") - formatter = described_class.new(output_to_close) output_to_close.close expect { formatter.close(RSpec::Core::Notifications::NullNotification) }.not_to raise_error end + + it "flushes output before closing the stream so buffered bytes are not lost if we exit right away" do + expect(output_to_close).to receive(:flush).ordered.and_call_original + # Windows appears to not let the `:isolated_directory` shared group cleanup if + # the file isn't closed, so we need to use `and_call_original` here. + expect(output_to_close).to receive(:close).ordered.and_call_original + + formatter.close(RSpec::Core::Notifications::NullNotification) + end end describe "#dump_summary" do diff --git a/spec/rspec/core/reporter_spec.rb b/spec/rspec/core/reporter_spec.rb index fefd5fac09..2b554a0abb 100644 --- a/spec/rspec/core/reporter_spec.rb +++ b/spec/rspec/core/reporter_spec.rb @@ -206,6 +206,29 @@ module RSpec::Core end end + describe "#abort_with" do + before { allow(reporter).to receive(:exit!) } + + it "publishes the message and notifies :close" do + listener = double("Listener") + reporter.register_listener(listener, :message, :close) + stream = StringIO.new + + allow(listener).to receive(:message) { |n| stream << n.message } + allow(listener).to receive(:close) { stream.close } + + reporter.register_listener(listener) + reporter.abort_with("Booom!", 1) + + expect(stream).to have_attributes(:string => "Booom!").and be_closed + end + + it "exits with the provided exit code" do + reporter.abort_with("msg", 13) + expect(reporter).to have_received(:exit!).with(13) + end + end + describe "timing" do before do config.start_time = start_time diff --git a/spec/rspec/core/world_spec.rb b/spec/rspec/core/world_spec.rb index 318f579a6d..476e1c84f8 100644 --- a/spec/rspec/core/world_spec.rb +++ b/spec/rspec/core/world_spec.rb @@ -109,9 +109,49 @@ module RSpec::Core end describe "#announce_filters" do - let(:reporter) { double('reporter').as_null_object } + let(:reporter) { instance_spy(Reporter) } before { allow(world).to receive(:reporter) { reporter } } + context "when --only-failures is passed" do + before { configuration.force(:only_failures => true) } + + context "and `example_status_persistence_file_path` is not configured" do + it 'aborts with a message explaining the config option must be set first' do + configuration.example_status_persistence_file_path = nil + world.announce_filters + expect(reporter).to have_received(:abort_with).with(/example_status_persistence_file_path/, 1) + end + end + + context "and `example_status_persistence_file_path` is configured" do + it 'does not abort' do + configuration.example_status_persistence_file_path = "foo.txt" + world.announce_filters + expect(reporter).not_to have_received(:abort_with) + end + end + end + + context "when --only-failures is not passed" do + before { expect(configuration.only_failures?).not_to eq true } + + context "and `example_status_persistence_file_path` is not configured" do + it 'does not abort' do + configuration.example_status_persistence_file_path = nil + world.announce_filters + expect(reporter).not_to have_received(:abort_with) + end + end + + context "and `example_status_persistence_file_path` is configured" do + it 'does not abort' do + configuration.example_status_persistence_file_path = "foo.txt" + world.announce_filters + expect(reporter).not_to have_received(:abort_with) + end + end + end + context "with no examples" do before { allow(world).to receive(:example_count) { 0 } } From 3258466dda760908d226ab7c2749c7a219ff2947 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Sat, 14 Mar 2015 23:42:42 -0700 Subject: [PATCH 088/258] =?UTF-8?q?Provide=20friendly=20warnings=20when=20?= =?UTF-8?q?we=20can=E2=80=99t=20access=20status=20file.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/rspec/core/configuration.rb | 14 +++- lib/rspec/core/runner.rb | 14 ++-- spec/integration/persistence_failures_spec.rb | 69 +++++++++++++++++++ spec/support/aruba_support.rb | 3 + 4 files changed, 91 insertions(+), 9 deletions(-) create mode 100644 spec/integration/persistence_failures_spec.rb diff --git a/lib/rspec/core/configuration.rb b/lib/rspec/core/configuration.rb index 4fe02fdbb8..ef5088cad4 100644 --- a/lib/rspec/core/configuration.rb +++ b/lib/rspec/core/configuration.rb @@ -841,9 +841,17 @@ def files_to_run def last_run_statuses @last_run_statuses ||= Hash.new(UNKNOWN_STATUS).tap do |statuses| if (path = example_status_persistence_file_path) - ExampleStatusPersister.load_from(path).inject(statuses) do |hash, example| - hash[example.fetch(:example_id)] = example.fetch(:status) - hash + begin + ExampleStatusPersister.load_from(path).inject(statuses) do |hash, example| + hash[example.fetch(:example_id)] = example.fetch(:status) + hash + end + rescue SystemCallError => e + RSpec.warning "Could not read from #{path.inspect} (configured as " \ + "`config.example_status_persistence_file_path`) due " \ + "to a system error: #{e.inspect}. Please check that " \ + "the config option is set to an accessible, valid " \ + "file path", :call_site => nil end end end diff --git a/lib/rspec/core/runner.rb b/lib/rspec/core/runner.rb index fc0075eeb6..84db998cb2 100644 --- a/lib/rspec/core/runner.rb +++ b/lib/rspec/core/runner.rb @@ -117,12 +117,14 @@ def run_specs(example_groups) private def persist_example_statuses - return unless @configuration.example_status_persistence_file_path - - ExampleStatusPersister.persist( - @world.all_examples, - @configuration.example_status_persistence_file_path - ) + return unless (path = @configuration.example_status_persistence_file_path) + + ExampleStatusPersister.persist(@world.all_examples, path) + rescue SystemCallError => e + RSpec.warning "Could not write example statuses to #{path} (configured as " \ + "`config.example_status_persistence_file_path`) due to a " \ + "system error: #{e.inspect}. Please check that the config " \ + "option is set to an accessible, valid file path", :call_site => nil end # @private diff --git a/spec/integration/persistence_failures_spec.rb b/spec/integration/persistence_failures_spec.rb new file mode 100644 index 0000000000..d26a78a8e6 --- /dev/null +++ b/spec/integration/persistence_failures_spec.rb @@ -0,0 +1,69 @@ +require 'support/aruba_support' + +RSpec.describe 'Persistence failures' do + include_context "aruba support" + before { clean_current_dir } + + context "when `config.example_status_persistence_file_path` is configured" do + context "to an invalid file path (e.g. spec/spec_helper.rb/examples.txt)" do + before do + write_file_formatted "spec/1_spec.rb", " + RSpec.configure do |c| + c.example_status_persistence_file_path = 'spec/1_spec.rb/examples.txt' + end + RSpec.describe { example { } } + " + end + + it 'emits a helpful warning to the user, indicating we cannot write to it, and still runs the spec suite' do + run_command "spec/1_spec.rb" + + expect(last_cmd_stderr).to include( + "WARNING: Could not write", + "spec/1_spec.rb/examples.txt", + "config.example_status_persistence_file_path", + "Errno:" + ) + expect(last_cmd_stdout).to include("1 example") + end + end + + context "to a file path for which we lack permissions" do + before do + write_file_formatted "spec/1_spec.rb", " + RSpec.configure do |c| + c.example_status_persistence_file_path = 'spec/examples.txt' + end + RSpec.describe { example { } } + " + + write_file_formatted "spec/examples.txt", "" + in_current_dir do + FileUtils.chmod 0000, "spec/examples.txt" + end + end + + + it 'emits a helpful warning to the user, indicating we cannot read from it, and still runs the spec suite' do + run_command "spec/1_spec.rb" + + expected_snippets = [ + "WARNING: Could not read", + "spec/examples.txt", + "config.example_status_persistence_file_path", + "Errno:" + ] + + if RSpec::Support::OS.windows? + # Not sure why, but on windows it doesn't trigger the read error, it + # triggers a write error instead. The important thing is that whatever + # system error occurs is reported accurately. + expected_snippets[0] = "WARNING: Could not write" + end + + expect(last_cmd_stderr).to include(*expected_snippets) + expect(last_cmd_stdout).to include("1 example") + end + end + end +end diff --git a/spec/support/aruba_support.rb b/spec/support/aruba_support.rb index 28929775da..492af9ebb2 100644 --- a/spec/support/aruba_support.rb +++ b/spec/support/aruba_support.rb @@ -17,6 +17,9 @@ def run_command(cmd) temp_stdout = StringIO.new temp_stderr = StringIO.new + + # So that `RSpec.warning` will go to temp_stderr. + allow(::Kernel).to receive(:warn) { |msg| temp_stderr.puts(msg) } cmd_parts = Shellwords.split(cmd) handle_current_dir_change do From 8ced2876e5fb5edac778b74cbc610586825d8b05 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Sat, 14 Mar 2015 23:51:50 -0700 Subject: [PATCH 089/258] Add `example_status_persistence_file_path` generated spec_helper.rb --- lib/rspec/core/project_initializer/spec/spec_helper.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/rspec/core/project_initializer/spec/spec_helper.rb b/lib/rspec/core/project_initializer/spec/spec_helper.rb index b598abee98..dde64fb899 100644 --- a/lib/rspec/core/project_initializer/spec/spec_helper.rb +++ b/lib/rspec/core/project_initializer/spec/spec_helper.rb @@ -50,6 +50,11 @@ config.filter_run :focus config.run_all_when_everything_filtered = true + # Allows RSpec to persist some state between runs in order to support + # the `--only-failures` and `--next-failure` CLI options. We recommend + # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = "spec/examples.txt" + # Limits the available syntax to the non-monkey patched syntax that is # recommended. For more details, see: # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax From af8628f83718a7eee9d5910cd4684b8d274101da Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Sun, 15 Mar 2015 14:42:22 -0700 Subject: [PATCH 090/258] Add backtrace to appveyor build (will later port to rspec-dev). --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 15461acc38..b9be4b3c17 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -25,7 +25,7 @@ install: - cinst ansicon test_script: - - bundle exec rspec + - bundle exec rspec --backtrace environment: matrix: From 6fc8cf19181e00d7e0d9245e030af0a8e89cf39a Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Sun, 15 Mar 2015 22:57:16 -0700 Subject: [PATCH 091/258] Updated travis build scripts (from rspec-dev) --- .rubocop_rspec_base.yml | 2 +- .travis.yml | 2 +- appveyor.yml | 2 +- script/clone_all_rspec_repos | 2 +- script/functions.sh | 3 ++- script/predicate_functions.sh | 2 +- script/run_build | 2 +- script/travis_functions.sh | 2 +- 8 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.rubocop_rspec_base.yml b/.rubocop_rspec_base.yml index 9e52c97e36..5a3a5b0caf 100644 --- a/.rubocop_rspec_base.yml +++ b/.rubocop_rspec_base.yml @@ -1,4 +1,4 @@ -# This file was generated on 2015-02-24T14:46:39-08:00 from the rspec-dev repo. +# This file was generated on 2015-03-15T22:57:16-07:00 from the rspec-dev repo. # DO NOT modify it by hand as your changes will get lost the next time it is generated. # This file contains defaults for RSpec projects. Individual projects diff --git a/.travis.yml b/.travis.yml index 636f134028..8a9d1e33c1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ -# This file was generated on 2015-02-24T14:46:39-08:00 from the rspec-dev repo. +# This file was generated on 2015-03-15T22:57:16-07:00 from the rspec-dev repo. # DO NOT modify it by hand as your changes will get lost the next time it is generated. language: ruby diff --git a/appveyor.yml b/appveyor.yml index b9be4b3c17..668e41645b 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,4 @@ -# This file was generated on 2015-02-24T14:46:39-08:00 from the rspec-dev repo. +# This file was generated on 2015-03-15T22:57:16-07:00 from the rspec-dev repo. # DO NOT modify it by hand as your changes will get lost the next time it is generated. version: "{build}" diff --git a/script/clone_all_rspec_repos b/script/clone_all_rspec_repos index c7fae3a8a1..c0bf6b51e6 100755 --- a/script/clone_all_rspec_repos +++ b/script/clone_all_rspec_repos @@ -1,5 +1,5 @@ #!/bin/bash -# This file was generated on 2015-02-24T14:46:39-08:00 from the rspec-dev repo. +# This file was generated on 2015-03-15T22:57:16-07:00 from the rspec-dev repo. # DO NOT modify it by hand as your changes will get lost the next time it is generated. set -e diff --git a/script/functions.sh b/script/functions.sh index 1faf04ad51..30b0b42692 100644 --- a/script/functions.sh +++ b/script/functions.sh @@ -1,4 +1,4 @@ -# This file was generated on 2015-02-24T14:46:39-08:00 from the rspec-dev repo. +# This file was generated on 2015-03-15T22:57:16-07:00 from the rspec-dev repo. # DO NOT modify it by hand as your changes will get lost the next time it is generated. SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" @@ -60,6 +60,7 @@ function run_specs_one_by_one { echo "Running each spec file, one-by-one..." for file in `find spec -iname '*_spec.rb'`; do + echo "Running $file" bin/rspec $file -b --format progress done } diff --git a/script/predicate_functions.sh b/script/predicate_functions.sh index 1c9275fbfb..f4478edf1d 100644 --- a/script/predicate_functions.sh +++ b/script/predicate_functions.sh @@ -1,4 +1,4 @@ -# This file was generated on 2015-02-24T14:46:39-08:00 from the rspec-dev repo. +# This file was generated on 2015-03-15T22:57:16-07:00 from the rspec-dev repo. # DO NOT modify it by hand as your changes will get lost the next time it is generated. function is_mri { diff --git a/script/run_build b/script/run_build index e78cbd8435..26f1b82f41 100755 --- a/script/run_build +++ b/script/run_build @@ -1,5 +1,5 @@ #!/bin/bash -# This file was generated on 2015-02-24T14:46:39-08:00 from the rspec-dev repo. +# This file was generated on 2015-03-15T22:57:16-07:00 from the rspec-dev repo. # DO NOT modify it by hand as your changes will get lost the next time it is generated. set -e diff --git a/script/travis_functions.sh b/script/travis_functions.sh index 8e86947b78..5c3691fb96 100644 --- a/script/travis_functions.sh +++ b/script/travis_functions.sh @@ -1,4 +1,4 @@ -# This file was generated on 2015-02-24T14:46:39-08:00 from the rspec-dev repo. +# This file was generated on 2015-03-15T22:57:16-07:00 from the rspec-dev repo. # DO NOT modify it by hand as your changes will get lost the next time it is generated. # Taken from: From e072e5358b85a971010bc1a22d04eeca646321c8 Mon Sep 17 00:00:00 2001 From: Fabio Napoleoni Date: Sun, 15 Mar 2015 22:30:16 +0100 Subject: [PATCH 092/258] Warn users when overriding methods (via let, def or define_method) in the same example group. --- .../example_groups/shared_examples.feature | 44 +++++++++++++++ lib/rspec/core/example_group.rb | 14 +++++ spec/rspec/core/example_group_spec.rb | 53 +++++++++++++++++++ 3 files changed, 111 insertions(+) diff --git a/features/example_groups/shared_examples.feature b/features/example_groups/shared_examples.feature index 27563be13a..f96efa5135 100644 --- a/features/example_groups/shared_examples.feature +++ b/features/example_groups/shared_examples.feature @@ -19,6 +19,50 @@ Feature: shared examples anything special (like autoload). Doing so would require a strict naming convention for files that would break existing suites. + **WARNING:** When you include parameterized examples in the current context multiple + times, you may override previous method definitions and last declaration wins. + So if you have this kind of shared example (or shared context) + + ```ruby + RSpec.shared_examples "some example" do |parameter| + \# Same behavior is triggered also with either `def something; 'some value'; end` + \# or `define_method(:something) { 'some value' }` + let(:something) { parameter } + it "uses the given parameter" do + expect(something).to eq(parameter) + end + end + + RSpec.describe SomeClass do + include_example "some example", "parameter1" + include_example "some example", "parameter2" + end + ``` + + You're actually doing this (notice that first example will fail): + + ```ruby + RSpec.describe SomeClass do + \# Reordered code for better understanding of what is happening + let(:something) { "parameter1" } + let(:something) { "parameter2" } + + it "uses the given parameter" do + \# This example will fail because last let "wins" + expect(something).to eq("parameter1") + end + + it "uses the given parameter" do + expect(something).to eq("parameter2") + end + end + ``` + + To prevent this kind of subtle error a warning is emitted if you declare multiple + methods with the same name in the same context. Should you get this warning + the simplest solution is to replace `include_example` with `it_behaves_like`, in this + way method overriding is avoided because of the nested context created by `it_behaves_like` + Conventions: ------------ diff --git a/lib/rspec/core/example_group.rb b/lib/rspec/core/example_group.rb index de555e280a..eeef4625ce 100644 --- a/lib/rspec/core/example_group.rb +++ b/lib/rspec/core/example_group.rb @@ -2,6 +2,8 @@ module RSpec module Core + # rubocop:disable Style/ClassLength + # ExampleGroup and {Example} are the main structural elements of # rspec-core. Consider this example: # @@ -650,6 +652,16 @@ def self.method_missing(name, *args) end private_class_method :method_missing + # @private + def self.method_added(method_name) + if (@__added_methods ||= Set.new).include?(method_name) + RSpec.warning "`#{self}##{method_name}` is being redefined " \ + "at #{RSpec::CallerFilter.first_non_rspec_line}. The original " \ + "definition will never be used and can be removed", :call_site => nil + end + @__added_methods << method_name + end + private def method_missing(name, *args) @@ -665,6 +677,8 @@ def method_missing(name, *args) end end + # rubocop:enable Style/ClassLength + # @private # Unnamed example group used by `SuiteHookContext`. class AnonymousExampleGroup < ExampleGroup diff --git a/spec/rspec/core/example_group_spec.rb b/spec/rspec/core/example_group_spec.rb index f19175777f..bbd6680419 100644 --- a/spec/rspec/core/example_group_spec.rb +++ b/spec/rspec/core/example_group_spec.rb @@ -1306,6 +1306,59 @@ def extract_execution_results(group) end + describe "when methods are redefined" do + + context "in the same example group" do + + it 'emits a warning when overriding methods using `let`' do + expect { + RSpec.describe do + let(:foo) { 'first value' } + let(:foo) { 'second value' } + end + }.to output(a_string_including("#foo", "is being redefined at #{__FILE__}:#{__LINE__ - 2}")).to_stderr + end + + it 'emits a warning when overriding methods using `def`' do + expect { + RSpec.describe do + let(:foo) { 'first value' } + def foo; 'second value'; end + end + }.to output(a_string_including("#foo", "is being redefined at #{__FILE__}:#{__LINE__ - 2}")).to_stderr + end + + it 'emits a warning when overriding methods using `define_method`' do + expect { + RSpec.describe do + let(:foo) { 'first value' } + define_method(:foo) { 'second value' } + end + }.to output(a_string_including("#foo", "is being redefined at #{__FILE__}:#{__LINE__ - 2}")).to_stderr + end + + end + + context "in nested example groups" do + + it 'does not emit warnings' do + expect { + RSpec.describe do + let(:foo) { 'first value' } + context 'in sub context' do + let(:foo) { 'second value' } + context 'in sub sub context' do + def foo; 'third value'; end + end + end + end + }.to avoid_outputting.to_stdout.and avoid_outputting.to_stderr + end + + end + + end + describe "ivars are not shared across examples" do it "(first example)" do @a = 1 From f98223d3dadae9a5180df7f209a2c0110539ddd6 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Mon, 16 Mar 2015 10:03:17 -0700 Subject: [PATCH 093/258] Add changelog for #1903. [ci skip] --- Changelog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Changelog.md b/Changelog.md index 7135b40b55..5104f29d2b 100644 --- a/Changelog.md +++ b/Changelog.md @@ -24,6 +24,8 @@ Enhancements: * Add `--next-failure` CLI option which allows you to repeatedly focus on just one of the currently failing examples, then move on to the next failure, etc. (Myron Marston, #1888) +* Warn when a helper method definition stomps an earlier definition + in the same example group. (Fabio Napoleoni, #1903) Bug Fixes: From 4a61665e173f83ee54c0831766efea0613867052 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Tue, 17 Mar 2015 23:09:49 -0700 Subject: [PATCH 094/258] =?UTF-8?q?Rename=20`safely`=20to=20make=20it=20mo?= =?UTF-8?q?re=20clear=20what=20it=E2=80=99s=20for.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/rspec/core/backtrace_formatter_spec.rb | 2 +- spec/rspec/core/formatters/snippet_extractor_spec.rb | 2 +- spec/rspec/core/metadata_spec.rb | 2 +- spec/rspec/core/notifications_spec.rb | 2 +- spec/support/helper_methods.rb | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/rspec/core/backtrace_formatter_spec.rb b/spec/rspec/core/backtrace_formatter_spec.rb index 7355243229..d9d9f554dd 100644 --- a/spec/rspec/core/backtrace_formatter_spec.rb +++ b/spec/rspec/core/backtrace_formatter_spec.rb @@ -206,7 +206,7 @@ def make_backtrace_formatter(exclusion_patterns=nil, inclusion_patterns=nil) end it "deals gracefully with a security error" do - safely do + with_safe_set_to_level_that_triggers_security_errors do self.formatter.__send__(:backtrace_line, __FILE__) # on some rubies, this doesn't raise a SecurityError; this test just # assures that if it *does* raise an error, the error is caught inside diff --git a/spec/rspec/core/formatters/snippet_extractor_spec.rb b/spec/rspec/core/formatters/snippet_extractor_spec.rb index 5227e2532f..2708be83a6 100644 --- a/spec/rspec/core/formatters/snippet_extractor_spec.rb +++ b/spec/rspec/core/formatters/snippet_extractor_spec.rb @@ -14,7 +14,7 @@ module Formatters it "falls back on a default message when it gets a security error" do message = nil - safely do + with_safe_set_to_level_that_triggers_security_errors do message = RSpec::Core::Formatters::SnippetExtractor.new.lines_around("blech", 8) end expect(message).to eq("# Couldn't get snippet for blech") diff --git a/spec/rspec/core/metadata_spec.rb b/spec/rspec/core/metadata_spec.rb index 7a6b2da1aa..5e363508fa 100644 --- a/spec/rspec/core/metadata_spec.rb +++ b/spec/rspec/core/metadata_spec.rb @@ -15,7 +15,7 @@ module Core end # I have no idea what line = line.sub(/\A([^:]+:\d+)$/, '\\1') is supposed to do it "gracefully returns nil if run in a secure thread" do - safely do + with_safe_set_to_level_that_triggers_security_errors do value = Metadata.relative_path(".") # on some rubies, File.expand_path is not a security error, so accept "." as well expect([nil, "."]).to include(value) diff --git a/spec/rspec/core/notifications_spec.rb b/spec/rspec/core/notifications_spec.rb index 499980a4ab..291dec721d 100644 --- a/spec/rspec/core/notifications_spec.rb +++ b/spec/rspec/core/notifications_spec.rb @@ -33,7 +33,7 @@ let(:exception) { instance_double(Exception, :backtrace => [ "#{__FILE__}:#{__LINE__}"]) } it "is handled gracefully" do - safely do + with_safe_set_to_level_that_triggers_security_errors do expect { notification.send(:read_failed_line) }.not_to raise_error end end diff --git a/spec/support/helper_methods.rb b/spec/support/helper_methods.rb index 4b6abe8705..ba7bab4882 100644 --- a/spec/support/helper_methods.rb +++ b/spec/support/helper_methods.rb @@ -11,7 +11,7 @@ def ignoring_warnings result end - def safely + def with_safe_set_to_level_that_triggers_security_errors Thread.new do ignoring_warnings { $SAFE = 3 } yield From ad53e7f8acd469e160a21121f23851183423ddfe Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Tue, 17 Mar 2015 17:43:54 -0700 Subject: [PATCH 095/258] Fix `RSpec::Core::RakeTask#failure_message`. `system` returns a boolean value to indicate success/failure, but the use of `failure_message` was in a `rescue` block that never got executed. It appears this has been broken since ea70e4edbbc52846dcd63308ee349166bbcef29c. Before that commit, we used `Rake::FileUtilsExt#ruby`, which does indicate failure by raising an error (and thus the `rescue` was correct). In ea70e4edbbc52846dcd63308ee349166bbcef29c, we switched to using `system` and the rescue was left in place but never got hit anymore. The lack of test coverage here is why we never noticed, so I addressed that as well. --- Changelog.md | 2 ++ lib/rspec/core/rake_task.rb | 12 ++++-------- spec/rspec/core/rake_task_spec.rb | 17 +++++++++++++++++ 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/Changelog.md b/Changelog.md index 5104f29d2b..9e0d380d67 100644 --- a/Changelog.md +++ b/Changelog.md @@ -32,6 +32,8 @@ Bug Fixes: * Handle invalid UTF-8 strings within exception methods. (Benjamin Fleischer, #1760) * Fix Rake Task quoting of file names with quotes to work properly on Windows. (Myron Marston, #1887) +* Fix `RSpec::Core::RakeTask#failure_message` so that it gets printed + when the task failed. (Myron Marston, #1905) ### 3.2.2 / 2015-03-11 [Full Changelog](http://github.com/rspec/rspec-core/compare/v3.2.1...v3.2.2) diff --git a/lib/rspec/core/rake_task.rb b/lib/rspec/core/rake_task.rb index 94ebe99a7b..78c32a8a36 100644 --- a/lib/rspec/core/rake_task.rb +++ b/lib/rspec/core/rake_task.rb @@ -63,16 +63,12 @@ def initialize(*args, &task_block) # @private def run_task(verbose) command = spec_command + puts command if verbose - begin - puts command if verbose - success = system(command) - rescue - puts failure_message if failure_message - end - - return unless fail_on_error && !success + return if system(command) + puts failure_message if failure_message + return unless fail_on_error $stderr.puts "#{command} failed" if verbose exit $?.exitstatus end diff --git a/spec/rspec/core/rake_task_spec.rb b/spec/rspec/core/rake_task_spec.rb index 94a0b5c0c6..f376da7b2f 100644 --- a/spec/rspec/core/rake_task_spec.rb +++ b/spec/rspec/core/rake_task_spec.rb @@ -80,6 +80,23 @@ def spec_command end end + context "when `failure_message` is configured" do + before do + allow(task).to receive(:exit) + task.failure_message = "Bad news" + end + + it 'prints it if the RSpec run failed' do + task.ruby_opts = '-e "exit(1);" ;#' + expect { task.run_task false }.to output(/Bad news/).to_stdout + end + + it 'does not print it if the RSpec run succeeded' do + task.ruby_opts = '-e "exit(0);" ;#' + expect { task.run_task false }.not_to output(/Bad/).to_stdout + end + end + context 'with custom exit status' do def silence_output(&block) expect(&block).to output(anything).to_stdout.and output(anything).to_stderr From 2ecdecc409746c4b38a3c3cbdbdd922066d898dd Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Tue, 17 Mar 2015 23:02:55 -0700 Subject: [PATCH 096/258] Remove unreachable code. `RSpec::CallerFilter.first_non_rspec_line` either returns the line or raises an error, so the branch for when `line` is nil could never be reached. --- lib/rspec/core/formatters.rb | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/rspec/core/formatters.rb b/lib/rspec/core/formatters.rb index 194da7957d..9a6eb160d3 100644 --- a/lib/rspec/core/formatters.rb +++ b/lib/rspec/core/formatters.rb @@ -138,13 +138,7 @@ def add(formatter_to_use, *paths) formatter = RSpec::LegacyFormatters.load_formatter formatter_class, *args @reporter.register_listener formatter, *formatter.notifications else - line = ::RSpec::CallerFilter.first_non_rspec_line - if line - call_site = "Formatter added at: #{line}" - else - call_site = "The formatter was added via command line flag or your "\ - "`.rspec` file." - end + call_site = "Formatter added at: #{::RSpec::CallerFilter.first_non_rspec_line}" RSpec.warn_deprecation <<-WARNING.gsub(/\s*\|/, ' ') |The #{formatter_class} formatter uses the deprecated formatter From 76e69766ac1713e534fbfe1b443fba5d188b203f Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Sun, 15 Mar 2015 14:37:07 -0700 Subject: [PATCH 097/258] Exclude version/platform-specific code from simplecov. --- lib/rspec/core/configuration.rb | 6 +++++- lib/rspec/core/example_group.rb | 8 ++++++++ lib/rspec/core/flat_map.rb | 2 ++ lib/rspec/core/hooks.rb | 6 +++++- lib/rspec/core/memoized_helpers.rb | 2 ++ lib/rspec/core/metadata_filter.rb | 4 ++++ lib/rspec/core/notifications.rb | 4 +++- lib/rspec/core/ordering.rb | 4 ++++ lib/rspec/core/rake_task.rb | 2 ++ lib/rspec/core/shared_example_group.rb | 2 ++ 10 files changed, 37 insertions(+), 3 deletions(-) diff --git a/lib/rspec/core/configuration.rb b/lib/rspec/core/configuration.rb index ef5088cad4..283fe01d50 100644 --- a/lib/rspec/core/configuration.rb +++ b/lib/rspec/core/configuration.rb @@ -1261,7 +1261,8 @@ def safe_include(mod, host) def safe_extend(mod, host) host.extend(mod) unless host.singleton_class < mod end - else + else # for 1.8.7 + # :nocov: # @private def safe_include(mod, host) host.__send__(:include, mod) unless host.included_modules.include?(mod) @@ -1271,6 +1272,7 @@ def safe_include(mod, host) def safe_extend(mod, host) host.extend(mod) unless (class << host; self; end).included_modules.include?(mod) end + # :nocov: end # @private @@ -1665,6 +1667,7 @@ def file_glob_from(path, pattern) end if RSpec::Support::OS.windows? + # :nocov: def absolute_pattern?(pattern) pattern =~ /\A[A-Z]:\\/ || windows_absolute_network_path?(pattern) end @@ -1673,6 +1676,7 @@ def windows_absolute_network_path?(pattern) return false unless ::File::ALT_SEPARATOR pattern.start_with?(::File::ALT_SEPARATOR + ::File::ALT_SEPARATOR) end + # :nocov: else def absolute_pattern?(pattern) pattern.start_with?(File::Separator) diff --git a/lib/rspec/core/example_group.rb b/lib/rspec/core/example_group.rb index eeef4625ce..1f69530bd9 100644 --- a/lib/rspec/core/example_group.rb +++ b/lib/rspec/core/example_group.rb @@ -482,6 +482,7 @@ def self.superclass_before_context_ivars superclass.before_context_ivars end else # 1.8.7 + # :nocov: # @private def self.superclass_before_context_ivars if superclass.respond_to?(:before_context_ivars) @@ -496,6 +497,7 @@ def self.superclass_before_context_ivars ancestors.find { |a| a.respond_to?(:before_context_ivars) }.before_context_ivars end end + # :nocov: end # @private @@ -603,8 +605,10 @@ def self.set_ivars(instance, ivars) end if RUBY_VERSION.to_f < 1.9 + # :nocov: # @private INSTANCE_VARIABLE_TO_IGNORE = '@__inspect_output'.freeze + # :nocov: else # @private INSTANCE_VARIABLE_TO_IGNORE = :@__inspect_output @@ -627,10 +631,12 @@ def inspect end unless method_defined?(:singleton_class) # for 1.8.7 + # :nocov: # @private def singleton_class class << self; self; end end + # :nocov: end # Raised when an RSpec API is called in the wrong scope, such as `before` @@ -774,6 +780,7 @@ def self.base_name_for(group) end if RUBY_VERSION == '1.9.2' + # :nocov: class << self alias _base_name_for base_name_for def base_name_for(group) @@ -781,6 +788,7 @@ def base_name_for(group) end end private_class_method :_base_name_for + # :nocov: end def self.disambiguate(name, const_scope) diff --git a/lib/rspec/core/flat_map.rb b/lib/rspec/core/flat_map.rb index 71093ac832..0e30cceb51 100644 --- a/lib/rspec/core/flat_map.rb +++ b/lib/rspec/core/flat_map.rb @@ -7,9 +7,11 @@ def flat_map(array, &block) array.flat_map(&block) end else # for 1.8.7 + # :nocov: def flat_map(array, &block) array.map(&block).flatten(1) end + # :nocov: end module_function :flat_map diff --git a/lib/rspec/core/hooks.rb b/lib/rspec/core/hooks.rb index 9c5601777c..84d8dda5a5 100644 --- a/lib/rspec/core/hooks.rb +++ b/lib/rspec/core/hooks.rb @@ -394,10 +394,12 @@ def execute_with(example, procsy) def hook_description "around hook at #{Metadata.relative_path(block.source_location.join(':'))}" end - else + else # for 1.8.7 + # :nocov: def hook_description "around hook" end + # :nocov: end end @@ -622,9 +624,11 @@ def owner_parent_groups @owner.parent_groups end else # Ruby < 2.1 (see https://bugs.ruby-lang.org/issues/8035) + # :nocov: def owner_parent_groups @owner_parent_groups ||= [@owner] + @owner.parent_groups end + # :nocov: end end end diff --git a/lib/rspec/core/memoized_helpers.rb b/lib/rspec/core/memoized_helpers.rb index bc95a07ded..857ce90af6 100644 --- a/lib/rspec/core/memoized_helpers.rb +++ b/lib/rspec/core/memoized_helpers.rb @@ -443,6 +443,7 @@ def self.define_helpers_on(example_group) # Gets the named constant or yields. # On 1.8, const_defined? / const_get do not take into # account the inheritance hierarchy. + # :nocov: def self.get_constant_or_yield(example_group, name) if example_group.const_defined?(name) example_group.const_get(name) @@ -450,6 +451,7 @@ def self.get_constant_or_yield(example_group, name) yield end end + # :nocov: else # @private # diff --git a/lib/rspec/core/metadata_filter.rb b/lib/rspec/core/metadata_filter.rb index 9c58d7f194..ee02570db0 100644 --- a/lib/rspec/core/metadata_filter.rb +++ b/lib/rspec/core/metadata_filter.rb @@ -127,6 +127,7 @@ def items_for(request_meta) end unless [].respond_to?(:each_with_object) # For 1.8.7 + # :nocov: undef items_for def items_for(request_meta) @items_and_filters.inject([]) do |to_return, (item, item_meta)| @@ -135,6 +136,7 @@ def items_for(request_meta) to_return end end + # :nocov: end end @@ -217,6 +219,7 @@ def proc_keys_from(metadata) end unless [].respond_to?(:each_with_object) # For 1.8.7 + # :nocov: undef proc_keys_from def proc_keys_from(metadata) metadata.inject([]) do |to_return, (key, value)| @@ -224,6 +227,7 @@ def proc_keys_from(metadata) to_return end end + # :nocov: end end end diff --git a/lib/rspec/core/notifications.rb b/lib/rspec/core/notifications.rb index 15cfbbc37e..638ba18099 100644 --- a/lib/rspec/core/notifications.rb +++ b/lib/rspec/core/notifications.rb @@ -209,13 +209,15 @@ def encoding_of(string) def encoded_string(string) RSpec::Support::EncodedString.new(string, Encoding.default_external) end - else + else # for 1.8.7 + # :nocov: def encoding_of(_string) end def encoded_string(string) RSpec::Support::EncodedString.new(string) end + # :nocov: end def backtrace_formatter diff --git a/lib/rspec/core/ordering.rb b/lib/rspec/core/ordering.rb index c274cdcd35..61546139ea 100644 --- a/lib/rspec/core/ordering.rb +++ b/lib/rspec/core/ordering.rb @@ -4,9 +4,11 @@ module Core # @private RandomNumberGenerator = ::Random else + # :nocov: RSpec::Support.require_rspec_core "backport_random" # @private RandomNumberGenerator = RSpec::Core::Backports::Random + # :nocov: end # @private @@ -42,6 +44,7 @@ def shuffle(list, rng) list.shuffle(:random => rng) end else + # :nocov: def shuffle(list, rng) shuffled = list.dup shuffled.size.times do |i| @@ -52,6 +55,7 @@ def shuffle(list, rng) shuffled end + # :nocov: end end diff --git a/lib/rspec/core/rake_task.rb b/lib/rspec/core/rake_task.rb index 78c32a8a36..dffea4736b 100644 --- a/lib/rspec/core/rake_task.rb +++ b/lib/rspec/core/rake_task.rb @@ -123,9 +123,11 @@ def file_inclusion_specification end if RSpec::Support::OS.windows? + # :nocov: def escape(shell_command) "'#{shell_command.gsub("'", "\\\\'")}'" end + # :nocov: else require 'shellwords' diff --git a/lib/rspec/core/shared_example_group.rb b/lib/rspec/core/shared_example_group.rb index 4af32607cb..4854f8b013 100644 --- a/lib/rspec/core/shared_example_group.rb +++ b/lib/rspec/core/shared_example_group.rb @@ -195,10 +195,12 @@ def formatted_location(block) if Proc.method_defined?(:source_location) def ensure_block_has_source_location(_block); end else # for 1.8.7 + # :nocov: def ensure_block_has_source_location(block) source_location = yield.split(':') block.extend Module.new { define_method(:source_location) { source_location } } end + # :nocov: end end end From 74a286d1fe44fe6a3a6a248ee2e92718b7353e71 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Mon, 16 Mar 2015 20:34:12 -0700 Subject: [PATCH 098/258] Remove dead code. --- lib/rspec/core/backtrace_formatter.rb | 2 -- lib/rspec/core/example.rb | 8 -------- lib/rspec/core/filter_manager.rb | 4 ---- lib/rspec/core/formatters/documentation_formatter.rb | 4 ---- lib/rspec/core/formatters/html_formatter.rb | 5 +---- lib/rspec/core/formatters/html_printer.rb | 8 ++------ lib/rspec/core/metadata.rb | 6 ------ lib/rspec/core/world.rb | 8 +------- 8 files changed, 4 insertions(+), 41 deletions(-) diff --git a/lib/rspec/core/backtrace_formatter.rb b/lib/rspec/core/backtrace_formatter.rb index b1dff2f1c7..e382e6e462 100644 --- a/lib/rspec/core/backtrace_formatter.rb +++ b/lib/rspec/core/backtrace_formatter.rb @@ -47,8 +47,6 @@ def format_backtrace(backtrace, options={}) def backtrace_line(line) Metadata.relative_path(line) unless exclude?(line) - rescue SecurityError - nil end def exclude?(line) diff --git a/lib/rspec/core/example.rb b/lib/rspec/core/example.rb index 6141f72e60..c38a260071 100644 --- a/lib/rspec/core/example.rb +++ b/lib/rspec/core/example.rb @@ -460,14 +460,6 @@ def location_description "example at #{location}" end - def skip_message - if String === skip - skip - else - Pending::NO_REASON_GIVEN - end - end - # Represents the result of executing an example. # Behaves like a hash for backwards compatibility. class ExecutionResult diff --git a/lib/rspec/core/filter_manager.rb b/lib/rspec/core/filter_manager.rb index 72d63f1774..886958bd4a 100644 --- a/lib/rspec/core/filter_manager.rb +++ b/lib/rspec/core/filter_manager.rb @@ -183,10 +183,6 @@ def add_with_low_priority(*args) apply_standalone_filter(*args) || super end - def use(*args) - apply_standalone_filter(*args) || super - end - def include_example?(example) @rules.empty? || super end diff --git a/lib/rspec/core/formatters/documentation_formatter.rb b/lib/rspec/core/formatters/documentation_formatter.rb index 5deb4a754f..fd50c8758b 100644 --- a/lib/rspec/core/formatters/documentation_formatter.rb +++ b/lib/rspec/core/formatters/documentation_formatter.rb @@ -64,10 +64,6 @@ def next_failure_index def current_indentation ' ' * @group_level end - - def example_group_chain - example_group.parent_groups.reverse - end end end end diff --git a/lib/rspec/core/formatters/html_formatter.rb b/lib/rspec/core/formatters/html_formatter.rb index 30868b45ba..bc348fa7a1 100644 --- a/lib/rspec/core/formatters/html_formatter.rb +++ b/lib/rspec/core/formatters/html_formatter.rb @@ -74,8 +74,6 @@ def example_failed(failure) :message => exception.message, :backtrace => failure.formatted_backtrace.join("\n") } - else - false end extra = extra_failure_content(failure) @@ -85,8 +83,7 @@ def example_failed(failure) example.execution_result.run_time, @failed_examples.size, exception_details, - (extra == "") ? false : extra, - true + (extra == "") ? false : extra ) @printer.flush end diff --git a/lib/rspec/core/formatters/html_printer.rb b/lib/rspec/core/formatters/html_printer.rb index 32a9f97d39..dfb68a94ae 100644 --- a/lib/rspec/core/formatters/html_printer.rb +++ b/lib/rspec/core/formatters/html_printer.rb @@ -35,7 +35,7 @@ def print_example_passed(description, run_time) # rubocop:disable Style/ParameterLists def print_example_failed(pending_fixed, description, run_time, failure_id, - exception, extra_content, escape_backtrace=false) + exception, extra_content) # rubocop:enable Style/ParameterLists formatted_run_time = "%.5f" % run_time @@ -45,11 +45,7 @@ def print_example_failed(pending_fixed, description, run_time, failure_id, @output.puts "
" if exception @output.puts "
#{h(exception[:message])}
" - if escape_backtrace - @output.puts "
#{h exception[:backtrace]}
" - else - @output.puts "
#{exception[:backtrace]}
" - end + @output.puts "
#{h exception[:backtrace]}
" end @output.puts extra_content if extra_content @output.puts "
" diff --git a/lib/rspec/core/metadata.rb b/lib/rspec/core/metadata.rb index c7350cadcc..a46578dff2 100644 --- a/lib/rspec/core/metadata.rb +++ b/lib/rspec/core/metadata.rb @@ -99,12 +99,6 @@ def self.deep_hash_dup(object) end end - # @private - def self.backtrace_from(block) - return caller unless block.respond_to?(:source_location) - [block.source_location.join(':')] - end - # @private def self.id_from(metadata) "#{metadata[:rerun_file_path]}[#{metadata[:scoped_id]}]" diff --git a/lib/rspec/core/world.rb b/lib/rspec/core/world.rb index 48c22b2dd4..fd6d7be858 100644 --- a/lib/rspec/core/world.rb +++ b/lib/rspec/core/world.rb @@ -137,13 +137,7 @@ def announce_filters example_groups.clear if filter_manager.empty? reporter.message("No examples found.") - elsif exclusion_filter.empty? - message = everything_filtered_message - if @configuration.run_all_when_everything_filtered? - message << "; ignoring #{inclusion_filter.description}" - end - reporter.message(message) - elsif inclusion_filter.empty? + elsif exclusion_filter.empty? || inclusion_filter.empty? reporter.message(everything_filtered_message) end end From 4dede483a24a30c042565c9ee4bb212b0d62e1db Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Mon, 16 Mar 2015 20:55:52 -0700 Subject: [PATCH 099/258] Add specs covering uncovered code. --- .../core/formatters/snippet_extractor.rb | 19 +++-- lib/rspec/core/runner.rb | 35 +++++--- spec/rspec/core/backtrace_formatter_spec.rb | 1 + spec/rspec/core/configuration_spec.rb | 30 +++++++ spec/rspec/core/drb_spec.rb | 9 +++ spec/rspec/core/example_group_spec.rb | 14 ++-- spec/rspec/core/example_spec.rb | 8 ++ .../core/formatters/json_formatter_spec.rb | 8 +- .../core/formatters/snippet_extractor_spec.rb | 27 ++++++- spec/rspec/core/metadata_spec.rb | 2 + spec/rspec/core/notifications_spec.rb | 31 +++++++ spec/rspec/core/option_parser_spec.rb | 29 +++++++ spec/rspec/core/runner_spec.rb | 81 +++++++++++++++++-- spec/rspec/core/shared_example_group_spec.rb | 1 + spec/rspec/core_spec.rb | 2 + spec/support/formatter_support.rb | 3 +- 16 files changed, 266 insertions(+), 34 deletions(-) diff --git a/lib/rspec/core/formatters/snippet_extractor.rb b/lib/rspec/core/formatters/snippet_extractor.rb index 2546acae69..bae3132d40 100644 --- a/lib/rspec/core/formatters/snippet_extractor.rb +++ b/lib/rspec/core/formatters/snippet_extractor.rb @@ -7,26 +7,30 @@ module Formatters # and applies synax highlighting and line numbers using html. class SnippetExtractor # @private - class NullConverter - def convert(code) + module NullConverter + def self.convert(code) %Q(#{code}\n# Install the coderay gem to get syntax highlighting) end end # @private - class CoderayConverter - def convert(code) + module CoderayConverter + def self.convert(code) CodeRay.scan(code, :ruby).html(:line_numbers => false) end end + # rubocop:disable Style/ClassVars + @@converter = NullConverter begin require 'coderay' - # rubocop:disable Style/ClassVars - @@converter = CoderayConverter.new + @@converter = CoderayConverter + # rubocop:disable Lint/HandleExceptions rescue LoadError - @@converter = NullConverter.new + # it'll fall back to the NullConverter assigned above + # rubocop:enable Lint/HandleExceptions end + # rubocop:enable Style/ClassVars # @api private @@ -43,6 +47,7 @@ def snippet(backtrace) highlighted = @@converter.convert(raw_code) post_process(highlighted, line) end + # rubocop:enable Style/ClassVars # @api private # diff --git a/lib/rspec/core/runner.rb b/lib/rspec/core/runner.rb index 84db998cb2..18dbc11d3f 100644 --- a/lib/rspec/core/runner.rb +++ b/lib/rspec/core/runner.rb @@ -17,20 +17,23 @@ def self.autorun return end - at_exit do - # Don't bother running any specs and just let the program terminate - # if we got here due to an unrescued exception (anything other than - # SystemExit, which is raised when somebody calls Kernel#exit). - next unless $!.nil? || $!.is_a?(SystemExit) - - # We got here because either the end of the program was reached or - # somebody called Kernel#exit. Run the specs and then override any - # existing exit status with RSpec's exit status if any specs failed. - invoke - end + at_exit { perform_at_exit } @installed_at_exit = true end + # @private + def self.perform_at_exit + # Don't bother running any specs and just let the program terminate + # if we got here due to an unrescued exception (anything other than + # SystemExit, which is raised when somebody calls Kernel#exit). + return unless $!.nil? || $!.is_a?(SystemExit) + + # We got here because either the end of the program was reached or + # somebody called Kernel#exit. Run the specs and then override any + # existing exit status with RSpec's exit status if any specs failed. + invoke + end + # Runs the suite of specs and exits the process with an appropriate exit # code. def self.invoke @@ -159,8 +162,14 @@ def self.running_in_drb? # @private def self.trap_interrupt - trap('INT') do - exit!(1) if RSpec.world.wants_to_quit + trap('INT') { handle_interrupt } + end + + # @private + def self.handle_interrupt + if RSpec.world.wants_to_quit + exit!(1) + else RSpec.world.wants_to_quit = true STDERR.puts "\nRSpec is shutting down and will print the summary report... Interrupt again to force quit." end diff --git a/spec/rspec/core/backtrace_formatter_spec.rb b/spec/rspec/core/backtrace_formatter_spec.rb index d9d9f554dd..099efb78cd 100644 --- a/spec/rspec/core/backtrace_formatter_spec.rb +++ b/spec/rspec/core/backtrace_formatter_spec.rb @@ -206,6 +206,7 @@ def make_backtrace_formatter(exclusion_patterns=nil, inclusion_patterns=nil) end it "deals gracefully with a security error" do + Metadata.instance_eval { @relative_path_regex = nil } with_safe_set_to_level_that_triggers_security_errors do self.formatter.__send__(:backtrace_line, __FILE__) # on some rubies, this doesn't raise a SecurityError; this test just diff --git a/spec/rspec/core/configuration_spec.rb b/spec/rspec/core/configuration_spec.rb index a2e49d4f60..78b0a7a0d1 100644 --- a/spec/rspec/core/configuration_spec.rb +++ b/spec/rspec/core/configuration_spec.rb @@ -1341,6 +1341,20 @@ def metadata_hash(*args) end end + describe "#backtrace_inclusion_patterns" do + before { config.backtrace_exclusion_patterns << /.*/ } + + it 'can be assigned to' do + config.backtrace_inclusion_patterns = [/foo/] + expect(config.backtrace_formatter.exclude?("food")).to be false + end + + it 'can be appended to' do + config.backtrace_inclusion_patterns << /foo/ + expect(config.backtrace_formatter.exclude?("food")).to be false + end + end + describe "#filter_gems_from_backtrace" do def exclude?(line) config.backtrace_formatter.exclude?(line) @@ -1357,6 +1371,22 @@ def exclude?(line) end end + describe "#profile_examples" do + it "defaults to false" do + expect(config.profile_examples).to be false + end + + it "can be set to an integer value" do + config.profile_examples = 17 + expect(config.profile_examples).to eq(17) + end + + it "returns 10 when set simply enabled" do + config.profile_examples = true + expect(config.profile_examples).to eq(10) + end + end + describe "#libs=" do it "adds directories to the LOAD_PATH" do expect($LOAD_PATH).to receive(:unshift).with("a/dir") diff --git a/spec/rspec/core/drb_spec.rb b/spec/rspec/core/drb_spec.rb index 505294d565..52d9adff96 100644 --- a/spec/rspec/core/drb_spec.rb +++ b/spec/rspec/core/drb_spec.rb @@ -82,6 +82,15 @@ def self.run(argv, err, out) DRb::stop_service end + it "falls back to `druby://:0` when `druby://localhost:0` fails" do + # see https://bugs.ruby-lang.org/issues/496 for background + expect(::DRb).to receive(:start_service).with("druby://localhost:0").and_raise(SocketError) + expect(::DRb).to receive(:start_service).with("druby://:0").and_call_original + + result = runner("--drb-port", @drb_port, passing_spec_filename).run(err, out) + expect(result).to be(0) + end + it "returns 0 if spec passes" do result = runner("--drb-port", @drb_port, passing_spec_filename).run(err, out) expect(result).to be(0) diff --git a/spec/rspec/core/example_group_spec.rb b/spec/rspec/core/example_group_spec.rb index bbd6680419..96a91c5521 100644 --- a/spec/rspec/core/example_group_spec.rb +++ b/spec/rspec/core/example_group_spec.rb @@ -1419,8 +1419,7 @@ def foo; 'third value'; end let(:group) { RSpec.describe } before do - allow(RSpec.world).to receive(:wants_to_quit) { true } - allow(RSpec.world).to receive(:clear_remaining_example_groups) + RSpec.world.wants_to_quit = true end it "returns without starting the group" do @@ -1430,16 +1429,19 @@ def foo; 'third value'; end context "at top level" do it "purges remaining groups" do - expect(RSpec.world).to receive(:clear_remaining_example_groups) - self.group.run(reporter) + expect { + self.group.run(reporter) + }.to change { RSpec.world.example_groups }.from([self.group]).to([]) end end context "in a nested group" do it "does not purge remaining groups" do nested_group = self.group.describe - expect(RSpec.world).not_to receive(:clear_remaining_example_groups) - nested_group.run(reporter) + + expect { + nested_group.run(reporter) + }.not_to change { RSpec.world.example_groups }.from([self.group]) end end end diff --git a/spec/rspec/core/example_spec.rb b/spec/rspec/core/example_spec.rb index 05d864ca48..cee173c6ed 100644 --- a/spec/rspec/core/example_spec.rb +++ b/spec/rspec/core/example_spec.rb @@ -22,6 +22,14 @@ def metadata_hash(*args) expect { ignoring_warnings { pp example_instance }}.to output(/RSpec::Core::Example/).to_stdout end + describe "#rerun_argument" do + it "returns the location-based rerun argument" do + allow(RSpec.configuration).to receive_messages(:loaded_spec_files => [__FILE__]) + example = RSpec.describe.example + expect(example.rerun_argument).to eq("#{RSpec::Core::Metadata.relative_path(__FILE__)}:#{__LINE__ - 1}") + end + end + describe "#exception" do it "supplies the first exception raised, if any" do RSpec.configuration.output_stream = StringIO.new diff --git a/spec/rspec/core/formatters/json_formatter_spec.rb b/spec/rspec/core/formatters/json_formatter_spec.rb index 4e42215be1..9193e56b51 100644 --- a/spec/rspec/core/formatters/json_formatter_spec.rb +++ b/spec/rspec/core/formatters/json_formatter_spec.rb @@ -13,7 +13,13 @@ RSpec.describe RSpec::Core::Formatters::JsonFormatter do include FormatterSupport - it "outputs json (brittle high level functional test)" do + it "can be loaded via `--format json`" do + formatter_output = run_example_specs_with_formatter("json", false) + parsed = JSON.parse(formatter_output) + expect(parsed.keys).to include("examples", "summary", "summary_line") + end + + it "outputs expected json (brittle high level functional test)" do group = RSpec.describe("one apiece") do it("succeeds") { expect(1).to eq 1 } it("fails") { fail "eek" } diff --git a/spec/rspec/core/formatters/snippet_extractor_spec.rb b/spec/rspec/core/formatters/snippet_extractor_spec.rb index 2708be83a6..1bece6c724 100644 --- a/spec/rspec/core/formatters/snippet_extractor_spec.rb +++ b/spec/rspec/core/formatters/snippet_extractor_spec.rb @@ -9,7 +9,7 @@ module Formatters end it "falls back on a default message when it doesn't find the file" do - expect(RSpec::Core::Formatters::SnippetExtractor.new.lines_around("blech", 8)).to eq("# Couldn't get snippet for blech") + expect(RSpec::Core::Formatters::SnippetExtractor.new.lines_around("blech", 8)).to eq("# Couldn't get snippet for blech") end it "falls back on a default message when it gets a security error" do @@ -19,6 +19,31 @@ module Formatters end expect(message).to eq("# Couldn't get snippet for blech") end + + describe "snippet extraction" do + let(:snippet) do + SnippetExtractor.new.snippet(["#{__FILE__}:#{__LINE__}"]) + end + + before do + # `send` is required for 1.8.7... + @orig_converter = SnippetExtractor.send(:class_variable_get, :@@converter) + end + + after do + SnippetExtractor.send(:class_variable_set, :@@converter, @orig_converter) + end + + it 'suggests you install coderay when it cannot be loaded' do + SnippetExtractor.send(:class_variable_set, :@@converter, SnippetExtractor::NullConverter) + + expect(snippet).to include("Install the coderay gem") + end + + it 'does not suggest installing coderay normally' do + expect(snippet).to exclude("Install the coderay gem") + end + end end end end diff --git a/spec/rspec/core/metadata_spec.rb b/spec/rspec/core/metadata_spec.rb index 5e363508fa..04ff8d0e99 100644 --- a/spec/rspec/core/metadata_spec.rb +++ b/spec/rspec/core/metadata_spec.rb @@ -15,6 +15,8 @@ module Core end # I have no idea what line = line.sub(/\A([^:]+:\d+)$/, '\\1') is supposed to do it "gracefully returns nil if run in a secure thread" do + # Ensure our call to `File.expand_path` is not cached as that is the insecure operation. + Metadata.instance_eval { @relative_path_regex = nil } with_safe_set_to_level_that_triggers_security_errors do value = Metadata.relative_path(".") # on some rubies, File.expand_path is not a security error, so accept "." as well diff --git a/spec/rspec/core/notifications_spec.rb b/spec/rspec/core/notifications_spec.rb index 291dec721d..dcf4992955 100644 --- a/spec/rspec/core/notifications_spec.rb +++ b/spec/rspec/core/notifications_spec.rb @@ -47,6 +47,16 @@ end end + context "when ruby reports a file that does not exist" do + let(:file) { "#{__FILE__}/blah.rb" } + let(:exception) { instance_double(Exception, :backtrace => [ "#{file}:1"]) } + + it "reports the filename and that it was unable to find the matching line" do + example.metadata[:absolute_file_path] = file + expect(notification.send(:read_failed_line)).to include("Unable to find #{file} to read failed line") + end + end + context "when the stacktrace includes relative paths (which can happen when using `rspec/autorun` and running files through `ruby`)" do let(:relative_file) { Pathname(__FILE__).relative_path_from(Pathname(Dir.pwd)) } line = __LINE__ @@ -117,3 +127,24 @@ end end end + +module RSpec::Core::Notifications + RSpec.describe ExamplesNotification do + include FormatterSupport + + describe "#notifications" do + it 'returns an array of notification objects for all the examples' do + reporter = RSpec::Core::Reporter.new(RSpec.configuration) + example = new_example + + reporter.example_started(example) + reporter.example_passed(example) + + notification = ExamplesNotification.new(reporter) + expect(notification.notifications).to match [ + an_instance_of(ExampleNotification) & an_object_having_attributes(:example => example) + ] + end + end + end +end diff --git a/spec/rspec/core/option_parser_spec.rb b/spec/rspec/core/option_parser_spec.rb index e78b521162..4544e1e396 100644 --- a/spec/rspec/core/option_parser_spec.rb +++ b/spec/rspec/core/option_parser_spec.rb @@ -1,3 +1,5 @@ +require 'rspec/core/project_initializer' + module RSpec::Core RSpec.describe OptionParser do before do @@ -59,6 +61,33 @@ def generate_help_text expect { generate_help_text }.to_not output(useless_lines).to_stdout end + %w[ -v --version ].each do |option| + describe option do + it "prints the version and exits" do + parser = Parser.new + expect(parser).to receive(:exit) + + expect { + parser.parse([option]) + }.to output("#{RSpec::Core::Version::STRING}\n").to_stdout + end + end + end + + describe "--init" do + it "initializes a project and exits" do + project_init = instance_double(ProjectInitializer) + allow(ProjectInitializer).to receive_messages(:new => project_init) + + parser = Parser.new + + expect(project_init).to receive(:run).ordered + expect(parser).to receive(:exit).ordered + + parser.parse(["--init"]) + end + end + describe "-I" do it "sets the path" do options = Parser.parse(%w[-I path/to/foo]) diff --git a/spec/rspec/core/runner_spec.rb b/spec/rspec/core/runner_spec.rb index 17f5114c60..830fbc2e9d 100644 --- a/spec/rspec/core/runner_spec.rb +++ b/spec/rspec/core/runner_spec.rb @@ -21,10 +21,30 @@ module RSpec::Core end end - describe 'at_exit' do + describe '.autorun' do + before do + @original_ivars = Hash[ Runner.instance_variables.map do |ivar| + [ivar, Runner.instance_variable_get(ivar)] + end ] + end + + after do + (@original_ivars.keys | Runner.instance_variables).each do |ivar| + if @original_ivars.key?(ivar) + Runner.instance_variable_set(ivar, @original_ivars[ivar]) + else + # send is necessary for 1.8.7 + Runner.send(:remove_instance_variable, ivar) + end + end + end + it 'sets an at_exit hook if none is already set' do - allow(RSpec::Core::Runner).to receive(:autorun_disabled?).and_return(false) - allow(RSpec::Core::Runner).to receive(:installed_at_exit?).and_return(false) + Runner.instance_eval do + @autorun_disabled = false + @installed_at_exit = false + end + allow(RSpec::Core::Runner).to receive(:running_in_drb?).and_return(false) allow(RSpec::Core::Runner).to receive(:invoke) expect(RSpec::Core::Runner).to receive(:at_exit) @@ -32,14 +52,65 @@ module RSpec::Core end it 'does not set the at_exit hook if it is already set' do - allow(RSpec::Core::Runner).to receive(:autorun_disabled?).and_return(false) - allow(RSpec::Core::Runner).to receive(:installed_at_exit?).and_return(true) + Runner.instance_eval do + @autorun_disabled = false + @installed_at_exit = true + end + allow(RSpec::Core::Runner).to receive(:running_in_drb?).and_return(false) expect(RSpec::Core::Runner).to receive(:at_exit).never RSpec::Core::Runner.autorun end end + describe "at_exit hook" do + before { allow(Runner).to receive(:invoke) } + + it 'normally runs the spec suite' do + Runner.perform_at_exit + expect(Runner).to have_received(:invoke) + end + + it 'does not run the suite if an error triggered the exit' do + begin + raise "boom" + rescue + Runner.perform_at_exit + end + + expect(Runner).not_to have_received(:invoke) + end + + it 'stil runs the suite if a `SystemExit` occurs since that is caused by `Kernel#exit`' do + begin + exit + rescue SystemExit + Runner.perform_at_exit + end + + expect(Runner).to have_received(:invoke) + end + end + + describe "interrupt handling" do + before { allow(Runner).to receive(:exit!) } + + it 'prints a message the first time, then exits the second time' do + expect { + Runner.handle_interrupt + }.to output(/shutting down/).to_stderr_from_any_process & + change { RSpec.world.wants_to_quit }.from(a_falsey_value).to(true) + + expect(Runner).not_to have_received(:exit!) + + expect { + Runner.handle_interrupt + }.not_to output.to_stderr_from_any_process + + expect(Runner).to have_received(:exit!) + end + end + # This is intermittently slow because this method calls out to the network # interface. describe ".running_in_drb?", :slow do diff --git a/spec/rspec/core/shared_example_group_spec.rb b/spec/rspec/core/shared_example_group_spec.rb index 65b82b8576..513932d9ad 100644 --- a/spec/rspec/core/shared_example_group_spec.rb +++ b/spec/rspec/core/shared_example_group_spec.rb @@ -53,6 +53,7 @@ module Core it "is not exposed to the global namespace when monkey patching is disabled" do RSpec.configuration.expose_dsl_globally = false + expect(RSpec.configuration.expose_dsl_globally?).to eq(false) expect(Kernel).to_not respond_to(shared_method_name) end diff --git a/spec/rspec/core_spec.rb b/spec/rspec/core_spec.rb index e061d6f5f6..d9ac27800d 100644 --- a/spec/rspec/core_spec.rb +++ b/spec/rspec/core_spec.rb @@ -237,6 +237,8 @@ expect(err).to eq("") expect(out.split("\n")).to eq(%w[ RSpec::Mocks RSpec::Expectations ]) expect(status.exitstatus).to eq(0) + + expect(RSpec.const_missing(:Expectations)).to be(RSpec::Expectations) end it 'correctly raises an error when an invalid const is referenced' do diff --git a/spec/support/formatter_support.rb b/spec/support/formatter_support.rb index 95a8377689..0e66e80d32 100644 --- a/spec/support/formatter_support.rb +++ b/spec/support/formatter_support.rb @@ -1,5 +1,5 @@ module FormatterSupport - def run_example_specs_with_formatter(formatter_option) + def run_example_specs_with_formatter(formatter_option, normalize_output=true) options = RSpec::Core::ConfigurationOptions.new(%W[spec/rspec/core/resources/formatter_specs.rb --format #{formatter_option} --order defined]) err, out = StringIO.new, StringIO.new @@ -13,6 +13,7 @@ def run_example_specs_with_formatter(formatter_option) runner.run(err, out) output = out.string + return output unless normalize_output output.gsub!(/\d+(?:\.\d+)?(s| seconds)/, "n.nnnn\\1") caller_line = RSpec::Core::Metadata.relative_path(caller.first) From 4cbc8c5f93467776fb34bf774c7a7f8924c69648 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Tue, 17 Mar 2015 23:55:40 -0700 Subject: [PATCH 100/258] Enforce 100% coverage on ruby >= 2.1.0. --- script/rspec_with_simplecov | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script/rspec_with_simplecov b/script/rspec_with_simplecov index 94f32b85a3..8a76dca3d5 100755 --- a/script/rspec_with_simplecov +++ b/script/rspec_with_simplecov @@ -24,14 +24,14 @@ begin # Simplecov emits some ruby warnings when loaded, so silence them. old_verbose, $VERBOSE = $VERBOSE, false - unless ENV['NO_COVERAGE'] || RUBY_VERSION < '1.9.3' + unless ENV['NO_COVERAGE'] || RUBY_VERSION.to_f < 2.1 require 'simplecov' SimpleCov.start do add_filter "./bundle/" add_filter "./tmp/" add_filter "./spec/" - minimum_coverage(RUBY_PLATFORM == 'java' ? 93 : 97) + minimum_coverage(100) end end rescue LoadError From b03aa2887fad5b0d17c2a5c452f6a2bb1bd77e03 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Wed, 18 Mar 2015 07:59:57 -0700 Subject: [PATCH 101/258] =?UTF-8?q?Make=20`=E2=80=94order=20random`=20more?= =?UTF-8?q?=20deterministic.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When you’re troubleshooting an order dependent failure, you want to get the repro case down to a minimal run that loads and runs as few specs as possible. With the old random ordering implementation, that was hard to achieve because while rerunning with a given seed produced the same order when the exact same set of examples were loaded, the ordering would be completely different when a subset was loaded. By ordering by `hash(seed + example_id)` it ensures that the ordering of any two examples should stay consistently regardless of how many other examples are loaded. Jenkins or MD5 is significantly slower than `shuffle`, but I think the tradeoff is worth it here. This isn’t a hot spot. --- benchmarks/hash_functions.rb | 74 ++++ .../shuffle_vs_sort_by_for_random_ordering.rb | 131 +++++++ benchmarks/sort_by_v_shuffle.rb | 83 ----- lib/rspec/core/backport_random.rb | 339 ------------------ lib/rspec/core/ordering.rb | 57 ++- spec/rspec/core/configuration_spec.rb | 15 +- spec/rspec/core/ordering_spec.rb | 39 +- spec/rspec/core/random_spec.rb | 45 --- 8 files changed, 279 insertions(+), 504 deletions(-) create mode 100644 benchmarks/hash_functions.rb create mode 100644 benchmarks/shuffle_vs_sort_by_for_random_ordering.rb delete mode 100644 benchmarks/sort_by_v_shuffle.rb delete mode 100644 lib/rspec/core/backport_random.rb delete mode 100644 spec/rspec/core/random_spec.rb diff --git a/benchmarks/hash_functions.rb b/benchmarks/hash_functions.rb new file mode 100644 index 0000000000..f394542bb2 --- /dev/null +++ b/benchmarks/hash_functions.rb @@ -0,0 +1,74 @@ +require 'benchmark/ips' +require 'digest/md5' + +MAX_32_BIT = 4294967295 + +def jenkins_iterative(string) + hash = 0 + + string.each_byte do |byte| + hash += byte + hash &= MAX_32_BIT + hash += ((hash << 10) & MAX_32_BIT) + hash &= MAX_32_BIT + hash ^= hash >> 6 + end + + hash += (hash << 3 & MAX_32_BIT) + hash &= MAX_32_BIT + hash ^= hash >> 11 + hash += (hash << 15 & MAX_32_BIT) + hash &= MAX_32_BIT + hash +end + +def jenkins_inject(string) + hash = string.each_byte.inject(0) do |byte, hash| + hash += byte + hash &= MAX_32_BIT + hash += ((hash << 10) & MAX_32_BIT) + hash &= MAX_32_BIT + hash ^= hash >> 6 + end + + hash += (hash << 3 & MAX_32_BIT) + hash &= MAX_32_BIT + hash ^= hash >> 11 + hash += (hash << 15 & MAX_32_BIT) + hash &= MAX_32_BIT + hash +end + +require 'benchmark/ips' + +Benchmark.ips do |x| + x.report("md5") do + Digest::MD5.digest("string") + end + + x.report("jenkins iterative") do + jenkins_iterative("string") + end + + x.report("jenkins inject") do + jenkins_inject("string") + end + + x.compare! +end + +__END__ + +Calculating ------------------------------------- + md5 39.416k i/100ms + jenkins iterative 22.646k i/100ms + jenkins inject 18.271k i/100ms +------------------------------------------------- + md5 654.294k (±15.7%) i/s - 3.193M + jenkins iterative 349.669k (±10.3%) i/s - 1.744M + jenkins inject 286.774k (± 5.5%) i/s - 1.443M + +Comparison: + md5: 654293.8 i/s + jenkins iterative: 349668.8 i/s - 1.87x slower + jenkins inject: 286774.4 i/s - 2.28x slower diff --git a/benchmarks/shuffle_vs_sort_by_for_random_ordering.rb b/benchmarks/shuffle_vs_sort_by_for_random_ordering.rb new file mode 100644 index 0000000000..5014538ae4 --- /dev/null +++ b/benchmarks/shuffle_vs_sort_by_for_random_ordering.rb @@ -0,0 +1,131 @@ +require 'benchmark/ips' +require 'digest/md5' + +class Digest::Jenkins + MAX_32_BIT = 4294967295 + + def self.digest(string) + hash = 0 + + string.each_byte do |byte| + hash += byte + hash &= MAX_32_BIT + hash += ((hash << 10) & MAX_32_BIT) + hash &= MAX_32_BIT + hash ^= hash >> 6 + end + + hash += (hash << 3 & MAX_32_BIT) + hash &= MAX_32_BIT + hash ^= hash >> 11 + hash += (hash << 15 & MAX_32_BIT) + hash &= MAX_32_BIT + hash + end +end + +Example = Struct.new(:id) +$seed = Kernel.srand.to_s + +def shuffle_list(list) + list.shuffle +end + +def sort_using_id(list) + list.sort_by(&:id) +end + +def sort_using_md5(list) + list.sort_by { |item| Digest::MD5.digest($seed + item.id) } +end + +def sort_using_jenkins(list) + list.sort_by { |item| Digest::Jenkins.digest($seed + item.id) } +end + +[10, 100, 1000, 10000].each do |size| + puts "Size: #{size}" + list = Array.new(size) { |i| Example.new("./some_spec.rb[1:#{i}]") } + + Benchmark.ips do |x| + x.report("shuffle") { shuffle_list(list) } + x.report("use id") { sort_using_id(list) } + x.report("use md5") { sort_using_md5(list) } + x.report("use jenkins") { sort_using_md5(list) } + x.compare! + end +end + +__END__ + +Size: 10 +Calculating ------------------------------------- + shuffle 71.860k i/100ms + use id 22.562k i/100ms + use md5 4.620k i/100ms + use jenkins 4.644k i/100ms +------------------------------------------------- + shuffle 1.594M (±12.4%) i/s - 7.905M + use id 299.105k (± 7.1%) i/s - 1.489M + use md5 49.663k (± 7.5%) i/s - 249.480k + use jenkins 49.389k (± 7.5%) i/s - 246.132k + +Comparison: + shuffle: 1593820.8 i/s + use id: 299104.9 i/s - 5.33x slower + use md5: 49662.9 i/s - 32.09x slower + use jenkins: 49389.2 i/s - 32.27x slower + +Size: 100 +Calculating ------------------------------------- + shuffle 24.629k i/100ms + use id 2.076k i/100ms + use md5 477.000 i/100ms + use jenkins 483.000 i/100ms +------------------------------------------------- + shuffle 317.269k (±13.8%) i/s - 1.576M + use id 20.958k (± 4.2%) i/s - 105.876k + use md5 4.916k (± 7.5%) i/s - 24.804k + use jenkins 4.824k (± 8.6%) i/s - 24.150k + +Comparison: + shuffle: 317269.5 i/s + use id: 20957.6 i/s - 15.14x slower + use md5: 4916.5 i/s - 64.53x slower + use jenkins: 4823.5 i/s - 65.78x slower + +Size: 1000 +Calculating ------------------------------------- + shuffle 3.862k i/100ms + use id 134.000 i/100ms + use md5 44.000 i/100ms + use jenkins 44.000 i/100ms +------------------------------------------------- + shuffle 40.104k (± 4.4%) i/s - 200.824k + use id 1.424k (±13.5%) i/s - 6.968k + use md5 450.556 (± 8.0%) i/s - 2.244k + use jenkins 450.189 (± 7.6%) i/s - 2.244k + +Comparison: + shuffle: 40104.2 i/s + use id: 1423.9 i/s - 28.16x slower + use md5: 450.6 i/s - 89.01x slower + use jenkins: 450.2 i/s - 89.08x slower + +Size: 10000 +Calculating ------------------------------------- + shuffle 374.000 i/100ms + use id 10.000 i/100ms + use md5 3.000 i/100ms + use jenkins 4.000 i/100ms +------------------------------------------------- + shuffle 3.750k (± 5.4%) i/s - 18.700k + use id 109.008 (± 4.6%) i/s - 550.000 + use md5 40.614 (± 9.8%) i/s - 201.000 + use jenkins 39.975 (± 7.5%) i/s - 200.000 + +Comparison: + shuffle: 3750.0 i/s + use id: 109.0 i/s - 34.40x slower + use md5: 40.6 i/s - 92.33x slower + use jenkins: 40.0 i/s - 93.81x slower diff --git a/benchmarks/sort_by_v_shuffle.rb b/benchmarks/sort_by_v_shuffle.rb deleted file mode 100644 index 1223c90889..0000000000 --- a/benchmarks/sort_by_v_shuffle.rb +++ /dev/null @@ -1,83 +0,0 @@ -require "benchmark" - -# This benchmark demonstrates the speed of Array#shuffle versus sorting by -# random numbers. This is in reference to ordering examples using the -# --order=rand command line flag. Array#shuffle also respects seeded random via -# Kernel.srand. - -LIST = (1..1_000).to_a.freeze - -Benchmark.bmbm do |x| - x.report("sort_by") do - 1_000.times do - LIST.sort_by { Kernel.rand(LIST.size) } - end - end - - x.report("shuffle") do - 1_000.times do - LIST.shuffle - end - end - - # http://en.wikipedia.org/wiki/Fisher-Yates_shuffle - # - # We use this algorithm as an alternative to `shuffle` on - # rubies (< 1.9.3) for which Array#shuffle does not accept - # a `:random` option. We do this to avoid affecting ruby's - # global randomization. - x.report('fisher-yates') do - 1_000.times do - rng = Random.new - list = LIST.dup - LIST.size.times do |i| - j = i + rng.rand(LIST.size - i) - next if i == j - list[i], list[j] = list[j], list[i] - end - end - end -end - -=begin - -Ruby 2.0.0: - -Rehearsal ------------------------------------------------ -sort_by 0.570000 0.010000 0.580000 ( 0.581875) -shuffle 0.020000 0.000000 0.020000 ( 0.021524) -fisher-yates 0.370000 0.020000 0.390000 ( 0.387855) ---------------------------------------- total: 0.990000sec - - user system total real -sort_by 0.560000 0.000000 0.560000 ( 0.561014) -shuffle 0.010000 0.000000 0.010000 ( 0.019814) -fisher-yates 0.350000 0.010000 0.360000 ( 0.358932) - -Ruby 1.9.3: - -Rehearsal ------------------------------------------------ -sort_by 0.690000 0.010000 0.700000 ( 0.701035) -shuffle 0.020000 0.000000 0.020000 ( 0.017603) -fisher-yates 0.440000 0.020000 0.460000 ( 0.464778) ---------------------------------------- total: 1.180000sec - - user system total real -sort_by 0.690000 0.000000 0.690000 ( 0.697824) -shuffle 0.020000 0.000000 0.020000 ( 0.018622) -fisher-yates 0.440000 0.010000 0.450000 ( 0.452260) - -JRuby: - -Rehearsal ------------------------------------------------ -sort_by 2.550000 0.050000 2.600000 ( 1.325000) -shuffle 0.090000 0.000000 0.090000 ( 0.057000) -fisher-yates 0.770000 0.010000 0.780000 ( 0.477000) ---------------------------------------- total: 3.470000sec - - user system total real -sort_by 0.470000 0.010000 0.480000 ( 0.442000) -shuffle 0.040000 0.000000 0.040000 ( 0.042000) -fisher-yates 0.300000 0.010000 0.310000 ( 0.283000) - -=end diff --git a/lib/rspec/core/backport_random.rb b/lib/rspec/core/backport_random.rb deleted file mode 100644 index 1b8afaf56a..0000000000 --- a/lib/rspec/core/backport_random.rb +++ /dev/null @@ -1,339 +0,0 @@ -module RSpec - module Core - # @private - # - # Methods used internally by the backports. - # - # This code was (mostly) ported from the backports gem found at - # https://github.com/marcandre/backports which is subject to this license: - # - # ========================================================================= - # - # Copyright (c) 2009 Marc-Andre Lafortune - # - # Permission is hereby granted, free of charge, to any person obtaining - # a copy of this software and associated documentation files (the - # "Software"), to deal in the Software without restriction, including - # without limitation the rights to use, copy, modify, merge, publish, - # distribute, sublicense, and/or sell copies of the Software, and to - # permit persons to whom the Software is furnished to do so, subject to - # the following conditions: - # - # The above copyright notice and this permission notice shall be - # included in all copies or substantial portions of the Software. - # - # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - # - # ========================================================================= - # - # The goal is to provide a random number generator in Ruby versions that do - # not have one. This was added to support localization of random spec - # ordering. - # - # These were in multiple files in backports, but merged into one here. - module Backports - # Helper method to coerce a value into a specific class. - # Raises a TypeError if the coercion fails or the returned value - # is not of the right class. - # (from Rubinius) - def self.coerce_to(obj, cls, meth) - return obj if obj.kind_of?(cls) - - begin - ret = obj.__send__(meth) - rescue Exception => e - raise TypeError, "Coercion error: #{obj.inspect}.#{meth} => #{cls} failed:\n" \ - "(#{e.message})" - end - raise TypeError, "Coercion error: obj.#{meth} did NOT return a #{cls} (was #{ret.class})" unless ret.kind_of? cls - ret - end - - # @private - def self.coerce_to_int(obj) - coerce_to(obj, Integer, :to_int) - end - - # Used internally to make it easy to deal with optional arguments. - # (from Rubinius) - Undefined = Object.new - - # @private - class Random - # @private - # An implementation of Mersenne Twister MT19937 in Ruby. - class MT19937 - STATE_SIZE = 624 - LAST_STATE = STATE_SIZE - 1 - PAD_32_BITS = 0xffffffff - - # See seed= - def initialize(seed) - self.seed = seed - end - - LAST_31_BITS = 0x7fffffff - OFFSET = 397 - - # Generates a completely new state out of the previous one. - def next_state - STATE_SIZE.times do |i| - mix = @state[i] & 0x80000000 | @state[i+1 - STATE_SIZE] & 0x7fffffff - @state[i] = @state[i+OFFSET - STATE_SIZE] ^ (mix >> 1) - @state[i] ^= 0x9908b0df if mix.odd? - end - @last_read = -1 - end - - # Seed must be either an Integer (only the first 32 bits will be used) - # or an Array of Integers (of which only the first 32 bits will be - # used). - # - # No conversion or type checking is done at this level. - def seed=(seed) - case seed - when Integer - @state = Array.new(STATE_SIZE) - @state[0] = seed & PAD_32_BITS - (1..LAST_STATE).each do |i| - @state[i] = (1812433253 * (@state[i-1] ^ @state[i-1]>>30) + i)& PAD_32_BITS - end - @last_read = LAST_STATE - when Array - self.seed = 19650218 - i=1 - j=0 - [STATE_SIZE, seed.size].max.times do - @state[i] = (@state[i] ^ (@state[i-1] ^ @state[i-1]>>30) * 1664525) + j + seed[j] & PAD_32_BITS - if (i+=1) >= STATE_SIZE - @state[0] = @state[-1] - i = 1 - end - j = 0 if (j+=1) >= seed.size - end - (STATE_SIZE-1).times do - @state[i] = (@state[i] ^ (@state[i-1] ^ @state[i-1]>>30) * 1566083941) - i & PAD_32_BITS - if (i+=1) >= STATE_SIZE - @state[0] = @state[-1] - i = 1 - end - end - @state[0] = 0x80000000 - else - raise ArgumentError, "Seed must be an Integer or an Array" - end - end - - # Returns a random Integer from the range 0 ... (1 << 32). - def random_32_bits - next_state if @last_read >= LAST_STATE - @last_read += 1 - y = @state[@last_read] - # Tempering - y ^= (y >> 11) - y ^= (y << 7) & 0x9d2c5680 - y ^= (y << 15) & 0xefc60000 - y ^= (y >> 18) - end - - # Supplement the MT19937 class with methods to do - # conversions the same way as MRI. - # No argument checking is done here either. - - FLOAT_FACTOR = 1.0/9007199254740992.0 - # Generates a random number on [0, 1) with 53-bit resolution. - def random_float - ((random_32_bits >> 5) * 67108864.0 + (random_32_bits >> 6)) * FLOAT_FACTOR; - end - - # Returns an integer within 0...upto. - def random_integer(upto) - n = upto - 1 - nb_full_32 = 0 - while n > PAD_32_BITS - n >>= 32 - nb_full_32 += 1 - end - mask = mask_32_bits(n) - begin - rand = random_32_bits & mask - nb_full_32.times do - rand <<= 32 - rand |= random_32_bits - end - end until rand < upto - rand - end - - def random_bytes(nb) - nb_32_bits = (nb + 3) / 4 - random = nb_32_bits.times.map { random_32_bits } - random.pack("L" * nb_32_bits)[0, nb] - end - - def state_as_bignum - b = 0 - @state.each_with_index do |val, i| - b |= val << (32 * i) - end - b - end - - def left # It's actually the number of words left + 1, as per MRI... - MT19937::STATE_SIZE - @last_read - end - - def marshal_dump - [state_as_bignum, left] - end - - def marshal_load(ary) - b, left = ary - @last_read = MT19937::STATE_SIZE - left - @state = Array.new(STATE_SIZE) - STATE_SIZE.times do |i| - @state[i] = b & PAD_32_BITS - b >>= 32 - end - end - - # Convert an Integer seed of arbitrary size to either a single 32 bit - # integer, or an Array of 32 bit integers. - def self.convert_seed(seed) - seed = seed.abs - long_values = [] - begin - long_values << (seed & PAD_32_BITS) - seed >>= 32 - end until seed == 0 - - # Done to allow any kind of sequence of integers. - long_values.pop if long_values[-1] == 1 && long_values.size > 1 - - long_values.size > 1 ? long_values : long_values.first - end - - def self.[](seed) - new(convert_seed(seed)) - end - - private - - MASK_BY = [1,2,4,8,16] - def mask_32_bits(n) - MASK_BY.each do |shift| - n |= n >> shift - end - n - end - end - - # @private - # Implementation corresponding to the actual Random class of Ruby - # The actual random generator (mersenne twister) is in MT19937. - # Ruby specific conversions are handled in bits_and_bytes. - # The high level stuff (argument checking) is done here. - module Implementation - attr_reader :seed - - def initialize(seed = 0) - super() - seed_rand seed - end - - def seed_rand(new_seed = 0) - new_seed = Backports.coerce_to_int(new_seed) - @seed = nil unless defined?(@seed) - old, @seed = @seed, new_seed.nonzero? || Random.new_seed - @mt = MT19937[ @seed ] - old - end - - def rand(limit = Backports::Undefined) - case limit - when Backports::Undefined - @mt.random_float - when Float - limit * @mt.random_float unless limit <= 0 - when Range - _rand_range(limit) - else - limit = Backports.coerce_to_int(limit) - @mt.random_integer(limit) unless limit <= 0 - end || raise(ArgumentError, "invalid argument #{limit}") - end - - def bytes(nb) - nb = Backports.coerce_to_int(nb) - raise ArgumentError, "negative size" if nb < 0 - @mt.random_bytes(nb) - end - - def ==(other) - other.is_a?(Random) && - seed == other.seed && - left == other.send(:left) && - state == other.send(:state) - end - - def marshal_dump - @mt.marshal_dump << @seed - end - - def marshal_load(ary) - @seed = ary.pop - @mt = MT19937.allocate - @mt.marshal_load(ary) - end - - private - - def state - @mt.state_as_bignum - end - - def left - @mt.left - end - - def _rand_range(limit) - range = limit.end - limit.begin - if (!range.is_a?(Float)) && range.respond_to?(:to_int) && range = Backports.coerce_to_int(range) - range += 1 unless limit.exclude_end? - limit.begin + @mt.random_integer(range) unless range <= 0 - elsif range = Backports.coerce_to(range, Float, :to_f) - if range < 0 - nil - elsif limit.exclude_end? - limit.begin + @mt.random_float * range unless range <= 0 - else - # cheat a bit... this will reduce the nb of random bits - loop do - r = @mt.random_float * range * 1.0001 - break limit.begin + r unless r > range - end - end - end - end - end - - def self.new_seed - (2 ** 62) + Kernel.rand(2 ** 62) - end - end - - class Random - include Implementation - class << self - include Implementation - end - end - end - end -end diff --git a/lib/rspec/core/ordering.rb b/lib/rspec/core/ordering.rb index 61546139ea..b505bc26a3 100644 --- a/lib/rspec/core/ordering.rb +++ b/lib/rspec/core/ordering.rb @@ -1,16 +1,5 @@ module RSpec module Core - if defined?(::Random) - # @private - RandomNumberGenerator = ::Random - else - # :nocov: - RSpec::Support.require_rspec_core "backport_random" - # @private - RandomNumberGenerator = RSpec::Core::Backports::Random - # :nocov: - end - # @private module Ordering # @private @@ -35,28 +24,38 @@ def used? def order(items) @used = true - rng = RandomNumberGenerator.new(@configuration.seed) - shuffle items, rng + + seed = @configuration.seed.to_s + items.sort_by { |item| jenkins_hash_digest(seed + item.id) } end - if RUBY_VERSION > '1.9.3' - def shuffle(list, rng) - list.shuffle(:random => rng) - end - else - # :nocov: - def shuffle(list, rng) - shuffled = list.dup - shuffled.size.times do |i| - j = i + rng.rand(shuffled.size - i) - next if i == j - shuffled[i], shuffled[j] = shuffled[j], shuffled[i] - end - - shuffled + private + + # http://en.wikipedia.org/wiki/Jenkins_hash_function + # Jenkins provides a good distribution and is simpler than MD5. + # It's a bit slower than MD5 (primarily because `Digest::MD5` is + # implemented in C) but has the advantage of not requiring us + # to load another part of stdlib, which we try to minimize. + def jenkins_hash_digest(string) + hash = 0 + + string.each_byte do |byte| + hash += byte + hash &= MAX_32_BIT + hash += ((hash << 10) & MAX_32_BIT) + hash &= MAX_32_BIT + hash ^= hash >> 6 end - # :nocov: + + hash += (hash << 3 & MAX_32_BIT) + hash &= MAX_32_BIT + hash ^= hash >> 11 + hash += (hash << 15 & MAX_32_BIT) + hash &= MAX_32_BIT + hash end + + MAX_32_BIT = 4_294_967_295 end # @private diff --git a/spec/rspec/core/configuration_spec.rb b/spec/rspec/core/configuration_spec.rb index 78b0a7a0d1..aae6a600a3 100644 --- a/spec/rspec/core/configuration_spec.rb +++ b/spec/rspec/core/configuration_spec.rb @@ -1760,18 +1760,21 @@ def metadata_hash(*args) end end + def example_numbered(num) + instance_double(Example, :id => "./foo_spec.rb[1:#{num}]") + end + describe "#force" do context "for ordering options" do - let(:list) { [1, 2, 3, 4] } + let(:list) { 1.upto(4).map { |i| example_numbered(i) } } let(:ordering_strategy) { config.ordering_registry.fetch(:global) } - let(:rng) { RSpec::Core::RandomNumberGenerator.new config.seed } - let(:shuffled) { Ordering::Random.new(config).shuffle list, rng } + let(:shuffled) { Ordering::Random.new(config).order list } specify "CLI `--order defined` takes precedence over `config.order = rand`" do config.force :order => "defined" config.order = "rand" - expect(ordering_strategy.order(list)).to eq([1, 2, 3, 4]) + expect(ordering_strategy.order(list)).to eq(list) end specify "CLI `--order rand:37` takes precedence over `config.order = defined`" do @@ -1793,7 +1796,7 @@ def metadata_hash(*args) specify "CLI `--order defined` takes precedence over `config.register_ordering(:global)`" do config.force :order => "defined" config.register_ordering(:global, &:reverse) - expect(ordering_strategy.order(list)).to eq([1, 2, 3, 4]) + expect(ordering_strategy.order(list)).to eq(list) end end @@ -1817,7 +1820,7 @@ def metadata_hash(*args) describe "#seed_used?" do def use_seed_on(registry) - registry.fetch(:random).order([1, 2]) + registry.fetch(:random).order([example_numbered(1), example_numbered(2)]) end it 'returns false if neither ordering registry used the seed' do diff --git a/spec/rspec/core/ordering_spec.rb b/spec/rspec/core/ordering_spec.rb index 2cda655899..579150c8ba 100644 --- a/spec/rspec/core/ordering_spec.rb +++ b/spec/rspec/core/ordering_spec.rb @@ -11,8 +11,12 @@ module Ordering describe '.order' do subject { described_class.new(configuration) } + def item(n) + instance_double(Example, :id => "./some_spec.rb[1:#{n}]") + end + let(:configuration) { RSpec::Core::Configuration.new } - let(:items) { 10.times.map { |n| n } } + let(:items) { 10.times.map { |n| item(n) } } let(:shuffled_items) { subject.order items } it 'shuffles the items randomly' do @@ -26,6 +30,37 @@ module Ordering end end + def order_with(seed) + configuration.seed = seed + subject.order(items) + end + + it 'has a good distribution', :slow do + orderings = 1.upto(1000).map do |seed| + order_with(seed) + end.uniq + + # Here we are making sure that our hash function used for ordering has a + # good distribution. Each seed produces a deterministic order and we want + # 99%+ of 1000 to be different. + expect(orderings.count).to be > 990 + end + + context "when given a subset of a list that was previously shuffled with the same seed" do + it "orders that subset the same as it was ordered before" do + all_items = 20.times.map { |n| item(n) } + + all_shuffled = subject.order(all_items) + expect(all_shuffled).not_to eq(all_items) + + last_half = all_items[10, 10] + last_half_shuffled = subject.order(last_half) + last_half_from_all_shuffled = all_shuffled.select { |i| last_half.include?(i) } + + expect(last_half_from_all_shuffled.map(&:id)).to eq(last_half_shuffled.map(&:id)) + end + end + context 'given randomization has been seeded explicitly' do before { @seed = srand } after { srand @seed } @@ -69,7 +104,7 @@ module Ordering end it 'returns true if the random orderer has been used' do - registry.fetch(:random).order([1, 2]) + registry.fetch(:random).order([RSpec.describe, RSpec.describe]) expect(registry.used_random_seed?).to be true end end diff --git a/spec/rspec/core/random_spec.rb b/spec/rspec/core/random_spec.rb deleted file mode 100644 index f1e8ea9c9a..0000000000 --- a/spec/rspec/core/random_spec.rb +++ /dev/null @@ -1,45 +0,0 @@ -module RSpec - module Core - RSpec.describe RandomNumberGenerator do - it 'is a random number generator' do - random = described_class.new - - expect([Fixnum, Bignum]).to include random.seed.class - expect(random.rand).to be_a Float - - rands = [] - 100.times do - rands << random.rand - end - - expect(rands.uniq.count).to be > 90 - end - - it 'produces the same results given the same seed' do - seed = rand(999) - - random = described_class.new(seed) - - expect(random.seed).to eq seed - - expected = [] - 5.times do - expected << random.rand(999) - end - - 10.times do - random = described_class.new(seed) - - expect(random.seed).to eq seed - - actual = [] - 5.times do - actual << random.rand(999) - end - - expect(actual).to eq expected - end - end - end - end -end From e1fe8b35038d38f8feb117100dcaf36cd98a65ed Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Thu, 19 Mar 2015 00:09:28 -0700 Subject: [PATCH 102/258] Add changelog for #1908. --- Changelog.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Changelog.md b/Changelog.md index 9e0d380d67..ed96ea732f 100644 --- a/Changelog.md +++ b/Changelog.md @@ -26,6 +26,9 @@ Enhancements: next failure, etc. (Myron Marston, #1888) * Warn when a helper method definition stomps an earlier definition in the same example group. (Fabio Napoleoni, #1903) +* Make `--order random` ordering stable, so that when you rerun a + subset with a given seed, the examples will be order consistently + relative to each other. (Myron Marston, #1908) Bug Fixes: From 3932d820b94e7be71a182549db171c1a3ac0a8cd Mon Sep 17 00:00:00 2001 From: Alex Kwiatkowski and Ryan Ong Date: Thu, 19 Mar 2015 08:36:53 -0400 Subject: [PATCH 103/258] Check if method belongs to singleton_class before removing it Fixes #1906 --- lib/rspec/core/example_group.rb | 2 +- spec/rspec/core_spec.rb | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/rspec/core/example_group.rb b/lib/rspec/core/example_group.rb index 1f69530bd9..f3e55c7750 100644 --- a/lib/rspec/core/example_group.rb +++ b/lib/rspec/core/example_group.rb @@ -37,7 +37,7 @@ class ExampleGroup # @private def self.idempotently_define_singleton_method(name, &definition) (class << self; self; end).module_exec do - remove_method(name) if method_defined?(name) + remove_method(name) if method_defined?(name) && instance_method(name).owner == self define_method(name, &definition) end end diff --git a/spec/rspec/core_spec.rb b/spec/rspec/core_spec.rb index d9ac27800d..a8f528404a 100644 --- a/spec/rspec/core_spec.rb +++ b/spec/rspec/core_spec.rb @@ -246,5 +246,14 @@ RSpec::NotAConst }.to raise_error(NameError, /RSpec::NotAConst/) end + + it "does not blow up if some gem defines `Kernel#it`", :slow do + code = 'Kernel.module_eval { def it(*); end }; require "rspec/core"' + out, err, status = run_ruby_with_current_load_path(code) + + expect(err).to eq("") + expect(out).to eq("") + expect(status.exitstatus).to eq(0) + end end From dcfec9c643e3f45377a41efd10a5edfc9e9a05c1 Mon Sep 17 00:00:00 2001 From: Leo Arnold Date: Thu, 19 Mar 2015 15:00:22 +0100 Subject: [PATCH 104/258] Corrected a typo in documentation --- features/example_groups/shared_examples.feature | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/features/example_groups/shared_examples.feature b/features/example_groups/shared_examples.feature index 27563be13a..331121aa35 100644 --- a/features/example_groups/shared_examples.feature +++ b/features/example_groups/shared_examples.feature @@ -60,13 +60,13 @@ Feature: shared examples end describe "#include?" do - context "with an an item that is in the collection" do + context "with an item that is in the collection" do it "returns true" do expect(collection.include?(7)).to be_truthy end end - context "with an an item that is not in the collection" do + context "with an item that is not in the collection" do it "returns false" do expect(collection.include?(9)).to be_falsey end @@ -91,9 +91,9 @@ Feature: shared examples initialized with 3 items says it has three items #include? - with an an item that is in the collection + with an item that is in the collection returns true - with an an item that is not in the collection + with an item that is not in the collection returns false Set @@ -101,9 +101,9 @@ Feature: shared examples initialized with 3 items says it has three items #include? - with an an item that is in the collection + with an item that is in the collection returns true - with an an item that is not in the collection + with an item that is not in the collection returns false """ From a56084a87b19ec502a594ddc1b85dd8d59b93f67 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Thu, 19 Mar 2015 08:08:18 -0700 Subject: [PATCH 105/258] Add changelog for #1907. [ci skip] --- Changelog.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Changelog.md b/Changelog.md index 9e0d380d67..6e01895a9c 100644 --- a/Changelog.md +++ b/Changelog.md @@ -34,6 +34,9 @@ Bug Fixes: Windows. (Myron Marston, #1887) * Fix `RSpec::Core::RakeTask#failure_message` so that it gets printed when the task failed. (Myron Marston, #1905) +* Fix how the DSL methods are defined so that RSpec is compatible with + gems that define methods of the same name on `Kernel` (such as + the `its-it` gem). (Alex Kwiatkowski, #1907) ### 3.2.2 / 2015-03-11 [Full Changelog](http://github.com/rspec/rspec-core/compare/v3.2.1...v3.2.2) From 8830f4a3fc6aaaaf04d4f9593e39bea73936974a Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Thu, 19 Mar 2015 08:12:19 -0700 Subject: [PATCH 106/258] Correct author list for #1907. [ci skip] --- Changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index 6e01895a9c..401ee438d9 100644 --- a/Changelog.md +++ b/Changelog.md @@ -36,7 +36,7 @@ Bug Fixes: when the task failed. (Myron Marston, #1905) * Fix how the DSL methods are defined so that RSpec is compatible with gems that define methods of the same name on `Kernel` (such as - the `its-it` gem). (Alex Kwiatkowski, #1907) + the `its-it` gem). (Alex Kwiatkowski, Ryan Ong, #1907) ### 3.2.2 / 2015-03-11 [Full Changelog](http://github.com/rspec/rspec-core/compare/v3.2.1...v3.2.2) From f0f5fffbd519011e50832f1578bdec42249b2c8f Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Thu, 19 Mar 2015 08:26:27 -0700 Subject: [PATCH 107/258] Use parens to clarify order of operations. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (This doesn’t actually change the order). --- lib/rspec/core/ordering.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rspec/core/ordering.rb b/lib/rspec/core/ordering.rb index b505bc26a3..f2284fb0d7 100644 --- a/lib/rspec/core/ordering.rb +++ b/lib/rspec/core/ordering.rb @@ -47,10 +47,10 @@ def jenkins_hash_digest(string) hash ^= hash >> 6 end - hash += (hash << 3 & MAX_32_BIT) + hash += ((hash << 3) & MAX_32_BIT) hash &= MAX_32_BIT hash ^= hash >> 11 - hash += (hash << 15 & MAX_32_BIT) + hash += ((hash << 15) & MAX_32_BIT) hash &= MAX_32_BIT hash end From 5dc5088cfbb220762b1bdfa27392a64271de139c Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Thu, 19 Mar 2015 23:46:42 -0700 Subject: [PATCH 108/258] Assign example group constant earlier. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This ensures it is set as soon as possible, before shared contexts included via metadata are evaluated, so that if there’s an error, the example group class name will be included in it. --- lib/rspec/core/example_group.rb | 2 +- spec/rspec/core/example_group_spec.rb | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/rspec/core/example_group.rb b/lib/rspec/core/example_group.rb index f3e55c7750..dbc4826892 100644 --- a/lib/rspec/core/example_group.rb +++ b/lib/rspec/core/example_group.rb @@ -361,7 +361,6 @@ def self.find_and_eval_shared(label, name, inclusion_location, *args, &customiza def self.subclass(parent, description, args, &example_group_block) subclass = Class.new(parent) subclass.set_it_up(description, *args, &example_group_block) - ExampleGroups.assign_const(subclass) subclass.module_exec(&example_group_block) if example_group_block # The LetDefinitions module must be included _after_ other modules @@ -392,6 +391,7 @@ def self.set_it_up(description, *args, &example_group_block) superclass.method(:next_runnable_index_for), description, *args, &example_group_block ) + ExampleGroups.assign_const(self) hooks.register_globals(self, RSpec.configuration.hooks) RSpec.configuration.configure_group(self) diff --git a/spec/rspec/core/example_group_spec.rb b/spec/rspec/core/example_group_spec.rb index 96a91c5521..78caa53f56 100644 --- a/spec/rspec/core/example_group_spec.rb +++ b/spec/rspec/core/example_group_spec.rb @@ -168,6 +168,16 @@ def metadata_hash(*args) }.to raise_error(/ExampleGroups::CallingAnUndefinedMethod/) end + it "assigns the const before including shared contexts via metadata so error messages from eval'ing the context include the name" do + RSpec.shared_context("foo", :foo) { bar } + + expect { + RSpec.describe("Including shared context via metadata", :foo) + }.to raise_error(NameError, + a_string_including('ExampleGroups::IncludingSharedContextViaMetadata', 'bar') + ) + end + it 'does not have problems with example groups named "Core"', :unless => RUBY_VERSION == '1.9.2' do RSpec.describe("Core") expect(defined?(::RSpec::ExampleGroups::Core)).to be_truthy From 359745122c147233aa33c4b6389a50df159ad398 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 20 Mar 2015 08:02:00 -0700 Subject: [PATCH 109/258] Add changelog entry for #1749. Somehow I forgot to add this before... [ci skip] --- Changelog.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Changelog.md b/Changelog.md index adc5a3383c..1f905a95e4 100644 --- a/Changelog.md +++ b/Changelog.md @@ -104,6 +104,13 @@ Enhancements: * Make `-I` option support multiple values when separated by `File::PATH_SEPARATOR`, such as `rspec -I foo:bar`. This matches the behavior of Ruby's `-I` option. (Fumiaki Matsushima, #1855). +* Treat each example as having a singleton example group for the + purposes of applying metadata-based features that normally apply + to example groups to individually tagged examples. For example, + `RSpec.shared_context "Uses redis", :uses_redis` will now apply + to individual examples tagged with `:uses_redis`, as will + `config.include RedisHelpers, :uses_redis`, and + `config.before(:context, :uses_redis) { }`, etc. (Myron Marston, #1749) Bug Fixes: From 69b5b2415ac2390285c84dc0158ccccbaf86ba6c Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 20 Mar 2015 00:28:52 -0700 Subject: [PATCH 110/258] Make `let` work properly when shared context is applied to a single example. --- Changelog.md | 2 ++ lib/rspec/core/configuration.rb | 9 +++++++-- spec/rspec/core/shared_context_spec.rb | 17 +++++++++++++++++ spec/rspec/core/shared_example_group_spec.rb | 16 ++++++++++++++++ 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/Changelog.md b/Changelog.md index 1f905a95e4..9de8d9dd1b 100644 --- a/Changelog.md +++ b/Changelog.md @@ -40,6 +40,8 @@ Bug Fixes: * Fix how the DSL methods are defined so that RSpec is compatible with gems that define methods of the same name on `Kernel` (such as the `its-it` gem). (Alex Kwiatkowski, Ryan Ong, #1907) +* Make `let` work properly when defined in a shared context that is applied + to an individual example via metadata. (Myron Marston, #1912) ### 3.2.2 / 2015-03-11 [Full Changelog](http://github.com/rspec/rspec-core/compare/v3.2.1...v3.2.2) diff --git a/lib/rspec/core/configuration.rb b/lib/rspec/core/configuration.rb index 283fe01d50..41a52c0077 100644 --- a/lib/rspec/core/configuration.rb +++ b/lib/rspec/core/configuration.rb @@ -1226,12 +1226,17 @@ def configure_group_with(group, module_list, application_method) # Used internally to extend the singleton class of a single example's # example group instance with modules using `include` and/or `extend`. def configure_example(example) + singleton_group = example.example_group_instance.singleton_class + # We replace the metadata so that SharedExampleGroupModule#included # has access to the example's metadata[:location]. - example.example_group_instance.singleton_class.with_replaced_metadata(example.metadata) do - @include_modules.items_for(example.metadata).each do |mod| + singleton_group.with_replaced_metadata(example.metadata) do + modules = @include_modules.items_for(example.metadata) + modules.each do |mod| safe_include(mod, example.example_group_instance.singleton_class) end + + MemoizedHelpers.define_helpers_on(singleton_group) unless modules.empty? end end diff --git a/spec/rspec/core/shared_context_spec.rb b/spec/rspec/core/shared_context_spec.rb index 2fb1cc9228..dfd11b7b80 100644 --- a/spec/rspec/core/shared_context_spec.rb +++ b/spec/rspec/core/shared_context_spec.rb @@ -66,6 +66,23 @@ expect(group.new.foo).to eq('foo') end + it "supports let when applied to an individual example via metadata" do + shared = Module.new do + extend RSpec::SharedContext + let(:foo) { "bar" } + end + + RSpec.configuration.include shared, :include_it + + ex = value = nil + RSpec.describe "group" do + ex = example("ex1", :include_it) { value = foo } + end.run + + expect(ex.execution_result).to have_attributes(:status => :passed, :exception => nil) + expect(value).to eq("bar") + end + it 'supports explicit subjects' do shared = Module.new do extend RSpec::SharedContext diff --git a/spec/rspec/core/shared_example_group_spec.rb b/spec/rspec/core/shared_example_group_spec.rb index 513932d9ad..39165199d7 100644 --- a/spec/rspec/core/shared_example_group_spec.rb +++ b/spec/rspec/core/shared_example_group_spec.rb @@ -134,6 +134,22 @@ module Core expect(non_matching_group).not_to respond_to(:bar) end + describe "when it has a `let` and applies to an individual example via metadata" do + it 'defines the `let` method correctly' do + define_shared_group("name", :include_it) do + let(:foo) { "bar" } + end + + ex = value = nil + RSpec.describe "group" do + ex = example("ex1", :include_it) { value = foo } + end.run + + expect(ex.execution_result).to have_attributes(:status => :passed, :exception => nil) + expect(value).to eq("bar") + end + end + describe "hooks for individual examples that have matching metadata" do before do skip "These specs pass in 2.0 mode on JRuby 1.7.8 but fail on " \ From b663028431bf14bd6ea2456957ee6e2ae25c3d62 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 20 Mar 2015 17:11:41 -0700 Subject: [PATCH 111/258] Revert "Warn users when overriding methods (via let, def or define_method) in the same example group." This reverts the lib and spec pieces of e072e5358b85a971010bc1a22d04eeca646321c8. The warning is overzealous. After upgrading a project to RSpec HEAD, I got spammed with tons of warnings of situations that weren't actually problematic. For example, it's common to define a `let` in a shared context that provides a default value, and than to purposefully override that in a host group where the shared context is included. We should'nt warn in such a situation, but this did warn. See #1903 for the original code this reverts. --- Changelog.md | 2 - lib/rspec/core/example_group.rb | 14 ------- spec/rspec/core/example_group_spec.rb | 53 --------------------------- 3 files changed, 69 deletions(-) diff --git a/Changelog.md b/Changelog.md index 1f905a95e4..88d053425c 100644 --- a/Changelog.md +++ b/Changelog.md @@ -24,8 +24,6 @@ Enhancements: * Add `--next-failure` CLI option which allows you to repeatedly focus on just one of the currently failing examples, then move on to the next failure, etc. (Myron Marston, #1888) -* Warn when a helper method definition stomps an earlier definition - in the same example group. (Fabio Napoleoni, #1903) * Make `--order random` ordering stable, so that when you rerun a subset with a given seed, the examples will be order consistently relative to each other. (Myron Marston, #1908) diff --git a/lib/rspec/core/example_group.rb b/lib/rspec/core/example_group.rb index f3e55c7750..67101a83a4 100644 --- a/lib/rspec/core/example_group.rb +++ b/lib/rspec/core/example_group.rb @@ -2,8 +2,6 @@ module RSpec module Core - # rubocop:disable Style/ClassLength - # ExampleGroup and {Example} are the main structural elements of # rspec-core. Consider this example: # @@ -658,16 +656,6 @@ def self.method_missing(name, *args) end private_class_method :method_missing - # @private - def self.method_added(method_name) - if (@__added_methods ||= Set.new).include?(method_name) - RSpec.warning "`#{self}##{method_name}` is being redefined " \ - "at #{RSpec::CallerFilter.first_non_rspec_line}. The original " \ - "definition will never be used and can be removed", :call_site => nil - end - @__added_methods << method_name - end - private def method_missing(name, *args) @@ -683,8 +671,6 @@ def method_missing(name, *args) end end - # rubocop:enable Style/ClassLength - # @private # Unnamed example group used by `SuiteHookContext`. class AnonymousExampleGroup < ExampleGroup diff --git a/spec/rspec/core/example_group_spec.rb b/spec/rspec/core/example_group_spec.rb index 96a91c5521..b6e3768404 100644 --- a/spec/rspec/core/example_group_spec.rb +++ b/spec/rspec/core/example_group_spec.rb @@ -1306,59 +1306,6 @@ def extract_execution_results(group) end - describe "when methods are redefined" do - - context "in the same example group" do - - it 'emits a warning when overriding methods using `let`' do - expect { - RSpec.describe do - let(:foo) { 'first value' } - let(:foo) { 'second value' } - end - }.to output(a_string_including("#foo", "is being redefined at #{__FILE__}:#{__LINE__ - 2}")).to_stderr - end - - it 'emits a warning when overriding methods using `def`' do - expect { - RSpec.describe do - let(:foo) { 'first value' } - def foo; 'second value'; end - end - }.to output(a_string_including("#foo", "is being redefined at #{__FILE__}:#{__LINE__ - 2}")).to_stderr - end - - it 'emits a warning when overriding methods using `define_method`' do - expect { - RSpec.describe do - let(:foo) { 'first value' } - define_method(:foo) { 'second value' } - end - }.to output(a_string_including("#foo", "is being redefined at #{__FILE__}:#{__LINE__ - 2}")).to_stderr - end - - end - - context "in nested example groups" do - - it 'does not emit warnings' do - expect { - RSpec.describe do - let(:foo) { 'first value' } - context 'in sub context' do - let(:foo) { 'second value' } - context 'in sub sub context' do - def foo; 'third value'; end - end - end - end - }.to avoid_outputting.to_stdout.and avoid_outputting.to_stderr - end - - end - - end - describe "ivars are not shared across examples" do it "(first example)" do @a = 1 From e747c7fc4a506d1cbe643ddf57ad626773b8396f Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 20 Mar 2015 17:32:00 -0700 Subject: [PATCH 112/258] Don't purge example groups when a `--fail-fast` failure is hit. Doing so prevents us from persisting updates to example statuses, interfering with `--next-failure` from working properly. The purging was introduced in 6946d2d but has actually done nothing since fde59555903644ba13df27519f494b4f874d60e8, when the runner switched from doing `@world.example_groups.map { |g| g.run }` to `order(@world.example_groups).map { |g| g.run }`. With the ordering applied, purging the example group no longer had the intended "abort the loop early" effect, since we were now mapping over a derived ordered array. The purging isn't really needed anyway; each example group aborts early if `wants_to_quit` is set, so we still don't run any more examples or groups. It might be nice to bring back the "abort the loop early" effect but I can't think of an elegant way to do it and it doesn't seem worth the complexity at this point. Fixes #1914. --- lib/rspec/core/example_group.rb | 10 +--------- lib/rspec/core/world.rb | 6 ------ spec/rspec/core/example_group_spec.rb | 18 ------------------ 3 files changed, 1 insertion(+), 33 deletions(-) diff --git a/lib/rspec/core/example_group.rb b/lib/rspec/core/example_group.rb index 74f39a7e08..00a11becb2 100644 --- a/lib/rspec/core/example_group.rb +++ b/lib/rspec/core/example_group.rb @@ -435,11 +435,6 @@ def self.parent_groups @parent_groups ||= ancestors.select { |a| a < RSpec::Core::ExampleGroup } end - # @private - def self.top_level? - @top_level ||= superclass == ExampleGroup - end - # @private def self.ensure_example_groups_are_configured unless defined?(@@example_groups_configured) @@ -511,10 +506,7 @@ def self.run_after_context_hooks(example_group_instance) # Runs all the examples in this group. def self.run(reporter=RSpec::Core::NullReporter) - if RSpec.world.wants_to_quit - RSpec.world.clear_remaining_example_groups if top_level? - return - end + return if RSpec.world.wants_to_quit reporter.example_group_started(self) should_run_context_hooks = descendant_filtered_examples.any? diff --git a/lib/rspec/core/world.rb b/lib/rspec/core/world.rb index fd6d7be858..bfd59192cb 100644 --- a/lib/rspec/core/world.rb +++ b/lib/rspec/core/world.rb @@ -24,12 +24,6 @@ def initialize(configuration=RSpec.configuration) end end - # @private - # Used internally to clear remaining groups when fail_fast is set. - def clear_remaining_example_groups - example_groups.clear - end - # @api private # # Apply ordering strategy from configuration to example groups. diff --git a/spec/rspec/core/example_group_spec.rb b/spec/rspec/core/example_group_spec.rb index 2250d161cb..5bbcad7296 100644 --- a/spec/rspec/core/example_group_spec.rb +++ b/spec/rspec/core/example_group_spec.rb @@ -1383,24 +1383,6 @@ def extract_execution_results(group) expect(reporter).not_to receive(:example_group_started) self.group.run(reporter) end - - context "at top level" do - it "purges remaining groups" do - expect { - self.group.run(reporter) - }.to change { RSpec.world.example_groups }.from([self.group]).to([]) - end - end - - context "in a nested group" do - it "does not purge remaining groups" do - nested_group = self.group.describe - - expect { - nested_group.run(reporter) - }.not_to change { RSpec.world.example_groups }.from([self.group]) - end - end end context "with all examples passing" do From bae781ad617b2fde287178e34509074616abb74b Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 20 Mar 2015 21:54:13 -0700 Subject: [PATCH 113/258] Don't stomp the order the user has already specified. Fixes #1910. --- lib/rspec/core/option_parser.rb | 8 ++------ spec/rspec/core/option_parser_spec.rb | 10 ++++++++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/rspec/core/option_parser.rb b/lib/rspec/core/option_parser.rb index f76a248ca5..6cec8e7af4 100644 --- a/lib/rspec/core/option_parser.rb +++ b/lib/rspec/core/option_parser.rb @@ -47,7 +47,7 @@ def parser(options) ' [rand] randomize the order of groups and examples', ' [random] alias for rand', ' [random:SEED] e.g. --order random:123') do |o| - set_order(options, o) + options[:order] = o end parser.on('--seed SEED', Integer, 'Equivalent of --order rand:SEED.') do |seed| @@ -162,7 +162,7 @@ def parser(options) " (Equivalent to `--only-failures --fail-fast --order defined`)") do configure_only_failures(options) set_fail_fast(options, true) - set_order(options, "defined") + options[:order] ||= 'defined' end parser.on('-P', '--pattern PATTERN', 'Load files matching pattern (default: "spec/**/*_spec.rb").') do |o| @@ -249,10 +249,6 @@ def set_fail_fast(options, value) options[:fail_fast] = value end - def set_order(options, value) - options[:order] = value - end - def configure_only_failures(options) options[:only_failures] = true add_tag_filter(options, :inclusion_filter, :last_run_status, 'failed') diff --git a/spec/rspec/core/option_parser_spec.rb b/spec/rspec/core/option_parser_spec.rb index 4544e1e396..27ed1351ed 100644 --- a/spec/rspec/core/option_parser_spec.rb +++ b/spec/rspec/core/option_parser_spec.rb @@ -168,6 +168,16 @@ def generate_help_text expect(next_failure).to include(long_form) end + + it 'does not force `--order defined` over a specified `--seed 1234` option that comes before it' do + options = Parser.parse(%w[ --seed 1234 --next-failure ]) + expect(options).to include(:order => "rand:1234") + end + + it 'does not force `--order defined` over a specified `--seed 1234` option that comes after it' do + options = Parser.parse(%w[ --next-failure --seed 1234 ]) + expect(options).to include(:order => "rand:1234") + end end %w[--example -e].each do |option| From e93a627757c375c8abb29c2ac232c1f42c2fc147 Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Sun, 22 Mar 2015 11:07:07 +1100 Subject: [PATCH 114/258] changelog for #1911 [skip ci] --- Changelog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Changelog.md b/Changelog.md index e041aed91c..140746c032 100644 --- a/Changelog.md +++ b/Changelog.md @@ -27,6 +27,8 @@ Enhancements: * Make `--order random` ordering stable, so that when you rerun a subset with a given seed, the examples will be order consistently relative to each other. (Myron Marston, #1908) +* Set example group constant earlier so errors when evaluating the context + include the example group name (Myron Marson, #1911) Bug Fixes: From ffe00a1d4e369e312881e6b2c091c8b6fb7e6087 Mon Sep 17 00:00:00 2001 From: Josh Cheek Date: Sun, 1 Feb 2015 14:53:46 -0700 Subject: [PATCH 115/258] Make memoized helpers threadsafe See https://github.com/rspec/rspec-core/pull/1858 for discussion. The unsquashed version can be seen at https://github.com/JoshCheek/rspec-core/tree/threadsafe-let-block-unsquashed in case the intermediate state and thoughts have value. Motivation ---------- When working in a truly threaded environment (e.g. Rbx and JRuby), you can write a test like the one below, which should pass. But, it will fail sometimes, because two threads request the uninitialized counter concurrently. The first one to receive the value will then be incrementing a counter, that is later overwritten when the second thread's let block returns. You can verify this by running the code sample below in Rubinius. After some number of attempts, the value will be less than 10k. If you then make the `let` block a `let!` block, it will memoize before the threads are created, and the values will be correct again. While this is a reasonable solution, it is incredibly confusing when it arises (I spent a lot of hours trying to find the bug in my code), and requires that the user understand it's something they need to be aware of and guard against. ```ruby class Counter def initialize @mutex = Mutex.new @count = 0 end attr_reader :count def increment @mutex.synchronize { @count += 1 } end end RSpec.describe Counter do let(:counter) { Counter.new } it 'increments the count in a threadsafe manner' do threads = 10.times.map do Thread.new { 1000.times { counter.increment } } end threads.each &:join expect(counter.count).to eq 10_000 end end ``` Relevant Changes ---------------- * Adds `--[no]-threadsafe` command-line option * `RSpec::Core::Configuration#threadsafe{?,=}` (notation there is from bash expansion) setting, defaults to `true` * Mamoized is a threadsafe object instead of a hash, so rename `RSpec::Core::MemoizedHelpers::ContextHookMemoizedHash` to `RSpec::Core::MemoizedHelpers::ContextHookMemoized` Create an object because only #fetch was being called on the hash, and to make that threadsafe required adding a new method. At that point, there is no value in having it subclass Hash, having to support an entire API that nothing else uses. So just make it an object which wraps a hash and presents a memoized getter/setter. Also named the method in such a way as to make it very clear that it's not a hash, so a dev won't mistakenly think they are working with one when they aren't. * Adds private class `RSpec::Core::ReentrantMutex`, which is basically just `Monitor` from the stdlib. * `RSpec::Core::ExampleGroup#initialize` now calls super so that `RSpec::Core::MemoizedHelpers` can initialize the memoized helper when the example is instantiated, rather than when it is accessed, as this is not threadsafe. Context and daydreams --------------------- * PR can be seen unsquashed at: https://github.com/JoshCheek/rspec-core/tree/threadsafe-let-block-unsquashed This is my first "real" squash, and it wasn't super smooth. I think I got it right, in the end, but if you're looking at it going "wtf?", and want some more context into the squash itself, I documented it at: https://gist.github.com/JoshCheek/017c399641a44286428b Also, while I'm pretty sold against anything that changes history, there's probably a way to do this that's better than what I did, so if you read that and have insights, shoot em my wya, I'd love to get better! * This does not add any stedlib dependencies as they affect the test environment of all users. * The threads need to be reentrant, which will allow a let block in a thread to access its parent let block. This can be achieved with Monitor from the stdlib. However, to avoid the dependency, the code was copied from https://github.com/ruby/ruby/blob/eb7ddaa3a47bf48045d26c72eb0f263a53524ebc/lib/monitor.rb#L9 and pasted/edited into `lib/rspec/core/reentrant_mutex.rb` This way the user's test environment is preserved, but we still get reentrant mutexes. * Reentrant mutexes are built on top of normal mutexes. These are in core now, but for 1.8.7, were defined in the stdlib's thread.rb So, similarly, copy that code into `RSpec::Core::ReentrantMutex::MUTEX` (capitalization due to Rubocop rules), with a note stating that it should be deleted once 1.8 support is dropped. If there is a Mutex available already, though, as on 1.9.x+, it will use that. * Adds a development dependency on a gem, thread_order, which I extracted out of this work. Its purpose was to ease the difficulty and opacity of specifying what should happen and when, with regards to getting the threads into situations to illustrate expected behaviour. It similarly works on 1.8.7 - 2.2, and on JRuby and Rbx, without dependencies on the stdlib. * Add benchmark for threadsafe let block, here is a summary: ``` MRI 2.2 1 call to let -- each sets the value non-threadsafe (original): 830988.5 i/s non-threadsafe (config) : 665661.9 i/s - 1.25x slower threadsafe : 323574.9 i/s - 2.57x slower 10 calls to let -- 9 will find memoized value non-threadsafe (original): 346302.0 i/s non-threadsafe (config) : 309970.2 i/s - 1.12x slower threadsafe : 208946.3 i/s - 1.66x slower 1 call to let which invokes super non-threadsafe (original): 591906.3 i/s non-threadsafe (config) : 511295.0 i/s - 1.16x slower threadsafe : 246079.6 i/s - 2.41x slower 10 calls to let which invokes super non-threadsafe (original): 297422.6 i/s non-threadsafe (config) : 264045.8 i/s - 1.13x slower threadsafe : 170853.1 i/s - 1.74x slower ``` The threasafe let is 1.5 to 2.5 times slower than the original hash implementation. The hash alternative is 1.1 to 1.25 times slower. If this matters for you, you can configure which one you want to use. Either way, you can call the method defined by a let block hundreds of thousands of times a second. --- benchmarks/threadsafe_let_block.rb | 312 ++++++++++++++++++ features/helper_methods/let.feature | 39 +++ lib/rspec/core/configuration.rb | 6 + lib/rspec/core/example_group.rb | 5 +- lib/rspec/core/memoized_helpers.rb | 92 +++++- lib/rspec/core/option_parser.rb | 4 + lib/rspec/core/reentrant_mutex.rb | 107 ++++++ rspec-core.gemspec | 7 +- spec/rspec/core/configuration_options_spec.rb | 12 + spec/rspec/core/configuration_spec.rb | 14 + spec/rspec/core/memoized_helpers_spec.rb | 116 +++++++ spec/rspec/core/reentrant_mutex_spec.rb | 30 ++ 12 files changed, 722 insertions(+), 22 deletions(-) create mode 100644 benchmarks/threadsafe_let_block.rb create mode 100644 lib/rspec/core/reentrant_mutex.rb create mode 100644 spec/rspec/core/reentrant_mutex_spec.rb diff --git a/benchmarks/threadsafe_let_block.rb b/benchmarks/threadsafe_let_block.rb new file mode 100644 index 0000000000..d0e10b4bc6 --- /dev/null +++ b/benchmarks/threadsafe_let_block.rb @@ -0,0 +1,312 @@ +require 'rspec/core' +require 'rspec/expectations' + +# switches between these implementations - https://github.com/rspec/rspec-core/pull/1858/files +# benchmark requested in this PR - https://github.com/rspec/rspec-core/pull/1858 +# +# I ran these from lib root by adding "gem 'benchmark-ips'" to ../Gemfile-custom +# then ran `bundle install --standalone --binstubs bundle/bin` +# then ran `ruby --disable-gems -I lib -I "$PWD/bundle" -r bundler/setup -S benchmarks/threadsafe_let_block.rb` + +# The old, non-thread safe implementation, imported from the `master` branch and pared down. +module OriginalNonThreadSafeMemoizedHelpers + def __memoized + @__memoized ||= {} + end + + module ClassMethods + def let(name, &block) + # We have to pass the block directly to `define_method` to + # allow it to use method constructs like `super` and `return`. + raise "#let or #subject called without a block" if block.nil? + OriginalNonThreadSafeMemoizedHelpers.module_for(self).__send__(:define_method, name, &block) + + # Apply the memoization. The method has been defined in an ancestor + # module so we can use `super` here to get the value. + if block.arity == 1 + define_method(name) { __memoized.fetch(name) { |k| __memoized[k] = super(RSpec.current_example, &nil) } } + else + define_method(name) { __memoized.fetch(name) { |k| __memoized[k] = super(&nil) } } + end + end + end + + def self.module_for(example_group) + get_constant_or_yield(example_group, :LetDefinitions) do + mod = Module.new do + include Module.new { + example_group.const_set(:NamedSubjectPreventSuper, self) + } + end + + example_group.const_set(:LetDefinitions, mod) + mod + end + end + + # @private + def self.define_helpers_on(example_group) + example_group.__send__(:include, module_for(example_group)) + end + + def self.get_constant_or_yield(example_group, name) + if example_group.const_defined?(name, (check_ancestors = false)) + example_group.const_get(name, check_ancestors) + else + yield + end + end +end + +class HostBase + # wires the implementation + # adds `let(:name) { nil }` + # returns `Class.new(self) { let(:name) { super() } }` + def self.prepare_using(memoized_helpers, options={}) + include memoized_helpers + extend memoized_helpers::ClassMethods + memoized_helpers.define_helpers_on(self) + + define_method(:initialize, &options[:initialize]) if options[:initialize] + let(:name) { nil } + + verify_memoizes memoized_helpers, options[:verify] + + Class.new(self) do + memoized_helpers.define_helpers_on(self) + let(:name) { super() } + end + end + + def self.verify_memoizes(memoized_helpers, additional_verification) + # Since we're using custom code, ensure it actually memoizes as we expect... + counter_class = Class.new(self) do + include RSpec::Matchers + memoized_helpers.define_helpers_on(self) + counter = 0 + let(:count) { counter += 1 } + end + extend RSpec::Matchers + + instance_1 = counter_class.new + expect(instance_1.count).to eq(1) + expect(instance_1.count).to eq(1) + + instance_2 = counter_class.new + expect(instance_2.count).to eq(2) + expect(instance_2.count).to eq(2) + + instance_3 = counter_class.new + instance_3.instance_eval &additional_verification if additional_verification + end +end + +class OriginalNonThreadSafeHost < HostBase + Subclass = prepare_using OriginalNonThreadSafeMemoizedHelpers +end + +class ThreadSafeHost < HostBase + Subclass = prepare_using RSpec::Core::MemoizedHelpers, + :initialize => lambda { |*| @__memoized = ThreadsafeMemoized.new }, + :verify => lambda { |*| expect(__memoized).to be_a_kind_of RSpec::Core::MemoizedHelpers::ThreadsafeMemoized } +end + +class ConfigNonThreadSafeHost < HostBase + Subclass = prepare_using RSpec::Core::MemoizedHelpers, + :initialize => lambda { |*| @__memoized = NonThreadSafeMemoized.new }, + :verify => lambda { |*| expect(__memoized).to be_a_kind_of RSpec::Core::MemoizedHelpers::NonThreadSafeMemoized } +end + +def title(title) + hr = "#" * (title.length + 6) + blank = "# #{' ' * title.length} #" + [hr, blank, "# #{title} #", blank, hr] +end + +require 'benchmark/ips' + +puts title "versions" +puts "RUBY_VERSION #{RUBY_VERSION}" +puts "RUBY_PLATFORM #{RUBY_PLATFORM}" +puts "RUBY_ENGINE #{RUBY_ENGINE}" +puts "ruby -v #{`ruby -v`}" +puts "Benchmark::IPS::VERSION #{Benchmark::IPS::VERSION}" +puts "rspec-core SHA #{`git log --pretty=format:%H -1`}" +puts + +puts title "1 call to let -- each sets the value" +Benchmark.ips do |x| + x.report("non-threadsafe (original)") { OriginalNonThreadSafeHost.new.name } + x.report("non-threadsafe (config) ") { ConfigNonThreadSafeHost.new.name } + x.report("threadsafe ") { ThreadSafeHost.new.name } + x.compare! +end + +puts title "10 calls to let -- 9 will find memoized value" +Benchmark.ips do |x| + x.report("non-threadsafe (original)") do + i = OriginalNonThreadSafeHost.new + i.name; i.name; i.name; i.name; i.name + i.name; i.name; i.name; i.name; i.name + end + + x.report("non-threadsafe (config) ") do + i = ConfigNonThreadSafeHost.new + i.name; i.name; i.name; i.name; i.name + i.name; i.name; i.name; i.name; i.name + end + + x.report("threadsafe ") do + i = ThreadSafeHost.new + i.name; i.name; i.name; i.name; i.name + i.name; i.name; i.name; i.name; i.name + end + + x.compare! +end + +puts title "1 call to let which invokes super" + +Benchmark.ips do |x| + x.report("non-threadsafe (original)") { OriginalNonThreadSafeHost::Subclass.new.name } + x.report("non-threadsafe (config) ") { ConfigNonThreadSafeHost::Subclass.new.name } + x.report("threadsafe ") { ThreadSafeHost::Subclass.new.name } + x.compare! +end + +puts title "10 calls to let which invokes super" +Benchmark.ips do |x| + x.report("non-threadsafe (original)") do + i = OriginalNonThreadSafeHost::Subclass.new + i.name; i.name; i.name; i.name; i.name + i.name; i.name; i.name; i.name; i.name + end + + x.report("non-threadsafe (config) ") do + i = ConfigNonThreadSafeHost::Subclass.new + i.name; i.name; i.name; i.name; i.name + i.name; i.name; i.name; i.name; i.name + end + + x.report("threadsafe ") do + i = ThreadSafeHost::Subclass.new + i.name; i.name; i.name; i.name; i.name + i.name; i.name; i.name; i.name; i.name + end + + x.compare! +end + +__END__ + +############## +# # +# versions # +# # +############## +RUBY_VERSION 2.2.0 +RUBY_PLATFORM x86_64-darwin13 +RUBY_ENGINE ruby +ruby -v ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin13] +Benchmark::IPS::VERSION 2.1.1 +rspec-core SHA 1ee7a8d8cde6ba2dd13d35e90e824e8e5ba7db76 + +########################################## +# # +# 1 call to let -- each sets the value # +# # +########################################## +Calculating ------------------------------------- +non-threadsafe (original) + 53.722k i/100ms +non-threadsafe (config) + 44.998k i/100ms +threadsafe + 26.123k i/100ms +------------------------------------------------- +non-threadsafe (original) + 830.988k (± 6.3%) i/s - 4.190M +non-threadsafe (config) + 665.662k (± 6.7%) i/s - 3.330M +threadsafe + 323.575k (± 5.6%) i/s - 1.620M + +Comparison: +non-threadsafe (original): 830988.5 i/s +non-threadsafe (config) : 665661.9 i/s - 1.25x slower +threadsafe : 323574.9 i/s - 2.57x slower + +################################################### +# # +# 10 calls to let -- 9 will find memoized value # +# # +################################################### +Calculating ------------------------------------- +non-threadsafe (original) + 28.724k i/100ms +non-threadsafe (config) + 25.357k i/100ms +threadsafe + 18.349k i/100ms +------------------------------------------------- +non-threadsafe (original) + 346.302k (± 6.1%) i/s - 1.752M +non-threadsafe (config) + 309.970k (± 5.4%) i/s - 1.547M +threadsafe + 208.946k (± 5.2%) i/s - 1.046M + +Comparison: +non-threadsafe (original): 346302.0 i/s +non-threadsafe (config) : 309970.2 i/s - 1.12x slower +threadsafe : 208946.3 i/s - 1.66x slower + +####################################### +# # +# 1 call to let which invokes super # +# # +####################################### +Calculating ------------------------------------- +non-threadsafe (original) + 42.458k i/100ms +non-threadsafe (config) + 37.367k i/100ms +threadsafe + 21.088k i/100ms +------------------------------------------------- +non-threadsafe (original) + 591.906k (± 6.3%) i/s - 2.972M +non-threadsafe (config) + 511.295k (± 4.7%) i/s - 2.578M +threadsafe + 246.080k (± 5.8%) i/s - 1.244M + +Comparison: +non-threadsafe (original): 591906.3 i/s +non-threadsafe (config) : 511295.0 i/s - 1.16x slower +threadsafe : 246079.6 i/s - 2.41x slower + +######################################### +# # +# 10 calls to let which invokes super # +# # +######################################### +Calculating ------------------------------------- +non-threadsafe (original) + 24.282k i/100ms +non-threadsafe (config) + 22.762k i/100ms +threadsafe + 14.685k i/100ms +------------------------------------------------- +non-threadsafe (original) + 297.423k (± 5.0%) i/s - 1.505M +non-threadsafe (config) + 264.046k (± 5.6%) i/s - 1.320M +threadsafe + 170.853k (± 4.7%) i/s - 866.415k + +Comparison: +non-threadsafe (original): 297422.6 i/s +non-threadsafe (config) : 264045.8 i/s - 1.13x slower +threadsafe : 170853.1 i/s - 1.74x slower diff --git a/features/helper_methods/let.feature b/features/helper_methods/let.feature index 0417bd0e2f..d4b8422098 100644 --- a/features/helper_methods/let.feature +++ b/features/helper_methods/let.feature @@ -48,3 +48,42 @@ Feature: let and let! """ When I run `rspec let_bang_spec.rb` Then the examples should all pass + + Scenario: Use --threadsafe to set `RSpec.configuration.threadsafe` (defaults to true) + Given a file named "let_threadsafe.rb" with: + """ruby + require 'thread' + accesses = Queue.new + turns = Queue.new + + RSpec.describe "threadsafe let" do + let :resource do + turns.shift + accesses << :from_let + end + + it "will only ever access the let block once" do + first_access = Thread.new { resource } + second_access = Thread.new { resource } + loop do + Thread.pass + break if first_access.stop? && second_access.stop? + end + turns << nil + turns << nil + first_access.join + second_access.join + accesses << :from_example + expect(accesses.shift).to eq :from_let + expect(accesses.shift).to eq :from_example + end + end + """ + When I run `rspec let_threadsafe.rb --threadsafe` + Then the examples should all pass + + When I run `rspec let_threadsafe.rb --no-threadsafe` + Then the output should contain "1 example, 1 failure" + + When I run `rspec let_threadsafe.rb` + Then the examples should all pass diff --git a/lib/rspec/core/configuration.rb b/lib/rspec/core/configuration.rb index 41a52c0077..58e37410d4 100644 --- a/lib/rspec/core/configuration.rb +++ b/lib/rspec/core/configuration.rb @@ -308,6 +308,11 @@ def treat_symbols_as_metadata_keys_with_true_values=(_value) # Record the start time of the spec suite to measure load time. add_setting :start_time + # @macro add_setting + # Use threadsafe options where available. + # Currently this will place a mutex around memoized values such as let blocks. + add_setting :threadsafe + # @private add_setting :tty # @private @@ -361,6 +366,7 @@ def initialize @requires = [] @libs = [] @derived_metadata_blocks = FilterableItemRepository::QueryOptimized.new(:any?) + @threadsafe = true end # @private diff --git a/lib/rspec/core/example_group.rb b/lib/rspec/core/example_group.rb index 00a11becb2..fdce80ef3f 100644 --- a/lib/rspec/core/example_group.rb +++ b/lib/rspec/core/example_group.rb @@ -462,7 +462,7 @@ def self.store_before_context_ivars(example_group_instance) def self.run_before_context_hooks(example_group_instance) set_ivars(example_group_instance, superclass_before_context_ivars) - ContextHookMemoizedHash::Before.isolate_for_context_hook(example_group_instance) do + ContextHookMemoized::Before.isolate_for_context_hook(example_group_instance) do hooks.run(:before, :context, example_group_instance) end ensure @@ -497,7 +497,7 @@ def self.superclass_before_context_ivars def self.run_after_context_hooks(example_group_instance) set_ivars(example_group_instance, before_context_ivars) - ContextHookMemoizedHash::After.isolate_for_context_hook(example_group_instance) do + ContextHookMemoized::After.isolate_for_context_hook(example_group_instance) do hooks.run(:after, :context, example_group_instance) end ensure @@ -613,6 +613,7 @@ def self.each_instance_variable_for_example(group) def initialize(inspect_output=nil) @__inspect_output = inspect_output || '(no description provided)' + super() # no args get passed end # @private diff --git a/lib/rspec/core/memoized_helpers.rb b/lib/rspec/core/memoized_helpers.rb index 857ce90af6..f7d904f121 100644 --- a/lib/rspec/core/memoized_helpers.rb +++ b/lib/rspec/core/memoized_helpers.rb @@ -1,3 +1,5 @@ +require 'rspec/core/reentrant_mutex' + module RSpec module Core # This module is included in {ExampleGroup}, making the methods @@ -53,11 +55,9 @@ module MemoizedHelpers # @see #should_not # @see #is_expected def subject - __memoized.fetch(:subject) do - __memoized[:subject] = begin - described = described_class || self.class.metadata.fetch(:description_args).first - Class === described ? described.new : described - end + __memoized.fetch_or_store(:subject) do + described = described_class || self.class.metadata.fetch(:description_args).first + Class === described ? described.new : described end end @@ -119,33 +119,85 @@ def is_expected expect(subject) end + # @private + # should just be placed in private section, + # but Ruby issues warnings on private attributes. + # and expanding it to the equivalent method upsets Rubocop, + # b/c it should obviously be a reader + attr_reader :__memoized + private :__memoized + private # @private - def __memoized - @__memoized ||= {} + def initialize(*) + __init_memoized + super + end + + # @private + def __init_memoized + @__memoized = if RSpec.configuration.threadsafe? + ThreadsafeMemoized.new + else + NonThreadSafeMemoized.new + end + end + + # @private + class ThreadsafeMemoized + def initialize + @memoized = {} + @mutex = ReentrantMutex.new + end + + def fetch_or_store(key) + @memoized.fetch(key) do # only first access pays for synchronization + @mutex.synchronize do + @memoized.fetch(key) { @memoized[key] = yield } + end + end + end + end + + # @private + class NonThreadSafeMemoized + def initialize + @memoized = {} + end + + def fetch_or_store(key) + @memoized.fetch(key) { @memoized[key] = yield } + end end # Used internally to customize the behavior of the # memoized hash when used in a `before(:context)` hook. # # @private - class ContextHookMemoizedHash + class ContextHookMemoized def self.isolate_for_context_hook(example_group_instance) - hash = self + exploding_memoized = self example_group_instance.instance_exec do - @__memoized = hash + @__memoized = exploding_memoized begin yield ensure - @__memoized = nil + # This is doing a reset instead of just isolating for context hook. + # Really, this should set the old @__memoized back into place. + # + # Caller is the before and after context hooks + # which are both called from self.run + # I didn't look at why it made tests fail, maybe an object was getting reused in RSpec tests, + # if so, then that probably already works, and its the tests that are wrong. + __init_memoized end end end - def self.fetch(key, &_block) + def self.fetch_or_store(key, &_block) description = if key == :subject "subject" else @@ -206,9 +258,10 @@ module ClassMethods # maybe 3 declarations) in any given example group, but that can # quickly degrade with overuse. YMMV. # - # @note `let` uses an `||=` conditional that has the potential to - # behave in surprising ways in examples that spawn separate threads, - # though we have yet to see this in practice. You've been warned. + # @note `let` can be configured to be threadsafe or not. + # If it is threadsafe, it will take longer to access the value. + # If it is not threadsafe, it may behave in surprising ways in examples + # that spawn separate threads. Specify this on `RSpec.configure` # # @note Because `let` is designed to create state that is reset between # each example, and `before(:context)` is designed to setup state that @@ -237,9 +290,9 @@ def let(name, &block) # Apply the memoization. The method has been defined in an ancestor # module so we can use `super` here to get the value. if block.arity == 1 - define_method(name) { __memoized.fetch(name) { |k| __memoized[k] = super(RSpec.current_example, &nil) } } + define_method(name) { __memoized.fetch_or_store(name) { super(RSpec.current_example, &nil) } } else - define_method(name) { __memoized.fetch(name) { |k| __memoized[k] = super(&nil) } } + define_method(name) { __memoized.fetch_or_store(name) { super(&nil) } } end end @@ -312,6 +365,11 @@ def let!(name, &block) # # When given a `name`, calling `super` in the block is not supported. # + # @note `subject` can be configured to be threadsafe or not. + # If it is threadsafe, it will take longer to access the value. + # If it is not threadsafe, it may behave in surprising ways in examples + # that spawn separate threads. Specify this on `RSpec.configure` + # # @param name [String,Symbol] used to define an accessor with an # intention revealing name # @param block defines the value to be returned by `subject` in examples diff --git a/lib/rspec/core/option_parser.rb b/lib/rspec/core/option_parser.rb index 6cec8e7af4..6606637741 100644 --- a/lib/rspec/core/option_parser.rb +++ b/lib/rspec/core/option_parser.rb @@ -115,6 +115,10 @@ def parser(options) options[:color] = o end + parser.on('--[no-]threadsafe', 'Turn on threadsafety where available') do |o| + options[:threadsafe] = o + end + parser.on('-p', '--[no-]profile [COUNT]', 'Enable profiling of examples and list the slowest examples (default: 10).') do |argument| options[:profile_examples] = if argument.nil? diff --git a/lib/rspec/core/reentrant_mutex.rb b/lib/rspec/core/reentrant_mutex.rb new file mode 100644 index 0000000000..352de4e669 --- /dev/null +++ b/lib/rspec/core/reentrant_mutex.rb @@ -0,0 +1,107 @@ +module RSpec + module Core + # Allows a thread to lock out other threads from a critical section of code, + # while allowing the thread with the lock to reenter that section. + # + # Based on Monitor as of 2.2 - https://github.com/ruby/ruby/blob/eb7ddaa3a47bf48045d26c72eb0f263a53524ebc/lib/monitor.rb#L9 + # + # Depends on Mutex, but Mutex is only available as part of core since 1.9.1: + # exists - http://ruby-doc.org/core-1.9.1/Mutex.html + # dne - http://ruby-doc.org/core-1.9.0/Mutex.html + # + # @private + class ReentrantMutex + def initialize + @owner = nil + @count = 0 + @mutex = MUTEX.new + end + + def synchronize + enter + yield + ensure + exit + end + + private + + def enter + @mutex.lock if @owner != Thread.current + @owner = Thread.current + @count += 1 + end + + def exit + @count -= 1 + return unless @count == 0 + @owner = nil + @mutex.unlock + end + end + + # @private + # :nocov: + # This can be deleted once support for 1.8.7 is dropped + MUTEX = if defined? ::Mutex + # On 1.9 and up, this is in core, so we just use the real one + ::Mutex + else + # On 1.8.7, it's in the stdlib. + # We don't want to load the stdlib, b/c this is a test tool, and can affect the test environment, + # causing tests to pass where they should fail. + # + # So we're transcribing/modifying it from https://github.com/ruby/ruby/blob/v1_8_7_374/lib/thread.rb#L56 + # Some methods we don't need are deleted. + # Anything I don't understand (there's quite a bit, actually) is left in. + # Some formating changes are made to appease the robot overlord: + # https://travis-ci.org/rspec/rspec-core/jobs/54410874 + Class.new do + def initialize + @waiting = [] + @locked = false + @waiting.taint + taint + end + + def lock + while Thread.critical = true && @locked + @waiting.push Thread.current + Thread.stop + end + @locked = true + Thread.critical = false + self + end + + def unlock + return unless @locked + Thread.critical = true + @locked = false + begin + t = @waiting.shift + t.wakeup if t + rescue ThreadError + retry + end + Thread.critical = false + begin + t.run if t + rescue ThreadError + :noop + end + self + end + + def synchronize + lock + begin + yield + ensure + unlock + end + end + end + end + end +end diff --git a/rspec-core.gemspec b/rspec-core.gemspec index 301dba5b3f..98ff30bf8d 100644 --- a/rspec-core.gemspec +++ b/rspec-core.gemspec @@ -47,7 +47,8 @@ Gem::Specification.new do |s| s.add_development_dependency "nokogiri", (RUBY_VERSION < '1.9.3' ? "1.5.2" : "~> 1.5") s.add_development_dependency "coderay", "~> 1.0.9" - s.add_development_dependency "mocha", "~> 0.13.0" - s.add_development_dependency "rr", "~> 1.0.4" - s.add_development_dependency "flexmock", "~> 0.9.0" + s.add_development_dependency "mocha", "~> 0.13.0" + s.add_development_dependency "rr", "~> 1.0.4" + s.add_development_dependency "flexmock", "~> 0.9.0" + s.add_development_dependency "thread_order", "~> 1.1.0" end diff --git a/spec/rspec/core/configuration_options_spec.rb b/spec/rspec/core/configuration_options_spec.rb index f4887ff23e..7b98abed9c 100644 --- a/spec/rspec/core/configuration_options_spec.rb +++ b/spec/rspec/core/configuration_options_spec.rb @@ -211,6 +211,18 @@ end end + describe '--threadsafe', :threadsafe => true do + it 'sets :threadsafe => true' do + expect(parse_options('--threadsafe')).to include(:threadsafe => true) + end + end + + describe '--no-threadsafe', :threadsafe => true do + it 'sets :threadsafe => false' do + expect(parse_options('--no-threadsafe')).to include(:threadsafe => false) + end + end + describe "-I" do example "adds to :libs" do expect(parse_options('-I', 'a_dir')).to include(:libs => ['a_dir']) diff --git a/spec/rspec/core/configuration_spec.rb b/spec/rspec/core/configuration_spec.rb index aae6a600a3..306725d13c 100644 --- a/spec/rspec/core/configuration_spec.rb +++ b/spec/rspec/core/configuration_spec.rb @@ -2093,6 +2093,20 @@ def emulate_not_configured_expectation_framework include_examples "warning of deprecated `:example_group` during filtering configuration", :before, :each end + describe '#threadsafe', :threadsafe => true do + it 'defaults to false' do + expect(config.threadsafe).to eq true + end + + it 'can be configured to true or false' do + config.threadsafe = true + expect(config.threadsafe).to eq true + + config.threadsafe = false + expect(config.threadsafe).to eq false + end + end + # assigns files_or_directories_to_run and triggers post-processing # via `files_to_run`. def assign_files_or_directories_to_run(*value) diff --git a/spec/rspec/core/memoized_helpers_spec.rb b/spec/rspec/core/memoized_helpers_spec.rb index 7c9ad11105..df0d3086cb 100644 --- a/spec/rspec/core/memoized_helpers_spec.rb +++ b/spec/rspec/core/memoized_helpers_spec.rb @@ -1,3 +1,5 @@ +require 'thread_order' + module RSpec::Core RSpec.describe MemoizedHelpers do before(:each) { RSpec.configuration.configure_expectation_framework } @@ -363,6 +365,120 @@ def not_ok?; false; end expect(subject).to eq(3) end end + + describe 'threadsafety', :threadsafe => true do + before(:all) { eq 1 } # explanation: https://github.com/rspec/rspec-core/pull/1858/files#r25411166 + + class RaiseOnFailuresReporter < RSpec::Core::NullReporter + def self.example_failed(example) + raise example.exception + end + end + + def describe_successfully(&describe_body) + example_group = RSpec.describe(&describe_body) + ran_successfully = example_group.run RaiseOnFailuresReporter + expect(ran_successfully).to eq true + end + + + context 'when not threadsafe' do + # would be nice to not set this on the global + before { RSpec.configuration.threadsafe = false } + + it 'can wind up overwriting the previous memoized value (but if you don\'t need threadsafety, this is faster)' do + describe_successfully do + let!(:order) { ThreadOrder.new } + after { order.apocalypse! :join } + + let :memoized_value do + if order.current == :second + :second_access + else + order.pass_to :second, :resume_on => :exit + :first_access + end + end + + example do + order.declare(:second) { expect(memoized_value).to eq :second_access } + expect(memoized_value).to eq :first_access + end + end + end + end + + context 'when threadsafe' do + before(:context) { RSpec.configuration.threadsafe = true } + specify 'first thread to access determines the return value' do + describe_successfully do + let!(:order) { ThreadOrder.new } + after { order.apocalypse! :join } + + let :memoized_value do + if order.current == :second + :second_access + else + order.pass_to :second, :resume_on => :sleep + :first_access + end + end + + example do + order.declare(:second) { expect(memoized_value).to eq :first_access } + expect(memoized_value).to eq :first_access + end + end + end + + specify 'memoized block will only be evaluated once' do + describe_successfully do + let!(:order) { ThreadOrder.new } + after { order.apocalypse! } + before { @previously_accessed = false } + + let :memoized_value do + raise 'Called multiple times!' if @previously_accessed + @previously_accessed = true + order.pass_to :second, :resume_on => :sleep + end + + example do + order.declare(:second) { memoized_value } + memoized_value + order.join_all + end + end + end + + specify 'memoized blocks prevent other threads from accessing, even when it is accesssed in a superclass' do + describe_successfully do + let!(:order) { ThreadOrder.new } + after { order.apocalypse! :join } + + let!(:calls) { {:parent => 0, :child => 0} } + let(:memoized_value) do + calls[:parent] += 1 + order.pass_to :second, :resume_on => :sleep + 'parent' + end + + describe 'child' do + let :memoized_value do + calls[:child] += 1 + "#{super()}/child" + end + + example do + order.declare(:second) { expect(memoized_value).to eq 'parent/child' } + expect(memoized_value).to eq 'parent/child' + expect(calls).to eq :parent => 1, :child => 1 + end + end + end + end + end + end end RSpec.describe "#let" do diff --git a/spec/rspec/core/reentrant_mutex_spec.rb b/spec/rspec/core/reentrant_mutex_spec.rb new file mode 100644 index 0000000000..e3fae36ebf --- /dev/null +++ b/spec/rspec/core/reentrant_mutex_spec.rb @@ -0,0 +1,30 @@ +require 'rspec/core/reentrant_mutex' +require 'thread_order' + +# There are no assertions specifically +# They are pass if they don't deadlock +RSpec.describe RSpec::Core::ReentrantMutex do + let!(:mutex) { described_class.new } + let!(:order) { ThreadOrder.new } + after { order.apocalypse! } + + it 'can repeatedly synchronize within the same thread' do + mutex.synchronize { mutex.synchronize { } } + end + + it 'locks other threads out while in the synchronize block' do + order.declare(:before) { mutex.synchronize { } } + order.declare(:within) { mutex.synchronize { } } + order.declare(:after) { mutex.synchronize { } } + + order.pass_to :before, :resume_on => :exit + mutex.synchronize { order.pass_to :within, :resume_on => :sleep } + order.pass_to :after, :resume_on => :exit + end + + it 'resumes the next thread once all its synchronize blocks have completed' do + order.declare(:thread) { mutex.synchronize { } } + mutex.synchronize { order.pass_to :thread, :resume_on => :sleep } + order.join_all + end +end From 3de3c43152a59e69710c2089dff43fa0da9a32ee Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Thu, 2 Apr 2015 23:07:30 -0700 Subject: [PATCH 116/258] Tweak a few things about the threadsafe let solution from #1858. 1). Remove `--threadsafe` CLI option. In general, we don't expose every config option via the CLI. We want to make it easy for users to run `rspec --help` and find the options that they are commonly going to want to customize for a particular CLI run. I don't think `--threadsafe` is one of those -- it's more something that'll be turned off globally for the project or not touched at all, and as such, it adds noise to the `--help` output to include it. 2). Extract Mutex class into its own file. - Remove need for use of `MUTEX` over `Mutex`. - No reason to force Ruby to parse the code for 1.8.7 Mutex implementation on other Rubies. 3). Remove threadsafe scenario. A note about threadsafey is sufficient for the docs. Having a full threadsafety example spec in the scenario is more detail than users are likely to want to read. --- Changelog.md | 1 + features/helper_methods/let.feature | 42 +---------- lib/rspec/core/mutex.rb | 63 ++++++++++++++++ lib/rspec/core/option_parser.rb | 4 - lib/rspec/core/reentrant_mutex.rb | 73 +++---------------- spec/rspec/core/configuration_options_spec.rb | 12 --- 6 files changed, 76 insertions(+), 119 deletions(-) create mode 100644 lib/rspec/core/mutex.rb diff --git a/Changelog.md b/Changelog.md index 140746c032..d8a0845c16 100644 --- a/Changelog.md +++ b/Changelog.md @@ -29,6 +29,7 @@ Enhancements: relative to each other. (Myron Marston, #1908) * Set example group constant earlier so errors when evaluating the context include the example group name (Myron Marson, #1911) +* Make `let` and `subject` threadsafe. (Josh Cheek, #1858) Bug Fixes: diff --git a/features/helper_methods/let.feature b/features/helper_methods/let.feature index d4b8422098..7c5d0b0c1d 100644 --- a/features/helper_methods/let.feature +++ b/features/helper_methods/let.feature @@ -7,6 +7,9 @@ Feature: let and let! the method it defines is invoked. You can use `let!` to force the method's invocation before each example. + By default, `let` is threadsafe, but you can configure it not to be + by disabling `config.threadsafe`, which makes `let` perform a bit faster. + Scenario: Use `let` to define memoized helper method Given a file named "let_spec.rb" with: """ruby @@ -48,42 +51,3 @@ Feature: let and let! """ When I run `rspec let_bang_spec.rb` Then the examples should all pass - - Scenario: Use --threadsafe to set `RSpec.configuration.threadsafe` (defaults to true) - Given a file named "let_threadsafe.rb" with: - """ruby - require 'thread' - accesses = Queue.new - turns = Queue.new - - RSpec.describe "threadsafe let" do - let :resource do - turns.shift - accesses << :from_let - end - - it "will only ever access the let block once" do - first_access = Thread.new { resource } - second_access = Thread.new { resource } - loop do - Thread.pass - break if first_access.stop? && second_access.stop? - end - turns << nil - turns << nil - first_access.join - second_access.join - accesses << :from_example - expect(accesses.shift).to eq :from_let - expect(accesses.shift).to eq :from_example - end - end - """ - When I run `rspec let_threadsafe.rb --threadsafe` - Then the examples should all pass - - When I run `rspec let_threadsafe.rb --no-threadsafe` - Then the output should contain "1 example, 1 failure" - - When I run `rspec let_threadsafe.rb` - Then the examples should all pass diff --git a/lib/rspec/core/mutex.rb b/lib/rspec/core/mutex.rb new file mode 100644 index 0000000000..57945ee1eb --- /dev/null +++ b/lib/rspec/core/mutex.rb @@ -0,0 +1,63 @@ +module RSpec + module Core + # On 1.8.7, it's in the stdlib. + # We don't want to load the stdlib, b/c this is a test tool, and can affect the test environment, + # causing tests to pass where they should fail. + # + # So we're transcribing/modifying it from https://github.com/ruby/ruby/blob/v1_8_7_374/lib/thread.rb#L56 + # Some methods we don't need are deleted. + # Anything I don't understand (there's quite a bit, actually) is left in. + # Some formating changes are made to appease the robot overlord: + # https://travis-ci.org/rspec/rspec-core/jobs/54410874 + # @private + class Mutex + def initialize + @waiting = [] + @locked = false + @waiting.taint + taint + end + + # @private + def lock + while Thread.critical = true && @locked + @waiting.push Thread.current + Thread.stop + end + @locked = true + Thread.critical = false + self + end + + # @private + def unlock + return unless @locked + Thread.critical = true + @locked = false + begin + t = @waiting.shift + t.wakeup if t + rescue ThreadError + retry + end + Thread.critical = false + begin + t.run if t + rescue ThreadError + :noop + end + self + end + + # @private + def synchronize + lock + begin + yield + ensure + unlock + end + end + end unless defined?(::RSpec::Core::Mutex) # Avoid warnings for library wide checks spec + end +end diff --git a/lib/rspec/core/option_parser.rb b/lib/rspec/core/option_parser.rb index 6606637741..6cec8e7af4 100644 --- a/lib/rspec/core/option_parser.rb +++ b/lib/rspec/core/option_parser.rb @@ -115,10 +115,6 @@ def parser(options) options[:color] = o end - parser.on('--[no-]threadsafe', 'Turn on threadsafety where available') do |o| - options[:threadsafe] = o - end - parser.on('-p', '--[no-]profile [COUNT]', 'Enable profiling of examples and list the slowest examples (default: 10).') do |argument| options[:profile_examples] = if argument.nil? diff --git a/lib/rspec/core/reentrant_mutex.rb b/lib/rspec/core/reentrant_mutex.rb index 352de4e669..c3065ec7a0 100644 --- a/lib/rspec/core/reentrant_mutex.rb +++ b/lib/rspec/core/reentrant_mutex.rb @@ -14,7 +14,7 @@ class ReentrantMutex def initialize @owner = nil @count = 0 - @mutex = MUTEX.new + @mutex = Mutex.new end def synchronize @@ -40,68 +40,13 @@ def exit end end - # @private - # :nocov: - # This can be deleted once support for 1.8.7 is dropped - MUTEX = if defined? ::Mutex - # On 1.9 and up, this is in core, so we just use the real one - ::Mutex - else - # On 1.8.7, it's in the stdlib. - # We don't want to load the stdlib, b/c this is a test tool, and can affect the test environment, - # causing tests to pass where they should fail. - # - # So we're transcribing/modifying it from https://github.com/ruby/ruby/blob/v1_8_7_374/lib/thread.rb#L56 - # Some methods we don't need are deleted. - # Anything I don't understand (there's quite a bit, actually) is left in. - # Some formating changes are made to appease the robot overlord: - # https://travis-ci.org/rspec/rspec-core/jobs/54410874 - Class.new do - def initialize - @waiting = [] - @locked = false - @waiting.taint - taint - end - - def lock - while Thread.critical = true && @locked - @waiting.push Thread.current - Thread.stop - end - @locked = true - Thread.critical = false - self - end - - def unlock - return unless @locked - Thread.critical = true - @locked = false - begin - t = @waiting.shift - t.wakeup if t - rescue ThreadError - retry - end - Thread.critical = false - begin - t.run if t - rescue ThreadError - :noop - end - self - end - - def synchronize - lock - begin - yield - ensure - unlock - end - end - end - end + if defined? ::Mutex + # On 1.9 and up, this is in core, so we just use the real one + Mutex = ::Mutex + else # For 1.8.7 + # :nocov: + RSpec::Support.require_rspec_core "mutex" + # :nocov: + end end end diff --git a/spec/rspec/core/configuration_options_spec.rb b/spec/rspec/core/configuration_options_spec.rb index 7b98abed9c..f4887ff23e 100644 --- a/spec/rspec/core/configuration_options_spec.rb +++ b/spec/rspec/core/configuration_options_spec.rb @@ -211,18 +211,6 @@ end end - describe '--threadsafe', :threadsafe => true do - it 'sets :threadsafe => true' do - expect(parse_options('--threadsafe')).to include(:threadsafe => true) - end - end - - describe '--no-threadsafe', :threadsafe => true do - it 'sets :threadsafe => false' do - expect(parse_options('--no-threadsafe')).to include(:threadsafe => false) - end - end - describe "-I" do example "adds to :libs" do expect(parse_options('-I', 'a_dir')).to include(:libs => ['a_dir']) From 57ccf137727df4b6b40395f00eabe746d248b0ab Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 3 Apr 2015 22:40:14 -0700 Subject: [PATCH 117/258] Remove unnecessary `uniq!` call. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No code is faster than no code. It appears that the `uniq!` comes from micronaut: https://github.com/rspec/rspec-core/commit/afca72820329d5f84aa60fcecb7bf188d8394919#diff-117c39fd53498f0c3f5a53efb3c24f05R52 However, it’s clear that it’s not actually needed. It didn’t do anything for 2 years due to being a `uniq` (not `uniq!`) call with an ignored return value, beginning in this commit: https://github.com/rspec/rspec-core/commit/ad9281626a8e8bafe3c97d5e97a18fa9f981d74e#diff-3831ad1cf9ec21d559640a066a9d3bc8R40 …until this one, 2 years later: 67923ba940f0360740292710e798836c1c353d20 I don’t think there’s any way for there to be any duplicates to begin with, so it’s safe to remove. --- lib/rspec/core/world.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/rspec/core/world.rb b/lib/rspec/core/world.rb index bfd59192cb..53f68ca68f 100644 --- a/lib/rspec/core/world.rb +++ b/lib/rspec/core/world.rb @@ -18,7 +18,6 @@ def initialize(configuration=RSpec.configuration) hash[group] = begin examples = group.examples.dup examples = filter_manager.prune(examples) - examples.uniq! examples end end From 3b714637855e9bd11edd4f89b960a47f7d440597 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 3 Apr 2015 22:46:20 -0700 Subject: [PATCH 118/258] Remove unnecessary `dup`. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `filter_manager.prune` avoids mutating the input array, like a good citizen, so there’s no need to dup it. --- lib/rspec/core/world.rb | 6 +--- spec/rspec/core/filter_manager_spec.rb | 49 ++++++++++++++++---------- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/lib/rspec/core/world.rb b/lib/rspec/core/world.rb index 53f68ca68f..10ce57c43a 100644 --- a/lib/rspec/core/world.rb +++ b/lib/rspec/core/world.rb @@ -15,11 +15,7 @@ def initialize(configuration=RSpec.configuration) @example_groups = [] @example_group_counts_by_spec_file = Hash.new(0) @filtered_examples = Hash.new do |hash, group| - hash[group] = begin - examples = group.examples.dup - examples = filter_manager.prune(examples) - examples - end + hash[group] = filter_manager.prune(group.examples) end end diff --git a/spec/rspec/core/filter_manager_spec.rb b/spec/rspec/core/filter_manager_spec.rb index ab46d9cf0d..0973bf75ad 100644 --- a/spec/rspec/core/filter_manager_spec.rb +++ b/spec/rspec/core/filter_manager_spec.rb @@ -8,6 +8,17 @@ def opposite(name) let(:inclusions) { filter_manager.inclusions } let(:exclusions) { filter_manager.exclusions } + def prune(examples) + # We want to enforce that our FilterManager, like a good citizen, + # leaves the input array unmodified. There are a lot of code paths + # through the filter manager, so rather than write one + # `it 'does not mutate the input'` example that would not cover + # all code paths, we're freezing the input here in order to + # enforce that for ALL examples in this file that call `prune`, + # the input array is not mutated. + filter_manager.prune(examples.freeze) + end + %w[include inclusions exclude exclusions].each_slice(2) do |name, type| describe "##{name}" do subject(:rules) { send(type).rules } @@ -114,7 +125,7 @@ def example_with(*args) add_filter(:line_number => line, :scoped_id => "1:1") filter_manager.exclude_with_low_priority :slow => true - expect(filter_manager.prune([included, excluded])).to eq([included]) + expect(prune([included, excluded])).to eq([included]) end it "prefers #{type} on entire group to exclusion filter on a nested example" do @@ -127,7 +138,7 @@ def example_with(*args) add_filter(:line_number => line, :scoped_id => "1") filter_manager.exclude_with_low_priority :slow => true - expect(filter_manager.prune([included, excluded])).to eq([included]) + expect(prune([included, excluded])).to eq([included]) end it "still applies inclusion filters to examples from files with no #{type} filters" do @@ -143,7 +154,7 @@ def example_with(*args) add_filter(:line_number => line, :scoped_id => "1:1") filter_manager.include_with_low_priority :foo => true - expect(filter_manager.prune([ + expect(prune([ included_via_loc_or_id, excluded_via_loc_or_id, included_via_tag, excluded_via_tag ]).map(&:description)).to eq([included_via_loc_or_id, included_via_tag].map(&:description)) @@ -178,7 +189,7 @@ def add_filter(options) filter_manager.add_ids(__FILE__, ["1:1", "1:3"]) filter_manager.add_location(__FILE__, [line_1, line_2]) - expect(filter_manager.prune([ + expect(prune([ matches_id, matches_line_number, matches_both, matches_neither ])).to eq([matches_id, matches_line_number, matches_both]) end @@ -198,7 +209,7 @@ def add_filter(options) expect { filter_manager.add_location(__FILE__, [line]) }.to change { - filter_manager.prune([this_file_example, other_file_example]).map(&:description) + prune([this_file_example, other_file_example]).map(&:description) }.from([]).to([this_file_example.description]) end end @@ -209,21 +220,21 @@ def add_filter(options) excluded = group.example("exclude") {} filter_manager.include(:full_description => /include/) filter_manager.exclude_with_low_priority :slow => true - expect(filter_manager.prune([included, excluded])).to eq([included]) + expect(prune([included, excluded])).to eq([included]) end it "includes objects with tags matching inclusions" do included = example_with({:foo => :bar}) excluded = example_with filter_manager.include :foo => :bar - expect(filter_manager.prune([included, excluded])).to eq([included]) + expect(prune([included, excluded])).to eq([included]) end it "excludes objects with tags matching exclusions" do included = example_with excluded = example_with({:foo => :bar}) filter_manager.exclude :foo => :bar - expect(filter_manager.prune([included, excluded])).to eq([included]) + expect(prune([included, excluded])).to eq([included]) end it "prefers exclusion when matches previously set inclusion" do @@ -231,7 +242,7 @@ def add_filter(options) excluded = example_with({:foo => :bar}) filter_manager.include :foo => :bar filter_manager.exclude :foo => :bar - expect(filter_manager.prune([included, excluded])).to eq([included]) + expect(prune([included, excluded])).to eq([included]) end it "prefers inclusion when matches previously set exclusion" do @@ -239,7 +250,7 @@ def add_filter(options) excluded = example_with filter_manager.exclude :foo => :bar filter_manager.include :foo => :bar - expect(filter_manager.prune([included, excluded])).to eq([included]) + expect(prune([included, excluded])).to eq([included]) end it "prefers previously set inclusion when exclusion matches but has lower priority" do @@ -247,7 +258,7 @@ def add_filter(options) excluded = example_with filter_manager.include :foo => :bar filter_manager.exclude_with_low_priority :foo => :bar - expect(filter_manager.prune([included, excluded])).to eq([included]) + expect(prune([included, excluded])).to eq([included]) end it "prefers previously set exclusion when inclusion matches but has lower priority" do @@ -255,7 +266,7 @@ def add_filter(options) excluded = example_with({:foo => :bar}) filter_manager.exclude :foo => :bar filter_manager.include_with_low_priority :foo => :bar - expect(filter_manager.prune([included, excluded])).to eq([included]) + expect(prune([included, excluded])).to eq([included]) end context "with multiple inclusion filters" do @@ -267,7 +278,7 @@ def add_filter(options) ] filter_manager.include :foo => true, :bar => true - expect(filter_manager.prune(examples)).to contain_exactly(included_1, included_2) + expect(prune(examples)).to contain_exactly(included_1, included_2) end end @@ -280,7 +291,7 @@ def add_filter(options) end filter_manager.add_ids(Metadata.relative_path(__FILE__), %w[ 1:2 ]) - expect(filter_manager.prune([ex_1, ex_2])).to eq([ex_2]) + expect(prune([ex_1, ex_2])).to eq([ex_2]) end it 'can work with absolute file paths' do @@ -291,7 +302,7 @@ def add_filter(options) end filter_manager.add_ids(File.expand_path(__FILE__), %w[ 1:2 ]) - expect(filter_manager.prune([ex_1, ex_2])).to eq([ex_2]) + expect(prune([ex_1, ex_2])).to eq([ex_2]) end it "can work with relative paths that lack the leading `.`" do @@ -304,7 +315,7 @@ def add_filter(options) end filter_manager.add_ids(path, %w[ 1:2 ]) - expect(filter_manager.prune([ex_1, ex_2])).to eq([ex_2]) + expect(prune([ex_1, ex_2])).to eq([ex_2]) end it 'can select groups' do @@ -316,7 +327,7 @@ def add_filter(options) end filter_manager.add_ids(Metadata.relative_path(__FILE__), %w[ 2 ]) - expect(filter_manager.prune([ex_1, ex_2, ex_3])).to eq([ex_2, ex_3]) + expect(prune([ex_1, ex_2, ex_3])).to eq([ex_2, ex_3]) end it 'uses the rerun file path when applying the id filter' do @@ -334,7 +345,7 @@ def add_filter(options) RSpec.describe { include_examples "shared" } filter_manager.add_ids(__FILE__, %w[ 1:1 ]) - expect(filter_manager.prune([ex_1, ex_2]).map(&:description)).to eq([ex_1].map(&:description)) + expect(prune([ex_1, ex_2]).map(&:description)).to eq([ex_1].map(&:description)) end end end @@ -404,7 +415,7 @@ def example_with_metadata(metadata) end def exclude?(example) - filter_manager.prune([example]).empty? + prune([example]).empty? end describe "the default :if filter" do From 1dd82dd4b1d692ab2fa3314987fcd93be4372e41 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 3 Apr 2015 23:00:53 -0700 Subject: [PATCH 119/258] Abort filtering early if the input array is empty. --- lib/rspec/core/filter_manager.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/rspec/core/filter_manager.rb b/lib/rspec/core/filter_manager.rb index 886958bd4a..8351b2776f 100644 --- a/lib/rspec/core/filter_manager.rb +++ b/lib/rspec/core/filter_manager.rb @@ -32,6 +32,13 @@ def empty? end def prune(examples) + # Semantically, this is unnecessary (the filtering below will return the empty + # array unmodified), but for perf reasons it's worth exiting early here. Users + # commonly have top-level examples groups that do not have any direct examples + # and instead have nested groups with examples. In that kind of situation, + # `examples` will be empty. + return examples if examples.empty? + examples = prune_conditionally_filtered_examples(examples) if inclusions.standalone? From 68b634e9d87875a9ed552f60ddd9cd00b4c2bfd5 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Sat, 4 Apr 2015 19:29:30 -0700 Subject: [PATCH 120/258] Make `describe_successfully` available for all specs. --- spec/rspec/core/memoized_helpers_spec.rb | 13 ------------- spec/spec_helper.rb | 17 +++++++++++++++-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/spec/rspec/core/memoized_helpers_spec.rb b/spec/rspec/core/memoized_helpers_spec.rb index df0d3086cb..3869127b63 100644 --- a/spec/rspec/core/memoized_helpers_spec.rb +++ b/spec/rspec/core/memoized_helpers_spec.rb @@ -369,19 +369,6 @@ def not_ok?; false; end describe 'threadsafety', :threadsafe => true do before(:all) { eq 1 } # explanation: https://github.com/rspec/rspec-core/pull/1858/files#r25411166 - class RaiseOnFailuresReporter < RSpec::Core::NullReporter - def self.example_failed(example) - raise example.exception - end - end - - def describe_successfully(&describe_body) - example_group = RSpec.describe(&describe_body) - ran_successfully = example_group.run RaiseOnFailuresReporter - expect(ran_successfully).to eq true - end - - context 'when not threadsafe' do # would be nice to not set this on the global before { RSpec.configuration.threadsafe = false } diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 6a314d2b5d..9c7032178b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -31,7 +31,20 @@ def self.new(*args, &block) require file.gsub("./spec/support", "support") end -module EnvHelpers + class RaiseOnFailuresReporter < RSpec::Core::NullReporter + def self.example_failed(example) + raise example.exception + end + end + +module CommonHelpers + def describe_successfully(&describe_body) + example_group = RSpec.describe(&describe_body) + ran_successfully = example_group.run RaiseOnFailuresReporter + expect(ran_successfully).to eq true + example_group + end + def with_env_vars(vars) original = ENV.to_hash vars.each { |k, v| ENV[k] = v } @@ -76,7 +89,7 @@ def handle_current_dir_change # runtime options c.raise_errors_for_deprecations! c.color = true - c.include EnvHelpers + c.include CommonHelpers c.filter_run_excluding :ruby => lambda {|version| case version.to_s when "!jruby" From 8e69df186121441ae4b2030aa323bfa348643d16 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Sat, 4 Apr 2015 19:37:38 -0700 Subject: [PATCH 121/258] Groups with `before(:all) { skip }` should pass. Fixes #1925. --- lib/rspec/core/example_group.rb | 1 + spec/rspec/core/example_spec.rb | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rspec/core/example_group.rb b/lib/rspec/core/example_group.rb index fdce80ef3f..fcc75c2259 100644 --- a/lib/rspec/core/example_group.rb +++ b/lib/rspec/core/example_group.rb @@ -517,6 +517,7 @@ def self.run(reporter=RSpec::Core::NullReporter) result_for_this_group && results_for_descendants rescue Pending::SkipDeclaredInExample => ex for_filtered_examples(reporter) { |example| example.skip_with_exception(reporter, ex) } + true rescue Exception => ex RSpec.world.wants_to_quit = true if fail_fast? for_filtered_examples(reporter) { |example| example.fail_with_exception(reporter, ex) } diff --git a/spec/rspec/core/example_spec.rb b/spec/rspec/core/example_spec.rb index cee173c6ed..e0465f0826 100644 --- a/spec/rspec/core/example_spec.rb +++ b/spec/rspec/core/example_spec.rb @@ -656,12 +656,11 @@ def expect_pending_result(example) context "in before(:all)" do it "sets each example to skipped" do - group = RSpec.describe do + group = describe_successfully do before(:all) { skip("not done"); fail } example {} example {} end - group.run expect(group.examples.first).to be_skipped_with("not done") expect(group.examples.last).to be_skipped_with("not done") end From 795c22d3fe68a5aeb445b728bd02ff6d558226a0 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Sat, 4 Apr 2015 19:42:35 -0700 Subject: [PATCH 122/258] Explicitly indicate that the example group failed. Before we were relying upon the return value of `for_filtered_examples`, which is a bad idea. --- lib/rspec/core/example_group.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/rspec/core/example_group.rb b/lib/rspec/core/example_group.rb index fcc75c2259..98f0abeea2 100644 --- a/lib/rspec/core/example_group.rb +++ b/lib/rspec/core/example_group.rb @@ -521,6 +521,7 @@ def self.run(reporter=RSpec::Core::NullReporter) rescue Exception => ex RSpec.world.wants_to_quit = true if fail_fast? for_filtered_examples(reporter) { |example| example.fail_with_exception(reporter, ex) } + false ensure run_after_context_hooks(new('after(:context) hook')) if should_run_context_hooks reporter.example_group_finished(self) From f1f4908cf29bf6b8a739e56005c79dbb934449c1 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Sat, 4 Apr 2015 19:46:27 -0700 Subject: [PATCH 123/258] Use `describe_successfully` in a few more places. This helper makes the additional helpful assertion about the return value of `group.run`. --- spec/rspec/core/example_spec.rb | 42 ++++++++++++--------------------- 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/spec/rspec/core/example_spec.rb b/spec/rspec/core/example_spec.rb index e0465f0826..c747bd6db8 100644 --- a/spec/rspec/core/example_spec.rb +++ b/spec/rspec/core/example_spec.rb @@ -486,23 +486,21 @@ def expect_pending_result(example) context "in the example" do it "sets the example to pending" do - group = RSpec.describe do + group = describe_successfully do example { pending; fail } end - group.run expect_pending_result(group.examples.first) end it "allows post-example processing in around hooks (see https://github.com/rspec/rspec-core/issues/322)" do blah = nil - group = RSpec.describe do + describe_successfully do around do |example| example.run blah = :success end - example { pending } + example { pending; fail } end - group.run expect(blah).to be(:success) end @@ -523,22 +521,20 @@ def expect_pending_result(example) context "in before(:each)" do it "sets each example to pending" do - group = RSpec.describe do + group = describe_successfully do before(:each) { pending } example { fail } example { fail } end - group.run expect_pending_result(group.examples.first) expect_pending_result(group.examples.last) end it 'sets example to pending when failure occurs in before(:each)' do - group = RSpec.describe do + group = describe_successfully do before(:each) { pending; fail } example {} end - group.run expect_pending_result(group.examples.first) end end @@ -574,32 +570,29 @@ def expect_pending_result(example) context "in around(:each)" do it "sets the example to pending" do - group = RSpec.describe do + group = describe_successfully do around(:each) { pending } example { fail } end - group.run expect_pending_result(group.examples.first) end it 'sets example to pending when failure occurs in around(:each)' do - group = RSpec.describe do + group = describe_successfully do around(:each) { pending; fail } example {} end - group.run expect_pending_result(group.examples.first) end end context "in after(:each)" do it "sets each example to pending" do - group = RSpec.describe do + group = describe_successfully do after(:each) { pending; fail } example { } example { } end - group.run expect_pending_result(group.examples.first) expect_pending_result(group.examples.last) end @@ -610,32 +603,29 @@ def expect_pending_result(example) describe "#skip" do context "in the example" do it "sets the example to skipped" do - group = RSpec.describe do + group = describe_successfully do example { skip } end - group.run expect(group.examples.first).to be_skipped end it "allows post-example processing in around hooks (see https://github.com/rspec/rspec-core/issues/322)" do blah = nil - group = RSpec.describe do + describe_successfully do around do |example| example.run blah = :success end example { skip } end - group.run expect(blah).to be(:success) end context "with a message" do it "sets the example to skipped with the provided message" do - group = RSpec.describe do + group = describe_successfully do example { skip "lorem ipsum" } end - group.run expect(group.examples.first).to be_skipped_with("lorem ipsum") end end @@ -643,12 +633,11 @@ def expect_pending_result(example) context "in before(:each)" do it "sets each example to skipped" do - group = RSpec.describe do + group = describe_successfully do before(:each) { skip } example {} example {} end - group.run expect(group.examples.first).to be_skipped expect(group.examples.last).to be_skipped end @@ -668,11 +657,10 @@ def expect_pending_result(example) context "in around(:each)" do it "sets the example to skipped" do - group = RSpec.describe do + group = describe_successfully do around(:each) { skip } example {} end - group.run expect(group.examples.first).to be_skipped end end @@ -695,12 +683,12 @@ def expect_pending_result(example) RSpec.configuration.order = :random - RSpec.describe do + describe_successfully do # The bug was only triggered when the examples # were in nested contexts; see https://github.com/rspec/rspec-core/pull/837 context { example { values << rand } } context { example { values << rand } } - end.run + end expect(values.uniq.count).to eq(2) end From e2b05553335b05fd5a94ede6691ca36d7bc7e505 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Sat, 4 Apr 2015 19:48:00 -0700 Subject: [PATCH 124/258] Add changelog entry. --- Changelog.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Changelog.md b/Changelog.md index d8a0845c16..e9dddd83fd 100644 --- a/Changelog.md +++ b/Changelog.md @@ -43,6 +43,9 @@ Bug Fixes: the `its-it` gem). (Alex Kwiatkowski, Ryan Ong, #1907) * Make `let` work properly when defined in a shared context that is applied to an individual example via metadata. (Myron Marston, #1912) +* Fix `before(:context) { skip }` so that it does not wrongly cause the + spec suite to exit with a non-zero status when no examples failed. + (Myron Marston, #1926) ### 3.2.2 / 2015-03-11 [Full Changelog](http://github.com/rspec/rspec-core/compare/v3.2.1...v3.2.2) From 86a68b43acab2091c1b4026ddcb41dd7f5e1b05d Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Sun, 5 Apr 2015 19:43:22 +1000 Subject: [PATCH 125/258] remove extraneous information --- lib/rspec/core/formatters/json_formatter.rb | 1 - spec/rspec/core/formatters/json_formatter_spec.rb | 2 -- 2 files changed, 3 deletions(-) diff --git a/lib/rspec/core/formatters/json_formatter.rb b/lib/rspec/core/formatters/json_formatter.rb index 83876caf8b..cf21c0ab3d 100644 --- a/lib/rspec/core/formatters/json_formatter.rb +++ b/lib/rspec/core/formatters/json_formatter.rb @@ -13,7 +13,6 @@ class JsonFormatter < BaseFormatter def initialize(output) super @output_hash = { - :type => 'rspec-json', :version => RSpec::Core::Version::STRING } end diff --git a/spec/rspec/core/formatters/json_formatter_spec.rb b/spec/rspec/core/formatters/json_formatter_spec.rb index d6499aeb17..edf82149fd 100644 --- a/spec/rspec/core/formatters/json_formatter_spec.rb +++ b/spec/rspec/core/formatters/json_formatter_spec.rb @@ -41,7 +41,6 @@ this_file = relative_path(__FILE__) expected = { - :type => 'rspec-json', :version => RSpec::Core::Version::STRING, :examples => [ { @@ -98,7 +97,6 @@ expect(output.string).to eq "" send_notification :close, null_notification expect(output.string).to eq({ - :type => 'rspec-json', :version => RSpec::Core::Version::STRING }.to_json) end From 0312e5b59bb12d9cef727aad7aff3d0bf95d41d8 Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Sun, 5 Apr 2015 19:44:21 +1000 Subject: [PATCH 126/258] changelog post #1883 --- Changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Changelog.md b/Changelog.md index e9dddd83fd..a15270fa7d 100644 --- a/Changelog.md +++ b/Changelog.md @@ -30,6 +30,7 @@ Enhancements: * Set example group constant earlier so errors when evaluating the context include the example group name (Myron Marson, #1911) * Make `let` and `subject` threadsafe. (Josh Cheek, #1858) +* Add version information into the JSON formatter. (Mark Swinson, #1883) Bug Fixes: From d48d07fd26170192e00f18abb7e578b52320d4aa Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Sat, 21 Mar 2015 19:46:25 -0700 Subject: [PATCH 127/258] Remove unused instance variable. --- spec/rspec/core/drb_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/rspec/core/drb_spec.rb b/spec/rspec/core/drb_spec.rb index 52d9adff96..1fd1585971 100644 --- a/spec/rspec/core/drb_spec.rb +++ b/spec/rspec/core/drb_spec.rb @@ -74,7 +74,6 @@ def self.run(argv, err, out) before(:all) do @drb_port = '8990' - @drb_example_file_counter = 0 DRb::start_service("druby://127.0.0.1:#{@drb_port}", SimpleDRbSpecServer) end From 65c341ef2ae3cc55915ced0a6431202794d749c8 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Sat, 21 Mar 2015 21:07:25 -0700 Subject: [PATCH 128/258] Extract ShellEscape module. --- lib/rspec/core/notifications.rb | 31 ++------------------- lib/rspec/core/rake_task.rb | 16 ++--------- lib/rspec/core/shell_escape.rb | 49 +++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 42 deletions(-) create mode 100644 lib/rspec/core/shell_escape.rb diff --git a/lib/rspec/core/notifications.rb b/lib/rspec/core/notifications.rb index 638ba18099..6c363dc673 100644 --- a/lib/rspec/core/notifications.rb +++ b/lib/rspec/core/notifications.rb @@ -1,4 +1,5 @@ RSpec::Support.require_rspec_core "formatters/helpers" +RSpec::Support.require_rspec_core "shell_escape" RSpec::Support.require_rspec_support "encoded_string" module RSpec::Core @@ -520,6 +521,8 @@ def fully_formatted(colorizer=::RSpec::Core::Formatters::ConsoleCodes) private + include RSpec::Core::ShellEscape + def rerun_argument_for(example) location = example.location_rerun_argument return location unless duplicate_rerun_locations.include?(location) @@ -537,34 +540,6 @@ def duplicate_rerun_locations end end end - - # Known shells that require quoting: zsh, csh, tcsh. - # - # Feel free to add other shells to this list that are known to - # allow `rspec ./some_spec.rb[1:1]` syntax without quoting the id. - # - # @private - SHELLS_ALLOWING_UNQUOTED_IDS = %w[ bash ksh fish ] - - def conditionally_quote(id) - return id if shell_allows_unquoted_ids? - "'#{id.gsub("'", "\\\\'")}'" - end - - def shell_allows_unquoted_ids? - return @shell_allows_unquoted_ids if defined?(@shell_allows_unquoted_ids) - - @shell_allows_unquoted_ids = SHELLS_ALLOWING_UNQUOTED_IDS.include?( - # Note: ENV['SHELL'] isn't necessarily the shell the user is currently running. - # According to http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html: - # "This variable shall represent a pathname of the user's preferred command language interpreter." - # - # It's the best we can easily do, though. We err on the side of safety (quoting - # the id when not actually needed) so it's not a big deal if the user is actually - # using a different shell. - ENV['SHELL'].to_s.split('/').last - ) - end end # The `ProfileNotification` holds information about the results of running a diff --git a/lib/rspec/core/rake_task.rb b/lib/rspec/core/rake_task.rb index dffea4736b..60308fd565 100644 --- a/lib/rspec/core/rake_task.rb +++ b/lib/rspec/core/rake_task.rb @@ -1,6 +1,7 @@ require 'rake' require 'rake/tasklib' require 'rspec/support/ruby_features' +require 'rspec/core/shell_escape' module RSpec module Core @@ -9,6 +10,7 @@ module Core # @see Rakefile class RakeTask < ::Rake::TaskLib include ::Rake::DSL if defined?(::Rake::DSL) + include RSpec::Core::ShellEscape # Default path to the RSpec executable. DEFAULT_RSPEC_PATH = File.expand_path('../../../../exe/rspec', __FILE__) @@ -122,20 +124,6 @@ def file_inclusion_specification end end - if RSpec::Support::OS.windows? - # :nocov: - def escape(shell_command) - "'#{shell_command.gsub("'", "\\\\'")}'" - end - # :nocov: - else - require 'shellwords' - - def escape(shell_command) - shell_command.shellescape - end - end - def file_exclusion_specification " --exclude-pattern #{escape exclude_pattern}" if exclude_pattern end diff --git a/lib/rspec/core/shell_escape.rb b/lib/rspec/core/shell_escape.rb new file mode 100644 index 0000000000..f83c799530 --- /dev/null +++ b/lib/rspec/core/shell_escape.rb @@ -0,0 +1,49 @@ +module RSpec + module Core + # @private + # Deals with the fact that `shellwords` only works on POSIX systems. + module ShellEscape + if RSpec::Support::OS.windows? + # :nocov: + def escape(shell_command) + "'#{shell_command.gsub("'", "\\\\'")}'" + end + # :nocov: + else + require 'shellwords' + + def escape(shell_command) + shell_command.shellescape + end + end + + # Known shells that require quoting: zsh, csh, tcsh. + # + # Feel free to add other shells to this list that are known to + # allow `rspec ./some_spec.rb[1:1]` syntax without quoting the id. + # + # @private + SHELLS_ALLOWING_UNQUOTED_IDS = %w[ bash ksh fish ] + + def conditionally_quote(id) + return id if shell_allows_unquoted_ids? + "'#{id.gsub("'", "\\\\'")}'" + end + + def shell_allows_unquoted_ids? + return @shell_allows_unquoted_ids if defined?(@shell_allows_unquoted_ids) + + @shell_allows_unquoted_ids = SHELLS_ALLOWING_UNQUOTED_IDS.include?( + # Note: ENV['SHELL'] isn't necessarily the shell the user is currently running. + # According to http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html: + # "This variable shall represent a pathname of the user's preferred command language interpreter." + # + # It's the best we can easily do, though. We err on the side of safety (quoting + # the id when not actually needed) so it's not a big deal if the user is actually + # using a different shell. + ENV['SHELL'].to_s.split('/').last + ) + end + end + end +end From f63add0dc0f5f69792f6d6c1d0ed08c8d512c92b Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Sun, 22 Mar 2015 20:10:46 -0700 Subject: [PATCH 129/258] Make option parsing simpler and more consistent. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Do not mutate the provided args. - Include `:files_or_directories_to_run` in the returned options hash. After all, it’s part of the parsed options. This will help support the new `--bisect` option by making it easy for us to split CLI args into options (which get re-used throughout the bisect process) and files_or_directories_to_run (which get replaced during bisect). --- lib/rspec/core/configuration_options.rb | 13 +++++++-- lib/rspec/core/option_parser.rb | 4 ++- spec/rspec/core/configuration_options_spec.rb | 10 +++++++ spec/rspec/core/option_parser_spec.rb | 28 ++++++++++++++++--- 4 files changed, 47 insertions(+), 8 deletions(-) diff --git a/lib/rspec/core/configuration_options.rb b/lib/rspec/core/configuration_options.rb index 7f42180c07..faf6bd431f 100644 --- a/lib/rspec/core/configuration_options.rb +++ b/lib/rspec/core/configuration_options.rb @@ -119,11 +119,12 @@ def file_options end def env_options - ENV["SPEC_OPTS"] ? Parser.parse(Shellwords.split(ENV["SPEC_OPTS"])) : {} + return {} unless ENV['SPEC_OPTS'] + parse_args_ignoring_files_or_dirs_to_run(Shellwords.split(ENV["SPEC_OPTS"])) end def command_line_options - @command_line_options ||= Parser.parse(@args).merge :files_or_directories_to_run => @args + @command_line_options ||= Parser.parse(@args) end def custom_options @@ -143,7 +144,13 @@ def global_options end def options_from(path) - Parser.parse(args_from_options_file(path)) + parse_args_ignoring_files_or_dirs_to_run(args_from_options_file(path)) + end + + def parse_args_ignoring_files_or_dirs_to_run(args) + options = Parser.parse(args) + options.delete(:files_or_directories_to_run) + options end def args_from_options_file(path) diff --git a/lib/rspec/core/option_parser.rb b/lib/rspec/core/option_parser.rb index 6cec8e7af4..58c6e3cf2f 100644 --- a/lib/rspec/core/option_parser.rb +++ b/lib/rspec/core/option_parser.rb @@ -9,7 +9,8 @@ def self.parse(args) end def parse(args) - return {} if args.empty? + return { :files_or_directories_to_run => [] } if args.empty? + args = args.dup options = args.delete('--tty') ? { :tty => true } : {} begin @@ -18,6 +19,7 @@ def parse(args) abort "#{e.message}\n\nPlease use --help for a listing of valid options" end + options[:files_or_directories_to_run] = args options end diff --git a/spec/rspec/core/configuration_options_spec.rb b/spec/rspec/core/configuration_options_spec.rb index f4887ff23e..869d06200b 100644 --- a/spec/rspec/core/configuration_options_spec.rb +++ b/spec/rspec/core/configuration_options_spec.rb @@ -382,6 +382,16 @@ end end + it 'ignores file or dir names put in one of the option files or in SPEC_OPTS, since those are for persistent options' do + File.open("./.rspec", "w") { |f| f << "path/to/spec_1.rb" } + File.open("./.rspec-local", "w") { |f| f << "path/to/spec_2.rb" } + File.open(File.expand_path("~/.rspec"), "w") {|f| f << "path/to/spec_3.rb"} + with_env_vars 'SPEC_OPTS' => "path/to/spec_4.rb" do + options = parse_options() + expect(options[:files_or_directories_to_run]).to eq([]) + end + end + it "prefers SPEC_OPTS over CLI" do with_env_vars 'SPEC_OPTS' => "--format spec_opts" do expect(parse_options("--format", "cli")[:formatters]).to eq([['spec_opts']]) diff --git a/spec/rspec/core/option_parser_spec.rb b/spec/rspec/core/option_parser_spec.rb index 27ed1351ed..7aad8093ef 100644 --- a/spec/rspec/core/option_parser_spec.rb +++ b/spec/rspec/core/option_parser_spec.rb @@ -13,10 +13,23 @@ module RSpec::Core end end - it "does not parse empty args" do - parser = Parser.new - expect(OptionParser).not_to receive(:new) - parser.parse([]) + context "when given empty args" do + it "does not parse them" do + parser = Parser.new + expect(OptionParser).not_to receive(:new) + parser.parse([]) + end + + it "still returns a `:files_or_directories_to_run` entry since callers expect that" do + expect( + Parser.parse([]) + ).to eq(:files_or_directories_to_run => []) + end + end + + it 'does not mutate the provided args array' do + args = %w[ --require foo ] + expect { Parser.parse(args) }.not_to change { args } end it "proposes you to use --help and returns an error on incorrect argument" do @@ -30,6 +43,13 @@ module RSpec::Core parser.parse([option]) end + it 'treats additional arguments as `:files_or_directories_to_run`' do + options = Parser.parse(%w[ path/to/spec.rb --fail-fast spec/unit -Ibar 1_spec.rb:23 ]) + expect(options).to include( + :files_or_directories_to_run => %w[ path/to/spec.rb spec/unit 1_spec.rb:23 ] + ) + end + { '--init' => ['-i','--I'], '--default-path' => ['-d'], From a3a929c70a942c63bb46d7bb016e8553d4659388 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Mon, 23 Mar 2015 22:52:21 -0700 Subject: [PATCH 130/258] Store original args as an attribute of the Parser. This will make it easier to implement `--bisect`, where we need access to the original CLI options. --- lib/rspec/core/option_parser.rb | 14 ++++++++++---- spec/rspec/core/option_parser_spec.rb | 24 +++++++++++------------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/lib/rspec/core/option_parser.rb b/lib/rspec/core/option_parser.rb index 58c6e3cf2f..23f3ae8fb3 100644 --- a/lib/rspec/core/option_parser.rb +++ b/lib/rspec/core/option_parser.rb @@ -5,12 +5,18 @@ module RSpec::Core # @private class Parser def self.parse(args) - new.parse(args) + new(args).parse end - def parse(args) - return { :files_or_directories_to_run => [] } if args.empty? - args = args.dup + attr_reader :original_args + + def initialize(original_args) + @original_args = original_args + end + + def parse + return { :files_or_directories_to_run => [] } if original_args.empty? + args = original_args.dup options = args.delete('--tty') ? { :tty => true } : {} begin diff --git a/spec/rspec/core/option_parser_spec.rb b/spec/rspec/core/option_parser_spec.rb index 7aad8093ef..2ca8bc615b 100644 --- a/spec/rspec/core/option_parser_spec.rb +++ b/spec/rspec/core/option_parser_spec.rb @@ -15,9 +15,8 @@ module RSpec::Core context "when given empty args" do it "does not parse them" do - parser = Parser.new expect(OptionParser).not_to receive(:new) - parser.parse([]) + Parser.parse([]) end it "still returns a `:files_or_directories_to_run` entry since callers expect that" do @@ -33,14 +32,13 @@ module RSpec::Core end it "proposes you to use --help and returns an error on incorrect argument" do - parser = Parser.new - option = "--my_wrong_arg" + parser = Parser.new([option = "--my_wrong_arg"]) expect(parser).to receive(:abort) do |msg| expect(msg).to include('use --help', option) end - parser.parse([option]) + parser.parse end it 'treats additional arguments as `:files_or_directories_to_run`' do @@ -58,22 +56,22 @@ module RSpec::Core }.each do |long, shorts| shorts.each do |option| it "won't parse #{option} as a shorthand for #{long}" do - parser = Parser.new + parser = Parser.new([option]) expect(parser).to receive(:abort) do |msg| expect(msg).to include('use --help', option) end - parser.parse([option]) + parser.parse end end end it "won't display invalid options in the help output" do def generate_help_text - parser = Parser.new + parser = Parser.new(["--help"]) allow(parser).to receive(:exit) - parser.parse(["--help"]) + parser.parse end useless_lines = /^\s*--?\w+\s*$\n/ @@ -84,11 +82,11 @@ def generate_help_text %w[ -v --version ].each do |option| describe option do it "prints the version and exits" do - parser = Parser.new + parser = Parser.new([option]) expect(parser).to receive(:exit) expect { - parser.parse([option]) + parser.parse }.to output("#{RSpec::Core::Version::STRING}\n").to_stdout end end @@ -99,12 +97,12 @@ def generate_help_text project_init = instance_double(ProjectInitializer) allow(ProjectInitializer).to receive_messages(:new => project_init) - parser = Parser.new + parser = Parser.new(["--init"]) expect(project_init).to receive(:run).ordered expect(parser).to receive(:exit).ordered - parser.parse(["--init"]) + parser.parse end end From ebc861501ad2c6ed3fb1e8de9ddbe3c8be808ee5 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Sat, 21 Mar 2015 08:45:15 -0700 Subject: [PATCH 131/258] Add bisect formatter and server. --- lib/rspec/core/bisect/server.rb | 46 ++++++++++ lib/rspec/core/formatters.rb | 3 + lib/rspec/core/formatters/bisect_formatter.rb | 66 ++++++++++++++ spec/rspec/core/bisect/server_spec.rb | 90 +++++++++++++++++++ 4 files changed, 205 insertions(+) create mode 100644 lib/rspec/core/bisect/server.rb create mode 100644 lib/rspec/core/formatters/bisect_formatter.rb create mode 100644 spec/rspec/core/bisect/server_spec.rb 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 From bdb75db8c1a9e3b108aa4be9bf21b68b28177fd5 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Sat, 21 Mar 2015 20:48:54 -0700 Subject: [PATCH 132/258] Add bisect runner. --- lib/rspec/core/bisect/runner.rb | 117 +++++++++++++++ spec/rspec/core/bisect/runner_spec.rb | 205 ++++++++++++++++++++++++++ spec/spec_helper.rb | 9 ++ spec/support/matchers.rb | 10 ++ 4 files changed, 341 insertions(+) create mode 100644 lib/rspec/core/bisect/runner.rb create mode 100644 spec/rspec/core/bisect/runner_spec.rb diff --git a/lib/rspec/core/bisect/runner.rb b/lib/rspec/core/bisect/runner.rb new file mode 100644 index 0000000000..cd9c68767d --- /dev/null +++ b/lib/rspec/core/bisect/runner.rb @@ -0,0 +1,117 @@ +RSpec::Support.require_rspec_core "shell_escape" + +module RSpec + module Core + module Bisect + # Provides an API to run the suite for a set of locations, using + # the given bisect server to capture the results. + # @private + class Runner + include RSpec::Core::ShellEscape + + def initialize(server, original_cli_args) + @server = server + @original_cli_args = original_cli_args - ["--bisect"] + end + + def run(locations) + @server.capture_run_results do + system command_for(locations) + end + end + + def command_for(locations) + parts = [] + + parts << RUBY << load_path + parts << escape(RSpec::Core.path_to_executable) + + parts << "--format" << "bisect" + parts << "--drb-port" << @server.drb_port + parts.concat reusable_cli_options + parts.concat locations.map { |l| escape(l) } + + parts.join(" ") + end + + def repro_command_from(locations) + parts = [] + + parts << "rspec" + parts.concat organize_locations(locations) + parts.concat original_cli_args_without_locations + + parts.join(" ") + end + + def original_results + @original_results ||= run(original_locations) + end + + private + + def reusable_cli_options + @reusable_cli_options ||= begin + opts = original_cli_args_without_locations + + if (port = parsed_original_cli_options[:drb_port]) + opts -= %W[ --drb-port #{port} ] + end + + parsed_original_cli_options.fetch(:formatters) { [] }.each do |(name, out)| + opts -= %W[ --format #{name} ] + opts -= %W[ --out #{out} ] + opts -= %W[ -f #{name} ] + opts -= %W[ -o #{out} ] + end + + opts + end + end + + def organize_locations(locations) + grouped = locations.inject(Hash.new { |h, k| h[k] = [] }) do |hash, location| + file, id = location.split(Configuration::ON_SQUARE_BRACKETS) + hash[file] << id + hash + end + + grouped.sort_by(&:first).map do |file, ids| + ids = ids.sort_by { |id| id.split(':').map(&:to_i) } + id = Metadata.id_from(:rerun_file_path => file, :scoped_id => ids.join(',')) + conditionally_quote(id) + end + end + + def original_cli_args_without_locations + @original_cli_args_without_locations ||= begin + files_or_dirs = parsed_original_cli_options.fetch(:files_or_directories_to_run) + @original_cli_args - files_or_dirs + end + end + + def parsed_original_cli_options + @parsed_original_cli_options ||= Parser.parse(@original_cli_args) + end + + def original_locations + parsed_original_cli_options.fetch(:files_or_directories_to_run) + end + + def load_path + @load_path ||= "-I#{$LOAD_PATH.map { |p| escape(p) }.join(':')}" + end + + # Path to the currently running Ruby executable, borrowed from Rake: + # https://github.com/ruby/rake/blob/v10.4.2/lib/rake/file_utils.rb#L8-L12 + # Note that we skip `ENV['RUBY']` because we don't have to deal with running + # RSpec from within a MRI source repository: + # https://github.com/ruby/rake/commit/968682759b3b65e42748cd2befb2ff3e982272d9 + RUBY = File.join( + RbConfig::CONFIG['bindir'], + RbConfig::CONFIG['ruby_install_name'] + RbConfig::CONFIG['EXEEXT']). + sub(/.*\s.*/m, '"\&"') + end + end + end +end diff --git a/spec/rspec/core/bisect/runner_spec.rb b/spec/rspec/core/bisect/runner_spec.rb new file mode 100644 index 0000000000..7f60fc2f62 --- /dev/null +++ b/spec/rspec/core/bisect/runner_spec.rb @@ -0,0 +1,205 @@ +require 'rspec/core/bisect/runner' + +module RSpec::Core + RSpec.describe Bisect::Runner do + let(:server) { instance_double("RSpec::Core::Bisect::Server", :drb_port => 1234) } + let(:runner) { described_class.new(server, original_cli_args) } + + describe "#command_for" do + def command_for(locations, options={}) + load_path = options.fetch(:load_path) { [] } + orig_load_path = $LOAD_PATH.dup + $LOAD_PATH.replace(load_path) + runner.command_for(locations) + ensure + $LOAD_PATH.replace(orig_load_path) + end + + let(:original_cli_args) { %w[ spec/unit -rfoo -Ibar --warnings --backtrace ] } + + it "includes the original CLI arg options" do + cmd = command_for(%w[ spec/1.rb spec/2.rb ]) + expect(cmd).to include("-rfoo -Ibar --warnings --backtrace") + end + + it 'replaces the locations from the original CLI args with the provided locations' do + cmd = command_for(%w[ spec/1.rb spec/2.rb ]) + expect(cmd).to match(%r{'?spec/1\.rb'? '?spec/2\.rb'?}).and exclude("spec/unit") + end + + it 'escapes locations' do + cmd = command_for(["path/with spaces/to/spec.rb"]) + if RSpec::Support::OS.windows? + expect(cmd).to include("'path/with spaces/to/spec.rb'") + else + expect(cmd).to include('path/with\ spaces/to/spec.rb') + end + end + + it "includes an option for the server's DRB port" do + cmd = command_for([]) + expect(cmd).to include("--drb-port #{server.drb_port}") + end + + it "ignores an existing --drb-port option (since we use the server's port instead)" do + original_cli_args << "--drb-port" << "9999" + cmd = command_for([]) + expect(cmd).to include("--drb-port #{server.drb_port}").and exclude("9999") + expect(cmd.scan("--drb-port").count).to eq(1) + end + + it 'ignores the `--bisect` option since that would infinitely recurse' do + original_cli_args << "--bisect" + cmd = command_for([]) + expect(cmd).to exclude("--bisect") + end + + it 'uses the bisect formatter' do + cmd = command_for([]) + expect(cmd).to include("--format bisect") + end + + def expect_formatters_to_be_excluded + cmd = command_for([]) + expect(cmd).to include("--format bisect").and exclude( + "progress", "html", "--out", "specs.html", "-f ", "-o " + ) + expect(cmd.scan("--format").count).to eq(1) + end + + it 'excludes any --format and matching --out options passed in the original args' do + original_cli_args.concat %w[ --format progress --format html --out specs.html ] + expect_formatters_to_be_excluded + end + + it 'excludes any -f and matching -o options passed in the original args' do + original_cli_args.concat %w[ -f progress -f html -o specs.html ] + expect_formatters_to_be_excluded + end + + it 'starts with the path to the current ruby executable' do + cmd = command_for([]) + expect(cmd).to start_with(File.join( + RbConfig::CONFIG['bindir'], + RbConfig::CONFIG['ruby_install_name'] + )) + end + + it 'includes the path to the rspec executable after the ruby executable' do + cmd = command_for([]) + expect(cmd).to first_include("ruby").then_include(RSpec::Core.path_to_executable) + end + + it 'escapes the rspec executable' do + allow(RSpec::Core).to receive(:path_to_executable).and_return("path/with spaces/rspec") + cmd = command_for([]) + + if RSpec::Support::OS.windows? + expect(cmd).to include("'path/with spaces/rspec'") + else + expect(cmd).to include('path/with\ spaces/rspec') + end + end + + it 'includes the current load path as an option to `ruby`, not as an option to `rspec`' do + cmd = command_for([], :load_path => %W[ lp/foo lp/bar ]) + if RSpec::Support::OS.windows? + expect(cmd).to first_include("-I'lp/foo':'lp/bar'").then_include(RSpec::Core.path_to_executable) + else + expect(cmd).to first_include("-Ilp/foo:lp/bar").then_include(RSpec::Core.path_to_executable) + end + end + + it 'escapes the load path entries' do + cmd = command_for([], :load_path => ['l p/foo', 'l p/bar' ]) + if RSpec::Support::OS.windows? + expect(cmd).to first_include("-I'l p/foo':'l p/bar'").then_include(RSpec::Core.path_to_executable) + else + expect(cmd).to first_include('-Il\ p/foo:l\ p/bar').then_include(RSpec::Core.path_to_executable) + end + end + end + + describe "#repro_command_from", :simulate_shell_allowing_unquoted_ids do + let(:original_cli_args) { %w[ spec/unit --seed 1234 ] } + + def repro_command_from(ids) + runner.repro_command_from(ids) + end + + it 'starts with `rspec #{example_ids}`' do + cmd = repro_command_from(%w[ ./spec/unit/1_spec.rb[1:1] ./spec/unit/2_spec.rb[1:1] ]) + expect(cmd).to start_with("rspec ./spec/unit/1_spec.rb[1:1] ./spec/unit/2_spec.rb[1:1]") + end + + it 'includes the original CLI args but excludes the original CLI locations' do + cmd = repro_command_from(%w[ ./spec/unit/1_spec.rb[1:1] ./spec/unit/2_spec.rb[1:1] ]) + expect(cmd).to include("--seed 1234").and exclude("spec/unit ") + end + + it 'includes original options that `command_for` excludes' do + original_cli_args << "--format" << "progress" + expect(runner.command_for(%w[ ./foo.rb[1:1] ])).to exclude("--format progress") + expect(repro_command_from(%w[ ./foo.rb[1:1] ])).to include("--format progress") + end + + it 'groups multiple ids for the same file together' do + cmd = repro_command_from(%w[ ./spec/unit/1_spec.rb[1:1] ./spec/unit/1_spec.rb[1:2] ]) + expect(cmd).to include("./spec/unit/1_spec.rb[1:1,1:2]") + end + + it 'prints the files in alphabetical order' do + cmd = repro_command_from(%w[ ./spec/unit/2_spec.rb[1:1] ./spec/unit/1_spec.rb[1:1] ]) + expect(cmd).to include("./spec/unit/1_spec.rb[1:1] ./spec/unit/2_spec.rb[1:1]") + end + + it 'prints ids from the same file in sequential order' do + cmd = repro_command_from(%w[ + ./spec/unit/1_spec.rb[2:1] + ./spec/unit/1_spec.rb[1:2] + ./spec/unit/1_spec.rb[1:1] + ./spec/unit/1_spec.rb[1:10] + ./spec/unit/1_spec.rb[1:9] + ]) + + expect(cmd).to include("./spec/unit/1_spec.rb[1:1,1:2,1:9,1:10,2:1]") + end + + it 'does not include `--bisect` even though the original args do' do + original_cli_args << "--bisect" + expect(repro_command_from(%w[ ./foo.rb[1:1] ])).to exclude("bisect") + end + + it 'quotes the ids on a shell like ZSH that requires it' do + with_env_vars 'SHELL' => '/usr/local/bin/zsh' do + expect(repro_command_from(%w[ ./foo.rb[1:1] ])).to include("'./foo.rb[1:1]'") + end + end + end + + describe "#original_results" do + let(:original_cli_args) { %w[spec/unit] } + + before do + allow(runner).to receive(:system) + allow(server).to receive(:capture_run_results) do |&block| + block.call + "the results" + end + end + + it "runs the suite with the locations from the original CLI args" do + runner.original_results + expect(runner).to have_received(:system).with(a_string_including("spec/unit")) + end + + it 'returns the run results' do + expect(runner.original_results).to eq("the results") + end + + it 'memoizes, since it is expensive to re-run the suite' do + expect(runner.original_results).to be(runner.original_results) + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 9c7032178b..d680c03770 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -90,6 +90,15 @@ def handle_current_dir_change c.raise_errors_for_deprecations! c.color = true c.include CommonHelpers + + c.expect_with :rspec do |expectations| + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + c.around(:example, :simulate_shell_allowing_unquoted_ids) do |ex| + with_env_vars('SHELL' => '/usr/local/bin/bash', &ex) + end + c.filter_run_excluding :ruby => lambda {|version| case version.to_s when "!jruby" diff --git a/spec/support/matchers.rb b/spec/support/matchers.rb index b83b9789ca..da25778741 100644 --- a/spec/support/matchers.rb +++ b/spec/support/matchers.rb @@ -107,6 +107,16 @@ def failure_reason(example) failure_message_when_negated { contain_exactly_matcher.failure_message_when_negated } end +RSpec::Matchers.define :first_include do |first_snippet| + chain :then_include, :second_snippet + + match do |string| + string.include?(first_snippet) && + string.include?(second_snippet) && + string.index(first_snippet) < string.index(second_snippet) + end +end + RSpec::Matchers.alias_matcher :a_file_collection, :contain_files RSpec::Matchers.define_negated_matcher :avoid_outputting, :output From d1eafe401a231d514e15508ae311d7e879c508a3 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Sun, 22 Mar 2015 23:48:57 -0700 Subject: [PATCH 133/258] Add a SubsetEnumerator for bisect. --- lib/rspec/core/bisect/subset_enumerator.rb | 34 ++++++++++++++ .../core/bisect/subset_enumerator_spec.rb | 47 +++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 lib/rspec/core/bisect/subset_enumerator.rb create mode 100644 spec/rspec/core/bisect/subset_enumerator_spec.rb diff --git a/lib/rspec/core/bisect/subset_enumerator.rb b/lib/rspec/core/bisect/subset_enumerator.rb new file mode 100644 index 0000000000..7bdfa19563 --- /dev/null +++ b/lib/rspec/core/bisect/subset_enumerator.rb @@ -0,0 +1,34 @@ +module RSpec + module Core + module Bisect + # Enumerates each subset of the given list of ids that is half the + # size of the total list, so that hopefully we can discard half the + # list each repeatedly in order to our minimal repro case. + class SubsetEnumerator + include Enumerable + + def initialize(ids) + @ids = ids + end + + def each + yielded = Set.new + slice_size = (@ids.size / 2.0).ceil + combo_count = 1 + + while slice_size > 0 + @ids.each_slice(slice_size).to_a.combination(combo_count) do |combos| + subset = combos.flatten + next if yielded.include?(subset) + yield subset + yielded << subset + end + + slice_size /= 2 + combo_count *= 2 + end + end + end + end + end +end diff --git a/spec/rspec/core/bisect/subset_enumerator_spec.rb b/spec/rspec/core/bisect/subset_enumerator_spec.rb new file mode 100644 index 0000000000..7f612e1a98 --- /dev/null +++ b/spec/rspec/core/bisect/subset_enumerator_spec.rb @@ -0,0 +1,47 @@ +require 'rspec/core/bisect/subset_enumerator' + +module RSpec::Core + RSpec.describe Bisect::SubsetEnumerator do + def enum_for(ids) + Bisect::SubsetEnumerator.new(ids) + end + + it 'is enumerable' do + expect(enum_for([])).to be_an(Enumerable) + end + + it 'systematically enumerates each subset of the given size, starting off with disjoint sets' do + ids = %w[ 1 2 3 4 5 6 7 8 ] + enum = enum_for(ids) + combos = enum.to_a + expect(combos).to start_with([ + # start with each half... + %w[ 1 2 3 4 ], %w[ 5 6 7 8 ], + # then cut in 4ths and combine those in all the unseen combos... + %w[ 1 2 5 6 ], %w[ 1 2 7 8 ], + %w[ 3 4 5 6 ], %w[ 3 4 7 8 ], + # then cut in 8ths and do the same... + %w[ 1 2 3 5 ], %w[ 1 2 3 6 ], %w[ 1 2 3 7 ], %w[ 1 2 3 8 ], + %w[ 1 2 4 5 ], %w[ 1 2 4 6 ], %w[ 1 2 4 7 ], %w[ 1 2 4 8 ] + ]) + + # We don't care to specify the rest of the order, but we care that all combos were hit. + expect(combos).to match_array(ids.combination(4)) + end + + it 'works with a list size that is not a power of 2' do + ids = %w[ 1 2 3 4 5 6 7 ] + enum = enum_for(ids) + combos = enum.to_a + expect(combos).to start_with([ + %w[ 1 2 3 4 ], %w[ 5 6 7 ], + %w[ 1 2 5 6 ], %w[ 1 2 7 ], + %w[ 3 4 5 6 ], %w[ 3 4 7 ] + ]) + + # Would be better to do: expect(combos).to match_array(ids.combination(4)) + # ...but we include a few extra sets of 3 due to our algorithm. + expect(combos).to include(*ids.combination(4)) + end + end +end From 670fd13f4753cdcbb6ed644dd5b8cfb6b4a4a8f3 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Sun, 22 Mar 2015 22:06:44 -0700 Subject: [PATCH 134/258] Add ExampleMinimizer. --- lib/rspec/core/bisect/example_minimizer.rb | 55 +++++++++++++++++++ .../core/bisect/example_minimizer_spec.rb | 34 ++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 lib/rspec/core/bisect/example_minimizer.rb create mode 100644 spec/rspec/core/bisect/example_minimizer_spec.rb diff --git a/lib/rspec/core/bisect/example_minimizer.rb b/lib/rspec/core/bisect/example_minimizer.rb new file mode 100644 index 0000000000..e5901c75e4 --- /dev/null +++ b/lib/rspec/core/bisect/example_minimizer.rb @@ -0,0 +1,55 @@ +RSpec::Support.require_rspec_core "bisect/subset_enumerator" + +module RSpec + module Core + module Bisect + # @private + # Contains the core bisect logic. Searches for examples we can ignore by + # repeatedly running different subsets of the suite. + class ExampleMinimizer + attr_reader :runner, :all_example_ids_in_execution_order, :failed_example_ids + + def initialize(runner) + @runner = runner + @all_example_ids_in_execution_order = runner.original_results.all_example_ids_in_execution_order + @failed_example_ids = runner.original_results.failed_example_ids + end + + def find_minimal_repro + remaining_ids = all_example_ids_in_execution_order - failed_example_ids + debug 0, "Initial failed_example_ids: #{failed_example_ids}" + debug 0, "Initial remaining_ids: #{remaining_ids}" + + loop do + ids_to_ignore = SubsetEnumerator.new(remaining_ids).find do |ids| + get_same_failures?(remaining_ids - ids) + end + + break unless ids_to_ignore + remaining_ids -= ids_to_ignore + debug 1, "Removed #{ids_to_ignore}; remaining_ids: #{remaining_ids}" + end + + remaining_ids + failed_example_ids + end + + private + + def get_same_failures?(ids) + results = runner.run(ids + failed_example_ids) + (results.failed_example_ids == failed_example_ids).tap do |same| + if same + debug 2, "Running with #{ids}, got same failures" + else + debug 2, "Running with #{ids}, got different failures: #{results.failed_example_ids}" + end + end + end + + def debug(level, msg) + puts "#{' ' * level}Minimizer: #{msg}" if ENV['DEBUG_RSPEC_BISECT'] + end + end + end + end +end diff --git a/spec/rspec/core/bisect/example_minimizer_spec.rb b/spec/rspec/core/bisect/example_minimizer_spec.rb new file mode 100644 index 0000000000..2a21d15613 --- /dev/null +++ b/spec/rspec/core/bisect/example_minimizer_spec.rb @@ -0,0 +1,34 @@ +require 'rspec/core/bisect/example_minimizer' +require 'rspec/core/formatters/bisect_formatter' + +module RSpec::Core + RSpec.describe Bisect::ExampleMinimizer do + RunResults = Formatters::BisectFormatter::RunResults + + FakeRunner = Struct.new(:all_ids, :always_failures, :dependent_failures) do + def original_results + RunResults.new(all_ids, always_failures | dependent_failures.keys) + end + + def run(ids) + failures = ids & always_failures + dependent_failures.each do |failing_example, depends_upon| + failures << failing_example if ids.include?(depends_upon) + end + + RunResults.new(ids, failures) + end + end + + it 'repeatedly runs various subsets of the suite, removing examples that have no effect on the failing examples' do + minimizer = Bisect::ExampleMinimizer.new(FakeRunner.new( + %w[ ex_1 ex_2 ex_3 ex_4 ex_5 ex_6 ex_7 ex_8 ], + %w[ ex_2 ], + { "ex_5" => "ex_4" } + )) + + ids = minimizer.find_minimal_repro + expect(ids).to match_array(%w[ ex_2 ex_4 ex_5 ]) + end + end +end From 5981247ed50b932c94361e368072c72dc975526d Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Tue, 24 Mar 2015 08:04:01 -0700 Subject: [PATCH 135/258] Get `--bisect` to work end-to-end. --- features/command_line/bisect.feature | 48 ++++++++++++ .../step_definitions/additional_cli_steps.rb | 14 ++++ features/support/env.rb | 3 + lib/rspec/core/bisect/coordinator.rb | 49 ++++++++++++ lib/rspec/core/bisect/example_minimizer.rb | 75 +++++++++++++++++-- lib/rspec/core/bisect/runner.rb | 2 + lib/rspec/core/bisect/subset_enumerator.rb | 7 +- .../formatters/bisect_progress_formatter.rb | 57 ++++++++++++++ lib/rspec/core/option_parser.rb | 7 ++ lib/rspec/core/reporter.rb | 29 ++++--- spec/integration/bisect_spec.rb | 30 ++++++++ .../core/bisect/example_minimizer_spec.rb | 6 +- .../core/resources/order_dependent_specs.rb | 29 +++++++ spec/support/formatter_support.rb | 6 +- 14 files changed, 340 insertions(+), 22 deletions(-) create mode 100644 features/command_line/bisect.feature create mode 100644 lib/rspec/core/bisect/coordinator.rb create mode 100644 lib/rspec/core/formatters/bisect_progress_formatter.rb create mode 100644 spec/integration/bisect_spec.rb create mode 100644 spec/rspec/core/resources/order_dependent_specs.rb diff --git a/features/command_line/bisect.feature b/features/command_line/bisect.feature new file mode 100644 index 0000000000..d3b429daf4 --- /dev/null +++ b/features/command_line/bisect.feature @@ -0,0 +1,48 @@ +Feature: Bisect + + RSpec's `--order random` and `--seed` options help surface flickering examples that only fail when one or more other examples are executed first. It can be very difficult to isolate the exact combination of examples that triggers the failure. The `--bisect` flag helps solve that problem. + + Pass the `--bisect` option (in addition to `--seed` any other options) and RSpec will repeatedly run subsets of your suite in order to isolate the minimal set of examples that reproduce the failure. + + Scenario: Use `--bisect` flag to create a minimal repro case for the ordering dependency + Given a file named "lib/calculator.rb" with: + """ruby + class Calculator + def self.add(x, y) + x + y + end + end + """ + And a file named "spec/calculator_1_spec.rb" with: + """ruby + require 'calculator' + + RSpec.describe "Calculator" do + it 'adds numbers' do + expect(Calculator.add(1, 2)).to eq(3) + end + end + """ + And files "spec/calculator_2_spec.rb" through "spec/calculator_9_spec.rb" with an unrelated passing spec in each file + And a file named "spec/calculator_10_spec.rb" with: + """ruby + require 'calculator' + + RSpec.describe "Monkey patched Calculator" do + it 'does screwy math' do + # monkey patching `Calculator` affects examples that are + # executed after this one! + def Calculator.add(x, y) + x - y + end + + expect(Calculator.add(5, 10)).to eq(-5) + end + end + """ + When I run `rspec --seed 1234` + Then the output should contain "10 examples, 1 failure" + When I run `rspec --seed 1234 --bisect` + Then the output should contain "rspec ./spec/calculator_10_spec.rb[1:1] ./spec/calculator_1_spec.rb[1:1] --seed 1234" + When I run `rspec ./spec/calculator_10_spec.rb[1:1] ./spec/calculator_1_spec.rb[1:1] --seed 1234` + Then the output should contain "2 examples, 1 failure" diff --git a/features/step_definitions/additional_cli_steps.rb b/features/step_definitions/additional_cli_steps.rb index 1f58a4e5a0..36fa09eab3 100644 --- a/features/step_definitions/additional_cli_steps.rb +++ b/features/step_definitions/additional_cli_steps.rb @@ -150,3 +150,17 @@ File.open("spec/spec_helper.rb", "w") { |f| f.write("") } end end + +Given(/^files "(.*?)" through "(.*?)" with an unrelated passing spec in each file$/) do |file1, file2| + index_1 = Integer(file1[/\d+/]) + index_2 = Integer(file2[/\d+/]) + pattern = file1.sub(/\d+/, '%s') + + index_1.upto(index_2) do |index| + write_file(pattern % index, <<-EOS) + RSpec.describe "Spec file #{index}" do + example { } + end + EOS + end +end diff --git a/features/support/env.rb b/features/support/env.rb index 36b8026e6d..e31479aa5d 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -1,6 +1,9 @@ require 'aruba/cucumber' Before do + # Force ids to be printed unquoted for consistency + set_env('SHELL', '/usr/bin/bash') + if RUBY_PLATFORM =~ /java/ || defined?(Rubinius) @aruba_timeout_seconds = 60 else diff --git a/lib/rspec/core/bisect/coordinator.rb b/lib/rspec/core/bisect/coordinator.rb new file mode 100644 index 0000000000..817c2b796f --- /dev/null +++ b/lib/rspec/core/bisect/coordinator.rb @@ -0,0 +1,49 @@ +RSpec::Support.require_rspec_core "bisect/server" +RSpec::Support.require_rspec_core "bisect/runner" +RSpec::Support.require_rspec_core "bisect/example_minimizer" +RSpec::Support.require_rspec_core "formatters/bisect_progress_formatter" + +module RSpec + module Core + module Bisect + # @private + # The main entry point into the bisect logic. Coordinates among: + # - Bisect::Server: Receives suite results. + # - Bisect::Runner: Runs a set of examples and directs the results + # to the server. + # - Bisect::ExampleMinimizer: Contains the core bisect logic. + # - Formatters::BisectProgressFormatter: provides progress updates + # to the user. + class Coordinator + def self.bisect_with(original_cli_args, configuration) + new(original_cli_args, configuration).bisect + end + + def initialize(original_cli_args, configuration) + @original_cli_args = original_cli_args + @configuration = configuration + end + + def bisect + @configuration.add_formatter Formatters::BisectProgressFormatter + + reporter.close_after do + repro = Server.run do |server| + runner = Runner.new(server, @original_cli_args) + minimizer = ExampleMinimizer.new(runner, reporter) + runner.repro_command_from(minimizer.find_minimal_repro) + end + + reporter.publish(:bisect_repro_command, :repro => repro) + end + end + + private + + def reporter + @configuration.reporter + end + end + end + end +end diff --git a/lib/rspec/core/bisect/example_minimizer.rb b/lib/rspec/core/bisect/example_minimizer.rb index e5901c75e4..adae3f604e 100644 --- a/lib/rspec/core/bisect/example_minimizer.rb +++ b/lib/rspec/core/bisect/example_minimizer.rb @@ -7,25 +7,27 @@ module Bisect # Contains the core bisect logic. Searches for examples we can ignore by # repeatedly running different subsets of the suite. class ExampleMinimizer - attr_reader :runner, :all_example_ids_in_execution_order, :failed_example_ids + attr_reader :runner, :reporter, :all_example_ids_in_execution_order, :failed_example_ids - def initialize(runner) - @runner = runner - @all_example_ids_in_execution_order = runner.original_results.all_example_ids_in_execution_order - @failed_example_ids = runner.original_results.failed_example_ids + def initialize(runner, reporter) + @runner = runner + @reporter = reporter end def find_minimal_repro + prep + remaining_ids = all_example_ids_in_execution_order - failed_example_ids debug 0, "Initial failed_example_ids: #{failed_example_ids}" debug 0, "Initial remaining_ids: #{remaining_ids}" - loop do - ids_to_ignore = SubsetEnumerator.new(remaining_ids).find do |ids| + each_bisect_round(lambda { remaining_ids }) do |subsets| + ids_to_ignore = subsets.find do |ids| get_same_failures?(remaining_ids - ids) end - break unless ids_to_ignore + next :done unless ids_to_ignore + remaining_ids -= ids_to_ignore debug 1, "Removed #{ids_to_ignore}; remaining_ids: #{remaining_ids}" end @@ -35,8 +37,28 @@ def find_minimal_repro private + def prep + notify(:bisect_starting, :original_cli_args => runner.original_cli_args) + + _, duration = track_duration do + original_results = runner.original_results + @all_example_ids_in_execution_order = original_results.all_example_ids_in_execution_order + @failed_example_ids = original_results.failed_example_ids + end + + notify(:original_bisect_run_complete, :failures => failed_example_ids.size, + :non_failures => non_failing_example_ids.size, + :duration => duration) + end + + def non_failing_example_ids + @non_failing_example_ids ||= all_example_ids_in_execution_order - failed_example_ids + end + def get_same_failures?(ids) results = runner.run(ids + failed_example_ids) + notify(:individual_run_complete) + (results.failed_example_ids == failed_example_ids).tap do |same| if same debug 2, "Running with #{ids}, got same failures" @@ -49,6 +71,43 @@ def get_same_failures?(ids) def debug(level, msg) puts "#{' ' * level}Minimizer: #{msg}" if ENV['DEBUG_RSPEC_BISECT'] end + + INFINITY = (1.0 / 0) # 1.8.7 doesn't define Float::INFINITY so we define our own... + + def each_bisect_round(get_remaining_ids, &block) + last_round, duration = track_duration do + 1.upto(INFINITY) do |round| + break if :done == bisect_round(round, get_remaining_ids.call, &block) + end + end + + notify(:bisect_complete, :round => last_round, :duration => duration, + :original_non_failing_count => non_failing_example_ids.size, + :remaining_count => get_remaining_ids.call.size) + end + + def bisect_round(round, remaining_ids) + value, duration = track_duration do + subsets = SubsetEnumerator.new(remaining_ids) + notify(:bisect_round_started, :round => round, + :subset_size => subsets.subset_size, + :remaining_count => remaining_ids.size) + + yield subsets + end + + notify(:bisect_round_finished, :duration => duration) + value + end + + def track_duration + start = ::RSpec::Core::Time.now + [yield, ::RSpec::Core::Time.now - start] + end + + def notify(*args) + reporter.publish(*args) + end end end end diff --git a/lib/rspec/core/bisect/runner.rb b/lib/rspec/core/bisect/runner.rb index cd9c68767d..ee8074ff9a 100644 --- a/lib/rspec/core/bisect/runner.rb +++ b/lib/rspec/core/bisect/runner.rb @@ -9,6 +9,8 @@ module Bisect class Runner include RSpec::Core::ShellEscape + attr_reader :original_cli_args + def initialize(server, original_cli_args) @server = server @original_cli_args = original_cli_args - ["--bisect"] diff --git a/lib/rspec/core/bisect/subset_enumerator.rb b/lib/rspec/core/bisect/subset_enumerator.rb index 7bdfa19563..7dc52cf88d 100644 --- a/lib/rspec/core/bisect/subset_enumerator.rb +++ b/lib/rspec/core/bisect/subset_enumerator.rb @@ -4,6 +4,7 @@ module Bisect # Enumerates each subset of the given list of ids that is half the # size of the total list, so that hopefully we can discard half the # list each repeatedly in order to our minimal repro case. + # @private class SubsetEnumerator include Enumerable @@ -11,9 +12,13 @@ def initialize(ids) @ids = ids end + def subset_size + @subset_size ||= (@ids.size / 2.0).ceil + end + def each yielded = Set.new - slice_size = (@ids.size / 2.0).ceil + slice_size = subset_size combo_count = 1 while slice_size > 0 diff --git a/lib/rspec/core/formatters/bisect_progress_formatter.rb b/lib/rspec/core/formatters/bisect_progress_formatter.rb new file mode 100644 index 0000000000..5d9f2cf1d8 --- /dev/null +++ b/lib/rspec/core/formatters/bisect_progress_formatter.rb @@ -0,0 +1,57 @@ +RSpec::Support.require_rspec_core "formatters/base_text_formatter" + +module RSpec + module Core + module Formatters + # @private + # Produces progress output while bisecting. + class BisectProgressFormatter < BaseTextFormatter + Formatters.register self, :bisect_starting, :original_bisect_run_complete, + :bisect_round_started, :individual_run_complete, + :bisect_round_finished, :bisect_complete, :bisect_repro_command + + def bisect_starting(notification) + options = notification.original_cli_args.join(' ') + output.puts "Bisect started using options: #{options.inspect}" + output.print "Running suite to find failures..." + end + + def original_bisect_run_complete(notification) + failures = Helpers.pluralize(notification.failures, "failed example") + non_failures = Helpers.pluralize(notification.non_failures, "non-failing example") + + output.puts " (#{Helpers.format_duration(notification.duration)})" + output.puts "Starting bisect with #{failures} and #{non_failures}." + end + + def bisect_round_started(notification) + search_desc = Helpers.pluralize( + notification.subset_size, "non-failing example" + ) + + output.print "\nRound #{notification.round}: searching for #{search_desc}" \ + " (of #{notification.remaining_count}) to ignore: " + end + + def bisect_round_finished(notification) + output.print " (#{Helpers.format_duration(notification.duration)})" + end + + def individual_run_complete(_) + output.print '.' + end + + def bisect_complete(notification) + output.puts "\nBisect complete! Reduced necessary non-failing examples " \ + "from #{notification.original_non_failing_count} to " \ + "#{notification.remaining_count} in " \ + "#{Helpers.format_duration(notification.duration)}." + end + + def bisect_repro_command(notification) + output.puts "\nThe minimal reproduction command is:\n #{notification.repro}" + end + end + end + end +end diff --git a/lib/rspec/core/option_parser.rb b/lib/rspec/core/option_parser.rb index 23f3ae8fb3..6950be24d8 100644 --- a/lib/rspec/core/option_parser.rb +++ b/lib/rspec/core/option_parser.rb @@ -62,6 +62,13 @@ def parser(options) options[:order] = "rand:#{seed}" end + parser.on('--bisect', 'Repeatedly runs the suite in order to isolates the failures to the ', + ' smallest reproducible case.') do + RSpec::Support.require_rspec_core "bisect/coordinator" + Bisect::Coordinator.bisect_with(original_args, RSpec.configuration) + exit + end + parser.on('--[no-]fail-fast', 'Abort the run on first failure.') do |value| set_fail_fast(options, value) end diff --git a/lib/rspec/core/reporter.rb b/lib/rspec/core/reporter.rb index 5c430cfa3c..fa08618132 100644 --- a/lib/rspec/core/reporter.rb +++ b/lib/rspec/core/reporter.rb @@ -141,18 +141,25 @@ def deprecation(hash) # @private def finish - stop - notify :start_dump, Notifications::NullNotification - notify :dump_pending, Notifications::ExamplesNotification.new(self) - notify :dump_failures, Notifications::ExamplesNotification.new(self) - notify :deprecation_summary, Notifications::NullNotification - unless mute_profile_output? - notify :dump_profile, Notifications::ProfileNotification.new(@duration, @examples, - @configuration.profile_examples) + close_after do + stop + notify :start_dump, Notifications::NullNotification + notify :dump_pending, Notifications::ExamplesNotification.new(self) + notify :dump_failures, Notifications::ExamplesNotification.new(self) + notify :deprecation_summary, Notifications::NullNotification + unless mute_profile_output? + notify :dump_profile, Notifications::ProfileNotification.new(@duration, @examples, + @configuration.profile_examples) + end + notify :dump_summary, Notifications::SummaryNotification.new(@duration, @examples, @failed_examples, + @pending_examples, @load_time) + notify :seed, Notifications::SeedNotification.new(@configuration.seed, seed_used?) end - notify :dump_summary, Notifications::SummaryNotification.new(@duration, @examples, @failed_examples, - @pending_examples, @load_time) - notify :seed, Notifications::SeedNotification.new(@configuration.seed, seed_used?) + end + + # @private + def close_after + yield ensure close end diff --git a/spec/integration/bisect_spec.rb b/spec/integration/bisect_spec.rb new file mode 100644 index 0000000000..2a1f9586b2 --- /dev/null +++ b/spec/integration/bisect_spec.rb @@ -0,0 +1,30 @@ +module RSpec::Core + RSpec.describe "Bisect", :slow, :simulate_shell_allowing_unquoted_ids do + include FormatterSupport + + it 'finds the minimum rerun command and exits' do + RSpec.configuration.output_stream = out = StringIO.new + parser = Parser.new(%w[spec/rspec/core/resources/order_dependent_specs.rb --order defined --bisect]) + expect(parser).to receive(:exit) + + parser.parse + + output = normalize_durations(out.string) + expect(output).to eq(<<-EOS.gsub(/^\s+\|/, '')) + |Bisect started using options: "spec/rspec/core/resources/order_dependent_specs.rb --order defined" + |Running suite to find failures... (n.nnnn seconds) + |Starting bisect with 1 failed example and 21 non-failing examples. + | + |Round 1: searching for 11 non-failing examples (of 21) to ignore: .. (n.nnnn seconds) + |Round 2: searching for 6 non-failing examples (of 11) to ignore: . (n.nnnn seconds) + |Round 3: searching for 3 non-failing examples (of 5) to ignore: . (n.nnnn seconds) + |Round 4: searching for 1 non-failing example (of 2) to ignore: . (n.nnnn seconds) + |Round 5: searching for 1 non-failing example (of 1) to ignore: . (n.nnnn seconds) + |Bisect complete! Reduced necessary non-failing examples from 21 to 1 in n.nnnn seconds. + | + |The minimal reproduction command is: + | rspec ./spec/rspec/core/resources/order_dependent_specs.rb[11:1,22:1] --order defined + EOS + end + end +end diff --git a/spec/rspec/core/bisect/example_minimizer_spec.rb b/spec/rspec/core/bisect/example_minimizer_spec.rb index 2a21d15613..1238ca709a 100644 --- a/spec/rspec/core/bisect/example_minimizer_spec.rb +++ b/spec/rspec/core/bisect/example_minimizer_spec.rb @@ -6,6 +6,10 @@ module RSpec::Core RunResults = Formatters::BisectFormatter::RunResults FakeRunner = Struct.new(:all_ids, :always_failures, :dependent_failures) do + def original_cli_args + [] + end + def original_results RunResults.new(all_ids, always_failures | dependent_failures.keys) end @@ -25,7 +29,7 @@ def run(ids) %w[ ex_1 ex_2 ex_3 ex_4 ex_5 ex_6 ex_7 ex_8 ], %w[ ex_2 ], { "ex_5" => "ex_4" } - )) + ), RSpec::Core::NullReporter) ids = minimizer.find_minimal_repro expect(ids).to match_array(%w[ ex_2 ex_4 ex_5 ]) diff --git a/spec/rspec/core/resources/order_dependent_specs.rb b/spec/rspec/core/resources/order_dependent_specs.rb new file mode 100644 index 0000000000..b3f6649d11 --- /dev/null +++ b/spec/rspec/core/resources/order_dependent_specs.rb @@ -0,0 +1,29 @@ +# Deliberately named _specs.rb to avoid being loaded except when specified + +$global_state_for_bisect_specs = {} + +10.times do |i| + RSpec.describe "Group 1-#{i}" do + it "passes" do + end + end +end + +RSpec.describe "Group 2" do + it "passes" do + $global_state_for_bisect_specs[:foo] = 1 + end +end + +10.times do |i| + RSpec.describe "Group 3-#{i}" do + it "passes" do + end + end +end + +RSpec.describe "Group 4" do + it "fails" do + expect($global_state_for_bisect_specs).to eq({}) + end +end diff --git a/spec/support/formatter_support.rb b/spec/support/formatter_support.rb index 0e66e80d32..91ff0ed424 100644 --- a/spec/support/formatter_support.rb +++ b/spec/support/formatter_support.rb @@ -14,7 +14,7 @@ def run_example_specs_with_formatter(formatter_option, normalize_output=true) output = out.string return output unless normalize_output - output.gsub!(/\d+(?:\.\d+)?(s| seconds)/, "n.nnnn\\1") + output = normalize_durations(output) caller_line = RSpec::Core::Metadata.relative_path(caller.first) output.lines.reject do |line| @@ -30,6 +30,10 @@ def run_example_specs_with_formatter(formatter_option, normalize_output=true) end.join end + def normalize_durations(output) + output.gsub(/\d+(?:\.\d+)?(s| seconds)/, "n.nnnn\\1") + end + if RUBY_VERSION.to_f < 1.9 def expected_summary_output_for_example_specs <<-EOS.gsub(/^\s+\|/, '').chomp From c0022584c905fb299de1aa51f18743103ab1c6af Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Tue, 24 Mar 2015 20:30:48 -0700 Subject: [PATCH 136/258] Rename attribute. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The “in execution order” part isn’t actually important for how we are using it, and makes the name unnecessarily long. --- lib/rspec/core/bisect/example_minimizer.rb | 10 +++++----- lib/rspec/core/formatters/bisect_formatter.rb | 3 +-- spec/rspec/core/bisect/server_spec.rb | 4 ++-- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/lib/rspec/core/bisect/example_minimizer.rb b/lib/rspec/core/bisect/example_minimizer.rb index adae3f604e..bb19ef7673 100644 --- a/lib/rspec/core/bisect/example_minimizer.rb +++ b/lib/rspec/core/bisect/example_minimizer.rb @@ -7,7 +7,7 @@ module Bisect # Contains the core bisect logic. Searches for examples we can ignore by # repeatedly running different subsets of the suite. class ExampleMinimizer - attr_reader :runner, :reporter, :all_example_ids_in_execution_order, :failed_example_ids + attr_reader :runner, :reporter, :all_example_ids, :failed_example_ids def initialize(runner, reporter) @runner = runner @@ -17,7 +17,7 @@ def initialize(runner, reporter) def find_minimal_repro prep - remaining_ids = all_example_ids_in_execution_order - failed_example_ids + remaining_ids = all_example_ids - failed_example_ids debug 0, "Initial failed_example_ids: #{failed_example_ids}" debug 0, "Initial remaining_ids: #{remaining_ids}" @@ -41,8 +41,8 @@ def prep notify(:bisect_starting, :original_cli_args => runner.original_cli_args) _, duration = track_duration do - original_results = runner.original_results - @all_example_ids_in_execution_order = original_results.all_example_ids_in_execution_order + original_results = runner.original_results + @all_example_ids = original_results.all_example_ids @failed_example_ids = original_results.failed_example_ids end @@ -52,7 +52,7 @@ def prep end def non_failing_example_ids - @non_failing_example_ids ||= all_example_ids_in_execution_order - failed_example_ids + @non_failing_example_ids ||= all_example_ids - failed_example_ids end def get_same_failures?(ids) diff --git a/lib/rspec/core/formatters/bisect_formatter.rb b/lib/rspec/core/formatters/bisect_formatter.rb index 71a7e922ed..4e08b406b0 100644 --- a/lib/rspec/core/formatters/bisect_formatter.rb +++ b/lib/rspec/core/formatters/bisect_formatter.rb @@ -51,8 +51,7 @@ def start_dump(_notification) ) end - RunResults = Struct.new(:all_example_ids_in_execution_order, - :failed_example_ids) + RunResults = Struct.new(:all_example_ids, :failed_example_ids) private diff --git a/spec/rspec/core/bisect/server_spec.rb b/spec/rspec/core/bisect/server_spec.rb index fc94d81585..683abdab82 100644 --- a/spec/rspec/core/bisect/server_spec.rb +++ b/spec/rspec/core/bisect/server_spec.rb @@ -49,7 +49,7 @@ def run_formatter_specs end expect(results).to have_attributes( - :all_example_ids_in_execution_order => %w[ + :all_example_ids => %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] @@ -73,7 +73,7 @@ def run_formatter_specs end expect(results).to have_attributes( - :all_example_ids_in_execution_order => %w[ + :all_example_ids => %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] From 32045fd6633d94c772e0a133e79298f462311f0a Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Tue, 24 Mar 2015 21:40:48 -0700 Subject: [PATCH 137/258] When bisecting, exit each run as soon as possible. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - When an expected failure passes (or is pending) we don’t care about any other results. - When the last expected failure finishes, we don’t care about any later examples. --- lib/rspec/core/bisect/runner.rb | 12 ++- lib/rspec/core/bisect/server.rb | 6 +- lib/rspec/core/formatters/bisect_formatter.rb | 17 +++-- lib/rspec/core/set.rb | 8 ++ spec/rspec/core/bisect/runner_spec.rb | 22 ++++++ spec/rspec/core/bisect/server_spec.rb | 73 +++++++++++++++---- spec/rspec/core/set_spec.rb | 13 ++++ 7 files changed, 122 insertions(+), 29 deletions(-) diff --git a/lib/rspec/core/bisect/runner.rb b/lib/rspec/core/bisect/runner.rb index ee8074ff9a..2def69d16b 100644 --- a/lib/rspec/core/bisect/runner.rb +++ b/lib/rspec/core/bisect/runner.rb @@ -17,9 +17,7 @@ def initialize(server, original_cli_args) end def run(locations) - @server.capture_run_results do - system command_for(locations) - end + run_locations(locations, original_results.failed_example_ids) end def command_for(locations) @@ -47,11 +45,17 @@ def repro_command_from(locations) end def original_results - @original_results ||= run(original_locations) + @original_results ||= run_locations(original_locations) end private + def run_locations(locations, *capture_args) + @server.capture_run_results(*capture_args) do + system command_for(locations) + end + end + def reusable_cli_options @reusable_cli_options ||= begin opts = original_cli_args_without_locations diff --git a/lib/rspec/core/bisect/server.rb b/lib/rspec/core/bisect/server.rb index a24771c677..35734a591e 100644 --- a/lib/rspec/core/bisect/server.rb +++ b/lib/rspec/core/bisect/server.rb @@ -16,8 +16,8 @@ def self.run server.stop end - def capture_run_results(abort_after_example_id=nil) - self.abort_after_example_id = abort_after_example_id + def capture_run_results(expected_failures=[]) + self.expected_failures = expected_failures yield latest_run_results end @@ -36,7 +36,7 @@ def drb_port end # Fetched via DRb by the BisectFormatter to determine when to abort. - attr_accessor :abort_after_example_id + attr_accessor :expected_failures # Set via DRb by the BisectFormatter with the results of the run. attr_accessor :latest_run_results diff --git a/lib/rspec/core/formatters/bisect_formatter.rb b/lib/rspec/core/formatters/bisect_formatter.rb index 4e08b406b0..3f404d040c 100644 --- a/lib/rspec/core/formatters/bisect_formatter.rb +++ b/lib/rspec/core/formatters/bisect_formatter.rb @@ -21,11 +21,11 @@ def initialize(_output) @all_example_ids = [] @failed_example_ids = [] @bisect_server = DRbObject.new_with_uri(drb_uri) - @abort_after_id = nil + @remaining_failures = [] end def start(_notification) - @abort_after_id = @bisect_server.abort_after_example_id + @remaining_failures = Set.new(@bisect_server.expected_failures) end def example_started(notification) @@ -34,15 +34,15 @@ def example_started(notification) def example_failed(notification) @failed_example_ids << notification.example.id - example_finished(notification) + example_finished(notification, :failed) end def example_passed(notification) - example_finished(notification) + example_finished(notification, :passed) end def example_pending(notification) - example_finished(notification) + example_finished(notification, :pending) end def start_dump(_notification) @@ -55,8 +55,11 @@ def start_dump(_notification) private - def example_finished(notification) - return unless notification.example.id == @abort_after_id + def example_finished(notification, status) + return unless @remaining_failures.include?(notification.example.id) + @remaining_failures.delete(notification.example.id) + + return if status == :failed && !@remaining_failures.empty? RSpec.world.wants_to_quit = true end end diff --git a/lib/rspec/core/set.rb b/lib/rspec/core/set.rb index 19af219669..359199ae53 100644 --- a/lib/rspec/core/set.rb +++ b/lib/rspec/core/set.rb @@ -16,11 +16,19 @@ def initialize(array=[]) merge(array) end + def empty? + @values.empty? + end + def <<(key) @values[key] = true self end + def delete(key) + @values.delete(key) + end + def each(&block) @values.keys.each(&block) self diff --git a/spec/rspec/core/bisect/runner_spec.rb b/spec/rspec/core/bisect/runner_spec.rb index 7f60fc2f62..0a4b81a43a 100644 --- a/spec/rspec/core/bisect/runner_spec.rb +++ b/spec/rspec/core/bisect/runner_spec.rb @@ -1,10 +1,32 @@ require 'rspec/core/bisect/runner' +require 'rspec/core/formatters/bisect_formatter' module RSpec::Core RSpec.describe Bisect::Runner do let(:server) { instance_double("RSpec::Core::Bisect::Server", :drb_port => 1234) } let(:runner) { described_class.new(server, original_cli_args) } + describe "#run" do + let(:original_cli_args) { %w[ spec/1_spec.rb ] } + + it "passes the failed examples from the original run as the expected failures so the runs can abort early" do + original_results = Formatters::BisectFormatter::RunResults.new( + [], %w[ spec/failure_spec.rb[1:1] spec/failure_spec.rb[1:2] ] + ) + + expect(server).to receive(:capture_run_results). + with(no_args). + ordered. + and_return(original_results) + + expect(server).to receive(:capture_run_results). + with(original_results.failed_example_ids). + ordered + + runner.run(%w[ spec/1_spec.rb[1:1] spec/1_spec.rb[1:2] ]) + end + end + describe "#command_for" do def command_for(locations, options={}) load_path = options.fetch(:load_path) { [] } diff --git a/spec/rspec/core/bisect/server_spec.rb b/spec/rspec/core/bisect/server_spec.rb index 683abdab82..757f1c306d 100644 --- a/spec/rspec/core/bisect/server_spec.rb +++ b/spec/rspec/core/bisect/server_spec.rb @@ -67,24 +67,67 @@ def run_formatter_specs ) 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 => %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[ + describe "aborting the run early" do + it "aborts as soon as the last expected failure finishes, since we don't care about what happens after that" do + expected_failures = %w[ ./spec/rspec/core/resources/formatter_specs.rb[2:2:1] + ./spec/rspec/core/resources/formatter_specs.rb[4:1] ] - ) - end - # TODO: test aborting after pending vs failed vs passing example if we keep this feature. + results = server.capture_run_results(expected_failures) do + run_formatter_specs + end + + expect(results).to have_attributes( + :all_example_ids => %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] + ], + :failed_example_ids => %w[ + ./spec/rspec/core/resources/formatter_specs.rb[2:2:1] + ./spec/rspec/core/resources/formatter_specs.rb[4:1] + ] + ) + end + + it 'aborts after an expected failure passes instead, even when there are remaining failing examples' do + passing_example = "./spec/rspec/core/resources/formatter_specs.rb[3:1]" + later_failing_example = "./spec/rspec/core/resources/formatter_specs.rb[4:1]" + + results = server.capture_run_results([passing_example, later_failing_example]) do + run_formatter_specs + end + + expect(results).to have_attributes( + :all_example_ids => %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] + ], + :failed_example_ids => %w[ + ./spec/rspec/core/resources/formatter_specs.rb[2:2:1] + ] + ) + end + + it 'aborts after an expected failure is pending instead, even when there are remaining failing examples' do + pending_example = "./spec/rspec/core/resources/formatter_specs.rb[1:1]" + later_failing_example = "./spec/rspec/core/resources/formatter_specs.rb[4:1]" + + results = server.capture_run_results([pending_example, later_failing_example]) do + run_formatter_specs + end + + expect(results).to have_attributes( + :all_example_ids => %w[ ./spec/rspec/core/resources/formatter_specs.rb[1:1] ], + :failed_example_ids => %w[] + ) + end + end end end end diff --git a/spec/rspec/core/set_spec.rb b/spec/rspec/core/set_spec.rb index 36f46d135b..5388f02c6f 100644 --- a/spec/rspec/core/set_spec.rb +++ b/spec/rspec/core/set_spec.rb @@ -20,4 +20,17 @@ expect(set).to be_an Enumerable expect { |p| set.each(&p) }.to yield_successive_args(1, 2, 3) end + + it 'supports deletions' do + expect { + set.delete(1) + }.to change { set.include?(1) }.from(true).to(false) + end + + it 'indicates if it is empty' do + set = RSpec::Core::Set.new + expect { + set << 1 + }.to change { set.empty? }.from(true).to(false) + end end From 5c14942154d83af84cc5ce7e95ecda1423c2a91d Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Wed, 25 Mar 2015 23:19:37 -0700 Subject: [PATCH 138/258] Rename `output` to `formatter_output`. `output` conflicts with the `output` formatter. --- .../formatters/base_text_formatter_spec.rb | 30 +++++++++---------- .../documentation_formatter_spec.rb | 8 ++--- .../core/formatters/json_formatter_spec.rb | 10 +++---- .../core/formatters/profile_formatter_spec.rb | 16 +++++----- .../formatters/progress_formatter_spec.rb | 16 +++++----- spec/support/formatter_support.rb | 6 ++-- 6 files changed, 43 insertions(+), 43 deletions(-) diff --git a/spec/rspec/core/formatters/base_text_formatter_spec.rb b/spec/rspec/core/formatters/base_text_formatter_spec.rb index 43159cbe55..d05a766504 100644 --- a/spec/rspec/core/formatters/base_text_formatter_spec.rb +++ b/spec/rspec/core/formatters/base_text_formatter_spec.rb @@ -27,17 +27,17 @@ describe "#dump_summary" do it "with 0s outputs pluralized (excluding pending)" do send_notification :dump_summary, summary_notification(0, [], [], [], 0) - expect(output.string).to match("0 examples, 0 failures") + expect(formatter_output.string).to match("0 examples, 0 failures") end it "with 1s outputs singular (including pending)" do send_notification :dump_summary, summary_notification(0, examples(1), examples(1), examples(1), 0) - expect(output.string).to match("1 example, 1 failure, 1 pending") + expect(formatter_output.string).to match("1 example, 1 failure, 1 pending") end it "with 2s outputs pluralized (including pending)" do send_notification :dump_summary, summary_notification(2, examples(2), examples(2), examples(2), 0) - expect(output.string).to match("2 examples, 2 failures, 2 pending") + expect(formatter_output.string).to match("2 examples, 2 failures, 2 pending") end describe "rerun command for failed examples" do @@ -104,7 +104,7 @@ def output_from_running(example_group) examples = example_group.examples failed = examples.select { |e| e.execution_result.status == :failed } send_notification :dump_summary, summary_notification(1, examples, failed, [], 0) - output.string + formatter_output.string end end end @@ -124,8 +124,8 @@ def run_all_and_dump_failures run_all_and_dump_failures - expect(output.string).to match(/group name example name/m) - expect(output.string).to match(/(\s+)expected: \"that\"\n\1 got: \"this\"/m) + expect(formatter_output.string).to match(/group name example name/m) + expect(formatter_output.string).to match(/(\s+)expected: \"that\"\n\1 got: \"this\"/m) end context "with an exception without a message" do @@ -157,7 +157,7 @@ def run_all_and_dump_failures exception = Class.new(StandardError).new group.example("example name") { raise exception } run_all_and_dump_failures - expect(output.string).to include('(anonymous error class)') + expect(formatter_output.string).to include('(anonymous error class)') end end @@ -165,7 +165,7 @@ def run_all_and_dump_failures it "does not show the error class" do group.example("example name") { raise NameError.new('foo') } run_all_and_dump_failures - expect(output.string).to match(/NameError/m) + expect(formatter_output.string).to match(/NameError/m) end end @@ -174,7 +174,7 @@ def run_all_and_dump_failures it "runs without encountering an encoding exception" do group.example("Mixing encodings, e.g. UTF-8: © and Binary") { raise "Error: \xC2\xA9".force_encoding("ASCII-8BIT") } run_all_and_dump_failures - expect(output.string).to match(/RuntimeError:\n\s+Error: \?\?/m) # ?? because the characters dont encode properly + expect(formatter_output.string).to match(/RuntimeError:\n\s+Error: \?\?/m) # ?? because the characters dont encode properly end end end @@ -183,7 +183,7 @@ def run_all_and_dump_failures it "does not show the error class" do group.example("example name") { expect("this").to eq("that") } run_all_and_dump_failures - expect(output.string).not_to match(/RSpec/m) + expect(formatter_output.string).not_to match(/RSpec/m) end end @@ -191,7 +191,7 @@ def run_all_and_dump_failures it "does not show the error class" do group.example("example name") { expect("this").to receive("that") } run_all_and_dump_failures - expect(output.string).not_to match(/RSpec/m) + expect(formatter_output.string).not_to match(/RSpec/m) end end @@ -207,7 +207,7 @@ def run_all_and_dump_failures run_all_and_dump_failures - expect(output.string.lines).to include(a_string_ending_with( + expect(formatter_output.string.lines).to include(a_string_ending_with( 'Shared Example Group: "foo bar" called from ' + "#{RSpec::Core::Metadata.relative_path(__FILE__)}:#{line}\n" )) @@ -226,7 +226,7 @@ def run_all_and_dump_failures run_all_and_dump_failures - expect(output.string.lines).to include(a_string_ending_with( + expect(formatter_output.string.lines).to include(a_string_ending_with( 'Shared Example Group: "foo bar" called from ' + "./spec/rspec/core/formatters/base_text_formatter_spec.rb:#{line}\n" )) @@ -249,7 +249,7 @@ def run_all_and_dump_failures run_all_and_dump_failures - expect(output.string.lines.grep(/Shared Example Group/)).to match [ + expect(formatter_output.string.lines.grep(/Shared Example Group/)).to match [ a_string_ending_with( 'Shared Example Group: "inner" called from ' + "./spec/rspec/core/formatters/base_text_formatter_spec.rb:#{inner_line}\n" @@ -273,7 +273,7 @@ def run_all_and_dump_failures config.success_color = :cyan end send_notification :dump_summary, summary_notification(0, examples(1), [], [], 0) - expect(output.string).to include("\e[36m") + expect(formatter_output.string).to include("\e[36m") end end end diff --git a/spec/rspec/core/formatters/documentation_formatter_spec.rb b/spec/rspec/core/formatters/documentation_formatter_spec.rb index 2c14d8d2cc..cbe11fc6ad 100644 --- a/spec/rspec/core/formatters/documentation_formatter_spec.rb +++ b/spec/rspec/core/formatters/documentation_formatter_spec.rb @@ -25,8 +25,8 @@ def execution_result(values) :execution_result => execution_result(:status => :failed, :exception => Exception.new) )) - expect(output.string).to match(/first example \(FAILED - 1\)/m) - expect(output.string).to match(/second example \(FAILED - 2\)/m) + expect(formatter_output.string).to match(/first example \(FAILED - 1\)/m) + expect(formatter_output.string).to match(/second example \(FAILED - 2\)/m) end it "represents nested group using hierarchy tree" do @@ -45,7 +45,7 @@ def execution_result(values) group.run(reporter) - expect(output.string).to eql(" + expect(formatter_output.string).to eql(" root context 1 nested example 1.1 @@ -68,7 +68,7 @@ def execution_result(values) group.run(reporter) - expect(output.string).to eql(" + expect(formatter_output.string).to eql(" root nested example 1 diff --git a/spec/rspec/core/formatters/json_formatter_spec.rb b/spec/rspec/core/formatters/json_formatter_spec.rb index edf82149fd..5e0f5ba4f1 100644 --- a/spec/rspec/core/formatters/json_formatter_spec.rb +++ b/spec/rspec/core/formatters/json_formatter_spec.rb @@ -14,8 +14,8 @@ include FormatterSupport it "can be loaded via `--format json`" do - formatter_output = run_example_specs_with_formatter("json", false) - parsed = JSON.parse(formatter_output) + output = run_example_specs_with_formatter("json", false) + parsed = JSON.parse(output) expect(parsed.keys).to include("examples", "summary", "summary_line") end @@ -82,7 +82,7 @@ :summary_line => "3 examples, 1 failure, 1 pending" } expect(formatter.output_hash).to eq expected - expect(output.string).to eq expected.to_json + expect(formatter_output.string).to eq expected.to_json end describe "#stop" do @@ -94,9 +94,9 @@ describe "#close" do it "outputs the results as a JSON string" do - expect(output.string).to eq "" + expect(formatter_output.string).to eq "" send_notification :close, null_notification - expect(output.string).to eq({ + expect(formatter_output.string).to eq({ :version => RSpec::Core::Version::STRING }.to_json) end diff --git a/spec/rspec/core/formatters/profile_formatter_spec.rb b/spec/rspec/core/formatters/profile_formatter_spec.rb index ee53ce3b42..48cf754bbf 100644 --- a/spec/rspec/core/formatters/profile_formatter_spec.rb +++ b/spec/rspec/core/formatters/profile_formatter_spec.rb @@ -15,24 +15,24 @@ def profile *groups shared_examples_for "profiles examples" do it "names the example" do - expect(output.string).to match(/group example/m) + expect(formatter_output.string).to match(/group example/m) end it "prints the time" do - expect(output.string).to match(/0(\.\d+)? seconds/) + expect(formatter_output.string).to match(/0(\.\d+)? seconds/) end it "prints the path" do filename = __FILE__.split(File::SEPARATOR).last - expect(output.string).to match(/#{filename}\:#{example_line_number}/) + expect(formatter_output.string).to match(/#{filename}\:#{example_line_number}/) end it "prints the percentage taken from the total runtime" do - expect(output.string).to match(/, 100.0% of total time\):/) + expect(formatter_output.string).to match(/, 100.0% of total time\):/) end it "doesn't profile a single example group" do - expect(output.string).not_to match(/slowest example groups/) + expect(formatter_output.string).not_to match(/slowest example groups/) end end @@ -71,15 +71,15 @@ def profile *groups end it "prints the slowest example groups" do - expect(output.string).to match(/slowest example groups/) + expect(formatter_output.string).to match(/slowest example groups/) end it "prints the time" do - expect(output.string).to match(/0(\.\d+)? seconds/) + expect(formatter_output.string).to match(/0(\.\d+)? seconds/) end it "ranks the example groups by average time" do - expect(output.string).to match(/slow group(.*)fast group/m) + expect(formatter_output.string).to match(/slow group(.*)fast group/m) end end diff --git a/spec/rspec/core/formatters/progress_formatter_spec.rb b/spec/rspec/core/formatters/progress_formatter_spec.rb index 9386b6b1c4..22d587f1ec 100644 --- a/spec/rspec/core/formatters/progress_formatter_spec.rb +++ b/spec/rspec/core/formatters/progress_formatter_spec.rb @@ -10,34 +10,34 @@ it 'prints a . on example_passed' do send_notification :example_passed, example_notification - expect(output.string).to eq(".") + expect(formatter_output.string).to eq(".") end it 'prints a * on example_pending' do send_notification :example_pending, example_notification - expect(output.string).to eq("*") + expect(formatter_output.string).to eq("*") end it 'prints a F on example_failed' do send_notification :example_failed, example_notification - expect(output.string).to eq("F") + expect(formatter_output.string).to eq("F") end it "produces standard summary without pending when pending has a 0 count" do send_notification :dump_summary, summary_notification(0.00001, examples(2), [], [], 0) - expect(output.string).to match(/^\n/) - expect(output.string).to match(/2 examples, 0 failures/i) - expect(output.string).not_to match(/0 pending/i) + expect(formatter_output.string).to match(/^\n/) + expect(formatter_output.string).to match(/2 examples, 0 failures/i) + expect(formatter_output.string).not_to match(/0 pending/i) end it "pushes nothing on start" do #start already sent - expect(output.string).to eq("") + expect(formatter_output.string).to eq("") end it "pushes nothing on start dump" do send_notification :start_dump, null_notification - expect(output.string).to eq("\n") + expect(formatter_output.string).to eq("\n") end # The backtrace is slightly different on JRuby/Rubinius so we skip there. diff --git a/spec/support/formatter_support.rb b/spec/support/formatter_support.rb index 91ff0ed424..841342f704 100644 --- a/spec/support/formatter_support.rb +++ b/spec/support/formatter_support.rb @@ -180,15 +180,15 @@ def setup_reporter(*streams) @reporter = config.reporter end - def output - @output ||= StringIO.new + def formatter_output + @formatter_output ||= StringIO.new end def config @configuration ||= begin config = RSpec::Core::Configuration.new - config.output_stream = output + config.output_stream = formatter_output config end end From 911458b1eb53f6223f0152058f4e722c8d5358a1 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Wed, 25 Mar 2015 23:12:13 -0700 Subject: [PATCH 139/258] While bisecting, silence stdout/stderr when shelling out. --- lib/rspec/core/bisect/runner.rb | 19 ++++++++++++++++++- spec/integration/bisect_spec.rb | 4 +++- spec/rspec/core/bisect/runner_spec.rb | 6 ++++-- .../core/resources/order_dependent_specs.rb | 4 ++++ spec/rspec/core_spec.rb | 1 + 5 files changed, 30 insertions(+), 4 deletions(-) diff --git a/lib/rspec/core/bisect/runner.rb b/lib/rspec/core/bisect/runner.rb index 2def69d16b..a6f4e1971b 100644 --- a/lib/rspec/core/bisect/runner.rb +++ b/lib/rspec/core/bisect/runner.rb @@ -1,4 +1,5 @@ RSpec::Support.require_rspec_core "shell_escape" +require 'open3' module RSpec module Core @@ -52,10 +53,26 @@ def original_results def run_locations(locations, *capture_args) @server.capture_run_results(*capture_args) do - system command_for(locations) + run_command command_for(locations) end end + if Open3.respond_to?(:capture2e) + def run_command(cmd) + Open3.capture2e(cmd) + end + else # for 1.8.7 + # :nocov: + def run_command(cmd) + Open3.popen3(cmd) do |_, stdout, stderr| + # Reading the streams blocks until the process is complete + stdout.read + stderr.read + end + end + # :nocov: + end + def reusable_cli_options @reusable_cli_options ||= begin opts = original_cli_args_without_locations diff --git a/spec/integration/bisect_spec.rb b/spec/integration/bisect_spec.rb index 2a1f9586b2..1e3545fc32 100644 --- a/spec/integration/bisect_spec.rb +++ b/spec/integration/bisect_spec.rb @@ -7,7 +7,9 @@ module RSpec::Core parser = Parser.new(%w[spec/rspec/core/resources/order_dependent_specs.rb --order defined --bisect]) expect(parser).to receive(:exit) - parser.parse + expect { + parser.parse + }.to avoid_outputting.to_stdout_from_any_process.and avoid_outputting.to_stderr_from_any_process output = normalize_durations(out.string) expect(output).to eq(<<-EOS.gsub(/^\s+\|/, '')) diff --git a/spec/rspec/core/bisect/runner_spec.rb b/spec/rspec/core/bisect/runner_spec.rb index 0a4b81a43a..4923ee517f 100644 --- a/spec/rspec/core/bisect/runner_spec.rb +++ b/spec/rspec/core/bisect/runner_spec.rb @@ -202,8 +202,10 @@ def repro_command_from(ids) describe "#original_results" do let(:original_cli_args) { %w[spec/unit] } + open3_method = Open3.respond_to?(:capture2e) ? :capture2e : :popen3 + before do - allow(runner).to receive(:system) + allow(Open3).to receive(open3_method) allow(server).to receive(:capture_run_results) do |&block| block.call "the results" @@ -212,7 +214,7 @@ def repro_command_from(ids) it "runs the suite with the locations from the original CLI args" do runner.original_results - expect(runner).to have_received(:system).with(a_string_including("spec/unit")) + expect(Open3).to have_received(open3_method).with(a_string_including("spec/unit")) end it 'returns the run results' do diff --git a/spec/rspec/core/resources/order_dependent_specs.rb b/spec/rspec/core/resources/order_dependent_specs.rb index b3f6649d11..373bb0f19d 100644 --- a/spec/rspec/core/resources/order_dependent_specs.rb +++ b/spec/rspec/core/resources/order_dependent_specs.rb @@ -10,6 +10,10 @@ end RSpec.describe "Group 2" do + before do + puts "Stdout output while bisecting should not be shown to the user" + end + it "passes" do $global_state_for_bisect_specs[:foo] = 1 end diff --git a/spec/rspec/core_spec.rb b/spec/rspec/core_spec.rb index a8f528404a..1902167562 100644 --- a/spec/rspec/core_spec.rb +++ b/spec/rspec/core_spec.rb @@ -3,6 +3,7 @@ RSpec.describe RSpec do fake_libs = File.expand_path('../../support/fake_libs', __FILE__) allowed_loaded_features = [ + /open3.rb/, # Used by Bisect::Runner, which is not normally loaded /optparse\.rb/, # Used by OptionParser. /rbconfig\.rb/, # loaded by rspec-support for OS detection. /shellwords\.rb/, # used by ConfigurationOptions and RakeTask. From 5a8f7d5aee919238471811de50870d10d1e5477c Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Thu, 26 Mar 2015 08:02:46 -0700 Subject: [PATCH 140/258] Surface spec suite load-time problems during bisect. --- lib/rspec/core/bisect/coordinator.rb | 2 ++ lib/rspec/core/bisect/runner.rb | 14 +++++++++---- lib/rspec/core/bisect/server.rb | 17 +++++++++++++--- .../formatters/bisect_progress_formatter.rb | 10 +++++++++- spec/integration/bisect_spec.rb | 20 +++++++++++++++---- spec/rspec/core/bisect/runner_spec.rb | 5 ++++- spec/rspec/core/bisect/server_spec.rb | 13 ++++++++++++ .../core/resources/fail_on_load_spec.rb_ | 9 +++++++++ 8 files changed, 77 insertions(+), 13 deletions(-) create mode 100644 spec/rspec/core/resources/fail_on_load_spec.rb_ diff --git a/lib/rspec/core/bisect/coordinator.rb b/lib/rspec/core/bisect/coordinator.rb index 817c2b796f..732c3dfe46 100644 --- a/lib/rspec/core/bisect/coordinator.rb +++ b/lib/rspec/core/bisect/coordinator.rb @@ -36,6 +36,8 @@ def bisect reporter.publish(:bisect_repro_command, :repro => repro) end + rescue Server::DidNotGetRunResults => e + reporter.publish(:bisect_failed, :run_output => e.run_output) end private diff --git a/lib/rspec/core/bisect/runner.rb b/lib/rspec/core/bisect/runner.rb index a6f4e1971b..5c5ad3877b 100644 --- a/lib/rspec/core/bisect/runner.rb +++ b/lib/rspec/core/bisect/runner.rb @@ -57,18 +57,24 @@ def run_locations(locations, *capture_args) end end - if Open3.respond_to?(:capture2e) + # `Open3.capture2e` does not work on JRuby: + # https://github.com/jruby/jruby/issues/2766 + if Open3.respond_to?(:capture2e) && !RSpec::Support::Ruby.jruby? def run_command(cmd) - Open3.capture2e(cmd) + Open3.capture2e(cmd).first end else # for 1.8.7 # :nocov: def run_command(cmd) + out = err = nil + Open3.popen3(cmd) do |_, stdout, stderr| # Reading the streams blocks until the process is complete - stdout.read - stderr.read + out = stdout.read + err = stderr.read end + + "Stdout:\n#{out}\n\nStderr:\n#{err}" end # :nocov: end diff --git a/lib/rspec/core/bisect/server.rb b/lib/rspec/core/bisect/server.rb index 35734a591e..c7e430c2d6 100644 --- a/lib/rspec/core/bisect/server.rb +++ b/lib/rspec/core/bisect/server.rb @@ -16,10 +16,21 @@ def self.run server.stop end + # @private + class DidNotGetRunResults < StandardError + attr_reader :run_output + + def initialize(run_output) + @run_output = run_output + super("Did not get run results, but got output: #{run_output}") + end + end + def capture_run_results(expected_failures=[]) - self.expected_failures = expected_failures - yield - latest_run_results + self.expected_failures = expected_failures + self.latest_run_results = nil + run_output = yield + latest_run_results || raise(DidNotGetRunResults, run_output) end def start diff --git a/lib/rspec/core/formatters/bisect_progress_formatter.rb b/lib/rspec/core/formatters/bisect_progress_formatter.rb index 5d9f2cf1d8..040b8166da 100644 --- a/lib/rspec/core/formatters/bisect_progress_formatter.rb +++ b/lib/rspec/core/formatters/bisect_progress_formatter.rb @@ -8,7 +8,8 @@ module Formatters class BisectProgressFormatter < BaseTextFormatter Formatters.register self, :bisect_starting, :original_bisect_run_complete, :bisect_round_started, :individual_run_complete, - :bisect_round_finished, :bisect_complete, :bisect_repro_command + :bisect_round_finished, :bisect_complete, :bisect_repro_command, + :bisect_failed def bisect_starting(notification) options = notification.original_cli_args.join(' ') @@ -51,6 +52,13 @@ def bisect_complete(notification) def bisect_repro_command(notification) output.puts "\nThe minimal reproduction command is:\n #{notification.repro}" end + + def bisect_failed(notification) + output.puts + output.puts ConsoleCodes.wrap("Spec run failed!", :failure) + output.puts + output.puts ConsoleCodes.wrap(notification.run_output, :failure) + end end end end diff --git a/spec/integration/bisect_spec.rb b/spec/integration/bisect_spec.rb index 1e3545fc32..1a8f72f196 100644 --- a/spec/integration/bisect_spec.rb +++ b/spec/integration/bisect_spec.rb @@ -2,16 +2,21 @@ module RSpec::Core RSpec.describe "Bisect", :slow, :simulate_shell_allowing_unquoted_ids do include FormatterSupport - it 'finds the minimum rerun command and exits' do - RSpec.configuration.output_stream = out = StringIO.new - parser = Parser.new(%w[spec/rspec/core/resources/order_dependent_specs.rb --order defined --bisect]) + def bisect(cli_args) + RSpec.configuration.output_stream = formatter_output + parser = Parser.new(cli_args) expect(parser).to receive(:exit) expect { parser.parse }.to avoid_outputting.to_stdout_from_any_process.and avoid_outputting.to_stderr_from_any_process - output = normalize_durations(out.string) + normalize_durations(formatter_output.string) + end + + it 'finds the minimum rerun command and exits' do + output = bisect(%w[spec/rspec/core/resources/order_dependent_specs.rb --order defined --bisect]) + expect(output).to eq(<<-EOS.gsub(/^\s+\|/, '')) |Bisect started using options: "spec/rspec/core/resources/order_dependent_specs.rb --order defined" |Running suite to find failures... (n.nnnn seconds) @@ -28,5 +33,12 @@ module RSpec::Core | rspec ./spec/rspec/core/resources/order_dependent_specs.rb[11:1,22:1] --order defined EOS end + + context "when a load-time problem occurs while running the suite" do + it 'surfaces the stdout and stderr output to the user' do + output = bisect(%w[spec/rspec/core/resources/fail_on_load_spec.rb_ --bisect]) + expect(output).to include("Spec run failed", "undefined method `contex'", "About to call misspelled method") + end + end end end diff --git a/spec/rspec/core/bisect/runner_spec.rb b/spec/rspec/core/bisect/runner_spec.rb index 4923ee517f..50a74ef778 100644 --- a/spec/rspec/core/bisect/runner_spec.rb +++ b/spec/rspec/core/bisect/runner_spec.rb @@ -203,9 +203,12 @@ def repro_command_from(ids) let(:original_cli_args) { %w[spec/unit] } open3_method = Open3.respond_to?(:capture2e) ? :capture2e : :popen3 + open3_method = :popen3 if RSpec::Support::Ruby.jruby? before do - allow(Open3).to receive(open3_method) + allow(Open3).to receive(open3_method).and_return( + [double("Exit Status"), double("Stdout/err")] + ) allow(server).to receive(:capture_run_results) do |&block| block.call "the results" diff --git a/spec/rspec/core/bisect/server_spec.rb b/spec/rspec/core/bisect/server_spec.rb index 757f1c306d..3bda5cfa8f 100644 --- a/spec/rspec/core/bisect/server_spec.rb +++ b/spec/rspec/core/bisect/server_spec.rb @@ -26,6 +26,19 @@ module RSpec::Core expect(DRb).not_to have_running_server end + context "when results are failed to be reported" do + let(:server) { Bisect::Server.new } + + it "raises an error with the output" do + expect { + server.capture_run_results { "the output" } + }.to raise_error(an_object_having_attributes( + :class => Bisect::Server::DidNotGetRunResults, + :run_output => "the output" + )) + end + end + context "when used in combination with the BisectFormatter", :slow do include FormatterSupport diff --git a/spec/rspec/core/resources/fail_on_load_spec.rb_ b/spec/rspec/core/resources/fail_on_load_spec.rb_ new file mode 100644 index 0000000000..6255adcb03 --- /dev/null +++ b/spec/rspec/core/resources/fail_on_load_spec.rb_ @@ -0,0 +1,9 @@ +# Deliberately named *.rb_ to avoid being loaded except when specified + +RSpec.describe "A group" do + puts "About to call misspelled method" + contex "misspelled" do + it "fails" do + end + end +end From 4a5c2991fbfe403deddd72658516218a1ac941b9 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 27 Mar 2015 10:29:53 -0700 Subject: [PATCH 141/258] Get bisect runner to work properly on JRuby. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unfortunately, on JRuby, `Open3.popen3` doesn’t handle shell escaped args properly :(. --- lib/rspec/core/bisect/runner.rb | 19 ++++++++++++++----- lib/rspec/core/shell_escape.rb | 10 ++++++---- spec/rspec/core/bisect/runner_spec.rb | 12 ++++++++---- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/lib/rspec/core/bisect/runner.rb b/lib/rspec/core/bisect/runner.rb index 5c5ad3877b..d946f35ada 100644 --- a/lib/rspec/core/bisect/runner.rb +++ b/lib/rspec/core/bisect/runner.rb @@ -8,8 +8,6 @@ module Bisect # the given bisect server to capture the results. # @private class Runner - include RSpec::Core::ShellEscape - attr_reader :original_cli_args def initialize(server, original_cli_args) @@ -25,12 +23,12 @@ def command_for(locations) parts = [] parts << RUBY << load_path - parts << escape(RSpec::Core.path_to_executable) + parts << open3_safe_escape(RSpec::Core.path_to_executable) parts << "--format" << "bisect" parts << "--drb-port" << @server.drb_port parts.concat reusable_cli_options - parts.concat locations.map { |l| escape(l) } + parts.concat locations.map { |l| open3_safe_escape(l) } parts.join(" ") end @@ -51,6 +49,17 @@ def original_results private + include RSpec::Core::ShellEscape + # On JRuby, Open3.popen3 does not handle shellescaped args properly: + # https://github.com/jruby/jruby/issues/2767 + if RSpec::Support::Ruby.jruby? + # :nocov: + alias open3_safe_escape quote + # :nocov: + else + alias open3_safe_escape escape + end + def run_locations(locations, *capture_args) @server.capture_run_results(*capture_args) do run_command command_for(locations) @@ -128,7 +137,7 @@ def original_locations end def load_path - @load_path ||= "-I#{$LOAD_PATH.map { |p| escape(p) }.join(':')}" + @load_path ||= "-I#{$LOAD_PATH.map { |p| open3_safe_escape(p) }.join(':')}" end # Path to the currently running Ruby executable, borrowed from Rake: diff --git a/lib/rspec/core/shell_escape.rb b/lib/rspec/core/shell_escape.rb index f83c799530..af8f841687 100644 --- a/lib/rspec/core/shell_escape.rb +++ b/lib/rspec/core/shell_escape.rb @@ -3,11 +3,13 @@ module Core # @private # Deals with the fact that `shellwords` only works on POSIX systems. module ShellEscape + def quote(argument) + "'#{argument.gsub("'", "\\\\'")}'" + end + if RSpec::Support::OS.windows? # :nocov: - def escape(shell_command) - "'#{shell_command.gsub("'", "\\\\'")}'" - end + alias escape quote # :nocov: else require 'shellwords' @@ -27,7 +29,7 @@ def escape(shell_command) def conditionally_quote(id) return id if shell_allows_unquoted_ids? - "'#{id.gsub("'", "\\\\'")}'" + quote(id) end def shell_allows_unquoted_ids? diff --git a/spec/rspec/core/bisect/runner_spec.rb b/spec/rspec/core/bisect/runner_spec.rb index 50a74ef778..b8504cd7ab 100644 --- a/spec/rspec/core/bisect/runner_spec.rb +++ b/spec/rspec/core/bisect/runner_spec.rb @@ -51,7 +51,7 @@ def command_for(locations, options={}) it 'escapes locations' do cmd = command_for(["path/with spaces/to/spec.rb"]) - if RSpec::Support::OS.windows? + if uses_quoting_for_escaping? expect(cmd).to include("'path/with spaces/to/spec.rb'") else expect(cmd).to include('path/with\ spaces/to/spec.rb') @@ -116,7 +116,7 @@ def expect_formatters_to_be_excluded allow(RSpec::Core).to receive(:path_to_executable).and_return("path/with spaces/rspec") cmd = command_for([]) - if RSpec::Support::OS.windows? + if uses_quoting_for_escaping? expect(cmd).to include("'path/with spaces/rspec'") else expect(cmd).to include('path/with\ spaces/rspec') @@ -125,7 +125,7 @@ def expect_formatters_to_be_excluded it 'includes the current load path as an option to `ruby`, not as an option to `rspec`' do cmd = command_for([], :load_path => %W[ lp/foo lp/bar ]) - if RSpec::Support::OS.windows? + if uses_quoting_for_escaping? expect(cmd).to first_include("-I'lp/foo':'lp/bar'").then_include(RSpec::Core.path_to_executable) else expect(cmd).to first_include("-Ilp/foo:lp/bar").then_include(RSpec::Core.path_to_executable) @@ -134,7 +134,7 @@ def expect_formatters_to_be_excluded it 'escapes the load path entries' do cmd = command_for([], :load_path => ['l p/foo', 'l p/bar' ]) - if RSpec::Support::OS.windows? + if uses_quoting_for_escaping? expect(cmd).to first_include("-I'l p/foo':'l p/bar'").then_include(RSpec::Core.path_to_executable) else expect(cmd).to first_include('-Il\ p/foo:l\ p/bar').then_include(RSpec::Core.path_to_executable) @@ -228,5 +228,9 @@ def repro_command_from(ids) expect(runner.original_results).to be(runner.original_results) end end + + def uses_quoting_for_escaping? + RSpec::Support::OS.windows? || RSpec::Support::Ruby.jruby? + end end end From 4ce24f019f0daed363cda38565f1e1369ae61cbf Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 27 Mar 2015 13:14:43 -0700 Subject: [PATCH 142/258] Abort bisect if the ordering is inconsistent. --- lib/rspec/core/bisect/coordinator.rb | 4 ++-- lib/rspec/core/bisect/example_minimizer.rb | 11 ++++++++++ lib/rspec/core/bisect/server.rb | 22 +++++++++---------- .../formatters/bisect_progress_formatter.rb | 5 +---- spec/integration/bisect_spec.rb | 15 +++++++++---- .../core/bisect/example_minimizer_spec.rb | 5 +++-- .../resources/inconsistently_ordered_specs.rb | 12 ++++++++++ 7 files changed, 51 insertions(+), 23 deletions(-) create mode 100644 spec/rspec/core/resources/inconsistently_ordered_specs.rb diff --git a/lib/rspec/core/bisect/coordinator.rb b/lib/rspec/core/bisect/coordinator.rb index 732c3dfe46..a96f71331c 100644 --- a/lib/rspec/core/bisect/coordinator.rb +++ b/lib/rspec/core/bisect/coordinator.rb @@ -36,8 +36,8 @@ def bisect reporter.publish(:bisect_repro_command, :repro => repro) end - rescue Server::DidNotGetRunResults => e - reporter.publish(:bisect_failed, :run_output => e.run_output) + rescue BisectFailedError => e + reporter.publish(:bisect_failed, :failure_explanation => e.message) end private diff --git a/lib/rspec/core/bisect/example_minimizer.rb b/lib/rspec/core/bisect/example_minimizer.rb index bb19ef7673..9155c49c2e 100644 --- a/lib/rspec/core/bisect/example_minimizer.rb +++ b/lib/rspec/core/bisect/example_minimizer.rb @@ -59,6 +59,8 @@ def get_same_failures?(ids) results = runner.run(ids + failed_example_ids) notify(:individual_run_complete) + abort_if_ordering_inconsistent(results) + (results.failed_example_ids == failed_example_ids).tap do |same| if same debug 2, "Running with #{ids}, got same failures" @@ -105,6 +107,15 @@ def track_duration [yield, ::RSpec::Core::Time.now - start] end + def abort_if_ordering_inconsistent(results) + expected_order = all_example_ids & results.all_example_ids + return if expected_order == results.all_example_ids + + raise BisectFailedError, "\n\nThe example ordering is inconsistent. " \ + "`--bisect` relies upon consistent ordering (e.g. by passing " \ + "`--seed` if you're using random ordering) to work properly." + end + def notify(*args) reporter.publish(*args) end diff --git a/lib/rspec/core/bisect/server.rb b/lib/rspec/core/bisect/server.rb index c7e430c2d6..c668cdb4ad 100644 --- a/lib/rspec/core/bisect/server.rb +++ b/lib/rspec/core/bisect/server.rb @@ -4,6 +4,9 @@ module RSpec module Core # @private module Bisect + # @private + BisectFailedError = Class.new(StandardError) + # @private # A DRb server that receives run results from a separate RSpec process # started by the bisect process. @@ -16,21 +19,11 @@ def self.run server.stop end - # @private - class DidNotGetRunResults < StandardError - attr_reader :run_output - - def initialize(run_output) - @run_output = run_output - super("Did not get run results, but got output: #{run_output}") - end - end - def capture_run_results(expected_failures=[]) self.expected_failures = expected_failures self.latest_run_results = nil run_output = yield - latest_run_results || raise(DidNotGetRunResults, run_output) + latest_run_results || raise_bisect_failed(run_output) end def start @@ -51,6 +44,13 @@ def drb_port # Set via DRb by the BisectFormatter with the results of the run. attr_accessor :latest_run_results + + private + + def raise_bisect_failed(run_output) + raise BisectFailedError, "Failed to get results from the spec " \ + "run. Spec run output:\n\n#{run_output}" + end end end end diff --git a/lib/rspec/core/formatters/bisect_progress_formatter.rb b/lib/rspec/core/formatters/bisect_progress_formatter.rb index 040b8166da..8854ce4571 100644 --- a/lib/rspec/core/formatters/bisect_progress_formatter.rb +++ b/lib/rspec/core/formatters/bisect_progress_formatter.rb @@ -54,10 +54,7 @@ def bisect_repro_command(notification) end def bisect_failed(notification) - output.puts - output.puts ConsoleCodes.wrap("Spec run failed!", :failure) - output.puts - output.puts ConsoleCodes.wrap(notification.run_output, :failure) + output.puts "\nBisect failed! #{notification.failure_explanation}" end end end diff --git a/spec/integration/bisect_spec.rb b/spec/integration/bisect_spec.rb index 1a8f72f196..441e6a38a5 100644 --- a/spec/integration/bisect_spec.rb +++ b/spec/integration/bisect_spec.rb @@ -4,7 +4,7 @@ module RSpec::Core def bisect(cli_args) RSpec.configuration.output_stream = formatter_output - parser = Parser.new(cli_args) + parser = Parser.new(cli_args + ["--bisect"]) expect(parser).to receive(:exit) expect { @@ -15,7 +15,7 @@ def bisect(cli_args) end it 'finds the minimum rerun command and exits' do - output = bisect(%w[spec/rspec/core/resources/order_dependent_specs.rb --order defined --bisect]) + output = bisect(%w[spec/rspec/core/resources/order_dependent_specs.rb --order defined]) expect(output).to eq(<<-EOS.gsub(/^\s+\|/, '')) |Bisect started using options: "spec/rspec/core/resources/order_dependent_specs.rb --order defined" @@ -36,8 +36,15 @@ def bisect(cli_args) context "when a load-time problem occurs while running the suite" do it 'surfaces the stdout and stderr output to the user' do - output = bisect(%w[spec/rspec/core/resources/fail_on_load_spec.rb_ --bisect]) - expect(output).to include("Spec run failed", "undefined method `contex'", "About to call misspelled method") + output = bisect(%w[spec/rspec/core/resources/fail_on_load_spec.rb_]) + expect(output).to include("Bisect failed!", "undefined method `contex'", "About to call misspelled method") + end + end + + context "when the spec ordering is inconsistent" do + it 'stops bisecting and surfaces the problem to the user' do + output = bisect(%W[spec/rspec/core/resources/inconsistently_ordered_specs.rb]) + expect(output).to include("Bisect failed!", "The example ordering is inconsistent") end end end diff --git a/spec/rspec/core/bisect/example_minimizer_spec.rb b/spec/rspec/core/bisect/example_minimizer_spec.rb index 1238ca709a..a689b4854d 100644 --- a/spec/rspec/core/bisect/example_minimizer_spec.rb +++ b/spec/rspec/core/bisect/example_minimizer_spec.rb @@ -11,7 +11,8 @@ def original_cli_args end def original_results - RunResults.new(all_ids, always_failures | dependent_failures.keys) + failures = always_failures | dependent_failures.keys + RunResults.new(all_ids, failures.sort) end def run(ids) @@ -20,7 +21,7 @@ def run(ids) failures << failing_example if ids.include?(depends_upon) end - RunResults.new(ids, failures) + RunResults.new(ids.sort, failures.sort) end end diff --git a/spec/rspec/core/resources/inconsistently_ordered_specs.rb b/spec/rspec/core/resources/inconsistently_ordered_specs.rb new file mode 100644 index 0000000000..213d01a4b3 --- /dev/null +++ b/spec/rspec/core/resources/inconsistently_ordered_specs.rb @@ -0,0 +1,12 @@ +# Deliberately named _specs.rb to avoid being loaded except when specified + +RSpec.configure do |c| + c.register_ordering(:global, &:shuffle) +end + +10.times do |i| + RSpec.describe "Group #{i}" do + it("passes") { } + it("fails") { fail } + end +end From 0b6acf33f029c2b57d463ccb8e15124c30aec942 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 27 Mar 2015 13:38:38 -0700 Subject: [PATCH 143/258] Indicate if the bisection was successful via the exit code. --- lib/rspec/core/bisect/coordinator.rb | 3 +++ lib/rspec/core/option_parser.rb | 4 ++-- spec/integration/bisect_spec.rb | 10 +++++----- spec/rspec/core/bisect/server_spec.rb | 4 ++-- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/lib/rspec/core/bisect/coordinator.rb b/lib/rspec/core/bisect/coordinator.rb index a96f71331c..1f7bba1067 100644 --- a/lib/rspec/core/bisect/coordinator.rb +++ b/lib/rspec/core/bisect/coordinator.rb @@ -36,8 +36,11 @@ def bisect reporter.publish(:bisect_repro_command, :repro => repro) end + + true rescue BisectFailedError => e reporter.publish(:bisect_failed, :failure_explanation => e.message) + false end private diff --git a/lib/rspec/core/option_parser.rb b/lib/rspec/core/option_parser.rb index 6950be24d8..f65795ae92 100644 --- a/lib/rspec/core/option_parser.rb +++ b/lib/rspec/core/option_parser.rb @@ -65,8 +65,8 @@ def parser(options) parser.on('--bisect', 'Repeatedly runs the suite in order to isolates the failures to the ', ' smallest reproducible case.') do RSpec::Support.require_rspec_core "bisect/coordinator" - Bisect::Coordinator.bisect_with(original_args, RSpec.configuration) - exit + success = Bisect::Coordinator.bisect_with(original_args, RSpec.configuration) + exit(success ? 0 : 1) end parser.on('--[no-]fail-fast', 'Abort the run on first failure.') do |value| diff --git a/spec/integration/bisect_spec.rb b/spec/integration/bisect_spec.rb index 441e6a38a5..81d0acb770 100644 --- a/spec/integration/bisect_spec.rb +++ b/spec/integration/bisect_spec.rb @@ -2,10 +2,10 @@ module RSpec::Core RSpec.describe "Bisect", :slow, :simulate_shell_allowing_unquoted_ids do include FormatterSupport - def bisect(cli_args) + def bisect(cli_args, expected_status) RSpec.configuration.output_stream = formatter_output parser = Parser.new(cli_args + ["--bisect"]) - expect(parser).to receive(:exit) + expect(parser).to receive(:exit).with(expected_status) expect { parser.parse @@ -15,7 +15,7 @@ def bisect(cli_args) end it 'finds the minimum rerun command and exits' do - output = bisect(%w[spec/rspec/core/resources/order_dependent_specs.rb --order defined]) + output = bisect(%w[spec/rspec/core/resources/order_dependent_specs.rb --order defined], 0) expect(output).to eq(<<-EOS.gsub(/^\s+\|/, '')) |Bisect started using options: "spec/rspec/core/resources/order_dependent_specs.rb --order defined" @@ -36,14 +36,14 @@ def bisect(cli_args) context "when a load-time problem occurs while running the suite" do it 'surfaces the stdout and stderr output to the user' do - output = bisect(%w[spec/rspec/core/resources/fail_on_load_spec.rb_]) + output = bisect(%w[spec/rspec/core/resources/fail_on_load_spec.rb_], 1) expect(output).to include("Bisect failed!", "undefined method `contex'", "About to call misspelled method") end end context "when the spec ordering is inconsistent" do it 'stops bisecting and surfaces the problem to the user' do - output = bisect(%W[spec/rspec/core/resources/inconsistently_ordered_specs.rb]) + output = bisect(%W[spec/rspec/core/resources/inconsistently_ordered_specs.rb], 1) expect(output).to include("Bisect failed!", "The example ordering is inconsistent") end end diff --git a/spec/rspec/core/bisect/server_spec.rb b/spec/rspec/core/bisect/server_spec.rb index 3bda5cfa8f..2e302c8acf 100644 --- a/spec/rspec/core/bisect/server_spec.rb +++ b/spec/rspec/core/bisect/server_spec.rb @@ -33,8 +33,8 @@ module RSpec::Core expect { server.capture_run_results { "the output" } }.to raise_error(an_object_having_attributes( - :class => Bisect::Server::DidNotGetRunResults, - :run_output => "the output" + :class => Bisect::BisectFailedError, + :message => a_string_including("Failed to get results", "the output") )) end end From 269dddc00108568ea45ce5cdf31a588c7a579c21 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 27 Mar 2015 17:01:51 -0700 Subject: [PATCH 144/258] Extract helper methods for options that do something and exit. This addresses a rubocop cyclomatic complexity failure. --- lib/rspec/core/option_parser.rb | 38 +++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/lib/rspec/core/option_parser.rb b/lib/rspec/core/option_parser.rb index f65795ae92..a0d3077ad5 100644 --- a/lib/rspec/core/option_parser.rb +++ b/lib/rspec/core/option_parser.rb @@ -64,9 +64,7 @@ def parser(options) parser.on('--bisect', 'Repeatedly runs the suite in order to isolates the failures to the ', ' smallest reproducible case.') do - RSpec::Support.require_rspec_core "bisect/coordinator" - success = Bisect::Coordinator.bisect_with(original_args, RSpec.configuration) - exit(success ? 0 : 1) + bisect_and_exit end parser.on('--[no-]fail-fast', 'Abort the run on first failure.') do |value| @@ -92,9 +90,7 @@ def parser(options) end parser.on('--init', 'Initialize your project with RSpec.') do |_cmd| - RSpec::Support.require_rspec_core "project_initializer" - ProjectInitializer.new.run - exit + initialize_project_and_exit end parser.separator("\n **** Output ****\n\n") @@ -227,8 +223,7 @@ def parser(options) parser.separator("\n **** Utility ****\n\n") parser.on('-v', '--version', 'Display the version.') do - puts RSpec::Core::Version::STRING - exit + print_version_and_exit end # These options would otherwise be confusing to users, so we forcibly @@ -240,9 +235,7 @@ def parser(options) invalid_options = %w[-d --I] parser.on_tail('-h', '--help', "You're looking at it.") do - # Removing the blank invalid options from the output. - puts parser.to_s.gsub(/^\s+(#{invalid_options.join('|')})\s*$\n/, '') - exit + print_help_and_exit(parser, invalid_options) end # This prevents usage of the invalid_options. @@ -268,5 +261,28 @@ def configure_only_failures(options) options[:only_failures] = true add_tag_filter(options, :inclusion_filter, :last_run_status, 'failed') end + + def initialize_project_and_exit + RSpec::Support.require_rspec_core "project_initializer" + ProjectInitializer.new.run + exit + end + + def bisect_and_exit + RSpec::Support.require_rspec_core "bisect/coordinator" + success = Bisect::Coordinator.bisect_with(original_args, RSpec.configuration) + exit(success ? 0 : 1) + end + + def print_version_and_exit + puts RSpec::Core::Version::STRING + exit + end + + def print_help_and_exit(parser, invalid_options) + # Removing the blank invalid options from the output. + puts parser.to_s.gsub(/^\s+(#{invalid_options.join('|')})\s*$\n/, '') + exit + end end end From e4076f617be30f11812fa42caae18b87a10a184b Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 27 Mar 2015 17:29:54 -0700 Subject: [PATCH 145/258] Limit DRb access to only localhost for security reasons. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It would be nice to have a test for this but I’m not sure how to simulate a request from a different host :(. --- lib/rspec/core/bisect/server.rb | 4 ++++ spec/support/fake_libs/drb/acl.rb | 0 2 files changed, 4 insertions(+) create mode 100644 spec/support/fake_libs/drb/acl.rb diff --git a/lib/rspec/core/bisect/server.rb b/lib/rspec/core/bisect/server.rb index c668cdb4ad..35fe97d4e4 100644 --- a/lib/rspec/core/bisect/server.rb +++ b/lib/rspec/core/bisect/server.rb @@ -1,4 +1,5 @@ require 'drb/drb' +require 'drb/acl' module RSpec module Core @@ -27,6 +28,9 @@ def capture_run_results(expected_failures=[]) end def start + # Only allow remote DRb requests from this machine. + DRb.install_acl ACL.new(%w[ deny all allow localhost allow 127.0.0.1 ]) + # We pass `nil` as the first arg to allow it to pick a DRb port. @drb = DRb.start_service(nil, self) end diff --git a/spec/support/fake_libs/drb/acl.rb b/spec/support/fake_libs/drb/acl.rb new file mode 100644 index 0000000000..e69de29bb2 From 4c74a6f039b9658773a5ee5e90f6b2ca6c9a322a Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Mon, 30 Mar 2015 10:40:55 -0700 Subject: [PATCH 146/258] Standardize bisect notifications on `bisect_` prefix. --- lib/rspec/core/bisect/example_minimizer.rb | 4 ++-- lib/rspec/core/formatters/bisect_progress_formatter.rb | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/rspec/core/bisect/example_minimizer.rb b/lib/rspec/core/bisect/example_minimizer.rb index 9155c49c2e..04e4734d12 100644 --- a/lib/rspec/core/bisect/example_minimizer.rb +++ b/lib/rspec/core/bisect/example_minimizer.rb @@ -46,7 +46,7 @@ def prep @failed_example_ids = original_results.failed_example_ids end - notify(:original_bisect_run_complete, :failures => failed_example_ids.size, + notify(:bisect_original_run_complete, :failures => failed_example_ids.size, :non_failures => non_failing_example_ids.size, :duration => duration) end @@ -57,7 +57,7 @@ def non_failing_example_ids def get_same_failures?(ids) results = runner.run(ids + failed_example_ids) - notify(:individual_run_complete) + notify(:bisect_individual_run_complete) abort_if_ordering_inconsistent(results) diff --git a/lib/rspec/core/formatters/bisect_progress_formatter.rb b/lib/rspec/core/formatters/bisect_progress_formatter.rb index 8854ce4571..daaa970479 100644 --- a/lib/rspec/core/formatters/bisect_progress_formatter.rb +++ b/lib/rspec/core/formatters/bisect_progress_formatter.rb @@ -6,8 +6,9 @@ module Formatters # @private # Produces progress output while bisecting. class BisectProgressFormatter < BaseTextFormatter - Formatters.register self, :bisect_starting, :original_bisect_run_complete, - :bisect_round_started, :individual_run_complete, + # We've named all events with a `bisect_` prefix to prevent naming collisions. + Formatters.register self, :bisect_starting, :bisect_original_run_complete, + :bisect_round_started, :bisect_individual_run_complete, :bisect_round_finished, :bisect_complete, :bisect_repro_command, :bisect_failed @@ -17,7 +18,7 @@ def bisect_starting(notification) output.print "Running suite to find failures..." end - def original_bisect_run_complete(notification) + def bisect_original_run_complete(notification) failures = Helpers.pluralize(notification.failures, "failed example") non_failures = Helpers.pluralize(notification.non_failures, "non-failing example") @@ -38,7 +39,7 @@ def bisect_round_finished(notification) output.print " (#{Helpers.format_duration(notification.duration)})" end - def individual_run_complete(_) + def bisect_individual_run_complete(_) output.print '.' end From d208b707e145014d90d0c85d05765fdc86be8033 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Mon, 30 Mar 2015 10:21:30 -0700 Subject: [PATCH 147/258] Refactor bisect debug output. - Move formatting logic into a formatter. - Improve the formatting. - Leverage notification events. --- lib/rspec/core/bisect/coordinator.rb | 6 +- lib/rspec/core/bisect/example_minimizer.rb | 30 ++++------ lib/rspec/core/bisect/runner.rb | 16 +----- .../formatters/bisect_progress_formatter.rb | 55 +++++++++++++++++-- lib/rspec/core/formatters/helpers.rb | 18 ++++++ lib/rspec/core/shell_escape.rb | 22 ++++---- spec/integration/bisect_spec.rb | 52 ++++++++++++++++++ .../core/bisect/example_minimizer_spec.rb | 4 ++ 8 files changed, 151 insertions(+), 52 deletions(-) diff --git a/lib/rspec/core/bisect/coordinator.rb b/lib/rspec/core/bisect/coordinator.rb index 1f7bba1067..81eecdab38 100644 --- a/lib/rspec/core/bisect/coordinator.rb +++ b/lib/rspec/core/bisect/coordinator.rb @@ -25,7 +25,7 @@ def initialize(original_cli_args, configuration) end def bisect - @configuration.add_formatter Formatters::BisectProgressFormatter + @configuration.add_formatter bisect_formatter reporter.close_after do repro = Server.run do |server| @@ -48,6 +48,10 @@ def bisect def reporter @configuration.reporter end + + def bisect_formatter + ENV['DEBUG_RSPEC_BISECT'] ? Formatters::BisectDebugFormatter : Formatters::BisectProgressFormatter + end end end end diff --git a/lib/rspec/core/bisect/example_minimizer.rb b/lib/rspec/core/bisect/example_minimizer.rb index 04e4734d12..525c012889 100644 --- a/lib/rspec/core/bisect/example_minimizer.rb +++ b/lib/rspec/core/bisect/example_minimizer.rb @@ -17,9 +17,7 @@ def initialize(runner, reporter) def find_minimal_repro prep - remaining_ids = all_example_ids - failed_example_ids - debug 0, "Initial failed_example_ids: #{failed_example_ids}" - debug 0, "Initial remaining_ids: #{remaining_ids}" + remaining_ids = non_failing_example_ids each_bisect_round(lambda { remaining_ids }) do |subsets| ids_to_ignore = subsets.find do |ids| @@ -29,7 +27,7 @@ def find_minimal_repro next :done unless ids_to_ignore remaining_ids -= ids_to_ignore - debug 1, "Removed #{ids_to_ignore}; remaining_ids: #{remaining_ids}" + notify(:bisect_ignoring_ids, :ids_to_ignore => ids_to_ignore, :remaining_ids => remaining_ids) end remaining_ids + failed_example_ids @@ -46,8 +44,8 @@ def prep @failed_example_ids = original_results.failed_example_ids end - notify(:bisect_original_run_complete, :failures => failed_example_ids.size, - :non_failures => non_failing_example_ids.size, + notify(:bisect_original_run_complete, :failed_example_ids => failed_example_ids, + :non_failing_example_ids => non_failing_example_ids, :duration => duration) end @@ -56,22 +54,14 @@ def non_failing_example_ids end def get_same_failures?(ids) - results = runner.run(ids + failed_example_ids) - notify(:bisect_individual_run_complete) + ids_to_run = ids + failed_example_ids + notify(:bisect_individual_run_start, :command => runner.repro_command_from(ids_to_run)) - abort_if_ordering_inconsistent(results) - - (results.failed_example_ids == failed_example_ids).tap do |same| - if same - debug 2, "Running with #{ids}, got same failures" - else - debug 2, "Running with #{ids}, got different failures: #{results.failed_example_ids}" - end - end - end + results, duration = track_duration { runner.run(ids_to_run) } + notify(:bisect_individual_run_complete, :duration => duration, :results => results) - def debug(level, msg) - puts "#{' ' * level}Minimizer: #{msg}" if ENV['DEBUG_RSPEC_BISECT'] + abort_if_ordering_inconsistent(results) + results.failed_example_ids == failed_example_ids end INFINITY = (1.0 / 0) # 1.8.7 doesn't define Float::INFINITY so we define our own... diff --git a/lib/rspec/core/bisect/runner.rb b/lib/rspec/core/bisect/runner.rb index d946f35ada..19c0ca4886 100644 --- a/lib/rspec/core/bisect/runner.rb +++ b/lib/rspec/core/bisect/runner.rb @@ -37,7 +37,7 @@ def repro_command_from(locations) parts = [] parts << "rspec" - parts.concat organize_locations(locations) + parts.concat Formatters::Helpers.organize_ids(locations) parts.concat original_cli_args_without_locations parts.join(" ") @@ -107,20 +107,6 @@ def reusable_cli_options end end - def organize_locations(locations) - grouped = locations.inject(Hash.new { |h, k| h[k] = [] }) do |hash, location| - file, id = location.split(Configuration::ON_SQUARE_BRACKETS) - hash[file] << id - hash - end - - grouped.sort_by(&:first).map do |file, ids| - ids = ids.sort_by { |id| id.split(':').map(&:to_i) } - id = Metadata.id_from(:rerun_file_path => file, :scoped_id => ids.join(',')) - conditionally_quote(id) - end - end - def original_cli_args_without_locations @original_cli_args_without_locations ||= begin files_or_dirs = parsed_original_cli_options.fetch(:files_or_directories_to_run) diff --git a/lib/rspec/core/formatters/bisect_progress_formatter.rb b/lib/rspec/core/formatters/bisect_progress_formatter.rb index daaa970479..7862cd01b3 100644 --- a/lib/rspec/core/formatters/bisect_progress_formatter.rb +++ b/lib/rspec/core/formatters/bisect_progress_formatter.rb @@ -19,20 +19,21 @@ def bisect_starting(notification) end def bisect_original_run_complete(notification) - failures = Helpers.pluralize(notification.failures, "failed example") - non_failures = Helpers.pluralize(notification.non_failures, "non-failing example") + failures = Helpers.pluralize(notification.failed_example_ids.size, "failed example") + non_failures = Helpers.pluralize(notification.non_failing_example_ids.size, "non-failing example") output.puts " (#{Helpers.format_duration(notification.duration)})" output.puts "Starting bisect with #{failures} and #{non_failures}." end - def bisect_round_started(notification) + def bisect_round_started(notification, include_trailing_space=true) search_desc = Helpers.pluralize( notification.subset_size, "non-failing example" ) output.print "\nRound #{notification.round}: searching for #{search_desc}" \ - " (of #{notification.remaining_count}) to ignore: " + " (of #{notification.remaining_count}) to ignore:" + output.print " " if include_trailing_space end def bisect_round_finished(notification) @@ -58,6 +59,52 @@ def bisect_failed(notification) output.puts "\nBisect failed! #{notification.failure_explanation}" end end + + # @private + # Produces detailed debug output while bisecting. Used when + # bisect is performed while the `DEBUG_RSPEC_BISECT` ENV var is used. + # Designed to provide details for us when we need to troubleshoot bisect bugs. + class BisectDebugFormatter < BisectProgressFormatter + Formatters.register self, :bisect_original_run_complete, :bisect_individual_run_start, + :bisect_individual_run_complete, :bisect_round_finished, :bisect_ignoring_ids + + def bisect_original_run_complete(notification) + output.puts " (#{Helpers.format_duration(notification.duration)})" + + output.puts " - #{describe_ids 'Failing examples', notification.failed_example_ids}" + output.puts " - #{describe_ids 'Non-failing examples', notification.non_failing_example_ids}" + end + + def bisect_individual_run_start(notification) + output.print "\n - Running: #{notification.command}" + end + + def bisect_individual_run_complete(notification) + output.print " (#{Helpers.format_duration(notification.duration)})" + end + + def bisect_round_started(notification) + super(notification, false) + end + + def bisect_round_finished(notification) + output.print "\n - Round finished" + super + end + + def bisect_ignoring_ids(notification) + output.print "\n - #{describe_ids 'Examples we can safely ignore', notification.ids_to_ignore}" + output.print "\n - #{describe_ids 'Remaining non-failing examples', notification.remaining_ids}" + end + + private + + def describe_ids(description, ids) + organized_ids = Formatters::Helpers.organize_ids(ids) + formatted_ids = organized_ids.map { |id| " - #{id}" }.join("\n") + "#{description} (#{ids.size}):\n#{formatted_ids}" + end + end end end end diff --git a/lib/rspec/core/formatters/helpers.rb b/lib/rspec/core/formatters/helpers.rb index f6acca2a1c..195052f310 100644 --- a/lib/rspec/core/formatters/helpers.rb +++ b/lib/rspec/core/formatters/helpers.rb @@ -1,3 +1,5 @@ +RSpec::Support.require_rspec_core "shell_escape" + module RSpec module Core module Formatters @@ -83,6 +85,22 @@ def self.strip_trailing_zeroes(string) def self.pluralize(count, string) "#{count} #{string}#{'s' unless count.to_f == 1}" end + + # @api private + # Given a list of example ids, organizes them into a compact, ordered list. + def self.organize_ids(ids) + grouped = ids.inject(Hash.new { |h, k| h[k] = [] }) do |hash, id| + file, id = id.split(Configuration::ON_SQUARE_BRACKETS) + hash[file] << id + hash + end + + grouped.sort_by(&:first).map do |file, grouped_ids| + grouped_ids = grouped_ids.sort_by { |id| id.split(':').map(&:to_i) } + id = Metadata.id_from(:rerun_file_path => file, :scoped_id => grouped_ids.join(',')) + ShellEscape.conditionally_quote(id) + end + end end end end diff --git a/lib/rspec/core/shell_escape.rb b/lib/rspec/core/shell_escape.rb index af8f841687..46950cc78b 100644 --- a/lib/rspec/core/shell_escape.rb +++ b/lib/rspec/core/shell_escape.rb @@ -3,6 +3,8 @@ module Core # @private # Deals with the fact that `shellwords` only works on POSIX systems. module ShellEscape + module_function + def quote(argument) "'#{argument.gsub("'", "\\\\'")}'" end @@ -33,18 +35,14 @@ def conditionally_quote(id) end def shell_allows_unquoted_ids? - return @shell_allows_unquoted_ids if defined?(@shell_allows_unquoted_ids) - - @shell_allows_unquoted_ids = SHELLS_ALLOWING_UNQUOTED_IDS.include?( - # Note: ENV['SHELL'] isn't necessarily the shell the user is currently running. - # According to http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html: - # "This variable shall represent a pathname of the user's preferred command language interpreter." - # - # It's the best we can easily do, though. We err on the side of safety (quoting - # the id when not actually needed) so it's not a big deal if the user is actually - # using a different shell. - ENV['SHELL'].to_s.split('/').last - ) + # Note: ENV['SHELL'] isn't necessarily the shell the user is currently running. + # According to http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html: + # "This variable shall represent a pathname of the user's preferred command language interpreter." + # + # It's the best we can easily do, though. We err on the side of safety (quoting + # the id when not actually needed) so it's not a big deal if the user is actually + # using a different shell. + SHELLS_ALLOWING_UNQUOTED_IDS.include?(ENV['SHELL'].to_s.split('/').last) end end end diff --git a/spec/integration/bisect_spec.rb b/spec/integration/bisect_spec.rb index 81d0acb770..02af22a443 100644 --- a/spec/integration/bisect_spec.rb +++ b/spec/integration/bisect_spec.rb @@ -34,6 +34,58 @@ def bisect(cli_args, expected_status) EOS end + it 'supports a verbose mode via `DEBUG_RSPEC_BISECT` so we can get detailed log output from users when they report bugs' do + with_env_vars 'DEBUG_RSPEC_BISECT' => '1' do + output = bisect(%w[spec/rspec/core/resources/order_dependent_specs.rb --order defined], 0) + + expect(output).to eq(<<-EOS.gsub(/^\s+\|/, '')) + |Bisect started using options: "spec/rspec/core/resources/order_dependent_specs.rb --order defined" + |Running suite to find failures... (n.nnnn seconds) + | - Failing examples (1): + | - ./spec/rspec/core/resources/order_dependent_specs.rb[22:1] + | - Non-failing examples (21): + | - ./spec/rspec/core/resources/order_dependent_specs.rb[1:1,2:1,3:1,4:1,5:1,6:1,7:1,8:1,9:1,10:1,11:1,12:1,13:1,14:1,15:1,16:1,17:1,18:1,19:1,20:1,21:1] + | + |Round 1: searching for 11 non-failing examples (of 21) to ignore: + | - Running: rspec ./spec/rspec/core/resources/order_dependent_specs.rb[12:1,13:1,14:1,15:1,16:1,17:1,18:1,19:1,20:1,21:1,22:1] --order defined (n.nnnn seconds) + | - Running: rspec ./spec/rspec/core/resources/order_dependent_specs.rb[1:1,2:1,3:1,4:1,5:1,6:1,7:1,8:1,9:1,10:1,11:1,22:1] --order defined (n.nnnn seconds) + | - Examples we can safely ignore (10): + | - ./spec/rspec/core/resources/order_dependent_specs.rb[12:1,13:1,14:1,15:1,16:1,17:1,18:1,19:1,20:1,21:1] + | - Remaining non-failing examples (11): + | - ./spec/rspec/core/resources/order_dependent_specs.rb[1:1,2:1,3:1,4:1,5:1,6:1,7:1,8:1,9:1,10:1,11:1] + | - Round finished (n.nnnn seconds) + |Round 2: searching for 6 non-failing examples (of 11) to ignore: + | - Running: rspec ./spec/rspec/core/resources/order_dependent_specs.rb[7:1,8:1,9:1,10:1,11:1,22:1] --order defined (n.nnnn seconds) + | - Examples we can safely ignore (6): + | - ./spec/rspec/core/resources/order_dependent_specs.rb[1:1,2:1,3:1,4:1,5:1,6:1] + | - Remaining non-failing examples (5): + | - ./spec/rspec/core/resources/order_dependent_specs.rb[7:1,8:1,9:1,10:1,11:1] + | - Round finished (n.nnnn seconds) + |Round 3: searching for 3 non-failing examples (of 5) to ignore: + | - Running: rspec ./spec/rspec/core/resources/order_dependent_specs.rb[10:1,11:1,22:1] --order defined (n.nnnn seconds) + | - Examples we can safely ignore (3): + | - ./spec/rspec/core/resources/order_dependent_specs.rb[7:1,8:1,9:1] + | - Remaining non-failing examples (2): + | - ./spec/rspec/core/resources/order_dependent_specs.rb[10:1,11:1] + | - Round finished (n.nnnn seconds) + |Round 4: searching for 1 non-failing example (of 2) to ignore: + | - Running: rspec ./spec/rspec/core/resources/order_dependent_specs.rb[11:1,22:1] --order defined (n.nnnn seconds) + | - Examples we can safely ignore (1): + | - ./spec/rspec/core/resources/order_dependent_specs.rb[10:1] + | - Remaining non-failing examples (1): + | - ./spec/rspec/core/resources/order_dependent_specs.rb[11:1] + | - Round finished (n.nnnn seconds) + |Round 5: searching for 1 non-failing example (of 1) to ignore: + | - Running: rspec ./spec/rspec/core/resources/order_dependent_specs.rb[22:1] --order defined (n.nnnn seconds) + | - Round finished (n.nnnn seconds) + |Bisect complete! Reduced necessary non-failing examples from 21 to 1 in n.nnnn seconds. + | + |The minimal reproduction command is: + | rspec ./spec/rspec/core/resources/order_dependent_specs.rb[11:1,22:1] --order defined + EOS + end + end + context "when a load-time problem occurs while running the suite" do it 'surfaces the stdout and stderr output to the user' do output = bisect(%w[spec/rspec/core/resources/fail_on_load_spec.rb_], 1) diff --git a/spec/rspec/core/bisect/example_minimizer_spec.rb b/spec/rspec/core/bisect/example_minimizer_spec.rb index a689b4854d..28163b2c25 100644 --- a/spec/rspec/core/bisect/example_minimizer_spec.rb +++ b/spec/rspec/core/bisect/example_minimizer_spec.rb @@ -23,6 +23,10 @@ def run(ids) RunResults.new(ids.sort, failures.sort) end + + def repro_command_from(locations) + "" + end end it 'repeatedly runs various subsets of the suite, removing examples that have no effect on the failing examples' do From 45ed50c512c824ddf3cd87c0c69a2ffe964493f1 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Tue, 31 Mar 2015 01:05:55 -0700 Subject: [PATCH 148/258] Add spec demonstrating that ENV vars are propagated properly. --- spec/rspec/core/bisect/runner_spec.rb | 14 ++++++++++++++ spec/rspec/core/resources/echo_env_var.rb | 1 + 2 files changed, 15 insertions(+) create mode 100644 spec/rspec/core/resources/echo_env_var.rb diff --git a/spec/rspec/core/bisect/runner_spec.rb b/spec/rspec/core/bisect/runner_spec.rb index b8504cd7ab..232ed94889 100644 --- a/spec/rspec/core/bisect/runner_spec.rb +++ b/spec/rspec/core/bisect/runner_spec.rb @@ -25,6 +25,20 @@ module RSpec::Core runner.run(%w[ spec/1_spec.rb[1:1] spec/1_spec.rb[1:2] ]) end + + it 'ensures environment variables are propagated to the spawned process', :slow do + output = nil + allow(server).to receive(:capture_run_results) do |&block| + output = block.call + Formatters::BisectFormatter::RunResults.new([], []) + end + + with_env_vars 'MY_ENV_VAR' => 'secret' do + runner.run(%w[ spec/rspec/core/resources/echo_env_var.rb ]) + end + + expect(output).to include("MY_ENV_VAR=secret") + end end describe "#command_for" do diff --git a/spec/rspec/core/resources/echo_env_var.rb b/spec/rspec/core/resources/echo_env_var.rb new file mode 100644 index 0000000000..afb80adbfc --- /dev/null +++ b/spec/rspec/core/resources/echo_env_var.rb @@ -0,0 +1 @@ +puts "MY_ENV_VAR=#{ENV['MY_ENV_VAR']}" if ENV.key?("MY_ENV_VAR") From 27920b4ac3aae53a0f8b4ea3bb08a4a1d0c843dd Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Tue, 31 Mar 2015 11:12:26 -0700 Subject: [PATCH 149/258] Fix duration normalization. - Handle "second" vs "seconds". - Handle minutes, too. --- spec/support/formatter_support.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/spec/support/formatter_support.rb b/spec/support/formatter_support.rb index 841342f704..ac1071c64b 100644 --- a/spec/support/formatter_support.rb +++ b/spec/support/formatter_support.rb @@ -31,7 +31,10 @@ def run_example_specs_with_formatter(formatter_option, normalize_output=true) end def normalize_durations(output) - output.gsub(/\d+(?:\.\d+)?(s| seconds)/, "n.nnnn\\1") + output.gsub(/(?:\d+ minutes? )?\d+(?:\.\d+)?(s| seconds?)/) do |dur| + suffix = $1 == "s" ? "s" : " seconds" + "n.nnnn#{suffix}" + end end if RUBY_VERSION.to_f < 1.9 From e38fab7d695e836b98cf728d497d77785c88f361 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Tue, 31 Mar 2015 18:12:25 -0700 Subject: [PATCH 150/258] Gracefully handle SIGINT during bisect. --- lib/rspec/core/bisect/coordinator.rb | 13 ++++- lib/rspec/core/bisect/example_minimizer.rb | 26 ++++++--- .../formatters/bisect_progress_formatter.rb | 7 ++- spec/integration/bisect_spec.rb | 55 ++++++++++++++++++- .../core/bisect/example_minimizer_spec.rb | 13 ++++- 5 files changed, 99 insertions(+), 15 deletions(-) diff --git a/lib/rspec/core/bisect/coordinator.rb b/lib/rspec/core/bisect/coordinator.rb index 81eecdab38..314dfc1766 100644 --- a/lib/rspec/core/bisect/coordinator.rb +++ b/lib/rspec/core/bisect/coordinator.rb @@ -31,7 +31,10 @@ def bisect repro = Server.run do |server| runner = Runner.new(server, @original_cli_args) minimizer = ExampleMinimizer.new(runner, reporter) - runner.repro_command_from(minimizer.find_minimal_repro) + + gracefully_abort_on_sigint(minimizer) + minimizer.find_minimal_repro + minimizer.repro_command_for_currently_needed_ids end reporter.publish(:bisect_repro_command, :repro => repro) @@ -52,6 +55,14 @@ def reporter def bisect_formatter ENV['DEBUG_RSPEC_BISECT'] ? Formatters::BisectDebugFormatter : Formatters::BisectProgressFormatter end + + def gracefully_abort_on_sigint(minimizer) + trap('INT') do + repro = minimizer.repro_command_for_currently_needed_ids + reporter.publish(:bisect_aborted, :repro => repro) + exit(1) + end + end end end end diff --git a/lib/rspec/core/bisect/example_minimizer.rb b/lib/rspec/core/bisect/example_minimizer.rb index 525c012889..50ed80a855 100644 --- a/lib/rspec/core/bisect/example_minimizer.rb +++ b/lib/rspec/core/bisect/example_minimizer.rb @@ -8,6 +8,7 @@ module Bisect # repeatedly running different subsets of the suite. class ExampleMinimizer attr_reader :runner, :reporter, :all_example_ids, :failed_example_ids + attr_accessor :remaining_ids def initialize(runner, reporter) @runner = runner @@ -17,22 +18,31 @@ def initialize(runner, reporter) def find_minimal_repro prep - remaining_ids = non_failing_example_ids + self.remaining_ids = non_failing_example_ids - each_bisect_round(lambda { remaining_ids }) do |subsets| + each_bisect_round do |subsets| ids_to_ignore = subsets.find do |ids| get_same_failures?(remaining_ids - ids) end next :done unless ids_to_ignore - remaining_ids -= ids_to_ignore + self.remaining_ids -= ids_to_ignore notify(:bisect_ignoring_ids, :ids_to_ignore => ids_to_ignore, :remaining_ids => remaining_ids) end + currently_needed_ids + end + + def currently_needed_ids remaining_ids + failed_example_ids end + def repro_command_for_currently_needed_ids + return runner.repro_command_from(currently_needed_ids) if remaining_ids + "(Not yet enough information to provide any repro command)" + end + private def prep @@ -66,19 +76,19 @@ def get_same_failures?(ids) INFINITY = (1.0 / 0) # 1.8.7 doesn't define Float::INFINITY so we define our own... - def each_bisect_round(get_remaining_ids, &block) + def each_bisect_round(&block) last_round, duration = track_duration do 1.upto(INFINITY) do |round| - break if :done == bisect_round(round, get_remaining_ids.call, &block) + break if :done == bisect_round(round, &block) end end notify(:bisect_complete, :round => last_round, :duration => duration, :original_non_failing_count => non_failing_example_ids.size, - :remaining_count => get_remaining_ids.call.size) + :remaining_count => remaining_ids.size) end - def bisect_round(round, remaining_ids) + def bisect_round(round) value, duration = track_duration do subsets = SubsetEnumerator.new(remaining_ids) notify(:bisect_round_started, :round => round, @@ -88,7 +98,7 @@ def bisect_round(round, remaining_ids) yield subsets end - notify(:bisect_round_finished, :duration => duration) + notify(:bisect_round_finished, :duration => duration, :round => round) value end diff --git a/lib/rspec/core/formatters/bisect_progress_formatter.rb b/lib/rspec/core/formatters/bisect_progress_formatter.rb index 7862cd01b3..bcf4288ca3 100644 --- a/lib/rspec/core/formatters/bisect_progress_formatter.rb +++ b/lib/rspec/core/formatters/bisect_progress_formatter.rb @@ -10,7 +10,7 @@ class BisectProgressFormatter < BaseTextFormatter Formatters.register self, :bisect_starting, :bisect_original_run_complete, :bisect_round_started, :bisect_individual_run_complete, :bisect_round_finished, :bisect_complete, :bisect_repro_command, - :bisect_failed + :bisect_failed, :bisect_aborted def bisect_starting(notification) options = notification.original_cli_args.join(' ') @@ -58,6 +58,11 @@ def bisect_repro_command(notification) def bisect_failed(notification) output.puts "\nBisect failed! #{notification.failure_explanation}" end + + def bisect_aborted(notification) + output.puts "\n\nBisect aborted!" + output.puts "\nThe most minimal reproduction command discovered so far is:\n #{notification.repro}" + end end # @private diff --git a/spec/integration/bisect_spec.rb b/spec/integration/bisect_spec.rb index 02af22a443..c63a7fbf88 100644 --- a/spec/integration/bisect_spec.rb +++ b/spec/integration/bisect_spec.rb @@ -1,11 +1,13 @@ +RSpec::Support.require_rspec_core "formatters/bisect_progress_formatter" + module RSpec::Core RSpec.describe "Bisect", :slow, :simulate_shell_allowing_unquoted_ids do include FormatterSupport - def bisect(cli_args, expected_status) + def bisect(cli_args, expected_status=nil) RSpec.configuration.output_stream = formatter_output parser = Parser.new(cli_args + ["--bisect"]) - expect(parser).to receive(:exit).with(expected_status) + expect(parser).to receive(:exit).with(expected_status) if expected_status expect { parser.parse @@ -99,5 +101,54 @@ def bisect(cli_args, expected_status) expect(output).to include("Bisect failed!", "The example ordering is inconsistent") end end + + context "when the user aborts the bisect with ctrl-c" do + before do + formatter_subclass = Class.new(Formatters::BisectProgressFormatter) do + Formatters.register self, :bisect_round_finished + + def bisect_round_finished(notification) + if notification.round == 2 + Process.kill("INT", Process.pid) + # Process.kill is not a synchronous call, so to ensure the output + # below aborts at a deterministic place, we need to block here. + # The sleep will be interrupted by the signal once the OS sends it. + # For the most part, this is only needed on JRuby, but we saw + # the asynchronous behavior on an MRI 2.0 travis build as well. + sleep 5 + else + super + end + end + end + + stub_const(Formatters::BisectProgressFormatter.name, formatter_subclass) + end + + it "prints the most minimal repro command it has found so far" do + expect { + bisect(%w[spec/rspec/core/resources/order_dependent_specs.rb --order defined]) + }.to raise_error(an_object_having_attributes( + :class => SystemExit, + :status => 1 + )) + + output = normalize_durations(formatter_output.string) + + expect(output).to eq(<<-EOS.gsub(/^\s+\|/, '')) + |Bisect started using options: "spec/rspec/core/resources/order_dependent_specs.rb --order defined" + |Running suite to find failures... (n.nnnn seconds) + |Starting bisect with 1 failed example and 21 non-failing examples. + | + |Round 1: searching for 11 non-failing examples (of 21) to ignore: .. (n.nnnn seconds) + |Round 2: searching for 6 non-failing examples (of 11) to ignore: . + | + |Bisect aborted! + | + |The most minimal reproduction command discovered so far is: + | rspec ./spec/rspec/core/resources/order_dependent_specs.rb[7:1,8:1,9:1,10:1,11:1,22:1] --order defined + EOS + end + end end end diff --git a/spec/rspec/core/bisect/example_minimizer_spec.rb b/spec/rspec/core/bisect/example_minimizer_spec.rb index 28163b2c25..fdd3acd953 100644 --- a/spec/rspec/core/bisect/example_minimizer_spec.rb +++ b/spec/rspec/core/bisect/example_minimizer_spec.rb @@ -25,7 +25,7 @@ def run(ids) end def repro_command_from(locations) - "" + "rspec #{locations.sort.join(' ')}" end end @@ -36,8 +36,15 @@ def repro_command_from(locations) { "ex_5" => "ex_4" } ), RSpec::Core::NullReporter) - ids = minimizer.find_minimal_repro - expect(ids).to match_array(%w[ ex_2 ex_4 ex_5 ]) + minimizer.find_minimal_repro + expect(minimizer.repro_command_for_currently_needed_ids).to eq("rspec ex_2 ex_4 ex_5") + end + + context "when the `repro_command_for_currently_needed_ids` is queried before it has sufficient information" do + it 'returns an explanation that will be printed when the bisect run is aborted immediately' do + minimizer = Bisect::ExampleMinimizer.new(FakeRunner.new([], [], {}), RSpec::Core::NullReporter) + expect(minimizer.repro_command_for_currently_needed_ids).to include("Not yet enough information") + end end end end From d7c31250b9f7addfead5104bd5235469655c8fac Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Tue, 31 Mar 2015 21:09:27 -0700 Subject: [PATCH 151/258] Abort early if there are no failures. --- lib/rspec/core/bisect/example_minimizer.rb | 11 ++++++++--- spec/rspec/core/bisect/example_minimizer_spec.rb | 11 +++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/lib/rspec/core/bisect/example_minimizer.rb b/lib/rspec/core/bisect/example_minimizer.rb index 50ed80a855..f52085ecc5 100644 --- a/lib/rspec/core/bisect/example_minimizer.rb +++ b/lib/rspec/core/bisect/example_minimizer.rb @@ -54,9 +54,14 @@ def prep @failed_example_ids = original_results.failed_example_ids end - notify(:bisect_original_run_complete, :failed_example_ids => failed_example_ids, - :non_failing_example_ids => non_failing_example_ids, - :duration => duration) + if @failed_example_ids.empty? + raise BisectFailedError, "\n\nNo failures found. Bisect only works " \ + "in the presence of one or more failing examples." + else + notify(:bisect_original_run_complete, :failed_example_ids => failed_example_ids, + :non_failing_example_ids => non_failing_example_ids, + :duration => duration) + end end def non_failing_example_ids diff --git a/spec/rspec/core/bisect/example_minimizer_spec.rb b/spec/rspec/core/bisect/example_minimizer_spec.rb index fdd3acd953..e0a85f73e2 100644 --- a/spec/rspec/core/bisect/example_minimizer_spec.rb +++ b/spec/rspec/core/bisect/example_minimizer_spec.rb @@ -1,5 +1,6 @@ require 'rspec/core/bisect/example_minimizer' require 'rspec/core/formatters/bisect_formatter' +require 'rspec/core/bisect/server' module RSpec::Core RSpec.describe Bisect::ExampleMinimizer do @@ -40,6 +41,16 @@ def repro_command_from(locations) expect(minimizer.repro_command_for_currently_needed_ids).to eq("rspec ex_2 ex_4 ex_5") end + it 'aborts early when no examples fail' do + minimizer = Bisect::ExampleMinimizer.new(FakeRunner.new( + %w[ ex_1 ex_2 ], [], {} + ), RSpec::Core::NullReporter) + + expect { + minimizer.find_minimal_repro + }.to raise_error(RSpec::Core::Bisect::BisectFailedError, /No failures found/i) + end + context "when the `repro_command_for_currently_needed_ids` is queried before it has sufficient information" do it 'returns an explanation that will be printed when the bisect run is aborted immediately' do minimizer = Bisect::ExampleMinimizer.new(FakeRunner.new([], [], {}), RSpec::Core::NullReporter) From 04d3f5e0bd06f58723a710d9dc54a5b20f9cfab1 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Tue, 31 Mar 2015 23:58:09 -0700 Subject: [PATCH 152/258] Fix bisect runner to handle `-fd` in addition to `-f d`. --- lib/rspec/core/bisect/runner.rb | 6 ++---- spec/rspec/core/bisect/runner_spec.rb | 7 ++++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/rspec/core/bisect/runner.rb b/lib/rspec/core/bisect/runner.rb index 19c0ca4886..e43f4c9fc8 100644 --- a/lib/rspec/core/bisect/runner.rb +++ b/lib/rspec/core/bisect/runner.rb @@ -97,10 +97,8 @@ def reusable_cli_options end parsed_original_cli_options.fetch(:formatters) { [] }.each do |(name, out)| - opts -= %W[ --format #{name} ] - opts -= %W[ --out #{out} ] - opts -= %W[ -f #{name} ] - opts -= %W[ -o #{out} ] + opts -= %W[ --format #{name} -f -f#{name} ] + opts -= %W[ --out #{out} -o -o#{out} ] end opts diff --git a/spec/rspec/core/bisect/runner_spec.rb b/spec/rspec/core/bisect/runner_spec.rb index 232ed94889..2f8da15318 100644 --- a/spec/rspec/core/bisect/runner_spec.rb +++ b/spec/rspec/core/bisect/runner_spec.rb @@ -108,11 +108,16 @@ def expect_formatters_to_be_excluded expect_formatters_to_be_excluded end - it 'excludes any -f and matching -o options passed in the original args' do + it 'excludes any -f and matching -o options passed in the original args' do original_cli_args.concat %w[ -f progress -f html -o specs.html ] expect_formatters_to_be_excluded end + it 'excludes any -f and matching -o options passed in the original args' do + original_cli_args.concat %w[ -fprogress -fhtml -ospecs.html ] + expect_formatters_to_be_excluded + end + it 'starts with the path to the current ruby executable' do cmd = command_for([]) expect(cmd).to start_with(File.join( From cf48b93f1d2aec2889ff372ab8bd75ff56fdf846 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Wed, 1 Apr 2015 00:02:19 -0700 Subject: [PATCH 153/258] Standardize bisect progress output on "failing" over "failed". --- lib/rspec/core/formatters/bisect_progress_formatter.rb | 2 +- spec/integration/bisect_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/rspec/core/formatters/bisect_progress_formatter.rb b/lib/rspec/core/formatters/bisect_progress_formatter.rb index bcf4288ca3..3b12c219ef 100644 --- a/lib/rspec/core/formatters/bisect_progress_formatter.rb +++ b/lib/rspec/core/formatters/bisect_progress_formatter.rb @@ -19,7 +19,7 @@ def bisect_starting(notification) end def bisect_original_run_complete(notification) - failures = Helpers.pluralize(notification.failed_example_ids.size, "failed example") + failures = Helpers.pluralize(notification.failed_example_ids.size, "failing example") non_failures = Helpers.pluralize(notification.non_failing_example_ids.size, "non-failing example") output.puts " (#{Helpers.format_duration(notification.duration)})" diff --git a/spec/integration/bisect_spec.rb b/spec/integration/bisect_spec.rb index c63a7fbf88..e46663e3b1 100644 --- a/spec/integration/bisect_spec.rb +++ b/spec/integration/bisect_spec.rb @@ -22,7 +22,7 @@ def bisect(cli_args, expected_status=nil) expect(output).to eq(<<-EOS.gsub(/^\s+\|/, '')) |Bisect started using options: "spec/rspec/core/resources/order_dependent_specs.rb --order defined" |Running suite to find failures... (n.nnnn seconds) - |Starting bisect with 1 failed example and 21 non-failing examples. + |Starting bisect with 1 failing example and 21 non-failing examples. | |Round 1: searching for 11 non-failing examples (of 21) to ignore: .. (n.nnnn seconds) |Round 2: searching for 6 non-failing examples (of 11) to ignore: . (n.nnnn seconds) @@ -138,7 +138,7 @@ def bisect_round_finished(notification) expect(output).to eq(<<-EOS.gsub(/^\s+\|/, '')) |Bisect started using options: "spec/rspec/core/resources/order_dependent_specs.rb --order defined" |Running suite to find failures... (n.nnnn seconds) - |Starting bisect with 1 failed example and 21 non-failing examples. + |Starting bisect with 1 failing example and 21 non-failing examples. | |Round 1: searching for 11 non-failing examples (of 21) to ignore: .. (n.nnnn seconds) |Round 2: searching for 6 non-failing examples (of 11) to ignore: . From eb5c5fe9b47afc9650857ae94c023693c4d2b36c Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Wed, 1 Apr 2015 00:12:41 -0700 Subject: [PATCH 154/258] Ignore flapping examples that did not fail on original run. We only care that all the failures from the original run are still failing; additional failures can be ignored. --- lib/rspec/core/bisect/example_minimizer.rb | 6 ++--- .../core/bisect/example_minimizer_spec.rb | 24 ++++++++++++++++--- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/lib/rspec/core/bisect/example_minimizer.rb b/lib/rspec/core/bisect/example_minimizer.rb index f52085ecc5..f8a6022daf 100644 --- a/lib/rspec/core/bisect/example_minimizer.rb +++ b/lib/rspec/core/bisect/example_minimizer.rb @@ -22,7 +22,7 @@ def find_minimal_repro each_bisect_round do |subsets| ids_to_ignore = subsets.find do |ids| - get_same_failures?(remaining_ids - ids) + get_expected_failures_for?(remaining_ids - ids) end next :done unless ids_to_ignore @@ -68,7 +68,7 @@ def non_failing_example_ids @non_failing_example_ids ||= all_example_ids - failed_example_ids end - def get_same_failures?(ids) + def get_expected_failures_for?(ids) ids_to_run = ids + failed_example_ids notify(:bisect_individual_run_start, :command => runner.repro_command_from(ids_to_run)) @@ -76,7 +76,7 @@ def get_same_failures?(ids) notify(:bisect_individual_run_complete, :duration => duration, :results => results) abort_if_ordering_inconsistent(results) - results.failed_example_ids == failed_example_ids + (failed_example_ids & results.failed_example_ids) == failed_example_ids end INFINITY = (1.0 / 0) # 1.8.7 doesn't define Float::INFINITY so we define our own... diff --git a/spec/rspec/core/bisect/example_minimizer_spec.rb b/spec/rspec/core/bisect/example_minimizer_spec.rb index e0a85f73e2..0ff4bdf23a 100644 --- a/spec/rspec/core/bisect/example_minimizer_spec.rb +++ b/spec/rspec/core/bisect/example_minimizer_spec.rb @@ -30,13 +30,31 @@ def repro_command_from(locations) end end - it 'repeatedly runs various subsets of the suite, removing examples that have no effect on the failing examples' do - minimizer = Bisect::ExampleMinimizer.new(FakeRunner.new( + let(:fake_runner) do + FakeRunner.new( %w[ ex_1 ex_2 ex_3 ex_4 ex_5 ex_6 ex_7 ex_8 ], %w[ ex_2 ], { "ex_5" => "ex_4" } - ), RSpec::Core::NullReporter) + ) + end + + it 'repeatedly runs various subsets of the suite, removing examples that have no effect on the failing examples' do + minimizer = Bisect::ExampleMinimizer.new(fake_runner, RSpec::Core::NullReporter) + minimizer.find_minimal_repro + expect(minimizer.repro_command_for_currently_needed_ids).to eq("rspec ex_2 ex_4 ex_5") + end + + it 'ignores flapping examples that did not fail on the initial full run but fail on fail runs' do + def fake_runner.run(ids) + super.tap do |results| + @run_count ||= 0 + if (@run_count += 1) > 1 + results.failed_example_ids << "ex_8" + end + end + end + minimizer = Bisect::ExampleMinimizer.new(fake_runner, RSpec::Core::NullReporter) minimizer.find_minimal_repro expect(minimizer.repro_command_for_currently_needed_ids).to eq("rspec ex_2 ex_4 ex_5") end From fc959783352642143f754b9b1f32dcbdd9f94d23 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Wed, 1 Apr 2015 00:19:29 -0700 Subject: [PATCH 155/258] Fix help text grammar. --- lib/rspec/core/option_parser.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rspec/core/option_parser.rb b/lib/rspec/core/option_parser.rb index a0d3077ad5..1d46f3c18b 100644 --- a/lib/rspec/core/option_parser.rb +++ b/lib/rspec/core/option_parser.rb @@ -62,7 +62,7 @@ def parser(options) options[:order] = "rand:#{seed}" end - parser.on('--bisect', 'Repeatedly runs the suite in order to isolates the failures to the ', + parser.on('--bisect', 'Repeatedly runs the suite in order to isolate the failures to the ', ' smallest reproducible case.') do bisect_and_exit end From 0e916fb5b93d1a686ec227ac0de90b85d5a90367 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Wed, 1 Apr 2015 00:26:44 -0700 Subject: [PATCH 156/258] Add changelog entry. --- Changelog.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Changelog.md b/Changelog.md index a15270fa7d..bba50f57c2 100644 --- a/Changelog.md +++ b/Changelog.md @@ -31,6 +31,9 @@ Enhancements: include the example group name (Myron Marson, #1911) * Make `let` and `subject` threadsafe. (Josh Cheek, #1858) * Add version information into the JSON formatter. (Mark Swinson, #1883) +* Add `--bisect` CLI option, which will repeatedly run your suite in + order to isolate the failures to the smallest reproducible case. + (Myron Marston, #1917) Bug Fixes: From 361b6d5f0104665097c171525c859963de0dfa9a Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Wed, 1 Apr 2015 10:21:53 -0700 Subject: [PATCH 157/258] Skip specs that are failing on AppVeyor on Ruby 2.1. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I have no idea why these pass on 1.9.3 but not on 2.1 on AppVeyor. It would be nice to get them to pass but I don’t have access to a windows box and lack to the time fix it now. --- spec/integration/bisect_spec.rb | 4 ++++ spec/rspec/core/bisect/runner_spec.rb | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/spec/integration/bisect_spec.rb b/spec/integration/bisect_spec.rb index e46663e3b1..a62b485586 100644 --- a/spec/integration/bisect_spec.rb +++ b/spec/integration/bisect_spec.rb @@ -4,6 +4,10 @@ module RSpec::Core RSpec.describe "Bisect", :slow, :simulate_shell_allowing_unquoted_ids do include FormatterSupport + before do + skip "These specs do not consistently pass or fail on AppVeyor on Ruby 2.1+" + end if ENV['APPVEYOR'] && RUBY_VERSION.to_f > 2.0 + def bisect(cli_args, expected_status=nil) RSpec.configuration.output_stream = formatter_output parser = Parser.new(cli_args + ["--bisect"]) diff --git a/spec/rspec/core/bisect/runner_spec.rb b/spec/rspec/core/bisect/runner_spec.rb index 2f8da15318..ee258afc5e 100644 --- a/spec/rspec/core/bisect/runner_spec.rb +++ b/spec/rspec/core/bisect/runner_spec.rb @@ -27,6 +27,10 @@ module RSpec::Core end it 'ensures environment variables are propagated to the spawned process', :slow do + if ENV['APPVEYOR'] && RUBY_VERSION.to_f > 2.0 + skip "These specs do not consistently pass or fail on AppVeyor on Ruby 2.1+" + end + output = nil allow(server).to receive(:capture_run_results) do |&block| output = block.call From ee718e07074bb169c00d213ae5b075b8f86e8b56 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Mon, 6 Apr 2015 16:34:11 -0700 Subject: [PATCH 158/258] Forwardport 3.2.3 release notes. --- Changelog.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Changelog.md b/Changelog.md index a15270fa7d..d72b63b564 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,5 @@ ### Development -[Full Changelog](http://github.com/rspec/rspec-core/compare/v3.2.2...master) +[Full Changelog](http://github.com/rspec/rspec-core/compare/v3.2.3...master) Enhancements: @@ -39,11 +39,17 @@ Bug Fixes: Windows. (Myron Marston, #1887) * Fix `RSpec::Core::RakeTask#failure_message` so that it gets printed when the task failed. (Myron Marston, #1905) +* Make `let` work properly when defined in a shared context that is applied + to an individual example via metadata. (Myron Marston, #1912) + +### 3.2.3 / 2015-04-06 +[Full Changelog](http://github.com/rspec/rspec-core/compare/v3.2.2...v3.2.3) + +Bug Fixes: + * Fix how the DSL methods are defined so that RSpec is compatible with gems that define methods of the same name on `Kernel` (such as the `its-it` gem). (Alex Kwiatkowski, Ryan Ong, #1907) -* Make `let` work properly when defined in a shared context that is applied - to an individual example via metadata. (Myron Marston, #1912) * Fix `before(:context) { skip }` so that it does not wrongly cause the spec suite to exit with a non-zero status when no examples failed. (Myron Marston, #1926) From 9e90efe325722d195d286b45d836a88b376a2b10 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Tue, 7 Apr 2015 00:05:16 -0700 Subject: [PATCH 159/258] Fix oddly worded sentences. --- features/command_line/bisect.feature | 2 +- spec/rspec/core/bisect/example_minimizer_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/features/command_line/bisect.feature b/features/command_line/bisect.feature index d3b429daf4..dfc976144f 100644 --- a/features/command_line/bisect.feature +++ b/features/command_line/bisect.feature @@ -2,7 +2,7 @@ Feature: Bisect RSpec's `--order random` and `--seed` options help surface flickering examples that only fail when one or more other examples are executed first. It can be very difficult to isolate the exact combination of examples that triggers the failure. The `--bisect` flag helps solve that problem. - Pass the `--bisect` option (in addition to `--seed` any other options) and RSpec will repeatedly run subsets of your suite in order to isolate the minimal set of examples that reproduce the failure. + Pass the `--bisect` option (in addition to `--seed` and any other options) and RSpec will repeatedly run subsets of your suite in order to isolate the minimal set of examples that reproduce the failure. Scenario: Use `--bisect` flag to create a minimal repro case for the ordering dependency Given a file named "lib/calculator.rb" with: diff --git a/spec/rspec/core/bisect/example_minimizer_spec.rb b/spec/rspec/core/bisect/example_minimizer_spec.rb index 0ff4bdf23a..d14994343a 100644 --- a/spec/rspec/core/bisect/example_minimizer_spec.rb +++ b/spec/rspec/core/bisect/example_minimizer_spec.rb @@ -44,7 +44,7 @@ def repro_command_from(locations) expect(minimizer.repro_command_for_currently_needed_ids).to eq("rspec ex_2 ex_4 ex_5") end - it 'ignores flapping examples that did not fail on the initial full run but fail on fail runs' do + it 'ignores flapping examples that did not fail on the initial full run but fail on later runs' do def fake_runner.run(ids) super.tap do |results| @run_count ||= 0 From 20c9709ec1236f0e49c89cd3396c0665cdf11732 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Tue, 7 Apr 2015 10:04:45 -0700 Subject: [PATCH 160/258] Prefer localhost over 127.0.0.1. 127.0.0.1 is ipv4, whereas localhost should work for ipv4 and ipv6. --- lib/rspec/core/formatters/bisect_formatter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rspec/core/formatters/bisect_formatter.rb b/lib/rspec/core/formatters/bisect_formatter.rb index 3f404d040c..60d86789a6 100644 --- a/lib/rspec/core/formatters/bisect_formatter.rb +++ b/lib/rspec/core/formatters/bisect_formatter.rb @@ -17,7 +17,7 @@ class BisectFormatter def initialize(_output) port = RSpec.configuration.drb_port - drb_uri = "druby://127.0.0.1:#{port}" + drb_uri = "druby://localhost:#{port}" @all_example_ids = [] @failed_example_ids = [] @bisect_server = DRbObject.new_with_uri(drb_uri) From f5483c45401a397a5b955b737e3b8a18989b5770 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Tue, 7 Apr 2015 10:14:51 -0700 Subject: [PATCH 161/258] Change how we whitelist open3. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This allows it to be loaded by the bisect runner but if it’s loaded by other things it’ll notify by causing the “minimizes stdlibs” spec to fail. --- spec/rspec/core_spec.rb | 1 - spec/support/fake_libs/open3.rb | 9 +++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 spec/support/fake_libs/open3.rb diff --git a/spec/rspec/core_spec.rb b/spec/rspec/core_spec.rb index 1902167562..a8f528404a 100644 --- a/spec/rspec/core_spec.rb +++ b/spec/rspec/core_spec.rb @@ -3,7 +3,6 @@ RSpec.describe RSpec do fake_libs = File.expand_path('../../support/fake_libs', __FILE__) allowed_loaded_features = [ - /open3.rb/, # Used by Bisect::Runner, which is not normally loaded /optparse\.rb/, # Used by OptionParser. /rbconfig\.rb/, # loaded by rspec-support for OS detection. /shellwords\.rb/, # used by ConfigurationOptions and RakeTask. diff --git a/spec/support/fake_libs/open3.rb b/spec/support/fake_libs/open3.rb new file mode 100644 index 0000000000..596d503017 --- /dev/null +++ b/spec/support/fake_libs/open3.rb @@ -0,0 +1,9 @@ +unless caller.any? { |line| line.include?("rspec/core/bisect/runner.rb") } + raise "open3 loaded from unexpected file. " \ + "It is allowed to be loaded by the Bisect::Runner " \ + "because that is not loaded in the same process as end-user code, " \ + "and we generally don't want open3 loaded for other things." +end + +module Open3 +end From 01e567fc343760ab2071554978c183f176708084 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Wed, 8 Apr 2015 00:09:16 -0700 Subject: [PATCH 162/258] Remove slow spec that's not needed. I wasn't sure that ENV vars would be propagated to spawned process properly, but no logic was required to make that work. I tried backticks and `system` as well and those also do the right thing. Given that, there's no need to keep this slow spec around. Revert "Add spec demonstrating that ENV vars are propagated properly." This reverts commit 45ed50c512c824ddf3cd87c0c69a2ffe964493f1. --- spec/rspec/core/bisect/runner_spec.rb | 18 ------------------ spec/rspec/core/resources/echo_env_var.rb | 1 - 2 files changed, 19 deletions(-) delete mode 100644 spec/rspec/core/resources/echo_env_var.rb diff --git a/spec/rspec/core/bisect/runner_spec.rb b/spec/rspec/core/bisect/runner_spec.rb index ee258afc5e..fd86909c96 100644 --- a/spec/rspec/core/bisect/runner_spec.rb +++ b/spec/rspec/core/bisect/runner_spec.rb @@ -25,24 +25,6 @@ module RSpec::Core runner.run(%w[ spec/1_spec.rb[1:1] spec/1_spec.rb[1:2] ]) end - - it 'ensures environment variables are propagated to the spawned process', :slow do - if ENV['APPVEYOR'] && RUBY_VERSION.to_f > 2.0 - skip "These specs do not consistently pass or fail on AppVeyor on Ruby 2.1+" - end - - output = nil - allow(server).to receive(:capture_run_results) do |&block| - output = block.call - Formatters::BisectFormatter::RunResults.new([], []) - end - - with_env_vars 'MY_ENV_VAR' => 'secret' do - runner.run(%w[ spec/rspec/core/resources/echo_env_var.rb ]) - end - - expect(output).to include("MY_ENV_VAR=secret") - end end describe "#command_for" do diff --git a/spec/rspec/core/resources/echo_env_var.rb b/spec/rspec/core/resources/echo_env_var.rb deleted file mode 100644 index afb80adbfc..0000000000 --- a/spec/rspec/core/resources/echo_env_var.rb +++ /dev/null @@ -1 +0,0 @@ -puts "MY_ENV_VAR=#{ENV['MY_ENV_VAR']}" if ENV.key?("MY_ENV_VAR") From dc07521fef50ea115be04278042b3487e6da0465 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Wed, 8 Apr 2015 00:20:39 -0700 Subject: [PATCH 163/258] Move bisect integration tests into cucumber scenarios. - This surfaces the output to the user so they know what to expect based on reading the relish docs. - This greatly reduces the runtime of our spec suite, from ~11.8 seconds to ~6.9 seconds. --- features/command_line/bisect.feature | 119 ++++++++++++++- .../step_definitions/additional_cli_steps.rb | 24 +++ features/support/send_sigint_during_bisect.rb | 21 +++ spec/integration/bisect_spec.rb | 121 --------------- spec/rspec/core/bisect/coordinator_spec.rb | 139 ++++++++++++++++++ .../core/bisect/example_minimizer_spec.rb | 33 +---- .../core/resources/order_dependent_specs.rb | 33 ----- spec/support/fake_bisect_runner.rb | 23 +++ 8 files changed, 327 insertions(+), 186 deletions(-) create mode 100644 features/support/send_sigint_during_bisect.rb create mode 100644 spec/rspec/core/bisect/coordinator_spec.rb delete mode 100644 spec/rspec/core/resources/order_dependent_specs.rb create mode 100644 spec/support/fake_bisect_runner.rb diff --git a/features/command_line/bisect.feature b/features/command_line/bisect.feature index dfc976144f..ee1a6c49d9 100644 --- a/features/command_line/bisect.feature +++ b/features/command_line/bisect.feature @@ -2,9 +2,13 @@ Feature: Bisect RSpec's `--order random` and `--seed` options help surface flickering examples that only fail when one or more other examples are executed first. It can be very difficult to isolate the exact combination of examples that triggers the failure. The `--bisect` flag helps solve that problem. - Pass the `--bisect` option (in addition to `--seed` and any other options) and RSpec will repeatedly run subsets of your suite in order to isolate the minimal set of examples that reproduce the failure. + Pass the `--bisect` option (in addition to `--seed` and any other options) and RSpec will repeatedly run subsets of your suite in order to isolate the minimal set of examples that reproduce the same failures. - Scenario: Use `--bisect` flag to create a minimal repro case for the ordering dependency + At any point during the bisect run, you can hit ctrl-c to abort and it will provide you with the most minimal reproduction command it has discovered so far. + + To get more detailed output (particularly useful if you want to report a bug with bisect), you can set the `DEBUG_RSPEC_BISECT` environment variable. + + Background: Given a file named "lib/calculator.rb" with: """ruby class Calculator @@ -40,9 +44,118 @@ Feature: Bisect end end """ + + Scenario: Use `--bisect` flag to create a minimal repro case for the ordering dependency When I run `rspec --seed 1234` Then the output should contain "10 examples, 1 failure" When I run `rspec --seed 1234 --bisect` - Then the output should contain "rspec ./spec/calculator_10_spec.rb[1:1] ./spec/calculator_1_spec.rb[1:1] --seed 1234" + Then bisect should succeed with output like: + """ + Bisect started using options: "--seed 1234" + Running suite to find failures... (0.16755 seconds) + Starting bisect with 1 failing example and 9 non-failing examples. + + Round 1: searching for 5 non-failing examples (of 9) to ignore: .. (0.30166 seconds) + Round 2: searching for 3 non-failing examples (of 5) to ignore: .. (0.30306 seconds) + Round 3: searching for 2 non-failing examples (of 3) to ignore: .. (0.33292 seconds) + Round 4: searching for 1 non-failing example (of 2) to ignore: . (0.16476 seconds) + Round 5: searching for 1 non-failing example (of 1) to ignore: . (0.15329 seconds) + Bisect complete! Reduced necessary non-failing examples from 9 to 1 in 1.26 seconds. + + The minimal reproduction command is: + rspec ./spec/calculator_10_spec.rb[1:1] ./spec/calculator_1_spec.rb[1:1] --seed 1234 + """ + When I run `rspec ./spec/calculator_10_spec.rb[1:1] ./spec/calculator_1_spec.rb[1:1] --seed 1234` + Then the output should contain "2 examples, 1 failure" + + Scenario: Ctrl-C can be used to abort the bisect early and get the most minimal command it has discovered so far + When I run `rspec --seed 1234 --bisect` and abort in the middle with ctrl-c + Then bisect should fail with output like: + """ + Bisect started using options: "--seed 1234" + Running suite to find failures... (0.17102 seconds) + Starting bisect with 1 failing example and 9 non-failing examples. + + Round 1: searching for 5 non-failing examples (of 9) to ignore: .. (0.32943 seconds) + Round 2: searching for 3 non-failing examples (of 5) to ignore: .. (0.3154 seconds) + Round 3: searching for 2 non-failing examples (of 3) to ignore: .. + + Bisect aborted! + + The most minimal reproduction command discovered so far is: + rspec ./spec/calculator_10_spec.rb[1:1] ./spec/calculator_1_spec.rb[1:1] ./spec/calculator_3_spec.rb[1:1] --seed 1234 + """ + When I run `rspec ./spec/calculator_10_spec.rb[1:1] ./spec/calculator_1_spec.rb[1:1] ./spec/calculator_3_spec.rb[1:1] --seed 1234` + Then the output should contain "3 examples, 1 failure" + + Scenario: Use DEBUG_RSPEC_BISECT=1 to enable verbose debug mode for more detail + When I run `rspec --seed 1234 --bisect` with `DEBUG_RSPEC_BISECT=1` set + Then bisect should succeed with output like: + """ + Bisect started using options: "--seed 1234" + Running suite to find failures... (0.16528 seconds) + - Failing examples (1): + - ./spec/calculator_1_spec.rb[1:1] + - Non-failing examples (9): + - ./spec/calculator_10_spec.rb[1:1] + - ./spec/calculator_2_spec.rb[1:1] + - ./spec/calculator_3_spec.rb[1:1] + - ./spec/calculator_4_spec.rb[1:1] + - ./spec/calculator_5_spec.rb[1:1] + - ./spec/calculator_6_spec.rb[1:1] + - ./spec/calculator_7_spec.rb[1:1] + - ./spec/calculator_8_spec.rb[1:1] + - ./spec/calculator_9_spec.rb[1:1] + + Round 1: searching for 5 non-failing examples (of 9) to ignore: + - Running: rspec ./spec/calculator_1_spec.rb[1:1] ./spec/calculator_6_spec.rb[1:1] ./spec/calculator_7_spec.rb[1:1] ./spec/calculator_8_spec.rb[1:1] ./spec/calculator_9_spec.rb[1:1] --seed 1234 (0.15302 seconds) + - Running: rspec ./spec/calculator_10_spec.rb[1:1] ./spec/calculator_1_spec.rb[1:1] ./spec/calculator_2_spec.rb[1:1] ./spec/calculator_3_spec.rb[1:1] ./spec/calculator_4_spec.rb[1:1] ./spec/calculator_5_spec.rb[1:1] --seed 1234 (0.19708 seconds) + - Examples we can safely ignore (4): + - ./spec/calculator_6_spec.rb[1:1] + - ./spec/calculator_7_spec.rb[1:1] + - ./spec/calculator_8_spec.rb[1:1] + - ./spec/calculator_9_spec.rb[1:1] + - Remaining non-failing examples (5): + - ./spec/calculator_10_spec.rb[1:1] + - ./spec/calculator_2_spec.rb[1:1] + - ./spec/calculator_3_spec.rb[1:1] + - ./spec/calculator_4_spec.rb[1:1] + - ./spec/calculator_5_spec.rb[1:1] + - Round finished (0.35172 seconds) + Round 2: searching for 3 non-failing examples (of 5) to ignore: + - Running: rspec ./spec/calculator_1_spec.rb[1:1] ./spec/calculator_4_spec.rb[1:1] ./spec/calculator_5_spec.rb[1:1] --seed 1234 (0.15836 seconds) + - Running: rspec ./spec/calculator_10_spec.rb[1:1] ./spec/calculator_1_spec.rb[1:1] ./spec/calculator_2_spec.rb[1:1] ./spec/calculator_3_spec.rb[1:1] --seed 1234 (0.19065 seconds) + - Examples we can safely ignore (2): + - ./spec/calculator_4_spec.rb[1:1] + - ./spec/calculator_5_spec.rb[1:1] + - Remaining non-failing examples (3): + - ./spec/calculator_10_spec.rb[1:1] + - ./spec/calculator_2_spec.rb[1:1] + - ./spec/calculator_3_spec.rb[1:1] + - Round finished (0.35022 seconds) + Round 3: searching for 2 non-failing examples (of 3) to ignore: + - Running: rspec ./spec/calculator_1_spec.rb[1:1] ./spec/calculator_2_spec.rb[1:1] --seed 1234 (0.21028 seconds) + - Running: rspec ./spec/calculator_10_spec.rb[1:1] ./spec/calculator_1_spec.rb[1:1] ./spec/calculator_3_spec.rb[1:1] --seed 1234 (0.1975 seconds) + - Examples we can safely ignore (1): + - ./spec/calculator_2_spec.rb[1:1] + - Remaining non-failing examples (2): + - ./spec/calculator_10_spec.rb[1:1] + - ./spec/calculator_3_spec.rb[1:1] + - Round finished (0.40882 seconds) + Round 4: searching for 1 non-failing example (of 2) to ignore: + - Running: rspec ./spec/calculator_10_spec.rb[1:1] ./spec/calculator_1_spec.rb[1:1] --seed 1234 (0.17173 seconds) + - Examples we can safely ignore (1): + - ./spec/calculator_3_spec.rb[1:1] + - Remaining non-failing examples (1): + - ./spec/calculator_10_spec.rb[1:1] + - Round finished (0.17234 seconds) + Round 5: searching for 1 non-failing example (of 1) to ignore: + - Running: rspec ./spec/calculator_1_spec.rb[1:1] --seed 1234 (0.18279 seconds) + - Round finished (0.18312 seconds) + Bisect complete! Reduced necessary non-failing examples from 9 to 1 in 1.47 seconds. + + The minimal reproduction command is: + rspec ./spec/calculator_10_spec.rb[1:1] ./spec/calculator_1_spec.rb[1:1] --seed 1234 + """ When I run `rspec ./spec/calculator_10_spec.rb[1:1] ./spec/calculator_1_spec.rb[1:1] --seed 1234` Then the output should contain "2 examples, 1 failure" diff --git a/features/step_definitions/additional_cli_steps.rb b/features/step_definitions/additional_cli_steps.rb index 36fa09eab3..2aa4ff34c8 100644 --- a/features/step_definitions/additional_cli_steps.rb +++ b/features/step_definitions/additional_cli_steps.rb @@ -1,5 +1,7 @@ require 'rspec/core' # to fix annoying "undefined method `configuration' for RSpec:Module (NoMethodError)" +require './spec/support/formatter_support' + Then /^the output should contain all of these:$/ do |table| table.raw.flatten.each do |string| assert_partial_output(string, all_output) @@ -164,3 +166,25 @@ EOS end end + +Then(/^bisect should (succeed|fail) with output like:$/) do |succeed, expected_output| + last_process = only_processes.last + expect(last_exit_status).to eq(succeed == "succeed" ? 0 : 1) + + expected = normalize_durations(expected_output) + actual = normalize_durations(last_process.stdout) + + expect(actual.sub(/\n+\Z/, '')).to eq(expected) +end + +When(/^I run `([^`]+)` with `([^=]+)=([^`]+)` set$/) do |cmd, env_key, env_value| + set_env(env_key, env_value) + step "I run `#{cmd}`" +end + +When(/^I run `([^`]+)` and abort in the middle with ctrl\-c$/) do |cmd| + set_env('RUBYOPT', ENV['RUBYOPT'] + " -r#{File.expand_path("../../support/send_sigint_during_bisect.rb", __FILE__)}") + step "I run `#{cmd}`" +end + +World(FormatterSupport) diff --git a/features/support/send_sigint_during_bisect.rb b/features/support/send_sigint_during_bisect.rb new file mode 100644 index 0000000000..9002f73635 --- /dev/null +++ b/features/support/send_sigint_during_bisect.rb @@ -0,0 +1,21 @@ +require 'rspec/core' +RSpec::Support.require_rspec_core "formatters/bisect_progress_formatter" + +module RSpec::Core::Formatters + BisectProgressFormatter = Class.new(remove_const :BisectProgressFormatter) do + RSpec::Core::Formatters.register self + + def bisect_round_finished(notification) + return super unless notification.round == 3 + + Process.kill("INT", Process.pid) + # Process.kill is not a synchronous call, so to ensure the output + # below aborts at a deterministic place, we need to block here. + # The sleep will be interrupted by the signal once the OS sends it. + # For the most part, this is only needed on JRuby, but we saw + # the asynchronous behavior on an MRI 2.0 travis build as well. + sleep 5 + end + end +end + diff --git a/spec/integration/bisect_spec.rb b/spec/integration/bisect_spec.rb index a62b485586..6d673f3418 100644 --- a/spec/integration/bisect_spec.rb +++ b/spec/integration/bisect_spec.rb @@ -20,78 +20,6 @@ def bisect(cli_args, expected_status=nil) normalize_durations(formatter_output.string) end - it 'finds the minimum rerun command and exits' do - output = bisect(%w[spec/rspec/core/resources/order_dependent_specs.rb --order defined], 0) - - expect(output).to eq(<<-EOS.gsub(/^\s+\|/, '')) - |Bisect started using options: "spec/rspec/core/resources/order_dependent_specs.rb --order defined" - |Running suite to find failures... (n.nnnn seconds) - |Starting bisect with 1 failing example and 21 non-failing examples. - | - |Round 1: searching for 11 non-failing examples (of 21) to ignore: .. (n.nnnn seconds) - |Round 2: searching for 6 non-failing examples (of 11) to ignore: . (n.nnnn seconds) - |Round 3: searching for 3 non-failing examples (of 5) to ignore: . (n.nnnn seconds) - |Round 4: searching for 1 non-failing example (of 2) to ignore: . (n.nnnn seconds) - |Round 5: searching for 1 non-failing example (of 1) to ignore: . (n.nnnn seconds) - |Bisect complete! Reduced necessary non-failing examples from 21 to 1 in n.nnnn seconds. - | - |The minimal reproduction command is: - | rspec ./spec/rspec/core/resources/order_dependent_specs.rb[11:1,22:1] --order defined - EOS - end - - it 'supports a verbose mode via `DEBUG_RSPEC_BISECT` so we can get detailed log output from users when they report bugs' do - with_env_vars 'DEBUG_RSPEC_BISECT' => '1' do - output = bisect(%w[spec/rspec/core/resources/order_dependent_specs.rb --order defined], 0) - - expect(output).to eq(<<-EOS.gsub(/^\s+\|/, '')) - |Bisect started using options: "spec/rspec/core/resources/order_dependent_specs.rb --order defined" - |Running suite to find failures... (n.nnnn seconds) - | - Failing examples (1): - | - ./spec/rspec/core/resources/order_dependent_specs.rb[22:1] - | - Non-failing examples (21): - | - ./spec/rspec/core/resources/order_dependent_specs.rb[1:1,2:1,3:1,4:1,5:1,6:1,7:1,8:1,9:1,10:1,11:1,12:1,13:1,14:1,15:1,16:1,17:1,18:1,19:1,20:1,21:1] - | - |Round 1: searching for 11 non-failing examples (of 21) to ignore: - | - Running: rspec ./spec/rspec/core/resources/order_dependent_specs.rb[12:1,13:1,14:1,15:1,16:1,17:1,18:1,19:1,20:1,21:1,22:1] --order defined (n.nnnn seconds) - | - Running: rspec ./spec/rspec/core/resources/order_dependent_specs.rb[1:1,2:1,3:1,4:1,5:1,6:1,7:1,8:1,9:1,10:1,11:1,22:1] --order defined (n.nnnn seconds) - | - Examples we can safely ignore (10): - | - ./spec/rspec/core/resources/order_dependent_specs.rb[12:1,13:1,14:1,15:1,16:1,17:1,18:1,19:1,20:1,21:1] - | - Remaining non-failing examples (11): - | - ./spec/rspec/core/resources/order_dependent_specs.rb[1:1,2:1,3:1,4:1,5:1,6:1,7:1,8:1,9:1,10:1,11:1] - | - Round finished (n.nnnn seconds) - |Round 2: searching for 6 non-failing examples (of 11) to ignore: - | - Running: rspec ./spec/rspec/core/resources/order_dependent_specs.rb[7:1,8:1,9:1,10:1,11:1,22:1] --order defined (n.nnnn seconds) - | - Examples we can safely ignore (6): - | - ./spec/rspec/core/resources/order_dependent_specs.rb[1:1,2:1,3:1,4:1,5:1,6:1] - | - Remaining non-failing examples (5): - | - ./spec/rspec/core/resources/order_dependent_specs.rb[7:1,8:1,9:1,10:1,11:1] - | - Round finished (n.nnnn seconds) - |Round 3: searching for 3 non-failing examples (of 5) to ignore: - | - Running: rspec ./spec/rspec/core/resources/order_dependent_specs.rb[10:1,11:1,22:1] --order defined (n.nnnn seconds) - | - Examples we can safely ignore (3): - | - ./spec/rspec/core/resources/order_dependent_specs.rb[7:1,8:1,9:1] - | - Remaining non-failing examples (2): - | - ./spec/rspec/core/resources/order_dependent_specs.rb[10:1,11:1] - | - Round finished (n.nnnn seconds) - |Round 4: searching for 1 non-failing example (of 2) to ignore: - | - Running: rspec ./spec/rspec/core/resources/order_dependent_specs.rb[11:1,22:1] --order defined (n.nnnn seconds) - | - Examples we can safely ignore (1): - | - ./spec/rspec/core/resources/order_dependent_specs.rb[10:1] - | - Remaining non-failing examples (1): - | - ./spec/rspec/core/resources/order_dependent_specs.rb[11:1] - | - Round finished (n.nnnn seconds) - |Round 5: searching for 1 non-failing example (of 1) to ignore: - | - Running: rspec ./spec/rspec/core/resources/order_dependent_specs.rb[22:1] --order defined (n.nnnn seconds) - | - Round finished (n.nnnn seconds) - |Bisect complete! Reduced necessary non-failing examples from 21 to 1 in n.nnnn seconds. - | - |The minimal reproduction command is: - | rspec ./spec/rspec/core/resources/order_dependent_specs.rb[11:1,22:1] --order defined - EOS - end - end - context "when a load-time problem occurs while running the suite" do it 'surfaces the stdout and stderr output to the user' do output = bisect(%w[spec/rspec/core/resources/fail_on_load_spec.rb_], 1) @@ -105,54 +33,5 @@ def bisect(cli_args, expected_status=nil) expect(output).to include("Bisect failed!", "The example ordering is inconsistent") end end - - context "when the user aborts the bisect with ctrl-c" do - before do - formatter_subclass = Class.new(Formatters::BisectProgressFormatter) do - Formatters.register self, :bisect_round_finished - - def bisect_round_finished(notification) - if notification.round == 2 - Process.kill("INT", Process.pid) - # Process.kill is not a synchronous call, so to ensure the output - # below aborts at a deterministic place, we need to block here. - # The sleep will be interrupted by the signal once the OS sends it. - # For the most part, this is only needed on JRuby, but we saw - # the asynchronous behavior on an MRI 2.0 travis build as well. - sleep 5 - else - super - end - end - end - - stub_const(Formatters::BisectProgressFormatter.name, formatter_subclass) - end - - it "prints the most minimal repro command it has found so far" do - expect { - bisect(%w[spec/rspec/core/resources/order_dependent_specs.rb --order defined]) - }.to raise_error(an_object_having_attributes( - :class => SystemExit, - :status => 1 - )) - - output = normalize_durations(formatter_output.string) - - expect(output).to eq(<<-EOS.gsub(/^\s+\|/, '')) - |Bisect started using options: "spec/rspec/core/resources/order_dependent_specs.rb --order defined" - |Running suite to find failures... (n.nnnn seconds) - |Starting bisect with 1 failing example and 21 non-failing examples. - | - |Round 1: searching for 11 non-failing examples (of 21) to ignore: .. (n.nnnn seconds) - |Round 2: searching for 6 non-failing examples (of 11) to ignore: . - | - |Bisect aborted! - | - |The most minimal reproduction command discovered so far is: - | rspec ./spec/rspec/core/resources/order_dependent_specs.rb[7:1,8:1,9:1,10:1,11:1,22:1] --order defined - EOS - end - end end end diff --git a/spec/rspec/core/bisect/coordinator_spec.rb b/spec/rspec/core/bisect/coordinator_spec.rb new file mode 100644 index 0000000000..d562812cb8 --- /dev/null +++ b/spec/rspec/core/bisect/coordinator_spec.rb @@ -0,0 +1,139 @@ +require 'rspec/core/bisect/coordinator' +require 'support/fake_bisect_runner' +require 'support/formatter_support' + +module RSpec::Core + RSpec.describe Bisect::Coordinator, :simulate_shell_allowing_unquoted_ids do + include FormatterSupport + + let(:fake_runner) do + FakeBisectRunner.new( + 1.upto(8).map { |i| "#{i}.rb[1:1]" }, + %w[ 2.rb[1:1] ], + { "5.rb[1:1]" => "4.rb[1:1]" } + ) + end + + def find_minimal_repro + allow(Bisect::Server).to receive(:run).and_yield(instance_double(Bisect::Server)) + allow(Bisect::Runner).to receive(:new).and_return(fake_runner) + + formatter_output = RSpec.configuration.output_stream = StringIO.new + Bisect::Coordinator.bisect_with([], RSpec.configuration) + + normalize_durations(formatter_output.string) + end + + it 'notifies the bisect progress formatter of progress' do + output = find_minimal_repro + + expect(output).to eq(<<-EOS.gsub(/^\s+\|/, '')) + |Bisect started using options: "" + |Running suite to find failures... (n.nnnn seconds) + |Starting bisect with 2 failing examples and 6 non-failing examples. + | + |Round 1: searching for 3 non-failing examples (of 6) to ignore: .. (n.nnnn seconds) + |Round 2: searching for 2 non-failing examples (of 3) to ignore: . (n.nnnn seconds) + |Round 3: searching for 1 non-failing example (of 1) to ignore: . (n.nnnn seconds) + |Bisect complete! Reduced necessary non-failing examples from 6 to 1 in n.nnnn seconds. + | + |The minimal reproduction command is: + | rspec 2.rb[1:1] 4.rb[1:1] 5.rb[1:1] + EOS + end + + it 'can use the bisect debug formatter to get detailed progress' do + output = with_env_vars('DEBUG_RSPEC_BISECT' => '1') { find_minimal_repro } + + expect(output).to eq(<<-EOS.gsub(/^\s+\|/, '')) + |Bisect started using options: "" + |Running suite to find failures... (n.nnnn seconds) + | - Failing examples (2): + | - 2.rb[1:1] + | - 5.rb[1:1] + | - Non-failing examples (6): + | - 1.rb[1:1] + | - 3.rb[1:1] + | - 4.rb[1:1] + | - 6.rb[1:1] + | - 7.rb[1:1] + | - 8.rb[1:1] + | + |Round 1: searching for 3 non-failing examples (of 6) to ignore: + | - Running: rspec 2.rb[1:1] 5.rb[1:1] 6.rb[1:1] 7.rb[1:1] 8.rb[1:1] (n.nnnn seconds) + | - Running: rspec 1.rb[1:1] 2.rb[1:1] 3.rb[1:1] 4.rb[1:1] 5.rb[1:1] (n.nnnn seconds) + | - Examples we can safely ignore (3): + | - 6.rb[1:1] + | - 7.rb[1:1] + | - 8.rb[1:1] + | - Remaining non-failing examples (3): + | - 1.rb[1:1] + | - 3.rb[1:1] + | - 4.rb[1:1] + | - Round finished (n.nnnn seconds) + |Round 2: searching for 2 non-failing examples (of 3) to ignore: + | - Running: rspec 2.rb[1:1] 4.rb[1:1] 5.rb[1:1] (n.nnnn seconds) + | - Examples we can safely ignore (2): + | - 1.rb[1:1] + | - 3.rb[1:1] + | - Remaining non-failing examples (1): + | - 4.rb[1:1] + | - Round finished (n.nnnn seconds) + |Round 3: searching for 1 non-failing example (of 1) to ignore: + | - Running: rspec 2.rb[1:1] 5.rb[1:1] (n.nnnn seconds) + | - Round finished (n.nnnn seconds) + |Bisect complete! Reduced necessary non-failing examples from 6 to 1 in n.nnnn seconds. + | + |The minimal reproduction command is: + | rspec 2.rb[1:1] 4.rb[1:1] 5.rb[1:1] + EOS + end + + context "when the user aborst the bisect with ctrl-c" do + before do + formatter_subclass = Class.new(Formatters::BisectProgressFormatter) do + Formatters.register self + + def bisect_round_finished(notification) + return super unless notification.round == 2 + + Process.kill("INT", Process.pid) + # Process.kill is not a synchronous call, so to ensure the output + # below aborts at a deterministic place, we need to block here. + # The sleep will be interrupted by the signal once the OS sends it. + # For the most part, this is only needed on JRuby, but we saw + # the asynchronous behavior on an MRI 2.0 travis build as well. + sleep 5 + end + end + + stub_const(Formatters::BisectProgressFormatter.name, formatter_subclass) + end + + it "prints the most minimal repro command it has found so far" do + expect { + find_minimal_repro + }.to raise_error(an_object_having_attributes( + :class => SystemExit, + :status => 1 + )) + + output = normalize_durations(RSpec.configuration.output_stream.string) + + expect(output).to eq(<<-EOS.gsub(/^\s+\|/, '')) + |Bisect started using options: "" + |Running suite to find failures... (n.nnnn seconds) + |Starting bisect with 2 failing examples and 6 non-failing examples. + | + |Round 1: searching for 3 non-failing examples (of 6) to ignore: .. (n.nnnn seconds) + |Round 2: searching for 2 non-failing examples (of 3) to ignore: . + | + |Bisect aborted! + | + |The most minimal reproduction command discovered so far is: + | rspec 2.rb[1:1] 4.rb[1:1] 5.rb[1:1] + EOS + end + end + end +end diff --git a/spec/rspec/core/bisect/example_minimizer_spec.rb b/spec/rspec/core/bisect/example_minimizer_spec.rb index d14994343a..14dbb08def 100644 --- a/spec/rspec/core/bisect/example_minimizer_spec.rb +++ b/spec/rspec/core/bisect/example_minimizer_spec.rb @@ -1,37 +1,12 @@ require 'rspec/core/bisect/example_minimizer' require 'rspec/core/formatters/bisect_formatter' require 'rspec/core/bisect/server' +require 'support/fake_bisect_runner' module RSpec::Core RSpec.describe Bisect::ExampleMinimizer do - RunResults = Formatters::BisectFormatter::RunResults - - FakeRunner = Struct.new(:all_ids, :always_failures, :dependent_failures) do - def original_cli_args - [] - end - - def original_results - failures = always_failures | dependent_failures.keys - RunResults.new(all_ids, failures.sort) - end - - def run(ids) - failures = ids & always_failures - dependent_failures.each do |failing_example, depends_upon| - failures << failing_example if ids.include?(depends_upon) - end - - RunResults.new(ids.sort, failures.sort) - end - - def repro_command_from(locations) - "rspec #{locations.sort.join(' ')}" - end - end - let(:fake_runner) do - FakeRunner.new( + FakeBisectRunner.new( %w[ ex_1 ex_2 ex_3 ex_4 ex_5 ex_6 ex_7 ex_8 ], %w[ ex_2 ], { "ex_5" => "ex_4" } @@ -60,7 +35,7 @@ def fake_runner.run(ids) end it 'aborts early when no examples fail' do - minimizer = Bisect::ExampleMinimizer.new(FakeRunner.new( + minimizer = Bisect::ExampleMinimizer.new(FakeBisectRunner.new( %w[ ex_1 ex_2 ], [], {} ), RSpec::Core::NullReporter) @@ -71,7 +46,7 @@ def fake_runner.run(ids) context "when the `repro_command_for_currently_needed_ids` is queried before it has sufficient information" do it 'returns an explanation that will be printed when the bisect run is aborted immediately' do - minimizer = Bisect::ExampleMinimizer.new(FakeRunner.new([], [], {}), RSpec::Core::NullReporter) + minimizer = Bisect::ExampleMinimizer.new(FakeBisectRunner.new([], [], {}), RSpec::Core::NullReporter) expect(minimizer.repro_command_for_currently_needed_ids).to include("Not yet enough information") end end diff --git a/spec/rspec/core/resources/order_dependent_specs.rb b/spec/rspec/core/resources/order_dependent_specs.rb deleted file mode 100644 index 373bb0f19d..0000000000 --- a/spec/rspec/core/resources/order_dependent_specs.rb +++ /dev/null @@ -1,33 +0,0 @@ -# Deliberately named _specs.rb to avoid being loaded except when specified - -$global_state_for_bisect_specs = {} - -10.times do |i| - RSpec.describe "Group 1-#{i}" do - it "passes" do - end - end -end - -RSpec.describe "Group 2" do - before do - puts "Stdout output while bisecting should not be shown to the user" - end - - it "passes" do - $global_state_for_bisect_specs[:foo] = 1 - end -end - -10.times do |i| - RSpec.describe "Group 3-#{i}" do - it "passes" do - end - end -end - -RSpec.describe "Group 4" do - it "fails" do - expect($global_state_for_bisect_specs).to eq({}) - end -end diff --git a/spec/support/fake_bisect_runner.rb b/spec/support/fake_bisect_runner.rb new file mode 100644 index 0000000000..45df8edea4 --- /dev/null +++ b/spec/support/fake_bisect_runner.rb @@ -0,0 +1,23 @@ +FakeBisectRunner = Struct.new(:all_ids, :always_failures, :dependent_failures) do + def original_cli_args + [] + end + + def original_results + failures = always_failures | dependent_failures.keys + RSpec::Core::Formatters::BisectFormatter::RunResults.new(all_ids, failures.sort) + end + + def run(ids) + failures = ids & always_failures + dependent_failures.each do |failing_example, depends_upon| + failures << failing_example if ids.include?(depends_upon) + end + + RSpec::Core::Formatters::BisectFormatter::RunResults.new(ids.sort, failures.sort) + end + + def repro_command_from(locations) + "rspec #{locations.sort.join(' ')}" + end +end From 1f11351a0cc7c0498560f220f76c59c6682c293c Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Wed, 8 Apr 2015 21:41:31 -0700 Subject: [PATCH 164/258] Update test to ensure bisect coordinator closes stream. --- spec/rspec/core/bisect/coordinator_spec.rb | 24 ++++++++++++++-------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/spec/rspec/core/bisect/coordinator_spec.rb b/spec/rspec/core/bisect/coordinator_spec.rb index d562812cb8..e275e50267 100644 --- a/spec/rspec/core/bisect/coordinator_spec.rb +++ b/spec/rspec/core/bisect/coordinator_spec.rb @@ -14,18 +14,21 @@ module RSpec::Core ) end - def find_minimal_repro + def find_minimal_repro(output) allow(Bisect::Server).to receive(:run).and_yield(instance_double(Bisect::Server)) allow(Bisect::Runner).to receive(:new).and_return(fake_runner) - formatter_output = RSpec.configuration.output_stream = StringIO.new + RSpec.configuration.output_stream = output Bisect::Coordinator.bisect_with([], RSpec.configuration) - - normalize_durations(formatter_output.string) + ensure + RSpec.reset # so that RSpec.configuration.output_stream isn't closed end - it 'notifies the bisect progress formatter of progress' do - output = find_minimal_repro + it 'notifies the bisect progress formatter of progress and closes the output' do + tempfile = Tempfile.new("bisect") + output_file = File.open(tempfile.path, "w") + expect { find_minimal_repro(output_file) }.to change(output_file, :closed?).from(false).to(true) + output = normalize_durations(File.read(tempfile.path)).chomp expect(output).to eq(<<-EOS.gsub(/^\s+\|/, '')) |Bisect started using options: "" @@ -43,7 +46,9 @@ def find_minimal_repro end it 'can use the bisect debug formatter to get detailed progress' do - output = with_env_vars('DEBUG_RSPEC_BISECT' => '1') { find_minimal_repro } + output = StringIO.new + with_env_vars('DEBUG_RSPEC_BISECT' => '1') { find_minimal_repro(output) } + output = normalize_durations(output.string) expect(output).to eq(<<-EOS.gsub(/^\s+\|/, '')) |Bisect started using options: "" @@ -111,14 +116,15 @@ def bisect_round_finished(notification) end it "prints the most minimal repro command it has found so far" do + output = StringIO.new expect { - find_minimal_repro + find_minimal_repro(output) }.to raise_error(an_object_having_attributes( :class => SystemExit, :status => 1 )) - output = normalize_durations(RSpec.configuration.output_stream.string) + output = normalize_durations(output.string) expect(output).to eq(<<-EOS.gsub(/^\s+\|/, '')) |Bisect started using options: "" From 2666401debc7d984f7251815fdc6cf178dbf902f Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Wed, 8 Apr 2015 21:57:47 -0700 Subject: [PATCH 165/258] Use the `--bisect` flag value rather than ENV var for verbose mode. --- features/command_line/bisect.feature | 6 +++--- .../step_definitions/additional_cli_steps.rb | 5 ----- lib/rspec/core/bisect/coordinator.rb | 13 +++++------- lib/rspec/core/bisect/runner.rb | 2 +- lib/rspec/core/option_parser.rb | 21 ++++++++++++++----- spec/rspec/core/bisect/coordinator_spec.rb | 14 ++++++------- spec/rspec/core/bisect/runner_spec.rb | 10 +++++---- 7 files changed, 37 insertions(+), 34 deletions(-) diff --git a/features/command_line/bisect.feature b/features/command_line/bisect.feature index ee1a6c49d9..d56df3c1d2 100644 --- a/features/command_line/bisect.feature +++ b/features/command_line/bisect.feature @@ -6,7 +6,7 @@ Feature: Bisect At any point during the bisect run, you can hit ctrl-c to abort and it will provide you with the most minimal reproduction command it has discovered so far. - To get more detailed output (particularly useful if you want to report a bug with bisect), you can set the `DEBUG_RSPEC_BISECT` environment variable. + To get more detailed output (particularly useful if you want to report a bug with bisect), use `--bisect=verbose`. Background: Given a file named "lib/calculator.rb" with: @@ -88,8 +88,8 @@ Feature: Bisect When I run `rspec ./spec/calculator_10_spec.rb[1:1] ./spec/calculator_1_spec.rb[1:1] ./spec/calculator_3_spec.rb[1:1] --seed 1234` Then the output should contain "3 examples, 1 failure" - Scenario: Use DEBUG_RSPEC_BISECT=1 to enable verbose debug mode for more detail - When I run `rspec --seed 1234 --bisect` with `DEBUG_RSPEC_BISECT=1` set + Scenario: Use `--bisect=verbose` to enable verbose debug mode for more detail + When I run `rspec --seed 1234 --bisect=verbose` Then bisect should succeed with output like: """ Bisect started using options: "--seed 1234" diff --git a/features/step_definitions/additional_cli_steps.rb b/features/step_definitions/additional_cli_steps.rb index 2aa4ff34c8..6bfe7919ab 100644 --- a/features/step_definitions/additional_cli_steps.rb +++ b/features/step_definitions/additional_cli_steps.rb @@ -177,11 +177,6 @@ expect(actual.sub(/\n+\Z/, '')).to eq(expected) end -When(/^I run `([^`]+)` with `([^=]+)=([^`]+)` set$/) do |cmd, env_key, env_value| - set_env(env_key, env_value) - step "I run `#{cmd}`" -end - When(/^I run `([^`]+)` and abort in the middle with ctrl\-c$/) do |cmd| set_env('RUBYOPT', ENV['RUBYOPT'] + " -r#{File.expand_path("../../support/send_sigint_during_bisect.rb", __FILE__)}") step "I run `#{cmd}`" diff --git a/lib/rspec/core/bisect/coordinator.rb b/lib/rspec/core/bisect/coordinator.rb index 314dfc1766..16448c1809 100644 --- a/lib/rspec/core/bisect/coordinator.rb +++ b/lib/rspec/core/bisect/coordinator.rb @@ -15,17 +15,18 @@ module Bisect # - Formatters::BisectProgressFormatter: provides progress updates # to the user. class Coordinator - def self.bisect_with(original_cli_args, configuration) - new(original_cli_args, configuration).bisect + def self.bisect_with(original_cli_args, configuration, formatter) + new(original_cli_args, configuration, formatter).bisect end - def initialize(original_cli_args, configuration) + def initialize(original_cli_args, configuration, formatter) @original_cli_args = original_cli_args @configuration = configuration + @formatter = formatter end def bisect - @configuration.add_formatter bisect_formatter + @configuration.add_formatter @formatter reporter.close_after do repro = Server.run do |server| @@ -52,10 +53,6 @@ def reporter @configuration.reporter end - def bisect_formatter - ENV['DEBUG_RSPEC_BISECT'] ? Formatters::BisectDebugFormatter : Formatters::BisectProgressFormatter - end - def gracefully_abort_on_sigint(minimizer) trap('INT') do repro = minimizer.repro_command_for_currently_needed_ids diff --git a/lib/rspec/core/bisect/runner.rb b/lib/rspec/core/bisect/runner.rb index e43f4c9fc8..a9d07dcd69 100644 --- a/lib/rspec/core/bisect/runner.rb +++ b/lib/rspec/core/bisect/runner.rb @@ -12,7 +12,7 @@ class Runner def initialize(server, original_cli_args) @server = server - @original_cli_args = original_cli_args - ["--bisect"] + @original_cli_args = original_cli_args.reject { |arg| arg.start_with?("--bisect") } end def run(locations) diff --git a/lib/rspec/core/option_parser.rb b/lib/rspec/core/option_parser.rb index 1d46f3c18b..f565cb78f6 100644 --- a/lib/rspec/core/option_parser.rb +++ b/lib/rspec/core/option_parser.rb @@ -62,9 +62,9 @@ def parser(options) options[:order] = "rand:#{seed}" end - parser.on('--bisect', 'Repeatedly runs the suite in order to isolate the failures to the ', - ' smallest reproducible case.') do - bisect_and_exit + parser.on('--bisect [verbose]', 'Repeatedly runs the suite in order to isolate the failures to the ', + ' smallest reproducible case.') do |argument| + bisect_and_exit(argument) end parser.on('--[no-]fail-fast', 'Abort the run on first failure.') do |value| @@ -268,12 +268,23 @@ def initialize_project_and_exit exit end - def bisect_and_exit + def bisect_and_exit(argument) RSpec::Support.require_rspec_core "bisect/coordinator" - success = Bisect::Coordinator.bisect_with(original_args, RSpec.configuration) + + success = Bisect::Coordinator.bisect_with( + original_args, + RSpec.configuration, + bisect_formatter_for(argument) + ) + exit(success ? 0 : 1) end + def bisect_formatter_for(argument) + return Formatters::BisectDebugFormatter if argument == "verbose" + Formatters::BisectProgressFormatter + end + def print_version_and_exit puts RSpec::Core::Version::STRING exit diff --git a/spec/rspec/core/bisect/coordinator_spec.rb b/spec/rspec/core/bisect/coordinator_spec.rb index e275e50267..1713b644c5 100644 --- a/spec/rspec/core/bisect/coordinator_spec.rb +++ b/spec/rspec/core/bisect/coordinator_spec.rb @@ -14,12 +14,12 @@ module RSpec::Core ) end - def find_minimal_repro(output) + def find_minimal_repro(output, formatter=Formatters::BisectProgressFormatter) allow(Bisect::Server).to receive(:run).and_yield(instance_double(Bisect::Server)) allow(Bisect::Runner).to receive(:new).and_return(fake_runner) RSpec.configuration.output_stream = output - Bisect::Coordinator.bisect_with([], RSpec.configuration) + Bisect::Coordinator.bisect_with([], RSpec.configuration, formatter) ensure RSpec.reset # so that RSpec.configuration.output_stream isn't closed end @@ -47,7 +47,7 @@ def find_minimal_repro(output) it 'can use the bisect debug formatter to get detailed progress' do output = StringIO.new - with_env_vars('DEBUG_RSPEC_BISECT' => '1') { find_minimal_repro(output) } + find_minimal_repro(output, Formatters::BisectDebugFormatter) output = normalize_durations(output.string) expect(output).to eq(<<-EOS.gsub(/^\s+\|/, '')) @@ -95,8 +95,8 @@ def find_minimal_repro(output) end context "when the user aborst the bisect with ctrl-c" do - before do - formatter_subclass = Class.new(Formatters::BisectProgressFormatter) do + let(:aborting_formatter) do + Class.new(Formatters::BisectProgressFormatter) do Formatters.register self def bisect_round_finished(notification) @@ -111,14 +111,12 @@ def bisect_round_finished(notification) sleep 5 end end - - stub_const(Formatters::BisectProgressFormatter.name, formatter_subclass) end it "prints the most minimal repro command it has found so far" do output = StringIO.new expect { - find_minimal_repro(output) + find_minimal_repro(output, aborting_formatter) }.to raise_error(an_object_having_attributes( :class => SystemExit, :status => 1 diff --git a/spec/rspec/core/bisect/runner_spec.rb b/spec/rspec/core/bisect/runner_spec.rb index fd86909c96..cec5102b05 100644 --- a/spec/rspec/core/bisect/runner_spec.rb +++ b/spec/rspec/core/bisect/runner_spec.rb @@ -70,10 +70,12 @@ def command_for(locations, options={}) expect(cmd.scan("--drb-port").count).to eq(1) end - it 'ignores the `--bisect` option since that would infinitely recurse' do - original_cli_args << "--bisect" - cmd = command_for([]) - expect(cmd).to exclude("--bisect") + %w[ --bisect --bisect=verbose --bisect=blah ].each do |value| + it "ignores a `#{value}` option since that would infinitely recurse" do + original_cli_args << value + cmd = command_for([]) + expect(cmd).to exclude(value) + end end it 'uses the bisect formatter' do From cca061ed262da33f1267b787b8030c4aeb0ccb87 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Thu, 9 Apr 2015 10:45:08 -0700 Subject: [PATCH 166/258] Fix help text for bisect to make it clear it's `--bisect=verbose` --- lib/rspec/core/option_parser.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rspec/core/option_parser.rb b/lib/rspec/core/option_parser.rb index f565cb78f6..324503c5ec 100644 --- a/lib/rspec/core/option_parser.rb +++ b/lib/rspec/core/option_parser.rb @@ -62,7 +62,7 @@ def parser(options) options[:order] = "rand:#{seed}" end - parser.on('--bisect [verbose]', 'Repeatedly runs the suite in order to isolate the failures to the ', + parser.on('--bisect[=verbose]', 'Repeatedly runs the suite in order to isolate the failures to the ', ' smallest reproducible case.') do |argument| bisect_and_exit(argument) end From 47761bf83fc2109aeb7843e3538003852bae7efb Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Fri, 10 Apr 2015 10:06:39 +1000 Subject: [PATCH 167/258] Fix scenario demonstrating the default behaviour of expose_dsl_globally --- features/configuration/enable_global_dsl.feature | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/features/configuration/enable_global_dsl.feature b/features/configuration/enable_global_dsl.feature index 3009c80b95..c1ef2b3120 100644 --- a/features/configuration/enable_global_dsl.feature +++ b/features/configuration/enable_global_dsl.feature @@ -24,10 +24,11 @@ Feature: Global namespace DSL For backwards compatibility it defaults to `true`. + @allow-should-syntax Scenario: By default RSpec allows the DSL to be used globally Given a file named "spec/example_spec.rb" with: """ruby - RSpec.describe "specs here" do + describe "specs here" do it "passes" do end end From 33e41fcb41ac6a419dd27a041ebf83d105689cff Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Fri, 10 Apr 2015 10:29:34 +1000 Subject: [PATCH 168/258] ensure the default configuration is loaded in rspec/autorun --- features/configuration/enable_global_dsl.feature | 13 +++++++++++++ lib/rspec/autorun.rb | 1 + lib/rspec/core.rb | 7 ++----- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/features/configuration/enable_global_dsl.feature b/features/configuration/enable_global_dsl.feature index c1ef2b3120..cf0d38f0ea 100644 --- a/features/configuration/enable_global_dsl.feature +++ b/features/configuration/enable_global_dsl.feature @@ -36,6 +36,19 @@ Feature: Global namespace DSL When I run `rspec` Then the output should contain "1 example, 0 failures" + @allow-should-syntax + Scenario: By default rspec/autorun allows the DSL to be used globally + Given a file named "spec/example_spec.rb" with: + """ruby + require 'rspec/autorun' + describe "specs here" do + it "passes" do + end + end + """ + When I run `ruby spec/example_spec.rb` + Then the output should contain "1 example, 0 failures" + Scenario: When exposing globally is disabled the top level DSL no longer works Given a file named "spec/example_spec.rb" with: """ruby diff --git a/lib/rspec/autorun.rb b/lib/rspec/autorun.rb index 18cc1eddb3..3080cfdd4b 100644 --- a/lib/rspec/autorun.rb +++ b/lib/rspec/autorun.rb @@ -1,2 +1,3 @@ require 'rspec/core' +# Ensure the default config is loaded RSpec::Core::Runner.autorun diff --git a/lib/rspec/core.rb b/lib/rspec/core.rb index 4fb631e023..9e2ee5ab18 100644 --- a/lib/rspec/core.rb +++ b/lib/rspec/core.rb @@ -81,12 +81,9 @@ def self.clear_examples # @see RSpec.configure # @see Core::Configuration def self.configuration - @configuration ||= begin - config = RSpec::Core::Configuration.new - config.expose_dsl_globally = true - config - end + @configuration ||= RSpec::Core::Configuration.new end + configuration.expose_dsl_globally = true # Yields the global configuration to a block. # @yield [Configuration] global configuration From 0935042ae8059fd04f8750903e54f3fc7ac95081 Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Fri, 10 Apr 2015 10:32:11 +1000 Subject: [PATCH 169/258] changelog for #1933 --- Changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Changelog.md b/Changelog.md index bf06da09eb..6b428843b6 100644 --- a/Changelog.md +++ b/Changelog.md @@ -44,6 +44,7 @@ Bug Fixes: when the task failed. (Myron Marston, #1905) * Make `let` work properly when defined in a shared context that is applied to an individual example via metadata. (Myron Marston, #1912) +* Ensure `rspec/autorun` respects configuration defaults. (Jon Rowe, #1933) ### 3.2.3 / 2015-04-06 [Full Changelog](http://github.com/rspec/rspec-core/compare/v3.2.2...v3.2.3) From b261081111f1b092cf5d3c7c581214ca37a1cf62 Mon Sep 17 00:00:00 2001 From: Eugene Kenny Date: Tue, 14 Apr 2015 00:47:03 +0100 Subject: [PATCH 170/258] Apply helper modules to existing groups when added When a helper module is configured, it is now applied to all existing matching example groups. This means that the order in which example groups are defined and helper modules are configured no longer matters. --- lib/rspec/core/configuration.rb | 11 ++++++ lib/rspec/core/world.rb | 8 +++-- spec/rspec/core/configuration_spec.rb | 51 +++++++++++++++++++++++++++ spec/rspec/core/world_spec.rb | 21 +++++++++++ 4 files changed, 89 insertions(+), 2 deletions(-) diff --git a/lib/rspec/core/configuration.rb b/lib/rspec/core/configuration.rb index 58e37410d4..202f846f97 100644 --- a/lib/rspec/core/configuration.rb +++ b/lib/rspec/core/configuration.rb @@ -1134,6 +1134,7 @@ def exclusion_filter def include(mod, *filters) meta = Metadata.build_hash_from(filters, :warn_about_example_group_filtering) @include_modules.append(mod, meta) + configure_existing_groups(mod, meta, :safe_include) end # Tells RSpec to extend example groups with `mod`. Methods defined in @@ -1169,6 +1170,7 @@ def include(mod, *filters) def extend(mod, *filters) meta = Metadata.build_hash_from(filters, :warn_about_example_group_filtering) @extend_modules.append(mod, meta) + configure_existing_groups(mod, meta, :safe_extend) end if RSpec::Support::RubyFeatures.module_prepends_supported? @@ -1207,6 +1209,7 @@ def extend(mod, *filters) def prepend(mod, *filters) meta = Metadata.build_hash_from(filters, :warn_about_example_group_filtering) @prepend_modules.append(mod, meta) + configure_existing_groups(mod, meta, :safe_prepend) end end @@ -1227,6 +1230,14 @@ def configure_group_with(group, module_list, application_method) end end + # @private + def configure_existing_groups(mod, meta, application_method) + RSpec.world.all_example_groups.each do |group| + next unless meta.empty? || MetadataFilter.apply?(:any?, meta, group.metadata) + __send__(application_method, mod, group) + end + end + # @private # # Used internally to extend the singleton class of a single example's diff --git a/lib/rspec/core/world.rb b/lib/rspec/core/world.rb index 10ce57c43a..1ab2372f65 100644 --- a/lib/rspec/core/world.rb +++ b/lib/rspec/core/world.rb @@ -77,10 +77,14 @@ def example_count(groups=example_groups) inject(0) { |a, e| a + e.filtered_examples.size } end + # @private + def all_example_groups + FlatMap.flat_map(example_groups) { |g| g.descendants } + end + # @private def all_examples - flattened_groups = FlatMap.flat_map(example_groups) { |g| g.descendants } - FlatMap.flat_map(flattened_groups) { |g| g.examples } + FlatMap.flat_map(all_example_groups) { |g| g.examples } end # @api private diff --git a/spec/rspec/core/configuration_spec.rb b/spec/rspec/core/configuration_spec.rb index 306725d13c..0b4a7a8005 100644 --- a/spec/rspec/core/configuration_spec.rb +++ b/spec/rspec/core/configuration_spec.rb @@ -879,6 +879,17 @@ def metadata_hash(*args) expect(group).not_to respond_to(:you_call_this_a_blt?) expect(group.new.you_call_this_a_blt?).to eq("egad man, where's the mayo?!?!?") end + + it "includes the given module into each existing example group" do + group = RSpec.describe('does like, stuff and junk', :magic_key => :include) { } + + RSpec.configure do |c| + c.include(InstanceLevelMethods) + end + + expect(group).not_to respond_to(:you_call_this_a_blt?) + expect(group.new.you_call_this_a_blt?).to eq("egad man, where's the mayo?!?!?") + end end context "with a filter" do @@ -892,6 +903,17 @@ def metadata_hash(*args) expect(group.new.you_call_this_a_blt?).to eq("egad man, where's the mayo?!?!?") end + it "includes the given module into each existing matching example group" do + group = RSpec.describe('does like, stuff and junk', :magic_key => :include) { } + + RSpec.configure do |c| + c.include(InstanceLevelMethods, :magic_key => :include) + end + + expect(group).not_to respond_to(:you_call_this_a_blt?) + expect(group.new.you_call_this_a_blt?).to eq("egad man, where's the mayo?!?!?") + end + it "includes the given module into the singleton class of matching examples" do RSpec.configure do |c| c.include(InstanceLevelMethods, :magic_key => :include) @@ -981,6 +1003,15 @@ def metadata_hash(*args) expect(group).to respond_to(:that_thing) end + it "extends the given module into each existing matching example group" do + group = RSpec.describe(ThatThingISentYou, :magic_key => :extend) { } + + RSpec.configure do |c| + c.extend(ThatThingISentYou, :magic_key => :extend) + end + + expect(group).to respond_to(:that_thing) + end end describe "#prepend", :if => RSpec::Support::RubyFeatures.module_prepends_supported? do @@ -1008,6 +1039,16 @@ def metadata_hash(*args) group = RSpec.describe('yo') { } expect(group.new.foo).to eq("foobar") end + + it "prepends the given module into each existing example group" do + group = RSpec.describe('yo') { } + + RSpec.configure do |c| + c.prepend(SomeRandomMod) + end + + expect(group.new.foo).to eq("foobar") + end end context "with a filter" do @@ -1019,6 +1060,16 @@ def metadata_hash(*args) group = RSpec.describe('yo', :magic_key => :include) { } expect(group.new.foo).to eq("foobar") end + + it "prepends the given module into each existing matching example group" do + group = RSpec.describe('yo', :magic_key => :include) { } + + RSpec.configure do |c| + c.prepend(SomeRandomMod, :magic_key => :include) + end + + expect(group.new.foo).to eq("foobar") + end end end diff --git a/spec/rspec/core/world_spec.rb b/spec/rspec/core/world_spec.rb index 476e1c84f8..cc3456bc25 100644 --- a/spec/rspec/core/world_spec.rb +++ b/spec/rspec/core/world_spec.rb @@ -23,6 +23,27 @@ module RSpec::Core end end + describe "#all_example_groups" do + it "contains all example groups from all levels of nesting" do + RSpec.describe "eg1" do + context "eg2" do + context "eg3" + context "eg4" + end + + context "eg5" + end + + RSpec.describe "eg6" do + example + end + + expect(RSpec.world.all_example_groups.map(&:description)).to match_array(%w[ + eg1 eg2 eg3 eg4 eg5 eg6 + ]) + end + end + describe "#all_examples" do it "contains all examples from all levels of nesting" do RSpec.describe do From 4b7d39aa3ff32b5a35993cbf463c92dec99d7668 Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Tue, 14 Apr 2015 13:50:57 +1000 Subject: [PATCH 171/258] changelog for #1935 --- Changelog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Changelog.md b/Changelog.md index 6b428843b6..e2b5814c05 100644 --- a/Changelog.md +++ b/Changelog.md @@ -45,6 +45,8 @@ Bug Fixes: * Make `let` work properly when defined in a shared context that is applied to an individual example via metadata. (Myron Marston, #1912) * Ensure `rspec/autorun` respects configuration defaults. (Jon Rowe, #1933) +* Prevent modules overriding example group defined methods when included, + prepended or extended by config defined after an example group. (Eugene Kenny, #1935) ### 3.2.3 / 2015-04-06 [Full Changelog](http://github.com/rspec/rspec-core/compare/v3.2.2...v3.2.3) From f2b70c9bdf0d8a2afc5dec70d70b4e975afe5c8f Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Mon, 13 Apr 2015 23:04:36 -0700 Subject: [PATCH 172/258] Make specs from #1935 more robust. - Ensure the specified behavior applies to nested groups and not simply top-level groups. The existing specs would have passed if the implementation of `configure_existing_groups` wrongly used `RSpec.world.example_groups` instead of `RSpec.world.all_example_groups`. This guards against that. - Ensure metadata-based `include`/`extend`/`prepend` applies only to matching example groups. The existing specs would pass even if the metadata check was removed from the implementation of `configure_existing_groups`. --- spec/rspec/core/configuration_spec.rb | 30 ++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/spec/rspec/core/configuration_spec.rb b/spec/rspec/core/configuration_spec.rb index 0b4a7a8005..c510961356 100644 --- a/spec/rspec/core/configuration_spec.rb +++ b/spec/rspec/core/configuration_spec.rb @@ -904,14 +904,22 @@ def metadata_hash(*args) end it "includes the given module into each existing matching example group" do - group = RSpec.describe('does like, stuff and junk', :magic_key => :include) { } + matching_group = RSpec.describe('does like, stuff and junk', :magic_key => :include) { } + non_matching_group = RSpec.describe + nested_matching_group = non_matching_group.describe("", :magic_key => :include) RSpec.configure do |c| c.include(InstanceLevelMethods, :magic_key => :include) end - expect(group).not_to respond_to(:you_call_this_a_blt?) - expect(group.new.you_call_this_a_blt?).to eq("egad man, where's the mayo?!?!?") + expect(matching_group).not_to respond_to(:you_call_this_a_blt?) + expect(matching_group.new.you_call_this_a_blt?).to eq("egad man, where's the mayo?!?!?") + + expect(non_matching_group).not_to respond_to(:you_call_this_a_blt?) + expect(non_matching_group.new).not_to respond_to(:you_call_this_a_blt?) + + expect(nested_matching_group).not_to respond_to(:you_call_this_a_blt?) + expect(nested_matching_group.new.you_call_this_a_blt?).to eq("egad man, where's the mayo?!?!?") end it "includes the given module into the singleton class of matching examples" do @@ -1004,13 +1012,17 @@ def metadata_hash(*args) end it "extends the given module into each existing matching example group" do - group = RSpec.describe(ThatThingISentYou, :magic_key => :extend) { } + matching_group = RSpec.describe(ThatThingISentYou, :magic_key => :extend) { } + non_matching_group = RSpec.describe + nested_matching_group = non_matching_group.describe("Other", :magic_key => :extend) RSpec.configure do |c| c.extend(ThatThingISentYou, :magic_key => :extend) end - expect(group).to respond_to(:that_thing) + expect(matching_group).to respond_to(:that_thing) + expect(non_matching_group).not_to respond_to(:that_thing) + expect(nested_matching_group).to respond_to(:that_thing) end end @@ -1062,13 +1074,17 @@ def metadata_hash(*args) end it "prepends the given module into each existing matching example group" do - group = RSpec.describe('yo', :magic_key => :include) { } + matching_group = RSpec.describe('yo', :magic_key => :include) { } + non_matching_group = RSpec.describe + nested_matching_group = non_matching_group.describe('', :magic_key => :include) RSpec.configure do |c| c.prepend(SomeRandomMod, :magic_key => :include) end - expect(group.new.foo).to eq("foobar") + expect(matching_group.new.foo).to eq("foobar") + expect(non_matching_group.new).not_to respond_to(:foo) + expect(nested_matching_group.new.foo).to eq("foobar") end end From 86d6b888a4519a22c66400f4bd1c89fd58c9b4f3 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Mon, 13 Apr 2015 23:18:10 -0700 Subject: [PATCH 173/258] Update changelog for #1935. That PR fixed a bug (already added to the Changelog by @JonRowe) but it also is notable for this enhancement. --- Changelog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Changelog.md b/Changelog.md index e2b5814c05..8d5b173831 100644 --- a/Changelog.md +++ b/Changelog.md @@ -34,6 +34,8 @@ Enhancements: * Add `--bisect` CLI option, which will repeatedly run your suite in order to isolate the failures to the smallest reproducible case. (Myron Marston, #1917) +* For `config.include`, `config.extend` and `config.prepend`, apply the + module to previously defined matching example groups. (Eugene Kenny, #1935) Bug Fixes: From 0f04f40073e8c832897c6e042e62257068d62f47 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Sat, 18 Apr 2015 08:14:29 -0700 Subject: [PATCH 174/258] Tell users where the invalid options came from. Addresses the confusion reported in rspec/rspec-rails#1356. --- Changelog.md | 3 ++ lib/rspec/core/configuration_options.rb | 17 ++++--- lib/rspec/core/option_parser.rb | 10 ++-- spec/rspec/core/configuration_options_spec.rb | 48 +++++++++++++++++++ 4 files changed, 68 insertions(+), 10 deletions(-) diff --git a/Changelog.md b/Changelog.md index 8d5b173831..2b92512072 100644 --- a/Changelog.md +++ b/Changelog.md @@ -36,6 +36,9 @@ Enhancements: (Myron Marston, #1917) * For `config.include`, `config.extend` and `config.prepend`, apply the module to previously defined matching example groups. (Eugene Kenny, #1935) +* When invalid options are parsed, notify users where they came from + (e.g. `.rspec` or `~/.rspec` or `ENV['SPEC_OPTS']`) so they can + easily find the source of the problem. (Myron Marston, #1940) Bug Fixes: diff --git a/lib/rspec/core/configuration_options.rb b/lib/rspec/core/configuration_options.rb index faf6bd431f..86dee41bad 100644 --- a/lib/rspec/core/configuration_options.rb +++ b/lib/rspec/core/configuration_options.rb @@ -120,7 +120,11 @@ def file_options def env_options return {} unless ENV['SPEC_OPTS'] - parse_args_ignoring_files_or_dirs_to_run(Shellwords.split(ENV["SPEC_OPTS"])) + + parse_args_ignoring_files_or_dirs_to_run( + Shellwords.split(ENV["SPEC_OPTS"]), + "ENV['SPEC_OPTS']" + ) end def command_line_options @@ -144,11 +148,12 @@ def global_options end def options_from(path) - parse_args_ignoring_files_or_dirs_to_run(args_from_options_file(path)) + args = args_from_options_file(path) + parse_args_ignoring_files_or_dirs_to_run(args, path) end - def parse_args_ignoring_files_or_dirs_to_run(args) - options = Parser.parse(args) + def parse_args_ignoring_files_or_dirs_to_run(args, source) + options = Parser.parse(args, source) options.delete(:files_or_directories_to_run) options end @@ -168,11 +173,11 @@ def custom_options_file end def project_options_file - ".rspec" + "./.rspec" end def local_options_file - ".rspec-local" + "./.rspec-local" end def global_options_file diff --git a/lib/rspec/core/option_parser.rb b/lib/rspec/core/option_parser.rb index 324503c5ec..e9e278f742 100644 --- a/lib/rspec/core/option_parser.rb +++ b/lib/rspec/core/option_parser.rb @@ -4,8 +4,8 @@ module RSpec::Core # @private class Parser - def self.parse(args) - new(args).parse + def self.parse(args, source=nil) + new(args).parse(source) end attr_reader :original_args @@ -14,7 +14,7 @@ def initialize(original_args) @original_args = original_args end - def parse + def parse(source=nil) return { :files_or_directories_to_run => [] } if original_args.empty? args = original_args.dup @@ -22,7 +22,9 @@ def parse begin parser(options).parse!(args) rescue OptionParser::InvalidOption => e - abort "#{e.message}\n\nPlease use --help for a listing of valid options" + failure = e.message + failure << " (defined in #{source})" if source + abort "#{failure}\n\nPlease use --help for a listing of valid options" end options[:files_or_directories_to_run] = args diff --git a/spec/rspec/core/configuration_options_spec.rb b/spec/rspec/core/configuration_options_spec.rb index 869d06200b..d2119d31c9 100644 --- a/spec/rspec/core/configuration_options_spec.rb +++ b/spec/rspec/core/configuration_options_spec.rb @@ -367,6 +367,54 @@ end end + describe "invalid options" do + def expect_parsing_to_fail_mentioning_source(source, options=[]) + expect { + parse_options(*options) + }.to raise_error(SystemExit).and output(a_string_including( + "invalid option: --default_path (defined in #{source})", + "Please use --help for a listing of valid options" + )).to_stderr + end + + %w[ ~/.rspec ./.rspec ./.rspec-local ].each do |file_name| + context "defined in #{file_name}" do + it "mentions the file name in the error so users know where to look for it" do + file_name = File.expand_path(file_name) if file_name.start_with?("~") + File.open(File.expand_path(file_name), "w") { |f| f << "--default_path" } + expect_parsing_to_fail_mentioning_source(file_name) + end + end + end + + context "defined in SPEC_OPTS" do + it "mentions ENV['SPEC_OPTS'] as the source in the error so users know where to look for it" do + with_env_vars 'SPEC_OPTS' => "--default_path" do + expect_parsing_to_fail_mentioning_source("ENV['SPEC_OPTS']") + end + end + end + + context "defined in a custom file" do + it "mentions the custom file as the source of the error so users know where to look for it" do + File.open("./custom.opts", "w") {|f| f << "--default_path"} + + expect_parsing_to_fail_mentioning_source("./custom.opts", %w[-O ./custom.opts]) + end + + context "passed at the command line" do + it "does not mention the source since it is obvious where it came from" do + expect { + parse_options("--default_path") + }.to raise_error(SystemExit).and output(a_string_including( + "invalid option: --default_path\n", + "Please use --help for a listing of valid options" + )).to_stderr + end + end + end + end + describe "sources: ~/.rspec, ./.rspec, ./.rspec-local, custom, CLI, and SPEC_OPTS" do it "merges global, local, SPEC_OPTS, and CLI" do File.open("./.rspec", "w") {|f| f << "--require some_file"} From a392975f387024d6ed040b51aa4903944869415d Mon Sep 17 00:00:00 2001 From: Fabien Schurter Date: Wed, 22 Apr 2015 19:25:09 +0200 Subject: [PATCH 175/258] Correct a misspelled word in a feature --- features/metadata/user_defined.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/metadata/user_defined.feature b/features/metadata/user_defined.feature index b235035e0b..0383ce9743 100644 --- a/features/metadata/user_defined.feature +++ b/features/metadata/user_defined.feature @@ -8,7 +8,7 @@ Feature: User-defined metadata Metadata defined on an example group is available (and can be overridden) by any sub-group or from any example in that group or a sub-group. - In addition, you can specify metdata using just symbols. Each symbol passed + In addition, you can specify metadata using just symbols. Each symbol passed as an argument to `describe`, `context` or `it` will be a key in the metadata hash, with a corresponding value of `true`. From 9764767381b41ca45587daa98d1cebe4ace7d987 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Sat, 25 Apr 2015 15:49:47 -0700 Subject: [PATCH 176/258] `c.mock_framework = :foo` => `c.mock_with :foo` Both work but the latter is the way we document the config elsewhere, so it's worth standardizing here. --- .../use_any_framework.feature | 2 +- .../mock_framework_integration/use_flexmock.feature | 10 +++++----- .../mock_framework_integration/use_mocha.feature | 10 +++++----- features/mock_framework_integration/use_rr.feature | 10 +++++----- .../mock_framework_integration/use_rspec.feature | 12 ++++++------ 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/features/mock_framework_integration/use_any_framework.feature b/features/mock_framework_integration/use_any_framework.feature index ce88e1002b..a87e812c9c 100644 --- a/features/mock_framework_integration/use_any_framework.feature +++ b/features/mock_framework_integration/use_any_framework.feature @@ -84,7 +84,7 @@ Feature: mock with an alternative framework require File.expand_path("../expector", __FILE__) RSpec.configure do |config| - config.mock_framework = Expector::RSpecAdapter + config.mock_with Expector::RSpecAdapter end RSpec.describe Expector do diff --git a/features/mock_framework_integration/use_flexmock.feature b/features/mock_framework_integration/use_flexmock.feature index c709a0b7e2..7d1e314620 100644 --- a/features/mock_framework_integration/use_flexmock.feature +++ b/features/mock_framework_integration/use_flexmock.feature @@ -6,7 +6,7 @@ Feature: mock with flexmock Given a file named "example_spec.rb" with: """ruby RSpec.configure do |config| - config.mock_framework = :flexmock + config.mock_with :flexmock end RSpec.describe "mocking with Flexmock" do @@ -24,7 +24,7 @@ Feature: mock with flexmock Given a file named "example_spec.rb" with: """ruby RSpec.configure do |config| - config.mock_framework = :flexmock + config.mock_with :flexmock end RSpec.describe "mocking with Flexmock" do @@ -41,7 +41,7 @@ Feature: mock with flexmock Given a file named "example_spec.rb" with: """ruby RSpec.configure do |config| - config.mock_framework = :flexmock + config.mock_with :flexmock end RSpec.describe "failed message expectation in a pending example" do @@ -60,7 +60,7 @@ Feature: mock with flexmock Given a file named "example_spec.rb" with: """ruby RSpec.configure do |config| - config.mock_framework = :flexmock + config.mock_with :flexmock end RSpec.describe "passing message expectation in a pending example" do @@ -81,7 +81,7 @@ Feature: mock with flexmock Given a file named "example_spec.rb" with: """ruby RSpec.configure do |config| - config.mock_framework = :flexmock + config.mock_with :flexmock end RSpec.describe "RSpec.configuration.mock_framework.framework_name" do diff --git a/features/mock_framework_integration/use_mocha.feature b/features/mock_framework_integration/use_mocha.feature index 750db2c1be..1d116c4a6b 100644 --- a/features/mock_framework_integration/use_mocha.feature +++ b/features/mock_framework_integration/use_mocha.feature @@ -6,7 +6,7 @@ Feature: mock with mocha Given a file named "example_spec.rb" with: """ruby RSpec.configure do |config| - config.mock_framework = :mocha + config.mock_with :mocha end RSpec.describe "mocking with RSpec" do @@ -24,7 +24,7 @@ Feature: mock with mocha Given a file named "example_spec.rb" with: """ruby RSpec.configure do |config| - config.mock_framework = :mocha + config.mock_with :mocha end RSpec.describe "mocking with RSpec" do @@ -41,7 +41,7 @@ Feature: mock with mocha Given a file named "example_spec.rb" with: """ruby RSpec.configure do |config| - config.mock_framework = :mocha + config.mock_with :mocha end RSpec.describe "failed message expectation in a pending example" do @@ -60,7 +60,7 @@ Feature: mock with mocha Given a file named "example_spec.rb" with: """ruby RSpec.configure do |config| - config.mock_framework = :mocha + config.mock_with :mocha end RSpec.describe "passing message expectation in a pending example" do @@ -81,7 +81,7 @@ Feature: mock with mocha Given a file named "example_spec.rb" with: """ruby RSpec.configure do |config| - config.mock_framework = :mocha + config.mock_with :mocha end RSpec.describe "RSpec.configuration.mock_framework.framework_name" do diff --git a/features/mock_framework_integration/use_rr.feature b/features/mock_framework_integration/use_rr.feature index 29d07ed2b5..7a7a4e57fe 100644 --- a/features/mock_framework_integration/use_rr.feature +++ b/features/mock_framework_integration/use_rr.feature @@ -6,7 +6,7 @@ Feature: mock with rr Given a file named "example_spec.rb" with: """ruby RSpec.configure do |config| - config.mock_framework = :rr + config.mock_with :rr end RSpec.describe "mocking with RSpec" do @@ -24,7 +24,7 @@ Feature: mock with rr Given a file named "example_spec.rb" with: """ruby RSpec.configure do |config| - config.mock_framework = :rr + config.mock_with :rr end RSpec.describe "mocking with RSpec" do @@ -41,7 +41,7 @@ Feature: mock with rr Given a file named "example_spec.rb" with: """ruby RSpec.configure do |config| - config.mock_framework = :rr + config.mock_with :rr end RSpec.describe "failed message expectation in a pending example" do @@ -60,7 +60,7 @@ Feature: mock with rr Given a file named "example_spec.rb" with: """ruby RSpec.configure do |config| - config.mock_framework = :rr + config.mock_with :rr end RSpec.describe "passing message expectation in a pending example" do @@ -81,7 +81,7 @@ Feature: mock with rr Given a file named "example_spec.rb" with: """ruby RSpec.configure do |config| - config.mock_framework = :rr + config.mock_with :rr end RSpec.describe "RSpec.configuration.mock_framework.framework_name" do diff --git a/features/mock_framework_integration/use_rspec.feature b/features/mock_framework_integration/use_rspec.feature index 9089acc76e..4945bdcda4 100644 --- a/features/mock_framework_integration/use_rspec.feature +++ b/features/mock_framework_integration/use_rspec.feature @@ -7,7 +7,7 @@ Feature: mock with rspec Given a file named "example_spec.rb" with: """ruby RSpec.configure do |config| - config.mock_framework = :rspec + config.mock_with :rspec end RSpec.describe "mocking with RSpec" do @@ -25,7 +25,7 @@ Feature: mock with rspec Given a file named "example_spec.rb" with: """ruby RSpec.configure do |config| - config.mock_framework = :rspec + config.mock_with :rspec end RSpec.describe "mocking with RSpec" do @@ -42,7 +42,7 @@ Feature: mock with rspec Given a file named "example_spec.rb" with: """ruby RSpec.configure do |config| - config.mock_framework = :rspec + config.mock_with :rspec end RSpec.describe "failed message expectation in a pending example" do @@ -61,7 +61,7 @@ Feature: mock with rspec Given a file named "example_spec.rb" with: """ruby RSpec.configure do |config| - config.mock_framework = :rspec + config.mock_with :rspec end RSpec.describe "passing message expectation in a pending example" do @@ -82,7 +82,7 @@ Feature: mock with rspec Given a file named "example_spec.rb" with: """ruby RSpec.configure do |config| - config.mock_framework = :rspec + config.mock_with :rspec end RSpec.describe "RSpec.configuration.mock_framework.framework_name" do @@ -98,7 +98,7 @@ Feature: mock with rspec Given a file named "example_spec.rb" with: """ruby RSpec.configure do |config| - config.mock_framework = :rspec + config.mock_with :rspec end RSpec.describe "Testing" do From ec150a80dff198aa441616784ac18aa6470524d0 Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Thu, 23 Apr 2015 10:54:42 +1000 Subject: [PATCH 177/258] --only-failures takes precedence over run_all_when_everything_filtered --- features/command_line/only_failures.feature | 5 +++++ features/step_definitions/additional_cli_steps.rb | 5 +++++ lib/rspec/core/world.rb | 2 +- spec/rspec/core/world_spec.rb | 13 +++++++++++++ 4 files changed, 24 insertions(+), 1 deletion(-) diff --git a/features/command_line/only_failures.feature b/features/command_line/only_failures.feature index e974e874be..4d31817600 100644 --- a/features/command_line/only_failures.feature +++ b/features/command_line/only_failures.feature @@ -11,6 +11,7 @@ Feature: Only Failures """ruby RSpec.configure do |c| c.example_status_persistence_file_path = "examples.txt" + c.run_all_when_everything_filtered = true end """ And a file named ".rspec" with: @@ -103,6 +104,10 @@ Feature: Only Failures When I run `rspec --next-failure` Then the output should contain "All examples were filtered out" + Scenario: Running `rspec --only-failures` with spec files that pass doesn't run anything + When I run `rspec spec/passing_spec.rb --only-failures` + Then it should pass with "0 examples, 0 failures" + Scenario: Clear error given when using `--only-failures` without configuring `example_status_persistence_file_path` Given I have not configured `example_status_persistence_file_path` When I run `rspec --only-failures` diff --git a/features/step_definitions/additional_cli_steps.rb b/features/step_definitions/additional_cli_steps.rb index 6bfe7919ab..9af43ee139 100644 --- a/features/step_definitions/additional_cli_steps.rb +++ b/features/step_definitions/additional_cli_steps.rb @@ -28,6 +28,11 @@ step %q{the exit status should be 0} end +Then /^it should pass with "(.*?)"$/ do |string| + step %Q{the output should contain "#{string}"} + step %q{the exit status should be 0} +end + Then /^the example(?:s)? should(?: all)? fail$/ do step %q{the output should not contain "0 examples"} step %q{the output should not contain "0 failures"} diff --git a/lib/rspec/core/world.rb b/lib/rspec/core/world.rb index 1ab2372f65..ed15daa2cd 100644 --- a/lib/rspec/core/world.rb +++ b/lib/rspec/core/world.rb @@ -119,7 +119,7 @@ def announce_filters end end - if @configuration.run_all_when_everything_filtered? && example_count.zero? + if @configuration.run_all_when_everything_filtered? && example_count.zero? && !@configuration.only_failures? reporter.message("#{everything_filtered_message}; ignoring #{inclusion_filter.description}") filtered_examples.clear inclusion_filter.clear diff --git a/spec/rspec/core/world_spec.rb b/spec/rspec/core/world_spec.rb index cc3456bc25..95483bb11d 100644 --- a/spec/rspec/core/world_spec.rb +++ b/spec/rspec/core/world_spec.rb @@ -136,6 +136,19 @@ module RSpec::Core context "when --only-failures is passed" do before { configuration.force(:only_failures => true) } + context "and all examples are filtered out" do + before do + configuration.filter_run_including :foo => 'bar' + end + + it 'will ignore run_all_when_everything_filtered' do + configuration.run_all_when_everything_filtered = true + expect(world.filtered_examples).to_not receive(:clear) + expect(world.inclusion_filter).to_not receive(:clear) + world.announce_filters + end + end + context "and `example_status_persistence_file_path` is not configured" do it 'aborts with a message explaining the config option must be set first' do configuration.example_status_persistence_file_path = nil From 6457e3c222862316cd30c0c69e988eefaaf420b9 Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Tue, 28 Apr 2015 08:33:27 +1000 Subject: [PATCH 178/258] include the pending message in json formatter output --- lib/rspec/core/formatters/json_formatter.rb | 3 ++- spec/rspec/core/formatters/json_formatter_spec.rb | 9 ++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/rspec/core/formatters/json_formatter.rb b/lib/rspec/core/formatters/json_formatter.rb index cf21c0ab3d..9520387222 100644 --- a/lib/rspec/core/formatters/json_formatter.rb +++ b/lib/rspec/core/formatters/json_formatter.rb @@ -87,7 +87,8 @@ def format_example(example) :status => example.execution_result.status.to_s, :file_path => example.metadata[:file_path], :line_number => example.metadata[:line_number], - :run_time => example.execution_result.run_time + :run_time => example.execution_result.run_time, + :pending_message => example.execution_result.pending_message, } end end diff --git a/spec/rspec/core/formatters/json_formatter_spec.rb b/spec/rspec/core/formatters/json_formatter_spec.rb index 5e0f5ba4f1..d02e055ef4 100644 --- a/spec/rspec/core/formatters/json_formatter_spec.rb +++ b/spec/rspec/core/formatters/json_formatter_spec.rb @@ -49,7 +49,8 @@ :status => "passed", :file_path => this_file, :line_number => succeeding_line, - :run_time => formatter.output_hash[:examples][0][:run_time] + :run_time => formatter.output_hash[:examples][0][:run_time], + :pending_message => nil, }, { :description => "fails", @@ -58,11 +59,12 @@ :file_path => this_file, :line_number => failing_line, :run_time => formatter.output_hash[:examples][1][:run_time], + :pending_message => nil, :exception => { :class => "RuntimeError", :message => "eek", :backtrace => failing_backtrace - } + }, }, { :description => "pends", @@ -70,7 +72,8 @@ :status => "pending", :file_path => this_file, :line_number => pending_line, - :run_time => formatter.output_hash[:examples][2][:run_time] + :run_time => formatter.output_hash[:examples][2][:run_time], + :pending_message => "world peace", }, ], :summary => { From 81b4f10d330ad357cf278cae6972a4b7f918a798 Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Wed, 29 Apr 2015 10:09:51 +1000 Subject: [PATCH 179/258] Changelog for #1949 [skip ci] --- Changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Changelog.md b/Changelog.md index 2b92512072..5ed88fb0c9 100644 --- a/Changelog.md +++ b/Changelog.md @@ -39,6 +39,7 @@ Enhancements: * When invalid options are parsed, notify users where they came from (e.g. `.rspec` or `~/.rspec` or `ENV['SPEC_OPTS']`) so they can easily find the source of the problem. (Myron Marston, #1940) +* Add pending message contents to the json formatter output. (Jon Rowe, #1949) Bug Fixes: From a50b7653523507cd65d73d4ce98368c7e1e98253 Mon Sep 17 00:00:00 2001 From: Josh Cheek Date: Mon, 27 Apr 2015 21:24:23 -0600 Subject: [PATCH 180/258] Allows user code to retain references to memoized helpers Accomplish this by no longer clearing the example's ivars. Fixes rspec/rspec-core#1921 Context ------- * This pull request: https://github.com/rspec/rspec-core/pull/1950 * Originally clearing ivars due to memory leak: https://github.com/rspec/rspec-core/issues/321 * Threadsafe memoized helpers caused `__memoized` to stop lazily initializing: https://github.com/rspec/rspec-core/issues/321 * This caused it to be permawiped by by the resetting of the example's ivars: https://github.com/rspec/rspec-core/issues/1921 However, this patch tests the behaviour of the memory leak, rather than its mechanics, which shows that it was fixed at some point. So we simply remove that code. --- lib/rspec/core/example.rb | 5 +-- spec/rspec/core/example_spec.rb | 66 ++++++++++++++++++--------------- spec/spec_helper.rb | 12 +++--- 3 files changed, 43 insertions(+), 40 deletions(-) diff --git a/lib/rspec/core/example.rb b/lib/rspec/core/example.rb index c38a260071..4b29437af5 100644 --- a/lib/rspec/core/example.rb +++ b/lib/rspec/core/example.rb @@ -225,10 +225,7 @@ def run(example_group_instance, reporter) rescue Exception => e set_exception(e) ensure - ExampleGroup.each_instance_variable_for_example(@example_group_instance) do |ivar| - @example_group_instance.instance_variable_set(ivar, nil) - end - @example_group_instance = nil + @example_group_instance = nil # if you love something... let it go end finish(reporter) diff --git a/spec/rspec/core/example_spec.rb b/spec/rspec/core/example_spec.rb index c747bd6db8..488c48a6c3 100644 --- a/spec/rspec/core/example_spec.rb +++ b/spec/rspec/core/example_spec.rb @@ -251,14 +251,6 @@ def assert(val) end describe "#run" do - it "sets its reference to the example group instance to nil" do - group = RSpec.describe do - example('example') { expect(1).to eq(1) } - end - group.run - expect(group.examples.first.instance_variable_get("@example_group_instance")).to be_nil - end - it "generates a description before tearing down mocks in case a mock object is used in the description" do group = RSpec.describe do example { test = double('Test'); expect(test).to eq test } @@ -346,33 +338,47 @@ def assert(val) ]) end - context "clearing ivars" do - it "sets ivars to nil to prep them for GC" do - group = RSpec.describe do - before(:all) { @before_all = :before_all } - before(:each) { @before_each = :before_each } - after(:each) { @after_each = :after_each } - after(:all) { @after_all = :after_all } - end - group.example("does something") do - expect(@before_all).to eq(:before_all) - expect(@before_each).to eq(:before_each) - end - expect(group.run(double.as_null_object)).to be_truthy - group.new do |example| - %w[@before_all @before_each @after_each @after_all].each do |ivar| - expect(example.instance_variable_get(ivar)).to be_nil + + context 'memory leaks, see GH-321, GH-1921' do + def self.reliable_gc + 0 != GC.method(:start).arity # older Rubies don't give us options to ensure a full GC + end + + it 'releases references to the examples / their ivars', :if => reliable_gc do + config = RSpec::Core::Configuration.new + real_reporter = RSpec::Core::Reporter.new(config) # in case it is the cause of a leak + garbage = Struct.new :defined_in + group = RSpec.describe do + before(:all) { @before_all = garbage.new :before_all } + before(:each) { @before_each = garbage.new :before_each } + after(:each) { @after_each = garbage.new :after_each } + after(:all) { @after_all = garbage.new :after_all } + example "passing" do + @passing_example = garbage.new :passing_example + expect(@passing_example).to be + end + example "failing" do + @failing_example = garbage.new :failing_example + expect(@failing_example).to_not be end end + + GC.disable + group.run real_reporter + + expect { + GC.enable + GC.start :full_mark => true, :immediate_sweep => true + }.to change { ObjectSpace.each_object(garbage).count }.from(8).to(0) end - it "does not impact the before_all_ivars which are copied to each example" do - group = RSpec.describe do - before(:all) { @before_all = "abc" } - example("first") { expect(@before_all).not_to be_nil } - example("second") { expect(@before_all).not_to be_nil } + it 'can still be referenced by user code afterwards' do + calls_a = nil + describe_successfully 'saves a lambda that references its memoized helper' do + let(:a) { 123 } + example { calls_a = lambda { a } } end - expect(group.run).to be_truthy + expect(calls_a.call).to eq 123 end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index d680c03770..16f5019242 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -31,15 +31,15 @@ def self.new(*args, &block) require file.gsub("./spec/support", "support") end - class RaiseOnFailuresReporter < RSpec::Core::NullReporter - def self.example_failed(example) - raise example.exception - end +class RaiseOnFailuresReporter < RSpec::Core::NullReporter + def self.example_failed(example) + raise example.exception end +end module CommonHelpers - def describe_successfully(&describe_body) - example_group = RSpec.describe(&describe_body) + def describe_successfully(description="", &describe_body) + example_group = RSpec.describe(description, &describe_body) ran_successfully = example_group.run RaiseOnFailuresReporter expect(ran_successfully).to eq true example_group From 42f869476d70aade2fafe96693656cd3cb439601 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Wed, 29 Apr 2015 19:27:08 -0700 Subject: [PATCH 181/258] Refactor example creation. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove unnecessary `Array#last` call. - This version is threadsafe, whereas the prior version wasn’t. Consider what would happen if another thread added an example to the `examples` array while this was happening: it would have returned a different example than the one created here. We don’t actually do any multithreading when examples are defined, and have no plans to do so, but it’s always nice to make things more threadsafe. --- lib/rspec/core/example_group.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/rspec/core/example_group.rb b/lib/rspec/core/example_group.rb index 98f0abeea2..55cef09e89 100644 --- a/lib/rspec/core/example_group.rb +++ b/lib/rspec/core/example_group.rb @@ -142,8 +142,9 @@ def self.define_example_method(name, extra_options={}) options.update(:skip => RSpec::Core::Pending::NOT_YET_IMPLEMENTED) unless block options.update(extra_options) - examples << RSpec::Core::Example.new(self, desc, options, block) - examples.last + example = RSpec::Core::Example.new(self, desc, options, block) + examples << example + example end end From 108325a656b9740da426e9cfa13c678351d999f3 Mon Sep 17 00:00:00 2001 From: Josh Cheek Date: Mon, 4 May 2015 20:34:19 -0600 Subject: [PATCH 182/258] More helpful assertions on memory leak test The test for a memory leak failed on CI https://travis-ci.org/rspec/rspec-core/jobs/61159415, saying "expected result to have changed to 0, but is now 1" This updates it so that future failures will tell us which one was not garbage collected. See https://github.com/rspec/rspec-core/pull/1950#issuecomment-98784821 for more details. --- spec/rspec/core/example_spec.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/spec/rspec/core/example_spec.rb b/spec/rspec/core/example_spec.rb index 488c48a6c3..433bc1cfb3 100644 --- a/spec/rspec/core/example_spec.rb +++ b/spec/rspec/core/example_spec.rb @@ -369,7 +369,13 @@ def self.reliable_gc expect { GC.enable GC.start :full_mark => true, :immediate_sweep => true - }.to change { ObjectSpace.each_object(garbage).count }.from(8).to(0) + }.to change { + ObjectSpace.each_object(garbage).map(&:defined_in).map(&:to_s).sort + }.from(%w[ + after_all after_each after_each + before_all before_each before_each + failing_example passing_example + ]).to([]) end it 'can still be referenced by user code afterwards' do From b06724a215cdc13c11f0b7ef5fb4682fe770e79a Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Tue, 5 May 2015 17:56:25 +1000 Subject: [PATCH 183/258] Updated travis build scripts (from rspec-dev) --- .rubocop_rspec_base.yml | 2 +- .travis.yml | 2 +- appveyor.yml | 5 ++--- script/clone_all_rspec_repos | 2 +- script/functions.sh | 2 +- script/predicate_functions.sh | 2 +- script/run_build | 2 +- script/travis_functions.sh | 2 +- 8 files changed, 9 insertions(+), 10 deletions(-) diff --git a/.rubocop_rspec_base.yml b/.rubocop_rspec_base.yml index 5a3a5b0caf..c80142af50 100644 --- a/.rubocop_rspec_base.yml +++ b/.rubocop_rspec_base.yml @@ -1,4 +1,4 @@ -# This file was generated on 2015-03-15T22:57:16-07:00 from the rspec-dev repo. +# This file was generated on 2015-05-05T17:56:25+10:00 from the rspec-dev repo. # DO NOT modify it by hand as your changes will get lost the next time it is generated. # This file contains defaults for RSpec projects. Individual projects diff --git a/.travis.yml b/.travis.yml index 8a9d1e33c1..1e841a7805 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ -# This file was generated on 2015-03-15T22:57:16-07:00 from the rspec-dev repo. +# This file was generated on 2015-05-05T17:56:25+10:00 from the rspec-dev repo. # DO NOT modify it by hand as your changes will get lost the next time it is generated. language: ruby diff --git a/appveyor.yml b/appveyor.yml index 668e41645b..8a8dab9b1b 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,4 @@ -# This file was generated on 2015-03-15T22:57:16-07:00 from the rspec-dev repo. +# This file was generated on 2015-05-05T17:56:25+10:00 from the rspec-dev repo. # DO NOT modify it by hand as your changes will get lost the next time it is generated. version: "{build}" @@ -18,8 +18,7 @@ install: - SET PATH=C:\Ruby%ruby_version%\bin;%PATH% - ruby --version - gem --version - # We lock to 1.7.7 to avoid warnings from 1.7.8 - - gem install bundler -v=1.7.7 + - gem install bundler - bundler --version - bundle install - cinst ansicon diff --git a/script/clone_all_rspec_repos b/script/clone_all_rspec_repos index c0bf6b51e6..853182818a 100755 --- a/script/clone_all_rspec_repos +++ b/script/clone_all_rspec_repos @@ -1,5 +1,5 @@ #!/bin/bash -# This file was generated on 2015-03-15T22:57:16-07:00 from the rspec-dev repo. +# This file was generated on 2015-05-05T17:56:25+10:00 from the rspec-dev repo. # DO NOT modify it by hand as your changes will get lost the next time it is generated. set -e diff --git a/script/functions.sh b/script/functions.sh index 30b0b42692..1f216818f6 100644 --- a/script/functions.sh +++ b/script/functions.sh @@ -1,4 +1,4 @@ -# This file was generated on 2015-03-15T22:57:16-07:00 from the rspec-dev repo. +# This file was generated on 2015-05-05T17:56:25+10:00 from the rspec-dev repo. # DO NOT modify it by hand as your changes will get lost the next time it is generated. SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" diff --git a/script/predicate_functions.sh b/script/predicate_functions.sh index f4478edf1d..f7e370b006 100644 --- a/script/predicate_functions.sh +++ b/script/predicate_functions.sh @@ -1,4 +1,4 @@ -# This file was generated on 2015-03-15T22:57:16-07:00 from the rspec-dev repo. +# This file was generated on 2015-05-05T17:56:25+10:00 from the rspec-dev repo. # DO NOT modify it by hand as your changes will get lost the next time it is generated. function is_mri { diff --git a/script/run_build b/script/run_build index 26f1b82f41..0e381c4f31 100755 --- a/script/run_build +++ b/script/run_build @@ -1,5 +1,5 @@ #!/bin/bash -# This file was generated on 2015-03-15T22:57:16-07:00 from the rspec-dev repo. +# This file was generated on 2015-05-05T17:56:25+10:00 from the rspec-dev repo. # DO NOT modify it by hand as your changes will get lost the next time it is generated. set -e diff --git a/script/travis_functions.sh b/script/travis_functions.sh index 5c3691fb96..d93883d19b 100644 --- a/script/travis_functions.sh +++ b/script/travis_functions.sh @@ -1,4 +1,4 @@ -# This file was generated on 2015-03-15T22:57:16-07:00 from the rspec-dev repo. +# This file was generated on 2015-05-05T17:56:25+10:00 from the rspec-dev repo. # DO NOT modify it by hand as your changes will get lost the next time it is generated. # Taken from: From 1ddaf1c6b0e041257e0da3db62db6e5b5b657264 Mon Sep 17 00:00:00 2001 From: Melissa Xie Date: Fri, 8 May 2015 17:25:36 -0400 Subject: [PATCH 184/258] Rephrase the explanation of using shared contexts via metadata * Break up a long sentence. * Fix "context" misspelling. --- features/example_groups/shared_context.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/example_groups/shared_context.feature b/features/example_groups/shared_context.feature index c21e23861e..5f90d7a82f 100644 --- a/features/example_groups/shared_context.feature +++ b/features/example_groups/shared_context.feature @@ -4,7 +4,7 @@ Feature: shared context of example groups either explicitly, using `include_context`, or implicitly by matching metadata. - When implicitly including shared contexts via matching metadata, the normal way is to define matching metadata on an example group (in which case the ontext is included in the entire group), but you can also include it in an individual example. RSpec treats every example as having a singleton example group (analogous to Ruby's singleton classes) containing just the one example. + When implicitly including shared contexts via matching metadata, the normal way is to define matching metadata on an example group, in which case the context is included in the entire group. However, you also have the option to include it in an individual example instead. RSpec treats every example as having a singleton example group (analogous to Ruby's singleton classes) containing just the one example. Background: Given a file named "shared_stuff.rb" with: From 76ad12affaa9dec63d2bdc11c53ded8becd7a707 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Thu, 23 Apr 2015 14:51:44 -0700 Subject: [PATCH 185/258] Skip test on JRuby 1.8 mode due to the fact it flaps. --- spec/rspec/core/bisect/server_spec.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/rspec/core/bisect/server_spec.rb b/spec/rspec/core/bisect/server_spec.rb index 2e302c8acf..03a18b8a64 100644 --- a/spec/rspec/core/bisect/server_spec.rb +++ b/spec/rspec/core/bisect/server_spec.rb @@ -14,6 +14,8 @@ module RSpec::Core end it 'always stops the server, even if an error occurs while yielding' do + skip "This test flaps on JRuby 1.8 mode for some reason" if RSpec::Support::Ruby.jruby? && RUBY_VERSION.to_f < 1.9 + expect(DRb).not_to have_running_server expect { From 4473ef243a0c9d16cdf2d7aaf4639f447cf3c6d2 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Thu, 23 Apr 2015 00:33:48 -0700 Subject: [PATCH 186/258] Include shared group info in output for fixed pending examples. --- Changelog.md | 2 + lib/rspec/core/notifications.rb | 12 +++++- .../documentation_formatter_spec.rb | 4 +- .../rspec/core/formatters/html_formatted.html | 30 +++++++------- spec/rspec/core/resources/formatter_specs.rb | 14 ++++--- spec/support/formatter_support.rb | 40 ++++++++++--------- 6 files changed, 58 insertions(+), 44 deletions(-) diff --git a/Changelog.md b/Changelog.md index 5ed88fb0c9..3b0823f87d 100644 --- a/Changelog.md +++ b/Changelog.md @@ -40,6 +40,8 @@ Enhancements: (e.g. `.rspec` or `~/.rspec` or `ENV['SPEC_OPTS']`) so they can easily find the source of the problem. (Myron Marston, #1940) * Add pending message contents to the json formatter output. (Jon Rowe, #1949) +* Add shared group backtrace to the output displayed by the built-in + formatters for pending examples that have been fixed. (Myron Marston, #1946) Bug Fixes: diff --git a/lib/rspec/core/notifications.rb b/lib/rspec/core/notifications.rb index 6c363dc673..fc1acc128f 100644 --- a/lib/rspec/core/notifications.rb +++ b/lib/rspec/core/notifications.rb @@ -315,7 +315,7 @@ def description # # @return [Array] The example failure message def message_lines - ["Expected pending '#{example.execution_result.pending_message}' to fail. No Error was raised."] + add_shared_group_lines(raw_message_lines, NullColorizer) end # Returns the message generated for this failure colorized line by line. @@ -323,7 +323,15 @@ def message_lines # @param colorizer [#wrap] An object to colorize the message_lines by # @return [Array] The example failure message colorized def colorized_message_lines(colorizer=::RSpec::Core::Formatters::ConsoleCodes) - message_lines.map { |line| colorizer.wrap(line, RSpec.configuration.fixed_color) } + add_shared_group_lines(raw_message_lines, colorizer).map do |line| + colorizer.wrap line, RSpec.configuration.fixed_color + end + end + + private + + def raw_message_lines + ["Expected pending '#{example.execution_result.pending_message}' to fail. No Error was raised."] end end diff --git a/spec/rspec/core/formatters/documentation_formatter_spec.rb b/spec/rspec/core/formatters/documentation_formatter_spec.rb index cbe11fc6ad..eca00dec9d 100644 --- a/spec/rspec/core/formatters/documentation_formatter_spec.rb +++ b/spec/rspec/core/formatters/documentation_formatter_spec.rb @@ -90,8 +90,8 @@ def execution_result(values) |pending command with block format | with content that would fail | is pending (PENDING: No reason given) - | with content that would pass - | fails (FAILED - 1) + | behaves like shared + | is marked as pending but passes (FAILED - 1) | |passing spec | passes diff --git a/spec/rspec/core/formatters/html_formatted.html b/spec/rspec/core/formatters/html_formatted.html index 9c3b4cd0d0..0558fbef75 100644 --- a/spec/rspec/core/formatters/html_formatted.html +++ b/spec/rspec/core/formatters/html_formatted.html @@ -296,18 +296,18 @@

RSpec Code Examples

-
with content that would pass
+
behaves like shared
- fails + is marked as pending but passes n.nnnns
Expected example to fail since it is pending, but it passed.
-
./spec/rspec/core/resources/formatter_specs.rb:16
-
14
-15  context "with content that would pass" do
-16    it "fails" do
-17      pending
-18      expect(1).to eq(1)
+
./spec/rspec/core/resources/formatter_specs.rb:4
+
2
+3RSpec.shared_examples_for "shared" do
+4  it "is marked as pending but passes" do
+5    pending
+6    expect(1).to eq(1)
@@ -333,12 +333,12 @@

RSpec Code Examples

(compared using ==)
-
./spec/rspec/core/resources/formatter_specs.rb:31
-
29RSpec.describe "failing spec" do
-30  it "fails" do
-31    expect(1).to eq(2)
-32  end
-33end
+
./spec/rspec/core/resources/formatter_specs.rb:33
+
31RSpec.describe "failing spec" do
+32  it "fails" do
+33    expect(1).to eq(2)
+34  end
+35end
@@ -351,7 +351,7 @@

RSpec Code Examples

n.nnnns
foo
-
./spec/rspec/core/resources/formatter_specs.rb:39
+
./spec/rspec/core/resources/formatter_specs.rb:41
-1# Couldn't get snippet for (erb)
diff --git a/spec/rspec/core/resources/formatter_specs.rb b/spec/rspec/core/resources/formatter_specs.rb index cdf47830d8..ef4b9403d4 100644 --- a/spec/rspec/core/resources/formatter_specs.rb +++ b/spec/rspec/core/resources/formatter_specs.rb @@ -1,5 +1,12 @@ # Deliberately named _specs.rb to avoid being loaded except when specified +RSpec.shared_examples_for "shared" do + it "is marked as pending but passes" do + pending + expect(1).to eq(1) + end +end + RSpec.describe "pending spec with no implementation" do it "is pending" end @@ -12,12 +19,7 @@ end end - context "with content that would pass" do - it "fails" do - pending - expect(1).to eq(1) - end - end + it_behaves_like "shared" end RSpec.describe "passing spec" do diff --git a/spec/support/formatter_support.rb b/spec/support/formatter_support.rb index ac1071c64b..e692ad9c12 100644 --- a/spec/support/formatter_support.rb +++ b/spec/support/formatter_support.rb @@ -44,7 +44,7 @@ def expected_summary_output_for_example_specs | | 1) pending spec with no implementation is pending | # Not yet implemented - | # ./spec/rspec/core/resources/formatter_specs.rb:4 + | # ./spec/rspec/core/resources/formatter_specs.rb:11 | | 2) pending command with block format with content that would fail is pending | # No reason given @@ -54,16 +54,17 @@ def expected_summary_output_for_example_specs | got: 1 | | (compared using ==) - | # ./spec/rspec/core/resources/formatter_specs.rb:11 + | # ./spec/rspec/core/resources/formatter_specs.rb:18 | # ./spec/support/formatter_support.rb:13:in `run_example_specs_with_formatter' | # ./spec/support/sandboxing.rb:14 | # ./spec/support/sandboxing.rb:7 | |Failures: | - | 1) pending command with block format with content that would pass fails FIXED + | 1) pending command with block format behaves like shared is marked as pending but passes FIXED | Expected pending 'No reason given' to fail. No Error was raised. - | # ./spec/rspec/core/resources/formatter_specs.rb:16 + | Shared Example Group: "shared" called from ./spec/rspec/core/resources/formatter_specs.rb:22 + | # ./spec/rspec/core/resources/formatter_specs.rb:4 | | 2) failing spec fails | Failure/Error: expect(1).to eq(2) @@ -72,7 +73,7 @@ def expected_summary_output_for_example_specs | got: 1 | | (compared using ==) - | # ./spec/rspec/core/resources/formatter_specs.rb:31 + | # ./spec/rspec/core/resources/formatter_specs.rb:33 | # ./spec/support/formatter_support.rb:13:in `run_example_specs_with_formatter' | # ./spec/support/sandboxing.rb:14 | # ./spec/support/sandboxing.rb:7 @@ -94,10 +95,10 @@ def expected_summary_output_for_example_specs | |Failed examples: | - |rspec ./spec/rspec/core/resources/formatter_specs.rb:16 # pending command with block format with content that would pass fails - |rspec ./spec/rspec/core/resources/formatter_specs.rb:30 # failing spec fails - |rspec ./spec/rspec/core/resources/formatter_specs.rb:36 # a failing spec with odd backtraces fails with a backtrace that has no file - |rspec ./spec/rspec/core/resources/formatter_specs.rb:42 # a failing spec with odd backtraces fails with a backtrace containing an erb file + |rspec ./spec/rspec/core/resources/formatter_specs.rb:4 # pending command with block format behaves like shared is marked as pending but passes + |rspec ./spec/rspec/core/resources/formatter_specs.rb:32 # failing spec fails + |rspec ./spec/rspec/core/resources/formatter_specs.rb:38 # a failing spec with odd backtraces fails with a backtrace that has no file + |rspec ./spec/rspec/core/resources/formatter_specs.rb:44 # a failing spec with odd backtraces fails with a backtrace containing an erb file EOS end else @@ -107,7 +108,7 @@ def expected_summary_output_for_example_specs | | 1) pending spec with no implementation is pending | # Not yet implemented - | # ./spec/rspec/core/resources/formatter_specs.rb:4 + | # ./spec/rspec/core/resources/formatter_specs.rb:11 | | 2) pending command with block format with content that would fail is pending | # No reason given @@ -117,16 +118,17 @@ def expected_summary_output_for_example_specs | got: 1 | | (compared using ==) - | # ./spec/rspec/core/resources/formatter_specs.rb:11:in `block (3 levels) in ' + | # ./spec/rspec/core/resources/formatter_specs.rb:18:in `block (3 levels) in ' | # ./spec/support/formatter_support.rb:13:in `run_example_specs_with_formatter' | # ./spec/support/sandboxing.rb:14:in `block (3 levels) in ' | # ./spec/support/sandboxing.rb:7:in `block (2 levels) in ' | |Failures: | - | 1) pending command with block format with content that would pass fails FIXED + | 1) pending command with block format behaves like shared is marked as pending but passes FIXED | Expected pending 'No reason given' to fail. No Error was raised. - | # ./spec/rspec/core/resources/formatter_specs.rb:16 + | Shared Example Group: "shared" called from ./spec/rspec/core/resources/formatter_specs.rb:22 + | # ./spec/rspec/core/resources/formatter_specs.rb:4 | | 2) failing spec fails | Failure/Error: expect(1).to eq(2) @@ -135,7 +137,7 @@ def expected_summary_output_for_example_specs | got: 1 | | (compared using ==) - | # ./spec/rspec/core/resources/formatter_specs.rb:31:in `block (2 levels) in ' + | # ./spec/rspec/core/resources/formatter_specs.rb:33:in `block (2 levels) in ' | # ./spec/support/formatter_support.rb:13:in `run_example_specs_with_formatter' | # ./spec/support/sandboxing.rb:14:in `block (3 levels) in ' | # ./spec/support/sandboxing.rb:7:in `block (2 levels) in ' @@ -145,7 +147,7 @@ def expected_summary_output_for_example_specs | RuntimeError: | foo | # (erb):1:in `
' - | # ./spec/rspec/core/resources/formatter_specs.rb:39:in `block (2 levels) in ' + | # ./spec/rspec/core/resources/formatter_specs.rb:41:in `block (2 levels) in ' | # ./spec/support/formatter_support.rb:13:in `run_example_specs_with_formatter' | # ./spec/support/sandboxing.rb:14:in `block (3 levels) in ' | # ./spec/support/sandboxing.rb:7:in `block (2 levels) in ' @@ -161,10 +163,10 @@ def expected_summary_output_for_example_specs | |Failed examples: | - |rspec ./spec/rspec/core/resources/formatter_specs.rb:16 # pending command with block format with content that would pass fails - |rspec ./spec/rspec/core/resources/formatter_specs.rb:30 # failing spec fails - |rspec ./spec/rspec/core/resources/formatter_specs.rb:36 # a failing spec with odd backtraces fails with a backtrace that has no file - |rspec ./spec/rspec/core/resources/formatter_specs.rb:42 # a failing spec with odd backtraces fails with a backtrace containing an erb file + |rspec ./spec/rspec/core/resources/formatter_specs.rb:4 # pending command with block format behaves like shared is marked as pending but passes + |rspec ./spec/rspec/core/resources/formatter_specs.rb:32 # failing spec fails + |rspec ./spec/rspec/core/resources/formatter_specs.rb:38 # a failing spec with odd backtraces fails with a backtrace that has no file + |rspec ./spec/rspec/core/resources/formatter_specs.rb:44 # a failing spec with odd backtraces fails with a backtrace containing an erb file EOS end end From 744a3978c2b24d3a8f95e31fb31e5fee21052bd8 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Wed, 22 Apr 2015 23:11:02 -0700 Subject: [PATCH 187/258] Favor constructor arg injection over subclass method overriding. This will make it easier to extract an `ExceptionFormatter`. --- lib/rspec/core/notifications.rb | 95 +++++++------------ .../documentation_formatter_spec.rb | 2 + 2 files changed, 37 insertions(+), 60 deletions(-) diff --git a/lib/rspec/core/notifications.rb b/lib/rspec/core/notifications.rb index fc1acc128f..9e7b87b10a 100644 --- a/lib/rspec/core/notifications.rb +++ b/lib/rspec/core/notifications.rb @@ -151,14 +151,10 @@ class FailedExampleNotification < ExampleNotification public_class_method :new # @return [Exception] The example failure - def exception - example.execution_result.exception - end + attr_reader :exception # @return [String] The example description - def description - example.full_description - end + attr_reader :description # Returns the message generated for this failure line by line. # @@ -197,11 +193,24 @@ def colorized_formatted_backtrace(colorizer=::RSpec::Core::Formatters::ConsoleCo # @return [String] The failure information fully formatted in the way that # RSpec's built-in formatters emit. def fully_formatted(failure_number, colorizer=::RSpec::Core::Formatters::ConsoleCodes) - "\n #{failure_number}) #{description}\n#{formatted_message_and_backtrace(colorizer)}" + "\n #{failure_number}) #{description}#{detail_formatter.call(example, colorizer)}" \ + "\n#{formatted_message_and_backtrace(colorizer)}" end + attr_reader :message_color, :detail_formatter + private :message_color, :detail_formatter + private + def initialize(example, options={}) + @exception = options.fetch(:exception) { example.execution_result.exception } + @message_color = options.fetch(:message_color) { RSpec.configuration.failure_color } + @description = options.fetch(:description) { example.full_description } + @detail_formatter = options.fetch(:detail_formatter) { lambda { |*| } } + @failure_lines = options[:failure_lines] + super(example) + end + if String.method_defined?(:encoding) def encoding_of(string) string.encoding @@ -290,10 +299,6 @@ def formatted_message_and_backtrace(colorizer) formatted end - - def message_color - RSpec.configuration.failure_color - end end # The `PendingExampleFixedNotification` extends `ExampleNotification` with @@ -304,45 +309,21 @@ def message_color class PendingExampleFixedNotification < FailedExampleNotification public_class_method :new - # Returns the examples description. - # - # @return [String] The example description - def description - "#{example.full_description} FIXED" - end - - # Returns the message generated for this failure line by line. - # - # @return [Array] The example failure message - def message_lines - add_shared_group_lines(raw_message_lines, NullColorizer) - end - - # Returns the message generated for this failure colorized line by line. - # - # @param colorizer [#wrap] An object to colorize the message_lines by - # @return [Array] The example failure message colorized - def colorized_message_lines(colorizer=::RSpec::Core::Formatters::ConsoleCodes) - add_shared_group_lines(raw_message_lines, colorizer).map do |line| - colorizer.wrap line, RSpec.configuration.fixed_color - end - end - private - def raw_message_lines - ["Expected pending '#{example.execution_result.pending_message}' to fail. No Error was raised."] + def initialize(example) + super( + example, + :description => "#{example.full_description} FIXED", + :message_color => RSpec.configuration.fixed_color, + :failure_lines => ["Expected pending '#{example.execution_result.pending_message}' to fail. No Error was raised."] + ) end end # @private - module PendingExampleNotificationMethods - private - - def fully_formatted_header(pending_number, colorizer=::RSpec::Core::Formatters::ConsoleCodes) - colorizer.wrap("\n #{pending_number}) #{example.full_description}\n", :pending) << - colorizer.wrap(" # #{example.execution_result.pending_message}\n", :detail) - end + PENDING_DETAIL_FORMATTER = lambda do |example, colorizer| + colorizer.wrap("\n # #{example.execution_result.pending_message}", :detail) end # The `PendingExampleFailedAsExpectedNotification` extends `FailedExampleNotification` with @@ -351,24 +332,17 @@ def fully_formatted_header(pending_number, colorizer=::RSpec::Core::Formatters:: # @attr [RSpec::Core::Example] example the current example # @see ExampleNotification class PendingExampleFailedAsExpectedNotification < FailedExampleNotification - include PendingExampleNotificationMethods public_class_method :new - # @return [Exception] The exception that occurred while the pending example was executed - def exception - example.execution_result.pending_exception - end - - # @return [String] The pending detail fully formatted in the way that - # RSpec's built-in formatters emit. - def fully_formatted(pending_number, colorizer=::RSpec::Core::Formatters::ConsoleCodes) - fully_formatted_header(pending_number, colorizer) << formatted_message_and_backtrace(colorizer) - end - private - def message_color - RSpec.configuration.pending_color + def initialize(example) + super( + example, + :exception => example.execution_result.pending_exception, + :message_color => RSpec.configuration.pending_color, + :detail_formatter => PENDING_DETAIL_FORMATTER + ) end end @@ -378,14 +352,15 @@ def message_color # @attr [RSpec::Core::Example] example the current example # @see ExampleNotification class SkippedExampleNotification < ExampleNotification - include PendingExampleNotificationMethods public_class_method :new # @return [String] The pending detail fully formatted in the way that # RSpec's built-in formatters emit. def fully_formatted(pending_number, colorizer=::RSpec::Core::Formatters::ConsoleCodes) formatted_caller = RSpec.configuration.backtrace_formatter.backtrace_line(example.location) - fully_formatted_header(pending_number, colorizer) << colorizer.wrap(" # #{formatted_caller}\n", :detail) + colorizer.wrap("\n #{pending_number}) #{example.full_description}", :pending) << + PENDING_DETAIL_FORMATTER.call(example, colorizer) << "\n" << + colorizer.wrap(" # #{formatted_caller}\n", :detail) end end diff --git a/spec/rspec/core/formatters/documentation_formatter_spec.rb b/spec/rspec/core/formatters/documentation_formatter_spec.rb index eca00dec9d..bb7fe165cf 100644 --- a/spec/rspec/core/formatters/documentation_formatter_spec.rb +++ b/spec/rspec/core/formatters/documentation_formatter_spec.rb @@ -18,10 +18,12 @@ def execution_result(values) it "numbers the failures" do send_notification :example_failed, example_notification( double("example 1", :description => "first example", + :full_description => "group first example", :execution_result => execution_result(:status => :failed, :exception => Exception.new) )) send_notification :example_failed, example_notification( double("example 2", :description => "second example", + :full_description => "group second example", :execution_result => execution_result(:status => :failed, :exception => Exception.new) )) From e84ba30f6d1773eac7f8264a9bb9589f89babf05 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Thu, 23 Apr 2015 01:10:14 -0700 Subject: [PATCH 188/258] Move exception formatting logic into its own class. - Simplifies the notification classes. - Provides something we can use to format multiple expectation failures packaged as a single exception for `aggregate_failures`. --- .../core/formatters/exception_presenter.rb | 136 +++++++++++++++++ lib/rspec/core/notifications.rb | 137 +++--------------- .../formatters/exception_presenter_spec.rb | 95 ++++++++++++ spec/rspec/core/notifications_spec.rb | 85 +---------- 4 files changed, 259 insertions(+), 194 deletions(-) create mode 100644 lib/rspec/core/formatters/exception_presenter.rb create mode 100644 spec/rspec/core/formatters/exception_presenter_spec.rb diff --git a/lib/rspec/core/formatters/exception_presenter.rb b/lib/rspec/core/formatters/exception_presenter.rb new file mode 100644 index 0000000000..f0482dfe82 --- /dev/null +++ b/lib/rspec/core/formatters/exception_presenter.rb @@ -0,0 +1,136 @@ +module RSpec + module Core + module Formatters + # @private + class ExceptionPresenter + attr_reader :exception, :example, :description, :message_color, :detail_formatter + private :message_color, :detail_formatter + + def initialize(exception, example, options={}) + @exception = exception + @example = example + @message_color = options.fetch(:message_color) { RSpec.configuration.failure_color } + @description = options.fetch(:description) { example.full_description } + @detail_formatter = options.fetch(:detail_formatter) { lambda { |*| } } + @failure_lines = options[:failure_lines] + end + + def message_lines + add_shared_group_lines(failure_lines, Notifications::NullColorizer) + end + + def colorized_message_lines(colorizer=::RSpec::Core::Formatters::ConsoleCodes) + add_shared_group_lines(failure_lines, colorizer).map do |line| + colorizer.wrap line, message_color + end + end + + def formatted_backtrace + backtrace_formatter.format_backtrace(exception.backtrace, example.metadata) + end + + def colorized_formatted_backtrace(colorizer=::RSpec::Core::Formatters::ConsoleCodes) + formatted_backtrace.map do |backtrace_info| + colorizer.wrap "# #{backtrace_info}", RSpec.configuration.detail_color + end + end + + def fully_formatted(failure_number, colorizer=::RSpec::Core::Formatters::ConsoleCodes) + "\n #{failure_number}) #{description}#{detail_formatter.call(example, colorizer)}" \ + "\n#{formatted_message_and_backtrace(colorizer)}" + end + + private + + if String.method_defined?(:encoding) + def encoding_of(string) + string.encoding + end + + def encoded_string(string) + RSpec::Support::EncodedString.new(string, Encoding.default_external) + end + else # for 1.8.7 + # :nocov: + def encoding_of(_string) + end + + def encoded_string(string) + RSpec::Support::EncodedString.new(string) + end + # :nocov: + end + + def backtrace_formatter + RSpec.configuration.backtrace_formatter + end + + def exception_class_name + name = exception.class.name.to_s + name = "(anonymous error class)" if name == '' + name + end + + def failure_lines + @failure_lines ||= + begin + lines = ["Failure/Error: #{read_failed_line.strip}"] + lines << "#{exception_class_name}:" unless exception_class_name =~ /RSpec/ + encoded_string(exception.message.to_s).split("\n").each do |line| + lines << " #{line}" + end + lines + end + end + + def add_shared_group_lines(lines, colorizer) + example.metadata[:shared_group_inclusion_backtrace].each do |frame| + lines << colorizer.wrap(frame.description, RSpec.configuration.default_color) + end + + lines + end + + def read_failed_line + matching_line = find_failed_line + unless matching_line + return "Unable to find matching line from backtrace" + end + + file_path, line_number = matching_line.match(/(.+?):(\d+)(|:\d+)/)[1..2] + + if File.exist?(file_path) + File.readlines(file_path)[line_number.to_i - 1] || + "Unable to find matching line in #{file_path}" + else + "Unable to find #{file_path} to read failed line" + end + rescue SecurityError + "Unable to read failed line" + end + + def find_failed_line + example_path = example.metadata[:absolute_file_path].downcase + exception.backtrace.find do |line| + next unless (line_path = line[/(.+?):(\d+)(|:\d+)/, 1]) + File.expand_path(line_path).downcase == example_path + end + end + + def formatted_message_and_backtrace(colorizer) + formatted = "" + + colorized_message_lines(colorizer).each do |line| + formatted << RSpec::Support::EncodedString.new(" #{line}\n", encoding_of(formatted)) + end + + colorized_formatted_backtrace(colorizer).each do |line| + formatted << RSpec::Support::EncodedString.new(" #{line}\n", encoding_of(formatted)) + end + + formatted + end + end + end + end +end diff --git a/lib/rspec/core/notifications.rb b/lib/rspec/core/notifications.rb index 9e7b87b10a..02e7c80d49 100644 --- a/lib/rspec/core/notifications.rb +++ b/lib/rspec/core/notifications.rb @@ -1,3 +1,4 @@ +RSpec::Support.require_rspec_core "formatters/exception_presenter" RSpec::Support.require_rspec_core "formatters/helpers" RSpec::Support.require_rspec_core "shell_escape" RSpec::Support.require_rspec_support "encoded_string" @@ -151,16 +152,20 @@ class FailedExampleNotification < ExampleNotification public_class_method :new # @return [Exception] The example failure - attr_reader :exception + def exception + @exception_presenter.exception + end # @return [String] The example description - attr_reader :description + def description + @exception_presenter.description + end # Returns the message generated for this failure line by line. # # @return [Array] The example failure message def message_lines - add_shared_group_lines(failure_lines, NullColorizer) + @exception_presenter.message_lines end # Returns the message generated for this failure colorized line by line. @@ -168,16 +173,14 @@ def message_lines # @param colorizer [#wrap] An object to colorize the message_lines by # @return [Array] The example failure message colorized def colorized_message_lines(colorizer=::RSpec::Core::Formatters::ConsoleCodes) - add_shared_group_lines(failure_lines, colorizer).map do |line| - colorizer.wrap line, message_color - end + @exception_presenter.colorized_message_lines(colorizer) end # Returns the failures formatted backtrace. # # @return [Array] the examples backtrace lines def formatted_backtrace - backtrace_formatter.format_backtrace(exception.backtrace, example.metadata) + @exception_presenter.formatted_backtrace end # Returns the failures colorized formatted backtrace. @@ -185,120 +188,21 @@ def formatted_backtrace # @param colorizer [#wrap] An object to colorize the message_lines by # @return [Array] the examples colorized backtrace lines def colorized_formatted_backtrace(colorizer=::RSpec::Core::Formatters::ConsoleCodes) - formatted_backtrace.map do |backtrace_info| - colorizer.wrap "# #{backtrace_info}", RSpec.configuration.detail_color - end + @exception_presenter.colorized_formatted_backtrace(colorizer) end # @return [String] The failure information fully formatted in the way that # RSpec's built-in formatters emit. def fully_formatted(failure_number, colorizer=::RSpec::Core::Formatters::ConsoleCodes) - "\n #{failure_number}) #{description}#{detail_formatter.call(example, colorizer)}" \ - "\n#{formatted_message_and_backtrace(colorizer)}" + @exception_presenter.fully_formatted(failure_number, colorizer) end - attr_reader :message_color, :detail_formatter - private :message_color, :detail_formatter - private - def initialize(example, options={}) - @exception = options.fetch(:exception) { example.execution_result.exception } - @message_color = options.fetch(:message_color) { RSpec.configuration.failure_color } - @description = options.fetch(:description) { example.full_description } - @detail_formatter = options.fetch(:detail_formatter) { lambda { |*| } } - @failure_lines = options[:failure_lines] + def initialize(example, exception_presenter=Formatters::ExceptionPresenter.new(example.execution_result.exception, example)) + @exception_presenter = exception_presenter super(example) end - - if String.method_defined?(:encoding) - def encoding_of(string) - string.encoding - end - - def encoded_string(string) - RSpec::Support::EncodedString.new(string, Encoding.default_external) - end - else # for 1.8.7 - # :nocov: - def encoding_of(_string) - end - - def encoded_string(string) - RSpec::Support::EncodedString.new(string) - end - # :nocov: - end - - def backtrace_formatter - RSpec.configuration.backtrace_formatter - end - - def exception_class_name - name = exception.class.name.to_s - name = "(anonymous error class)" if name == '' - name - end - - def failure_lines - @failure_lines ||= - begin - lines = ["Failure/Error: #{read_failed_line.strip}"] - lines << "#{exception_class_name}:" unless exception_class_name =~ /RSpec/ - encoded_string(exception.message.to_s).split("\n").each do |line| - lines << " #{line}" - end - lines - end - end - - def add_shared_group_lines(lines, colorizer) - example.metadata[:shared_group_inclusion_backtrace].each do |frame| - lines << colorizer.wrap(frame.description, RSpec.configuration.default_color) - end - - lines - end - - def read_failed_line - matching_line = find_failed_line - unless matching_line - return "Unable to find matching line from backtrace" - end - - file_path, line_number = matching_line.match(/(.+?):(\d+)(|:\d+)/)[1..2] - - if File.exist?(file_path) - File.readlines(file_path)[line_number.to_i - 1] || - "Unable to find matching line in #{file_path}" - else - "Unable to find #{file_path} to read failed line" - end - rescue SecurityError - "Unable to read failed line" - end - - def find_failed_line - example_path = example.metadata[:absolute_file_path].downcase - exception.backtrace.find do |line| - next unless (line_path = line[/(.+?):(\d+)(|:\d+)/, 1]) - File.expand_path(line_path).downcase == example_path - end - end - - def formatted_message_and_backtrace(colorizer) - formatted = "" - - colorized_message_lines(colorizer).each do |line| - formatted << RSpec::Support::EncodedString.new(" #{line}\n", encoding_of(formatted)) - end - - colorized_formatted_backtrace(colorizer).each do |line| - formatted << RSpec::Support::EncodedString.new(" #{line}\n", encoding_of(formatted)) - end - - formatted - end end # The `PendingExampleFixedNotification` extends `ExampleNotification` with @@ -312,12 +216,12 @@ class PendingExampleFixedNotification < FailedExampleNotification private def initialize(example) - super( - example, + super(example, Formatters::ExceptionPresenter.new( + example.execution_result.exception, example, :description => "#{example.full_description} FIXED", :message_color => RSpec.configuration.fixed_color, :failure_lines => ["Expected pending '#{example.execution_result.pending_message}' to fail. No Error was raised."] - ) + )) end end @@ -337,12 +241,11 @@ class PendingExampleFailedAsExpectedNotification < FailedExampleNotification private def initialize(example) - super( - example, - :exception => example.execution_result.pending_exception, + super(example, Formatters::ExceptionPresenter.new( + example.execution_result.pending_exception, example, :message_color => RSpec.configuration.pending_color, :detail_formatter => PENDING_DETAIL_FORMATTER - ) + )) end end diff --git a/spec/rspec/core/formatters/exception_presenter_spec.rb b/spec/rspec/core/formatters/exception_presenter_spec.rb new file mode 100644 index 0000000000..afa163067e --- /dev/null +++ b/spec/rspec/core/formatters/exception_presenter_spec.rb @@ -0,0 +1,95 @@ +require 'pathname' + +module RSpec::Core + RSpec.describe Formatters::ExceptionPresenter do + include FormatterSupport + + let(:example) { new_example } + let(:presenter) { Formatters::ExceptionPresenter.new(exception, example) } + + before do + allow(example.execution_result).to receive(:exception) { exception } + example.metadata[:absolute_file_path] = __FILE__ + end + + describe "#read_failed_line" do + def read_failed_line + presenter.send(:read_failed_line) + end + + context "when backtrace is a heterogeneous language stack trace" do + let(:exception) do + instance_double(Exception, :backtrace => [ + "at Object.prototypeMethod (foo:331:18)", + "at Array.forEach (native)", + "at a_named_javascript_function (/some/javascript/file.js:39:5)", + "/some/line/of/ruby.rb:14" + ]) + end + + it "is handled gracefully" do + expect { read_failed_line }.not_to raise_error + end + end + + context "when backtrace will generate a security error" do + let(:exception) { instance_double(Exception, :backtrace => [ "#{__FILE__}:#{__LINE__}"]) } + + it "is handled gracefully" do + with_safe_set_to_level_that_triggers_security_errors do + expect { read_failed_line }.not_to raise_error + end + end + end + + context "when ruby reports a bogus line number in the stack trace" do + let(:exception) { instance_double(Exception, :backtrace => [ "#{__FILE__}:10000000"]) } + + it "reports the filename and that it was unable to find the matching line" do + expect(read_failed_line).to include("Unable to find matching line") + end + end + + context "when ruby reports a file that does not exist" do + let(:file) { "#{__FILE__}/blah.rb" } + let(:exception) { instance_double(Exception, :backtrace => [ "#{file}:1"]) } + + it "reports the filename and that it was unable to find the matching line" do + example.metadata[:absolute_file_path] = file + expect(read_failed_line).to include("Unable to find #{file} to read failed line") + end + end + + context "when the stacktrace includes relative paths (which can happen when using `rspec/autorun` and running files through `ruby`)" do + let(:relative_file) { Pathname(__FILE__).relative_path_from(Pathname(Dir.pwd)) } + line = __LINE__ + let(:exception) { instance_double(Exception, :backtrace => ["#{relative_file}:#{line}"]) } + + it 'still finds the backtrace line' do + expect(read_failed_line).to include("line = __LINE__") + end + end + + context "when String alias to_int to_i" do + before do + String.class_exec do + alias :to_int :to_i + end + end + + after do + String.class_exec do + undef to_int + end + end + + let(:exception) { instance_double(Exception, :backtrace => [ "#{__FILE__}:#{__LINE__}"]) } + + it "doesn't hang when file exists" do + expect(read_failed_line.strip).to eql( + %Q[let(:exception) { instance_double(Exception, :backtrace => [ "\#{__FILE__}:\#{__LINE__}"]) }]) + end + end + end + end +end diff --git a/spec/rspec/core/notifications_spec.rb b/spec/rspec/core/notifications_spec.rb index dcf4992955..575bc81a70 100644 --- a/spec/rspec/core/notifications_spec.rb +++ b/spec/rspec/core/notifications_spec.rb @@ -1,10 +1,11 @@ require 'rspec/core/notifications' -require 'pathname' RSpec.describe "FailedExampleNotification" do include FormatterSupport let(:example) { new_example } + exception_line = __LINE__ + 1 + let(:exception) { instance_double(Exception, :backtrace => [ "#{__FILE__}:#{exception_line}"], :message => 'Test exception') } let(:notification) { ::RSpec::Core::Notifications::FailedExampleNotification.new(example) } before do @@ -12,86 +13,16 @@ example.metadata[:absolute_file_path] = __FILE__ end - # ported from `base_formatter_spec` should be refactored by final - describe "#read_failed_line" do - context "when backtrace is a heterogeneous language stack trace" do - let(:exception) do - instance_double(Exception, :backtrace => [ - "at Object.prototypeMethod (foo:331:18)", - "at Array.forEach (native)", - "at a_named_javascript_function (/some/javascript/file.js:39:5)", - "/some/line/of/ruby.rb:14" - ]) - end - - it "is handled gracefully" do - expect { notification.send(:read_failed_line) }.not_to raise_error - end - end - - context "when backtrace will generate a security error" do - let(:exception) { instance_double(Exception, :backtrace => [ "#{__FILE__}:#{__LINE__}"]) } - - it "is handled gracefully" do - with_safe_set_to_level_that_triggers_security_errors do - expect { notification.send(:read_failed_line) }.not_to raise_error - end - end - end - - context "when ruby reports a bogus line number in the stack trace" do - let(:exception) { instance_double(Exception, :backtrace => [ "#{__FILE__}:10000000"]) } - - it "reports the filename and that it was unable to find the matching line" do - expect(notification.send(:read_failed_line)).to include("Unable to find matching line") - end - end - - context "when ruby reports a file that does not exist" do - let(:file) { "#{__FILE__}/blah.rb" } - let(:exception) { instance_double(Exception, :backtrace => [ "#{file}:1"]) } - - it "reports the filename and that it was unable to find the matching line" do - example.metadata[:absolute_file_path] = file - expect(notification.send(:read_failed_line)).to include("Unable to find #{file} to read failed line") - end - end - - context "when the stacktrace includes relative paths (which can happen when using `rspec/autorun` and running files through `ruby`)" do - let(:relative_file) { Pathname(__FILE__).relative_path_from(Pathname(Dir.pwd)) } - line = __LINE__ - let(:exception) { instance_double(Exception, :backtrace => ["#{relative_file}:#{line}"]) } - - it 'still finds the backtrace line' do - expect(notification.send(:read_failed_line)).to include("line = __LINE__") - end - end - - context "when String alias to_int to_i" do - before do - String.class_exec do - alias :to_int :to_i - end - end - - after do - String.class_exec do - undef to_int - end - end - - let(:exception) { instance_double(Exception, :backtrace => [ "#{__FILE__}:#{__LINE__}"]) } - - it "doesn't hang when file exists" do - expect(notification.send(:read_failed_line).strip).to eql( - %Q[let(:exception) { instance_double(Exception, :backtrace => [ "\#{__FILE__}:\#{__LINE__}"]) }]) - end + it 'provides a description' do + expect(notification.description).to eq(example.full_description) + end - end + it 'provides `colorized_formatted_backtrace`, which formats the backtrace and colorizes it' do + allow(RSpec.configuration).to receive(:color_enabled?).and_return(true) + expect(notification.colorized_formatted_backtrace).to eq(["\e[36m# #{RSpec::Core::Metadata.relative_path(__FILE__)}:#{exception_line}\e[0m"]) end describe '#message_lines' do - let(:exception) { instance_double(Exception, :backtrace => [ "#{__FILE__}:#{__LINE__}"], :message => 'Test exception') } let(:example_group) { class_double(RSpec::Core::ExampleGroup, :metadata => {}, :parent_groups => [], :location => "#{__FILE__}:#{__LINE__}") } before do From 96678ca51955f75c530c74834d2dea7b1371dd6b Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Tue, 28 Apr 2015 17:04:30 -0700 Subject: [PATCH 189/258] Improve indentation support in ExceptionPrinter. --- .../core/formatters/exception_presenter.rb | 18 ++++---- .../formatters/exception_presenter_spec.rb | 42 +++++++++++++++++++ 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/lib/rspec/core/formatters/exception_presenter.rb b/lib/rspec/core/formatters/exception_presenter.rb index f0482dfe82..f8475199bb 100644 --- a/lib/rspec/core/formatters/exception_presenter.rb +++ b/lib/rspec/core/formatters/exception_presenter.rb @@ -12,6 +12,7 @@ def initialize(exception, example, options={}) @message_color = options.fetch(:message_color) { RSpec.configuration.failure_color } @description = options.fetch(:description) { example.full_description } @detail_formatter = options.fetch(:detail_formatter) { lambda { |*| } } + @indentation = options.fetch(:indentation, 2) @failure_lines = options[:failure_lines] end @@ -36,8 +37,9 @@ def colorized_formatted_backtrace(colorizer=::RSpec::Core::Formatters::ConsoleCo end def fully_formatted(failure_number, colorizer=::RSpec::Core::Formatters::ConsoleCodes) - "\n #{failure_number}) #{description}#{detail_formatter.call(example, colorizer)}" \ - "\n#{formatted_message_and_backtrace(colorizer)}" + alignment_basis = "#{' ' * @indentation}#{failure_number}) " + "\n#{alignment_basis}#{description}#{detail_formatter.call(example, colorizer)}" \ + "\n#{formatted_message_and_backtrace(colorizer, alignment_basis.length)}" end private @@ -117,15 +119,13 @@ def find_failed_line end end - def formatted_message_and_backtrace(colorizer) - formatted = "" + def formatted_message_and_backtrace(colorizer, indentation) + lines = colorized_message_lines(colorizer) + colorized_formatted_backtrace(colorizer) - colorized_message_lines(colorizer).each do |line| - formatted << RSpec::Support::EncodedString.new(" #{line}\n", encoding_of(formatted)) - end + formatted = "" - colorized_formatted_backtrace(colorizer).each do |line| - formatted << RSpec::Support::EncodedString.new(" #{line}\n", encoding_of(formatted)) + lines.each do |line| + formatted << RSpec::Support::EncodedString.new("#{' ' * indentation}#{line}\n", encoding_of(formatted)) end formatted diff --git a/spec/rspec/core/formatters/exception_presenter_spec.rb b/spec/rspec/core/formatters/exception_presenter_spec.rb index afa163067e..d3e0b8c9e8 100644 --- a/spec/rspec/core/formatters/exception_presenter_spec.rb +++ b/spec/rspec/core/formatters/exception_presenter_spec.rb @@ -12,6 +12,48 @@ module RSpec::Core example.metadata[:absolute_file_path] = __FILE__ end + describe "#fully_formatted" do + line_num = __LINE__ + 2 + let(:exception) { instance_double(Exception, :message => "Boom\nBam", :backtrace => [ "#{__FILE__}:#{line_num}"]) } + # The failure happened here! + + it "formats the exception with all the normal details" do + expect(presenter.fully_formatted(1)).to eq(<<-EOS.gsub(/^ +\|/, '')) + | + | 1) Example + | Failure/Error: # The failure happened here! + | Boom + | Bam + | # ./spec/rspec/core/formatters/exception_presenter_spec.rb:#{line_num} + EOS + end + + it "indents properly when given a multiple-digit failure index" do + expect(presenter.fully_formatted(100)).to eq(<<-EOS.gsub(/^ +\|/, '')) + | + | 100) Example + | Failure/Error: # The failure happened here! + | Boom + | Bam + | # ./spec/rspec/core/formatters/exception_presenter_spec.rb:#{line_num} + EOS + end + + it "allows the caller to specify additional indentation" do + presenter = Formatters::ExceptionPresenter.new(exception, example, :indentation => 4) + + expect(presenter.fully_formatted(1)).to eq(<<-EOS.gsub(/^ +\|/, '')) + | + | 1) Example + | Failure/Error: # The failure happened here! + | Boom + | Bam + | # ./spec/rspec/core/formatters/exception_presenter_spec.rb:#{line_num} + EOS + end + + end + describe "#read_failed_line" do def read_failed_line presenter.send(:read_failed_line) From ca21838d32663e143ef22518eb68a18cd79afbd0 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Tue, 28 Apr 2015 17:17:02 -0700 Subject: [PATCH 190/258] Allow `Failure/Error` line to be used as the exception description. --- lib/rspec/core/formatters/exception_presenter.rb | 13 +++++++++---- lib/rspec/core/notifications.rb | 8 +++++--- .../core/formatters/exception_presenter_spec.rb | 12 ++++++++++++ 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/lib/rspec/core/formatters/exception_presenter.rb b/lib/rspec/core/formatters/exception_presenter.rb index f8475199bb..3bfcc3767a 100644 --- a/lib/rspec/core/formatters/exception_presenter.rb +++ b/lib/rspec/core/formatters/exception_presenter.rb @@ -9,9 +9,9 @@ class ExceptionPresenter def initialize(exception, example, options={}) @exception = exception @example = example - @message_color = options.fetch(:message_color) { RSpec.configuration.failure_color } - @description = options.fetch(:description) { example.full_description } - @detail_formatter = options.fetch(:detail_formatter) { lambda { |*| } } + @message_color = options.fetch(:message_color) { RSpec.configuration.failure_color } + @description = options.fetch(:description_formatter) { Proc.new { example.full_description } }.call(self) + @detail_formatter = options.fetch(:detail_formatter) { Proc.new {} } @indentation = options.fetch(:indentation, 2) @failure_lines = options[:failure_lines] end @@ -42,6 +42,10 @@ def fully_formatted(failure_number, colorizer=::RSpec::Core::Formatters::Console "\n#{formatted_message_and_backtrace(colorizer, alignment_basis.length)}" end + def failure_slash_error_line + @failure_slash_error_line ||= "Failure/Error: #{read_failed_line.strip}" + end + private if String.method_defined?(:encoding) @@ -76,7 +80,8 @@ def exception_class_name def failure_lines @failure_lines ||= begin - lines = ["Failure/Error: #{read_failed_line.strip}"] + lines = [] + lines << failure_slash_error_line unless (description == failure_slash_error_line) lines << "#{exception_class_name}:" unless exception_class_name =~ /RSpec/ encoded_string(exception.message.to_s).split("\n").each do |line| lines << " #{line}" diff --git a/lib/rspec/core/notifications.rb b/lib/rspec/core/notifications.rb index 02e7c80d49..cbbc2376da 100644 --- a/lib/rspec/core/notifications.rb +++ b/lib/rspec/core/notifications.rb @@ -216,11 +216,13 @@ class PendingExampleFixedNotification < FailedExampleNotification private def initialize(example) + execution_result = example.execution_result + super(example, Formatters::ExceptionPresenter.new( example.execution_result.exception, example, - :description => "#{example.full_description} FIXED", - :message_color => RSpec.configuration.fixed_color, - :failure_lines => ["Expected pending '#{example.execution_result.pending_message}' to fail. No Error was raised."] + :description_formatter => Proc.new { "#{example.full_description} FIXED" }, + :message_color => RSpec.configuration.fixed_color, + :failure_lines => ["Expected pending '#{execution_result.pending_message}' to fail. No Error was raised."] )) end end diff --git a/spec/rspec/core/formatters/exception_presenter_spec.rb b/spec/rspec/core/formatters/exception_presenter_spec.rb index d3e0b8c9e8..5f507d017f 100644 --- a/spec/rspec/core/formatters/exception_presenter_spec.rb +++ b/spec/rspec/core/formatters/exception_presenter_spec.rb @@ -52,6 +52,18 @@ module RSpec::Core EOS end + it 'allows the failure/error line to be used as the description' do + presenter = Formatters::ExceptionPresenter.new(exception, example, :description_formatter => lambda { |p| p.failure_slash_error_line }) + + expect(presenter.fully_formatted(1)).to eq(<<-EOS.gsub(/^ +\|/, '')) + | + | 1) Failure/Error: # The failure happened here! + | Boom + | Bam + | # ./spec/rspec/core/formatters/exception_presenter_spec.rb:#{line_num} + EOS + end + end describe "#read_failed_line" do From 73747539892cbf4f7f1a9da46f1eca08dbdd2ef0 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Tue, 28 Apr 2015 22:57:10 -0700 Subject: [PATCH 191/258] Pass indentation to `:detail_formatter` as well. --- .../core/formatters/exception_presenter.rb | 28 ++++++++------- lib/rspec/core/notifications.rb | 6 ++-- .../formatters/exception_presenter_spec.rb | 35 +++++++++++++++++++ 3 files changed, 54 insertions(+), 15 deletions(-) diff --git a/lib/rspec/core/formatters/exception_presenter.rb b/lib/rspec/core/formatters/exception_presenter.rb index 3bfcc3767a..201d3a1a42 100644 --- a/lib/rspec/core/formatters/exception_presenter.rb +++ b/lib/rspec/core/formatters/exception_presenter.rb @@ -3,17 +3,18 @@ module Core module Formatters # @private class ExceptionPresenter - attr_reader :exception, :example, :description, :message_color, :detail_formatter - private :message_color, :detail_formatter + attr_reader :exception, :example, :description, :message_color, :detail_formatter, :extra_detail_formatter + private :message_color, :detail_formatter, :extra_detail_formatter def initialize(exception, example, options={}) - @exception = exception - @example = example - @message_color = options.fetch(:message_color) { RSpec.configuration.failure_color } - @description = options.fetch(:description_formatter) { Proc.new { example.full_description } }.call(self) - @detail_formatter = options.fetch(:detail_formatter) { Proc.new {} } - @indentation = options.fetch(:indentation, 2) - @failure_lines = options[:failure_lines] + @exception = exception + @example = example + @message_color = options.fetch(:message_color) { RSpec.configuration.failure_color } + @description = options.fetch(:description_formatter) { Proc.new { example.full_description } }.call(self) + @detail_formatter = options.fetch(:detail_formatter) { Proc.new {} } + @extra_detail_formatter = options.fetch(:extra_detail_formatter) { Proc.new {} } + @indentation = options.fetch(:indentation, 2) + @failure_lines = options[:failure_lines] end def message_lines @@ -38,8 +39,11 @@ def colorized_formatted_backtrace(colorizer=::RSpec::Core::Formatters::ConsoleCo def fully_formatted(failure_number, colorizer=::RSpec::Core::Formatters::ConsoleCodes) alignment_basis = "#{' ' * @indentation}#{failure_number}) " - "\n#{alignment_basis}#{description}#{detail_formatter.call(example, colorizer)}" \ - "\n#{formatted_message_and_backtrace(colorizer, alignment_basis.length)}" + indentation = ' ' * alignment_basis.length + + "\n#{alignment_basis}#{description}#{detail_formatter.call(example, colorizer, indentation)}" \ + "\n#{formatted_message_and_backtrace(colorizer, indentation)}" \ + "#{extra_detail_formatter.call(failure_number, colorizer, indentation)}" end def failure_slash_error_line @@ -130,7 +134,7 @@ def formatted_message_and_backtrace(colorizer, indentation) formatted = "" lines.each do |line| - formatted << RSpec::Support::EncodedString.new("#{' ' * indentation}#{line}\n", encoding_of(formatted)) + formatted << RSpec::Support::EncodedString.new("#{indentation}#{line}\n", encoding_of(formatted)) end formatted diff --git a/lib/rspec/core/notifications.rb b/lib/rspec/core/notifications.rb index cbbc2376da..eaedd7e618 100644 --- a/lib/rspec/core/notifications.rb +++ b/lib/rspec/core/notifications.rb @@ -228,8 +228,8 @@ def initialize(example) end # @private - PENDING_DETAIL_FORMATTER = lambda do |example, colorizer| - colorizer.wrap("\n # #{example.execution_result.pending_message}", :detail) + PENDING_DETAIL_FORMATTER = lambda do |example, colorizer, indentation| + colorizer.wrap("\n#{indentation}# #{example.execution_result.pending_message}", :detail) end # The `PendingExampleFailedAsExpectedNotification` extends `FailedExampleNotification` with @@ -264,7 +264,7 @@ class SkippedExampleNotification < ExampleNotification def fully_formatted(pending_number, colorizer=::RSpec::Core::Formatters::ConsoleCodes) formatted_caller = RSpec.configuration.backtrace_formatter.backtrace_line(example.location) colorizer.wrap("\n #{pending_number}) #{example.full_description}", :pending) << - PENDING_DETAIL_FORMATTER.call(example, colorizer) << "\n" << + PENDING_DETAIL_FORMATTER.call(example, colorizer, " ") << "\n" << colorizer.wrap(" # #{formatted_caller}\n", :detail) end end diff --git a/spec/rspec/core/formatters/exception_presenter_spec.rb b/spec/rspec/core/formatters/exception_presenter_spec.rb index 5f507d017f..63ce970d2c 100644 --- a/spec/rspec/core/formatters/exception_presenter_spec.rb +++ b/spec/rspec/core/formatters/exception_presenter_spec.rb @@ -52,6 +52,24 @@ module RSpec::Core EOS end + it 'passes the indentation on to the `:detail_formatter` lambda so it can align things' do + detail_formatter = lambda do |ex, colorizer, indentation| + "\n#{indentation}Some Detail" + end + + presenter = Formatters::ExceptionPresenter.new(exception, example, :indentation => 4, + :detail_formatter => detail_formatter) + expect(presenter.fully_formatted(1)).to eq(<<-EOS.gsub(/^ +\|/, '')) + | + | 1) Example + | Some Detail + | Failure/Error: # The failure happened here! + | Boom + | Bam + | # ./spec/rspec/core/formatters/exception_presenter_spec.rb:#{line_num} + EOS + end + it 'allows the failure/error line to be used as the description' do presenter = Formatters::ExceptionPresenter.new(exception, example, :description_formatter => lambda { |p| p.failure_slash_error_line }) @@ -64,6 +82,23 @@ module RSpec::Core EOS end + it 'allows a caller to specify extra details that are added to the bottom' do + presenter = Formatters::ExceptionPresenter.new( + exception, example, :extra_detail_formatter => lambda do |failure_number, colorizer, indentation| + "#{indentation}extra detail for failure: #{failure_number}\n" + end + ) + + expect(presenter.fully_formatted(2)).to eq(<<-EOS.gsub(/^ +\|/, '')) + | + | 2) Example + | Failure/Error: # The failure happened here! + | Boom + | Bam + | # ./spec/rspec/core/formatters/exception_presenter_spec.rb:#{line_num} + | extra detail for failure: 2 + EOS + end end describe "#read_failed_line" do From ce7d9a2d9e38c8b9a1ac2dcd6d223a5ef54de144 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Tue, 28 Apr 2015 23:09:29 -0700 Subject: [PATCH 192/258] Implement presenter for aggregated failures. --- .../core/formatters/exception_presenter.rb | 47 +++++++++ lib/rspec/core/notifications.rb | 23 ++++- spec/rspec/core/notifications_spec.rb | 97 ++++++++++++++++++- 3 files changed, 163 insertions(+), 4 deletions(-) diff --git a/lib/rspec/core/formatters/exception_presenter.rb b/lib/rspec/core/formatters/exception_presenter.rb index 201d3a1a42..e552dfebab 100644 --- a/lib/rspec/core/formatters/exception_presenter.rb +++ b/lib/rspec/core/formatters/exception_presenter.rb @@ -140,6 +140,53 @@ def formatted_message_and_backtrace(colorizer, indentation) formatted end end + + # @private + class MultipleExpectationsNotMetPresenterOptions + def self.for(exception, example, options={}) + new(exception, example, options).presenter_options + end + + attr_reader :exception, :example, :options + + def initialize(exception, example, options={}) + @exception = exception + @example = example + @options = options + end + + def presenter_options + options.merge( + :failure_lines => [], + :detail_formatter => method(:failure_summary), + :extra_detail_formatter => method(:formatted_sub_failure_list) + ) + end + + private + + def failure_summary(_example, colorizer, indentation) + # TODO: ensure this is printed in pending color when appropriate + colorizer.wrap("\n#{indentation}#{exception.summary}.", RSpec.configuration.failure_color) + end + + def formatted_sub_failure_list(failure_number, colorizer, indentation) + # TODO: message_color + exception.all_exceptions.each_with_index.map do |failure, index| + ExceptionPresenter.new( + convert_to_relative_backtrace(failure), example, + :description_formatter => :failure_slash_error_line.to_proc, + :indentation => indentation.length + ).fully_formatted("#{failure_number}.#{index + 1}", colorizer) + end.join + end + + def convert_to_relative_backtrace(failure) + failure = failure.dup + failure.set_backtrace(failure.backtrace[0..-exception.backtrace.size]) + failure + end + end end end end diff --git a/lib/rspec/core/notifications.rb b/lib/rspec/core/notifications.rb index eaedd7e618..f4890b2ffe 100644 --- a/lib/rspec/core/notifications.rb +++ b/lib/rspec/core/notifications.rb @@ -47,13 +47,32 @@ def self.for(example) elsif execution_result.status == :pending PendingExampleFailedAsExpectedNotification.new(example) elsif execution_result.status == :failed - FailedExampleNotification.new(example) + FailedExampleNotification.new(example, exception_presenter_for(example)) else new(example) end end - private_class_method :new + def self.exception_presenter_for(example) + options = if multiple_exceptions_not_met_error?(example) + Formatters::MultipleExpectationsNotMetPresenterOptions.for( + example.execution_result.exception, example + ) + else + {} + end + + Formatters::ExceptionPresenter.new( + example.execution_result.exception, example, options + ) + end + + def self.multiple_exceptions_not_met_error?(example) + return false unless defined?(RSpec::Expectations::MultipleExpectationsNotMetError) + RSpec::Expectations::MultipleExpectationsNotMetError === example.execution_result.exception + end + + private_class_method :new, :exception_presenter_for, :multiple_exceptions_not_met_error? end # The `ExamplesNotification` represents notifications sent by the reporter diff --git a/spec/rspec/core/notifications_spec.rb b/spec/rspec/core/notifications_spec.rb index 575bc81a70..3cccd61faa 100644 --- a/spec/rspec/core/notifications_spec.rb +++ b/spec/rspec/core/notifications_spec.rb @@ -3,10 +3,10 @@ RSpec.describe "FailedExampleNotification" do include FormatterSupport - let(:example) { new_example } + let(:example) { new_example(:status => :failed) } exception_line = __LINE__ + 1 let(:exception) { instance_double(Exception, :backtrace => [ "#{__FILE__}:#{exception_line}"], :message => 'Test exception') } - let(:notification) { ::RSpec::Core::Notifications::FailedExampleNotification.new(example) } + let(:notification) { ::RSpec::Core::Notifications::ExampleNotification.for(example) } before do allow(example.execution_result).to receive(:exception) { exception } @@ -22,6 +22,99 @@ expect(notification.colorized_formatted_backtrace).to eq(["\e[36m# #{RSpec::Core::Metadata.relative_path(__FILE__)}:#{exception_line}\e[0m"]) end + describe "fully formatted failure output" do + def fully_formatted + notification.fully_formatted(1) + end + + def dedent(string) + string.gsub(/^ +\|/, '') + end + + context "when the exception is a MultipleExpectationsNotMetError" do + RSpec::Matchers.define :fail_with_description do |desc| + match { false } + description { desc } + failure_message { "expected pass, but #{desc}" } + end + + def capture_and_normalize_aggregation_error + backtrace_truncation_frames = caller.length + 2 + + begin + yield + rescue RSpec::Expectations::MultipleExpectationsNotMetError => failure + # To keep the output manageable, truncate the backtraces... + ([failure] + failure.all_exceptions).each do |exception| + exception.set_backtrace(exception.backtrace[0..-backtrace_truncation_frames]) + exception.backtrace.map! { |line| line.sub(/:in .*$/, '') } + end + + failure + end + end + + let(:aggregate_line) { __LINE__ + 3 } + let(:exception) do + capture_and_normalize_aggregation_error do + aggregate_failures("multiple expectations") do + expect(1).to fail_with_description("foo") + expect(1).to fail_with_description("bar") + end + end + end + + it 'provides a summary composed of example description, failure count and aggregate backtrace' do + expect(fully_formatted.lines.first(5)).to eq(dedent(<<-EOS).lines.to_a) + | + | 1) Example + | Got 2 failures from failure aggregation block "multiple expectations". + | # #{RSpec::Core::Metadata.relative_path(__FILE__)}:#{aggregate_line} + | + EOS + end + + it 'lists each individual expectation failure, with a backtrace relative to the aggregation block' do + expect(fully_formatted.lines.to_a.last(8)).to eq(dedent(<<-EOS).lines.to_a) + | + | 1.1) Failure/Error: expect(1).to fail_with_description("foo") + | expected pass, but foo + | # #{RSpec::Core::Metadata.relative_path(__FILE__)}:#{aggregate_line + 1} + | + | 1.2) Failure/Error: expect(1).to fail_with_description("bar") + | expected pass, but bar + | # #{RSpec::Core::Metadata.relative_path(__FILE__)}:#{aggregate_line + 2} + EOS + end + + context "when there are failures and other errors" do + let(:aggregate_line) { __LINE__ + 3 } + let(:exception) do + capture_and_normalize_aggregation_error do + aggregate_failures("multiple expectations") do + expect(1).to fail_with_description("foo") + raise "boom" + end + end + end + + it 'lists both types in the exception listing' do + expect(fully_formatted.lines.to_a.last(9)).to eq(dedent(<<-EOS).lines.to_a) + | + | 1.1) Failure/Error: expect(1).to fail_with_description("foo") + | expected pass, but foo + | # #{RSpec::Core::Metadata.relative_path(__FILE__)}:#{aggregate_line + 1} + | + | 1.2) Failure/Error: raise "boom" + | RuntimeError: + | boom + | # #{RSpec::Core::Metadata.relative_path(__FILE__)}:#{aggregate_line + 2} + EOS + end + end + end + end + describe '#message_lines' do let(:example_group) { class_double(RSpec::Core::ExampleGroup, :metadata => {}, :parent_groups => [], :location => "#{__FILE__}:#{__LINE__}") } From 58925e5eeac77f07b7444681ff32faeeb4577c81 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Sun, 3 May 2015 23:51:15 -0700 Subject: [PATCH 193/258] Refactor: centralize exception presenter options. We need to combine some of these cases (such as when we get a `MultipleExpectationsNotMetError` in a pending spec), and to do that we need to combine the options, so having the options listed in the same method is a stepping stone towards that. --- .../core/formatters/exception_presenter.rb | 47 ------- lib/rspec/core/notifications.rb | 119 +++++++++--------- 2 files changed, 57 insertions(+), 109 deletions(-) diff --git a/lib/rspec/core/formatters/exception_presenter.rb b/lib/rspec/core/formatters/exception_presenter.rb index e552dfebab..201d3a1a42 100644 --- a/lib/rspec/core/formatters/exception_presenter.rb +++ b/lib/rspec/core/formatters/exception_presenter.rb @@ -140,53 +140,6 @@ def formatted_message_and_backtrace(colorizer, indentation) formatted end end - - # @private - class MultipleExpectationsNotMetPresenterOptions - def self.for(exception, example, options={}) - new(exception, example, options).presenter_options - end - - attr_reader :exception, :example, :options - - def initialize(exception, example, options={}) - @exception = exception - @example = example - @options = options - end - - def presenter_options - options.merge( - :failure_lines => [], - :detail_formatter => method(:failure_summary), - :extra_detail_formatter => method(:formatted_sub_failure_list) - ) - end - - private - - def failure_summary(_example, colorizer, indentation) - # TODO: ensure this is printed in pending color when appropriate - colorizer.wrap("\n#{indentation}#{exception.summary}.", RSpec.configuration.failure_color) - end - - def formatted_sub_failure_list(failure_number, colorizer, indentation) - # TODO: message_color - exception.all_exceptions.each_with_index.map do |failure, index| - ExceptionPresenter.new( - convert_to_relative_backtrace(failure), example, - :description_formatter => :failure_slash_error_line.to_proc, - :indentation => indentation.length - ).fully_formatted("#{failure_number}.#{index + 1}", colorizer) - end.join - end - - def convert_to_relative_backtrace(failure) - failure = failure.dup - failure.set_backtrace(failure.backtrace[0..-exception.backtrace.size]) - failure - end - end end end end diff --git a/lib/rspec/core/notifications.rb b/lib/rspec/core/notifications.rb index f4890b2ffe..5eb893b4ed 100644 --- a/lib/rspec/core/notifications.rb +++ b/lib/rspec/core/notifications.rb @@ -40,31 +40,37 @@ class ExampleNotification def self.for(example) execution_result = example.execution_result + return SkippedExampleNotification.new(example) if execution_result.example_skipped? + return new(example) unless execution_result.status == :pending || execution_result.status == :failed + + klass = FailedExampleNotification + exception = execution_result.exception + ex_presenter_options = {} + if execution_result.pending_fixed? - PendingExampleFixedNotification.new(example) - elsif execution_result.example_skipped? - SkippedExampleNotification.new(example) + klass = PendingExampleFixedNotification + ex_presenter_options = { + :description_formatter => Proc.new { "#{example.full_description} FIXED" }, + :message_color => RSpec.configuration.fixed_color, + :failure_lines => ["Expected pending '#{execution_result.pending_message}' to fail. No Error was raised."] + } elsif execution_result.status == :pending - PendingExampleFailedAsExpectedNotification.new(example) - elsif execution_result.status == :failed - FailedExampleNotification.new(example, exception_presenter_for(example)) - else - new(example) + klass = PendingExampleFailedAsExpectedNotification + exception = example.execution_result.pending_exception + ex_presenter_options = { + :message_color => RSpec.configuration.pending_color, + :detail_formatter => PENDING_DETAIL_FORMATTER + } + elsif multiple_exceptions_not_met_error?(example) + ex_presenter_options = { + :failure_lines => [], + :detail_formatter => multiple_failure_sumarizer(exception), + :extra_detail_formatter => sub_failure_list_formatter(exception, example) + } end - end - def self.exception_presenter_for(example) - options = if multiple_exceptions_not_met_error?(example) - Formatters::MultipleExpectationsNotMetPresenterOptions.for( - example.execution_result.exception, example - ) - else - {} - end - - Formatters::ExceptionPresenter.new( - example.execution_result.exception, example, options - ) + ex_presenter = Formatters::ExceptionPresenter.new(exception, example, ex_presenter_options) + klass.new(example, ex_presenter) end def self.multiple_exceptions_not_met_error?(example) @@ -72,7 +78,30 @@ def self.multiple_exceptions_not_met_error?(example) RSpec::Expectations::MultipleExpectationsNotMetError === example.execution_result.exception end - private_class_method :new, :exception_presenter_for, :multiple_exceptions_not_met_error? + def self.multiple_failure_sumarizer(exception) + lambda do |_example, colorizer, indentation| + # TODO: ensure this is printed in pending color when appropriate + colorizer.wrap("\n#{indentation}#{exception.summary}.", RSpec.configuration.failure_color) + end + end + + def self.sub_failure_list_formatter(exception, example) + lambda do |failure_number, colorizer, indentation| + # TODO: message_color + exception.all_exceptions.each_with_index.map do |failure, index| + failure = failure.dup + failure.set_backtrace(failure.backtrace[0..-exception.backtrace.size]) + + Formatters::ExceptionPresenter.new( + failure, example, + :description_formatter => :failure_slash_error_line.to_proc, + :indentation => indentation.length + ).fully_formatted("#{failure_number}.#{index + 1}", colorizer) + end.join + end + end + + private_class_method :new, :multiple_exceptions_not_met_error?, :multiple_failure_sumarizer, :sub_failure_list_formatter end # The `ExamplesNotification` represents notifications sent by the reporter @@ -156,7 +185,8 @@ def format_examples(examples) end # The `FailedExampleNotification` extends `ExampleNotification` with - # things useful for failed specs. + # things useful for examples that have failure info -- typically a + # failed or pending spec. # # @example # def example_failed(notification) @@ -224,52 +254,17 @@ def initialize(example, exception_presenter=Formatters::ExceptionPresenter.new(e end end - # The `PendingExampleFixedNotification` extends `ExampleNotification` with - # things useful for specs that pass when they are expected to fail. - # - # @attr [RSpec::Core::Example] example the current example - # @see ExampleNotification - class PendingExampleFixedNotification < FailedExampleNotification - public_class_method :new - - private + # @deprecated Use {FailedExampleNotification} instead. + class PendingExampleFixedNotification < FailedExampleNotification; end - def initialize(example) - execution_result = example.execution_result - - super(example, Formatters::ExceptionPresenter.new( - example.execution_result.exception, example, - :description_formatter => Proc.new { "#{example.full_description} FIXED" }, - :message_color => RSpec.configuration.fixed_color, - :failure_lines => ["Expected pending '#{execution_result.pending_message}' to fail. No Error was raised."] - )) - end - end + # @deprecated Use {FailedExampleNotification} instead. + class PendingExampleFailedAsExpectedNotification < FailedExampleNotification; end # @private PENDING_DETAIL_FORMATTER = lambda do |example, colorizer, indentation| colorizer.wrap("\n#{indentation}# #{example.execution_result.pending_message}", :detail) end - # The `PendingExampleFailedAsExpectedNotification` extends `FailedExampleNotification` with - # things useful for pending specs that fail as expected. - # - # @attr [RSpec::Core::Example] example the current example - # @see ExampleNotification - class PendingExampleFailedAsExpectedNotification < FailedExampleNotification - public_class_method :new - - private - - def initialize(example) - super(example, Formatters::ExceptionPresenter.new( - example.execution_result.pending_exception, example, - :message_color => RSpec.configuration.pending_color, - :detail_formatter => PENDING_DETAIL_FORMATTER - )) - end - end - # The `SkippedExampleNotification` extends `ExampleNotification` with # things useful for specs that are skipped. # From 39fdad5a8d2eb26c79ec2505bfa68f13479ca278 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Mon, 4 May 2015 07:43:43 -0700 Subject: [PATCH 194/258] Fix formatting for pending MultipleExpectationsNotMetError. To get this to work properly, we have to compose the exception presenter options for pending and for MultipleExpectationsNotMetError. --- lib/rspec/core/notifications.rb | 34 ++++++++------- spec/rspec/core/notifications_spec.rb | 59 +++++++++++++++++++++++++-- 2 files changed, 76 insertions(+), 17 deletions(-) diff --git a/lib/rspec/core/notifications.rb b/lib/rspec/core/notifications.rb index 5eb893b4ed..3c22c4e9f2 100644 --- a/lib/rspec/core/notifications.rb +++ b/lib/rspec/core/notifications.rb @@ -61,33 +61,38 @@ def self.for(example) :message_color => RSpec.configuration.pending_color, :detail_formatter => PENDING_DETAIL_FORMATTER } - elsif multiple_exceptions_not_met_error?(example) - ex_presenter_options = { + end + + if multiple_exceptions_not_met_error?(exception) + ex_presenter_options.merge!( :failure_lines => [], - :detail_formatter => multiple_failure_sumarizer(exception), - :extra_detail_formatter => sub_failure_list_formatter(exception, example) - } + :extra_detail_formatter => sub_failure_list_formatter(exception, example, + ex_presenter_options[:message_color]), + :detail_formatter => multiple_failure_sumarizer(exception, + ex_presenter_options[:detail_formatter], + ex_presenter_options[:message_color]) + ) end ex_presenter = Formatters::ExceptionPresenter.new(exception, example, ex_presenter_options) klass.new(example, ex_presenter) end - def self.multiple_exceptions_not_met_error?(example) + def self.multiple_exceptions_not_met_error?(exception) return false unless defined?(RSpec::Expectations::MultipleExpectationsNotMetError) - RSpec::Expectations::MultipleExpectationsNotMetError === example.execution_result.exception + RSpec::Expectations::MultipleExpectationsNotMetError === exception end - def self.multiple_failure_sumarizer(exception) - lambda do |_example, colorizer, indentation| - # TODO: ensure this is printed in pending color when appropriate - colorizer.wrap("\n#{indentation}#{exception.summary}.", RSpec.configuration.failure_color) + def self.multiple_failure_sumarizer(exception, prior_detail_formatter, color) + lambda do |example, colorizer, indentation| + summary = "\n#{indentation}#{colorizer.wrap(exception.summary, color || RSpec.configuration.failure_color)}." + return summary unless prior_detail_formatter + "#{prior_detail_formatter.call(example, colorizer, indentation)}#{summary}" end end - def self.sub_failure_list_formatter(exception, example) + def self.sub_failure_list_formatter(exception, example, message_color) lambda do |failure_number, colorizer, indentation| - # TODO: message_color exception.all_exceptions.each_with_index.map do |failure, index| failure = failure.dup failure.set_backtrace(failure.backtrace[0..-exception.backtrace.size]) @@ -95,7 +100,8 @@ def self.sub_failure_list_formatter(exception, example) Formatters::ExceptionPresenter.new( failure, example, :description_formatter => :failure_slash_error_line.to_proc, - :indentation => indentation.length + :indentation => indentation.length, + :message_color => message_color || RSpec.configuration.failure_color ).fully_formatted("#{failure_number}.#{index + 1}", colorizer) end.join end diff --git a/spec/rspec/core/notifications_spec.rb b/spec/rspec/core/notifications_spec.rb index 3cccd61faa..6dacf3d888 100644 --- a/spec/rspec/core/notifications_spec.rb +++ b/spec/rspec/core/notifications_spec.rb @@ -9,7 +9,7 @@ let(:notification) { ::RSpec::Core::Notifications::ExampleNotification.for(example) } before do - allow(example.execution_result).to receive(:exception) { exception } + example.execution_result.exception = exception example.metadata[:absolute_file_path] = __FILE__ end @@ -23,14 +23,21 @@ end describe "fully formatted failure output" do - def fully_formatted - notification.fully_formatted(1) + def fully_formatted(*args) + notification.fully_formatted(1, *args) end def dedent(string) string.gsub(/^ +\|/, '') end + # ANSI codes aren't easy to read in failure output, so use tags instead + class TagColorizer + def self.wrap(text, code_or_symbol) + "<#{code_or_symbol}>#{text}" + end + end + context "when the exception is a MultipleExpectationsNotMetError" do RSpec::Matchers.define :fail_with_description do |desc| match { false } @@ -87,6 +94,19 @@ def capture_and_normalize_aggregation_error EOS end + it 'uses the `failure` color in the summary output' do + expect(fully_formatted(TagColorizer)).to include( + 'Got 2 failures from failure aggregation block "multiple expectations".' + ) + end + + it 'uses the `failure` color for the sub-failure messages' do + expect(fully_formatted(TagColorizer)).to include( + ' expected pass, but foo', + ' expected pass, but bar' + ) + end + context "when there are failures and other errors" do let(:aggregate_line) { __LINE__ + 3 } let(:exception) do @@ -112,6 +132,39 @@ def capture_and_normalize_aggregation_error EOS end end + + context "in a pending spec" do + before do + example.execution_result.status = :pending + example.execution_result.pending_message = 'Some pending reason' + example.execution_result.pending_exception = exception + example.execution_result.exception = nil + end + + it 'includes both the pending message and aggregate summary' do + expect(fully_formatted.lines.first(6)).to eq(dedent(<<-EOS).lines.to_a) + | + | 1) Example + | # Some pending reason + | Got 2 failures from failure aggregation block "multiple expectations". + | # #{RSpec::Core::Metadata.relative_path(__FILE__)}:#{aggregate_line} + | + EOS + end + + it 'uses the `pending` color in the summary output' do + expect(fully_formatted(TagColorizer)).to include( + 'Got 2 failures from failure aggregation block "multiple expectations".' + ) + end + + it 'uses the `pending` color for the sub-failure messages' do + expect(fully_formatted(TagColorizer)).to include( + ' expected pass, but foo', + ' expected pass, but bar' + ) + end + end end end From 93dff119fff4de737c3c888109a3fbc837a7bacc Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Mon, 4 May 2015 11:06:26 -0700 Subject: [PATCH 195/258] Exclude shared group backtrace from sub-failure backtraces. It is already listed on the aggregate failure backtrace, and would be redundant to list it on each sub-failure. --- .../core/formatters/exception_presenter.rb | 19 ++++++----- lib/rspec/core/notifications.rb | 7 ++-- spec/rspec/core/notifications_spec.rb | 32 +++++++++++++++++++ 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/lib/rspec/core/formatters/exception_presenter.rb b/lib/rspec/core/formatters/exception_presenter.rb index 201d3a1a42..5c7cbc6eae 100644 --- a/lib/rspec/core/formatters/exception_presenter.rb +++ b/lib/rspec/core/formatters/exception_presenter.rb @@ -7,14 +7,15 @@ class ExceptionPresenter private :message_color, :detail_formatter, :extra_detail_formatter def initialize(exception, example, options={}) - @exception = exception - @example = example - @message_color = options.fetch(:message_color) { RSpec.configuration.failure_color } - @description = options.fetch(:description_formatter) { Proc.new { example.full_description } }.call(self) - @detail_formatter = options.fetch(:detail_formatter) { Proc.new {} } - @extra_detail_formatter = options.fetch(:extra_detail_formatter) { Proc.new {} } - @indentation = options.fetch(:indentation, 2) - @failure_lines = options[:failure_lines] + @exception = exception + @example = example + @message_color = options.fetch(:message_color) { RSpec.configuration.failure_color } + @description = options.fetch(:description_formatter) { Proc.new { example.full_description } }.call(self) + @detail_formatter = options.fetch(:detail_formatter) { Proc.new {} } + @extra_detail_formatter = options.fetch(:extra_detail_formatter) { Proc.new {} } + @indentation = options.fetch(:indentation, 2) + @skip_shared_group_trace = options.fetch(:skip_shared_group_trace, false) + @failure_lines = options[:failure_lines] end def message_lines @@ -95,6 +96,8 @@ def failure_lines end def add_shared_group_lines(lines, colorizer) + return lines if @skip_shared_group_trace + example.metadata[:shared_group_inclusion_backtrace].each do |frame| lines << colorizer.wrap(frame.description, RSpec.configuration.default_color) end diff --git a/lib/rspec/core/notifications.rb b/lib/rspec/core/notifications.rb index 3c22c4e9f2..b2b6d38e7d 100644 --- a/lib/rspec/core/notifications.rb +++ b/lib/rspec/core/notifications.rb @@ -99,9 +99,10 @@ def self.sub_failure_list_formatter(exception, example, message_color) Formatters::ExceptionPresenter.new( failure, example, - :description_formatter => :failure_slash_error_line.to_proc, - :indentation => indentation.length, - :message_color => message_color || RSpec.configuration.failure_color + :description_formatter => :failure_slash_error_line.to_proc, + :indentation => indentation.length, + :message_color => message_color || RSpec.configuration.failure_color, + :skip_shared_group_trace => true ).fully_formatted("#{failure_number}.#{index + 1}", colorizer) end.join end diff --git a/spec/rspec/core/notifications_spec.rb b/spec/rspec/core/notifications_spec.rb index 6dacf3d888..672e2d579d 100644 --- a/spec/rspec/core/notifications_spec.rb +++ b/spec/rspec/core/notifications_spec.rb @@ -107,6 +107,38 @@ def capture_and_normalize_aggregation_error ) end + context "when the failure happened in a shared example group" do + before do |ex| + example.metadata[:shared_group_inclusion_backtrace] << RSpec::Core::SharedExampleGroupInclusionStackFrame.new( + "Stuff", "./some_shared_group_file.rb:13" + ) + end + + it "includes the shared group backtrace as part of the aggregate failure backtrace" do + expect(fully_formatted.lines.first(6)).to eq(dedent(<<-EOS).lines.to_a) + | + | 1) Example + | Got 2 failures from failure aggregation block "multiple expectations". + | Shared Example Group: "Stuff" called from ./some_shared_group_file.rb:13 + | # #{RSpec::Core::Metadata.relative_path(__FILE__)}:#{aggregate_line} + | + EOS + end + + it "does not include the shared group backtrace in the sub-failure backtraces" do + expect(fully_formatted.lines.to_a.last(8)).to eq(dedent(<<-EOS).lines.to_a) + | + | 1.1) Failure/Error: expect(1).to fail_with_description("foo") + | expected pass, but foo + | # #{RSpec::Core::Metadata.relative_path(__FILE__)}:#{aggregate_line + 1} + | + | 1.2) Failure/Error: expect(1).to fail_with_description("bar") + | expected pass, but bar + | # #{RSpec::Core::Metadata.relative_path(__FILE__)}:#{aggregate_line + 2} + EOS + end + end + context "when there are failures and other errors" do let(:aggregate_line) { __LINE__ + 3 } let(:exception) do From 1f162ed0f78cb1fb661d9019371d7c6b108aa034 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Tue, 5 May 2015 00:19:00 -0700 Subject: [PATCH 196/258] Mask .java lines in Ruby backtraces. This should hopefully make the travis build go green... --- .jrubyrc | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .jrubyrc diff --git a/.jrubyrc b/.jrubyrc new file mode 100644 index 0000000000..d077e16c96 --- /dev/null +++ b/.jrubyrc @@ -0,0 +1,5 @@ +# Remove `.java` lines from JRuby stacktraces. Necessary for a passing travis build +# on JRuby in 1.8 mode for the new failure aggregator specs. Our generated test +# fixture doesn't know how to deal with excess java lines so it's best to ignore +# those lines. +backtrace.mask=true From 6a18dbdd5f45f230cb3813911964537542e7a8b2 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Wed, 6 May 2015 08:10:56 -0700 Subject: [PATCH 197/258] =?UTF-8?q?Standardize=20on=20rspec-support?= =?UTF-8?q?=E2=80=99s=20thread=20local=20data=20hash.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/rspec/core.rb | 11 ++--------- lib/rspec/core/example_group.rb | 11 ++++++++--- lib/rspec/core/metadata.rb | 2 +- lib/rspec/core/metadata_filter.rb | 4 ++-- lib/rspec/core/shared_example_group.rb | 2 +- spec/rspec/core/example_group_spec.rb | 8 ++++---- spec/rspec/core_spec.rb | 8 ++++++++ 7 files changed, 26 insertions(+), 20 deletions(-) diff --git a/lib/rspec/core.rb b/lib/rspec/core.rb index 9e2ee5ab18..1c3b654e10 100644 --- a/lib/rspec/core.rb +++ b/lib/rspec/core.rb @@ -119,20 +119,13 @@ def self.configure # end # def self.current_example - thread_local_metadata[:current_example] + RSpec::Support.thread_local_data[:current_example] end # Set the current example being executed. # @api private def self.current_example=(example) - thread_local_metadata[:current_example] = example - end - - # @private - # A single thread local variable so we don't excessively pollute that - # namespace. - def self.thread_local_metadata - Thread.current[:_rspec] ||= { :shared_example_group_inclusions => [] } + RSpec::Support.thread_local_data[:current_example] = example end # @private diff --git a/lib/rspec/core/example_group.rb b/lib/rspec/core/example_group.rb index 55cef09e89..138151f5cc 100644 --- a/lib/rspec/core/example_group.rb +++ b/lib/rspec/core/example_group.rb @@ -231,7 +231,7 @@ def self.define_example_method(name, extra_options={}) # @see DSL#describe def self.define_example_group_method(name, metadata={}) idempotently_define_singleton_method(name) do |*args, &example_group_block| - thread_data = RSpec.thread_local_metadata + thread_data = RSpec::Support.thread_local_data top_level = self == ExampleGroup if top_level @@ -705,17 +705,22 @@ def description # @private def self.current_backtrace - RSpec.thread_local_metadata[:shared_example_group_inclusions].reverse + shared_example_group_inclusions.reverse end # @private def self.with_frame(name, location) - current_stack = RSpec.thread_local_metadata[:shared_example_group_inclusions] + current_stack = shared_example_group_inclusions current_stack << new(name, location) yield ensure current_stack.pop end + + # @private + def self.shared_example_group_inclusions + RSpec::Support.thread_local_data[:shared_example_group_inclusions] ||= [] + end end end diff --git a/lib/rspec/core/metadata.rb b/lib/rspec/core/metadata.rb index a46578dff2..2c02034bd7 100644 --- a/lib/rspec/core/metadata.rb +++ b/lib/rspec/core/metadata.rb @@ -270,7 +270,7 @@ def self.backwards_compatibility_default_proc(&example_group_selector) # that take a metadata hash, and MetadataFilter sets this thread # local to silence the warning here since it would be so # confusing. - unless RSpec.thread_local_metadata[:silence_metadata_example_group_deprecations] + unless RSpec::Support.thread_local_data[:silence_metadata_example_group_deprecations] RSpec.deprecate("The `:example_group` key in an example group's metadata hash", :replacement => "the example group's hash directly for the " \ "computed keys and `:parent_example_group` to access the parent " \ diff --git a/lib/rspec/core/metadata_filter.rb b/lib/rspec/core/metadata_filter.rb index ee02570db0..d544931d8e 100644 --- a/lib/rspec/core/metadata_filter.rb +++ b/lib/rspec/core/metadata_filter.rb @@ -78,10 +78,10 @@ def filters_apply?(key, value, metadata) end def silence_metadata_example_group_deprecations - RSpec.thread_local_metadata[:silence_metadata_example_group_deprecations] = true + RSpec::Support.thread_local_data[:silence_metadata_example_group_deprecations] = true yield ensure - RSpec.thread_local_metadata.delete(:silence_metadata_example_group_deprecations) + RSpec::Support.thread_local_data.delete(:silence_metadata_example_group_deprecations) end end end diff --git a/lib/rspec/core/shared_example_group.rb b/lib/rspec/core/shared_example_group.rb index 4854f8b013..e1b7b86374 100644 --- a/lib/rspec/core/shared_example_group.rb +++ b/lib/rspec/core/shared_example_group.rb @@ -80,7 +80,7 @@ module SharedExampleGroup # @see ExampleGroup.include_context def shared_examples(name, *args, &block) top_level = self == ExampleGroup - if top_level && RSpec.thread_local_metadata[:in_example_group] + if top_level && RSpec::Support.thread_local_data[:in_example_group] raise "Creating isolated shared examples from within a context is " \ "not allowed. Remove `RSpec.` prefix or move this to a " \ "top-level scope." diff --git a/spec/rspec/core/example_group_spec.rb b/spec/rspec/core/example_group_spec.rb index 5bbcad7296..2db12bb080 100644 --- a/spec/rspec/core/example_group_spec.rb +++ b/spec/rspec/core/example_group_spec.rb @@ -1460,7 +1460,7 @@ def extract_execution_results(group) it "leaves RSpec's thread metadata unchanged" do expect { self.group.send(name, "named this") - }.to avoid_changing(RSpec, :thread_local_metadata) + }.to avoid_changing(RSpec::Support, :thread_local_data) end it "leaves RSpec's thread metadata unchanged, even when an error occurs during evaluation" do @@ -1468,7 +1468,7 @@ def extract_execution_results(group) self.group.send(name, "named this") do raise "boom" end - }.to raise_error("boom").and avoid_changing(RSpec, :thread_local_metadata) + }.to raise_error("boom").and avoid_changing(RSpec::Support, :thread_local_data) end it "passes parameters to the shared content" do @@ -1637,7 +1637,7 @@ def foo; end shared_examples_for("stuff") { } it_should_behave_like "stuff" end - }.to avoid_changing(RSpec, :thread_local_metadata) + }.to avoid_changing(RSpec::Support, :thread_local_data) end it "leaves RSpec's thread metadata unchanged, even when an error occurs during evaluation" do @@ -1648,7 +1648,7 @@ def foo; end raise "boom" end end - }.to raise_error("boom").and avoid_changing(RSpec, :thread_local_metadata) + }.to raise_error("boom").and avoid_changing(RSpec::Support, :thread_local_data) end end diff --git a/spec/rspec/core_spec.rb b/spec/rspec/core_spec.rb index a8f528404a..a3f51b3fa0 100644 --- a/spec/rspec/core_spec.rb +++ b/spec/rspec/core_spec.rb @@ -218,6 +218,14 @@ end end + it 'uses only one thread local variable', :run_last do + # Trigger features that use thread locals... + aggregate_failures { } + RSpec.shared_examples_for("something") { } + + expect(Thread.current.keys.map(&:to_s).grep(/rspec/i).count).to eq(1) + end + describe "::Core.path_to_executable" do it 'returns the absolute location of the exe/rspec file' do expect(File.exist? RSpec::Core.path_to_executable).to be_truthy From 7e2d3995ab1490a30b5fa68969b25671e371a3e3 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Tue, 5 May 2015 09:17:23 -0700 Subject: [PATCH 198/258] Implement `:aggregate_failures` tagging. For rspec/rspec-expectations#733. --- lib/rspec/core/configuration.rb | 8 +++ .../core/formatters/exception_presenter.rb | 10 ++-- lib/rspec/core/notifications.rb | 21 +++++++- spec/rspec/core/aggregate_failures_spec.rb | 51 +++++++++++++++++++ spec/rspec/core/notifications_spec.rb | 27 +++++++++- 5 files changed, 108 insertions(+), 9 deletions(-) create mode 100644 spec/rspec/core/aggregate_failures_spec.rb diff --git a/lib/rspec/core/configuration.rb b/lib/rspec/core/configuration.rb index 202f846f97..96584ea514 100644 --- a/lib/rspec/core/configuration.rb +++ b/lib/rspec/core/configuration.rb @@ -367,6 +367,8 @@ def initialize @libs = [] @derived_metadata_blocks = FilterableItemRepository::QueryOptimized.new(:any?) @threadsafe = true + + define_built_in_hooks end # @private @@ -1732,6 +1734,12 @@ def value_for(key) @preferred_options.fetch(key) { yield } end + def define_built_in_hooks + around(:example, :aggregate_failures => true) do |ex| + aggregate_failures(nil, :from_around_hook => true, &ex) + end + end + def assert_no_example_groups_defined(config_option) return unless RSpec.world.example_groups.any? diff --git a/lib/rspec/core/formatters/exception_presenter.rb b/lib/rspec/core/formatters/exception_presenter.rb index 5c7cbc6eae..70df2ad363 100644 --- a/lib/rspec/core/formatters/exception_presenter.rb +++ b/lib/rspec/core/formatters/exception_presenter.rb @@ -3,8 +3,9 @@ module Core module Formatters # @private class ExceptionPresenter - attr_reader :exception, :example, :description, :message_color, :detail_formatter, :extra_detail_formatter - private :message_color, :detail_formatter, :extra_detail_formatter + attr_reader :exception, :example, :description, :message_color, + :detail_formatter, :extra_detail_formatter, :backtrace_formatter + private :message_color, :detail_formatter, :extra_detail_formatter, :backtrace_formatter def initialize(exception, example, options={}) @exception = exception @@ -13,6 +14,7 @@ def initialize(exception, example, options={}) @description = options.fetch(:description_formatter) { Proc.new { example.full_description } }.call(self) @detail_formatter = options.fetch(:detail_formatter) { Proc.new {} } @extra_detail_formatter = options.fetch(:extra_detail_formatter) { Proc.new {} } + @backtrace_formatter = options.fetch(:backtrace_formatter) { RSpec.configuration.backtrace_formatter } @indentation = options.fetch(:indentation, 2) @skip_shared_group_trace = options.fetch(:skip_shared_group_trace, false) @failure_lines = options[:failure_lines] @@ -72,10 +74,6 @@ def encoded_string(string) # :nocov: end - def backtrace_formatter - RSpec.configuration.backtrace_formatter - end - def exception_class_name name = exception.class.name.to_s name = "(anonymous error class)" if name == '' diff --git a/lib/rspec/core/notifications.rb b/lib/rspec/core/notifications.rb index b2b6d38e7d..cc539e62a2 100644 --- a/lib/rspec/core/notifications.rb +++ b/lib/rspec/core/notifications.rb @@ -72,12 +72,25 @@ def self.for(example) ex_presenter_options[:detail_formatter], ex_presenter_options[:message_color]) ) + + if exception.aggregation_metadata[:from_around_hook] + ex_presenter_options[:backtrace_formatter] = EmptyBacktraceFormatter + end end ex_presenter = Formatters::ExceptionPresenter.new(exception, example, ex_presenter_options) klass.new(example, ex_presenter) end + # @private + # Used to prevent a confusing backtrace from showing up from the `aggregate_failures` + # block declared for `:aggregate_failures` metadata. + module EmptyBacktraceFormatter + def self.format_backtrace(*) + [] + end + end + def self.multiple_exceptions_not_met_error?(exception) return false unless defined?(RSpec::Expectations::MultipleExpectationsNotMetError) RSpec::Expectations::MultipleExpectationsNotMetError === exception @@ -85,7 +98,13 @@ def self.multiple_exceptions_not_met_error?(exception) def self.multiple_failure_sumarizer(exception, prior_detail_formatter, color) lambda do |example, colorizer, indentation| - summary = "\n#{indentation}#{colorizer.wrap(exception.summary, color || RSpec.configuration.failure_color)}." + summary = if exception.aggregation_metadata[:from_around_hook] + "Got #{exception.exception_count_description}:" + else + "#{exception.summary}." + end + + summary = "\n#{indentation}#{colorizer.wrap(summary, color || RSpec.configuration.failure_color)}" return summary unless prior_detail_formatter "#{prior_detail_formatter.call(example, colorizer, indentation)}#{summary}" end diff --git a/spec/rspec/core/aggregate_failures_spec.rb b/spec/rspec/core/aggregate_failures_spec.rb new file mode 100644 index 0000000000..85bec9eddd --- /dev/null +++ b/spec/rspec/core/aggregate_failures_spec.rb @@ -0,0 +1,51 @@ +RSpec.describe "Using `:aggregate_failures` metadata" do + it 'applies `aggregate_failures` to examples or groups tagged with `:aggregate_failures`' do + ex = nil + + RSpec.describe "Aggregate failures", :aggregate_failures do + ex = it "has multiple failures" do + expect(1).to be_even + expect(2).to be_odd + end + end.run + + expect(ex.execution_result.exception).to have_attributes( + :failures => [ + an_object_having_attributes(:message => 'expected `1.even?` to return true, got false'), + an_object_having_attributes(:message => 'expected `2.odd?` to return true, got false') + ] + ) + end + + it 'does not interfere with other `around` hooks' do + events = [] + + RSpec.describe "Outer" do + around do |ex| + events << :outer_before + ex.run + events << :outer_after + end + + context "aggregating failures", :aggregate_failures do + context "inner" do + around do |ex| + events << :inner_before + ex.run + events << :inner_after + end + + it "has multiple failures" do + events << :example_before + expect(1).to be_even + expect(2).to be_odd + events << :example_after + end + end + end + end.run + + expect(events).to eq([:outer_before, :inner_before, :example_before, + :example_after, :inner_after, :outer_after]) + end +end diff --git a/spec/rspec/core/notifications_spec.rb b/spec/rspec/core/notifications_spec.rb index 672e2d579d..3cd7d6f69c 100644 --- a/spec/rspec/core/notifications_spec.rb +++ b/spec/rspec/core/notifications_spec.rb @@ -96,7 +96,7 @@ def capture_and_normalize_aggregation_error it 'uses the `failure` color in the summary output' do expect(fully_formatted(TagColorizer)).to include( - 'Got 2 failures from failure aggregation block "multiple expectations".' + 'Got 2 failures from failure aggregation block "multiple expectations".' ) end @@ -107,6 +107,29 @@ def capture_and_normalize_aggregation_error ) end + context "due to using `:aggregate_failures` metadata" do + let(:exception) do + ex = nil + RSpec.describe do + ex = it "", :aggregate_failures do + expect(1).to fail_with_description("foo") + expect(1).to fail_with_description("bar") + end + end.run + + capture_and_normalize_aggregation_error { raise ex.execution_result.exception } + end + + it 'uses an alternate format for the exception summary to avoid confusing references to the aggregation block or stack trace' do + expect(fully_formatted.lines.first(4)).to eq(dedent(<<-EOS).lines.to_a) + | + | 1) Example + | Got 2 failures: + | + EOS + end + end + context "when the failure happened in a shared example group" do before do |ex| example.metadata[:shared_group_inclusion_backtrace] << RSpec::Core::SharedExampleGroupInclusionStackFrame.new( @@ -186,7 +209,7 @@ def capture_and_normalize_aggregation_error it 'uses the `pending` color in the summary output' do expect(fully_formatted(TagColorizer)).to include( - 'Got 2 failures from failure aggregation block "multiple expectations".' + 'Got 2 failures from failure aggregation block "multiple expectations".' ) end From c07e2410f7d05a11d5cdb4aeec7e603b23d335fb Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Wed, 13 May 2015 21:03:04 -0700 Subject: [PATCH 199/258] Add cuke for aggregating failures. --- features/.nav | 1 + .../aggregating_failures.feature | 173 ++++++++++++++++++ .../step_definitions/additional_cli_steps.rb | 12 ++ 3 files changed, 186 insertions(+) create mode 100644 features/expectation_framework_integration/aggregating_failures.feature diff --git a/features/.nav b/features/.nav index 415b7d0b57..212dfb5751 100644 --- a/features/.nav +++ b/features/.nav @@ -46,6 +46,7 @@ - default_path.feature - expectation_framework_integration: - configure_expectation_framework.feature + - failure_aggregation.feature - mock_framework_integration: - use_rspec.feature - use_flexmock.feature diff --git a/features/expectation_framework_integration/aggregating_failures.feature b/features/expectation_framework_integration/aggregating_failures.feature new file mode 100644 index 0000000000..eccfbbcd77 --- /dev/null +++ b/features/expectation_framework_integration/aggregating_failures.feature @@ -0,0 +1,173 @@ +Feature: Aggregating Failures + + RSpec::Expectations provides [`aggregate_failures`](../../../rspec-expectations/docs/aggregating-failures), an API that allows you to group a set of expectations and see all the failures at once, rather than it aborting on the first failure. RSpec::Core improves on this feature in a couple of ways: + + * RSpec::Core provides much better failure output, adding code snippets and backtraces to the sub-failures, just like it does for any normal failure. + * RSpec::Core provides [metadata](../metadata/user-defined-metadata) integration for this feature. Each example that is tagged with `:aggregate_failures` will be wrapped in an `aggregate_failures` block. You can also use `config.define_derived_metadata` to apply this to every example automatically. + + The metadata form is quite convenient, but may not work well for end-to-end tests that have multiple distinct steps. For example, consider a spec for an HTTP client workflow that (1) makes a request, (2) expects a redirect, (3) follows the redirect, and (4) expects a particular response. You probably want the `expect(response.status).to be_between(300, 399)` expectation to immediately abort if it fails, because you can't perform the next step (following the redirect) if that is not satisfied. For these situations, we encourage you to use the `aggregate_failures` block form to wrap each set of expectations that represents a distinct step in the test workflow. + + Background: + Given a file named "lib/client.rb" with: + """ruby + Response = Struct.new(:status, :headers, :body) + + class Client + def self.make_request + Response.new(404, { "Content-Type" => "text/plain" }, "Not Found") + end + end + """ + + Scenario: Use `aggregate_failures` block form + Given a file named "spec/use_block_form_spec.rb" with: + """ruby + require 'client' + + RSpec.describe Client do + it "returns a successful response" do + response = Client.make_request + + aggregate_failures "testing reponse" do + expect(response.status).to eq(200) + expect(response.headers).to include("Content-Type" => "application/json") + expect(response.body).to eq('{"message":"Success"}') + end + end + end + """ + When I run `rspec spec/use_block_form_spec.rb` + Then it should fail listing all the failures: + """ + Failures: + + 1) Client returns a successful response + Got 3 failures from failure aggregation block "testing reponse". + # ./spec/use_block_form_spec.rb:7:in `block (2 levels) in ' + + 1.1) Failure/Error: expect(response.status).to eq(200) + + expected: 200 + got: 404 + + (compared using ==) + # ./spec/use_block_form_spec.rb:8:in `block (3 levels) in ' + + 1.2) Failure/Error: expect(response.headers).to include("Content-Type" => "application/json") + expected {"Content-Type" => "text/plain"} to include {"Content-Type" => "application/json"} + Diff: + @@ -1,2 +1,2 @@ + -[{"Content-Type"=>"application/json"}] + +"Content-Type" => "text/plain", + # ./spec/use_block_form_spec.rb:9:in `block (3 levels) in ' + + 1.3) Failure/Error: expect(response.body).to eq('{"message":"Success"}') + + expected: "{\"message\":\"Success\"}" + got: "Not Found" + + (compared using ==) + # ./spec/use_block_form_spec.rb:10:in `block (3 levels) in ' + """ + + Scenario: Use `:aggregate_failures` metadata + Given a file named "spec/use_metadata_spec.rb" with: + """ruby + require 'client' + + RSpec.describe Client do + it "returns a successful response", :aggregate_failures do + response = Client.make_request + + expect(response.status).to eq(200) + expect(response.headers).to include("Content-Type" => "application/json") + expect(response.body).to eq('{"message":"Success"}') + end + end + """ + When I run `rspec spec/use_metadata_spec.rb` + Then it should fail listing all the failures: + """ + Failures: + + 1) Client returns a successful response + Got 3 failures: + + 1.1) Failure/Error: expect(response.status).to eq(200) + + expected: 200 + got: 404 + + (compared using ==) + # ./spec/use_metadata_spec.rb:7:in `block (2 levels) in ' + + 1.2) Failure/Error: expect(response.headers).to include("Content-Type" => "application/json") + expected {"Content-Type" => "text/plain"} to include {"Content-Type" => "application/json"} + Diff: + @@ -1,2 +1,2 @@ + -[{"Content-Type"=>"application/json"}] + +"Content-Type" => "text/plain", + # ./spec/use_metadata_spec.rb:8:in `block (2 levels) in ' + + 1.3) Failure/Error: expect(response.body).to eq('{"message":"Success"}') + + expected: "{\"message\":\"Success\"}" + got: "Not Found" + + (compared using ==) + # ./spec/use_metadata_spec.rb:9:in `block (2 levels) in ' + """ + + Scenario: Enable failure aggregation globally using `define_derived_metadata` + Given a file named "spec/enable_globally_spec.rb" with: + """ruby + require 'client' + + RSpec.configure do |c| + c.define_derived_metadata do |meta| + meta[:aggregate_failures] = true + end + end + + RSpec.describe Client do + it "returns a successful response" do + response = Client.make_request + + expect(response.status).to eq(200) + expect(response.headers).to include("Content-Type" => "application/json") + expect(response.body).to eq('{"message":"Success"}') + end + end + """ + When I run `rspec spec/enable_globally_spec.rb` + Then it should fail listing all the failures: + """ + Failures: + + 1) Client returns a successful response + Got 3 failures: + + 1.1) Failure/Error: expect(response.status).to eq(200) + + expected: 200 + got: 404 + + (compared using ==) + # ./spec/enable_globally_spec.rb:13:in `block (2 levels) in ' + + 1.2) Failure/Error: expect(response.headers).to include("Content-Type" => "application/json") + expected {"Content-Type" => "text/plain"} to include {"Content-Type" => "application/json"} + Diff: + @@ -1,2 +1,2 @@ + -[{"Content-Type"=>"application/json"}] + +"Content-Type" => "text/plain", + # ./spec/enable_globally_spec.rb:14:in `block (2 levels) in ' + + 1.3) Failure/Error: expect(response.body).to eq('{"message":"Success"}') + + expected: "{\"message\":\"Success\"}" + got: "Not Found" + + (compared using ==) + # ./spec/enable_globally_spec.rb:15:in `block (2 levels) in ' + """ diff --git a/features/step_definitions/additional_cli_steps.rb b/features/step_definitions/additional_cli_steps.rb index 9af43ee139..cc807179ef 100644 --- a/features/step_definitions/additional_cli_steps.rb +++ b/features/step_definitions/additional_cli_steps.rb @@ -187,4 +187,16 @@ step "I run `#{cmd}`" end +Then(/^it should fail listing all the failures:$/) do |string| + step %q{the exit status should not be 0} + expect(normalize_whitespace_and_backtraces(all_output)).to include(normalize_whitespace_and_backtraces(string)) +end + +module WhitespaceNormalization + def normalize_whitespace_and_backtraces(text) + text.lines.map { |line| line.sub(/\s+$/, '').sub(/:in .*$/, '') }.join + end +end + +World(WhitespaceNormalization) World(FormatterSupport) From 2cfa3a839e1c4da520f9556869aabba127879cab Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Thu, 14 May 2015 07:26:23 -0700 Subject: [PATCH 200/258] Remove `:in ...` portion of backtrace lines from example output. - The presence of backticks within that part was causing rendering problems on relish. - Different rubies have different output there so we already ignore that part when comparing the expected to actual output. --- .../aggregating_failures.feature | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/features/expectation_framework_integration/aggregating_failures.feature b/features/expectation_framework_integration/aggregating_failures.feature index eccfbbcd77..4549502a78 100644 --- a/features/expectation_framework_integration/aggregating_failures.feature +++ b/features/expectation_framework_integration/aggregating_failures.feature @@ -43,7 +43,7 @@ Feature: Aggregating Failures 1) Client returns a successful response Got 3 failures from failure aggregation block "testing reponse". - # ./spec/use_block_form_spec.rb:7:in `block (2 levels) in ' + # ./spec/use_block_form_spec.rb:7 1.1) Failure/Error: expect(response.status).to eq(200) @@ -51,7 +51,7 @@ Feature: Aggregating Failures got: 404 (compared using ==) - # ./spec/use_block_form_spec.rb:8:in `block (3 levels) in ' + # ./spec/use_block_form_spec.rb:8 1.2) Failure/Error: expect(response.headers).to include("Content-Type" => "application/json") expected {"Content-Type" => "text/plain"} to include {"Content-Type" => "application/json"} @@ -59,7 +59,7 @@ Feature: Aggregating Failures @@ -1,2 +1,2 @@ -[{"Content-Type"=>"application/json"}] +"Content-Type" => "text/plain", - # ./spec/use_block_form_spec.rb:9:in `block (3 levels) in ' + # ./spec/use_block_form_spec.rb:9 1.3) Failure/Error: expect(response.body).to eq('{"message":"Success"}') @@ -67,7 +67,7 @@ Feature: Aggregating Failures got: "Not Found" (compared using ==) - # ./spec/use_block_form_spec.rb:10:in `block (3 levels) in ' + # ./spec/use_block_form_spec.rb:10 """ Scenario: Use `:aggregate_failures` metadata @@ -99,7 +99,7 @@ Feature: Aggregating Failures got: 404 (compared using ==) - # ./spec/use_metadata_spec.rb:7:in `block (2 levels) in ' + # ./spec/use_metadata_spec.rb:7 1.2) Failure/Error: expect(response.headers).to include("Content-Type" => "application/json") expected {"Content-Type" => "text/plain"} to include {"Content-Type" => "application/json"} @@ -107,7 +107,7 @@ Feature: Aggregating Failures @@ -1,2 +1,2 @@ -[{"Content-Type"=>"application/json"}] +"Content-Type" => "text/plain", - # ./spec/use_metadata_spec.rb:8:in `block (2 levels) in ' + # ./spec/use_metadata_spec.rb:8 1.3) Failure/Error: expect(response.body).to eq('{"message":"Success"}') @@ -115,7 +115,7 @@ Feature: Aggregating Failures got: "Not Found" (compared using ==) - # ./spec/use_metadata_spec.rb:9:in `block (2 levels) in ' + # ./spec/use_metadata_spec.rb:9 """ Scenario: Enable failure aggregation globally using `define_derived_metadata` @@ -153,7 +153,7 @@ Feature: Aggregating Failures got: 404 (compared using ==) - # ./spec/enable_globally_spec.rb:13:in `block (2 levels) in ' + # ./spec/enable_globally_spec.rb:13 1.2) Failure/Error: expect(response.headers).to include("Content-Type" => "application/json") expected {"Content-Type" => "text/plain"} to include {"Content-Type" => "application/json"} @@ -161,7 +161,7 @@ Feature: Aggregating Failures @@ -1,2 +1,2 @@ -[{"Content-Type"=>"application/json"}] +"Content-Type" => "text/plain", - # ./spec/enable_globally_spec.rb:14:in `block (2 levels) in ' + # ./spec/enable_globally_spec.rb:14 1.3) Failure/Error: expect(response.body).to eq('{"message":"Success"}') @@ -169,5 +169,5 @@ Feature: Aggregating Failures got: "Not Found" (compared using ==) - # ./spec/enable_globally_spec.rb:15:in `block (2 levels) in ' + # ./spec/enable_globally_spec.rb:15 """ From 86ab1fd2b089972d9ec3009441e8ec6cc6d5ca28 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Thu, 14 May 2015 22:01:19 -0700 Subject: [PATCH 201/258] Allow description to be omitted. This is necessary to support nested aggregation blocks. --- .../core/formatters/exception_presenter.rb | 8 +++++++- lib/rspec/core/notifications.rb | 10 +++++----- .../formatters/exception_presenter_spec.rb | 19 ++++++++++++++++--- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/lib/rspec/core/formatters/exception_presenter.rb b/lib/rspec/core/formatters/exception_presenter.rb index 70df2ad363..c3ca4279e1 100644 --- a/lib/rspec/core/formatters/exception_presenter.rb +++ b/lib/rspec/core/formatters/exception_presenter.rb @@ -44,7 +44,7 @@ def fully_formatted(failure_number, colorizer=::RSpec::Core::Formatters::Console alignment_basis = "#{' ' * @indentation}#{failure_number}) " indentation = ' ' * alignment_basis.length - "\n#{alignment_basis}#{description}#{detail_formatter.call(example, colorizer, indentation)}" \ + "\n#{alignment_basis}#{description_and_detail(colorizer, indentation)}" \ "\n#{formatted_message_and_backtrace(colorizer, indentation)}" \ "#{extra_detail_formatter.call(failure_number, colorizer, indentation)}" end @@ -55,6 +55,12 @@ def failure_slash_error_line private + def description_and_detail(colorizer, indentation) + detail = detail_formatter.call(example, colorizer, indentation) + return (description || detail) unless description && detail + "#{description}\n#{indentation}#{detail}" + end + if String.method_defined?(:encoding) def encoding_of(string) string.encoding diff --git a/lib/rspec/core/notifications.rb b/lib/rspec/core/notifications.rb index cc539e62a2..99e4f3a5c9 100644 --- a/lib/rspec/core/notifications.rb +++ b/lib/rspec/core/notifications.rb @@ -104,9 +104,9 @@ def self.multiple_failure_sumarizer(exception, prior_detail_formatter, color) "#{exception.summary}." end - summary = "\n#{indentation}#{colorizer.wrap(summary, color || RSpec.configuration.failure_color)}" + summary = colorizer.wrap(summary, color || RSpec.configuration.failure_color) return summary unless prior_detail_formatter - "#{prior_detail_formatter.call(example, colorizer, indentation)}#{summary}" + "#{prior_detail_formatter.call(example, colorizer, indentation)}\n#{indentation}#{summary}" end end @@ -287,8 +287,8 @@ class PendingExampleFixedNotification < FailedExampleNotification; end class PendingExampleFailedAsExpectedNotification < FailedExampleNotification; end # @private - PENDING_DETAIL_FORMATTER = lambda do |example, colorizer, indentation| - colorizer.wrap("\n#{indentation}# #{example.execution_result.pending_message}", :detail) + PENDING_DETAIL_FORMATTER = Proc.new do |example, colorizer| + colorizer.wrap("# #{example.execution_result.pending_message}", :detail) end # The `SkippedExampleNotification` extends `ExampleNotification` with @@ -304,7 +304,7 @@ class SkippedExampleNotification < ExampleNotification def fully_formatted(pending_number, colorizer=::RSpec::Core::Formatters::ConsoleCodes) formatted_caller = RSpec.configuration.backtrace_formatter.backtrace_line(example.location) colorizer.wrap("\n #{pending_number}) #{example.full_description}", :pending) << - PENDING_DETAIL_FORMATTER.call(example, colorizer, " ") << "\n" << + "\n " << PENDING_DETAIL_FORMATTER.call(example, colorizer) << "\n" << colorizer.wrap(" # #{formatted_caller}\n", :detail) end end diff --git a/spec/rspec/core/formatters/exception_presenter_spec.rb b/spec/rspec/core/formatters/exception_presenter_spec.rb index 63ce970d2c..174e6bb581 100644 --- a/spec/rspec/core/formatters/exception_presenter_spec.rb +++ b/spec/rspec/core/formatters/exception_presenter_spec.rb @@ -53,9 +53,7 @@ module RSpec::Core end it 'passes the indentation on to the `:detail_formatter` lambda so it can align things' do - detail_formatter = lambda do |ex, colorizer, indentation| - "\n#{indentation}Some Detail" - end + detail_formatter = Proc.new { "Some Detail" } presenter = Formatters::ExceptionPresenter.new(exception, example, :indentation => 4, :detail_formatter => detail_formatter) @@ -70,6 +68,21 @@ module RSpec::Core EOS end + it 'allows the caller to omit the description' do + presenter = Formatters::ExceptionPresenter.new(exception, example, + :detail_formatter => Proc.new { "Detail!" }, + :description_formatter => Proc.new { }) + + expect(presenter.fully_formatted(1)).to eq(<<-EOS.gsub(/^ +\|/, '')) + | + | 1) Detail! + | Failure/Error: # The failure happened here! + | Boom + | Bam + | # ./spec/rspec/core/formatters/exception_presenter_spec.rb:#{line_num} + EOS + end + it 'allows the failure/error line to be used as the description' do presenter = Formatters::ExceptionPresenter.new(exception, example, :description_formatter => lambda { |p| p.failure_slash_error_line }) From 2629b79feb74cde774ce63164674d47c8a6927b3 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Thu, 14 May 2015 23:33:22 -0700 Subject: [PATCH 202/258] Ensure exception formatting is applied recursively. This is necessary for niche situations where `aggregate_failures` is nested. --- .../aggregating_failures.feature | 64 ++++++++++++++++ lib/rspec/core/notifications.rb | 53 ++++++++----- spec/rspec/core/notifications_spec.rb | 74 ++++++++++++++++--- 3 files changed, 164 insertions(+), 27 deletions(-) diff --git a/features/expectation_framework_integration/aggregating_failures.feature b/features/expectation_framework_integration/aggregating_failures.feature index 4549502a78..52cd88fa87 100644 --- a/features/expectation_framework_integration/aggregating_failures.feature +++ b/features/expectation_framework_integration/aggregating_failures.feature @@ -171,3 +171,67 @@ Feature: Aggregating Failures (compared using ==) # ./spec/enable_globally_spec.rb:15 """ + + Scenario: Nested failure aggregation works + Given a file named "spec/nested_failure_aggregation_spec.rb" with: + """ruby + require 'client' + + RSpec.describe Client do + it "returns a successful response", :aggregate_failures do + response = Client.make_request + + expect(response.status).to eq(200) + + aggregate_failures "testing headers" do + expect(response.headers).to include("Content-Type" => "application/json") + expect(response.headers).to include("Content-Length" => "21") + end + + expect(response.body).to eq('{"message":"Success"}') + end + end + """ + When I run `rspec spec/nested_failure_aggregation_spec.rb` + Then it should fail listing all the failures: + """ + Failures: + + 1) Client returns a successful response + Got 3 failures: + + 1.1) Failure/Error: expect(response.status).to eq(200) + + expected: 200 + got: 404 + + (compared using ==) + # ./spec/nested_failure_aggregation_spec.rb:7 + + 1.2) Got 2 failures from failure aggregation block "testing headers". + # ./spec/nested_failure_aggregation_spec.rb:9 + + 1.2.1) Failure/Error: expect(response.headers).to include("Content-Type" => "application/json") + expected {"Content-Type" => "text/plain"} to include {"Content-Type" => "application/json"} + Diff: + @@ -1,2 +1,2 @@ + -[{"Content-Type"=>"application/json"}] + +"Content-Type" => "text/plain", + # ./spec/nested_failure_aggregation_spec.rb:10 + + 1.2.2) Failure/Error: expect(response.headers).to include("Content-Length" => "21") + expected {"Content-Type" => "text/plain"} to include {"Content-Length" => "21"} + Diff: + @@ -1,2 +1,2 @@ + -[{"Content-Length"=>"21"}] + +"Content-Type" => "text/plain", + # ./spec/nested_failure_aggregation_spec.rb:11 + + 1.3) Failure/Error: expect(response.body).to eq('{"message":"Success"}') + + expected: "{\"message\":\"Success\"}" + got: "Not Found" + + (compared using ==) + # ./spec/nested_failure_aggregation_spec.rb:14 + """ diff --git a/lib/rspec/core/notifications.rb b/lib/rspec/core/notifications.rb index 99e4f3a5c9..f4a3252320 100644 --- a/lib/rspec/core/notifications.rb +++ b/lib/rspec/core/notifications.rb @@ -64,24 +64,34 @@ def self.for(example) end if multiple_exceptions_not_met_error?(exception) - ex_presenter_options.merge!( - :failure_lines => [], - :extra_detail_formatter => sub_failure_list_formatter(exception, example, - ex_presenter_options[:message_color]), - :detail_formatter => multiple_failure_sumarizer(exception, - ex_presenter_options[:detail_formatter], - ex_presenter_options[:message_color]) + ex_presenter_options = exception_presenter_opts_for_multiple_error( + exception, example, ex_presenter_options ) - - if exception.aggregation_metadata[:from_around_hook] - ex_presenter_options[:backtrace_formatter] = EmptyBacktraceFormatter - end end ex_presenter = Formatters::ExceptionPresenter.new(exception, example, ex_presenter_options) klass.new(example, ex_presenter) end + def self.exception_presenter_opts_for_multiple_error(exception, example, options) + ex_presenter_options = options.merge( + :failure_lines => [], + :extra_detail_formatter => sub_failure_list_formatter(exception, example, + options[:message_color]), + :detail_formatter => multiple_failure_sumarizer(exception, + options[:detail_formatter], + options[:message_color]) + ) + + ex_presenter_options[:description_formatter] &&= Proc.new {} + + if exception.aggregation_metadata[:from_around_hook] + ex_presenter_options[:backtrace_formatter] = EmptyBacktraceFormatter + end + + ex_presenter_options + end + # @private # Used to prevent a confusing backtrace from showing up from the `aggregate_failures` # block declared for `:aggregate_failures` metadata. @@ -113,21 +123,30 @@ def self.multiple_failure_sumarizer(exception, prior_detail_formatter, color) def self.sub_failure_list_formatter(exception, example, message_color) lambda do |failure_number, colorizer, indentation| exception.all_exceptions.each_with_index.map do |failure, index| - failure = failure.dup - failure.set_backtrace(failure.backtrace[0..-exception.backtrace.size]) - - Formatters::ExceptionPresenter.new( - failure, example, + options = { :description_formatter => :failure_slash_error_line.to_proc, :indentation => indentation.length, :message_color => message_color || RSpec.configuration.failure_color, :skip_shared_group_trace => true + } + + if multiple_exceptions_not_met_error?(failure) + options = exception_presenter_opts_for_multiple_error(failure, example, options) + end + + failure = failure.dup + failure.set_backtrace(failure.backtrace[0..-exception.backtrace.size]) + + Formatters::ExceptionPresenter.new( + failure, example, options ).fully_formatted("#{failure_number}.#{index + 1}", colorizer) end.join end end - private_class_method :new, :multiple_exceptions_not_met_error?, :multiple_failure_sumarizer, :sub_failure_list_formatter + private_class_method :new, :multiple_exceptions_not_met_error?, + :multiple_failure_sumarizer, :sub_failure_list_formatter, + :exception_presenter_opts_for_multiple_error end # The `ExamplesNotification` represents notifications sent by the reporter diff --git a/spec/rspec/core/notifications_spec.rb b/spec/rspec/core/notifications_spec.rb index 3cd7d6f69c..2464b77814 100644 --- a/spec/rspec/core/notifications_spec.rb +++ b/spec/rspec/core/notifications_spec.rb @@ -46,19 +46,27 @@ def self.wrap(text, code_or_symbol) end def capture_and_normalize_aggregation_error - backtrace_truncation_frames = caller.length + 2 - - begin - yield - rescue RSpec::Expectations::MultipleExpectationsNotMetError => failure - # To keep the output manageable, truncate the backtraces... - ([failure] + failure.all_exceptions).each do |exception| - exception.set_backtrace(exception.backtrace[0..-backtrace_truncation_frames]) - exception.backtrace.map! { |line| line.sub(/:in .*$/, '') } + yield + rescue RSpec::Expectations::MultipleExpectationsNotMetError => failure + normalize_backtraces(failure) + failure + end + + def normalize_backtraces(failure) + failure.all_exceptions.each do |exception| + if exception.is_a?(RSpec::Expectations::MultipleExpectationsNotMetError) + normalize_backtraces(exception) end - failure + normalize_one_backtrace(exception) end + + normalize_one_backtrace(failure) + end + + def normalize_one_backtrace(exception) + line = exception.backtrace.find { |l| l.include?(__FILE__) } + exception.set_backtrace([ line.sub(/:in .*$/, '') ]) end let(:aggregate_line) { __LINE__ + 3 } @@ -162,6 +170,52 @@ def capture_and_normalize_aggregation_error end end + context "when `aggregate_failures` is used in nested fashion" do + let(:aggregate_line) { __LINE__ + 3 } + let(:exception) do + capture_and_normalize_aggregation_error do + aggregate_failures("outer") do + expect(1).to fail_with_description("foo") + + aggregate_failures("inner") do + expect(2).to fail_with_description("bar") + expect(3).to fail_with_description("baz") + end + + expect(1).to fail_with_description("qux") + end + end + end + + it 'recursively formats the nested aggregated failures' do + expect(fully_formatted).to eq(dedent <<-EOS) + | + | 1) Example + | Got 3 failures from failure aggregation block "outer". + | # #{RSpec::Core::Metadata.relative_path(__FILE__)}:#{aggregate_line} + | + | 1.1) Failure/Error: expect(1).to fail_with_description("foo") + | expected pass, but foo + | # #{RSpec::Core::Metadata.relative_path(__FILE__)}:#{aggregate_line + 1} + | + | 1.2) Got 2 failures from failure aggregation block "inner". + | # #{RSpec::Core::Metadata.relative_path(__FILE__)}:#{aggregate_line + 3} + | + | 1.2.1) Failure/Error: expect(2).to fail_with_description("bar") + | expected pass, but bar + | # #{RSpec::Core::Metadata.relative_path(__FILE__)}:#{aggregate_line + 4} + | + | 1.2.2) Failure/Error: expect(3).to fail_with_description("baz") + | expected pass, but baz + | # #{RSpec::Core::Metadata.relative_path(__FILE__)}:#{aggregate_line + 5} + | + | 1.3) Failure/Error: expect(1).to fail_with_description("qux") + | expected pass, but qux + | # #{RSpec::Core::Metadata.relative_path(__FILE__)}:#{aggregate_line + 8} + EOS + end + end + context "when there are failures and other errors" do let(:aggregate_line) { __LINE__ + 3 } let(:exception) do From d45569214b7ada6e6858d4d5e1dead34a1c7673a Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 15 May 2015 14:34:45 -0700 Subject: [PATCH 203/258] Add scenario demonstrating mock expectation aggregation. --- .../aggregating_failures.feature | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/features/expectation_framework_integration/aggregating_failures.feature b/features/expectation_framework_integration/aggregating_failures.feature index 52cd88fa87..ea5ec07ee4 100644 --- a/features/expectation_framework_integration/aggregating_failures.feature +++ b/features/expectation_framework_integration/aggregating_failures.feature @@ -235,3 +235,43 @@ Feature: Aggregating Failures (compared using ==) # ./spec/nested_failure_aggregation_spec.rb:14 """ + + Scenario: Mock expectation failures are aggregated as well + Given a file named "spec/mock_expectation_failure_spec.rb" with: + """ruby + require 'client' + + RSpec.describe "Aggregating Failures", :aggregate_failures do + it "has a normal expectation failure and a message expectation failure" do + client = double("Client") + expect(client).to receive(:put).with("updated data") + allow(client).to receive(:get).and_return(Response.new(404, {}, "Not Found")) + + response = client.get + expect(response.status).to eq(200) + end + end + """ + When I run `rspec spec/mock_expectation_failure_spec.rb` + Then it should fail listing all the failures: + """ + Failures: + + 1) Aggregating Failures has a normal expectation failure and a message expectation failure + Got 2 failures: + + 1.1) Failure/Error: expect(response.status).to eq(200) + + expected: 200 + got: 404 + + (compared using ==) + # ./spec/mock_expectation_failure_spec.rb:10 + + 1.2) Failure/Error: expect(client).to receive(:put).with("updated data") + (Double "Client").put("updated data") + expected: 1 time with arguments: ("updated data") + received: 0 times + # ./spec/mock_expectation_failure_spec.rb:6 + + """ From 2d171025c69e6f1d5f384adec7c1bf641d6a3191 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 15 May 2015 15:01:56 -0700 Subject: [PATCH 204/258] Reword cuke step. --- .../aggregating_failures.feature | 10 +++++----- features/step_definitions/additional_cli_steps.rb | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/features/expectation_framework_integration/aggregating_failures.feature b/features/expectation_framework_integration/aggregating_failures.feature index ea5ec07ee4..def8b9688a 100644 --- a/features/expectation_framework_integration/aggregating_failures.feature +++ b/features/expectation_framework_integration/aggregating_failures.feature @@ -37,7 +37,7 @@ Feature: Aggregating Failures end """ When I run `rspec spec/use_block_form_spec.rb` - Then it should fail listing all the failures: + Then it should fail and list all the failures: """ Failures: @@ -86,7 +86,7 @@ Feature: Aggregating Failures end """ When I run `rspec spec/use_metadata_spec.rb` - Then it should fail listing all the failures: + Then it should fail and list all the failures: """ Failures: @@ -140,7 +140,7 @@ Feature: Aggregating Failures end """ When I run `rspec spec/enable_globally_spec.rb` - Then it should fail listing all the failures: + Then it should fail and list all the failures: """ Failures: @@ -193,7 +193,7 @@ Feature: Aggregating Failures end """ When I run `rspec spec/nested_failure_aggregation_spec.rb` - Then it should fail listing all the failures: + Then it should fail and list all the failures: """ Failures: @@ -253,7 +253,7 @@ Feature: Aggregating Failures end """ When I run `rspec spec/mock_expectation_failure_spec.rb` - Then it should fail listing all the failures: + Then it should fail and list all the failures: """ Failures: diff --git a/features/step_definitions/additional_cli_steps.rb b/features/step_definitions/additional_cli_steps.rb index cc807179ef..a3fc8b4cf3 100644 --- a/features/step_definitions/additional_cli_steps.rb +++ b/features/step_definitions/additional_cli_steps.rb @@ -187,7 +187,7 @@ step "I run `#{cmd}`" end -Then(/^it should fail listing all the failures:$/) do |string| +Then(/^it should fail and list all the failures:$/) do |string| step %q{the exit status should not be 0} expect(normalize_whitespace_and_backtraces(all_output)).to include(normalize_whitespace_and_backtraces(string)) end From 98ade33737c1c2848aa6e32da286feef49065e8d Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 15 May 2015 19:10:32 -0700 Subject: [PATCH 205/258] Add changelog entry for #1946. [ci skip] --- Changelog.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Changelog.md b/Changelog.md index 3b0823f87d..ba7e75a852 100644 --- a/Changelog.md +++ b/Changelog.md @@ -42,6 +42,11 @@ Enhancements: * Add pending message contents to the json formatter output. (Jon Rowe, #1949) * Add shared group backtrace to the output displayed by the built-in formatters for pending examples that have been fixed. (Myron Marston, #1946) +* Add support for `:aggregate_failures` metadata. Tag an example or + group with this metadata and it'll use rspec-expectations' + `aggregate_failures` feature to allow multiple failures in an example + and list them all, rather than aborting on the first failure. (Myron + Marston, #1946) Bug Fixes: From b9cf09539ca538f10897a6bca3c0b1ba8146cca8 Mon Sep 17 00:00:00 2001 From: Josh Cheek Date: Sun, 17 May 2015 20:38:46 -0600 Subject: [PATCH 206/258] Run GC multiple times if necessary As suggested in https://github.com/rspec/rspec-core/pull/1950#issuecomment-99492550 Still leaves the conditional in, b/c Rbx didn't collect all the instances in my trial, and JRuby wouldn't iterate over ObjectSpace. --- spec/rspec/core/example_spec.rb | 41 ++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/spec/rspec/core/example_spec.rb b/spec/rspec/core/example_spec.rb index 433bc1cfb3..e5011ba43e 100644 --- a/spec/rspec/core/example_spec.rb +++ b/spec/rspec/core/example_spec.rb @@ -344,11 +344,33 @@ def self.reliable_gc 0 != GC.method(:start).arity # older Rubies don't give us options to ensure a full GC end + def expect_gc(opts) + get_all = opts.fetch :get_all + + begin + GC.disable + opts.fetch(:event).call + expect(get_all.call).to eq(opts.fetch :pre_gc) + ensure + GC.enable + end + + # See discussion on https://github.com/rspec/rspec-core/pull/1950 + # for why it's necessary to do this multiple times + 20.times do + GC.start :full_mark => true, :immediate_sweep => true + return if get_all.call == opts.fetch(:post_gc) + end + + expect(get_all.call).to eq opts.fetch(:post_gc) + end + it 'releases references to the examples / their ivars', :if => reliable_gc do config = RSpec::Core::Configuration.new real_reporter = RSpec::Core::Reporter.new(config) # in case it is the cause of a leak garbage = Struct.new :defined_in - group = RSpec.describe do + + group = RSpec.describe do before(:all) { @before_all = garbage.new :before_all } before(:each) { @before_each = garbage.new :before_each } after(:each) { @after_each = garbage.new :after_each } @@ -363,19 +385,10 @@ def self.reliable_gc end end - GC.disable - group.run real_reporter - - expect { - GC.enable - GC.start :full_mark => true, :immediate_sweep => true - }.to change { - ObjectSpace.each_object(garbage).map(&:defined_in).map(&:to_s).sort - }.from(%w[ - after_all after_each after_each - before_all before_each before_each - failing_example passing_example - ]).to([]) + expect_gc :event => lambda { group.run real_reporter }, + :get_all => lambda { ObjectSpace.each_object(garbage).map { |g| g.defined_in.to_s }.sort }, + :pre_gc => %w[after_all after_each after_each before_all before_each before_each failing_example passing_example], + :post_gc => [] end it 'can still be referenced by user code afterwards' do From b8319b0cc00643a9f18324c1f460e7f1a50b997d Mon Sep 17 00:00:00 2001 From: Ben Axnick Date: Sun, 17 May 2015 00:07:56 -0500 Subject: [PATCH 207/258] Use :rerun_file_path when checking location filters Fixes #1961 The problem with using :absolute_file_path is that it doesn't function correctly for shared examples in an external file. The filter manager thinks that there are no location filters set, when actually there are. Since MetadataFilter already handles location filtering correctly across multiple nested examples / files, correcting the path that is checked is sufficient to fix the line filtering issue. --- lib/rspec/core/filter_manager.rb | 4 +++- spec/rspec/core/filter_manager_spec.rb | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/lib/rspec/core/filter_manager.rb b/lib/rspec/core/filter_manager.rb index 8351b2776f..e5e062a7ed 100644 --- a/lib/rspec/core/filter_manager.rb +++ b/lib/rspec/core/filter_manager.rb @@ -99,8 +99,10 @@ def prune_conditionally_filtered_examples(examples) # defined in the same file as the location filters. Excluded specs in # other files should still be excluded. def file_scoped_include?(ex_metadata, ids, locations) - no_location_filters = locations[ex_metadata[:absolute_file_path]].empty? no_id_filters = ids[ex_metadata[:rerun_file_path]].empty? + no_location_filters = locations[ + File.expand_path(ex_metadata[:rerun_file_path]) + ].empty? return yield if no_location_filters && no_id_filters diff --git a/spec/rspec/core/filter_manager_spec.rb b/spec/rspec/core/filter_manager_spec.rb index 0973bf75ad..63173cbb42 100644 --- a/spec/rspec/core/filter_manager_spec.rb +++ b/spec/rspec/core/filter_manager_spec.rb @@ -159,6 +159,28 @@ def example_with(*args) included_via_tag, excluded_via_tag ]).map(&:description)).to eq([included_via_loc_or_id, included_via_tag].map(&:description)) end + + it "skips examples in external files when included from a #{type} filtered file" do + group = RSpec.describe("group") + + included_via_loc_or_id = group.example("inc via #{type}"); line = __LINE__ + + # instantiate shared example in external file + instance_eval <<-EOS, "a_shared_example.rb", 1 + RSpec.shared_examples_for("a shared example") do + example("inside of a shared example") + end + EOS + + included_via_behaves_like = group.it_behaves_like("a shared example") + test_inside_a_shared_example = included_via_behaves_like.examples.first + + add_filter(:line_number => line, :scoped_id => "1:1") + + expect(prune([ + included_via_loc_or_id, test_inside_a_shared_example + ]).map(&:description)).to eq([included_via_loc_or_id].map(&:description)) + end end describe "location filtering" do From dd37c0eaa1bc9fcb5b8d6c80f896fbf28ce50bfe Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Mon, 18 May 2015 17:54:02 +1000 Subject: [PATCH 208/258] changelog for #1963 [skip ci] --- Changelog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Changelog.md b/Changelog.md index ba7e75a852..cf53533f2e 100644 --- a/Changelog.md +++ b/Changelog.md @@ -60,6 +60,8 @@ Bug Fixes: * Ensure `rspec/autorun` respects configuration defaults. (Jon Rowe, #1933) * Prevent modules overriding example group defined methods when included, prepended or extended by config defined after an example group. (Eugene Kenny, #1935) +* Fix regression which caused shared examples to be mistakenly run when specs + where filtered to a particular location. (Ben Axnick, #1963) ### 3.2.3 / 2015-04-06 [Full Changelog](http://github.com/rspec/rspec-core/compare/v3.2.2...v3.2.3) From 36f01238768e978a2515bede7fcd4095e6244f6e Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Mon, 25 May 2015 18:28:37 +1000 Subject: [PATCH 209/258] Update @samphippen's blog url It seems the apex url isn't working anymore. --- lib/rspec/core/project_initializer/spec/spec_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rspec/core/project_initializer/spec/spec_helper.rb b/lib/rspec/core/project_initializer/spec/spec_helper.rb index dde64fb899..6839d5f9d2 100644 --- a/lib/rspec/core/project_initializer/spec/spec_helper.rb +++ b/lib/rspec/core/project_initializer/spec/spec_helper.rb @@ -58,7 +58,7 @@ # Limits the available syntax to the non-monkey patched syntax that is # recommended. For more details, see: # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax - # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ + # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching config.disable_monkey_patching! From ce26be01c6616b0d3d6790834aa30bfd8b615e7d Mon Sep 17 00:00:00 2001 From: takiy33 Date: Thu, 28 May 2015 06:28:09 +0900 Subject: [PATCH 210/258] Removed deprecated option from .gemspec --- rspec-core.gemspec | 2 -- 1 file changed, 2 deletions(-) diff --git a/rspec-core.gemspec b/rspec-core.gemspec index 98ff30bf8d..670603e80f 100644 --- a/rspec-core.gemspec +++ b/rspec-core.gemspec @@ -13,8 +13,6 @@ Gem::Specification.new do |s| s.summary = "rspec-core-#{RSpec::Core::Version::STRING}" s.description = "BDD for Ruby. RSpec runner and example groups." - s.rubyforge_project = "rspec" - s.files = `git ls-files -- lib/*`.split("\n") s.files += %w[README.md License.txt Changelog.md .yardopts .document] s.test_files = [] From 4fa21d09fcc491be111c8c9c18d85838c53136b2 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 29 May 2015 00:29:15 -0700 Subject: [PATCH 211/258] Refactor: extract ExceptionPresenter Factory. --- .../core/formatters/exception_presenter.rb | 115 +++++++++++++++++ lib/rspec/core/notifications.rb | 122 ++---------------- 2 files changed, 128 insertions(+), 109 deletions(-) diff --git a/lib/rspec/core/formatters/exception_presenter.rb b/lib/rspec/core/formatters/exception_presenter.rb index c3ca4279e1..31bba7d55b 100644 --- a/lib/rspec/core/formatters/exception_presenter.rb +++ b/lib/rspec/core/formatters/exception_presenter.rb @@ -146,6 +146,121 @@ def formatted_message_and_backtrace(colorizer, indentation) formatted end + + # @private + # Configuring the `ExceptionPresenter` with the right set of options to handle + # pending vs failed vs skipped and aggregated (or not) failures is not simple. + # This class takes care of building an appropriate `ExceptionPresenter` for the + # provided example. + class Factory + def build + ExceptionPresenter.new(@exception, @example, options) + end + + private + + def initialize(example) + @example = example + @execution_result = example.execution_result + @exception = if @execution_result.status == :pending + @execution_result.pending_exception + else + @execution_result.exception + end + end + + def options + with_multiple_error_options_as_needed(@exception, pending_options || {}) + end + + def pending_options + if @execution_result.pending_fixed? + { + :description_formatter => Proc.new { "#{@example.full_description} FIXED" }, + :message_color => RSpec.configuration.fixed_color, + :failure_lines => [ + "Expected pending '#{@execution_result.pending_message}' to fail. No Error was raised." + ] + } + elsif @execution_result.status == :pending + { + :message_color => RSpec.configuration.pending_color, + :detail_formatter => PENDING_DETAIL_FORMATTER + } + end + end + + def with_multiple_error_options_as_needed(exception, options) + return options unless multiple_exceptions_not_met_error?(exception) + + options = options.merge( + :failure_lines => [], + :extra_detail_formatter => sub_failure_list_formatter(exception, options[:message_color]), + :detail_formatter => multiple_failure_sumarizer(exception, + options[:detail_formatter], + options[:message_color]) + ) + + options[:description_formatter] &&= Proc.new {} + + return options unless exception.aggregation_metadata[:from_around_hook] + options[:backtrace_formatter] = EmptyBacktraceFormatter + options + end + + def multiple_exceptions_not_met_error?(exception) + return false unless defined?(RSpec::Expectations::MultipleExpectationsNotMetError) + RSpec::Expectations::MultipleExpectationsNotMetError === exception + end + + def multiple_failure_sumarizer(exception, prior_detail_formatter, color) + lambda do |example, colorizer, indentation| + summary = if exception.aggregation_metadata[:from_around_hook] + "Got #{exception.exception_count_description}:" + else + "#{exception.summary}." + end + + summary = colorizer.wrap(summary, color || RSpec.configuration.failure_color) + return summary unless prior_detail_formatter + "#{prior_detail_formatter.call(example, colorizer, indentation)}\n#{indentation}#{summary}" + end + end + + def sub_failure_list_formatter(exception, message_color) + lambda do |failure_number, colorizer, indentation| + exception.all_exceptions.each_with_index.map do |failure, index| + options = with_multiple_error_options_as_needed( + failure, + :description_formatter => :failure_slash_error_line.to_proc, + :indentation => indentation.length, + :message_color => message_color || RSpec.configuration.failure_color, + :skip_shared_group_trace => true + ) + + failure = failure.dup + failure.set_backtrace(failure.backtrace[0..-exception.backtrace.size]) + + presenter = ExceptionPresenter.new(failure, @example, options) + presenter.fully_formatted("#{failure_number}.#{index + 1}", colorizer) + end.join + end + end + + # @private + # Used to prevent a confusing backtrace from showing up from the `aggregate_failures` + # block declared for `:aggregate_failures` metadata. + module EmptyBacktraceFormatter + def self.format_backtrace(*) + [] + end + end + end + + # @private + PENDING_DETAIL_FORMATTER = Proc.new do |example, colorizer| + colorizer.wrap("# #{example.execution_result.pending_message}", :detail) + end end end end diff --git a/lib/rspec/core/notifications.rb b/lib/rspec/core/notifications.rb index f4a3252320..0a23ab1426 100644 --- a/lib/rspec/core/notifications.rb +++ b/lib/rspec/core/notifications.rb @@ -43,110 +43,19 @@ def self.for(example) return SkippedExampleNotification.new(example) if execution_result.example_skipped? return new(example) unless execution_result.status == :pending || execution_result.status == :failed - klass = FailedExampleNotification - exception = execution_result.exception - ex_presenter_options = {} - - if execution_result.pending_fixed? - klass = PendingExampleFixedNotification - ex_presenter_options = { - :description_formatter => Proc.new { "#{example.full_description} FIXED" }, - :message_color => RSpec.configuration.fixed_color, - :failure_lines => ["Expected pending '#{execution_result.pending_message}' to fail. No Error was raised."] - } - elsif execution_result.status == :pending - klass = PendingExampleFailedAsExpectedNotification - exception = example.execution_result.pending_exception - ex_presenter_options = { - :message_color => RSpec.configuration.pending_color, - :detail_formatter => PENDING_DETAIL_FORMATTER - } - end - - if multiple_exceptions_not_met_error?(exception) - ex_presenter_options = exception_presenter_opts_for_multiple_error( - exception, example, ex_presenter_options - ) - end - - ex_presenter = Formatters::ExceptionPresenter.new(exception, example, ex_presenter_options) - klass.new(example, ex_presenter) - end - - def self.exception_presenter_opts_for_multiple_error(exception, example, options) - ex_presenter_options = options.merge( - :failure_lines => [], - :extra_detail_formatter => sub_failure_list_formatter(exception, example, - options[:message_color]), - :detail_formatter => multiple_failure_sumarizer(exception, - options[:detail_formatter], - options[:message_color]) - ) - - ex_presenter_options[:description_formatter] &&= Proc.new {} - - if exception.aggregation_metadata[:from_around_hook] - ex_presenter_options[:backtrace_formatter] = EmptyBacktraceFormatter - end - - ex_presenter_options - end - - # @private - # Used to prevent a confusing backtrace from showing up from the `aggregate_failures` - # block declared for `:aggregate_failures` metadata. - module EmptyBacktraceFormatter - def self.format_backtrace(*) - [] - end - end + klass = if execution_result.pending_fixed? + PendingExampleFixedNotification + elsif execution_result.status == :pending + PendingExampleFailedAsExpectedNotification + else + FailedExampleNotification + end - def self.multiple_exceptions_not_met_error?(exception) - return false unless defined?(RSpec::Expectations::MultipleExpectationsNotMetError) - RSpec::Expectations::MultipleExpectationsNotMetError === exception + exception_presenter = Formatters::ExceptionPresenter::Factory.new(example).build + klass.new(example, exception_presenter) end - def self.multiple_failure_sumarizer(exception, prior_detail_formatter, color) - lambda do |example, colorizer, indentation| - summary = if exception.aggregation_metadata[:from_around_hook] - "Got #{exception.exception_count_description}:" - else - "#{exception.summary}." - end - - summary = colorizer.wrap(summary, color || RSpec.configuration.failure_color) - return summary unless prior_detail_formatter - "#{prior_detail_formatter.call(example, colorizer, indentation)}\n#{indentation}#{summary}" - end - end - - def self.sub_failure_list_formatter(exception, example, message_color) - lambda do |failure_number, colorizer, indentation| - exception.all_exceptions.each_with_index.map do |failure, index| - options = { - :description_formatter => :failure_slash_error_line.to_proc, - :indentation => indentation.length, - :message_color => message_color || RSpec.configuration.failure_color, - :skip_shared_group_trace => true - } - - if multiple_exceptions_not_met_error?(failure) - options = exception_presenter_opts_for_multiple_error(failure, example, options) - end - - failure = failure.dup - failure.set_backtrace(failure.backtrace[0..-exception.backtrace.size]) - - Formatters::ExceptionPresenter.new( - failure, example, options - ).fully_formatted("#{failure_number}.#{index + 1}", colorizer) - end.join - end - end - - private_class_method :new, :multiple_exceptions_not_met_error?, - :multiple_failure_sumarizer, :sub_failure_list_formatter, - :exception_presenter_opts_for_multiple_error + private_class_method :new end # The `ExamplesNotification` represents notifications sent by the reporter @@ -305,11 +214,6 @@ class PendingExampleFixedNotification < FailedExampleNotification; end # @deprecated Use {FailedExampleNotification} instead. class PendingExampleFailedAsExpectedNotification < FailedExampleNotification; end - # @private - PENDING_DETAIL_FORMATTER = Proc.new do |example, colorizer| - colorizer.wrap("# #{example.execution_result.pending_message}", :detail) - end - # The `SkippedExampleNotification` extends `ExampleNotification` with # things useful for specs that are skipped. # @@ -322,9 +226,9 @@ class SkippedExampleNotification < ExampleNotification # RSpec's built-in formatters emit. def fully_formatted(pending_number, colorizer=::RSpec::Core::Formatters::ConsoleCodes) formatted_caller = RSpec.configuration.backtrace_formatter.backtrace_line(example.location) - colorizer.wrap("\n #{pending_number}) #{example.full_description}", :pending) << - "\n " << PENDING_DETAIL_FORMATTER.call(example, colorizer) << "\n" << - colorizer.wrap(" # #{formatted_caller}\n", :detail) + colorizer.wrap("\n #{pending_number}) #{example.full_description}", :pending) << "\n " << + Formatters::ExceptionPresenter::PENDING_DETAIL_FORMATTER.call(example, colorizer) << + "\n" << colorizer.wrap(" # #{formatted_caller}\n", :detail) end end From 59a30e8daef84e13966502420efd517597459d12 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 29 May 2015 01:03:11 -0700 Subject: [PATCH 212/258] Make backtrace truncation much more robust. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is necessary for the changes in rspec/rspec-support#210. That adds an additional stack frame to the parent exception (for the lambda that wraps `raise`) and our truncation wasn’t working properly with it. Really, the old way just happened to work. This is much more robust. --- .../core/formatters/exception_presenter.rb | 29 ++++- .../formatters/exception_presenter_spec.rb | 112 ++++++++++++++++++ 2 files changed, 138 insertions(+), 3 deletions(-) diff --git a/lib/rspec/core/formatters/exception_presenter.rb b/lib/rspec/core/formatters/exception_presenter.rb index 31bba7d55b..bd3bd0fceb 100644 --- a/lib/rspec/core/formatters/exception_presenter.rb +++ b/lib/rspec/core/formatters/exception_presenter.rb @@ -228,6 +228,8 @@ def multiple_failure_sumarizer(exception, prior_detail_formatter, color) end def sub_failure_list_formatter(exception, message_color) + common_backtrace_truncater = CommonBacktraceTruncater.new(exception) + lambda do |failure_number, colorizer, indentation| exception.all_exceptions.each_with_index.map do |failure, index| options = with_multiple_error_options_as_needed( @@ -238,9 +240,7 @@ def sub_failure_list_formatter(exception, message_color) :skip_shared_group_trace => true ) - failure = failure.dup - failure.set_backtrace(failure.backtrace[0..-exception.backtrace.size]) - + failure = common_backtrace_truncater.with_truncated_backtrace(failure) presenter = ExceptionPresenter.new(failure, @example, options) presenter.fully_formatted("#{failure_number}.#{index + 1}", colorizer) end.join @@ -255,6 +255,29 @@ def self.format_backtrace(*) [] end end + + # @private + class CommonBacktraceTruncater + def initialize(parent) + @parent = parent + end + + def with_truncated_backtrace(child) + child_bt = child.backtrace + parent_bt = @parent.backtrace + return child if child_bt.nil? || child_bt.empty? || parent_bt.nil? + + index_before_first_common_frame = -1.downto(-child_bt.size).find do |index| + parent_bt[index] != child_bt[index] + end + + return child if index_before_first_common_frame == -1 + + child = child.dup + child.set_backtrace(child_bt[0..index_before_first_common_frame]) + child + end + end end # @private diff --git a/spec/rspec/core/formatters/exception_presenter_spec.rb b/spec/rspec/core/formatters/exception_presenter_spec.rb index 174e6bb581..f4cbf51d26 100644 --- a/spec/rspec/core/formatters/exception_presenter_spec.rb +++ b/spec/rspec/core/formatters/exception_presenter_spec.rb @@ -194,4 +194,116 @@ def read_failed_line end end end + + RSpec.describe Formatters::ExceptionPresenter::Factory::CommonBacktraceTruncater do + def truncate(parent, child) + described_class.new(parent).with_truncated_backtrace(child) + end + + def exception_with(backtrace) + exception = Exception.new + exception.set_backtrace(backtrace) + exception + end + + it 'returns an exception with the common part truncated' do + parent = exception_with %w[ foo.rb:1 bar.rb:2 car.rb:7 ] + child = exception_with %w[ file_1.rb:3 file_1.rb:9 foo.rb:1 bar.rb:2 car.rb:7 ] + + truncated = truncate(parent, child) + + expect(truncated.backtrace).to eq %w[ file_1.rb:3 file_1.rb:9 ] + end + + it 'ignores excess lines in the top of the parent trace that the child does not have' do + parent = exception_with %w[ foo.rb:1 foo.rb:2 foo.rb:3 bar.rb:2 car.rb:7 ] + child = exception_with %w[ file_1.rb:3 file_1.rb:9 bar.rb:2 car.rb:7 ] + + truncated = truncate(parent, child) + + expect(truncated.backtrace).to eq %w[ file_1.rb:3 file_1.rb:9 ] + end + + it 'does not truncate anything if the parent has excess lines at the bottom of the trace' do + parent = exception_with %w[ foo.rb:1 bar.rb:2 car.rb:7 bazz.rb:9 ] + child = exception_with %w[ file_1.rb:3 file_1.rb:9 foo.rb:1 bar.rb:2 car.rb:7 ] + + truncated = truncate(parent, child) + + expect(truncated.backtrace).to eq %w[ file_1.rb:3 file_1.rb:9 foo.rb:1 bar.rb:2 car.rb:7 ] + end + + it 'does not mutate the provided exception' do + parent = exception_with %w[ foo.rb:1 bar.rb:2 car.rb:7 ] + child = exception_with %w[ file_1.rb:3 file_1.rb:9 foo.rb:1 bar.rb:2 car.rb:7 ] + + expect { truncate(parent, child) }.not_to change(child, :backtrace) + end + + it 'returns an exception with all the same attributes (except backtrace) as the provided one' do + parent = exception_with %w[ foo.rb:1 bar.rb:2 car.rb:7 ] + + my_custom_exception_class = Class.new(StandardError) do + attr_accessor :foo, :bar + end + + child = my_custom_exception_class.new("Some Message") + child.foo = 13 + child.bar = 20 + child.set_backtrace(%w[ foo.rb:1 ]) + + truncated = truncate(parent, child) + + expect(truncated).to have_attributes( + :message => "Some Message", + :foo => 13, + :bar => 20 + ) + end + + it 'handles child exceptions that have a blank array for the backtrace' do + parent = exception_with %w[ foo.rb:1 bar.rb:2 car.rb:7 ] + child = exception_with %w[ ] + + truncated = truncate(parent, child) + + expect(truncated.backtrace).to eq %w[ ] + end + + it 'handles child exceptions that have `nil` for the backtrace' do + parent = exception_with %w[ foo.rb:1 bar.rb:2 car.rb:7 ] + child = Exception.new + + truncated = truncate(parent, child) + + expect(truncated.backtrace).to be_nil + end + + it 'handles parent exceptions that have a blank array for the backtrace' do + parent = exception_with %w[ ] + child = exception_with %w[ foo.rb:1 ] + + truncated = truncate(parent, child) + + expect(truncated.backtrace).to eq %w[ foo.rb:1 ] + end + + it 'handles parent exceptions that have `nil` for the backtrace' do + parent = Exception.new + child = exception_with %w[ foo.rb:1 ] + + truncated = truncate(parent, child) + + expect(truncated.backtrace).to eq %w[ foo.rb:1 ] + end + + it 'returns the original exception object (not a dup) when there is no need to update the backtrace' do + parent = exception_with %w[ bar.rb:1 ] + child = exception_with %w[ foo.rb:1 ] + + truncated = truncate(parent, child) + + expect(truncated).to be child + end + end end From b0c99318b75a8173fc82747a9b5a1102c675afde Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Tue, 2 Jun 2015 12:29:41 +1000 Subject: [PATCH 213/258] changelog for #1980 [skip ci] --- Changelog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Changelog.md b/Changelog.md index cf53533f2e..82dc5fa2e5 100644 --- a/Changelog.md +++ b/Changelog.md @@ -47,6 +47,8 @@ Enhancements: `aggregate_failures` feature to allow multiple failures in an example and list them all, rather than aborting on the first failure. (Myron Marston, #1946) +* When no formatter implements #message add a fallback to prevent those + messages being lost. (Jon Rowe, #1980) Bug Fixes: From a8749e90cec867fc5c1a6ded097bc69101f419f7 Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Tue, 2 Jun 2015 12:25:20 +1000 Subject: [PATCH 214/258] add the message formatter as a fallback when no other formatter implements it fixes #1978 --- lib/rspec/core/formatters.rb | 5 ++ .../core/formatters/message_formatter.rb | 28 ++++++++++ spec/rspec/core/formatters_spec.rb | 54 +++++++++++++------ spec/support/matchers.rb | 1 + 4 files changed, 73 insertions(+), 15 deletions(-) create mode 100644 lib/rspec/core/formatters/message_formatter.rb diff --git a/lib/rspec/core/formatters.rb b/lib/rspec/core/formatters.rb index b27e74f3a7..f2b82ae437 100644 --- a/lib/rspec/core/formatters.rb +++ b/lib/rspec/core/formatters.rb @@ -68,6 +68,7 @@ module RSpec::Core::Formatters autoload :DocumentationFormatter, 'rspec/core/formatters/documentation_formatter' autoload :HtmlFormatter, 'rspec/core/formatters/html_formatter' + autoload :MessageFormatter, 'rspec/core/formatters/message_formatter' autoload :ProgressFormatter, 'rspec/core/formatters/progress_formatter' autoload :ProfileFormatter, 'rspec/core/formatters/profile_formatter' autoload :JsonFormatter, 'rspec/core/formatters/json_formatter' @@ -121,6 +122,10 @@ def setup_default(output_stream, deprecation_stream) add DeprecationFormatter, deprecation_stream, output_stream end + unless existing_formatter_implements?(:message) + add MessageFormatter, output_stream + end + return unless RSpec.configuration.profile_examples? && !existing_formatter_implements?(:dump_profile) add RSpec::Core::Formatters::ProfileFormatter, output_stream diff --git a/lib/rspec/core/formatters/message_formatter.rb b/lib/rspec/core/formatters/message_formatter.rb new file mode 100644 index 0000000000..7d506f50b5 --- /dev/null +++ b/lib/rspec/core/formatters/message_formatter.rb @@ -0,0 +1,28 @@ +module RSpec + module Core + module Formatters + # @api private + # Formatter for providing message output as a fallback when no other + # profiler implements #message + class MessageFormatter + Formatters.register self, :message + + def initialize(output) + @output = output + end + + # @private + attr_reader :output + + # @api public + # + # Used by the reporter to send messages to the output stream. + # + # @param notification [MessageNotification] containing message + def message(notification) + output.puts notification.message + end + end + end + end +end diff --git a/spec/rspec/core/formatters_spec.rb b/spec/rspec/core/formatters_spec.rb index 1cf61feaba..1104a7dc21 100644 --- a/spec/rspec/core/formatters_spec.rb +++ b/spec/rspec/core/formatters_spec.rb @@ -130,28 +130,52 @@ module RSpec::Core::Formatters end end - describe "#setup_default", "with profiling enabled" do + describe "#setup_default" do let(:setup_default) { loader.setup_default output, output } - before do - allow(RSpec.configuration).to receive(:profile_examples?) { true } + context "with a formatter that implements #message" do + it 'doesnt add a fallback formatter' do + allow(reporter).to receive(:registered_listeners).with(:message) { [:json] } + setup_default + expect(loader.formatters).to exclude( + an_instance_of ::RSpec::Core::Formatters::MessageFormatter + ) + end end - context "without an existing profile formatter" do - it "will add the profile formatter" do - allow(reporter).to receive(:registered_listeners).with(:dump_profile) { [] } - setup_default - expect(loader.formatters.last).to be_a ::RSpec::Core::Formatters::ProfileFormatter + context "without a formatter that implements #message" do + it 'adds a fallback for message output' do + allow(reporter).to receive(:registered_listeners).with(:message) { [] } + expect { + setup_default + }.to change { loader.formatters }. + from( excluding an_instance_of ::RSpec::Core::Formatters::MessageFormatter ). + to( including an_instance_of ::RSpec::Core::Formatters::MessageFormatter ) end end - context "when a formatter that implement #dump_profile is added" do - it "wont add the profile formatter" do - allow(reporter).to receive(:registered_listeners).with(:dump_profile) { [:json] } - setup_default - expect( - loader.formatters.map(&:class) - ).to_not include ::RSpec::Core::Formatters::ProfileFormatter + context "with profiling enabled" do + before do + allow(reporter).to receive(:registered_listeners).with(:message) { [:json] } + allow(RSpec.configuration).to receive(:profile_examples?) { true } + end + + context "without an existing profile formatter" do + it "will add the profile formatter" do + allow(reporter).to receive(:registered_listeners).with(:dump_profile) { [] } + setup_default + expect(loader.formatters.last).to be_a ::RSpec::Core::Formatters::ProfileFormatter + end + end + + context "when a formatter that implement #dump_profile is added" do + it "wont add the profile formatter" do + allow(reporter).to receive(:registered_listeners).with(:dump_profile) { [:json] } + setup_default + expect( + loader.formatters.map(&:class) + ).to_not include ::RSpec::Core::Formatters::ProfileFormatter + end end end end diff --git a/spec/support/matchers.rb b/spec/support/matchers.rb index da25778741..fdf5d14480 100644 --- a/spec/support/matchers.rb +++ b/spec/support/matchers.rb @@ -121,4 +121,5 @@ def failure_reason(example) RSpec::Matchers.define_negated_matcher :avoid_outputting, :output RSpec::Matchers.define_negated_matcher :exclude, :include +RSpec::Matchers.define_negated_matcher :excluding, :include RSpec::Matchers.define_negated_matcher :avoid_changing, :change From fe7cb2903ea93a654e9af05f981dbdf824d4c8e2 Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Tue, 2 Jun 2015 19:41:50 +1000 Subject: [PATCH 215/258] rename MessageFormatter to FallbackMessageFormatter --- lib/rspec/core/formatters.rb | 16 ++++++++-------- ...ormatter.rb => fallback_message_formatter.rb} | 2 +- spec/rspec/core/formatters_spec.rb | 6 +++--- 3 files changed, 12 insertions(+), 12 deletions(-) rename lib/rspec/core/formatters/{message_formatter.rb => fallback_message_formatter.rb} (94%) diff --git a/lib/rspec/core/formatters.rb b/lib/rspec/core/formatters.rb index f2b82ae437..a00727caa6 100644 --- a/lib/rspec/core/formatters.rb +++ b/lib/rspec/core/formatters.rb @@ -66,13 +66,13 @@ # @see RSpec::Core::Formatters::BaseTextFormatter # @see RSpec::Core::Reporter module RSpec::Core::Formatters - autoload :DocumentationFormatter, 'rspec/core/formatters/documentation_formatter' - autoload :HtmlFormatter, 'rspec/core/formatters/html_formatter' - autoload :MessageFormatter, 'rspec/core/formatters/message_formatter' - 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' + autoload :DocumentationFormatter, 'rspec/core/formatters/documentation_formatter' + autoload :HtmlFormatter, 'rspec/core/formatters/html_formatter' + autoload :FallbackMessageFormatter, 'rspec/core/formatters/fallback_message_formatter' + 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 @@ -123,7 +123,7 @@ def setup_default(output_stream, deprecation_stream) end unless existing_formatter_implements?(:message) - add MessageFormatter, output_stream + add FallbackMessageFormatter, output_stream end return unless RSpec.configuration.profile_examples? && !existing_formatter_implements?(:dump_profile) diff --git a/lib/rspec/core/formatters/message_formatter.rb b/lib/rspec/core/formatters/fallback_message_formatter.rb similarity index 94% rename from lib/rspec/core/formatters/message_formatter.rb rename to lib/rspec/core/formatters/fallback_message_formatter.rb index 7d506f50b5..db4423ff18 100644 --- a/lib/rspec/core/formatters/message_formatter.rb +++ b/lib/rspec/core/formatters/fallback_message_formatter.rb @@ -4,7 +4,7 @@ module Formatters # @api private # Formatter for providing message output as a fallback when no other # profiler implements #message - class MessageFormatter + class FallbackMessageFormatter Formatters.register self, :message def initialize(output) diff --git a/spec/rspec/core/formatters_spec.rb b/spec/rspec/core/formatters_spec.rb index 1104a7dc21..4443bc629e 100644 --- a/spec/rspec/core/formatters_spec.rb +++ b/spec/rspec/core/formatters_spec.rb @@ -138,7 +138,7 @@ module RSpec::Core::Formatters allow(reporter).to receive(:registered_listeners).with(:message) { [:json] } setup_default expect(loader.formatters).to exclude( - an_instance_of ::RSpec::Core::Formatters::MessageFormatter + an_instance_of ::RSpec::Core::Formatters::FallbackMessageFormatter ) end end @@ -149,8 +149,8 @@ module RSpec::Core::Formatters expect { setup_default }.to change { loader.formatters }. - from( excluding an_instance_of ::RSpec::Core::Formatters::MessageFormatter ). - to( including an_instance_of ::RSpec::Core::Formatters::MessageFormatter ) + from( excluding an_instance_of ::RSpec::Core::Formatters::FallbackMessageFormatter ). + to( including an_instance_of ::RSpec::Core::Formatters::FallbackMessageFormatter ) end end From 5e9db38f0c3224627f73db6d4e382a2cdd817b5e Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Tue, 2 Jun 2015 19:47:08 +1000 Subject: [PATCH 216/258] add spec for FallbackMessageFormatter --- .../fallback_message_formatter_spec.rb | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 spec/rspec/core/formatters/fallback_message_formatter_spec.rb diff --git a/spec/rspec/core/formatters/fallback_message_formatter_spec.rb b/spec/rspec/core/formatters/fallback_message_formatter_spec.rb new file mode 100644 index 0000000000..0ba66325f7 --- /dev/null +++ b/spec/rspec/core/formatters/fallback_message_formatter_spec.rb @@ -0,0 +1,18 @@ +require 'rspec/core/reporter' +require 'rspec/core/formatters/fallback_message_formatter' + +module RSpec::Core::Formatters + RSpec.describe FallbackMessageFormatter do + include FormatterSupport + + describe "#message" do + it 'writes the message to the output' do + expect { + send_notification :message, message_notification('Custom Message') + }.to change { formatter_output.string }. + from(excluding 'Custom Message'). + to(including 'Custom Message') + end + end + end +end From 60b2dc7811f7d3717f47e4143402be1db56562e1 Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Tue, 2 Jun 2015 20:03:53 +1000 Subject: [PATCH 217/258] refactor ProfileFormatter specs in formatter loader --- spec/rspec/core/formatters_spec.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/spec/rspec/core/formatters_spec.rb b/spec/rspec/core/formatters_spec.rb index 4443bc629e..cccbd65fb1 100644 --- a/spec/rspec/core/formatters_spec.rb +++ b/spec/rspec/core/formatters_spec.rb @@ -163,8 +163,11 @@ module RSpec::Core::Formatters context "without an existing profile formatter" do it "will add the profile formatter" do allow(reporter).to receive(:registered_listeners).with(:dump_profile) { [] } - setup_default - expect(loader.formatters.last).to be_a ::RSpec::Core::Formatters::ProfileFormatter + expect { + setup_default + }.to change { loader.formatters }. + from( excluding an_instance_of ::RSpec::Core::Formatters::ProfileFormatter ). + to( including an_instance_of ::RSpec::Core::Formatters::ProfileFormatter ) end end From c7cf68859a6a399c41dd25d2c31ad4b75f8fa494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Laliberte=CC=81?= Date: Tue, 21 Apr 2015 09:25:17 -0400 Subject: [PATCH 218/258] Refactor group profiling to include before hook timings --- features/configuration/profile.feature | 22 ++++++++++++++ lib/rspec/core/formatters/json_formatter.rb | 24 +++++++++++++-- .../core/formatters/profile_formatter.rb | 30 ++++++++++++++++--- lib/rspec/core/notifications.rb | 20 ++----------- .../core/formatters/json_formatter_spec.rb | 6 ++-- 5 files changed, 76 insertions(+), 26 deletions(-) diff --git a/features/configuration/profile.feature b/features/configuration/profile.feature index 402d79d731..47d0ec7d7b 100644 --- a/features/configuration/profile.feature +++ b/features/configuration/profile.feature @@ -218,3 +218,25 @@ Feature: Profile examples When I run `rspec spec --fail-fast --profile` Then the output should not contain "Top 2 slowest examples" And the output should not contain "example 1" + + Scenario: Using `--profile` with slow before hooks includes hook execution time + Given a file named "spec/example_spec.rb" with: + """ruby + RSpec.describe "slow before context hook" do + before(:context) do + sleep 0.2 + end + it "example" do + expect(10).to eq(10) + end + end + + RSpec.describe "slow example" do + it "slow example" do + sleep 0.1 + expect(10).to eq(10) + end + end + """ + When I run `rspec spec --profile 1` + Then the output should contain "slow before context hook" diff --git a/lib/rspec/core/formatters/json_formatter.rb b/lib/rspec/core/formatters/json_formatter.rb index 9520387222..1fbccf64c9 100644 --- a/lib/rspec/core/formatters/json_formatter.rb +++ b/lib/rspec/core/formatters/json_formatter.rb @@ -6,17 +6,36 @@ module Core module Formatters # @private class JsonFormatter < BaseFormatter - Formatters.register self, :message, :dump_summary, :dump_profile, :stop, :close + Formatters.register self, :message, :dump_summary, :dump_profile, :stop, :close, + :example_group_started, :example_group_finished, + :example_started attr_reader :output_hash def initialize(output) super + @example_groups= {} #todo rename, maybe example_groups_data ... @output_hash = { :version => RSpec::Core::Version::STRING } end + #todo remove duplication with lib/rspec/core/formatters/profile_formatter.rb line 16 + def example_group_started(notification) + @example_groups[notification.group.id] = Hash.new(0) + @example_groups[notification.group.id][:start] = Time.now + @example_groups[notification.group.id][:description] = notification.group.top_level_description + end + + def example_group_finished(notification) + @example_groups[notification.group.id][:total_time] = Time.now - @example_groups[notification.group.id][:start] + end + + def example_started(notification) + group = notification.example.example_group.parent_groups.last.id + @example_groups[group][:count] += 1 + end + def message(notification) (@output_hash[:messages] ||= []) << notification.message end @@ -72,8 +91,9 @@ def dump_profile_slowest_examples(profile) # @api private def dump_profile_slowest_example_groups(profile) + slowest_groups = profile.calculate_slowest_groups(@example_groups) @output_hash[:profile] ||= {} - @output_hash[:profile][:groups] = profile.slowest_groups.map do |loc, hash| + @output_hash[:profile][:groups] = slowest_groups.map do |loc, hash| hash.update(:location => loc) end end diff --git a/lib/rspec/core/formatters/profile_formatter.rb b/lib/rspec/core/formatters/profile_formatter.rb index 4b95d9386f..7a6c158418 100644 --- a/lib/rspec/core/formatters/profile_formatter.rb +++ b/lib/rspec/core/formatters/profile_formatter.rb @@ -6,15 +6,36 @@ module Formatters # @api private # Formatter for providing profile output. class ProfileFormatter - Formatters.register self, :dump_profile + Formatters.register self, :dump_profile, :example_group_started, :example_group_finished, :example_started def initialize(output) + @example_groups = {} #todo rename, maybe groups_data, groups_information or profile_information @output = output end # @private attr_reader :output + # @private + def example_group_started(notification) + @example_groups[notification.group.id] = Hash.new(0) + @example_groups[notification.group.id][:start] = Time.now + @example_groups[notification.group.id][:description] = notification.group.top_level_description + end + + # @private + def example_group_finished(notification) + @example_groups[notification.group.id][:total_time] = Time.now - @example_groups[notification.group.id][:start] + end + + # @private + def example_started(notification) + #todo: maybe move example_group.parent_groups.last to an example or notification method like example.last_anscestor_group + group = notification.example.example_group.parent_groups.last.id + @example_groups[group][:count] += 1 + end + + # @api public # # This method is invoked after the dumping the summary if profiling is @@ -42,10 +63,11 @@ def dump_profile_slowest_examples(profile) end def dump_profile_slowest_example_groups(profile) - return if profile.slowest_groups.empty? + slowest_groups = profile.calculate_slowest_groups(@example_groups) + return if slowest_groups.empty? - @output.puts "\nTop #{profile.slowest_groups.size} slowest example groups:" - profile.slowest_groups.each do |loc, hash| + @output.puts "\nTop #{slowest_groups.size} slowest example groups:" + slowest_groups.each do |loc, hash| average = "#{bold(Helpers.format_seconds(hash[:average]))} #{bold("seconds")} average" total = "#{Helpers.format_seconds(hash[:total_time])} seconds" count = Helpers.pluralize(hash[:count], "example") diff --git a/lib/rspec/core/notifications.rb b/lib/rspec/core/notifications.rb index 0a23ab1426..745bb3b269 100644 --- a/lib/rspec/core/notifications.rb +++ b/lib/rspec/core/notifications.rb @@ -428,24 +428,8 @@ def percentage end # @return [Array] the slowest example groups - def slowest_groups - @slowest_groups ||= calculate_slowest_groups - end - - private - - def calculate_slowest_groups - example_groups = {} - - examples.each do |example| - location = example.example_group.parent_groups.last.metadata[:location] - - location_hash = example_groups[location] ||= Hash.new(0) - location_hash[:total_time] += example.execution_result.run_time - location_hash[:count] += 1 - next if location_hash.key?(:description) - location_hash[:description] = example.example_group.top_level_description - end + # todo rename this method or move it's code to the json_formater.rb and profile.formater.rb + def calculate_slowest_groups(example_groups) # stop if we've only one example group return {} if example_groups.keys.length <= 1 diff --git a/spec/rspec/core/formatters/json_formatter_spec.rb b/spec/rspec/core/formatters/json_formatter_spec.rb index d02e055ef4..523ebbe8c0 100644 --- a/spec/rspec/core/formatters/json_formatter_spec.rb +++ b/spec/rspec/core/formatters/json_formatter_spec.rb @@ -183,10 +183,12 @@ def profile *groups end it "provides information" do - expect(formatter.output_hash[:profile][:groups].first.keys).to match_array([:total_time, :count, :description, :average, :location]) + expect(formatter.output_hash[:profile][:groups].first.keys).to match_array([:total_time, :count, :description, :average, :location, :start]) end - it "ranks the example groups by average time" do + #todo the group profiling dont use the example clock anymore + #I need to adapt this test + xit "ranks the example groups by average time" do expect(formatter.output_hash[:profile][:groups].first[:description]).to eq("slow group") end end From a0dc34b1a150811e74420c2ce55d3d747814bef4 Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Wed, 27 May 2015 11:05:34 +1000 Subject: [PATCH 219/258] example formatter support to allow specifying group --- spec/support/formatter_support.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/support/formatter_support.rb b/spec/support/formatter_support.rb index e692ad9c12..bf7ca824a1 100644 --- a/spec/support/formatter_support.rb +++ b/spec/support/formatter_support.rb @@ -249,8 +249,8 @@ def example_notification(specific_example = new_example) ::RSpec::Core::Notifications::ExampleNotification.for specific_example end - def group_notification - ::RSpec::Core::Notifications::GroupNotification.new group + def group_notification group_to_notify = group + ::RSpec::Core::Notifications::GroupNotification.new group_to_notify end def message_notification(message) From c29fac9d1444bb5dfcf060afa9b22c10951af315 Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Wed, 27 May 2015 10:49:45 +1000 Subject: [PATCH 220/258] add new profiler class --- lib/rspec/core.rb | 1 + lib/rspec/core/configuration.rb | 12 +++++ lib/rspec/core/profiler.rb | 43 ++++++++++++++++++ lib/rspec/core/reporter.rb | 1 + spec/rspec/core/profiler_spec.rb | 77 ++++++++++++++++++++++++++++++++ 5 files changed, 134 insertions(+) create mode 100644 lib/rspec/core/profiler.rb create mode 100644 spec/rspec/core/profiler_spec.rb diff --git a/lib/rspec/core.rb b/lib/rspec/core.rb index 1c3b654e10..ccb424c1ff 100644 --- a/lib/rspec/core.rb +++ b/lib/rspec/core.rb @@ -137,6 +137,7 @@ def self.world # Namespace for the rspec-core code. module Core autoload :ExampleStatusPersister, "rspec/core/example_status_persister" + autoload :Profiler, "rspec/core/profiler" # @private # This avoids issues with reporting time caused by examples that diff --git a/lib/rspec/core/configuration.rb b/lib/rspec/core/configuration.rb index 96584ea514..678c932412 100644 --- a/lib/rspec/core/configuration.rb +++ b/lib/rspec/core/configuration.rb @@ -827,6 +827,18 @@ def profile_examples end end + # @private + def profiler + @profiler ||= + begin + if value_for(:profile_examples) { @profile_examples } + Profiler.new + else + NoProfiler + end + end + end + # @private def files_or_directories_to_run=(*files) files = files.flatten diff --git a/lib/rspec/core/profiler.rb b/lib/rspec/core/profiler.rb new file mode 100644 index 0000000000..0678040a93 --- /dev/null +++ b/lib/rspec/core/profiler.rb @@ -0,0 +1,43 @@ +module RSpec + module Core + # @private + class NoProfiler + def self.notifications + [] + end + + def self.example_groups + {} + end + end + + # @private + class Profiler + NOTIFICATIONS = [:example_group_started, :example_group_finished, :example_started] + + def initialize + @example_groups = Hash.new { |h, k| h[k] = { :count => 0 } } + end + + attr_reader :example_groups + + def notifications + NOTIFICATIONS + end + + def example_group_started(notification) + @example_groups[notification.group][:start] = Time.now + @example_groups[notification.group][:description] = notification.group.top_level_description + end + + def example_group_finished(notification) + @example_groups[notification.group][:total_time] = Time.now - @example_groups[notification.group][:start] + end + + def example_started(notification) + group = notification.example.example_group.parent_groups.last + @example_groups[group][:count] += 1 + end + end + end +end diff --git a/lib/rspec/core/reporter.rb b/lib/rspec/core/reporter.rb index fa08618132..4759f93f4d 100644 --- a/lib/rspec/core/reporter.rb +++ b/lib/rspec/core/reporter.rb @@ -18,6 +18,7 @@ def initialize(configuration) @failed_examples = [] @pending_examples = [] @duration = @start = @load_time = nil + register_listener @configuration.profiler, *@configuration.profiler.notifications end # @private diff --git a/spec/rspec/core/profiler_spec.rb b/spec/rspec/core/profiler_spec.rb new file mode 100644 index 0000000000..87e73a0eb1 --- /dev/null +++ b/spec/rspec/core/profiler_spec.rb @@ -0,0 +1,77 @@ +require 'rspec/core/profiler' + +RSpec.describe 'RSpec::Core::NoProfiler' do + let(:no_profiler) { RSpec::Core::NoProfiler } + + it 'has an empty hash of example_groups' do + expect(no_profiler.example_groups).to be_empty.and be_a Hash + end +end + +RSpec.describe 'RSpec::Core::Profiler' do + let(:profiler) { RSpec::Core::Profiler.new } + + it 'starts with an empty hash of example_groups' do + expect(profiler.example_groups).to be_empty.and be_a Hash + end + + context 'when hooked into the reporter' do + include FormatterSupport + + let(:description ) { "My Group" } + let(:now) { ::Time.utc(2015, 6, 2) } + + before do + allow(::RSpec::Core::Time).to receive(:now) { now } + end + + def group + @group ||= + begin + group = super + allow(group).to receive(:top_level_description) { description } + group + end + end + + describe '#example_group_started' do + it 'records example groups start time and description via id' do + expect { + profiler.example_group_started group_notification group + }.to change { profiler.example_groups[group] }.to include( + :start => now, :description => description + ) + end + end + + describe '#example_group_finished' do + before do + profiler.example_group_started group_notification group + allow(::RSpec::Core::Time).to receive(:now) { now + 1 } + end + + it 'records example groups total time and description via id' do + expect { + profiler.example_group_finished group_notification group + }.to change { profiler.example_groups[group] }.to include( + :total_time => 1.0 + ) + end + end + + describe '#example_started' do + let(:example) { new_example } + before do + allow(example).to receive(:example_group) { group } + allow(group).to receive(:parent_groups) { [group] } + profiler.example_group_started group_notification group + end + + it 'increments the count of examples for its parent group' do + expect { + profiler.example_started example_notification example + }.to change { profiler.example_groups[group][:count] }.by 1 + end + end + end +end From cb0b34ef25267b4d68da458716ce2cff7ef67506 Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Wed, 27 May 2015 11:23:15 +1000 Subject: [PATCH 221/258] refactor formatters to delegate profiling to the profiler --- lib/rspec/core/formatters/json_formatter.rb | 24 ++------------- .../core/formatters/profile_formatter.rb | 30 +++---------------- .../core/formatters/json_formatter_spec.rb | 4 +-- 3 files changed, 7 insertions(+), 51 deletions(-) diff --git a/lib/rspec/core/formatters/json_formatter.rb b/lib/rspec/core/formatters/json_formatter.rb index 1fbccf64c9..9520387222 100644 --- a/lib/rspec/core/formatters/json_formatter.rb +++ b/lib/rspec/core/formatters/json_formatter.rb @@ -6,36 +6,17 @@ module Core module Formatters # @private class JsonFormatter < BaseFormatter - Formatters.register self, :message, :dump_summary, :dump_profile, :stop, :close, - :example_group_started, :example_group_finished, - :example_started + Formatters.register self, :message, :dump_summary, :dump_profile, :stop, :close attr_reader :output_hash def initialize(output) super - @example_groups= {} #todo rename, maybe example_groups_data ... @output_hash = { :version => RSpec::Core::Version::STRING } end - #todo remove duplication with lib/rspec/core/formatters/profile_formatter.rb line 16 - def example_group_started(notification) - @example_groups[notification.group.id] = Hash.new(0) - @example_groups[notification.group.id][:start] = Time.now - @example_groups[notification.group.id][:description] = notification.group.top_level_description - end - - def example_group_finished(notification) - @example_groups[notification.group.id][:total_time] = Time.now - @example_groups[notification.group.id][:start] - end - - def example_started(notification) - group = notification.example.example_group.parent_groups.last.id - @example_groups[group][:count] += 1 - end - def message(notification) (@output_hash[:messages] ||= []) << notification.message end @@ -91,9 +72,8 @@ def dump_profile_slowest_examples(profile) # @api private def dump_profile_slowest_example_groups(profile) - slowest_groups = profile.calculate_slowest_groups(@example_groups) @output_hash[:profile] ||= {} - @output_hash[:profile][:groups] = slowest_groups.map do |loc, hash| + @output_hash[:profile][:groups] = profile.slowest_groups.map do |loc, hash| hash.update(:location => loc) end end diff --git a/lib/rspec/core/formatters/profile_formatter.rb b/lib/rspec/core/formatters/profile_formatter.rb index 7a6c158418..4b95d9386f 100644 --- a/lib/rspec/core/formatters/profile_formatter.rb +++ b/lib/rspec/core/formatters/profile_formatter.rb @@ -6,36 +6,15 @@ module Formatters # @api private # Formatter for providing profile output. class ProfileFormatter - Formatters.register self, :dump_profile, :example_group_started, :example_group_finished, :example_started + Formatters.register self, :dump_profile def initialize(output) - @example_groups = {} #todo rename, maybe groups_data, groups_information or profile_information @output = output end # @private attr_reader :output - # @private - def example_group_started(notification) - @example_groups[notification.group.id] = Hash.new(0) - @example_groups[notification.group.id][:start] = Time.now - @example_groups[notification.group.id][:description] = notification.group.top_level_description - end - - # @private - def example_group_finished(notification) - @example_groups[notification.group.id][:total_time] = Time.now - @example_groups[notification.group.id][:start] - end - - # @private - def example_started(notification) - #todo: maybe move example_group.parent_groups.last to an example or notification method like example.last_anscestor_group - group = notification.example.example_group.parent_groups.last.id - @example_groups[group][:count] += 1 - end - - # @api public # # This method is invoked after the dumping the summary if profiling is @@ -63,11 +42,10 @@ def dump_profile_slowest_examples(profile) end def dump_profile_slowest_example_groups(profile) - slowest_groups = profile.calculate_slowest_groups(@example_groups) - return if slowest_groups.empty? + return if profile.slowest_groups.empty? - @output.puts "\nTop #{slowest_groups.size} slowest example groups:" - slowest_groups.each do |loc, hash| + @output.puts "\nTop #{profile.slowest_groups.size} slowest example groups:" + profile.slowest_groups.each do |loc, hash| average = "#{bold(Helpers.format_seconds(hash[:average]))} #{bold("seconds")} average" total = "#{Helpers.format_seconds(hash[:total_time])} seconds" count = Helpers.pluralize(hash[:count], "example") diff --git a/spec/rspec/core/formatters/json_formatter_spec.rb b/spec/rspec/core/formatters/json_formatter_spec.rb index 523ebbe8c0..f60b3bf818 100644 --- a/spec/rspec/core/formatters/json_formatter_spec.rb +++ b/spec/rspec/core/formatters/json_formatter_spec.rb @@ -186,9 +186,7 @@ def profile *groups expect(formatter.output_hash[:profile][:groups].first.keys).to match_array([:total_time, :count, :description, :average, :location, :start]) end - #todo the group profiling dont use the example clock anymore - #I need to adapt this test - xit "ranks the example groups by average time" do + it "ranks the example groups by average time" do expect(formatter.output_hash[:profile][:groups].first[:description]).to eq("slow group") end end From 6acd72cd2cedea73fd357ec28a2c877d18110834 Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Wed, 27 May 2015 11:24:00 +1000 Subject: [PATCH 222/258] setup profile notification to delegate slowest_examples groups to the profiler --- lib/rspec/core/formatters/json_formatter.rb | 3 +-- lib/rspec/core/notifications.rb | 13 ++++++++++--- lib/rspec/core/reporter.rb | 3 ++- spec/rspec/core/formatters/json_formatter_spec.rb | 2 +- .../rspec/core/formatters/profile_formatter_spec.rb | 1 + spec/support/formatter_support.rb | 7 ++++++- 6 files changed, 21 insertions(+), 8 deletions(-) diff --git a/lib/rspec/core/formatters/json_formatter.rb b/lib/rspec/core/formatters/json_formatter.rb index 9520387222..510a80c648 100644 --- a/lib/rspec/core/formatters/json_formatter.rb +++ b/lib/rspec/core/formatters/json_formatter.rb @@ -60,8 +60,7 @@ def dump_profile(profile) # @api private def dump_profile_slowest_examples(profile) @output_hash[:profile] = {} - sorted_examples = profile.slowest_examples - @output_hash[:profile][:examples] = sorted_examples.map do |example| + @output_hash[:profile][:examples] = profile.slowest_examples.map do |example| format_example(example).tap do |hash| hash[:run_time] = example.execution_result.run_time end diff --git a/lib/rspec/core/notifications.rb b/lib/rspec/core/notifications.rb index 745bb3b269..2018a505ae 100644 --- a/lib/rspec/core/notifications.rb +++ b/lib/rspec/core/notifications.rb @@ -400,7 +400,8 @@ def duplicate_rerun_locations # @attr duration [Float] the time taken (in seconds) to run the suite # @attr examples [Array] the examples run # @attr number_of_examples [Fixnum] the number of examples to profile - ProfileNotification = Struct.new(:duration, :examples, :number_of_examples) + # @attr profiler [RSpec::Core::Profiler] the profiler used to capture examples + ProfileNotification = Struct.new(:duration, :examples, :number_of_examples, :profiler) class ProfileNotification # @return [Array] the slowest examples def slowest_examples @@ -428,8 +429,14 @@ def percentage end # @return [Array] the slowest example groups - # todo rename this method or move it's code to the json_formater.rb and profile.formater.rb - def calculate_slowest_groups(example_groups) + def slowest_groups + @slowest_groups ||= calculate_slowest_groups + end + + private + + def calculate_slowest_groups + example_groups = profiler.example_groups # stop if we've only one example group return {} if example_groups.keys.length <= 1 diff --git a/lib/rspec/core/reporter.rb b/lib/rspec/core/reporter.rb index 4759f93f4d..c544e84e23 100644 --- a/lib/rspec/core/reporter.rb +++ b/lib/rspec/core/reporter.rb @@ -150,7 +150,8 @@ def finish notify :deprecation_summary, Notifications::NullNotification unless mute_profile_output? notify :dump_profile, Notifications::ProfileNotification.new(@duration, @examples, - @configuration.profile_examples) + @configuration.profile_examples, + @configuration.profiler) end notify :dump_summary, Notifications::SummaryNotification.new(@duration, @examples, @failed_examples, @pending_examples, @load_time) diff --git a/spec/rspec/core/formatters/json_formatter_spec.rb b/spec/rspec/core/formatters/json_formatter_spec.rb index f60b3bf818..034305be3c 100644 --- a/spec/rspec/core/formatters/json_formatter_spec.rb +++ b/spec/rspec/core/formatters/json_formatter_spec.rb @@ -133,8 +133,8 @@ def profile *groups end before do + setup_profiler formatter - config.profile_examples = 10 end context "with one example group" do diff --git a/spec/rspec/core/formatters/profile_formatter_spec.rb b/spec/rspec/core/formatters/profile_formatter_spec.rb index 48cf754bbf..0d0527b00d 100644 --- a/spec/rspec/core/formatters/profile_formatter_spec.rb +++ b/spec/rspec/core/formatters/profile_formatter_spec.rb @@ -4,6 +4,7 @@ include FormatterSupport def profile *groups + setup_profiler groups.each { |group| group.run(reporter) } examples = groups.map(&:examples).flatten total_time = examples.map { |e| e.execution_result.run_time }.inject(&:+) diff --git a/spec/support/formatter_support.rb b/spec/support/formatter_support.rb index bf7ca824a1..35b3fad3c7 100644 --- a/spec/support/formatter_support.rb +++ b/spec/support/formatter_support.rb @@ -185,6 +185,11 @@ def setup_reporter(*streams) @reporter = config.reporter end + def setup_profiler + config.profile_examples = true + config.profiler + end + def formatter_output @formatter_output ||= StringIO.new end @@ -274,7 +279,7 @@ def summary_notification(duration, examples, failed, pending, time) end def profile_notification(duration, examples, number) - ::RSpec::Core::Notifications::ProfileNotification.new duration, examples, number + ::RSpec::Core::Notifications::ProfileNotification.new duration, examples, number, config.profiler end end From 1e175e3a438df617bd88ea374b6db02316435951 Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Fri, 29 May 2015 13:15:32 +1000 Subject: [PATCH 223/258] perform profiling all the time to allow dynamically changing profile_examples option --- lib/rspec/core/configuration.rb | 12 ------------ lib/rspec/core/profiler.rb | 15 --------------- lib/rspec/core/reporter.rb | 5 +++-- spec/rspec/core/profiler_spec.rb | 8 -------- spec/support/formatter_support.rb | 8 +++++--- 5 files changed, 8 insertions(+), 40 deletions(-) diff --git a/lib/rspec/core/configuration.rb b/lib/rspec/core/configuration.rb index 678c932412..96584ea514 100644 --- a/lib/rspec/core/configuration.rb +++ b/lib/rspec/core/configuration.rb @@ -827,18 +827,6 @@ def profile_examples end end - # @private - def profiler - @profiler ||= - begin - if value_for(:profile_examples) { @profile_examples } - Profiler.new - else - NoProfiler - end - end - end - # @private def files_or_directories_to_run=(*files) files = files.flatten diff --git a/lib/rspec/core/profiler.rb b/lib/rspec/core/profiler.rb index 0678040a93..ff5373be0d 100644 --- a/lib/rspec/core/profiler.rb +++ b/lib/rspec/core/profiler.rb @@ -1,16 +1,5 @@ module RSpec module Core - # @private - class NoProfiler - def self.notifications - [] - end - - def self.example_groups - {} - end - end - # @private class Profiler NOTIFICATIONS = [:example_group_started, :example_group_finished, :example_started] @@ -21,10 +10,6 @@ def initialize attr_reader :example_groups - def notifications - NOTIFICATIONS - end - def example_group_started(notification) @example_groups[notification.group][:start] = Time.now @example_groups[notification.group][:description] = notification.group.top_level_description diff --git a/lib/rspec/core/reporter.rb b/lib/rspec/core/reporter.rb index c544e84e23..f0009d94bb 100644 --- a/lib/rspec/core/reporter.rb +++ b/lib/rspec/core/reporter.rb @@ -18,7 +18,8 @@ def initialize(configuration) @failed_examples = [] @pending_examples = [] @duration = @start = @load_time = nil - register_listener @configuration.profiler, *@configuration.profiler.notifications + @profiler = Profiler.new + register_listener @profiler, *Profiler::NOTIFICATIONS end # @private @@ -151,7 +152,7 @@ def finish unless mute_profile_output? notify :dump_profile, Notifications::ProfileNotification.new(@duration, @examples, @configuration.profile_examples, - @configuration.profiler) + @profiler) end notify :dump_summary, Notifications::SummaryNotification.new(@duration, @examples, @failed_examples, @pending_examples, @load_time) diff --git a/spec/rspec/core/profiler_spec.rb b/spec/rspec/core/profiler_spec.rb index 87e73a0eb1..27cdb4179c 100644 --- a/spec/rspec/core/profiler_spec.rb +++ b/spec/rspec/core/profiler_spec.rb @@ -1,13 +1,5 @@ require 'rspec/core/profiler' -RSpec.describe 'RSpec::Core::NoProfiler' do - let(:no_profiler) { RSpec::Core::NoProfiler } - - it 'has an empty hash of example_groups' do - expect(no_profiler.example_groups).to be_empty.and be_a Hash - end -end - RSpec.describe 'RSpec::Core::Profiler' do let(:profiler) { RSpec::Core::Profiler.new } diff --git a/spec/support/formatter_support.rb b/spec/support/formatter_support.rb index 35b3fad3c7..77e9bb7697 100644 --- a/spec/support/formatter_support.rb +++ b/spec/support/formatter_support.rb @@ -187,7 +187,6 @@ def setup_reporter(*streams) def setup_profiler config.profile_examples = true - config.profiler end def formatter_output @@ -225,6 +224,7 @@ def new_example(metadata = {}) instance_double(RSpec::Core::Example, :description => "Example", :full_description => "Example", + :example_group => group, :execution_result => result, :location => "", :location_rerun_argument => "", @@ -239,7 +239,9 @@ def examples(n) end def group - class_double "RSpec::Core::ExampleGroup", :description => "Group" + group = class_double "RSpec::Core::ExampleGroup", :description => "Group" + allow(group).to receive(:parent_groups) { [group] } + group end def start_notification(count) @@ -279,7 +281,7 @@ def summary_notification(duration, examples, failed, pending, time) end def profile_notification(duration, examples, number) - ::RSpec::Core::Notifications::ProfileNotification.new duration, examples, number, config.profiler + ::RSpec::Core::Notifications::ProfileNotification.new duration, examples, number, reporter.instance_variable_get('@profiler') end end From 4ee7e3639f12dcc9e473ca7f95cf8d73cf93910f Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Fri, 29 May 2015 15:08:15 +1000 Subject: [PATCH 224/258] switch to passing in example_groups from the profiler to the notification rather than the profiler itself --- lib/rspec/core/notifications.rb | 19 ++++++++++++------- lib/rspec/core/reporter.rb | 2 +- spec/support/formatter_support.rb | 2 +- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/lib/rspec/core/notifications.rb b/lib/rspec/core/notifications.rb index 2018a505ae..b3d1c6b55b 100644 --- a/lib/rspec/core/notifications.rb +++ b/lib/rspec/core/notifications.rb @@ -400,9 +400,16 @@ def duplicate_rerun_locations # @attr duration [Float] the time taken (in seconds) to run the suite # @attr examples [Array] the examples run # @attr number_of_examples [Fixnum] the number of examples to profile - # @attr profiler [RSpec::Core::Profiler] the profiler used to capture examples - ProfileNotification = Struct.new(:duration, :examples, :number_of_examples, :profiler) + # @attr example_groups [Array] example groups run class ProfileNotification + def initialize(duration, examples, number_of_examples, example_groups) + @duration = duration + @examples = examples + @number_of_examples = number_of_examples + @example_groups = example_groups + end + attr_reader :duration, :examples, :number_of_examples + # @return [Array] the slowest examples def slowest_examples @slowest_examples ||= @@ -436,16 +443,14 @@ def slowest_groups private def calculate_slowest_groups - example_groups = profiler.example_groups - # stop if we've only one example group - return {} if example_groups.keys.length <= 1 + return {} if @example_groups.keys.length <= 1 - example_groups.each_value do |hash| + @example_groups.each_value do |hash| hash[:average] = hash[:total_time].to_f / hash[:count] end - example_groups.sort_by { |_, hash| -hash[:average] }.first(number_of_examples) + @example_groups.sort_by { |_, hash| -hash[:average] }.first(number_of_examples) end end diff --git a/lib/rspec/core/reporter.rb b/lib/rspec/core/reporter.rb index f0009d94bb..eac96e2f3e 100644 --- a/lib/rspec/core/reporter.rb +++ b/lib/rspec/core/reporter.rb @@ -152,7 +152,7 @@ def finish unless mute_profile_output? notify :dump_profile, Notifications::ProfileNotification.new(@duration, @examples, @configuration.profile_examples, - @profiler) + @profiler.example_groups) end notify :dump_summary, Notifications::SummaryNotification.new(@duration, @examples, @failed_examples, @pending_examples, @load_time) diff --git a/spec/support/formatter_support.rb b/spec/support/formatter_support.rb index 77e9bb7697..3815ada86f 100644 --- a/spec/support/formatter_support.rb +++ b/spec/support/formatter_support.rb @@ -281,7 +281,7 @@ def summary_notification(duration, examples, failed, pending, time) end def profile_notification(duration, examples, number) - ::RSpec::Core::Notifications::ProfileNotification.new duration, examples, number, reporter.instance_variable_get('@profiler') + ::RSpec::Core::Notifications::ProfileNotification.new duration, examples, number, reporter.instance_variable_get('@profiler').example_groups end end From 85cb4ee831b4ca83de1e6f58a105b9188afc15c8 Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Tue, 2 Jun 2015 09:49:12 +1000 Subject: [PATCH 225/258] refactor profiler specs to assert on hash changing --- spec/rspec/core/profiler_spec.rb | 12 ++++++------ spec/support/matchers.rb | 1 + 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/spec/rspec/core/profiler_spec.rb b/spec/rspec/core/profiler_spec.rb index 27cdb4179c..68029dbb00 100644 --- a/spec/rspec/core/profiler_spec.rb +++ b/spec/rspec/core/profiler_spec.rb @@ -30,9 +30,9 @@ def group it 'records example groups start time and description via id' do expect { profiler.example_group_started group_notification group - }.to change { profiler.example_groups[group] }.to include( - :start => now, :description => description - ) + }.to change { profiler.example_groups[group] }. + from(a_hash_excluding(:start, :description)). + to(a_hash_including(:start => now, :description => description)) end end @@ -45,9 +45,9 @@ def group it 'records example groups total time and description via id' do expect { profiler.example_group_finished group_notification group - }.to change { profiler.example_groups[group] }.to include( - :total_time => 1.0 - ) + }.to change { profiler.example_groups[group] }. + from(a_hash_excluding(:total_time)). + to(a_hash_including(:total_time => 1.0)) end end diff --git a/spec/support/matchers.rb b/spec/support/matchers.rb index fdf5d14480..44a2e4f591 100644 --- a/spec/support/matchers.rb +++ b/spec/support/matchers.rb @@ -123,3 +123,4 @@ def failure_reason(example) RSpec::Matchers.define_negated_matcher :exclude, :include RSpec::Matchers.define_negated_matcher :excluding, :include RSpec::Matchers.define_negated_matcher :avoid_changing, :change +RSpec::Matchers.define_negated_matcher :a_hash_excluding, :include From 2e59a6efca7ab5968b78da93205197f94d2778ec Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Tue, 2 Jun 2015 10:35:35 +1000 Subject: [PATCH 226/258] lazily instantiate profiler --- lib/rspec/core/formatters.rb | 6 +++++- lib/rspec/core/reporter.rb | 9 +++++++-- spec/rspec/core/formatters_spec.rb | 2 ++ spec/rspec/core/reporter_spec.rb | 1 + spec/support/formatter_support.rb | 1 + 5 files changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/rspec/core/formatters.rb b/lib/rspec/core/formatters.rb index a00727caa6..492af2291b 100644 --- a/lib/rspec/core/formatters.rb +++ b/lib/rspec/core/formatters.rb @@ -126,7 +126,11 @@ def setup_default(output_stream, deprecation_stream) add FallbackMessageFormatter, output_stream end - return unless RSpec.configuration.profile_examples? && !existing_formatter_implements?(:dump_profile) + return unless RSpec.configuration.profile_examples? + + @reporter.setup_profiler + + return if existing_formatter_implements?(:dump_profile) add RSpec::Core::Formatters::ProfileFormatter, output_stream end diff --git a/lib/rspec/core/reporter.rb b/lib/rspec/core/reporter.rb index eac96e2f3e..b396261999 100644 --- a/lib/rspec/core/reporter.rb +++ b/lib/rspec/core/reporter.rb @@ -18,8 +18,6 @@ def initialize(configuration) @failed_examples = [] @pending_examples = [] @duration = @start = @load_time = nil - @profiler = Profiler.new - register_listener @profiler, *Profiler::NOTIFICATIONS end # @private @@ -30,6 +28,13 @@ def reset @examples = [] @failed_examples = [] @pending_examples = [] + @profiler = Profiler.new if defined?(@profiler) + end + + # @private + def setup_profiler + @profiler = Profiler.new + register_listener @profiler, *Profiler::NOTIFICATIONS end # Registers a listener to a list of notifications. The reporter will send diff --git a/spec/rspec/core/formatters_spec.rb b/spec/rspec/core/formatters_spec.rb index cccbd65fb1..ae96a27fb7 100644 --- a/spec/rspec/core/formatters_spec.rb +++ b/spec/rspec/core/formatters_spec.rb @@ -163,6 +163,7 @@ module RSpec::Core::Formatters context "without an existing profile formatter" do it "will add the profile formatter" do allow(reporter).to receive(:registered_listeners).with(:dump_profile) { [] } + expect(reporter).to receive(:setup_profiler) expect { setup_default }.to change { loader.formatters }. @@ -174,6 +175,7 @@ module RSpec::Core::Formatters context "when a formatter that implement #dump_profile is added" do it "wont add the profile formatter" do allow(reporter).to receive(:registered_listeners).with(:dump_profile) { [:json] } + expect(reporter).to receive(:setup_profiler) setup_default expect( loader.formatters.map(&:class) diff --git a/spec/rspec/core/reporter_spec.rb b/spec/rspec/core/reporter_spec.rb index 2b554a0abb..f3730c127a 100644 --- a/spec/rspec/core/reporter_spec.rb +++ b/spec/rspec/core/reporter_spec.rb @@ -25,6 +25,7 @@ module RSpec::Core it "dumps the failure summary after the profile and deprecation summary so failures don't scroll off the screen and get missed" do config.profile_examples = 10 formatter = instance_double("RSpec::Core::Formatter::ProgressFormatter") + reporter.setup_profiler reporter.register_listener(formatter, :dump_summary, :dump_profile, :deprecation_summary) expect(formatter).to receive(:deprecation_summary).ordered diff --git a/spec/support/formatter_support.rb b/spec/support/formatter_support.rb index 3815ada86f..40725e1c36 100644 --- a/spec/support/formatter_support.rb +++ b/spec/support/formatter_support.rb @@ -187,6 +187,7 @@ def setup_reporter(*streams) def setup_profiler config.profile_examples = true + reporter.setup_profiler end def formatter_output From 7f4d70b0e68c27c7558c287198e6c6b39fafb2cd Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Tue, 2 Jun 2015 23:30:23 -0700 Subject: [PATCH 227/258] We no longer need this file. With the changes in rspec/rspec-expectations#796, this file is no longer necessary. --- .jrubyrc | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .jrubyrc diff --git a/.jrubyrc b/.jrubyrc deleted file mode 100644 index d077e16c96..0000000000 --- a/.jrubyrc +++ /dev/null @@ -1,5 +0,0 @@ -# Remove `.java` lines from JRuby stacktraces. Necessary for a passing travis build -# on JRuby in 1.8 mode for the new failure aggregator specs. Our generated test -# fixture doesn't know how to deal with excess java lines so it's best to ignore -# those lines. -backtrace.mask=true From bebc52c9e7c411d4b1951e7b3169eeb8528daab8 Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Wed, 3 Jun 2015 22:21:45 +1000 Subject: [PATCH 228/258] changelog for #1971 [skip ci] --- Changelog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Changelog.md b/Changelog.md index 82dc5fa2e5..544bc4f636 100644 --- a/Changelog.md +++ b/Changelog.md @@ -49,6 +49,8 @@ Enhancements: Marston, #1946) * When no formatter implements #message add a fallback to prevent those messages being lost. (Jon Rowe, #1980) +* Profiling examples now takes into account time spent in `before(:context)` + hooks. (Denis Laliberté, Jon Rowe, #1971) Bug Fixes: From 48db9020b714ac5a37cac52bf7e47bc8de27c5d2 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Wed, 3 Jun 2015 18:22:26 -0700 Subject: [PATCH 229/258] =?UTF-8?q?Don=E2=80=99t=20add=20explanatory=20mes?= =?UTF-8?q?sage=20to=20originally=20empty=20backtraces.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/rspec/core/backtrace_formatter.rb | 2 +- spec/rspec/core/backtrace_formatter_spec.rb | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/rspec/core/backtrace_formatter.rb b/lib/rspec/core/backtrace_formatter.rb index e382e6e462..74617ec258 100644 --- a/lib/rspec/core/backtrace_formatter.rb +++ b/lib/rspec/core/backtrace_formatter.rb @@ -31,7 +31,7 @@ def filter_gem(gem_name) end def format_backtrace(backtrace, options={}) - return backtrace if options[:full_backtrace] + return backtrace if options[:full_backtrace] || backtrace.empty? backtrace.map { |l| backtrace_line(l) }.compact. tap do |filtered| diff --git a/spec/rspec/core/backtrace_formatter_spec.rb b/spec/rspec/core/backtrace_formatter_spec.rb index 099efb78cd..ba81f4f86f 100644 --- a/spec/rspec/core/backtrace_formatter_spec.rb +++ b/spec/rspec/core/backtrace_formatter_spec.rb @@ -132,6 +132,13 @@ def make_backtrace_formatter(exclusion_patterns=nil, inclusion_patterns=nil) end end + describe "an empty backtrace" do + it "does not add the explanatory message about backtrace filtering" do + formatter = BacktraceFormatter.new + expect(formatter.format_backtrace([])).to eq([]) + end + end + context "when rspec is installed in the current working directory" do it "excludes lines from rspec libs by default", :unless => RSpec::Support::OS.windows? do backtrace = [ From 8839c52734abbdd9b477b6d454b4142c825c348b Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Thu, 4 Jun 2015 10:17:18 -0700 Subject: [PATCH 230/258] Rename option to be more explicit about its purpose. --- lib/rspec/core/configuration.rb | 2 +- lib/rspec/core/formatters/exception_presenter.rb | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/rspec/core/configuration.rb b/lib/rspec/core/configuration.rb index 96584ea514..9467a056c0 100644 --- a/lib/rspec/core/configuration.rb +++ b/lib/rspec/core/configuration.rb @@ -1736,7 +1736,7 @@ def value_for(key) def define_built_in_hooks around(:example, :aggregate_failures => true) do |ex| - aggregate_failures(nil, :from_around_hook => true, &ex) + aggregate_failures(nil, :hide_backtrace => true, &ex) end end diff --git a/lib/rspec/core/formatters/exception_presenter.rb b/lib/rspec/core/formatters/exception_presenter.rb index bd3bd0fceb..74beb9a876 100644 --- a/lib/rspec/core/formatters/exception_presenter.rb +++ b/lib/rspec/core/formatters/exception_presenter.rb @@ -203,7 +203,7 @@ def with_multiple_error_options_as_needed(exception, options) options[:description_formatter] &&= Proc.new {} - return options unless exception.aggregation_metadata[:from_around_hook] + return options unless exception.aggregation_metadata[:hide_backtrace] options[:backtrace_formatter] = EmptyBacktraceFormatter options end @@ -215,9 +215,13 @@ def multiple_exceptions_not_met_error?(exception) def multiple_failure_sumarizer(exception, prior_detail_formatter, color) lambda do |example, colorizer, indentation| - summary = if exception.aggregation_metadata[:from_around_hook] + summary = if exception.aggregation_metadata[:hide_backtrace] + # Since the backtrace is hidden, the subfailures will come + # immeidately after this, and using `:` will read well. "Got #{exception.exception_count_description}:" else + # The backtrace comes after this, so using a `:` doesn't make sense + # since the failures may be many lines below. "#{exception.summary}." end From ece6ea4a6384db6300aaa0a96c4c384f1f910bfe Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Wed, 3 Jun 2015 23:02:35 -0700 Subject: [PATCH 231/258] Add `MultipleExceptionError`. This will allow us to support better output when there are multiple exceptions. --- .../core/formatters/exception_presenter.rb | 67 +++++++++++ .../formatters/exception_presenter_spec.rb | 107 ++++++++++++++++++ 2 files changed, 174 insertions(+) diff --git a/lib/rspec/core/formatters/exception_presenter.rb b/lib/rspec/core/formatters/exception_presenter.rb index 74beb9a876..6cd103c491 100644 --- a/lib/rspec/core/formatters/exception_presenter.rb +++ b/lib/rspec/core/formatters/exception_presenter.rb @@ -290,5 +290,72 @@ def with_truncated_backtrace(child) end end end + + # Provides a single exception instance that provides access to + # multiple sub-exceptions. This is used in situations where a single + # individual spec has multiple exceptions, such as one in the `it` block + # and one in an `after` block. + class MultipleExceptionError < StandardError + # @return [Array] The list of failures. + attr_reader :failures + + # @return [Array] The list of other errors. + attr_reader :other_errors + + # @return [Array] The list of failures and other exceptions, combined. + attr_reader :all_exceptions + + # @return [Hash] Metadata used by RSpec for formatting purposes. + attr_reader :aggregation_metadata + + # @return [nil] Provided only for interface compatibility with + # `RSpec::Expectations::MultipleExpectationsNotMetError`. + attr_reader :aggregation_block_label + + # @param exceptions [Array] The initial list of exceptions. + def initialize(*exceptions) + super() + + @failures = [] + @other_errors = [] + @all_exceptions = [] + @aggregation_metadata = { :hide_backtrace => true } + @aggregation_block_label = nil + + exceptions.each { |e| add e } + end + + # Appends the provided exception to the list. + # @param exception [Exception] Exception to append to the list. + def add(exception) + @all_exceptions << exception + + if exception.class.name =~ /RSpec/ + @failures << exception + else + @other_errors << exception + end + end + + # @return [String] Combines all the exception messages into a single string. + # @note RSpec does not actually use this -- instead it formats each exception + # individually. + def message + all_exceptions.map(&:message).join("\n\n") + end + + # @return [String] A summary of the failure, including the block label and a count of failures. + def summary + "Got #{exception_count_description}" + end + + # return [String] A description of the failure/error counts. + def exception_count_description + failure_count = Formatters::Helpers.pluralize(failures.size, "failure") + return failure_count if other_errors.empty? + error_count = Formatters::Helpers.pluralize(other_errors.size, "other error") + "#{failure_count} and #{error_count}" + end + end end end diff --git a/spec/rspec/core/formatters/exception_presenter_spec.rb b/spec/rspec/core/formatters/exception_presenter_spec.rb index f4cbf51d26..ec540af9cc 100644 --- a/spec/rspec/core/formatters/exception_presenter_spec.rb +++ b/spec/rspec/core/formatters/exception_presenter_spec.rb @@ -306,4 +306,111 @@ def exception_with(backtrace) expect(truncated).to be child end end + + RSpec.describe MultipleExceptionError do + it 'supports the same interface as `RSpec::Expectations::MultipleExpectationsNotMetError`' do + skip "Skipping to allow an rspec-expectations PR to add a new method and remain green" if ENV['NEW_MUTLI_EXCEPTION_METHOD'] + + aggregate_failures { } # force autoload + + interface = RSpec::Expectations::MultipleExpectationsNotMetError.instance_methods - Exception.instance_methods + expect(MultipleExceptionError.new).to respond_to(*interface) + end + + def new_failure(*a) + RSpec::Expectations::ExpectationNotMetError.new(*a) + end + + def new_error(*a) + StandardError.new(*a) + end + + it 'allows you to keep track of failures and other errors in order' do + mee = MultipleExceptionError.new + + f1 = new_failure + e1 = new_error + f2 = new_failure + + expect { mee.add(f1) }.to change(mee, :failures).to [f1] + expect { mee.add(e1) }.to change(mee, :other_errors).to [e1] + expect { mee.add(f2) }.to change(mee, :failures).to [f1, f2] + + expect(mee.all_exceptions).to eq([f1, e1, f2]) + end + + it 'allows you to add exceptions of an anonymous class' do + mee = MultipleExceptionError.new + expect { + mee.add(Class.new(StandardError).new) + }.to change(mee.other_errors, :count).by 1 + end + + it 'allows you to instantiate it with an initial list of exceptions' do + mee = MultipleExceptionError.new(f1 = new_failure, e1 = new_error) + + expect(mee).to have_attributes( + :failures => [f1], + :other_errors => [e1], + :all_exceptions => [f1, e1] + ) + end + + specify 'the `message` implementation provides all failure messages, but is not well formatted because we do not actually use it' do + mee = MultipleExceptionError.new( + new_failure("failure 1"), + new_error("error 1") + ) + + expect(mee.message).to include("failure 1", "error 1") + end + + it 'provides a description of the exception counts, correctly categorized as failures or exceptions' do + mee = MultipleExceptionError.new + + expect { + mee.add new_failure + mee.add new_error + }.to change(mee, :exception_count_description). + from("0 failures"). + to("1 failure and 1 other error") + + expect { + mee.add new_failure + mee.add new_error + }.to change(mee, :exception_count_description). + to("2 failures and 2 other errors") + end + + it 'provides a summary of the exception counts' do + mee = MultipleExceptionError.new + + expect { + mee.add new_failure + mee.add new_error + }.to change(mee, :summary). + from("Got 0 failures"). + to("Got 1 failure and 1 other error") + + expect { + mee.add new_failure + mee.add new_error + }.to change(mee, :summary). + to("Got 2 failures and 2 other errors") + end + + it 'presents the same aggregation metadata that an `:aggregate_failures`-tagged example produces' do + ex = nil + + RSpec.describe do + ex = it "", :aggregate_failures do + expect(1).to eq(2) + expect(1).to eq(2) + end + end.run + + expected_metadata = ex.exception.aggregation_metadata + expect(MultipleExceptionError.new.aggregation_metadata).to eq(expected_metadata) + end + end end From c561ca222164fea9ce61067911674505f14b3386 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Thu, 4 Jun 2015 21:18:51 -0700 Subject: [PATCH 232/258] Provide common interface tag for detecting multiple exception errors. Also rename methods to not be specific to `RSpec::Expectations::MultipleExpectationsNotMetError`. --- lib/rspec/core/configuration.rb | 7 ++++++ .../core/formatters/exception_presenter.rb | 24 ++++++++++++------- spec/rspec/core/notifications_spec.rb | 19 +++++++++++++++ spec/support/fake_libs/rspec/expectations.rb | 1 + 4 files changed, 43 insertions(+), 8 deletions(-) diff --git a/lib/rspec/core/configuration.rb b/lib/rspec/core/configuration.rb index 9467a056c0..491e8f1f33 100644 --- a/lib/rspec/core/configuration.rb +++ b/lib/rspec/core/configuration.rb @@ -638,6 +638,13 @@ def expect_with(*frameworks) framework when :rspec require 'rspec/expectations' + + # Tag this exception class so our exception formatting logic knows + # that it satisfies the `MultipleExceptionError` interface. + ::RSpec::Expectations::MultipleExpectationsNotMetError.__send__( + :include, MultipleExceptionError::InterfaceTag + ) + ::RSpec::Matchers when :test_unit require 'rspec/core/test_unit_assertions_adapter' diff --git a/lib/rspec/core/formatters/exception_presenter.rb b/lib/rspec/core/formatters/exception_presenter.rb index 6cd103c491..658542d208 100644 --- a/lib/rspec/core/formatters/exception_presenter.rb +++ b/lib/rspec/core/formatters/exception_presenter.rb @@ -191,14 +191,14 @@ def pending_options end def with_multiple_error_options_as_needed(exception, options) - return options unless multiple_exceptions_not_met_error?(exception) + return options unless multiple_exceptions_error?(exception) options = options.merge( :failure_lines => [], :extra_detail_formatter => sub_failure_list_formatter(exception, options[:message_color]), - :detail_formatter => multiple_failure_sumarizer(exception, - options[:detail_formatter], - options[:message_color]) + :detail_formatter => multiple_exception_sumarizer(exception, + options[:detail_formatter], + options[:message_color]) ) options[:description_formatter] &&= Proc.new {} @@ -208,12 +208,11 @@ def with_multiple_error_options_as_needed(exception, options) options end - def multiple_exceptions_not_met_error?(exception) - return false unless defined?(RSpec::Expectations::MultipleExpectationsNotMetError) - RSpec::Expectations::MultipleExpectationsNotMetError === exception + def multiple_exceptions_error?(exception) + MultipleExceptionError::InterfaceTag === exception end - def multiple_failure_sumarizer(exception, prior_detail_formatter, color) + def multiple_exception_sumarizer(exception, prior_detail_formatter, color) lambda do |example, colorizer, indentation| summary = if exception.aggregation_metadata[:hide_backtrace] # Since the backtrace is hidden, the subfailures will come @@ -296,6 +295,15 @@ def with_truncated_backtrace(child) # individual spec has multiple exceptions, such as one in the `it` block # and one in an `after` block. class MultipleExceptionError < StandardError + # @private + # Used so there is a common module in the ancestor chain of this class + # and `RSpec::Expectations::MultipleExpectationsNotMetError`, which allows + # code to detect exceptions that are instances of either, without first + # checking to see if rspec-expectations is loaded. + InterfaceTag = Module.new + + include InterfaceTag + # @return [Array] The list of failures. attr_reader :failures diff --git a/spec/rspec/core/notifications_spec.rb b/spec/rspec/core/notifications_spec.rb index 2464b77814..93fbc41f51 100644 --- a/spec/rspec/core/notifications_spec.rb +++ b/spec/rspec/core/notifications_spec.rb @@ -275,6 +275,25 @@ def normalize_one_backtrace(exception) end end end + + context "when the exception is a MultipleExceptionError" do + let(:sub_failure_1) { StandardError.new("foo").tap { |e| e.set_backtrace([]) } } + let(:sub_failure_2) { StandardError.new("bar").tap { |e| e.set_backtrace([]) } } + let(:exception) { RSpec::Core::MultipleExceptionError.new(sub_failure_1, sub_failure_2) } + + it "lists each sub-failure, just like with MultipleExpectationsNotMetError" do + expect(fully_formatted.lines.to_a.last(8)).to eq(dedent(<<-EOS).lines.to_a) + | + | 1.1) Failure/Error: Unable to find matching line from backtrace + | StandardError: + | foo + | + | 1.2) Failure/Error: Unable to find matching line from backtrace + | StandardError: + | bar + EOS + end + end end describe '#message_lines' do diff --git a/spec/support/fake_libs/rspec/expectations.rb b/spec/support/fake_libs/rspec/expectations.rb index 33af050df1..3a47761793 100644 --- a/spec/support/fake_libs/rspec/expectations.rb +++ b/spec/support/fake_libs/rspec/expectations.rb @@ -1,5 +1,6 @@ module RSpec module Expectations + MultipleExpectationsNotMetError = Class.new(Exception) end module Matchers From 909487698a5aff5619db3693c230ad85cd5d34fb Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Wed, 3 Jun 2015 18:13:45 -0700 Subject: [PATCH 233/258] Improve handling of additional `around`/`after` exceptions. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now that we have nice formatting logic for multiple exception errors, let’s leverage that to improve the output for this case. Fixes #1966. --- .../aggregating_failures.feature | 63 +++++++---- lib/rspec/core/example.rb | 29 ++--- lib/rspec/core/hooks.rb | 2 +- spec/rspec/core/example_spec.rb | 106 ++++++++---------- 4 files changed, 101 insertions(+), 99 deletions(-) diff --git a/features/expectation_framework_integration/aggregating_failures.feature b/features/expectation_framework_integration/aggregating_failures.feature index def8b9688a..f3c88e1596 100644 --- a/features/expectation_framework_integration/aggregating_failures.feature +++ b/features/expectation_framework_integration/aggregating_failures.feature @@ -13,7 +13,7 @@ Feature: Aggregating Failures Response = Struct.new(:status, :headers, :body) class Client - def self.make_request + def self.make_request(url='/') Response.new(404, { "Content-Type" => "text/plain" }, "Not Found") end end @@ -25,6 +25,17 @@ Feature: Aggregating Failures require 'client' RSpec.describe Client do + after do + # this should be appended to failure list + expect(false).to be(true), "after hook failure" + end + + around do |ex| + ex.run + # this should also be appended to failure list + expect(false).to be(true), "around hook failure" + end + it "returns a successful response" do response = Client.make_request @@ -42,32 +53,44 @@ Feature: Aggregating Failures Failures: 1) Client returns a successful response - Got 3 failures from failure aggregation block "testing reponse". - # ./spec/use_block_form_spec.rb:7 + Got 3 failures: - 1.1) Failure/Error: expect(response.status).to eq(200) + 1.1) Got 3 failures from failure aggregation block "testing reponse". + # ./spec/use_block_form_spec.rb:18:in `block (2 levels) in ' + # ./spec/use_block_form_spec.rb:10:in `block (2 levels) in ' - expected: 200 - got: 404 + 1.1.1) Failure/Error: expect(response.status).to eq(200) - (compared using ==) - # ./spec/use_block_form_spec.rb:8 + expected: 200 + got: 404 - 1.2) Failure/Error: expect(response.headers).to include("Content-Type" => "application/json") - expected {"Content-Type" => "text/plain"} to include {"Content-Type" => "application/json"} - Diff: - @@ -1,2 +1,2 @@ - -[{"Content-Type"=>"application/json"}] - +"Content-Type" => "text/plain", - # ./spec/use_block_form_spec.rb:9 + (compared using ==) + # ./spec/use_block_form_spec.rb:19:in `block (3 levels) in ' - 1.3) Failure/Error: expect(response.body).to eq('{"message":"Success"}') + 1.1.2) Failure/Error: expect(response.headers).to include("Content-Type" => "application/json") + expected {"Content-Type" => "text/plain"} to include {"Content-Type" => "application/json"} + Diff: + @@ -1,2 +1,2 @@ + -[{"Content-Type"=>"application/json"}] + +"Content-Type" => "text/plain", + # ./spec/use_block_form_spec.rb:20:in `block (3 levels) in ' - expected: "{\"message\":\"Success\"}" - got: "Not Found" + 1.1.3) Failure/Error: expect(response.body).to eq('{"message":"Success"}') - (compared using ==) - # ./spec/use_block_form_spec.rb:10 + expected: "{\"message\":\"Success\"}" + got: "Not Found" + + (compared using ==) + # ./spec/use_block_form_spec.rb:21:in `block (3 levels) in ' + + 1.2) Failure/Error: expect(false).to be(true), "after hook failure" + after hook failure + # ./spec/use_block_form_spec.rb:6:in `block (2 levels) in ' + # ./spec/use_block_form_spec.rb:10:in `block (2 levels) in ' + + 1.3) Failure/Error: expect(false).to be(true), "around hook failure" + around hook failure + # ./spec/use_block_form_spec.rb:12:in `block (2 levels) in ' """ Scenario: Use `:aggregate_failures` metadata diff --git a/lib/rspec/core/example.rb b/lib/rspec/core/example.rb index 4b29437af5..2f794fe7c3 100644 --- a/lib/rspec/core/example.rb +++ b/lib/rspec/core/example.rb @@ -306,24 +306,17 @@ def inspect # # Used internally to set an exception in an after hook, which # captures the exception but doesn't raise it. - def set_exception(exception, context=nil) + def set_exception(exception) if pending? && !(Pending::PendingExampleFixedError === exception) execution_result.pending_exception = exception - else - if @exception - # An error has already been set; we don't want to override it, - # but we also don't want silence the error, so let's print it. - msg = <<-EOS - - An error occurred #{context} - #{exception.class}: #{exception.message} - occurred at #{exception.backtrace.first} - - EOS - RSpec.configuration.reporter.message(msg) + elsif @exception + unless RSpec::Core::MultipleExceptionError === @exception + @exception = RSpec::Core::MultipleExceptionError.new(@exception) end - @exception ||= exception + @exception.add exception + else + @exception = exception end end @@ -348,10 +341,10 @@ def skip_with_exception(reporter, exception) end # @private - def instance_exec_with_rescue(context, &block) + def instance_exec_with_rescue(&block) @example_group_instance.instance_exec(self, &block) rescue Exception => e - set_exception(e, context) + set_exception(e) end # @private @@ -368,7 +361,7 @@ def hooks def with_around_example_hooks hooks.run(:around, :example, self) { yield } rescue Exception => e - set_exception(e, "in an `around(:example)` hook") + set_exception(e) end def start(reporter) @@ -548,7 +541,7 @@ def initialize end # To ensure we don't silence errors. - def set_exception(exception, _context=nil) + def set_exception(exception) raise exception end end diff --git a/lib/rspec/core/hooks.rb b/lib/rspec/core/hooks.rb index 84d8dda5a5..5757f10f61 100644 --- a/lib/rspec/core/hooks.rb +++ b/lib/rspec/core/hooks.rb @@ -361,7 +361,7 @@ def run(example) # @private class AfterHook < Hook def run(example) - example.instance_exec_with_rescue("in an after hook", &block) + example.instance_exec_with_rescue(&block) end end diff --git a/spec/rspec/core/example_spec.rb b/spec/rspec/core/example_spec.rb index e5011ba43e..ebabc734d5 100644 --- a/spec/rspec/core/example_spec.rb +++ b/spec/rspec/core/example_spec.rb @@ -31,11 +31,8 @@ def metadata_hash(*args) end describe "#exception" do - it "supplies the first exception raised, if any" do - RSpec.configuration.output_stream = StringIO.new - + it "supplies the exception raised, if there is one" do example = example_group.example { raise "first" } - example_group.after { raise "second" } example_group.run expect(example.exception.message).to eq("first") end @@ -45,6 +42,25 @@ def metadata_hash(*args) example_group.run expect(example.exception).to be_nil end + + it 'provides a `MultipleExceptionError` if there are multiple exceptions (e.g. from `it`, `around` and `after`)' do + the_example = nil + + after_ex = StandardError.new("after") + around_ex = StandardError.new("around") + example_ex = StandardError.new("example") + + RSpec.describe do + the_example = example { raise example_ex } + after { raise after_ex } + around { |ex| ex.run; raise around_ex } + end.run + + expect(the_example.exception).to have_attributes( + :class => RSpec::Core::MultipleExceptionError, + :all_exceptions => [example_ex, after_ex, around_ex] + ) + end end describe "when there is an explicit description" do @@ -401,65 +417,20 @@ def expect_gc(opts) end end - context 'when the example raises an error' do - def run_and_capture_reported_message(group) - reported_msg = nil - # We can't use should_receive(:message).with(/.../) here, - # because if that fails, it would fail within our example-under-test, - # and since there's already two errors, it would just be reported again. - allow(RSpec.configuration.reporter).to receive(:message) { |msg| reported_msg = msg } - group.run - reported_msg - end + it "leaves raised exceptions unmodified (GH-1103)" do + # set the backtrace, otherwise MRI will build a whole new object, + # and thus mess with our expectations. Rubinius and JRuby are not + # affected. + exception = StandardError.new + exception.set_backtrace([]) - it "prints any around hook errors rather than silencing them" do - group = RSpec.describe do - around(:each) { |e| e.run; raise "around" } - example("e") { raise "example" } - end - - message = run_and_capture_reported_message(group) - expect(message).to match(/An error occurred in an `around.* hook/i) - end - - it "prints any after hook errors rather than silencing them" do - group = RSpec.describe do - after(:each) { raise "after" } - example("e") { raise "example" } - end - - message = run_and_capture_reported_message(group) - expect(message).to match(/An error occurred in an after.* hook/i) - end - - it "does not print mock expectation errors" do - group = RSpec.describe do - example do - foo = double - expect(foo).to receive(:bar) - raise "boom" - end - end - - message = run_and_capture_reported_message(group) - expect(message).to be_nil + group = RSpec.describe do + example { raise exception.freeze } end + group.run - it "leaves a raised exception unmodified (GH-1103)" do - # set the backtrace, otherwise MRI will build a whole new object, - # and thus mess with our expectations. Rubinius and JRuby are not - # affected. - exception = StandardError.new - exception.set_backtrace([]) - - group = RSpec.describe do - example { raise exception.freeze } - end - group.run - - actual = group.examples.first.execution_result.exception - expect(actual.__id__).to eq(exception.__id__) - end + actual = group.examples.first.execution_result.exception + expect(actual.__id__).to eq(exception.__id__) end context "with --dry-run" do @@ -752,6 +723,21 @@ def expect_pending_result(example) expect(ex).to fail_with(RSpec::Mocks::MockExpectationError) end + it 'skips mock verification if the example has already failed' do + ex = nil + boom = StandardError.new("boom") + + RSpec.describe do + ex = example do + dbl = double + expect(dbl).to receive(:Foo) + raise boom + end + end.run + + expect(ex.exception).to be boom + end + it 'allows `after(:example)` hooks to satisfy mock expectations, since examples are not complete until their `after` hooks run' do ex = nil From b7f038571163ebb18742650ed144a21499fdff83 Mon Sep 17 00:00:00 2001 From: Yule Date: Fri, 5 Jun 2015 11:58:40 +0100 Subject: [PATCH 234/258] stop 70 being displayed as 1 minute 1 second --- lib/rspec/core/formatters/helpers.rb | 3 +-- spec/rspec/core/formatters/helpers_spec.rb | 12 ++++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/rspec/core/formatters/helpers.rb b/lib/rspec/core/formatters/helpers.rb index 195052f310..94bfbd45cb 100644 --- a/lib/rspec/core/formatters/helpers.rb +++ b/lib/rspec/core/formatters/helpers.rb @@ -70,8 +70,7 @@ def self.format_seconds(float, precision=nil) # @param string [String] string with trailing zeros # @return [String] string with trailing zeros removed def self.strip_trailing_zeroes(string) - stripped = string.sub(/[^1-9]+$/, '') - stripped.empty? ? "0" : stripped + string.sub(/(?:(\..*[^0])0+|\.0+)$/, '\1') end private_class_method :strip_trailing_zeroes diff --git a/spec/rspec/core/formatters/helpers_spec.rb b/spec/rspec/core/formatters/helpers_spec.rb index 6fe50854b0..f0bcd952a3 100644 --- a/spec/rspec/core/formatters/helpers_spec.rb +++ b/spec/rspec/core/formatters/helpers_spec.rb @@ -45,6 +45,12 @@ expect(helper.format_duration(1)).to eq("1 second") end end + + context '= 70' do + it "returns 'x minute, x0 seconds' formatted string" do + expect(helper.format_duration(70)).to eq("1 minute 10 seconds") + end + end context 'with mathn loaded' do include MathnIntegrationSupport @@ -82,6 +88,12 @@ expect(helper.format_seconds(1.00000000001)).to eq("1") end end + + context "70" do + it "doesn't strip of meaningful trailing zeros" do + expect(helper.format_seconds(70)).to eq("70") + end + end end context "second and greater times" do From 993b7960d23b503874a98bceff7db4c08b20a603 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Thu, 4 Jun 2015 23:18:04 -0700 Subject: [PATCH 235/258] Refactor: in-line logic from a single-use method. --- lib/rspec/core/example.rb | 7 ------- lib/rspec/core/hooks.rb | 4 +++- spec/rspec/core/hooks_spec.rb | 9 +++++++++ 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/rspec/core/example.rb b/lib/rspec/core/example.rb index 2f794fe7c3..773a32d693 100644 --- a/lib/rspec/core/example.rb +++ b/lib/rspec/core/example.rb @@ -340,13 +340,6 @@ def skip_with_exception(reporter, exception) finish(reporter) end - # @private - def instance_exec_with_rescue(&block) - @example_group_instance.instance_exec(self, &block) - rescue Exception => e - set_exception(e) - end - # @private def instance_exec(*args, &block) @example_group_instance.instance_exec(*args, &block) diff --git a/lib/rspec/core/hooks.rb b/lib/rspec/core/hooks.rb index 5757f10f61..3d020b0d56 100644 --- a/lib/rspec/core/hooks.rb +++ b/lib/rspec/core/hooks.rb @@ -361,7 +361,9 @@ def run(example) # @private class AfterHook < Hook def run(example) - example.instance_exec_with_rescue(&block) + example.instance_exec(example, &block) + rescue Exception => ex + example.set_exception(ex) end end diff --git a/spec/rspec/core/hooks_spec.rb b/spec/rspec/core/hooks_spec.rb index d77b6f4b95..7c55fce4b9 100644 --- a/spec/rspec/core/hooks_spec.rb +++ b/spec/rspec/core/hooks_spec.rb @@ -74,6 +74,15 @@ def hook_collection_for(position, scope) instance.hooks.run(type, scope, double("Example").as_null_object) }.not_to yield_control end + + if scope == :example + it "yields the example as an argument to the hook" do + group = RSpec.describe + ex = group.example { } + + expect { |p| group.send(type, scope, &p); group.run }.to yield_with_args(ex) + end + end end end end From f989168c9dbbcd930af6e7cc61e08057194929a8 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Thu, 4 Jun 2015 23:22:55 -0700 Subject: [PATCH 236/258] Always verify mocks at the end of each example. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ...even if the example has already failed. Previously, we did not have a good way to display multiple failures for a single example, but we do now, so it’s simpler and more consistent to always verify, and to provide the user with multiple exceptions if there are multiple. --- lib/rspec/core/example.rb | 6 +----- spec/rspec/core/example_spec.rb | 33 ++++++++++++++++++--------------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/lib/rspec/core/example.rb b/lib/rspec/core/example.rb index 773a32d693..49733a8ecb 100644 --- a/lib/rspec/core/example.rb +++ b/lib/rspec/core/example.rb @@ -408,7 +408,7 @@ def run_after_example end def verify_mocks - @example_group_instance.verify_mocks_for_rspec if mocks_need_verification? + @example_group_instance.verify_mocks_for_rspec rescue Exception => e if pending? execution_result.pending_fixed = false @@ -419,10 +419,6 @@ def verify_mocks end end - def mocks_need_verification? - exception.nil? || execution_result.pending_fixed? - end - def assign_generated_description if metadata[:description].empty? && (description = generate_description) metadata[:description] = description diff --git a/spec/rspec/core/example_spec.rb b/spec/rspec/core/example_spec.rb index ebabc734d5..b91bce7089 100644 --- a/spec/rspec/core/example_spec.rb +++ b/spec/rspec/core/example_spec.rb @@ -723,32 +723,35 @@ def expect_pending_result(example) expect(ex).to fail_with(RSpec::Mocks::MockExpectationError) end - it 'skips mock verification if the example has already failed' do - ex = nil - boom = StandardError.new("boom") - - RSpec.describe do - ex = example do - dbl = double - expect(dbl).to receive(:Foo) - raise boom - end - end.run + context "when the example has already failed" do + it 'appends the mock error to a `MultipleExceptionError` so the user can see both' do + ex = nil + boom = StandardError.new("boom") + + RSpec.describe do + ex = example do + dbl = double + expect(dbl).to receive(:Foo) + raise boom + end + end.run - expect(ex.exception).to be boom + expect(ex.exception).to be_a(RSpec::Core::MultipleExceptionError) + expect(ex.exception.all_exceptions).to match [boom, an_instance_of(RSpec::Mocks::MockExpectationError)] + end end it 'allows `after(:example)` hooks to satisfy mock expectations, since examples are not complete until their `after` hooks run' do ex = nil RSpec.describe do - let(:dbl) { double } + let(:the_dbl) { double } ex = example do - expect(dbl).to receive(:foo) + expect(the_dbl).to receive(:foo) end - after { dbl.foo } + after { the_dbl.foo } end.run expect(ex).to pass From 019d640d40a7791dd1880c6bdd3118ef4c652b9b Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 5 Jun 2015 12:52:27 -0700 Subject: [PATCH 237/258] Add `add` method to `RSpec::Expectations::MultipleExpectationsNotMetError`. It's a little weird ot add it from rspec-core, but we only need it here and this allows us to provide a common implementation. --- .../core/formatters/exception_presenter.rb | 27 +++++------ .../formatters/exception_presenter_spec.rb | 46 ++++++++++++++----- 2 files changed, 48 insertions(+), 25 deletions(-) diff --git a/lib/rspec/core/formatters/exception_presenter.rb b/lib/rspec/core/formatters/exception_presenter.rb index 658542d208..a469dba637 100644 --- a/lib/rspec/core/formatters/exception_presenter.rb +++ b/lib/rspec/core/formatters/exception_presenter.rb @@ -300,7 +300,20 @@ class MultipleExceptionError < StandardError # and `RSpec::Expectations::MultipleExpectationsNotMetError`, which allows # code to detect exceptions that are instances of either, without first # checking to see if rspec-expectations is loaded. - InterfaceTag = Module.new + module InterfaceTag + # Appends the provided exception to the list. + # @param exception [Exception] Exception to append to the list. + # @private + def add(exception) + all_exceptions << exception + + if exception.class.name =~ /RSpec/ + failures << exception + else + other_errors << exception + end + end + end include InterfaceTag @@ -333,18 +346,6 @@ def initialize(*exceptions) exceptions.each { |e| add e } end - # Appends the provided exception to the list. - # @param exception [Exception] Exception to append to the list. - def add(exception) - @all_exceptions << exception - - if exception.class.name =~ /RSpec/ - @failures << exception - else - @other_errors << exception - end - end - # @return [String] Combines all the exception messages into a single string. # @note RSpec does not actually use this -- instead it formats each exception # individually. diff --git a/spec/rspec/core/formatters/exception_presenter_spec.rb b/spec/rspec/core/formatters/exception_presenter_spec.rb index ec540af9cc..a73c3b0a2b 100644 --- a/spec/rspec/core/formatters/exception_presenter_spec.rb +++ b/spec/rspec/core/formatters/exception_presenter_spec.rb @@ -307,16 +307,7 @@ def exception_with(backtrace) end end - RSpec.describe MultipleExceptionError do - it 'supports the same interface as `RSpec::Expectations::MultipleExpectationsNotMetError`' do - skip "Skipping to allow an rspec-expectations PR to add a new method and remain green" if ENV['NEW_MUTLI_EXCEPTION_METHOD'] - - aggregate_failures { } # force autoload - - interface = RSpec::Expectations::MultipleExpectationsNotMetError.instance_methods - Exception.instance_methods - expect(MultipleExceptionError.new).to respond_to(*interface) - end - + RSpec.shared_examples_for "a class satisfying the common multiple exception error interface" do def new_failure(*a) RSpec::Expectations::ExpectationNotMetError.new(*a) end @@ -326,7 +317,7 @@ def new_error(*a) end it 'allows you to keep track of failures and other errors in order' do - mee = MultipleExceptionError.new + mee = new_multiple_exception_error f1 = new_failure e1 = new_error @@ -340,12 +331,43 @@ def new_error(*a) end it 'allows you to add exceptions of an anonymous class' do - mee = MultipleExceptionError.new + mee = new_multiple_exception_error + expect { mee.add(Class.new(StandardError).new) }.to change(mee.other_errors, :count).by 1 end + it 'is tagged with a common module so it is clear it has the interface for multiple exceptions' do + expect(MultipleExceptionError::InterfaceTag).to be === new_multiple_exception_error + end + end + + RSpec.describe RSpec::Expectations::ExpectationNotMetError do + include_examples "a class satisfying the common multiple exception error interface" do + def new_multiple_exception_error + failure_aggregator = RSpec::Expectations::FailureAggregator.new(nil, {}) + RSpec::Expectations::MultipleExpectationsNotMetError.new(failure_aggregator) + end + end + end + + RSpec.describe MultipleExceptionError do + include_examples "a class satisfying the common multiple exception error interface" do + def new_multiple_exception_error + MultipleExceptionError.new + end + end + + it 'supports the same interface as `RSpec::Expectations::MultipleExpectationsNotMetError`' do + skip "Skipping to allow an rspec-expectations PR to add a new method and remain green" if ENV['NEW_MUTLI_EXCEPTION_METHOD'] + + aggregate_failures { } # force autoload + + interface = RSpec::Expectations::MultipleExpectationsNotMetError.instance_methods - Exception.instance_methods + expect(MultipleExceptionError.new).to respond_to(*interface) + end + it 'allows you to instantiate it with an initial list of exceptions' do mee = MultipleExceptionError.new(f1 = new_failure, e1 = new_error) From 5977ef02d70320487842df6e747f60d8242cb181 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 5 Jun 2015 13:11:12 -0700 Subject: [PATCH 238/258] Add coercion method for `MultipleExceptionError::InterfaceTag`. --- .../core/formatters/exception_presenter.rb | 9 ++++++ .../formatters/exception_presenter_spec.rb | 29 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/lib/rspec/core/formatters/exception_presenter.rb b/lib/rspec/core/formatters/exception_presenter.rb index a469dba637..053aeafb01 100644 --- a/lib/rspec/core/formatters/exception_presenter.rb +++ b/lib/rspec/core/formatters/exception_presenter.rb @@ -313,6 +313,15 @@ def add(exception) other_errors << exception end end + + # Provides a way to force `ex` to be something that satisfies the multiple + # exception error interface. If it already satisfies it, it will be returned; + # otherwise it will wrap it in a `MultipleExceptionError`. + # @private + def self.for(ex) + return ex if self === ex + MultipleExceptionError.new(ex) + end end include InterfaceTag diff --git a/spec/rspec/core/formatters/exception_presenter_spec.rb b/spec/rspec/core/formatters/exception_presenter_spec.rb index a73c3b0a2b..024e3b5d81 100644 --- a/spec/rspec/core/formatters/exception_presenter_spec.rb +++ b/spec/rspec/core/formatters/exception_presenter_spec.rb @@ -434,5 +434,34 @@ def new_multiple_exception_error expected_metadata = ex.exception.aggregation_metadata expect(MultipleExceptionError.new.aggregation_metadata).to eq(expected_metadata) end + + describe "::InterfaceTag.for" do + def value_for(ex) + described_class::InterfaceTag.for(ex) + end + + context "when given an `#{described_class.name}`" do + it 'returns the provided error' do + ex = MultipleExceptionError.new + expect(value_for ex).to be ex + end + end + + context "when given an `RSpec::Expectations::MultipleExpectationsNotMetError`" do + it 'returns the provided error' do + failure_aggregator = RSpec::Expectations::FailureAggregator.new(nil, {}) + ex = RSpec::Expectations::MultipleExpectationsNotMetError.new(failure_aggregator) + + expect(value_for ex).to be ex + end + end + + context "when given any other exception" do + it 'wraps it in a `RSpec::Expectations::MultipleExceptionError`' do + ex = StandardError.new + expect(value_for ex).to be_a(MultipleExceptionError).and have_attributes(:all_exceptions => [ex]) + end + end + end end end From 20adf7bfae712ebf7ac4ae79b1364b2d0c0c9206 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Thu, 4 Jun 2015 23:24:58 -0700 Subject: [PATCH 239/258] Ensure examples with aggregated failures and an additional exception are reported properly. --- .../aggregating_failures.feature | 42 ++-- lib/rspec/core/configuration.rb | 8 +- lib/rspec/core/example.rb | 52 ++++- spec/rspec/core/aggregate_failures_spec.rb | 186 ++++++++++++++---- 4 files changed, 220 insertions(+), 68 deletions(-) diff --git a/features/expectation_framework_integration/aggregating_failures.feature b/features/expectation_framework_integration/aggregating_failures.feature index f3c88e1596..605b2d6e8f 100644 --- a/features/expectation_framework_integration/aggregating_failures.feature +++ b/features/expectation_framework_integration/aggregating_failures.feature @@ -99,12 +99,16 @@ Feature: Aggregating Failures require 'client' RSpec.describe Client do - it "returns a successful response", :aggregate_failures do + it "follows a redirect", :aggregate_failures do response = Client.make_request - expect(response.status).to eq(200) - expect(response.headers).to include("Content-Type" => "application/json") - expect(response.body).to eq('{"message":"Success"}') + expect(response.status).to eq(302) + expect(response.body).to eq('{"message":"Redirect"}') + + redirect_response = Client.make_request(response.headers.fetch('Location')) + + expect(redirect_response.status).to eq(200) + expect(redirect_response.body).to eq('{"message":"OK"}') end end """ @@ -113,32 +117,30 @@ Feature: Aggregating Failures """ Failures: - 1) Client returns a successful response - Got 3 failures: + 1) Client follows a redirect + Got 2 failures and 1 other error: - 1.1) Failure/Error: expect(response.status).to eq(200) + 1.1) Failure/Error: expect(response.status).to eq(302) - expected: 200 + expected: 302 got: 404 (compared using ==) - # ./spec/use_metadata_spec.rb:7 + # ./spec/use_metadata_spec.rb:7:in `block (2 levels) in ' - 1.2) Failure/Error: expect(response.headers).to include("Content-Type" => "application/json") - expected {"Content-Type" => "text/plain"} to include {"Content-Type" => "application/json"} - Diff: - @@ -1,2 +1,2 @@ - -[{"Content-Type"=>"application/json"}] - +"Content-Type" => "text/plain", - # ./spec/use_metadata_spec.rb:8 - - 1.3) Failure/Error: expect(response.body).to eq('{"message":"Success"}') + 1.2) Failure/Error: expect(response.body).to eq('{"message":"Redirect"}') - expected: "{\"message\":\"Success\"}" + expected: "{\"message\":\"Redirect\"}" got: "Not Found" (compared using ==) - # ./spec/use_metadata_spec.rb:9 + # ./spec/use_metadata_spec.rb:8:in `block (2 levels) in ' + + 1.3) Failure/Error: redirect_response = Client.make_request(response.headers.fetch('Location')) + KeyError: + key not found: "Location" + # ./spec/use_metadata_spec.rb:10:in `fetch' + # ./spec/use_metadata_spec.rb:10:in `block (2 levels) in ' """ Scenario: Enable failure aggregation globally using `define_derived_metadata` diff --git a/lib/rspec/core/configuration.rb b/lib/rspec/core/configuration.rb index 491e8f1f33..2834d75797 100644 --- a/lib/rspec/core/configuration.rb +++ b/lib/rspec/core/configuration.rb @@ -1742,8 +1742,12 @@ def value_for(key) end def define_built_in_hooks - around(:example, :aggregate_failures => true) do |ex| - aggregate_failures(nil, :hide_backtrace => true, &ex) + around(:example, :aggregate_failures => true) do |procsy| + begin + aggregate_failures(nil, :hide_backtrace => true, &procsy) + rescue Exception => exception + procsy.example.set_aggregate_failures_exception(exception) + end end end diff --git a/lib/rspec/core/example.rb b/lib/rspec/core/example.rb index 49733a8ecb..81b84bbd33 100644 --- a/lib/rspec/core/example.rb +++ b/lib/rspec/core/example.rb @@ -302,24 +302,55 @@ def inspect end end + # @private + # + # The exception that will be displayed to the user -- either the failure of + # the example or the `pending_exception` if the example is pending. + def display_exception + @exception || execution_result.pending_exception + end + + # @private + # + # Assigns the exception that will be displayed to the user -- either the failure of + # the example or the `pending_exception` if the example is pending. + def display_exception=(ex) + if pending? && !(Pending::PendingExampleFixedError === ex) + execution_result.pending_exception = ex + else + @exception = ex + end + end + + # rubocop:disable Style/AccessorMethodName + # @private # # Used internally to set an exception in an after hook, which # captures the exception but doesn't raise it. def set_exception(exception) - if pending? && !(Pending::PendingExampleFixedError === exception) - execution_result.pending_exception = exception - elsif @exception - unless RSpec::Core::MultipleExceptionError === @exception - @exception = RSpec::Core::MultipleExceptionError.new(@exception) - end + return self.display_exception = exception unless display_exception - @exception.add exception - else - @exception = exception + unless RSpec::Core::MultipleExceptionError === display_exception + self.display_exception = RSpec::Core::MultipleExceptionError.new(display_exception) end + + display_exception.add exception + end + + # @private + # + # Used to set the exception when `aggregate_failures` fails. + def set_aggregate_failures_exception(exception) + return set_exception(exception) unless display_exception + + exception = RSpec::Core::MultipleExceptionError::InterfaceTag.for(exception) + exception.add display_exception + self.display_exception = exception end + # rubocop:enable Style/AccessorMethodName + # @private # # Used internally to set an exception and fail without actually executing @@ -529,10 +560,13 @@ def initialize super(AnonymousExampleGroup, "", {}) end + # rubocop:disable Style/AccessorMethodName + # To ensure we don't silence errors. def set_exception(exception) raise exception end + # rubocop:enable Style/AccessorMethodName end end end diff --git a/spec/rspec/core/aggregate_failures_spec.rb b/spec/rspec/core/aggregate_failures_spec.rb index 85bec9eddd..4271bb5ac7 100644 --- a/spec/rspec/core/aggregate_failures_spec.rb +++ b/spec/rspec/core/aggregate_failures_spec.rb @@ -1,51 +1,163 @@ -RSpec.describe "Using `:aggregate_failures` metadata" do - it 'applies `aggregate_failures` to examples or groups tagged with `:aggregate_failures`' do - ex = nil - - RSpec.describe "Aggregate failures", :aggregate_failures do - ex = it "has multiple failures" do - expect(1).to be_even - expect(2).to be_odd +RSpec.describe "Aggregating failures" do + shared_examples_for "failure aggregation" do |exception_attribute, example_meta| + context "via the `aggregate_failures` method" do + context 'when the example has an expectation failure, plus an `after` hook and an `around` hook failure' do + it 'presents a flat list of three failures' do + ex = nil + + RSpec.describe do + ex = example "ex", example_meta do + aggregate_failures { expect(1).to be_even } + end + after { raise "after" } + around { |example| example.run; raise "around" } + end.run + + expect(ex.execution_result.__send__(exception_attribute)).to have_attributes( + :all_exceptions => [ + an_object_having_attributes(:message => 'expected `1.even?` to return true, got false'), + an_object_having_attributes(:message => 'after'), + an_object_having_attributes(:message => 'around') + ] + ) + end end - end.run - - expect(ex.execution_result.exception).to have_attributes( - :failures => [ - an_object_having_attributes(:message => 'expected `1.even?` to return true, got false'), - an_object_having_attributes(:message => 'expected `2.odd?` to return true, got false') - ] - ) - end - it 'does not interfere with other `around` hooks' do - events = [] + context 'when the example has multiple expectation failures, plus an `after` hook and an `around` hook failure' do + it 'nests the expectation failures so that they can be labeled with the aggregation block label' do + ex = nil - RSpec.describe "Outer" do - around do |ex| - events << :outer_before - ex.run - events << :outer_after + RSpec.describe do + ex = example "ex", example_meta do + aggregate_failures do + expect(1).to be_even + expect(2).to be_odd + end + end + after { raise "after" } + around { |example| example.run; raise "around" } + end.run + + exception = ex.execution_result.__send__(exception_attribute) + + expect(exception).to have_attributes( + :all_exceptions => [ + an_object_having_attributes(:class => RSpec::Expectations::MultipleExpectationsNotMetError), + an_object_having_attributes(:message => 'after'), + an_object_having_attributes(:message => 'around') + ] + ) + + expect(exception.all_exceptions.first.all_exceptions).to match [ + an_object_having_attributes(:message => 'expected `1.even?` to return true, got false'), + an_object_having_attributes(:message => 'expected `2.odd?` to return true, got false') + ] + end end + end - context "aggregating failures", :aggregate_failures do - context "inner" do - around do |ex| - events << :inner_before - ex.run - events << :inner_after - end + context "via `:aggregate_failures` metadata" do + it 'applies `aggregate_failures` to examples or groups tagged with `:aggregate_failures`' do + pending "Not yet working with pending examples" if example_meta.key?(:pending) + + ex = nil - it "has multiple failures" do - events << :example_before + RSpec.describe "Aggregate failures", :aggregate_failures do + ex = it "has multiple failures", example_meta do expect(1).to be_even expect(2).to be_odd - events << :example_after end + end.run + + expect(ex.execution_result.__send__(exception_attribute)).to have_attributes( + :failures => [ + an_object_having_attributes(:message => 'expected `1.even?` to return true, got false'), + an_object_having_attributes(:message => 'expected `2.odd?` to return true, got false') + ] + ) + end + + context 'when the example has an exception, plus another error' do + it 'reports it as a multiple exception error' do + ex = nil + + RSpec.describe "Aggregate failures", :aggregate_failures do + ex = example "fail and raise", example_meta do + expect(1).to be_even + boom + end + end.run + + expect(ex.execution_result.__send__(exception_attribute)).to have_attributes( + :all_exceptions => [ + an_object_having_attributes(:message => 'expected `1.even?` to return true, got false'), + an_object_having_attributes(:class => NameError, :message => /boom/) + ] + ) end end - end.run - expect(events).to eq([:outer_before, :inner_before, :example_before, - :example_after, :inner_after, :outer_after]) + context 'when the example has multiple exceptions, plus another error' do + it 'reports it as a flat multiple exception error' do + ex = nil + + RSpec.describe "Aggregate failures", :aggregate_failures do + ex = example "fail and raise", example_meta do + expect(1).to be_even + expect(2).to be_odd + boom + end + end.run + + expect(ex.execution_result.__send__(exception_attribute)).to have_attributes( + :all_exceptions => [ + an_object_having_attributes(:message => 'expected `1.even?` to return true, got false'), + an_object_having_attributes(:message => 'expected `2.odd?` to return true, got false'), + an_object_having_attributes(:class => NameError, :message => /boom/) + ] + ) + end + end + end + end + + context "for a non-pending example" do + include_examples "failure aggregation", :exception, {} + + it 'does not interfere with other `around` hooks' do + events = [] + + RSpec.describe "Outer" do + around do |ex| + events << :outer_before + ex.run + events << :outer_after + end + + context "aggregating failures", :aggregate_failures do + context "inner" do + around do |ex| + events << :inner_before + ex.run + events << :inner_after + end + + it "has multiple failures" do + events << :example_before + expect(1).to be_even + expect(2).to be_odd + events << :example_after + end + end + end + end.run + + expect(events).to eq([:outer_before, :inner_before, :example_before, + :example_after, :inner_after, :outer_after]) + end + end + + context "for a pending example" do + include_examples "failure aggregation", :pending_exception, :pending => true end end From 96698b85633fa7ff55052192f93336ddf217b642 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 5 Jun 2015 13:35:45 -0700 Subject: [PATCH 240/258] Make `aggregate_failures` play nice with `pending`. --- .../aggregating_failures.feature | 50 +++++++++++++++++++ .../step_definitions/additional_cli_steps.rb | 5 ++ lib/rspec/core/example.rb | 10 ++-- .../core/formatters/exception_presenter.rb | 10 ++++ spec/rspec/core/aggregate_failures_spec.rb | 6 +-- .../formatters/exception_presenter_spec.rb | 10 ++++ 6 files changed, 81 insertions(+), 10 deletions(-) diff --git a/features/expectation_framework_integration/aggregating_failures.feature b/features/expectation_framework_integration/aggregating_failures.feature index 605b2d6e8f..124852f1e1 100644 --- a/features/expectation_framework_integration/aggregating_failures.feature +++ b/features/expectation_framework_integration/aggregating_failures.feature @@ -300,3 +300,53 @@ Feature: Aggregating Failures # ./spec/mock_expectation_failure_spec.rb:6 """ + + Scenario: Pending integrates properly with aggregated failures + Given a file named "spec/pending_spec.rb" with: + """ruby + require 'client' + + RSpec.describe Client do + it "returns a successful response", :aggregate_failures do + pending "Not yet ready" + response = Client.make_request + + expect(response.status).to eq(200) + expect(response.headers).to include("Content-Type" => "application/json") + expect(response.body).to eq('{"message":"Success"}') + end + end + """ + When I run `rspec spec/pending_spec.rb` + Then it should pass and list all the pending examples: + """ + Pending: (Failures listed here are expected and do not affect your suite's status) + + 1) Client returns a successful response + # Not yet ready + Got 3 failures: + + 1.1) Failure/Error: expect(response.status).to eq(200) + + expected: 200 + got: 404 + + (compared using ==) + # ./spec/pending_spec.rb:8:in `block (2 levels) in ' + + 1.2) Failure/Error: expect(response.headers).to include("Content-Type" => "application/json") + expected {"Content-Type" => "text/plain"} to include {"Content-Type" => "application/json"} + Diff: + @@ -1,2 +1,2 @@ + -[{"Content-Type"=>"application/json"}] + +"Content-Type" => "text/plain", + # ./spec/pending_spec.rb:9:in `block (2 levels) in ' + + 1.3) Failure/Error: expect(response.body).to eq('{"message":"Success"}') + + expected: "{\"message\":\"Success\"}" + got: "Not Found" + + (compared using ==) + # ./spec/pending_spec.rb:10:in `block (2 levels) in ' + """ diff --git a/features/step_definitions/additional_cli_steps.rb b/features/step_definitions/additional_cli_steps.rb index a3fc8b4cf3..5171206fc6 100644 --- a/features/step_definitions/additional_cli_steps.rb +++ b/features/step_definitions/additional_cli_steps.rb @@ -192,6 +192,11 @@ expect(normalize_whitespace_and_backtraces(all_output)).to include(normalize_whitespace_and_backtraces(string)) end +Then(/^it should pass and list all the pending examples:$/) do |string| + step %q{the exit status should be 0} + expect(normalize_whitespace_and_backtraces(all_output)).to include(normalize_whitespace_and_backtraces(string)) +end + module WhitespaceNormalization def normalize_whitespace_and_backtraces(text) text.lines.map { |line| line.sub(/\s+$/, '').sub(/:in .*$/, '') }.join diff --git a/lib/rspec/core/example.rb b/lib/rspec/core/example.rb index 81b84bbd33..08efab62a3 100644 --- a/lib/rspec/core/example.rb +++ b/lib/rspec/core/example.rb @@ -316,6 +316,8 @@ def display_exception # the example or the `pending_exception` if the example is pending. def display_exception=(ex) if pending? && !(Pending::PendingExampleFixedError === ex) + @exception = nil + execution_result.pending_fixed = false execution_result.pending_exception = ex else @exception = ex @@ -441,13 +443,7 @@ def run_after_example def verify_mocks @example_group_instance.verify_mocks_for_rspec rescue Exception => e - if pending? - execution_result.pending_fixed = false - execution_result.pending_exception = e - @exception = nil - else - set_exception(e) - end + set_exception(e) end def assign_generated_description diff --git a/lib/rspec/core/formatters/exception_presenter.rb b/lib/rspec/core/formatters/exception_presenter.rb index 053aeafb01..9c6fbf1efb 100644 --- a/lib/rspec/core/formatters/exception_presenter.rb +++ b/lib/rspec/core/formatters/exception_presenter.rb @@ -305,6 +305,16 @@ module InterfaceTag # @param exception [Exception] Exception to append to the list. # @private def add(exception) + # `PendingExampleFixedError` can be assigned to an example that initially has no + # failures, but when the `aggregate_failures` around hook completes, it notifies of + # a failure. If we do not ignore `PendingExampleFixedError` it would be surfaced to + # the user as part of a multiple exception error, which is undesirable. While it's + # pretty weird we handle this here, it's the best solution I've been able to come + # up with, and `PendingExampleFixedError` always represents the _lack_ of any exception + # so clearly when we are transitioning to a `MultipleExceptionError`, it makes sense to + # ignore it. + return if Pending::PendingExampleFixedError === exception + all_exceptions << exception if exception.class.name =~ /RSpec/ diff --git a/spec/rspec/core/aggregate_failures_spec.rb b/spec/rspec/core/aggregate_failures_spec.rb index 4271bb5ac7..df37c15401 100644 --- a/spec/rspec/core/aggregate_failures_spec.rb +++ b/spec/rspec/core/aggregate_failures_spec.rb @@ -58,8 +58,6 @@ context "via `:aggregate_failures` metadata" do it 'applies `aggregate_failures` to examples or groups tagged with `:aggregate_failures`' do - pending "Not yet working with pending examples" if example_meta.key?(:pending) - ex = nil RSpec.describe "Aggregate failures", :aggregate_failures do @@ -69,8 +67,10 @@ end end.run + expect(ex.execution_result).not_to be_pending_fixed + expect(ex.execution_result.status).to eq(:pending) if example_meta.key?(:pending) expect(ex.execution_result.__send__(exception_attribute)).to have_attributes( - :failures => [ + :all_exceptions => [ an_object_having_attributes(:message => 'expected `1.even?` to return true, got false'), an_object_having_attributes(:message => 'expected `2.odd?` to return true, got false') ] diff --git a/spec/rspec/core/formatters/exception_presenter_spec.rb b/spec/rspec/core/formatters/exception_presenter_spec.rb index 024e3b5d81..0fb0e51618 100644 --- a/spec/rspec/core/formatters/exception_presenter_spec.rb +++ b/spec/rspec/core/formatters/exception_presenter_spec.rb @@ -338,6 +338,16 @@ def new_error(*a) }.to change(mee.other_errors, :count).by 1 end + it 'ignores `Pending::PendingExampleFixedError` since it does not represent a real failure but rather the lack of one' do + mee = new_multiple_exception_error + + expect { + mee.add Pending::PendingExampleFixedError.new + }.to avoid_changing(mee.other_errors, :count). + and avoid_changing(mee.all_exceptions, :count). + and avoid_changing(mee.failures, :count) + end + it 'is tagged with a common module so it is clear it has the interface for multiple exceptions' do expect(MultipleExceptionError::InterfaceTag).to be === new_multiple_exception_error end From d83d7ee18f2b79ff28d590eedc6b01592a27da46 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 5 Jun 2015 14:10:52 -0700 Subject: [PATCH 241/258] Add changelog. --- Changelog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Changelog.md b/Changelog.md index 544bc4f636..61b6a6092d 100644 --- a/Changelog.md +++ b/Changelog.md @@ -51,6 +51,8 @@ Enhancements: messages being lost. (Jon Rowe, #1980) * Profiling examples now takes into account time spent in `before(:context)` hooks. (Denis Laliberté, Jon Rowe, #1971) +* Improve failure output when an example has multiple exceptions, such + as one from an `it` block and one from an `after` block. (Myron Marston, #1985) Bug Fixes: From 10cbea21f05c2730dfcfb81f5784fd1815e36f27 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 5 Jun 2015 17:11:14 -0700 Subject: [PATCH 242/258] Fix typos. --- lib/rspec/core/formatters/exception_presenter.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/rspec/core/formatters/exception_presenter.rb b/lib/rspec/core/formatters/exception_presenter.rb index 9c6fbf1efb..c97fece5e0 100644 --- a/lib/rspec/core/formatters/exception_presenter.rb +++ b/lib/rspec/core/formatters/exception_presenter.rb @@ -196,9 +196,9 @@ def with_multiple_error_options_as_needed(exception, options) options = options.merge( :failure_lines => [], :extra_detail_formatter => sub_failure_list_formatter(exception, options[:message_color]), - :detail_formatter => multiple_exception_sumarizer(exception, - options[:detail_formatter], - options[:message_color]) + :detail_formatter => multiple_exception_summarizer(exception, + options[:detail_formatter], + options[:message_color]) ) options[:description_formatter] &&= Proc.new {} @@ -212,11 +212,11 @@ def multiple_exceptions_error?(exception) MultipleExceptionError::InterfaceTag === exception end - def multiple_exception_sumarizer(exception, prior_detail_formatter, color) + def multiple_exception_summarizer(exception, prior_detail_formatter, color) lambda do |example, colorizer, indentation| summary = if exception.aggregation_metadata[:hide_backtrace] # Since the backtrace is hidden, the subfailures will come - # immeidately after this, and using `:` will read well. + # immediately after this, and using `:` will read well. "Got #{exception.exception_count_description}:" else # The backtrace comes after this, so using a `:` doesn't make sense From 7102fa56f75be5eb7db6b3f883f76d129487a432 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 5 Jun 2015 21:00:40 -0700 Subject: [PATCH 243/258] Make spec more tolerant of taking longer. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For some reason, this failed on Travis: https://travis-ci.org/rspec/rspec-core/jobs/65660471 I’m hoping that increasing it will prevent it from failing again. --- spec/rspec/core/formatters/json_formatter_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/rspec/core/formatters/json_formatter_spec.rb b/spec/rspec/core/formatters/json_formatter_spec.rb index 034305be3c..9953d476cd 100644 --- a/spec/rspec/core/formatters/json_formatter_spec.rb +++ b/spec/rspec/core/formatters/json_formatter_spec.rb @@ -163,7 +163,7 @@ def profile *groups context "with multiple example groups", :slow do before do - example_clock = class_double(RSpec::Core::Time, :now => RSpec::Core::Time.now + 0.5) + example_clock = class_double(RSpec::Core::Time, :now => RSpec::Core::Time.now + 5) group1 = RSpec.describe("slow group") do example("example") do |example| From 2179d99c2b4deb9fafd583a01fde7760dc45b67f Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Sat, 6 Jun 2015 01:10:05 -0700 Subject: [PATCH 244/258] Work around diff `Hash#fetch` errors on diff rubies. --- .../step_definitions/additional_cli_steps.rb | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/features/step_definitions/additional_cli_steps.rb b/features/step_definitions/additional_cli_steps.rb index 5171206fc6..609c1246f7 100644 --- a/features/step_definitions/additional_cli_steps.rb +++ b/features/step_definitions/additional_cli_steps.rb @@ -189,19 +189,25 @@ Then(/^it should fail and list all the failures:$/) do |string| step %q{the exit status should not be 0} - expect(normalize_whitespace_and_backtraces(all_output)).to include(normalize_whitespace_and_backtraces(string)) + expect(normalize_failure_output(all_output)).to include(normalize_failure_output(string)) end Then(/^it should pass and list all the pending examples:$/) do |string| step %q{the exit status should be 0} - expect(normalize_whitespace_and_backtraces(all_output)).to include(normalize_whitespace_and_backtraces(string)) + expect(normalize_failure_output(all_output)).to include(normalize_failure_output(string)) end -module WhitespaceNormalization - def normalize_whitespace_and_backtraces(text) - text.lines.map { |line| line.sub(/\s+$/, '').sub(/:in .*$/, '') }.join +module Normalization + def normalize_failure_output(text) + whitespace_normalized = text.lines.map { |line| line.sub(/\s+$/, '').sub(/:in .*$/, '') }.join + + # 1.8.7 and JRuby produce slightly different output for `Hash#fetch` errors, so we + # convert it to the same output here to match our expectation. + whitespace_normalized. + sub("IndexError", "KeyError"). + sub(/key not found.*$/, "key not found") end end -World(WhitespaceNormalization) +World(Normalization) World(FormatterSupport) From 3dc47ad24fffe745d5bb7fa9e81327b56f4ae05c Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Sat, 6 Jun 2015 12:31:56 -0700 Subject: [PATCH 245/258] =?UTF-8?q?Work=20around=20travis=20spec=20failure?= =?UTF-8?q?=20on=20MRI=202.0.0=20that=20I=20can=E2=80=99t=20repro.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/rspec/core/formatters/json_formatter_spec.rb | 10 +++++++++- spec/spec_helper.rb | 2 ++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/spec/rspec/core/formatters/json_formatter_spec.rb b/spec/rspec/core/formatters/json_formatter_spec.rb index 9953d476cd..b97841208b 100644 --- a/spec/rspec/core/formatters/json_formatter_spec.rb +++ b/spec/rspec/core/formatters/json_formatter_spec.rb @@ -186,7 +186,15 @@ def profile *groups expect(formatter.output_hash[:profile][:groups].first.keys).to match_array([:total_time, :count, :description, :average, :location, :start]) end - it "ranks the example groups by average time" do + it "ranks the example groups by average time" do |ex| + if ENV['TRAVIS'] && RSpec::Support::Ruby.mri? && RUBY_VERSION == '2.0.0' && + $original_rspec_configuration.loaded_spec_files.to_a == [File.expand_path(__FILE__)] + RSpec.current_example = ex # necessary due to sandboxing so that `skip` works. + skip "This spec fails on Travis on MRI 2.0.0 only when this spec file runs " \ + "individually. It passes on Travis when run as part of the entire suite " \ + "and I can't repro locally, so we are skipping it for this case." + end + expect(formatter.output_hash[:profile][:groups].first[:description]).to eq("slow group") end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 16f5019242..629f13add2 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -109,4 +109,6 @@ def handle_current_dir_change !(RUBY_VERSION.to_s =~ /^#{version.to_s}/) end } + + $original_rspec_configuration = c end From e4d9484ff0bf3ec1081a5bf943da901d5d883ad8 Mon Sep 17 00:00:00 2001 From: Yule Date: Mon, 8 Jun 2015 11:06:44 +0100 Subject: [PATCH 246/258] remove whitespace --- spec/rspec/core/formatters/helpers_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/rspec/core/formatters/helpers_spec.rb b/spec/rspec/core/formatters/helpers_spec.rb index f0bcd952a3..c789662b45 100644 --- a/spec/rspec/core/formatters/helpers_spec.rb +++ b/spec/rspec/core/formatters/helpers_spec.rb @@ -45,7 +45,7 @@ expect(helper.format_duration(1)).to eq("1 second") end end - + context '= 70' do it "returns 'x minute, x0 seconds' formatted string" do expect(helper.format_duration(70)).to eq("1 minute 10 seconds") @@ -91,7 +91,7 @@ context "70" do it "doesn't strip of meaningful trailing zeros" do - expect(helper.format_seconds(70)).to eq("70") + expect(helper.format_seconds(70)).to eq("70") end end end From 93dc03b356e036bd43c4aa466a442dc430405b82 Mon Sep 17 00:00:00 2001 From: Yule Date: Mon, 8 Jun 2015 11:18:04 +0100 Subject: [PATCH 247/258] link to rubular --- lib/rspec/core/formatters/helpers.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/rspec/core/formatters/helpers.rb b/lib/rspec/core/formatters/helpers.rb index 94bfbd45cb..3d3b0f58d1 100644 --- a/lib/rspec/core/formatters/helpers.rb +++ b/lib/rspec/core/formatters/helpers.rb @@ -67,6 +67,9 @@ def self.format_seconds(float, precision=nil) # # Remove trailing zeros from a string. # + # Only remove trailing zeros after a decimal place. + # see: http://rubular.com/r/ojtTydOgpn + # # @param string [String] string with trailing zeros # @return [String] string with trailing zeros removed def self.strip_trailing_zeroes(string) From a36a1332ce2d6be26aaaecc92666a66afe19ba32 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Mon, 8 Jun 2015 10:23:21 -0700 Subject: [PATCH 248/258] Add changelog for #1984. [ci skip] --- Changelog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Changelog.md b/Changelog.md index 61b6a6092d..da17c6c331 100644 --- a/Changelog.md +++ b/Changelog.md @@ -68,6 +68,8 @@ Bug Fixes: prepended or extended by config defined after an example group. (Eugene Kenny, #1935) * Fix regression which caused shared examples to be mistakenly run when specs where filtered to a particular location. (Ben Axnick, #1963) +* Fix time formatting logic so that it displays 70 seconds as "1 minute, + 10 seconds" rather than "1 minute, 1 second". (Paul Brennan, #1984) ### 3.2.3 / 2015-04-06 [Full Changelog](http://github.com/rspec/rspec-core/compare/v3.2.2...v3.2.3) From 5778abd5d389d7a20592b047d8f967951838b89d Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Tue, 9 Jun 2015 07:28:33 -0700 Subject: [PATCH 249/258] Fix typo: reponse -> response. --- .../aggregating_failures.feature | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/expectation_framework_integration/aggregating_failures.feature b/features/expectation_framework_integration/aggregating_failures.feature index 124852f1e1..aa0d294029 100644 --- a/features/expectation_framework_integration/aggregating_failures.feature +++ b/features/expectation_framework_integration/aggregating_failures.feature @@ -39,7 +39,7 @@ Feature: Aggregating Failures it "returns a successful response" do response = Client.make_request - aggregate_failures "testing reponse" do + aggregate_failures "testing response" do expect(response.status).to eq(200) expect(response.headers).to include("Content-Type" => "application/json") expect(response.body).to eq('{"message":"Success"}') @@ -55,7 +55,7 @@ Feature: Aggregating Failures 1) Client returns a successful response Got 3 failures: - 1.1) Got 3 failures from failure aggregation block "testing reponse". + 1.1) Got 3 failures from failure aggregation block "testing response". # ./spec/use_block_form_spec.rb:18:in `block (2 levels) in ' # ./spec/use_block_form_spec.rb:10:in `block (2 levels) in ' From 11a268fd71ee6216840efdf1406dfebb7b801856 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Wed, 10 Jun 2015 08:48:12 -0700 Subject: [PATCH 250/258] Revert "Always verify mocks at the end of each example." This reverts commit f989168c9dbbcd930af6e7cc61e08057194929a8. This is necessary to avoid the issue discussed in rspec/rspec-mocks#203. I'm reverting it so we can release 3.3. We may bring this back for 3.4 if we can find a better solution. --- lib/rspec/core/example.rb | 6 +++++- spec/rspec/core/example_spec.rb | 33 +++++++++++++++------------------ 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/lib/rspec/core/example.rb b/lib/rspec/core/example.rb index 08efab62a3..dc820495d4 100644 --- a/lib/rspec/core/example.rb +++ b/lib/rspec/core/example.rb @@ -441,11 +441,15 @@ def run_after_example end def verify_mocks - @example_group_instance.verify_mocks_for_rspec + @example_group_instance.verify_mocks_for_rspec if mocks_need_verification? rescue Exception => e set_exception(e) end + def mocks_need_verification? + exception.nil? || execution_result.pending_fixed? + end + def assign_generated_description if metadata[:description].empty? && (description = generate_description) metadata[:description] = description diff --git a/spec/rspec/core/example_spec.rb b/spec/rspec/core/example_spec.rb index b91bce7089..ebabc734d5 100644 --- a/spec/rspec/core/example_spec.rb +++ b/spec/rspec/core/example_spec.rb @@ -723,35 +723,32 @@ def expect_pending_result(example) expect(ex).to fail_with(RSpec::Mocks::MockExpectationError) end - context "when the example has already failed" do - it 'appends the mock error to a `MultipleExceptionError` so the user can see both' do - ex = nil - boom = StandardError.new("boom") - - RSpec.describe do - ex = example do - dbl = double - expect(dbl).to receive(:Foo) - raise boom - end - end.run + it 'skips mock verification if the example has already failed' do + ex = nil + boom = StandardError.new("boom") - expect(ex.exception).to be_a(RSpec::Core::MultipleExceptionError) - expect(ex.exception.all_exceptions).to match [boom, an_instance_of(RSpec::Mocks::MockExpectationError)] - end + RSpec.describe do + ex = example do + dbl = double + expect(dbl).to receive(:Foo) + raise boom + end + end.run + + expect(ex.exception).to be boom end it 'allows `after(:example)` hooks to satisfy mock expectations, since examples are not complete until their `after` hooks run' do ex = nil RSpec.describe do - let(:the_dbl) { double } + let(:dbl) { double } ex = example do - expect(the_dbl).to receive(:foo) + expect(dbl).to receive(:foo) end - after { the_dbl.foo } + after { dbl.foo } end.run expect(ex).to pass From 29019a869910ed165eefe2007b1cfe8f3a16f809 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Wed, 10 Jun 2015 10:18:12 -0700 Subject: [PATCH 251/258] Address ruby 2.2.0 warnings. warning: possible reference to past scope - dbl --- spec/rspec/core/example_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/rspec/core/example_spec.rb b/spec/rspec/core/example_spec.rb index ebabc734d5..bdb612ad06 100644 --- a/spec/rspec/core/example_spec.rb +++ b/spec/rspec/core/example_spec.rb @@ -742,13 +742,13 @@ def expect_pending_result(example) ex = nil RSpec.describe do - let(:dbl) { double } + let(:the_dbl) { double } ex = example do - expect(dbl).to receive(:foo) + expect(the_dbl).to receive(:foo) end - after { dbl.foo } + after { the_dbl.foo } end.run expect(ex).to pass From 4455319b2023ffe3e2585cea7517966763df0155 Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Thu, 11 Jun 2015 14:57:08 +1000 Subject: [PATCH 252/258] prevent formater loader registering duplicate listeners --- Changelog.md | 2 ++ lib/rspec/core/formatters.rb | 14 +++++++++----- spec/rspec/core/formatters_spec.rb | 11 ++++++++++- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/Changelog.md b/Changelog.md index da17c6c331..d68918c1b3 100644 --- a/Changelog.md +++ b/Changelog.md @@ -70,6 +70,8 @@ Bug Fixes: where filtered to a particular location. (Ben Axnick, #1963) * Fix time formatting logic so that it displays 70 seconds as "1 minute, 10 seconds" rather than "1 minute, 1 second". (Paul Brennan, #1984) +* Fix regression where the formatter loader would allow duplicate formatters. + (Jon Rowe, #1990) ### 3.2.3 / 2015-04-06 [Full Changelog](http://github.com/rspec/rspec-core/compare/v3.2.2...v3.2.3) diff --git a/lib/rspec/core/formatters.rb b/lib/rspec/core/formatters.rb index 492af2291b..368825b39a 100644 --- a/lib/rspec/core/formatters.rb +++ b/lib/rspec/core/formatters.rb @@ -143,10 +143,10 @@ def add(formatter_to_use, *paths) if !Loader.formatters[formatter_class].nil? formatter = formatter_class.new(*args) - @reporter.register_listener formatter, *notifications_for(formatter_class) + register formatter, notifications_for(formatter_class) elsif defined?(RSpec::LegacyFormatters) formatter = RSpec::LegacyFormatters.load_formatter formatter_class, *args - @reporter.register_listener formatter, *formatter.notifications + register formatter, formatter.notifications else call_site = "Formatter added at: #{::RSpec::CallerFilter.first_non_rspec_line}" @@ -161,10 +161,7 @@ def add(formatter_to_use, *paths) | |#{call_site} WARNING - return end - @formatters << formatter unless duplicate_formatter_exists?(formatter) - formatter end private @@ -176,6 +173,13 @@ def find_formatter(formatter_to_use) "maybe you meant 'documentation' or 'progress'?.") end + def register(formatter, notifications) + return if duplicate_formatter_exists?(formatter) + @reporter.register_listener formatter, *notifications + @formatters << formatter + formatter + end + def duplicate_formatter_exists?(new_formatter) @formatters.any? do |formatter| formatter.class === new_formatter && formatter.output == new_formatter.output diff --git a/spec/rspec/core/formatters_spec.rb b/spec/rspec/core/formatters_spec.rb index ae96a27fb7..6c3aa5eb23 100644 --- a/spec/rspec/core/formatters_spec.rb +++ b/spec/rspec/core/formatters_spec.rb @@ -41,7 +41,7 @@ module RSpec::Core::Formatters context "when a legacy formatter is added with RSpec::LegacyFormatters" do formatter_class = Struct.new(:output) - let(:formatter) { double "formatter", :notifications => notifications } + let(:formatter) { double "formatter", :notifications => notifications, :output => output } let(:notifications) { [:a, :b, :c] } before do @@ -57,6 +57,14 @@ module RSpec::Core::Formatters expect(reporter).to receive(:register_listener).with(formatter, *notifications) loader.add formatter_class, output end + + it "will ignore duplicate legacy formatters" do + loader.add formatter_class, output + expect(reporter).to_not receive(:register_listener) + expect { + loader.add formatter_class, output + }.not_to change { loader.formatters.length } + end end context "when a legacy formatter is added without RSpec::LegacyFormatters" do @@ -117,6 +125,7 @@ module RSpec::Core::Formatters before { loader.add :documentation, output } it "doesn't add the formatter for the same output target" do + expect(reporter).to_not receive(:register_listener) expect { loader.add :documentation, output }.not_to change { loader.formatters.length } From 31c18494b747883455576acecf9200b2cf5d7695 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 12 Jun 2015 00:39:51 -0700 Subject: [PATCH 253/258] =?UTF-8?q?Remove=20spec=20that=20doesn=E2=80=99t?= =?UTF-8?q?=20belong=20here.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This isn’t even exercising any of the profile formatter code! --- spec/rspec/core/formatters/profile_formatter_spec.rb | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/spec/rspec/core/formatters/profile_formatter_spec.rb b/spec/rspec/core/formatters/profile_formatter_spec.rb index 0d0527b00d..6df93b6177 100644 --- a/spec/rspec/core/formatters/profile_formatter_spec.rb +++ b/spec/rspec/core/formatters/profile_formatter_spec.rb @@ -83,17 +83,5 @@ def profile *groups expect(formatter_output.string).to match(/slow group(.*)fast group/m) end end - - it "depends on parent_groups to get the top level example group" do - ex = nil - group = RSpec.describe - group.describe("group 2") do - describe "group 3" do - ex = example("nested example 1") - end - end - - expect(ex.example_group.parent_groups.last).to eq(group) - end end end From a05496b77de40f678fa24f4160774d1f5c4248e7 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 12 Jun 2015 00:41:25 -0700 Subject: [PATCH 254/258] Use shared example group like it looks like it was intended. This shared group was only included at one place in this file but it looks like there were two intended inclusions. This fixes that. --- spec/rspec/core/formatters/profile_formatter_spec.rb | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/spec/rspec/core/formatters/profile_formatter_spec.rb b/spec/rspec/core/formatters/profile_formatter_spec.rb index 6df93b6177..2b313f112a 100644 --- a/spec/rspec/core/formatters/profile_formatter_spec.rb +++ b/spec/rspec/core/formatters/profile_formatter_spec.rb @@ -31,10 +31,6 @@ def profile *groups it "prints the percentage taken from the total runtime" do expect(formatter_output.string).to match(/, 100.0% of total time\):/) end - - it "doesn't profile a single example group" do - expect(formatter_output.string).not_to match(/slowest example groups/) - end end context "with one example group" do @@ -51,6 +47,10 @@ def profile *groups end it_should_behave_like "profiles examples" + + it "doesn't profile a single example group" do + expect(formatter_output.string).not_to match(/slowest example groups/) + end end context "with multiple example groups" do @@ -62,7 +62,7 @@ def profile *groups # make it look slow without actually taking up precious time example.clock = example_clock end - example_line_number = __LINE__ - 4 + example_line_number = __LINE__ - 4 end group2 = RSpec.describe("fast group") do example("example 1") { } @@ -71,6 +71,8 @@ def profile *groups profile group1, group2 end + it_should_behave_like "profiles examples" + it "prints the slowest example groups" do expect(formatter_output.string).to match(/slowest example groups/) end From bc11555d2729e761f730a6db0de9babf58ed1b7d Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 12 Jun 2015 00:50:04 -0700 Subject: [PATCH 255/258] Fix group profile output regressions introduced in #1971. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Regressions fixed by this commit: - We only considered top-level groups before, but #1971 made it consider nested groups as well. However, the example count was only incremented on the top-level group, which meant the output said “Inf seconds” since `float / 0` returns `Infinity`. - We were wrongly listing the example group class name rather than the location. This is because we changed from locations to groups in the hash. Fixes #1989. --- features/configuration/profile.feature | 9 +++-- .../step_definitions/additional_cli_steps.rb | 24 ++++++++++++++ lib/rspec/core/example_group.rb | 5 +++ lib/rspec/core/notifications.rb | 3 +- lib/rspec/core/profiler.rb | 4 +++ .../core/formatters/profile_formatter_spec.rb | 5 +++ spec/rspec/core/profiler_spec.rb | 33 +++++++++++++------ 7 files changed, 69 insertions(+), 14 deletions(-) diff --git a/features/configuration/profile.feature b/features/configuration/profile.feature index 47d0ec7d7b..7d2d6d1677 100644 --- a/features/configuration/profile.feature +++ b/features/configuration/profile.feature @@ -226,8 +226,11 @@ Feature: Profile examples before(:context) do sleep 0.2 end - it "example" do - expect(10).to eq(10) + + context "nested" do + it "example" do + expect(10).to eq(10) + end end end @@ -239,4 +242,4 @@ Feature: Profile examples end """ When I run `rspec spec --profile 1` - Then the output should contain "slow before context hook" + Then the output should report "slow before context hook" as the slowest example group diff --git a/features/step_definitions/additional_cli_steps.rb b/features/step_definitions/additional_cli_steps.rb index 609c1246f7..4d7da1b465 100644 --- a/features/step_definitions/additional_cli_steps.rb +++ b/features/step_definitions/additional_cli_steps.rb @@ -197,6 +197,30 @@ expect(normalize_failure_output(all_output)).to include(normalize_failure_output(string)) end +Then(/^the output should report "slow before context hook" as the slowest example group$/) do + # These expectations are trying to guard against a regression that introduced + # this output: + # Top 1 slowest example groups: + # slow before context hook + # Inf seconds average (0.00221 seconds / 0 examples) RSpec::ExampleGroups::SlowBeforeContextHook::Nested + # + # Problems: + # - "Inf seconds" + # - 0 examples + # - "Nested" group listed (it should be the outer group) + # - The example group class name is listed (it should be the location) + + expect(all_output).not_to match(/nested/i) + expect(all_output).not_to match(/inf/i) + expect(all_output).not_to match(/\b0 examples/i) + + seconds = '\d+(?:\.\d+)? seconds' + + expect(all_output).to match( + %r{Top 1 slowest example groups?:\n\s+slow before context hook\n\s+#{seconds} average \(#{seconds} / 1 example\) \./spec/example_spec\.rb:1} + ) +end + module Normalization def normalize_failure_output(text) whitespace_normalized = text.lines.map { |line| line.sub(/\s+$/, '').sub(/:in .*$/, '') }.join diff --git a/lib/rspec/core/example_group.rb b/lib/rspec/core/example_group.rb index 138151f5cc..97529553ea 100644 --- a/lib/rspec/core/example_group.rb +++ b/lib/rspec/core/example_group.rb @@ -436,6 +436,11 @@ def self.parent_groups @parent_groups ||= ancestors.select { |a| a < RSpec::Core::ExampleGroup } end + # @private + def self.top_level? + superclass == ExampleGroup + end + # @private def self.ensure_example_groups_are_configured unless defined?(@@example_groups_configured) diff --git a/lib/rspec/core/notifications.rb b/lib/rspec/core/notifications.rb index b3d1c6b55b..25f0faae90 100644 --- a/lib/rspec/core/notifications.rb +++ b/lib/rspec/core/notifications.rb @@ -450,7 +450,8 @@ def calculate_slowest_groups hash[:average] = hash[:total_time].to_f / hash[:count] end - @example_groups.sort_by { |_, hash| -hash[:average] }.first(number_of_examples) + groups = @example_groups.sort_by { |_, hash| -hash[:average] }.first(number_of_examples) + groups.map { |group, data| [group.location, data] } end end diff --git a/lib/rspec/core/profiler.rb b/lib/rspec/core/profiler.rb index ff5373be0d..afe7731105 100644 --- a/lib/rspec/core/profiler.rb +++ b/lib/rspec/core/profiler.rb @@ -11,11 +11,15 @@ def initialize attr_reader :example_groups def example_group_started(notification) + return unless notification.group.top_level? + @example_groups[notification.group][:start] = Time.now @example_groups[notification.group][:description] = notification.group.top_level_description end def example_group_finished(notification) + return unless notification.group.top_level? + @example_groups[notification.group][:total_time] = Time.now - @example_groups[notification.group][:start] end diff --git a/spec/rspec/core/formatters/profile_formatter_spec.rb b/spec/rspec/core/formatters/profile_formatter_spec.rb index 2b313f112a..9009b181aa 100644 --- a/spec/rspec/core/formatters/profile_formatter_spec.rb +++ b/spec/rspec/core/formatters/profile_formatter_spec.rb @@ -57,6 +57,7 @@ def profile *groups before do example_clock = class_double(RSpec::Core::Time, :now => RSpec::Core::Time.now + 0.5) + @slow_group_line_number = __LINE__ + 1 group1 = RSpec.describe("slow group") do example("example") do |example| # make it look slow without actually taking up precious time @@ -84,6 +85,10 @@ def profile *groups it "ranks the example groups by average time" do expect(formatter_output.string).to match(/slow group(.*)fast group/m) end + + it "prints the location of the slow groups" do + expect(formatter_output.string).to include("#{RSpec::Core::Metadata.relative_path __FILE__}:#{@slow_group_line_number}") + end end end end diff --git a/spec/rspec/core/profiler_spec.rb b/spec/rspec/core/profiler_spec.rb index 68029dbb00..7bd370a5cb 100644 --- a/spec/rspec/core/profiler_spec.rb +++ b/spec/rspec/core/profiler_spec.rb @@ -17,23 +17,26 @@ allow(::RSpec::Core::Time).to receive(:now) { now } end - def group - @group ||= - begin - group = super - allow(group).to receive(:top_level_description) { description } - group - end - end + let(:group) { RSpec.describe "My Group" } describe '#example_group_started' do - it 'records example groups start time and description via id' do + it 'records example groups start time and description' do expect { profiler.example_group_started group_notification group }.to change { profiler.example_groups[group] }. from(a_hash_excluding(:start, :description)). to(a_hash_including(:start => now, :description => description)) end + + context "when the group is not a top-level group" do + let(:group) { super().describe "nested" } + + it 'no-ops since we only consider top-level groups for profiling' do + expect { + profiler.example_group_started group_notification group + }.not_to change(profiler, :example_groups) + end + end end describe '#example_group_finished' do @@ -42,13 +45,23 @@ def group allow(::RSpec::Core::Time).to receive(:now) { now + 1 } end - it 'records example groups total time and description via id' do + it 'records example groups total time and description' do expect { profiler.example_group_finished group_notification group }.to change { profiler.example_groups[group] }. from(a_hash_excluding(:total_time)). to(a_hash_including(:total_time => 1.0)) end + + context "when the group is not a top-level group" do + let(:group) { super().describe "nested" } + + it 'no-ops since we only consider top-level groups for profiling' do + expect { + profiler.example_group_finished group_notification group + }.not_to change(profiler, :example_groups) + end + end end describe '#example_started' do From 473e3ed90739bb59592444d1fba5a5dfcb81dbf4 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 12 Jun 2015 01:15:07 -0700 Subject: [PATCH 256/258] Fix flickering test. Profile example group ordering no longer relies on `example.clock`. It relies directly on `RSpec::Core::Time.now` so we have to update the spec to make it consistent. --- .../core/formatters/json_formatter_spec.rb | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/spec/rspec/core/formatters/json_formatter_spec.rb b/spec/rspec/core/formatters/json_formatter_spec.rb index b97841208b..d84c430d3c 100644 --- a/spec/rspec/core/formatters/json_formatter_spec.rb +++ b/spec/rspec/core/formatters/json_formatter_spec.rb @@ -163,17 +163,19 @@ def profile *groups context "with multiple example groups", :slow do before do - example_clock = class_double(RSpec::Core::Time, :now => RSpec::Core::Time.now + 5) + start = Time.utc(2015, 6, 10, 12, 30) + now = start + + allow(RSpec::Core::Time).to receive(:now) { now } group1 = RSpec.describe("slow group") do - example("example") do |example| - # make it look slow without actually taking up precious time - example.clock = example_clock - end + example("example") { } + after { now += 100 } end group2 = RSpec.describe("fast group") do example("example 1") { } example("example 2") { } + after { now += 1 } end profile group1, group2 end @@ -187,14 +189,6 @@ def profile *groups end it "ranks the example groups by average time" do |ex| - if ENV['TRAVIS'] && RSpec::Support::Ruby.mri? && RUBY_VERSION == '2.0.0' && - $original_rspec_configuration.loaded_spec_files.to_a == [File.expand_path(__FILE__)] - RSpec.current_example = ex # necessary due to sandboxing so that `skip` works. - skip "This spec fails on Travis on MRI 2.0.0 only when this spec file runs " \ - "individually. It passes on Travis when run as part of the entire suite " \ - "and I can't repro locally, so we are skipping it for this case." - end - expect(formatter.output_hash[:profile][:groups].first[:description]).to eq("slow group") end end From 3c61c8900ac76b0d66c498a75ef307405d10f221 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 12 Jun 2015 08:02:16 -0700 Subject: [PATCH 257/258] Updates changelog for v3.3.0 [ci skip] --- Changelog.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Changelog.md b/Changelog.md index d68918c1b3..a0f9aa7bb6 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,5 @@ -### Development -[Full Changelog](http://github.com/rspec/rspec-core/compare/v3.2.3...master) +### 3.3.0 / 2015-06-12 +[Full Changelog](http://github.com/rspec/rspec-core/compare/v3.2.3...v3.3.0) Enhancements: From 192e7cba6a4a28a1d52a044cf4f7340bcfaf44c9 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Fri, 12 Jun 2015 08:04:05 -0700 Subject: [PATCH 258/258] Release 3.3.0 --- lib/rspec/core/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rspec/core/version.rb b/lib/rspec/core/version.rb index 9d70567630..043568bdaf 100644 --- a/lib/rspec/core/version.rb +++ b/lib/rspec/core/version.rb @@ -3,7 +3,7 @@ module Core # Version information for RSpec Core. module Version # Current version of RSpec Core, in semantic versioning format. - STRING = '3.3.0.pre' + STRING = '3.3.0' end end end