From f9cb278a7ec62582987cc3ed627903d007cb8a28 Mon Sep 17 00:00:00 2001 From: Matt Dick Date: Wed, 18 Feb 2026 12:38:34 -0800 Subject: [PATCH 1/3] [NOTIX] Add missing test coverage --- package-lock.json | 6 + test/test_dalli.rb | 184 ++++++++++++++++ test/test_dalli_module.rb | 63 ++++++ test/test_pid_cache.rb | 61 ++++++ test/test_ring.rb | 79 +++++++ test/test_server.rb | 438 ++++++++++++++++++++++++++++++++++++++ test/test_socket.rb | 225 ++++++++++++++++++++ test/test_threadsafe.rb | 56 +++++ 8 files changed, 1112 insertions(+) create mode 100644 package-lock.json create mode 100644 test/test_dalli_module.rb create mode 100644 test/test_pid_cache.rb create mode 100644 test/test_socket.rb create mode 100644 test/test_threadsafe.rb diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..1224a978 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "braze_dalli", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/test/test_dalli.rb b/test/test_dalli.rb index 4f1432b3..9fe50390 100644 --- a/test/test_dalli.rb +++ b/test/test_dalli.rb @@ -120,6 +120,190 @@ assert_equal "server2.example.com", s2 end + it "accept array with comma separated strings" do + dc = Dalli::Client.new(["server1.example.com:11211,server2.example.com:11211", "server3.example.com:11211"]) + ring = dc.send(:ring) + assert_equal 3, ring.servers.size + end + + it "use MEMCACHE_SERVERS env variable when no servers specified" do + old = ENV["MEMCACHE_SERVERS"] + ENV["MEMCACHE_SERVERS"] = "env-server.example.com:11211" + dc = Dalli::Client.new(nil) + ring = dc.send(:ring) + assert_equal "env-server.example.com", ring.servers.first.hostname + dc.close + ensure + ENV["MEMCACHE_SERVERS"] = old + end + + describe 'multi block' do + it 'sets and restores Thread.current[:dalli_multi]' do + dc = Dalli::Client.new('localhost:11211') + assert_nil Thread.current[:dalli_multi] + dc.multi do + assert_equal true, Thread.current[:dalli_multi] + end + assert_nil Thread.current[:dalli_multi] + end + + it 'restores previous multi state on nested calls' do + dc = Dalli::Client.new('localhost:11211') + dc.multi do + assert_equal true, Thread.current[:dalli_multi] + dc.multi do + assert_equal true, Thread.current[:dalli_multi] + end + assert_equal true, Thread.current[:dalli_multi] + end + assert_nil Thread.current[:dalli_multi] + end + + it 'restores state even when block raises' do + dc = Dalli::Client.new('localhost:11211') + begin + dc.multi do + raise RuntimeError, 'boom' + end + rescue RuntimeError + end + assert_nil Thread.current[:dalli_multi] + end + end + + describe '#with' do + it 'yields self' do + dc = Dalli::Client.new('localhost:11211') + dc.with do |client| + assert_same dc, client + end + end + + it 'returns block result' do + dc = Dalli::Client.new('localhost:11211') + result = dc.with { |c| 42 } + assert_equal 42, result + end + end + + describe '#close and #reset' do + it 'close resets the ring to nil' do + dc = Dalli::Client.new('localhost:11211') + dc.send(:ring) + refute_nil dc.instance_variable_get(:@ring) + dc.close + assert_nil dc.instance_variable_get(:@ring) + end + + it 'reset is an alias for close' do + dc = Dalli::Client.new('localhost:11211') + assert_equal dc.method(:close), dc.method(:reset) + end + + it 'close is safe when ring is nil' do + dc = Dalli::Client.new('localhost:11211') + dc.close + dc.close + end + end + + describe 'key helpers' do + it 'key_with_namespace prepends namespace' do + dc = Dalli::Client.new('localhost:11211', namespace: 'ns') + assert_equal 'ns:mykey', dc.send(:key_with_namespace, 'mykey') + end + + it 'key_with_namespace returns key when no namespace' do + dc = Dalli::Client.new('localhost:11211') + assert_equal 'mykey', dc.send(:key_with_namespace, 'mykey') + end + + it 'key_without_namespace strips namespace prefix' do + dc = Dalli::Client.new('localhost:11211', namespace: 'ns') + assert_equal 'mykey', dc.send(:key_without_namespace, 'ns:mykey') + end + + it 'key_without_namespace returns key when no namespace' do + dc = Dalli::Client.new('localhost:11211') + assert_equal 'mykey', dc.send(:key_without_namespace, 'mykey') + end + + it 'namespace returns string for symbol namespace' do + dc = Dalli::Client.new('localhost:11211', namespace: :test_ns) + assert_equal 'test_ns', dc.send(:namespace) + end + + it 'namespace calls proc when namespace is a Proc' do + call_count = 0 + dc = Dalli::Client.new('localhost:11211', namespace: Proc.new { call_count += 1; 'dynamic' }) + assert_equal 'dynamic', dc.send(:namespace) + assert_equal 1, call_count + end + + it 'namespace returns nil when not configured' do + dc = Dalli::Client.new('localhost:11211') + assert_nil dc.send(:namespace) + end + end + + describe 'ttl_or_default' do + it 'returns the given ttl as integer' do + dc = Dalli::Client.new('localhost:11211') + assert_equal 300, dc.send(:ttl_or_default, 300) + end + + it 'returns default expires_in when ttl is nil' do + dc = Dalli::Client.new('localhost:11211', expires_in: 600) + assert_equal 600, dc.send(:ttl_or_default, nil) + end + + it 'returns 0 when both ttl and expires_in are nil' do + dc = Dalli::Client.new('localhost:11211') + assert_equal 0, dc.send(:ttl_or_default, nil) + end + + it 'raises ArgumentError for unconvertible ttl' do + dc = Dalli::Client.new('localhost:11211') + assert_raises ArgumentError do + dc.send(:ttl_or_default, []) + end + end + end + + describe 'normalize_options' do + it 'converts :compression to :compress' do + dc = Dalli::Client.new('localhost:11211', compression: true) + opts = dc.instance_variable_get(:@options) + assert opts[:compress] + refute opts.key?(:compression) + end + + it 'converts expires_in to integer' do + dc = Dalli::Client.new('localhost:11211', expires_in: '300') + assert_equal 300, dc.instance_variable_get(:@options)[:expires_in] + end + end + + describe 'validate_key' do + it 'truncates keys longer than 250 chars with md5 hash' do + dc = Dalli::Client.new('localhost:11211') + long_key = 'x' * 300 + result = dc.send(:validate_key, long_key) + assert_operator result.length, :<=, 250 + assert_includes result, ':md5:' + end + + it 'uses custom digest_class for long keys' do + require 'openssl' + dc = Dalli::Client.new('localhost:11211', digest_class: OpenSSL::Digest::SHA1) + long_key = 'y' * 300 + result = dc.send(:validate_key, long_key) + assert_includes result, ':md5:' + sha1_hex = OpenSSL::Digest::SHA1.hexdigest(long_key) + assert_includes result, sha1_hex + end + end + describe 'using a live server' do it "support get/set" do diff --git a/test/test_dalli_module.rb b/test/test_dalli_module.rb new file mode 100644 index 00000000..a8939afa --- /dev/null +++ b/test/test_dalli_module.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true +require_relative 'helper' + +describe 'Dalli module' do + describe 'version' do + it 'is defined' do + refute_nil Dalli::VERSION + end + + it 'is a string' do + assert_kind_of String, Dalli::VERSION + end + end + + describe 'error classes' do + it 'defines DalliError as a RuntimeError' do + assert Dalli::DalliError < RuntimeError + end + + it 'defines NetworkError as a DalliError' do + assert Dalli::NetworkError < Dalli::DalliError + end + + it 'defines RingError as a DalliError' do + assert Dalli::RingError < Dalli::DalliError + end + + it 'defines MarshalError as a DalliError' do + assert Dalli::MarshalError < Dalli::DalliError + end + + it 'defines UnmarshalError as a DalliError' do + assert Dalli::UnmarshalError < Dalli::DalliError + end + + it 'defines ValueOverMaxSize as a DalliError' do + assert Dalli::ValueOverMaxSize < Dalli::DalliError + end + end + + describe 'logger' do + after do + Dalli.logger = Logger.new(STDOUT) + Dalli.logger.level = Logger::ERROR + end + + it 'has a default logger' do + Dalli.instance_variable_set(:@logger, nil) + refute_nil Dalli.logger + end + + it 'allows setting a custom logger' do + custom = Logger.new(nil) + Dalli.logger = custom + assert_equal custom, Dalli.logger + end + + it 'default_logger creates an INFO-level STDOUT logger' do + logger = Dalli.default_logger + assert_equal Logger::INFO, logger.level + end + end +end diff --git a/test/test_pid_cache.rb b/test/test_pid_cache.rb new file mode 100644 index 00000000..33326888 --- /dev/null +++ b/test/test_pid_cache.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true +require_relative 'helper' + +describe Dalli::PIDCache do + it 'returns the current process PID' do + assert_equal Process.pid, Dalli::PIDCache.pid + end + + it 'responds to .pid' do + assert_respond_to Dalli::PIDCache, :pid + end + + if Process.respond_to?(:_fork) + describe 'on Ruby 3.1+ with Process._fork' do + it 'responds to update!' do + assert_respond_to Dalli::PIDCache, :update! + end + + it 'updates pid to match Process.pid' do + Dalli::PIDCache.update! + assert_equal Process.pid, Dalli::PIDCache.pid + end + + it 'has CoreExt prepended on Process' do + assert Process.singleton_class.ancestors.include?(Dalli::PIDCache::CoreExt) + end + + if Process.respond_to?(:fork) + it 'resets pid cache in child after fork' do + parent_pid = Process.pid + + rd, wr = IO.pipe + child_pid = Process.fork do + rd.close + wr.write Dalli::PIDCache.pid.to_s + wr.close + end + wr.close + cached_pid_in_child = rd.read.to_i + rd.close + Process.wait(child_pid) + + assert_equal child_pid, cached_pid_in_child + refute_equal parent_pid, cached_pid_in_child + end + end + end + elsif !Process.respond_to?(:fork) + describe 'on JRuby/TruffleRuby without fork' do + it 'returns a fixed pid' do + assert_equal Process.pid, Dalli::PIDCache.pid + end + end + else + describe 'on Ruby 3.0 or older' do + it 'delegates directly to Process.pid' do + assert_equal Process.pid, Dalli::PIDCache.pid + end + end + end +end diff --git a/test/test_ring.rb b/test/test_ring.rb index 5618d51e..7fcb045a 100644 --- a/test/test_ring.rb +++ b/test/test_ring.rb @@ -83,4 +83,83 @@ end end end + + describe 'Ring::Entry' do + it 'stores value and server' do + server = stub(:name => 'localhost:11211') + entry = Dalli::Ring::Entry.new(12345, server) + assert_equal 12345, entry.value + assert_equal server, entry.server + end + end + + describe 'continuum with weighted servers' do + it 'gives more entries to higher-weight servers' do + light = stub(:name => 'light:11211', :weight => 1) + heavy = stub(:name => 'heavy:11211', :weight => 3) + ring = Dalli::Ring.new([light, heavy], {}) + + light_count = ring.continuum.count { |e| e.server == light } + heavy_count = ring.continuum.count { |e| e.server == heavy } + assert_operator heavy_count, :>, light_count + end + + it 'produces proportional entry counts' do + s1 = stub(:name => 'a:11211', :weight => 1) + s2 = stub(:name => 'b:11211', :weight => 2) + ring = Dalli::Ring.new([s1, s2], {}) + + s1_count = ring.continuum.count { |e| e.server == s1 } + s2_count = ring.continuum.count { |e| e.server == s2 } + ratio = s2_count.to_f / s1_count + assert_in_delta 2.0, ratio, 0.1 + end + end + + describe 'failover disabled' do + it 'raises RingError immediately when failover is false and server is down' do + servers = [ + Dalli::Server.new("localhost:12345"), + Dalli::Server.new("localhost:12346"), + ] + ring = Dalli::Ring.new(servers, { failover: false }) + assert_raises Dalli::RingError do + ring.server_for_key('test') + end + end + end + + describe 'lock' do + it 'locks and unlocks all servers around a block' do + s1 = stub(:name => 'a:11211', :weight => 1) + s2 = stub(:name => 'b:11211', :weight => 1) + s1.expects(:lock!) + s1.expects(:unlock!) + s2.expects(:lock!) + s2.expects(:unlock!) + ring = Dalli::Ring.new([s1, s2], { threadsafe: false }) + + result = ring.lock { 42 } + assert_equal 42, result + end + + it 'unlocks servers even when block raises' do + s1 = stub(:name => 'a:11211', :weight => 1) + s1.expects(:lock!) + s1.expects(:unlock!) + ring = Dalli::Ring.new([s1], { threadsafe: false }) + + assert_raises RuntimeError do + ring.lock { raise RuntimeError, 'boom' } + end + end + end + + describe 'single server without continuum' do + it 'does not create a continuum' do + server = stub(:name => 'localhost:11211', :weight => 1) + ring = Dalli::Ring.new([server], { threadsafe: false }) + assert_nil ring.continuum + end + end end diff --git a/test/test_server.rb b/test/test_server.rb index f26338d9..e4d17a2b 100644 --- a/test/test_server.rb +++ b/test/test_server.rb @@ -3,6 +3,444 @@ require_relative 'helper' describe Dalli::Server do + describe 'name' do + it 'returns hostname:port for tcp sockets' do + s = Dalli::Server.new('localhost:11211') + assert_equal 'localhost:11211', s.name + end + + it 'returns hostname for unix sockets' do + s = Dalli::Server.new('/var/run/memcached/sock') + assert_equal '/var/run/memcached/sock', s.name + end + + it 'uses default port in name when not specified' do + s = Dalli::Server.new('localhost') + assert_equal 'localhost:11211', s.name + end + end + + describe 'initialization defaults' do + it 'sets default options' do + s = Dalli::Server.new('localhost') + assert_equal 60, s.options[:down_retry_delay] + assert_equal 0.5, s.options[:socket_timeout] + assert_equal 2, s.options[:socket_max_failures] + assert_equal 0.01, s.options[:socket_failure_delay] + assert_equal 1024 * 1024, s.options[:value_max_bytes] + assert_equal false, s.options[:error_when_over_max_size] + assert_equal Dalli::Compressor, s.options[:compressor] + assert_equal 1024, s.options[:compression_min_size] + assert_equal false, s.options[:compression_max_size] + assert_equal Marshal, s.options[:serializer] + assert_nil s.options[:username] + assert_nil s.options[:password] + assert_equal true, s.options[:keepalive] + end + + it 'merges custom options with defaults' do + s = Dalli::Server.new('localhost', socket_timeout: 2.0, keepalive: false) + assert_equal 2.0, s.options[:socket_timeout] + assert_equal false, s.options[:keepalive] + assert_equal 60, s.options[:down_retry_delay] + end + + it 'starts with nil sock' do + s = Dalli::Server.new('localhost') + assert_nil s.sock + end + end + + describe 'serializer and compressor accessors' do + it 'returns the configured serializer' do + s = Dalli::Server.new('localhost') + assert_equal Marshal, s.serializer + end + + it 'returns the configured compressor' do + s = Dalli::Server.new('localhost') + assert_equal Dalli::Compressor, s.compressor + end + + it 'returns custom serializer' do + custom = Object.new + s = Dalli::Server.new('localhost', serializer: custom) + assert_equal custom, s.serializer + end + + it 'returns custom compressor' do + custom = Object.new + s = Dalli::Server.new('localhost', compressor: custom) + assert_equal custom, s.compressor + end + end + + describe 'close' do + it 'is a no-op when sock is nil' do + s = Dalli::Server.new('localhost') + assert_nil s.sock + s.close + assert_nil s.sock + end + + it 'closes the socket and resets state' do + s = Dalli::Server.new('localhost') + mock_sock = mock('socket') + mock_sock.expects(:close) + s.instance_variable_set(:@sock, mock_sock) + s.instance_variable_set(:@pid, Process.pid) + + s.close + + assert_nil s.sock + assert_nil s.instance_variable_get(:@pid) + assert_equal false, s.instance_variable_get(:@inprogress) + end + end + + describe 'alive?' do + it 'returns true when sock is present' do + s = Dalli::Server.new('localhost') + s.instance_variable_set(:@sock, stub('socket')) + assert_equal true, s.alive? + end + + it 'returns false during down_retry_delay period' do + s = Dalli::Server.new('localhost', down_retry_delay: 60) + s.instance_variable_set(:@last_down_at, Time.now) + assert_equal false, s.alive? + end + + it 'attempts reconnect after down_retry_delay expires' do + s = Dalli::Server.new('localhost:19999', down_retry_delay: 0) + s.instance_variable_set(:@last_down_at, Time.now - 1) + result = s.alive? + assert_equal false, result + end + end + + describe 'multi_response_completed?' do + it 'returns true when multi_buffer is nil' do + s = Dalli::Server.new('localhost') + s.instance_variable_set(:@multi_buffer, nil) + assert_equal true, s.multi_response_completed? + end + + it 'returns false when multi_buffer has content' do + s = Dalli::Server.new('localhost') + s.instance_variable_set(:@multi_buffer, 'data') + assert_equal false, s.multi_response_completed? + end + end + + describe 'multi_response_abort' do + it 'clears multi_buffer and position' do + s = Dalli::Server.new('localhost:19999') + s.instance_variable_set(:@multi_buffer, 'data') + s.instance_variable_set(:@position, 10) + s.instance_variable_set(:@inprogress, true) + + s.multi_response_abort + + assert_nil s.instance_variable_get(:@multi_buffer) + assert_nil s.instance_variable_get(:@position) + assert_equal false, s.instance_variable_get(:@inprogress) + end + end + + describe 'request error handling' do + it 'closes socket on Timeout::Error and re-raises' do + memcached_persistent do |dc| + ring = dc.send(:ring) + server = ring.servers.first + assert server.alive? + + server.stubs(:verify_state) + server.stubs(:get).raises(Timeout::Error.new('IO timeout')) + + assert_raises Timeout::Error do + server.request(:get, 'key') + end + + assert_nil server.sock + end + end + + it 'returns false on MarshalError' do + memcached_persistent do |dc| + ring = dc.send(:ring) + server = ring.servers.first + assert server.alive? + + server.stubs(:verify_state) + server.stubs(:set).raises(Dalli::MarshalError.new('cannot dump')) + + with_nil_logger do + result = server.request(:set, 'key', 'value') + assert_equal false, result + end + end + end + end + + describe 'serialize' do + subject { Dalli::Server.new('127.0.0.1') } + + it 'serializes with Marshal and sets FLAG_SERIALIZED' do + value, flags = subject.send(:serialize, 'key', 'test_value') + assert_equal Dalli::Server::FLAG_SERIALIZED, flags & Dalli::Server::FLAG_SERIALIZED + assert_equal 'test_value', Marshal.load(value) + end + + it 'does not serialize raw values' do + value, flags = subject.send(:serialize, 'key', 'raw_value', { raw: true }) + assert_equal 0, flags & Dalli::Server::FLAG_SERIALIZED + assert_equal 'raw_value', value + end + + it 'converts raw values to string' do + value, flags = subject.send(:serialize, 'key', 12345, { raw: true }) + assert_equal '12345', value + assert_equal 0, flags & Dalli::Server::FLAG_SERIALIZED + end + + it 'compresses large values when compress is enabled' do + s = Dalli::Server.new('127.0.0.1', compress: true, compression_min_size: 10) + large_value = 'x' * 100 + value, flags = s.send(:serialize, 'key', large_value, { raw: true }) + assert_equal Dalli::Server::FLAG_COMPRESSED, flags & Dalli::Server::FLAG_COMPRESSED + assert_operator value.bytesize, :<, large_value.bytesize + end + + it 'does not compress values below compression_min_size' do + s = Dalli::Server.new('127.0.0.1', compress: true, compression_min_size: 1024) + _value, flags = s.send(:serialize, 'key', 'small', { raw: true }) + assert_equal 0, flags & Dalli::Server::FLAG_COMPRESSED + end + + it 'does not compress values above compression_max_size' do + s = Dalli::Server.new('127.0.0.1', compress: true, compression_min_size: 10, compression_max_size: 50) + large_value = 'x' * 100 + _value, flags = s.send(:serialize, 'key', large_value, { raw: true }) + assert_equal 0, flags & Dalli::Server::FLAG_COMPRESSED + end + + it 'compresses when per-request :compress option is set' do + s = Dalli::Server.new('127.0.0.1', compression_min_size: 10) + large_value = 'x' * 100 + _value, flags = s.send(:serialize, 'key', large_value, { raw: true, compress: true }) + assert_equal Dalli::Server::FLAG_COMPRESSED, flags & Dalli::Server::FLAG_COMPRESSED + end + + it 'sets both flags when value is serialized and compressed' do + s = Dalli::Server.new('127.0.0.1', compress: true, compression_min_size: 10) + large_value = 'x' * 100 + _value, flags = s.send(:serialize, 'key', large_value) + expected = Dalli::Server::FLAG_SERIALIZED | Dalli::Server::FLAG_COMPRESSED + assert_equal expected, flags & expected + end + + it 'wraps serialization errors as MarshalError' do + assert_raises Dalli::MarshalError do + subject.send(:serialize, 'key', Proc.new { true }) + end + end + end + + describe 'deserialize' do + subject { Dalli::Server.new('127.0.0.1') } + + it 'returns raw value when no flags set' do + value = subject.send(:deserialize, 'plain', 0) + assert_equal 'plain', value + end + + it 'decompresses compressed values' do + original = 'test data' + compressed = Dalli::Compressor.compress(original) + value = subject.send(:deserialize, compressed, Dalli::Server::FLAG_COMPRESSED) + assert_equal original, value + end + + it 'decompresses and deserializes when both flags set' do + original = { key: 'value' } + serialized = Marshal.dump(original) + compressed = Dalli::Compressor.compress(serialized) + flags = Dalli::Server::FLAG_SERIALIZED | Dalli::Server::FLAG_COMPRESSED + value = subject.send(:deserialize, compressed, flags) + assert_equal original, value + end + + it 'raises UnmarshalError for Zlib decompression errors' do + assert_raises Dalli::UnmarshalError do + subject.send(:deserialize, 'not compressed', Dalli::Server::FLAG_COMPRESSED) + end + end + + it 'raises UnmarshalError for marshal data too short' do + assert_raises Dalli::UnmarshalError do + subject.send(:deserialize, '', Dalli::Server::FLAG_SERIALIZED) + end + end + end + + describe 'verify_state' do + it 'raises NetworkError when inprogress is true' do + s = Dalli::Server.new('localhost') + s.instance_variable_set(:@inprogress, true) + assert_raises Dalli::NetworkError do + s.send(:verify_state) + end + end + + it 'does nothing when state is clean' do + s = Dalli::Server.new('localhost') + s.instance_variable_set(:@inprogress, false) + s.instance_variable_set(:@pid, nil) + s.send(:verify_state) + end + + it 'reconnects when pid changes (fork detection)' do + s = Dalli::Server.new('localhost') + s.instance_variable_set(:@pid, -1) + s.instance_variable_set(:@inprogress, false) + assert_raises Dalli::NetworkError do + s.send(:verify_state) + end + end + end + + describe 'failure!' do + it 'reconnects when fail_count is below max' do + s = Dalli::Server.new('localhost') + s.instance_variable_set(:@fail_count, 0) + assert_raises Dalli::NetworkError do + s.send(:failure!, RuntimeError.new('test')) + end + assert_equal 1, s.instance_variable_get(:@fail_count) + end + + it 'marks server as down when fail_count reaches max' do + s = Dalli::Server.new('localhost', socket_max_failures: 1) + s.instance_variable_set(:@fail_count, 0) + assert_raises Dalli::NetworkError do + s.send(:failure!, RuntimeError.new('test')) + end + end + end + + describe 'down! and up!' do + it 'down! sets last_down_at and raises NetworkError' do + s = Dalli::Server.new('localhost') + assert_raises Dalli::NetworkError do + s.send(:down!) + end + refute_nil s.instance_variable_get(:@last_down_at) + refute_nil s.instance_variable_get(:@down_at) + end + + it 'up! clears failure state' do + s = Dalli::Server.new('localhost') + s.instance_variable_set(:@fail_count, 5) + s.instance_variable_set(:@down_at, Time.now) + s.instance_variable_set(:@last_down_at, Time.now) + s.instance_variable_set(:@msg, 'error') + s.instance_variable_set(:@error, 'RuntimeError') + + s.send(:up!) + + assert_equal 0, s.instance_variable_get(:@fail_count) + assert_nil s.instance_variable_get(:@down_at) + assert_nil s.instance_variable_get(:@last_down_at) + assert_nil s.instance_variable_get(:@msg) + assert_nil s.instance_variable_get(:@error) + end + end + + describe 'multi?' do + it 'returns true when Thread.current[:dalli_multi] is set' do + s = Dalli::Server.new('localhost') + Thread.current[:dalli_multi] = true + assert_equal true, s.send(:multi?) + Thread.current[:dalli_multi] = nil + end + + it 'returns nil when not in multi block' do + s = Dalli::Server.new('localhost') + Thread.current[:dalli_multi] = nil + assert_nil s.send(:multi?) + end + end + + describe 'split' do + it 'splits 64-bit integers into high and low 32-bit parts' do + s = Dalli::Server.new('localhost') + h, l = s.send(:split, 0x100000001) + assert_equal 1, h + assert_equal 1, l + end + + it 'handles zero' do + s = Dalli::Server.new('localhost') + h, l = s.send(:split, 0) + assert_equal 0, h + assert_equal 0, l + end + + it 'handles max 32-bit value' do + s = Dalli::Server.new('localhost') + h, l = s.send(:split, 0xFFFFFFFF) + assert_equal 0, h + assert_equal 0xFFFFFFFF, l + end + end + + describe 'need_auth?' do + it 'returns truthy when username option is set' do + s = Dalli::Server.new('localhost', username: 'user') + assert s.send(:need_auth?) + end + + it 'returns falsey when no credentials configured' do + old_user = ENV['MEMCACHE_USERNAME'] + ENV['MEMCACHE_USERNAME'] = nil + s = Dalli::Server.new('localhost') + refute s.send(:need_auth?) + ensure + ENV['MEMCACHE_USERNAME'] = old_user + end + + it 'returns truthy when MEMCACHE_USERNAME env var is set' do + old_user = ENV['MEMCACHE_USERNAME'] + ENV['MEMCACHE_USERNAME'] = 'envuser' + s = Dalli::Server.new('localhost') + assert s.send(:need_auth?) + ensure + ENV['MEMCACHE_USERNAME'] = old_user + end + end + + describe 'username and password' do + it 'returns option values' do + s = Dalli::Server.new('localhost', username: 'user', password: 'pass') + assert_equal 'user', s.send(:username) + assert_equal 'pass', s.send(:password) + end + + it 'falls back to environment variables' do + old_user = ENV['MEMCACHE_USERNAME'] + old_pass = ENV['MEMCACHE_PASSWORD'] + ENV['MEMCACHE_USERNAME'] = 'envuser' + ENV['MEMCACHE_PASSWORD'] = 'envpass' + s = Dalli::Server.new('localhost') + assert_equal 'envuser', s.send(:username) + assert_equal 'envpass', s.send(:password) + ensure + ENV['MEMCACHE_USERNAME'] = old_user + ENV['MEMCACHE_PASSWORD'] = old_pass + end + end + describe 'hostname parsing' do it 'handles unix socket with no weight' do s = Dalli::Server.new('/var/run/memcached/sock') diff --git a/test/test_socket.rb b/test/test_socket.rb new file mode 100644 index 00000000..e1129011 --- /dev/null +++ b/test/test_socket.rb @@ -0,0 +1,225 @@ +# frozen_string_literal: true +require_relative 'helper' + +class MockSocket + include Dalli::Socket::InstanceMethods + attr_accessor :options, :read_results + + def initialize(options = {}) + @options = options + @read_results = [] + @read_index = 0 + end + + def read_nonblock(_count, exception: true) + result = @read_results[@read_index] + @read_index += 1 + result + end +end + +describe 'Dalli::Socket::InstanceMethods' do + let(:sock) { MockSocket.new(socket_timeout: 1) } + + describe '#readfull' do + it 'reads the exact number of bytes requested' do + sock.read_results = ["hello"] + assert_equal "hello", sock.readfull(5) + end + + it 'accumulates data across multiple reads' do + sock.read_results = ["he", "llo"] + assert_equal "hello", sock.readfull(5) + end + + it 'retries on :wait_readable when IO.select succeeds' do + sock.read_results = [:wait_readable, "hello"] + IO.stubs(:select).with([sock], nil, nil, 1).returns([[sock]]) + assert_equal "hello", sock.readfull(5) + end + + it 'retries on :wait_writable when IO.select succeeds' do + sock.read_results = [:wait_writable, "hello"] + IO.stubs(:select).with(nil, [sock], nil, 1).returns([nil, [sock]]) + assert_equal "hello", sock.readfull(5) + end + + it 'raises Timeout::Error on :wait_readable when IO.select times out' do + sock.read_results = [:wait_readable] + IO.stubs(:select).with([sock], nil, nil, 1).returns(nil) + assert_raises(Timeout::Error) { sock.readfull(5) } + end + + it 'raises Timeout::Error on :wait_writable when IO.select times out' do + sock.read_results = [:wait_writable] + IO.stubs(:select).with(nil, [sock], nil, 1).returns(nil) + assert_raises(Timeout::Error) { sock.readfull(5) } + end + + it 'raises Errno::ECONNRESET when read returns nil' do + sock.read_results = [nil] + assert_raises(Errno::ECONNRESET) { sock.readfull(5) } + end + + describe 'with credentials' do + let(:sock) { MockSocket.new(socket_timeout: 1, username: 'admin', password: 'secret') } + + it 'excludes credentials from Timeout::Error message' do + sock.read_results = [:wait_readable] + IO.stubs(:select).with([sock], nil, nil, 1).returns(nil) + error = assert_raises(Timeout::Error) { sock.readfull(5) } + refute_match(/admin/, error.message) + refute_match(/secret/, error.message) + end + + it 'excludes credentials from Errno::ECONNRESET message' do + sock.read_results = [nil] + error = assert_raises(Errno::ECONNRESET) { sock.readfull(5) } + refute_match(/admin/, error.message) + refute_match(/secret/, error.message) + end + end + end + + describe '#read_available' do + it 'reads all available data until :wait_readable' do + sock.read_results = ["he", "llo", :wait_readable] + assert_equal "hello", sock.read_available + end + + it 'returns empty string when immediately :wait_readable' do + sock.read_results = [:wait_readable] + assert_equal "", sock.read_available + end + + it 'stops reading on :wait_writable' do + sock.read_results = ["data", :wait_writable] + assert_equal "data", sock.read_available + end + + it 'raises Errno::ECONNRESET when read returns nil' do + sock.read_results = [nil] + assert_raises(Errno::ECONNRESET) { sock.read_available } + end + + it 'raises Errno::ECONNRESET after partial read when read returns nil' do + sock.read_results = ["partial", nil] + assert_raises(Errno::ECONNRESET) { sock.read_available } + end + + describe 'with credentials' do + let(:sock) { MockSocket.new(socket_timeout: 1, username: 'admin', password: 'secret') } + + it 'excludes credentials from Errno::ECONNRESET message' do + sock.read_results = [nil] + error = assert_raises(Errno::ECONNRESET) { sock.read_available } + refute_match(/admin/, error.message) + refute_match(/secret/, error.message) + end + end + end + + describe '#safe_options' do + it 'filters out :username and :password' do + sock = MockSocket.new(host: 'localhost', port: 11211, username: 'admin', password: 'secret') + assert_equal({host: 'localhost', port: 11211}, sock.safe_options) + end + + it 'returns all options when no credentials are present' do + sock = MockSocket.new(host: 'localhost', port: 11211) + assert_equal({host: 'localhost', port: 11211}, sock.safe_options) + end + end +end + +describe 'Dalli::Socket::TCP' do + before do + @server = TCPServer.new('127.0.0.1', 0) + @port = @server.addr[1] + end + + after do + @sock&.close + @server&.close + end + + it 'sets TCP_NODELAY on the socket' do + @sock = Dalli::Socket::TCP.open('127.0.0.1', @port, 'srv', socket_timeout: 5) + + assert @sock.getsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY).bool + end + + it 'enables SO_KEEPALIVE when keepalive option is true' do + @sock = Dalli::Socket::TCP.open('127.0.0.1', @port, 'srv', socket_timeout: 5, keepalive: true) + + assert @sock.getsockopt(::Socket::SOL_SOCKET, ::Socket::SO_KEEPALIVE).bool + end + + it 'does not enable SO_KEEPALIVE by default' do + @sock = Dalli::Socket::TCP.open('127.0.0.1', @port, 'srv', socket_timeout: 5) + + refute @sock.getsockopt(::Socket::SOL_SOCKET, ::Socket::SO_KEEPALIVE).bool + end + + it 'sets SO_RCVBUF when rcvbuf option is provided' do + @sock = Dalli::Socket::TCP.open('127.0.0.1', @port, 'srv', socket_timeout: 5, rcvbuf: 65536) + + # Kernel may round up the requested value, so assert >= + assert_operator @sock.getsockopt(::Socket::SOL_SOCKET, ::Socket::SO_RCVBUF).int, :>=, 65536 + end + + it 'sets SO_SNDBUF when sndbuf option is provided' do + @sock = Dalli::Socket::TCP.open('127.0.0.1', @port, 'srv', socket_timeout: 5, sndbuf: 65536) + + assert_operator @sock.getsockopt(::Socket::SOL_SOCKET, ::Socket::SO_SNDBUF).int, :>=, 65536 + end + + it 'stores host, port, and options on the socket' do + @sock = Dalli::Socket::TCP.open('127.0.0.1', @port, 'srv', socket_timeout: 5, keepalive: true) + + expected = { host: '127.0.0.1', port: @port, socket_timeout: 5, keepalive: true } + assert_equal expected, @sock.options + end + + it 'assigns the server reference' do + @sock = Dalli::Socket::TCP.open('127.0.0.1', @port, 'my_server', socket_timeout: 5) + + assert_equal 'my_server', @sock.server + end +end + +describe 'Dalli::Socket::UNIX' do + before do + @tmpfile = Tempfile.new('dalli_socket_test') + @path = @tmpfile.path + @tmpfile.close + @tmpfile.unlink + @server = UNIXServer.new(@path) + end + + after do + @sock&.close + @server&.close + File.delete(@path) if File.exist?(@path) + end + + it 'returns a connected UNIX socket' do + @sock = Dalli::Socket::UNIX.open(@path, 'srv', socket_timeout: 5) + + assert_kind_of Dalli::Socket::UNIX, @sock + refute @sock.closed? + end + + it 'stores path and options on the socket' do + @sock = Dalli::Socket::UNIX.open(@path, 'srv', socket_timeout: 5) + + expected = { path: @path, socket_timeout: 5 } + assert_equal expected, @sock.options + end + + it 'assigns the server reference' do + @sock = Dalli::Socket::UNIX.open(@path, 'my_server', socket_timeout: 5) + + assert_equal 'my_server', @sock.server + end +end diff --git a/test/test_threadsafe.rb b/test/test_threadsafe.rb new file mode 100644 index 00000000..8ad3f8c6 --- /dev/null +++ b/test/test_threadsafe.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true +require_relative 'helper' + +describe Dalli::Threadsafe do + let(:server) { Dalli::Server.new('localhost:11211') } + + describe 'when extended onto a server' do + before do + server.extend(Dalli::Threadsafe) + end + + it 'initializes a Monitor lock' do + lock = server.instance_variable_get(:@lock) + assert_instance_of Monitor, lock + end + + it 'responds to lock! and unlock!' do + assert_respond_to server, :lock! + assert_respond_to server, :unlock! + end + + it 'lock! acquires the monitor' do + server.lock! + lock = server.instance_variable_get(:@lock) + assert lock.mon_owned? + server.unlock! + end + + it 'unlock! releases the monitor' do + server.lock! + server.unlock! + lock = server.instance_variable_get(:@lock) + refute lock.mon_owned? + end + + it 'wraps alive? in synchronize' do + lock = server.instance_variable_get(:@lock) + lock.expects(:synchronize).yields + server.stubs(:connect) + server.alive? + end + + it 'wraps close in synchronize' do + lock = server.instance_variable_get(:@lock) + lock.expects(:synchronize).yields + server.close + end + end + + describe 'without Threadsafe extension' do + it 'lock! and unlock! are no-ops' do + server.lock! + server.unlock! + end + end +end From 98f4f0eb4e41514a352b76fbf81d0dbe9d2c7101 Mon Sep 17 00:00:00 2001 From: Matt Dick Date: Thu, 19 Feb 2026 09:33:50 -0800 Subject: [PATCH 2/3] Remove errant package-lock.json --- package-lock.json | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 package-lock.json diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 1224a978..00000000 --- a/package-lock.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "braze_dalli", - "lockfileVersion": 3, - "requires": true, - "packages": {} -} From 705f88241e8a9d816e224495f9c13259ab320a68 Mon Sep 17 00:00:00 2001 From: Matt Dick Date: Thu, 19 Feb 2026 09:57:00 -0800 Subject: [PATCH 3/3] DRYer tests --- test/test_dalli.rb | 63 ++++++------- test/test_server.rb | 220 +++++++++++++++++++------------------------- 2 files changed, 129 insertions(+), 154 deletions(-) diff --git a/test/test_dalli.rb b/test/test_dalli.rb index 9fe50390..fae85174 100644 --- a/test/test_dalli.rb +++ b/test/test_dalli.rb @@ -138,8 +138,9 @@ end describe 'multi block' do + let(:dc) { Dalli::Client.new('localhost:11211') } + it 'sets and restores Thread.current[:dalli_multi]' do - dc = Dalli::Client.new('localhost:11211') assert_nil Thread.current[:dalli_multi] dc.multi do assert_equal true, Thread.current[:dalli_multi] @@ -148,7 +149,6 @@ end it 'restores previous multi state on nested calls' do - dc = Dalli::Client.new('localhost:11211') dc.multi do assert_equal true, Thread.current[:dalli_multi] dc.multi do @@ -160,7 +160,6 @@ end it 'restores state even when block raises' do - dc = Dalli::Client.new('localhost:11211') begin dc.multi do raise RuntimeError, 'boom' @@ -172,23 +171,24 @@ end describe '#with' do + let(:dc) { Dalli::Client.new('localhost:11211') } + it 'yields self' do - dc = Dalli::Client.new('localhost:11211') dc.with do |client| assert_same dc, client end end it 'returns block result' do - dc = Dalli::Client.new('localhost:11211') result = dc.with { |c| 42 } assert_equal 42, result end end describe '#close and #reset' do + let(:dc) { Dalli::Client.new('localhost:11211') } + it 'close resets the ring to nil' do - dc = Dalli::Client.new('localhost:11211') dc.send(:ring) refute_nil dc.instance_variable_get(:@ring) dc.close @@ -196,36 +196,42 @@ end it 'reset is an alias for close' do - dc = Dalli::Client.new('localhost:11211') assert_equal dc.method(:close), dc.method(:reset) end it 'close is safe when ring is nil' do - dc = Dalli::Client.new('localhost:11211') dc.close dc.close end end describe 'key helpers' do - it 'key_with_namespace prepends namespace' do - dc = Dalli::Client.new('localhost:11211', namespace: 'ns') - assert_equal 'ns:mykey', dc.send(:key_with_namespace, 'mykey') - end + describe 'with namespace' do + let(:dc) { Dalli::Client.new('localhost:11211', namespace: 'ns') } - it 'key_with_namespace returns key when no namespace' do - dc = Dalli::Client.new('localhost:11211') - assert_equal 'mykey', dc.send(:key_with_namespace, 'mykey') - end + it 'key_with_namespace prepends namespace' do + assert_equal 'ns:mykey', dc.send(:key_with_namespace, 'mykey') + end - it 'key_without_namespace strips namespace prefix' do - dc = Dalli::Client.new('localhost:11211', namespace: 'ns') - assert_equal 'mykey', dc.send(:key_without_namespace, 'ns:mykey') + it 'key_without_namespace strips namespace prefix' do + assert_equal 'mykey', dc.send(:key_without_namespace, 'ns:mykey') + end end - it 'key_without_namespace returns key when no namespace' do - dc = Dalli::Client.new('localhost:11211') - assert_equal 'mykey', dc.send(:key_without_namespace, 'mykey') + describe 'without namespace' do + let(:dc) { Dalli::Client.new('localhost:11211') } + + it 'key_with_namespace returns key as-is' do + assert_equal 'mykey', dc.send(:key_with_namespace, 'mykey') + end + + it 'key_without_namespace returns key as-is' do + assert_equal 'mykey', dc.send(:key_without_namespace, 'mykey') + end + + it 'namespace returns nil' do + assert_nil dc.send(:namespace) + end end it 'namespace returns string for symbol namespace' do @@ -239,16 +245,12 @@ assert_equal 'dynamic', dc.send(:namespace) assert_equal 1, call_count end - - it 'namespace returns nil when not configured' do - dc = Dalli::Client.new('localhost:11211') - assert_nil dc.send(:namespace) - end end describe 'ttl_or_default' do + let(:dc) { Dalli::Client.new('localhost:11211') } + it 'returns the given ttl as integer' do - dc = Dalli::Client.new('localhost:11211') assert_equal 300, dc.send(:ttl_or_default, 300) end @@ -258,12 +260,10 @@ end it 'returns 0 when both ttl and expires_in are nil' do - dc = Dalli::Client.new('localhost:11211') assert_equal 0, dc.send(:ttl_or_default, nil) end it 'raises ArgumentError for unconvertible ttl' do - dc = Dalli::Client.new('localhost:11211') assert_raises ArgumentError do dc.send(:ttl_or_default, []) end @@ -285,8 +285,9 @@ end describe 'validate_key' do + let(:dc) { Dalli::Client.new('localhost:11211') } + it 'truncates keys longer than 250 chars with md5 hash' do - dc = Dalli::Client.new('localhost:11211') long_key = 'x' * 300 result = dc.send(:validate_key, long_key) assert_operator result.length, :<=, 250 diff --git a/test/test_server.rb b/test/test_server.rb index e4d17a2b..d15b85cc 100644 --- a/test/test_server.rb +++ b/test/test_server.rb @@ -3,6 +3,8 @@ require_relative 'helper' describe Dalli::Server do + let(:server) { Dalli::Server.new('localhost') } + describe 'name' do it 'returns hostname:port for tcp sockets' do s = Dalli::Server.new('localhost:11211') @@ -15,27 +17,25 @@ end it 'uses default port in name when not specified' do - s = Dalli::Server.new('localhost') - assert_equal 'localhost:11211', s.name + assert_equal 'localhost:11211', server.name end end describe 'initialization defaults' do it 'sets default options' do - s = Dalli::Server.new('localhost') - assert_equal 60, s.options[:down_retry_delay] - assert_equal 0.5, s.options[:socket_timeout] - assert_equal 2, s.options[:socket_max_failures] - assert_equal 0.01, s.options[:socket_failure_delay] - assert_equal 1024 * 1024, s.options[:value_max_bytes] - assert_equal false, s.options[:error_when_over_max_size] - assert_equal Dalli::Compressor, s.options[:compressor] - assert_equal 1024, s.options[:compression_min_size] - assert_equal false, s.options[:compression_max_size] - assert_equal Marshal, s.options[:serializer] - assert_nil s.options[:username] - assert_nil s.options[:password] - assert_equal true, s.options[:keepalive] + assert_equal 60, server.options[:down_retry_delay] + assert_equal 0.5, server.options[:socket_timeout] + assert_equal 2, server.options[:socket_max_failures] + assert_equal 0.01, server.options[:socket_failure_delay] + assert_equal 1024 * 1024, server.options[:value_max_bytes] + assert_equal false, server.options[:error_when_over_max_size] + assert_equal Dalli::Compressor, server.options[:compressor] + assert_equal 1024, server.options[:compression_min_size] + assert_equal false, server.options[:compression_max_size] + assert_equal Marshal, server.options[:serializer] + assert_nil server.options[:username] + assert_nil server.options[:password] + assert_equal true, server.options[:keepalive] end it 'merges custom options with defaults' do @@ -46,20 +46,17 @@ end it 'starts with nil sock' do - s = Dalli::Server.new('localhost') - assert_nil s.sock + assert_nil server.sock end end describe 'serializer and compressor accessors' do it 'returns the configured serializer' do - s = Dalli::Server.new('localhost') - assert_equal Marshal, s.serializer + assert_equal Marshal, server.serializer end it 'returns the configured compressor' do - s = Dalli::Server.new('localhost') - assert_equal Dalli::Compressor, s.compressor + assert_equal Dalli::Compressor, server.compressor end it 'returns custom serializer' do @@ -77,32 +74,29 @@ describe 'close' do it 'is a no-op when sock is nil' do - s = Dalli::Server.new('localhost') - assert_nil s.sock - s.close - assert_nil s.sock + assert_nil server.sock + server.close + assert_nil server.sock end it 'closes the socket and resets state' do - s = Dalli::Server.new('localhost') mock_sock = mock('socket') mock_sock.expects(:close) - s.instance_variable_set(:@sock, mock_sock) - s.instance_variable_set(:@pid, Process.pid) + server.instance_variable_set(:@sock, mock_sock) + server.instance_variable_set(:@pid, Process.pid) - s.close + server.close - assert_nil s.sock - assert_nil s.instance_variable_get(:@pid) - assert_equal false, s.instance_variable_get(:@inprogress) + assert_nil server.sock + assert_nil server.instance_variable_get(:@pid) + assert_equal false, server.instance_variable_get(:@inprogress) end end describe 'alive?' do it 'returns true when sock is present' do - s = Dalli::Server.new('localhost') - s.instance_variable_set(:@sock, stub('socket')) - assert_equal true, s.alive? + server.instance_variable_set(:@sock, stub('socket')) + assert_equal true, server.alive? end it 'returns false during down_retry_delay period' do @@ -114,37 +108,33 @@ it 'attempts reconnect after down_retry_delay expires' do s = Dalli::Server.new('localhost:19999', down_retry_delay: 0) s.instance_variable_set(:@last_down_at, Time.now - 1) - result = s.alive? - assert_equal false, result + assert_equal false, s.alive? end end describe 'multi_response_completed?' do it 'returns true when multi_buffer is nil' do - s = Dalli::Server.new('localhost') - s.instance_variable_set(:@multi_buffer, nil) - assert_equal true, s.multi_response_completed? + server.instance_variable_set(:@multi_buffer, nil) + assert_equal true, server.multi_response_completed? end it 'returns false when multi_buffer has content' do - s = Dalli::Server.new('localhost') - s.instance_variable_set(:@multi_buffer, 'data') - assert_equal false, s.multi_response_completed? + server.instance_variable_set(:@multi_buffer, 'data') + assert_equal false, server.multi_response_completed? end end describe 'multi_response_abort' do it 'clears multi_buffer and position' do - s = Dalli::Server.new('localhost:19999') - s.instance_variable_set(:@multi_buffer, 'data') - s.instance_variable_set(:@position, 10) - s.instance_variable_set(:@inprogress, true) + server.instance_variable_set(:@multi_buffer, 'data') + server.instance_variable_set(:@position, 10) + server.instance_variable_set(:@inprogress, true) - s.multi_response_abort + server.multi_response_abort - assert_nil s.instance_variable_get(:@multi_buffer) - assert_nil s.instance_variable_get(:@position) - assert_equal false, s.instance_variable_get(:@inprogress) + assert_nil server.instance_variable_get(:@multi_buffer) + assert_nil server.instance_variable_get(:@position) + assert_equal false, server.instance_variable_get(:@inprogress) end end @@ -152,31 +142,31 @@ it 'closes socket on Timeout::Error and re-raises' do memcached_persistent do |dc| ring = dc.send(:ring) - server = ring.servers.first - assert server.alive? + s = ring.servers.first + assert s.alive? - server.stubs(:verify_state) - server.stubs(:get).raises(Timeout::Error.new('IO timeout')) + s.stubs(:verify_state) + s.stubs(:get).raises(Timeout::Error.new('IO timeout')) assert_raises Timeout::Error do - server.request(:get, 'key') + s.request(:get, 'key') end - assert_nil server.sock + assert_nil s.sock end end it 'returns false on MarshalError' do memcached_persistent do |dc| ring = dc.send(:ring) - server = ring.servers.first - assert server.alive? + s = ring.servers.first + assert s.alive? - server.stubs(:verify_state) - server.stubs(:set).raises(Dalli::MarshalError.new('cannot dump')) + s.stubs(:verify_state) + s.stubs(:set).raises(Dalli::MarshalError.new('cannot dump')) with_nil_logger do - result = server.request(:set, 'key', 'value') + result = s.request(:set, 'key', 'value') assert_equal false, result end end @@ -286,38 +276,34 @@ describe 'verify_state' do it 'raises NetworkError when inprogress is true' do - s = Dalli::Server.new('localhost') - s.instance_variable_set(:@inprogress, true) + server.instance_variable_set(:@inprogress, true) assert_raises Dalli::NetworkError do - s.send(:verify_state) + server.send(:verify_state) end end it 'does nothing when state is clean' do - s = Dalli::Server.new('localhost') - s.instance_variable_set(:@inprogress, false) - s.instance_variable_set(:@pid, nil) - s.send(:verify_state) + server.instance_variable_set(:@inprogress, false) + server.instance_variable_set(:@pid, nil) + server.send(:verify_state) end it 'reconnects when pid changes (fork detection)' do - s = Dalli::Server.new('localhost') - s.instance_variable_set(:@pid, -1) - s.instance_variable_set(:@inprogress, false) + server.instance_variable_set(:@pid, -1) + server.instance_variable_set(:@inprogress, false) assert_raises Dalli::NetworkError do - s.send(:verify_state) + server.send(:verify_state) end end end describe 'failure!' do it 'reconnects when fail_count is below max' do - s = Dalli::Server.new('localhost') - s.instance_variable_set(:@fail_count, 0) + server.instance_variable_set(:@fail_count, 0) assert_raises Dalli::NetworkError do - s.send(:failure!, RuntimeError.new('test')) + server.send(:failure!, RuntimeError.new('test')) end - assert_equal 1, s.instance_variable_get(:@fail_count) + assert_equal 1, server.instance_variable_get(:@fail_count) end it 'marks server as down when fail_count reaches max' do @@ -331,65 +317,60 @@ describe 'down! and up!' do it 'down! sets last_down_at and raises NetworkError' do - s = Dalli::Server.new('localhost') assert_raises Dalli::NetworkError do - s.send(:down!) + server.send(:down!) end - refute_nil s.instance_variable_get(:@last_down_at) - refute_nil s.instance_variable_get(:@down_at) + refute_nil server.instance_variable_get(:@last_down_at) + refute_nil server.instance_variable_get(:@down_at) end it 'up! clears failure state' do - s = Dalli::Server.new('localhost') - s.instance_variable_set(:@fail_count, 5) - s.instance_variable_set(:@down_at, Time.now) - s.instance_variable_set(:@last_down_at, Time.now) - s.instance_variable_set(:@msg, 'error') - s.instance_variable_set(:@error, 'RuntimeError') + server.instance_variable_set(:@fail_count, 5) + server.instance_variable_set(:@down_at, Time.now) + server.instance_variable_set(:@last_down_at, Time.now) + server.instance_variable_set(:@msg, 'error') + server.instance_variable_set(:@error, 'RuntimeError') - s.send(:up!) + server.send(:up!) - assert_equal 0, s.instance_variable_get(:@fail_count) - assert_nil s.instance_variable_get(:@down_at) - assert_nil s.instance_variable_get(:@last_down_at) - assert_nil s.instance_variable_get(:@msg) - assert_nil s.instance_variable_get(:@error) + assert_equal 0, server.instance_variable_get(:@fail_count) + assert_nil server.instance_variable_get(:@down_at) + assert_nil server.instance_variable_get(:@last_down_at) + assert_nil server.instance_variable_get(:@msg) + assert_nil server.instance_variable_get(:@error) end end describe 'multi?' do + after do + Thread.current[:dalli_multi] = nil + end + it 'returns true when Thread.current[:dalli_multi] is set' do - s = Dalli::Server.new('localhost') Thread.current[:dalli_multi] = true - assert_equal true, s.send(:multi?) - Thread.current[:dalli_multi] = nil + assert_equal true, server.send(:multi?) end it 'returns nil when not in multi block' do - s = Dalli::Server.new('localhost') - Thread.current[:dalli_multi] = nil - assert_nil s.send(:multi?) + assert_nil server.send(:multi?) end end describe 'split' do it 'splits 64-bit integers into high and low 32-bit parts' do - s = Dalli::Server.new('localhost') - h, l = s.send(:split, 0x100000001) + h, l = server.send(:split, 0x100000001) assert_equal 1, h assert_equal 1, l end it 'handles zero' do - s = Dalli::Server.new('localhost') - h, l = s.send(:split, 0) + h, l = server.send(:split, 0) assert_equal 0, h assert_equal 0, l end it 'handles max 32-bit value' do - s = Dalli::Server.new('localhost') - h, l = s.send(:split, 0xFFFFFFFF) + h, l = server.send(:split, 0xFFFFFFFF) assert_equal 0, h assert_equal 0xFFFFFFFF, l end @@ -404,8 +385,7 @@ it 'returns falsey when no credentials configured' do old_user = ENV['MEMCACHE_USERNAME'] ENV['MEMCACHE_USERNAME'] = nil - s = Dalli::Server.new('localhost') - refute s.send(:need_auth?) + refute server.send(:need_auth?) ensure ENV['MEMCACHE_USERNAME'] = old_user end @@ -457,11 +437,10 @@ end it 'handles no port or weight' do - s = Dalli::Server.new('localhost') - assert_equal 'localhost', s.hostname - assert_equal 11211, s.port - assert_equal 1, s.weight - assert_equal :tcp, s.socket_type + assert_equal 'localhost', server.hostname + assert_equal 11211, server.port + assert_equal 1, server.weight + assert_equal :tcp, server.socket_type end it 'handles a port, but no weight' do @@ -538,29 +517,25 @@ describe 'ttl translation' do it 'does not translate ttls under 30 days' do - s = Dalli::Server.new('localhost') - assert_equal s.send(:sanitize_ttl, 30*24*60*60), 30*24*60*60 + assert_equal 30*24*60*60, server.send(:sanitize_ttl, 30*24*60*60) end it 'translates ttls over 30 days into timestamps' do - s = Dalli::Server.new('localhost') - assert_equal s.send(:sanitize_ttl, 30*24*60*60 + 1), Time.now.to_i + 30*24*60*60+1 + assert_equal Time.now.to_i + 30*24*60*60+1, server.send(:sanitize_ttl, 30*24*60*60 + 1) end it 'does not translate ttls which are already timestamps' do - s = Dalli::Server.new('localhost') timestamp_ttl = Time.now.to_i + 60 - assert_equal s.send(:sanitize_ttl, timestamp_ttl), timestamp_ttl + assert_equal timestamp_ttl, server.send(:sanitize_ttl, timestamp_ttl) end end describe 'guard_max_value' do it 'yields when size is under max' do - s = Dalli::Server.new('127.0.0.1') value = OpenStruct.new(:bytesize => 1_048_576) yielded = false - s.send(:guard_max_value, :foo, value) do + server.send(:guard_max_value, :foo, value) do yielded = true end @@ -568,12 +543,11 @@ end it 'warns when size is over max' do - s = Dalli::Server.new('127.0.0.1') value = OpenStruct.new(:bytesize => 1_048_577) Dalli.logger.expects(:error).once.with("Value for foo over max size: 1048576 <= 1048577 - this value may be truncated by memcached") - s.send(:guard_max_value, :foo, value) + server.send(:guard_max_value, :foo, value) end it 'throws when size is over max and error_over_max_size true' do