gems.thread_safe-0.3.6-java.spec.thread_safe.cache_spec.rb Maven / Gradle / Ivy
The newest version!
Thread.abort_on_exception = true
module ThreadSafe
describe Cache do
before(:each) do
@cache = described_class.new
end
it 'concurrency' do
(1..THREADS).map do |i|
Thread.new 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 '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 = ThreadSafe::Test::Latch.new(1)
compute_proceed = ThreadSafe::Test::Latch.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.release
compute_started.await
end
expect_size_change 1 do
late_compute_threads = Array.new(late_compute_threads_count) do
Thread.new 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
Thread.new 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
Thread.new do
block_until_compute_started.call('getter')
Thread.pass while @cache[:a].nil?
expect(1).to eq @cache[:a]
end
end
Thread.new do
@cache.compute_if_absent(:a) do
compute_started.release
compute_proceed.await
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
getters_count = 20
key_klass = 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 = ThreadSafe::Test::Latch.new(1)
compute_finished = ThreadSafe::Test::Latch.new(1)
getters_started = ThreadSafe::Test::Latch.new(getters_count)
getters_finished = ThreadSafe::Test::Latch.new(getters_count)
computer_thread = Thread.new do
getters_started.await
@cache.compute_if_absent(key) do
compute_started.release
getters_finished.await
1
end
compute_finished.release
end
getter_threads = (1..getters_count).map do
Thread.new do
getters_started.release
inserted_keys.each do |inserted_key|
expect(true).to eq @cache.key?(inserted_key)
expect(1).to eq @cache[inserted_key]
end
expect(false).to eq @cache.key?(key)
compute_started.await
inserted_keys.each do |inserted_key|
expect(true).to eq @cache.key?(inserted_key)
expect(1).to eq @cache[inserted_key]
end
expect(false).to eq @cache.key?(key)
expect(nil).to eq @cache[key]
getters_finished.release
compute_finished.await
expect(true).to eq @cache.key?(key)
expect(1).to eq @cache[key]
end
end
(getter_threads << computer_thread).map do |t|
expect(t.join(2)).to be_truthy
end # asserting no deadlocks
inserted_keys << key
end
end
specify 'collision resistance' do
expect_collision_resistance(
(0..1000).map { |i| 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(
[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 = fetch_with_return
# r = lambda 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 = fetch_or_store_with_return
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 'common' do
@cache.each_pair { |k, v| fail }
expect(@cache).to eq @cache.each_pair {}
@cache[:a] = 1
h = {}
@cache.each_pair { |k, v| h[k] = v }
expect({:a => 1}).to eq h
@cache[:b] = 2
h = {}
@cache.each_pair { |k, v| h[k] = v }
expect({:a => 1, :b => 2}).to eq h
end
it 'pair iterator' do
@cache[:a] = 1
@cache[:b] = 2
i = 0
r = @cache.each_pair do |k, v|
if i == 0
i += 1
next
fail
elsif i == 1
break :breaked
end
end
expect(:breaked).to eq r
end
it 'allows modification' do
@cache[:a] = 1
@cache[:b] = 1
@cache[:c] = 1
expect_size_change(1) do
@cache.each_pair do |k, v|
@cache[:z] = 1
end
end
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(:default_value).to eq dupped[:d]
expect(false).to eq cache.key?(:d)
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 ThreadSafe::Cache
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 doesnt work with default proc' do
expect { Marshal.dump(ThreadSafe::Cache.new {}) }.to raise_error(TypeError)
end
private
def with_or_without_default_proc(&block)
block.call(false)
@cache = ThreadSafe::Cache.new { |h, k| h[k] = :default_value }
block.call(true)
end
def cache_with_default_proc(default_value = 1)
ThreadSafe::Cache.new { |cache, k| cache[k] = default_value }
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 = ThreadSafe::Cache.new(options)
expect(c).to be_an_instance_of ThreadSafe::Cache
end
def expect_invalid_option(option_name, value)
expect_invalid_options(option_name => value)
end
def expect_invalid_options(options)
expect { ThreadSafe::Cache.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
# Took out for compatibility with Rubinius, see https://github.com/rubinius/rubinius/issues/1312
def fetch_with_return
lambda do
@cache.fetch(:a) { return 10 }
end.call
end
# Took out for compatibility with Rubinius, see https://github.com/rubinius/rubinius/issues/1312
def fetch_or_store_with_return
lambda do
@cache.fetch_or_store(:a) { return 10 }
end.call
end
end
end