# frozen_string_literal: true # Tests the base functionality that should be identical across all cache stores. module CacheStoreBehavior def test_should_read_and_write_strings assert @cache.write("foo", "bar") assert_equal "bar", @cache.read("foo") end def test_should_overwrite @cache.write("foo", "bar") @cache.write("foo", "baz") assert_equal "baz", @cache.read("foo") end def test_fetch_without_cache_miss @cache.write("foo", "bar") assert_not_called(@cache, :write) do assert_equal "bar", @cache.fetch("foo") { "baz" } end end def test_fetch_with_cache_miss assert_called_with(@cache, :write, ["foo", "baz", @cache.options]) do assert_equal "baz", @cache.fetch("foo") { "baz" } end end def test_fetch_with_cache_miss_passes_key_to_block cache_miss = false assert_equal 3, @cache.fetch("foo") { |key| cache_miss = true; key.length } assert cache_miss cache_miss = false assert_equal 3, @cache.fetch("foo") { |key| cache_miss = true; key.length } assert_not cache_miss end def test_fetch_with_forced_cache_miss @cache.write("foo", "bar") assert_not_called(@cache, :read) do assert_called_with(@cache, :write, ["foo", "bar", @cache.options.merge(force: true)]) do @cache.fetch("foo", force: true) { "bar" } end end end def test_fetch_with_cached_nil @cache.write("foo", nil) assert_not_called(@cache, :write) do assert_nil @cache.fetch("foo") { "baz" } end end def test_fetch_cache_miss_with_skip_nil assert_not_called(@cache, :write) do assert_nil @cache.fetch("foo", skip_nil: true) { nil } assert_equal false, @cache.exist?("foo") end end def test_fetch_with_forced_cache_miss_with_block @cache.write("foo", "bar") assert_equal "foo_bar", @cache.fetch("foo", force: true) { "foo_bar" } end def test_fetch_with_forced_cache_miss_without_block @cache.write("foo", "bar") assert_raises(ArgumentError) do @cache.fetch("foo", force: true) end assert_equal "bar", @cache.read("foo") end def test_should_read_and_write_hash assert @cache.write("foo", a: "b") assert_equal({ a: "b" }, @cache.read("foo")) end def test_should_read_and_write_integer assert @cache.write("foo", 1) assert_equal 1, @cache.read("foo") end def test_should_read_and_write_nil assert @cache.write("foo", nil) assert_nil @cache.read("foo") end def test_should_read_and_write_false assert @cache.write("foo", false) assert_equal false, @cache.read("foo") end def test_read_multi @cache.write("foo", "bar") @cache.write("fu", "baz") @cache.write("fud", "biz") assert_equal({ "foo" => "bar", "fu" => "baz" }, @cache.read_multi("foo", "fu")) end def test_read_multi_with_expires time = Time.now @cache.write("foo", "bar", expires_in: 10) @cache.write("fu", "baz") @cache.write("fud", "biz") Time.stub(:now, time + 11) do assert_equal({ "fu" => "baz" }, @cache.read_multi("foo", "fu")) end end def test_fetch_multi @cache.write("foo", "bar") @cache.write("fud", "biz") values = @cache.fetch_multi("foo", "fu", "fud") { |value| value * 2 } assert_equal({ "foo" => "bar", "fu" => "fufu", "fud" => "biz" }, values) assert_equal("fufu", @cache.read("fu")) end def test_fetch_multi_without_expires_in @cache.write("foo", "bar") @cache.write("fud", "biz") values = @cache.fetch_multi("foo", "fu", "fud", expires_in: nil) { |value| value * 2 } assert_equal({ "foo" => "bar", "fu" => "fufu", "fud" => "biz" }, values) assert_equal("fufu", @cache.read("fu")) end def test_fetch_multi_with_objects cache_struct = Struct.new(:cache_key, :title) foo = cache_struct.new("foo", "FOO!") bar = cache_struct.new("bar") @cache.write("bar", "BAM!") values = @cache.fetch_multi(foo, bar) { |object| object.title } assert_equal({ foo => "FOO!", bar => "BAM!" }, values) end def test_fetch_multi_returns_ordered_names @cache.write("bam", "BAM") values = @cache.fetch_multi("foo", "bar", "bam") { |key| key.upcase } assert_equal(%w(foo bar bam), values.keys) end def test_fetch_multi_without_block assert_raises(ArgumentError) do @cache.fetch_multi("foo") end end # Use strings that are guaranteed to compress well, so we can easily tell if # the compression kicked in or not. SMALL_STRING = "0" * 100 LARGE_STRING = "0" * 2.kilobytes SMALL_OBJECT = { data: SMALL_STRING } LARGE_OBJECT = { data: LARGE_STRING } def test_nil_with_default_compression_settings assert_uncompressed(nil) end def test_nil_with_compress_true assert_uncompressed(nil, compress: true) end def test_nil_with_compress_false assert_uncompressed(nil, compress: false) end def test_nil_with_compress_low_compress_threshold assert_uncompressed(nil, compress: true, compress_threshold: 1) end def test_small_string_with_default_compression_settings assert_uncompressed(SMALL_STRING) end def test_small_string_with_compress_true assert_uncompressed(SMALL_STRING, compress: true) end def test_small_string_with_compress_false assert_uncompressed(SMALL_STRING, compress: false) end def test_small_string_with_low_compress_threshold assert_compressed(SMALL_STRING, compress: true, compress_threshold: 1) end def test_small_object_with_default_compression_settings assert_uncompressed(SMALL_OBJECT) end def test_small_object_with_compress_true assert_uncompressed(SMALL_OBJECT, compress: true) end def test_small_object_with_compress_false assert_uncompressed(SMALL_OBJECT, compress: false) end def test_small_object_with_low_compress_threshold assert_compressed(SMALL_OBJECT, compress: true, compress_threshold: 1) end def test_large_string_with_default_compression_settings assert_compressed(LARGE_STRING) end def test_large_string_with_compress_true assert_compressed(LARGE_STRING, compress: true) end def test_large_string_with_compress_false assert_uncompressed(LARGE_STRING, compress: false) end def test_large_string_with_high_compress_threshold assert_uncompressed(LARGE_STRING, compress: true, compress_threshold: 1.megabyte) end def test_large_object_with_default_compression_settings assert_compressed(LARGE_OBJECT) end def test_large_object_with_compress_true assert_compressed(LARGE_OBJECT, compress: true) end def test_large_object_with_compress_false assert_uncompressed(LARGE_OBJECT, compress: false) end def test_large_object_with_high_compress_threshold assert_uncompressed(LARGE_OBJECT, compress: true, compress_threshold: 1.megabyte) end def test_incompressable_data assert_uncompressed(nil, compress: true, compress_threshold: 1) assert_uncompressed(true, compress: true, compress_threshold: 1) assert_uncompressed(false, compress: true, compress_threshold: 1) assert_uncompressed(0, compress: true, compress_threshold: 1) assert_uncompressed(1.2345, compress: true, compress_threshold: 1) assert_uncompressed("", compress: true, compress_threshold: 1) incompressible = nil # generate an incompressible string loop do incompressible = SecureRandom.random_bytes(1.kilobyte) break if incompressible.bytesize < Zlib::Deflate.deflate(incompressible).bytesize end assert_uncompressed(incompressible, compress: true, compress_threshold: 1) end def test_cache_key obj = Object.new def obj.cache_key :foo end @cache.write(obj, "bar") assert_equal "bar", @cache.read("foo") end def test_param_as_cache_key obj = Object.new def obj.to_param "foo" end @cache.write(obj, "bar") assert_equal "bar", @cache.read("foo") end def test_unversioned_cache_key obj = Object.new def obj.cache_key "foo" end def obj.cache_key_with_version "foo-v1" end @cache.write(obj, "bar") assert_equal "bar", @cache.read("foo") end def test_array_as_cache_key @cache.write([:fu, "foo"], "bar") assert_equal "bar", @cache.read("fu/foo") end InstanceTest = Struct.new(:name, :id) do def cache_key "#{name}/#{id}" end def to_param "hello" end end def test_array_with_single_instance_as_cache_key_uses_cache_key_method test_instance_one = InstanceTest.new("test", 1) test_instance_two = InstanceTest.new("test", 2) @cache.write([test_instance_one], "one") @cache.write([test_instance_two], "two") assert_equal "one", @cache.read([test_instance_one]) assert_equal "two", @cache.read([test_instance_two]) end def test_array_with_multiple_instances_as_cache_key_uses_cache_key_method test_instance_one = InstanceTest.new("test", 1) test_instance_two = InstanceTest.new("test", 2) test_instance_three = InstanceTest.new("test", 3) @cache.write([test_instance_one, test_instance_three], "one") @cache.write([test_instance_two, test_instance_three], "two") assert_equal "one", @cache.read([test_instance_one, test_instance_three]) assert_equal "two", @cache.read([test_instance_two, test_instance_three]) end def test_format_of_expanded_key_for_single_instance test_instance_one = InstanceTest.new("test", 1) expanded_key = @cache.send(:expanded_key, test_instance_one) assert_equal expanded_key, test_instance_one.cache_key end def test_format_of_expanded_key_for_single_instance_in_array test_instance_one = InstanceTest.new("test", 1) expanded_key = @cache.send(:expanded_key, [test_instance_one]) assert_equal expanded_key, test_instance_one.cache_key end def test_hash_as_cache_key @cache.write({ foo: 1, fu: 2 }, "bar") assert_equal "bar", @cache.read("foo=1/fu=2") end def test_keys_are_case_sensitive @cache.write("foo", "bar") assert_nil @cache.read("FOO") end def test_exist @cache.write("foo", "bar") assert_equal true, @cache.exist?("foo") assert_equal false, @cache.exist?("bar") end def test_nil_exist @cache.write("foo", nil) assert @cache.exist?("foo") end def test_delete @cache.write("foo", "bar") assert @cache.exist?("foo") assert @cache.delete("foo") assert_not @cache.exist?("foo") end def test_original_store_objects_should_not_be_immutable bar = +"bar" @cache.write("foo", bar) assert_nothing_raised { bar.gsub!(/.*/, "baz") } end def test_expires_in time = Time.local(2008, 4, 24) Time.stub(:now, time) do @cache.write("foo", "bar") assert_equal "bar", @cache.read("foo") end Time.stub(:now, time + 30) do assert_equal "bar", @cache.read("foo") end Time.stub(:now, time + 61) do assert_nil @cache.read("foo") end end def test_race_condition_protection_skipped_if_not_defined @cache.write("foo", "bar") time = @cache.send(:read_entry, @cache.send(:normalize_key, "foo", {}), {}).expires_at Time.stub(:now, Time.at(time)) do result = @cache.fetch("foo") do assert_nil @cache.read("foo") "baz" end assert_equal "baz", result end end def test_race_condition_protection_is_limited time = Time.now @cache.write("foo", "bar", expires_in: 60) Time.stub(:now, time + 71) do result = @cache.fetch("foo", race_condition_ttl: 10) do assert_nil @cache.read("foo") "baz" end assert_equal "baz", result end end def test_race_condition_protection_is_safe time = Time.now @cache.write("foo", "bar", expires_in: 60) Time.stub(:now, time + 61) do begin @cache.fetch("foo", race_condition_ttl: 10) do assert_equal "bar", @cache.read("foo") raise ArgumentError.new end rescue ArgumentError end assert_equal "bar", @cache.read("foo") end Time.stub(:now, time + 91) do assert_nil @cache.read("foo") end end def test_race_condition_protection time = Time.now @cache.write("foo", "bar", expires_in: 60) Time.stub(:now, time + 61) do result = @cache.fetch("foo", race_condition_ttl: 10) do assert_equal "bar", @cache.read("foo") "baz" end assert_equal "baz", result end end def test_crazy_key_characters crazy_key = "#/:*(<+=> )&$%@?;'\"\'`~-" assert @cache.write(crazy_key, "1", raw: true) assert_equal "1", @cache.read(crazy_key) assert_equal "1", @cache.fetch(crazy_key) assert @cache.delete(crazy_key) assert_equal "2", @cache.fetch(crazy_key, raw: true) { "2" } assert_equal 3, @cache.increment(crazy_key) assert_equal 2, @cache.decrement(crazy_key) end def test_really_long_keys key = "x" * 2048 assert @cache.write(key, "bar") assert_equal "bar", @cache.read(key) assert_equal "bar", @cache.fetch(key) assert_nil @cache.read("#{key}x") assert_equal({ key => "bar" }, @cache.read_multi(key)) assert @cache.delete(key) end def test_cache_hit_instrumentation key = "test_key" @events = [] ActiveSupport::Notifications.subscribe "cache_read.active_support" do |*args| @events << ActiveSupport::Notifications::Event.new(*args) end assert @cache.write(key, "1", raw: true) assert @cache.fetch(key) { } assert_equal 1, @events.length assert_equal "cache_read.active_support", @events[0].name assert_equal :fetch, @events[0].payload[:super_operation] assert @events[0].payload[:hit] ensure ActiveSupport::Notifications.unsubscribe "cache_read.active_support" end def test_cache_miss_instrumentation @events = [] ActiveSupport::Notifications.subscribe(/^cache_(.*)\.active_support$/) do |*args| @events << ActiveSupport::Notifications::Event.new(*args) end assert_not @cache.fetch("bad_key") { } assert_equal 3, @events.length assert_equal "cache_read.active_support", @events[0].name assert_equal "cache_generate.active_support", @events[1].name assert_equal "cache_write.active_support", @events[2].name assert_equal :fetch, @events[0].payload[:super_operation] assert_not @events[0].payload[:hit] ensure ActiveSupport::Notifications.unsubscribe "cache_read.active_support" end private def assert_compressed(value, **options) assert_compression(true, value, **options) end def assert_uncompressed(value, **options) assert_compression(false, value, **options) end def assert_compression(should_compress, value, **options) freeze_time do @cache.write("actual", value, options) @cache.write("uncompressed", value, options.merge(compress: false)) end if value.nil? assert_nil @cache.read("actual") assert_nil @cache.read("uncompressed") else assert_equal value, @cache.read("actual") assert_equal value, @cache.read("uncompressed") end actual_entry = @cache.send(:read_entry, @cache.send(:normalize_key, "actual", {}), {}) uncompressed_entry = @cache.send(:read_entry, @cache.send(:normalize_key, "uncompressed", {}), {}) actual_size = Marshal.dump(actual_entry).bytesize uncompressed_size = Marshal.dump(uncompressed_entry).bytesize if should_compress assert_operator actual_size, :<, uncompressed_size, "value should be compressed" else assert_equal uncompressed_size, actual_size, "value should not be compressed" end end end