Testing Thread-Safe Map Operations in concurrent-ruby
This test suite thoroughly validates the functionality of the concurrent Map implementation in concurrent-ruby. It covers thread-safe operations, atomic updates, and proper handling of concurrent access patterns across multiple threads.
Test Coverage Overview
Implementation Analysis
Technical Details
Best Practices Demonstrated
ruby-concurrency/concurrent-ruby
spec/concurrent/map_spec.rb
require_relative 'collection_each_shared'
require 'concurrent/map'
require 'concurrent/atomic/count_down_latch'
Thread.abort_on_exception = true
module Concurrent
RSpec.describe Map do
before(:each) do
@cache = described_class.new
end
it 'default_proc is called with the Concurrent::Map and the key' do
map = Concurrent::Map.new do |h, k|
[h, k]
end
expect(map[:foo][0]).to be(map)
expect(map[:foo][1]).to eq(:foo)
end
it 'default_proc is called with the Concurrent::Map and the key after #dup' do
map = Concurrent::Map.new do |h, k|
[h, k]
end
copy = map.dup
expect(copy[:foo][0]).to be(copy)
expect(copy[:foo][1]).to eq(:foo)
end
it 'concurrency' do
(1..Concurrent::ThreadSafe::Test::THREADS).map do |i|
in_thread do
1000.times do |j|
key = i * 1000 + j
@cache[key] = i
@cache[key]
@cache.delete(key)
end
end
end.map(&:join)
end
it 'retrieval' do
expect_size_change(1) do
expect(nil).to eq @cache[:a]
expect(nil).to eq @cache.get(:a)
@cache[:a] = 1
expect(1).to eq @cache[:a]
expect(1).to eq @cache.get(:a)
end
end
it '#put_if_absent' do
with_or_without_default_proc do
expect_size_change(1) do
expect(nil).to eq @cache.put_if_absent(:a, 1)
expect(1).to eq @cache.put_if_absent(:a, 1)
expect(1).to eq @cache.put_if_absent(:a, 2)
expect(1).to eq @cache[:a]
end
end
end
describe '#compute_if_absent' do
it 'works in default_proc' do
map = Concurrent::Map.new do |map, key|
map.compute_if_absent(key) { key * 2 }
end
expect(map[3]).to eq 6
expect(map.keys).to eq [3]
end
it 'common' do
with_or_without_default_proc do
expect_size_change(3) do
expect(1).to eq @cache.compute_if_absent(:a) { 1 }
expect(1).to eq @cache.compute_if_absent(:a) { 2 }
expect(1).to eq @cache[:a]
@cache[:b] = nil
expect(nil).to eq @cache.compute_if_absent(:b) { 1 }
expect(nil).to eq @cache.compute_if_absent(:c) {}
expect(nil).to eq @cache[:c]
expect(true).to eq @cache.key?(:c)
end
end
end
it 'with return' do
with_or_without_default_proc do
expect_handles_return_lambda(:compute_if_absent, :a)
end
end
it 'exception' do
with_or_without_default_proc do
expect_handles_exception(:compute_if_absent, :a)
end
end
it 'atomicity' do
late_compute_threads_count = 10
late_put_if_absent_threads_count = 10
getter_threads_count = 5
compute_started = Concurrent::CountDownLatch.new(1)
compute_proceed = Concurrent::CountDownLatch.new(
late_compute_threads_count +
late_put_if_absent_threads_count +
getter_threads_count
)
block_until_compute_started = lambda do |name|
# what does it mean?
if (v = @cache[:a]) != nil
expect(nil).to v
end
compute_proceed.count_down
compute_started.wait
end
expect_size_change 1 do
late_compute_threads = ::Array.new(late_compute_threads_count) do
in_thread do
block_until_compute_started.call('compute_if_absent')
expect(1).to eq @cache.compute_if_absent(:a) { fail }
end
end
late_put_if_absent_threads = ::Array.new(late_put_if_absent_threads_count) do
in_thread do
block_until_compute_started.call('put_if_absent')
expect(1).to eq @cache.put_if_absent(:a, 2)
end
end
getter_threads = ::Array.new(getter_threads_count) do
in_thread do
block_until_compute_started.call('getter')
repeat_until_success { expect(1).to eq @cache[:a] }
end
end
in_thread do
@cache.compute_if_absent(:a) do
compute_started.count_down
compute_proceed.wait
sleep(0.2)
1
end
end.join
(late_compute_threads +
late_put_if_absent_threads +
getter_threads).each(&:join)
end
end
end
describe '#compute_if_present' do
it 'common' do
with_or_without_default_proc do
expect_no_size_change do
expect(nil).to eq @cache.compute_if_present(:a) {}
expect(nil).to eq @cache.compute_if_present(:a) { 1 }
expect(nil).to eq @cache.compute_if_present(:a) { fail }
expect(false).to eq @cache.key?(:a)
end
@cache[:a] = 1
expect_no_size_change do
expect(1).to eq @cache.compute_if_present(:a) { 1 }
expect(1).to eq @cache[:a]
expect(2).to eq @cache.compute_if_present(:a) { 2 }
expect(2).to eq @cache[:a]
expect(false).to eq @cache.compute_if_present(:a) { false }
expect(false).to eq @cache[:a]
@cache[:a] = 1
yielded = false
@cache.compute_if_present(:a) do |old_value|
yielded = true
expect(1).to eq old_value
2
end
expect(true).to eq yielded
end
expect_size_change(-1) do
expect(nil).to eq @cache.compute_if_present(:a) {}
expect(false).to eq @cache.key?(:a)
expect(nil).to eq @cache.compute_if_present(:a) { 1 }
expect(false).to eq @cache.key?(:a)
end
end
end
it 'with return' do
with_or_without_default_proc do
@cache[:a] = 1
expect_handles_return_lambda(:compute_if_present, :a)
end
end
it 'exception' do
with_or_without_default_proc do
@cache[:a] = 1
expect_handles_exception(:compute_if_present, :a)
end
end
end
describe '#compute' do
it 'common' do
with_or_without_default_proc do
expect_no_size_change do
expect_compute(:a, nil, nil) {}
end
expect_size_change(1) do
expect_compute(:a, nil, 1) { 1 }
expect_compute(:a, 1, 2) { 2 }
expect_compute(:a, 2, false) { false }
expect(false).to eq @cache[:a]
end
expect_size_change(-1) do
expect_compute(:a, false, nil) {}
end
end
end
it 'with return' do
with_or_without_default_proc do
expect_handles_return_lambda(:compute, :a)
@cache[:a] = 1
expect_handles_return_lambda(:compute, :a)
end
end
it 'exception' do
with_or_without_default_proc do
expect_handles_exception(:compute, :a)
@cache[:a] = 2
expect_handles_exception(:compute, :a)
end
end
end
describe '#merge_pair' do
it 'common' do
with_or_without_default_proc do
expect_size_change(1) do
expect(nil).to eq @cache.merge_pair(:a, nil) { fail }
expect(true).to eq @cache.key?(:a)
expect(nil).to eq @cache[:a]
end
expect_no_size_change do
expect_merge_pair(:a, nil, nil, false) { false }
expect_merge_pair(:a, nil, false, 1) { 1 }
expect_merge_pair(:a, nil, 1, 2) { 2 }
end
expect_size_change(-1) do
expect_merge_pair(:a, nil, 2, nil) {}
expect(false).to eq @cache.key?(:a)
end
end
end
it 'with return' do
with_or_without_default_proc do
@cache[:a] = 1
expect_handles_return_lambda(:merge_pair, :a, 2)
end
end
it 'exception' do
with_or_without_default_proc do
@cache[:a] = 1
expect_handles_exception(:merge_pair, :a, 2)
end
end
end
it 'updates dont block reads' do
if defined?(Concurrent::Collection::SynchronizedMapBackend) && described_class <= Concurrent::Collection::SynchronizedMapBackend
skip("Test does not apply to #{Concurrent::Collection::SynchronizedMapBackend}")
end
getters_count = 20
key_klass = Concurrent::ThreadSafe::Test::HashCollisionKey
keys = [key_klass.new(1, 100),
key_klass.new(2, 100),
key_klass.new(3, 100)] # hash colliding keys
inserted_keys = []
keys.each do |key, i|
compute_started = Concurrent::CountDownLatch.new(1)
compute_finished = Concurrent::CountDownLatch.new(1)
getters_started = Concurrent::CountDownLatch.new(getters_count)
getters_finished = Concurrent::CountDownLatch.new(getters_count)
computer_thread = in_thread do
getters_started.wait
@cache.compute_if_absent(key) do
compute_started.count_down
getters_finished.wait
1
end
compute_finished.count_down
end
getter_threads = getters_count.times.map do
in_thread do
getters_started.count_down
inserted_keys.each do |inserted_key|
expect(@cache.key?(inserted_key)).to eq true
expect(@cache[inserted_key]).to eq 1
end
expect(false).to eq @cache.key?(key)
compute_started.wait
inserted_keys.each do |inserted_key|
expect(@cache.key?(inserted_key)).to eq true
expect(@cache[inserted_key]).to eq 1
end
expect(@cache.key?(key)).to eq false
expect(@cache[key]).to eq nil
getters_finished.count_down
compute_finished.wait
expect(@cache.key?(key)).to eq true
expect(@cache[key]).to eq 1
end
end
join_with getter_threads + [computer_thread]
inserted_keys << key
end
end
specify 'collision resistance' do
expect_collision_resistance(
(0..1000).map { |i| Concurrent::ThreadSafe::Test::HashCollisionKey(i, 1) }
)
end
specify 'collision resistance with arrays' do
special_array_class = Class.new(::Array) do
def key # assert_collision_resistance expects to be able to call .key to get the "real" key
first.key
end
end
# Test collision resistance with a keys that say they responds_to <=>, but then raise exceptions
# when actually called (ie: an Array filled with non-comparable keys).
# See https://github.com/headius/thread_safe/issues/19 for more info.
expect_collision_resistance(
(0..100).map do |i|
special_array_class.new(
[Concurrent::ThreadSafe::Test::HashCollisionKeyNonComparable.new(i, 1)]
)
end
)
end
it '#replace_pair' do
with_or_without_default_proc do
expect_no_size_change do
expect(false).to eq @cache.replace_pair(:a, 1, 2)
expect(false).to eq @cache.replace_pair(:a, nil, nil)
expect(false).to eq @cache.key?(:a)
end
end
@cache[:a] = 1
expect_no_size_change do
expect(true).to eq @cache.replace_pair(:a, 1, 2)
expect(false).to eq @cache.replace_pair(:a, 1, 2)
expect(2).to eq @cache[:a]
expect(true).to eq @cache.replace_pair(:a, 2, 2)
expect(2).to eq @cache[:a]
expect(true).to eq @cache.replace_pair(:a, 2, nil)
expect(false).to eq @cache.replace_pair(:a, 2, nil)
expect(nil).to eq @cache[:a]
expect(true).to eq @cache.key?(:a)
expect(true).to eq @cache.replace_pair(:a, nil, nil)
expect(true).to eq @cache.key?(:a)
expect(true).to eq @cache.replace_pair(:a, nil, 1)
expect(1).to eq @cache[:a]
end
end
it '#replace_if_exists' do
with_or_without_default_proc do
expect_no_size_change do
expect(nil).to eq @cache.replace_if_exists(:a, 1)
expect(false).to eq @cache.key?(:a)
end
@cache[:a] = 1
expect_no_size_change do
expect(1).to eq @cache.replace_if_exists(:a, 2)
expect(2).to eq @cache[:a]
expect(2).to eq @cache.replace_if_exists(:a, nil)
expect(nil).to eq @cache[:a]
expect(true).to eq @cache.key?(:a)
expect(nil).to eq @cache.replace_if_exists(:a, 1)
expect(1).to eq @cache[:a]
end
end
end
it '#get_and_set' do
with_or_without_default_proc do
expect(nil).to eq @cache.get_and_set(:a, 1)
expect(true).to eq @cache.key?(:a)
expect(1).to eq @cache[:a]
expect(1).to eq @cache.get_and_set(:a, 2)
expect(2).to eq @cache.get_and_set(:a, nil)
expect(nil).to eq @cache[:a]
expect(true).to eq @cache.key?(:a)
expect(nil).to eq @cache.get_and_set(:a, 1)
expect(1).to eq @cache[:a]
end
end
it '#key' do
with_or_without_default_proc do
expect(nil).to eq @cache.key(1)
@cache[:a] = 1
expect(:a).to eq @cache.key(1)
expect(nil).to eq @cache.key(0)
end
end
it '#key?' do
with_or_without_default_proc do
expect(false).to eq @cache.key?(:a)
@cache[:a] = 1
expect(true).to eq @cache.key?(:a)
end
end
it '#value?' do
with_or_without_default_proc do
expect(false).to eq @cache.value?(1)
@cache[:a] = 1
expect(true).to eq @cache.value?(1)
end
end
it '#delete' do
with_or_without_default_proc do |default_proc_set|
expect_no_size_change do
expect(nil).to eq @cache.delete(:a)
end
@cache[:a] = 1
expect_size_change(-1) do
expect(1).to eq @cache.delete(:a)
end
expect_no_size_change do
expect(nil).to eq @cache[:a] unless default_proc_set
expect(false).to eq @cache.key?(:a)
expect(nil).to eq @cache.delete(:a)
end
end
end
it '#delete_pair' do
with_or_without_default_proc do
expect_no_size_change do
expect(false).to eq @cache.delete_pair(:a, 2)
expect(false).to eq @cache.delete_pair(:a, nil)
end
@cache[:a] = 1
expect_no_size_change do
expect(false).to eq @cache.delete_pair(:a, 2)
end
expect_size_change(-1) do
expect(1).to eq @cache[:a]
expect(true).to eq @cache.delete_pair(:a, 1)
expect(false).to eq @cache.delete_pair(:a, 1)
expect(false).to eq @cache.key?(:a)
end
end
end
specify 'default proc' do
@cache = cache_with_default_proc(1)
expect_no_size_change do
expect(false).to eq @cache.key?(:a)
end
expect_size_change(1) do
expect(1).to eq @cache[:a]
expect(true).to eq @cache.key?(:a)
end
end
specify 'falsy default proc' do
@cache = cache_with_default_proc(nil)
expect_no_size_change do
expect(false).to eq @cache.key?(:a)
end
expect_size_change(1) do
expect(nil).to eq @cache[:a]
expect(true).to eq @cache.key?(:a)
end
end
describe '#fetch' do
it 'common' do
with_or_without_default_proc do |default_proc_set|
expect_no_size_change do
expect(1).to eq @cache.fetch(:a, 1)
expect(1).to eq @cache.fetch(:a) { 1 }
expect(false).to eq @cache.key?(:a)
expect(nil).to eq @cache[:a] unless default_proc_set
end
@cache[:a] = 1
expect_no_size_change do
expect(1).to eq @cache.fetch(:a) { fail }
end
expect { @cache.fetch(:b) }.to raise_error(KeyError)
expect_no_size_change do
expect(1).to eq @cache.fetch(:b, :c) {1} # assert block supersedes default value argument
expect(false).to eq @cache.key?(:b)
end
end
end
it 'falsy' do
with_or_without_default_proc do
expect(false).to eq @cache.key?(:a)
expect_no_size_change do
expect(nil).to eq @cache.fetch(:a, nil)
expect(false).to eq @cache.fetch(:a, false)
expect(nil).to eq @cache.fetch(:a) {}
expect(false).to eq @cache.fetch(:a) { false }
end
@cache[:a] = nil
expect_no_size_change do
expect(true).to eq @cache.key?(:a)
expect(nil).to eq @cache.fetch(:a) { fail }
end
end
end
it 'with return' do
with_or_without_default_proc do
r = -> do
@cache.fetch(:a) { return 10 }
end.call
expect_no_size_change do
expect(10).to eq r
expect(false).to eq @cache.key?(:a)
end
end
end
end
describe '#fetch_or_store' do
it 'common' do
with_or_without_default_proc do |default_proc_set|
expect_size_change(1) do
expect(1).to eq @cache.fetch_or_store(:a, 1)
expect(1).to eq @cache[:a]
end
@cache.delete(:a)
expect_size_change 1 do
expect(1).to eq @cache.fetch_or_store(:a) { 1 }
expect(1).to eq @cache[:a]
end
expect_no_size_change do
expect(1).to eq @cache.fetch_or_store(:a) { fail }
end
expect { @cache.fetch_or_store(:b) }.
to raise_error(KeyError)
expect_size_change(1) do
expect(1).to eq @cache.fetch_or_store(:b, :c) { 1 } # assert block supersedes default value argument
expect(1).to eq @cache[:b]
end
end
end
it 'falsy' do
with_or_without_default_proc do
expect(false).to eq @cache.key?(:a)
expect_size_change(1) do
expect(nil).to eq @cache.fetch_or_store(:a, nil)
expect(nil).to eq @cache[:a]
expect(true).to eq @cache.key?(:a)
end
@cache.delete(:a)
expect_size_change(1) do
expect(false).to eq @cache.fetch_or_store(:a, false)
expect(false).to eq @cache[:a]
expect(true).to eq @cache.key?(:a)
end
@cache.delete(:a)
expect_size_change(1) do
expect(nil).to eq @cache.fetch_or_store(:a) {}
expect(nil).to eq @cache[:a]
expect(true).to eq @cache.key?(:a)
end
@cache.delete(:a)
expect_size_change(1) do
expect(false).to eq @cache.fetch_or_store(:a) { false }
expect(false).to eq @cache[:a]
expect(true).to eq @cache.key?(:a)
end
@cache[:a] = nil
expect_no_size_change do
expect(nil).to eq @cache.fetch_or_store(:a) { fail }
end
end
end
it 'with return' do
with_or_without_default_proc do
r = -> do
@cache.fetch_or_store(:a) { return 10 }
end.call
expect_no_size_change do
expect(10).to eq r
expect(false).to eq @cache.key?(:a)
end
end
end
end
it '#clear' do
@cache[:a] = 1
expect_size_change(-1) do
expect(@cache).to eq @cache.clear
expect(false).to eq @cache.key?(:a)
expect(nil).to eq @cache[:a]
end
end
describe '#each_pair' do
it_should_behave_like :collection_each do
let(:method) { :each_pair }
end
end
describe '#each' do
it_should_behave_like :collection_each do
let(:method) { :each }
end
end
it '#keys' do
expect([]).to eq @cache.keys
@cache[1] = 1
expect([1]).to eq @cache.keys
@cache[2] = 2
expect([1, 2]).to eq @cache.keys.sort
end
it '#values' do
expect([]).to eq @cache.values
@cache[1] = 1
expect([1]).to eq @cache.values
@cache[2] = 2
expect([1, 2]).to eq @cache.values.sort
end
it '#each_key' do
expect(@cache).to eq @cache.each_key { fail }
@cache[1] = 1
arr = []
@cache.each_key { |k| arr << k }
expect([1]).to eq arr
@cache[2] = 2
arr = []
@cache.each_key { |k| arr << k }
expect([1, 2]).to eq arr.sort
end
it '#each_value' do
expect(@cache).to eq @cache.each_value { fail }
@cache[1] = 1
arr = []
@cache.each_value { |k| arr << k }
expect([1]).to eq arr
@cache[2] = 2
arr = []
@cache.each_value { |k| arr << k }
expect([1, 2]).to eq arr.sort
end
it '#empty' do
expect(true).to eq @cache.empty?
@cache[:a] = 1
expect(false).to eq @cache.empty?
end
it 'options validation' do
expect_valid_options(nil)
expect_valid_options({})
expect_valid_options(foo: :bar)
end
it 'initial capacity options validation' do
expect_valid_option(:initial_capacity, nil)
expect_valid_option(:initial_capacity, 1)
expect_invalid_option(:initial_capacity, '')
expect_invalid_option(:initial_capacity, 1.0)
expect_invalid_option(:initial_capacity, -1)
end
it 'load factor options validation' do
expect_valid_option(:load_factor, nil)
expect_valid_option(:load_factor, 0.01)
expect_valid_option(:load_factor, 0.75)
expect_valid_option(:load_factor, 1)
expect_invalid_option(:load_factor, '')
expect_invalid_option(:load_factor, 0)
expect_invalid_option(:load_factor, 1.1)
expect_invalid_option(:load_factor, 2)
expect_invalid_option(:load_factor, -1)
end
it '#size' do
expect(0).to eq @cache.size
@cache[:a] = 1
expect(1).to eq @cache.size
@cache[:b] = 1
expect(2).to eq @cache.size
@cache.delete(:a)
expect(1).to eq @cache.size
@cache.delete(:b)
expect(0).to eq @cache.size
end
it '#get_or_default' do
with_or_without_default_proc do
expect(1).to eq @cache.get_or_default(:a, 1)
expect(nil).to eq @cache.get_or_default(:a, nil)
expect(false).to eq @cache.get_or_default(:a, false)
expect(false).to eq @cache.key?(:a)
@cache[:a] = 1
expect(1).to eq @cache.get_or_default(:a, 2)
end
end
it '#dup,#clone' do
[:dup, :clone].each do |meth|
cache = cache_with_default_proc(:default_value)
cache[:a] = 1
dupped = cache.send(meth)
expect(1).to eq dupped[:a]
expect(1).to eq dupped.size
expect_size_change(1, cache) do
expect_no_size_change(dupped) do
cache[:b] = 1
end
end
expect(false).to eq dupped.key?(:b)
expect_no_size_change(cache) do
expect_size_change(-1, dupped) do
dupped.delete(:a)
end
end
expect(false).to eq dupped.key?(:a)
expect(true).to eq cache.key?(:a)
# test default proc
expect_size_change(1, cache) do
expect_no_size_change dupped do
expect(:default_value).to eq cache[:c]
expect(false).to eq dupped.key?(:c)
end
end
expect_no_size_change cache do
expect_size_change 1, dupped do
expect(dupped[:d]).to eq :default_value
expect(cache.key?(:d)).to eq false
end
end
end
end
it 'is unfreezable' do
expect { @cache.freeze }.to raise_error(NoMethodError)
end
it 'marshal dump load' do
new_cache = Marshal.load(Marshal.dump(@cache))
expect(new_cache).to be_an_instance_of Concurrent::Map
expect(0).to eq new_cache.size
@cache[:a] = 1
new_cache = Marshal.load(Marshal.dump(@cache))
expect(1).to eq @cache[:a]
expect(1).to eq new_cache.size
end
it 'marshal dump does not work with default proc' do
expect { Marshal.dump(Concurrent::Map.new {}) }.to raise_error(TypeError)
end
it '#inspect' do
regexp = /\A#<Concurrent::Map:0x[0-9a-f]+ entries=[0-9]+ default_proc=.*>\Z/i
expect(Concurrent::Map.new.inspect).to match(regexp)
expect((Concurrent::Map.new {}).inspect).to match(regexp)
map = Concurrent::Map.new
map[:foo] = :bar
expect(map.inspect).to match(regexp)
end
private
def with_or_without_default_proc(&block)
block.call(false)
@cache = Concurrent::Map.new { |h, k|
expect(h).to be_kind_of(Concurrent::Map)
h[k] = :default_value
}
block.call(true)
end
def cache_with_default_proc(default_value = 1)
Concurrent::Map.new do |map, k|
expect(map).to be_kind_of(Concurrent::Map)
map[k] = default_value
end
end
def expect_size_change(change, cache = @cache, &block)
start = cache.size
block.call
expect(change).to eq cache.size - start
end
def expect_valid_option(option_name, value)
expect_valid_options(option_name => value)
end
def expect_valid_options(options)
c = Concurrent::Map.new(options)
expect(c).to be_an_instance_of Concurrent::Map
end
def expect_invalid_option(option_name, value)
expect_invalid_options(option_name => value)
end
def expect_invalid_options(options)
expect { Concurrent::Map.new(options) }.to raise_error(ArgumentError)
end
def expect_no_size_change(cache = @cache, &block)
expect_size_change(0, cache, &block)
end
def expect_handles_return_lambda(method, key, *args)
before_had_key = @cache.key?(key)
before_had_value = before_had_key ? @cache[key] : nil
returning_lambda = lambda do
@cache.send(method, key, *args) { return :direct_return }
end
expect_no_size_change do
expect(:direct_return).to eq returning_lambda.call
expect(before_had_key).to eq @cache.key?(key)
expect(before_had_value).to eq @cache[key] if before_had_value
end
end
class TestException < Exception; end
def expect_handles_exception(method, key, *args)
before_had_key = @cache.key?(key)
before_had_value = before_had_key ? @cache[key] : nil
expect_no_size_change do
expect { @cache.send(method, key, *args) { raise TestException, '' } }.
to raise_error(TestException)
expect(before_had_key).to eq @cache.key?(key)
expect(before_had_value).to eq @cache[key] if before_had_value
end
end
def expect_compute(key, expected_old_value, expected_result, &block)
result = @cache.compute(:a) do |old_value|
expect(expected_old_value).to eq old_value
block.call
end
expect(expected_result).to eq result
end
def expect_merge_pair(key, value, expected_old_value, expected_result, &block)
result = @cache.merge_pair(key, value) do |old_value|
expect(expected_old_value).to eq old_value
block.call
end
expect(expected_result).to eq result
end
def expect_collision_resistance(keys)
keys.each { |k| @cache[k] = k.key }
10.times do |i|
size = keys.size
while i < size
k = keys[i]
expect(k.key == @cache.delete(k) &&
[email protected]?(k) &&
(@cache[k] = k.key; @cache[k] == k.key)).to be_truthy
i += 10
end
end
expect(keys.all? { |k| @cache[k] == k.key }).to be_truthy
end
end
end