diff --git a/lib/rspec/matchers/built_in/compound.rb b/lib/rspec/matchers/built_in/compound.rb index 56f27b1a6..0c07d8ba2 100644 --- a/lib/rspec/matchers/built_in/compound.rb +++ b/lib/rspec/matchers/built_in/compound.rb @@ -75,6 +75,8 @@ def initialize_copy(other) end def match(_expected, actual) + @matcher_1_matches = nil + @matcher_2_matches = nil evaluator_klass = if supports_block_expectations? && Proc === actual NestedEvaluator else @@ -97,11 +99,13 @@ def compound_failure_message end def matcher_1_matches? - evaluator.matcher_matches?(matcher_1) + return @matcher_1_matches unless @matcher_1_matches.nil? + @matcher_1_matches = evaluator.matcher_matches?(matcher_1) end def matcher_2_matches? - evaluator.matcher_matches?(matcher_2) + return @matcher_2_matches unless @matcher_2_matches.nil? + @matcher_2_matches = evaluator.matcher_matches?(matcher_2) end def matcher_supports_block_expectations?(matcher) diff --git a/spec/rspec/matchers/built_in/compound_spec.rb b/spec/rspec/matchers/built_in/compound_spec.rb index 0594b6b0a..a562b7eeb 100644 --- a/spec/rspec/matchers/built_in/compound_spec.rb +++ b/spec/rspec/matchers/built_in/compound_spec.rb @@ -872,6 +872,52 @@ def expect_block | EOS end + + context "with long chains of compound matchers" do + let(:failing_matcher) { include(not_expected) } + let(:passing_matcher) { include(expected) } + let(:expected) { actual } + let(:not_expected) { 3 } + let(:actual) { 4 } + + context "with a failing first matcher" do + it "generates a failure description quickly" do + timeout_if_not_debugging(0.2) do + compound = failing_matcher + 15.times { compound = compound.and(passing_matcher) } + expect { expect([actual]).to compound }.to fail_including("expected [#{actual}] to include #{not_expected}") + end + end + end + + context "with a failing last matcher" do + it "generates a failure description quickly" do + timeout_if_not_debugging(0.2) do + compound = failing_matcher + 15.times { compound = passing_matcher.and(compound) } + expect { expect([actual]).to compound }.to fail_including("expected [#{actual}] to include #{not_expected}") + end + end + end + + context "with all failing matchers" do + it "generates a failure description quickly with and" do + timeout_if_not_debugging(0.2) do + compound = failing_matcher + 15.times { compound = compound.and(failing_matcher) } + expect { expect([actual]).to compound }.to fail_including("expected [#{actual}] to include #{not_expected}") + end + end + + it "generates a failure description quickly with or" do + timeout_if_not_debugging(0.2) do + compound = failing_matcher + 15.times { compound = compound.or(failing_matcher) } + expect { expect([actual]).to compound }.to fail_including("expected [#{actual}] to include #{not_expected}") + end + end + end + end end describe "expect(...).not_to matcher.or(other_matcher)" do diff --git a/spec/rspec/matchers/built_in/contain_exactly_spec.rb b/spec/rspec/matchers/built_in/contain_exactly_spec.rb index ac1996ae8..5d48c77f0 100644 --- a/spec/rspec/matchers/built_in/contain_exactly_spec.rb +++ b/spec/rspec/matchers/built_in/contain_exactly_spec.rb @@ -184,14 +184,6 @@ def array.send; :sent; end MESSAGE end - def timeout_if_not_debugging(time) - in_sub_process_if_possible do - require 'timeout' - return yield if defined?(::Debugger) - Timeout.timeout(time) { yield } - end - end - it 'fails a match of 11 items with duplicates in a reasonable amount of time' do timeout_if_not_debugging(0.1) do expected = [0, 1, 1, 3, 3, 3, 4, 4, 8, 8, 9 ] diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 595fafa2c..bb5a59486 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -25,6 +25,14 @@ def dedent(string) string.gsub(/^\s+\|/, '').chomp end + def timeout_if_not_debugging(time) + in_sub_process_if_possible do + require 'timeout' + return yield if defined?(::Debugger) + Timeout.timeout(time) { yield } + end + end + # We have to use Hash#inspect in examples that have multi-entry # hashes because the #inspect output on 1.8.7 is non-deterministic # due to the fact that hashes are not ordered. So we can't simply