|
3 | 3 |
|
4 | 4 | class MockSocket |
5 | 5 | include Dalli::Socket::InstanceMethods |
6 | | - attr_accessor :options, :read_results |
| 6 | + attr_accessor :options, :read_results, :write_results |
7 | 7 |
|
8 | 8 | def initialize(options = {}) |
9 | 9 | @options = options |
10 | 10 | @read_results = [] |
| 11 | + @write_results = [] |
11 | 12 | @read_index = 0 |
| 13 | + @write_index = 0 |
12 | 14 | end |
13 | 15 |
|
14 | 16 | def read_nonblock(_count, exception: true) |
15 | 17 | result = @read_results[@read_index] |
16 | 18 | @read_index += 1 |
17 | 19 | result |
18 | 20 | end |
| 21 | + |
| 22 | + def write_nonblock(_bytes, exception: true) |
| 23 | + result = @write_results[@write_index] |
| 24 | + @write_index += 1 |
| 25 | + result |
| 26 | + end |
19 | 27 | end |
20 | 28 |
|
21 | 29 | describe 'Dalli::Socket::InstanceMethods' do |
@@ -81,6 +89,70 @@ def read_nonblock(_count, exception: true) |
81 | 89 | end |
82 | 90 | end |
83 | 91 |
|
| 92 | + describe '#writefull' do |
| 93 | + it 'writes all bytes in a single call' do |
| 94 | + sock.write_results = [5] |
| 95 | + assert_equal 5, sock.writefull("hello") |
| 96 | + end |
| 97 | + |
| 98 | + it 'handles partial writes across multiple calls' do |
| 99 | + sock.write_results = [2, 3] |
| 100 | + assert_equal 5, sock.writefull("hello") |
| 101 | + end |
| 102 | + |
| 103 | + it 'retries on :wait_writable when IO.select succeeds' do |
| 104 | + sock.write_results = [:wait_writable, 5] |
| 105 | + IO.stubs(:select).with(nil, [sock], nil, 1).returns([nil, [sock]]) |
| 106 | + assert_equal 5, sock.writefull("hello") |
| 107 | + end |
| 108 | + |
| 109 | + it 'retries on :wait_readable when IO.select succeeds' do |
| 110 | + sock.write_results = [:wait_readable, 5] |
| 111 | + IO.stubs(:select).with([sock], nil, nil, 1).returns([[sock]]) |
| 112 | + assert_equal 5, sock.writefull("hello") |
| 113 | + end |
| 114 | + |
| 115 | + it 'raises Timeout::Error on :wait_writable when IO.select times out' do |
| 116 | + sock.write_results = [:wait_writable] |
| 117 | + IO.stubs(:select).with(nil, [sock], nil, 1).returns(nil) |
| 118 | + assert_raises(Timeout::Error) { sock.writefull("hello") } |
| 119 | + end |
| 120 | + |
| 121 | + it 'raises Timeout::Error on :wait_readable when IO.select times out' do |
| 122 | + sock.write_results = [:wait_readable] |
| 123 | + IO.stubs(:select).with([sock], nil, nil, 1).returns(nil) |
| 124 | + assert_raises(Timeout::Error) { sock.writefull("hello") } |
| 125 | + end |
| 126 | + |
| 127 | + it 'delivers all bytes through a real socket pair' do |
| 128 | + s1, s2 = Socket.pair(:UNIX, :STREAM, 0) |
| 129 | + s1.extend(Dalli::Socket::InstanceMethods) |
| 130 | + def s1.options; { socket_timeout: 5 }; end |
| 131 | + |
| 132 | + data = "hello world" |
| 133 | + result = s1.writefull(data) |
| 134 | + s1.close |
| 135 | + |
| 136 | + assert_equal data.bytesize, result |
| 137 | + assert_equal data, s2.read |
| 138 | + ensure |
| 139 | + s1&.close rescue nil |
| 140 | + s2&.close rescue nil |
| 141 | + end |
| 142 | + |
| 143 | + describe 'with credentials' do |
| 144 | + let(:sock) { MockSocket.new(socket_timeout: 1, username: 'admin', password: 'secret') } |
| 145 | + |
| 146 | + it 'excludes credentials from Timeout::Error message' do |
| 147 | + sock.write_results = [:wait_writable] |
| 148 | + IO.stubs(:select).with(nil, [sock], nil, 1).returns(nil) |
| 149 | + error = assert_raises(Timeout::Error) { sock.writefull("hello") } |
| 150 | + refute_match(/admin/, error.message) |
| 151 | + refute_match(/secret/, error.message) |
| 152 | + end |
| 153 | + end |
| 154 | + end |
| 155 | + |
84 | 156 | describe '#read_available' do |
85 | 157 | it 'reads all available data until :wait_readable' do |
86 | 158 | sock.read_results = ["he", "llo", :wait_readable] |
@@ -179,6 +251,19 @@ def read_nonblock(_count, exception: true) |
179 | 251 | @sock = Dalli::Socket::TCP.open('127.0.0.1', @port, 'my_server', socket_timeout: 5) |
180 | 252 | assert_equal 'my_server', @sock.server |
181 | 253 | end |
| 254 | + |
| 255 | + it 'raises SocketError for unresolvable hostname' do |
| 256 | + assert_raises(SocketError) do |
| 257 | + Dalli::Socket::TCP.open('this-host-does-not-exist.invalid', 11211, 'srv', socket_timeout: 1) |
| 258 | + end |
| 259 | + end |
| 260 | + |
| 261 | + it 'includes hostname in SocketError message for unresolvable host' do |
| 262 | + error = assert_raises(SocketError) do |
| 263 | + Dalli::Socket::TCP.open('this-host-does-not-exist.invalid', 11211, 'srv', socket_timeout: 1) |
| 264 | + end |
| 265 | + assert_match(/this-host-does-not-exist\.invalid/, error.message) |
| 266 | + end |
182 | 267 | end |
183 | 268 |
|
184 | 269 | describe 'Dalli::Socket::UNIX' do |
@@ -212,4 +297,10 @@ def read_nonblock(_count, exception: true) |
212 | 297 | @sock = Dalli::Socket::UNIX.open(@path, 'my_server', socket_timeout: 5) |
213 | 298 | assert_equal 'my_server', @sock.server |
214 | 299 | end |
| 300 | + |
| 301 | + it 'raises Errno::ENOENT for non-existent socket path' do |
| 302 | + assert_raises(Errno::ENOENT) do |
| 303 | + Dalli::Socket::UNIX.open('/tmp/nonexistent_dalli_test_socket', 'srv', socket_timeout: 1) |
| 304 | + end |
| 305 | + end |
215 | 306 | end |
0 commit comments