Skip to content

Commit

Permalink
Speed up the ContainExactly matcher
Browse files Browse the repository at this point in the history
Speed up the ContainExactly matcher by pre-emptively matching up corresponding elements in the expected and actual arrays.

This addresses #1006, #1161.

This PR is a collaboration between me and @genehsu based on
a couple of our earlier PRs and discussion that resulted:
1) #1325
2) #1328

Co-authored-by: Gene Hsu (@genehsu)
  • Loading branch information
bclayman-sq committed Oct 29, 2021
1 parent dba6798 commit 7972abf
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 18 deletions.
39 changes: 34 additions & 5 deletions lib/rspec/matchers/built_in/contain_exactly.rb
Original file line number Diff line number Diff line change
Expand Up @@ -128,17 +128,46 @@ def best_solution
@best_solution ||= pairings_maximizer.find_best_solution
end

def pairings_maximizer
def pairings_maximizer # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
@pairings_maximizer ||= begin
expected_matches = Hash[Array.new(expected.size) { |i| [i, []] }]
actual_matches = Hash[Array.new(actual.size) { |i| [i, []] }]

expected.each_with_index do |e, ei|
actual.each_with_index do |a, ai|
# Set the reciprocal pairings for matching elements
value_buckets = Hash.new { |hash, key| hash[key] = [] }
expected.each_with_index { |v, i| value_buckets[[v, :expected]] << i }
actual.each_with_index { |v, i| value_buckets[[v, :actual]] << i }
value_buckets.each do |key, indexes|
value, source = key
next unless source == :expected
next unless value_buckets.key? [value, :actual]

actual_indexes = value_buckets[[value, :actual]]
indexes.zip(actual_indexes).each do |expected_index, actual_index|
break unless actual_index

expected_matches[expected_index] << actual_index
actual_matches[actual_index] << expected_index
end
end

# Set the remaining elements to be paired
filtered_expected = []
filtered_actual = []
expected.each_with_index do |e, expected_index|
filtered_expected << [e, expected_index] if expected_matches[expected_index].empty?
end
actual.each_with_index do |a, actual_index|
filtered_actual << [a, actual_index] if actual_matches[actual_index].empty?
end

# Set the pairing combinations of the remaining elements
filtered_expected.each do |e, expected_index|
filtered_actual.each do |a, actual_index|
next unless values_match?(e, a)

expected_matches[ei] << ai
actual_matches[ai] << ei
expected_matches[expected_index] << actual_index
actual_matches[actual_index] << expected_index
end
end

Expand Down
78 changes: 65 additions & 13 deletions spec/rspec/matchers/built_in/contain_exactly_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -184,22 +184,74 @@ 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 }
context "speed checks" do
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
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 ]
actual = [ 1, 2, 3, 3, 3, 3, 7, 8, 8, 9, 9]
shared_examples "succeeds fast" do
it do
timeout_if_not_debugging(max_runtime) do
subject
end
end
end

expect {
expect(actual).to contain_exactly(*expected)
}.to fail_including("the missing elements were: [0, 1, 4, 4]")
shared_examples "fails fast" do |failure_msg|
it do
timeout_if_not_debugging(max_runtime) do
expect {
subject
}.to fail_with(/#{Regexp.quote(failure_msg)}/)
end
end
end

let(:max_runtime) { 1 }
let(:actual) { Array.new(10_000) { rand(10) } }

context "with a positive expectation" do
subject { expect(actual).to contain_exactly(*expected) }

context "that is valid" do
let(:expected) { actual.shuffle }

it "matches" do
subject
end

include_examples "succeeds fast"
end

context "that is not valid" do
let(:expected) { Array.new(10_000) { rand(10) } }

include_examples "fails fast", "expected collection contained"
end
end

context "with a negative expectation" do
subject { expect(actual).not_to contain_exactly(*expected) }

context "that is valid" do
let(:expected) { Array.new(10_000) { rand(10) } }

it "does not match" do
subject
end

include_examples "succeeds fast"
end

context "that is not valid" do
let(:expected) { actual.shuffle }

include_examples "fails fast", "not to contain exactly"
end
end
end

Expand Down

0 comments on commit 7972abf

Please sign in to comment.