Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cache match results for compound failures #1334

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions lib/rspec/matchers/built_in/compound.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
46 changes: 46 additions & 0 deletions spec/rspec/matchers/built_in/compound_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 0 additions & 8 deletions spec/rspec/matchers/built_in/contain_exactly_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 ]
Expand Down
8 changes: 8 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down