twitter-status-bot/.gems/gems/thread_safe-0.3.4/test/test_cache.rb

902 lines
24 KiB
Ruby

require 'thread_safe'
require 'thread'
require File.join(File.dirname(__FILE__), "test_helper")
Thread.abort_on_exception = true
class TestCache < Minitest::Test
def setup
@cache = ThreadSafe::Cache.new
end
def test_concurrency
cache = @cache
(1..100).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
def test_retrieval
assert_size_change 1 do
assert_equal nil, @cache[:a]
assert_equal nil, @cache.get(:a)
@cache[:a] = 1
assert_equal 1, @cache[:a]
assert_equal 1, @cache.get(:a)
end
end
def test_put_if_absent
with_or_without_default_proc do
assert_size_change 1 do
assert_equal nil, @cache.put_if_absent(:a, 1)
assert_equal 1, @cache.put_if_absent(:a, 1)
assert_equal 1, @cache.put_if_absent(:a, 2)
assert_equal 1, @cache[:a]
end
end
end
def test_compute_if_absent
with_or_without_default_proc do
assert_size_change 3 do
assert_equal(1, (@cache.compute_if_absent(:a) {1}))
assert_equal(1, (@cache.compute_if_absent(:a) {2}))
assert_equal 1, @cache[:a]
@cache[:b] = nil
assert_equal(nil, (@cache.compute_if_absent(:b) {1}))
assert_equal(nil, (@cache.compute_if_absent(:c) {}))
assert_equal nil, @cache[:c]
assert_equal true, @cache.key?(:c)
end
end
end
def test_compute_if_absent_with_return
with_or_without_default_proc { assert_handles_return_lambda(:compute_if_absent, :a) }
end
def test_compute_if_absent_exception
with_or_without_default_proc { assert_handles_exception(:compute_if_absent, :a) }
end
def test_compute_if_absent_atomicity
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|
if (v = @cache[:a]) != nil
assert_equal nil, v
end
compute_proceed.release
compute_started.await
end
assert_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')
assert_equal(1, (@cache.compute_if_absent(:a) { flunk }))
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')
assert_equal(1, @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?
assert_equal 1, @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
def test_compute_if_present
with_or_without_default_proc do
assert_no_size_change do
assert_equal(nil, @cache.compute_if_present(:a) {})
assert_equal(nil, @cache.compute_if_present(:a) {1})
assert_equal(nil, @cache.compute_if_present(:a) {flunk})
assert_equal false, @cache.key?(:a)
end
@cache[:a] = 1
assert_no_size_change do
assert_equal(1, @cache.compute_if_present(:a) {1})
assert_equal(1, @cache[:a])
assert_equal(2, @cache.compute_if_present(:a) {2})
assert_equal(2, @cache[:a])
assert_equal(false, @cache.compute_if_present(:a) {false})
assert_equal(false, @cache[:a])
@cache[:a] = 1
yielded = false
@cache.compute_if_present(:a) do |old_value|
yielded = true
assert_equal 1, old_value
2
end
assert yielded
end
assert_size_change -1 do
assert_equal(nil, @cache.compute_if_present(:a) {})
assert_equal(false, @cache.key?(:a))
assert_equal(nil, @cache.compute_if_present(:a) {1})
assert_equal(false, @cache.key?(:a))
end
end
end
def test_compute_if_present_with_return
with_or_without_default_proc do
@cache[:a] = 1
assert_handles_return_lambda(:compute_if_present, :a)
end
end
def test_compute_if_present_exception
with_or_without_default_proc do
@cache[:a] = 1
assert_handles_exception(:compute_if_present, :a)
end
end
def test_compute
with_or_without_default_proc do
assert_no_size_change do
assert_compute(:a, nil, nil) {}
end
assert_size_change 1 do
assert_compute(:a, nil, 1) {1}
assert_compute(:a, 1, 2) {2}
assert_compute(:a, 2, false) {false}
assert_equal false, @cache[:a]
end
assert_size_change -1 do
assert_compute(:a, false, nil) {}
end
end
end
def test_compute_with_return
with_or_without_default_proc do
assert_handles_return_lambda(:compute, :a)
@cache[:a] = 1
assert_handles_return_lambda(:compute, :a)
end
end
def test_compute_exception
with_or_without_default_proc do
assert_handles_exception(:compute, :a)
@cache[:a] = 1
assert_handles_exception(:compute, :a)
end
end
def test_merge_pair
with_or_without_default_proc do
assert_size_change 1 do
assert_equal(nil, @cache.merge_pair(:a, nil) {flunk})
assert_equal true, @cache.key?(:a)
assert_equal nil, @cache[:a]
end
assert_no_size_change do
assert_merge_pair(:a, nil, nil, false) {false}
assert_merge_pair(:a, nil, false, 1) {1}
assert_merge_pair(:a, nil, 1, 2) {2}
end
assert_size_change -1 do
assert_merge_pair(:a, nil, 2, nil) {}
assert_equal false, @cache.key?(:a)
end
end
end
def test_merge_pair_with_return
with_or_without_default_proc do
@cache[:a] = 1
assert_handles_return_lambda(:merge_pair, :a, 2)
end
end
def test_merge_pair_exception
with_or_without_default_proc do
@cache[:a] = 1
assert_handles_exception(:merge_pair, :a, 2)
end
end
def test_updates_dont_block_reads
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|
assert_equal true, @cache.key?(inserted_key)
assert_equal 1, @cache[inserted_key]
end
assert_equal false, @cache.key?(key)
compute_started.await
inserted_keys.each do |inserted_key|
assert_equal true, @cache.key?(inserted_key)
assert_equal 1, @cache[inserted_key]
end
assert_equal false, @cache.key?(key)
assert_equal nil, @cache[key]
getters_finished.release
compute_finished.await
assert_equal true, @cache.key?(key)
assert_equal 1, @cache[key]
end
end
(getter_threads << computer_thread).map {|t| assert(t.join(2))} # asserting no deadlocks
inserted_keys << key
end
end
def test_collision_resistance
assert_collision_resistance((0..1000).map {|i| ThreadSafe::Test::HashCollisionKey(i, 1)})
end
def test_collision_resistance_with_arrays
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.
assert_collision_resistance((0..100).map do |i|
special_array_class.new([ThreadSafe::Test::HashCollisionKeyNonComparable.new(i, 1)])
end)
end
def test_replace_pair
with_or_without_default_proc do
assert_no_size_change do
assert_equal false, @cache.replace_pair(:a, 1, 2)
assert_equal false, @cache.replace_pair(:a, nil, nil)
assert_equal false, @cache.key?(:a)
end
@cache[:a] = 1
assert_no_size_change do
assert_equal true, @cache.replace_pair(:a, 1, 2)
assert_equal false, @cache.replace_pair(:a, 1, 2)
assert_equal 2, @cache[:a]
assert_equal true, @cache.replace_pair(:a, 2, 2)
assert_equal 2, @cache[:a]
assert_equal true, @cache.replace_pair(:a, 2, nil)
assert_equal false, @cache.replace_pair(:a, 2, nil)
assert_equal nil, @cache[:a]
assert_equal true, @cache.key?(:a)
assert_equal true, @cache.replace_pair(:a, nil, nil)
assert_equal true, @cache.key?(:a)
assert_equal true, @cache.replace_pair(:a, nil, 1)
assert_equal 1, @cache[:a]
end
end
end
def test_replace_if_exists
with_or_without_default_proc do
assert_no_size_change do
assert_equal nil, @cache.replace_if_exists(:a, 1)
assert_equal false, @cache.key?(:a)
end
@cache[:a] = 1
assert_no_size_change do
assert_equal 1, @cache.replace_if_exists(:a, 2)
assert_equal 2, @cache[:a]
assert_equal 2, @cache.replace_if_exists(:a, nil)
assert_equal nil, @cache[:a]
assert_equal true, @cache.key?(:a)
assert_equal nil, @cache.replace_if_exists(:a, 1)
assert_equal 1, @cache[:a]
end
end
end
def test_get_and_set
with_or_without_default_proc do
assert_size_change 1 do
assert_equal nil, @cache.get_and_set(:a, 1)
assert_equal true, @cache.key?(:a)
assert_equal 1, @cache[:a]
assert_equal 1, @cache.get_and_set(:a, 2)
assert_equal 2, @cache.get_and_set(:a, nil)
assert_equal nil, @cache[:a]
assert_equal true, @cache.key?(:a)
assert_equal nil, @cache.get_and_set(:a, 1)
assert_equal 1, @cache[:a]
end
end
end
def test_key
with_or_without_default_proc do
assert_equal nil, @cache.key(1)
@cache[:a] = 1
assert_equal :a, @cache.key(1)
assert_equal nil, @cache.key(0)
assert_equal :a, @cache.index(1) if RUBY_VERSION =~ /1\.8/
end
end
def test_key?
with_or_without_default_proc do
assert_equal false, @cache.key?(:a)
@cache[:a] = 1
assert_equal true, @cache.key?(:a)
end
end
def test_value?
with_or_without_default_proc do
assert_equal false, @cache.value?(1)
@cache[:a] = 1
assert_equal true, @cache.value?(1)
end
end
def test_delete
with_or_without_default_proc do |default_proc_set|
assert_no_size_change do
assert_equal nil, @cache.delete(:a)
end
@cache[:a] = 1
assert_size_change -1 do
assert_equal 1, @cache.delete(:a)
end
assert_no_size_change do
assert_equal nil, @cache[:a] unless default_proc_set
assert_equal false, @cache.key?(:a)
assert_equal nil, @cache.delete(:a)
end
end
end
def test_delete_pair
with_or_without_default_proc do
assert_no_size_change do
assert_equal false, @cache.delete_pair(:a, 2)
assert_equal false, @cache.delete_pair(:a, nil)
end
@cache[:a] = 1
assert_no_size_change do
assert_equal false, @cache.delete_pair(:a, 2)
end
assert_size_change -1 do
assert_equal 1, @cache[:a]
assert_equal true, @cache.delete_pair(:a, 1)
assert_equal false, @cache.delete_pair(:a, 1)
assert_equal false, @cache.key?(:a)
end
end
end
def test_default_proc
@cache = cache_with_default_proc(1)
assert_no_size_change do
assert_equal false, @cache.key?(:a)
end
assert_size_change 1 do
assert_equal 1, @cache[:a]
assert_equal true, @cache.key?(:a)
end
end
def test_falsy_default_proc
@cache = cache_with_default_proc(nil)
assert_no_size_change do
assert_equal false, @cache.key?(:a)
end
assert_size_change 1 do
assert_equal nil, @cache[:a]
assert_equal true, @cache.key?(:a)
end
end
def test_fetch
with_or_without_default_proc do |default_proc_set|
assert_no_size_change do
assert_equal 1, @cache.fetch(:a, 1)
assert_equal(1, (@cache.fetch(:a) {1}))
assert_equal false, @cache.key?(:a)
assert_equal nil, @cache[:a] unless default_proc_set
end
@cache[:a] = 1
assert_no_size_change do
assert_equal(1, (@cache.fetch(:a) {flunk}))
end
assert_raises(ThreadSafe::Cache::KEY_ERROR) do
@cache.fetch(:b)
end
assert_no_size_change do
assert_equal 1, (@cache.fetch(:b, :c) {1}) # assert block supersedes default value argument
assert_equal false, @cache.key?(:b)
end
end
end
def test_falsy_fetch
with_or_without_default_proc do
assert_equal false, @cache.key?(:a)
assert_no_size_change do
assert_equal(nil, @cache.fetch(:a, nil))
assert_equal(false, @cache.fetch(:a, false))
assert_equal(nil, (@cache.fetch(:a) {}))
assert_equal(false, (@cache.fetch(:a) {false}))
end
@cache[:a] = nil
assert_no_size_change do
assert_equal true, @cache.key?(:a)
assert_equal(nil, (@cache.fetch(:a) {flunk}))
end
end
end
def test_fetch_with_return
with_or_without_default_proc do
r = lambda do
@cache.fetch(:a) { return 10 }
end.call
assert_no_size_change do
assert_equal 10, r
assert_equal false, @cache.key?(:a)
end
end
end
def test_fetch_or_store
with_or_without_default_proc do |default_proc_set|
assert_size_change 1 do
assert_equal 1, @cache.fetch_or_store(:a, 1)
assert_equal 1, @cache[:a]
end
@cache.delete(:a)
assert_size_change 1 do
assert_equal 1, (@cache.fetch_or_store(:a) {1})
assert_equal 1, @cache[:a]
end
assert_no_size_change do
assert_equal(1, (@cache.fetch_or_store(:a) {flunk}))
end
assert_raises(ThreadSafe::Cache::KEY_ERROR) do
@cache.fetch_or_store(:b)
end
assert_size_change 1 do
assert_equal 1, (@cache.fetch_or_store(:b, :c) {1}) # assert block supersedes default value argument
assert_equal 1, @cache[:b]
end
end
end
def test_falsy_fetch_or_store
with_or_without_default_proc do
assert_equal false, @cache.key?(:a)
assert_size_change 1 do
assert_equal(nil, @cache.fetch_or_store(:a, nil))
assert_equal nil, @cache[:a]
assert_equal true, @cache.key?(:a)
end
@cache.delete(:a)
assert_size_change 1 do
assert_equal(false, @cache.fetch_or_store(:a, false))
assert_equal false, @cache[:a]
assert_equal true, @cache.key?(:a)
end
@cache.delete(:a)
assert_size_change 1 do
assert_equal(nil, (@cache.fetch_or_store(:a) {}))
assert_equal nil, @cache[:a]
assert_equal true, @cache.key?(:a)
end
@cache.delete(:a)
assert_size_change 1 do
assert_equal(false, (@cache.fetch_or_store(:a) {false}))
assert_equal false, @cache[:a]
assert_equal true, @cache.key?(:a)
end
@cache[:a] = nil
assert_no_size_change do
assert_equal(nil, (@cache.fetch_or_store(:a) {flunk}))
end
end
end
def test_fetch_or_store_with_return
with_or_without_default_proc do
r = lambda do
@cache.fetch_or_store(:a) { return 10 }
end.call
assert_no_size_change do
assert_equal 10, r
assert_equal false, @cache.key?(:a)
end
end
end
def test_clear
@cache[:a] = 1
assert_size_change -1 do
assert_equal @cache, @cache.clear
assert_equal false, @cache.key?(:a)
assert_equal nil, @cache[:a]
end
end
def test_each_pair
@cache.each_pair {|k, v| flunk}
assert_equal(@cache, (@cache.each_pair {}))
@cache[:a] = 1
h = {}
@cache.each_pair {|k, v| h[k] = v}
assert_equal({:a => 1}, h)
@cache[:b] = 2
h = {}
@cache.each_pair {|k, v| h[k] = v}
assert_equal({:a => 1, :b => 2}, h)
end
def test_each_pair_iterator
@cache[:a] = 1
@cache[:b] = 2
i = 0
r = @cache.each_pair do |k, v|
if i == 0
i += 1
next
flunk
elsif i == 1
break :breaked
end
end
assert_equal :breaked, r
end
def test_each_pair_allows_modification
@cache[:a] = 1
@cache[:b] = 1
@cache[:c] = 1
assert_size_change 1 do
@cache.each_pair do |k, v|
@cache[:z] = 1
end
end
end
def test_keys
assert_equal [], @cache.keys
@cache[1] = 1
assert_equal [1], @cache.keys
@cache[2] = 2
assert_equal [1, 2], @cache.keys.sort
end
def test_values
assert_equal [], @cache.values
@cache[1] = 1
assert_equal [1], @cache.values
@cache[2] = 2
assert_equal [1, 2], @cache.values.sort
end
def test_each_key
assert_equal(@cache, (@cache.each_key {flunk}))
@cache[1] = 1
arr = []
@cache.each_key {|k| arr << k}
assert_equal [1], arr
@cache[2] = 2
arr = []
@cache.each_key {|k| arr << k}
assert_equal [1, 2], arr.sort
end
def test_each_value
assert_equal(@cache, (@cache.each_value {flunk}))
@cache[1] = 1
arr = []
@cache.each_value {|k| arr << k}
assert_equal [1], arr
@cache[2] = 2
arr = []
@cache.each_value {|k| arr << k}
assert_equal [1, 2], arr.sort
end
def test_empty
assert_equal true, @cache.empty?
@cache[:a] = 1
assert_equal false, @cache.empty?
end
def test_options_validation
assert_valid_options(nil)
assert_valid_options({})
assert_valid_options(:foo => :bar)
end
def test_initial_capacity_options_validation
assert_valid_option(:initial_capacity, nil)
assert_valid_option(:initial_capacity, 1)
assert_invalid_option(:initial_capacity, '')
assert_invalid_option(:initial_capacity, 1.0)
assert_invalid_option(:initial_capacity, -1)
end
def test_load_factor_options_validation
assert_valid_option(:load_factor, nil)
assert_valid_option(:load_factor, 0.01)
assert_valid_option(:load_factor, 0.75)
assert_valid_option(:load_factor, 1)
assert_invalid_option(:load_factor, '')
assert_invalid_option(:load_factor, 0)
assert_invalid_option(:load_factor, 1.1)
assert_invalid_option(:load_factor, 2)
assert_invalid_option(:load_factor, -1)
end
def test_size
assert_equal 0, @cache.size
@cache[:a] = 1
assert_equal 1, @cache.size
@cache[:b] = 1
assert_equal 2, @cache.size
@cache.delete(:a)
assert_equal 1, @cache.size
@cache.delete(:b)
assert_equal 0, @cache.size
end
def test_get_or_default
with_or_without_default_proc do
assert_equal 1, @cache.get_or_default(:a, 1)
assert_equal nil, @cache.get_or_default(:a, nil)
assert_equal false, @cache.get_or_default(:a, false)
assert_equal false, @cache.key?(:a)
@cache[:a] = 1
assert_equal 1, @cache.get_or_default(:a, 2)
end
end
def test_dup_clone
[:dup, :clone].each do |meth|
cache = cache_with_default_proc(:default_value)
cache[:a] = 1
dupped = cache.send(meth)
assert_equal 1, dupped[:a]
assert_equal 1, dupped.size
assert_size_change 1, cache do
assert_no_size_change dupped do
cache[:b] = 1
end
end
assert_equal false, dupped.key?(:b)
assert_no_size_change cache do
assert_size_change -1, dupped do
dupped.delete(:a)
end
end
assert_equal false, dupped.key?(:a)
assert_equal true, cache.key?(:a)
# test default proc
assert_size_change 1, cache do
assert_no_size_change dupped do
assert_equal :default_value, cache[:c]
assert_equal false, dupped.key?(:c)
end
end
assert_no_size_change cache do
assert_size_change 1, dupped do
assert_equal :default_value, dupped[:d]
assert_equal false, cache.key?(:d)
end
end
end
end
def test_is_unfreezable
assert_raises(NoMethodError) { @cache.freeze }
end
def test_marshal_dump_load
new_cache = Marshal.load(Marshal.dump(@cache))
assert_instance_of ThreadSafe::Cache, new_cache
assert_equal 0, new_cache.size
@cache[:a] = 1
new_cache = Marshal.load(Marshal.dump(@cache))
assert_equal 1, @cache[:a]
assert_equal 1, new_cache.size
end
def test_marshal_dump_doesnt_work_with_default_proc
assert_raises(TypeError) do
Marshal.dump(ThreadSafe::Cache.new {})
end
end
private
def with_or_without_default_proc
yield false
@cache = ThreadSafe::Cache.new {|h, k| h[k] = :default_value}
yield true
end
def cache_with_default_proc(default_value = 1)
ThreadSafe::Cache.new {|cache, k| cache[k] = default_value}
end
def assert_valid_option(option_name, value)
assert_valid_options(option_name => value)
end
def assert_valid_options(options)
c = ThreadSafe::Cache.new(options)
assert_instance_of ThreadSafe::Cache, c
end
def assert_invalid_option(option_name, value)
assert_invalid_options(option_name => value)
end
def assert_invalid_options(options)
assert_raises(ArgumentError) { ThreadSafe::Cache.new(options) }
end
def assert_size_change(change, cache = @cache)
start = cache.size
yield
assert_equal change, cache.size - start
end
def assert_no_size_change(cache = @cache, &block)
assert_size_change(0, cache, &block)
end
def assert_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
assert_no_size_change do
assert_equal(:direct_return, returning_lambda.call)
assert_equal before_had_key, @cache.key?(key)
assert_equal before_had_value, @cache[key] if before_had_value
end
end
class TestException < Exception; end
def assert_handles_exception(method, key, *args)
before_had_key = @cache.key?(key)
before_had_value = before_had_key ? @cache[key] : nil
assert_no_size_change do
assert_raises(TestException) do
@cache.send(method, key, *args) { raise TestException, '' }
end
assert_equal before_had_key, @cache.key?(key)
assert_equal before_had_value, @cache[key] if before_had_value
end
end
def assert_compute(key, expected_old_value, expected_result)
result = @cache.compute(:a) do |old_value|
assert_equal expected_old_value, old_value
yield
end
assert_equal expected_result, result
end
def assert_merge_pair(key, value, expected_old_value, expected_result)
result = @cache.merge_pair(key, value) do |old_value|
assert_equal expected_old_value, old_value
yield
end
assert_equal expected_result, result
end
def assert_collision_resistance(keys)
keys.each {|k| @cache[k] = k.key}
10.times do |i|
size = keys.size
while i < size
k = keys[i]
assert(k.key == @cache.delete(k) && !@cache.key?(k) && (@cache[k] = k.key; @cache[k] == k.key))
i += 10
end
end
assert(keys.all? {|k| @cache[k] == k.key})
end
end