tcp-client 0.10.1 → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +5 -4
- data/lib/tcp-client/configuration.rb +37 -21
- data/lib/tcp-client/default_configuration.rb +2 -2
- data/lib/tcp-client/mixin/io_with_deadline.rb +48 -79
- data/lib/tcp-client/version.rb +1 -1
- data/lib/tcp-client.rb +4 -3
- metadata +10 -87
- data/.gitignore +0 -6
- data/gems.rb +0 -4
- data/rakefile.rb +0 -18
- data/sample/google.rb +0 -21
- data/sample/google_ssl.rb +0 -28
- data/spec/helper.rb +0 -12
- data/spec/tcp-client/address_spec.rb +0 -132
- data/spec/tcp-client/configuration_spec.rb +0 -270
- data/spec/tcp-client/default_configuration_spec.rb +0 -22
- data/spec/tcp-client/version_spec.rb +0 -13
- data/spec/tcp_client_spec.rb +0 -800
- data/tcp-client.gemspec +0 -39
data/spec/tcp_client_spec.rb
DELETED
@@ -1,800 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative 'helper'
|
4
|
-
|
5
|
-
SOCKET_ERRORS = [
|
6
|
-
Errno::EADDRNOTAVAIL,
|
7
|
-
Errno::ECONNABORTED,
|
8
|
-
Errno::ECONNREFUSED,
|
9
|
-
Errno::ECONNRESET,
|
10
|
-
Errno::EHOSTUNREACH,
|
11
|
-
Errno::EINVAL,
|
12
|
-
Errno::ENETUNREACH,
|
13
|
-
Errno::EPIPE,
|
14
|
-
IOError,
|
15
|
-
SocketError,
|
16
|
-
::OpenSSL::SSL::SSLError
|
17
|
-
].freeze
|
18
|
-
|
19
|
-
RSpec.describe TCPClient do
|
20
|
-
subject(:client) { TCPClient.new.connect('localhost:1234', configuration) }
|
21
|
-
let(:configuration) do
|
22
|
-
TCPClient::Configuration.create(buffered: false, reverse_lookup: false)
|
23
|
-
end
|
24
|
-
|
25
|
-
describe 'a new instance' do
|
26
|
-
subject(:client) { TCPClient.new }
|
27
|
-
|
28
|
-
it 'is closed' do
|
29
|
-
expect(client).to be_closed
|
30
|
-
end
|
31
|
-
|
32
|
-
it 'has no address' do
|
33
|
-
expect(client.address).to be_nil
|
34
|
-
end
|
35
|
-
|
36
|
-
it 'returns no address string' do
|
37
|
-
expect(client.to_s).to eq ''
|
38
|
-
end
|
39
|
-
|
40
|
-
it 'has no configuration' do
|
41
|
-
expect(client.configuration).to be_nil
|
42
|
-
end
|
43
|
-
|
44
|
-
it 'failes when read is called' do
|
45
|
-
expect { client.read(42) }.to raise_error(TCPClient::NotConnectedError)
|
46
|
-
end
|
47
|
-
|
48
|
-
it 'failes when write is called' do
|
49
|
-
expect { client.write('?!') }.to raise_error(TCPClient::NotConnectedError)
|
50
|
-
end
|
51
|
-
|
52
|
-
it 'can be closed' do
|
53
|
-
expect_any_instance_of(::Socket).not_to receive(:close)
|
54
|
-
expect(client.close).to be client
|
55
|
-
end
|
56
|
-
|
57
|
-
it 'can be flushed' do
|
58
|
-
expect_any_instance_of(::Socket).not_to receive(:flush)
|
59
|
-
expect(client.flush).to be client
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
describe 'a connected instance' do
|
64
|
-
before { allow_any_instance_of(::Socket).to receive(:connect) }
|
65
|
-
|
66
|
-
it 'is not closed' do
|
67
|
-
expect(client).not_to be_closed
|
68
|
-
end
|
69
|
-
|
70
|
-
it 'has an address' do
|
71
|
-
expect(client.address).to be_a TCPClient::Address
|
72
|
-
end
|
73
|
-
|
74
|
-
it 'returns an address string' do
|
75
|
-
expect(client.to_s).to eq 'localhost:1234'
|
76
|
-
end
|
77
|
-
|
78
|
-
it 'uses the given configuration' do
|
79
|
-
expect(client.configuration).to be configuration
|
80
|
-
end
|
81
|
-
|
82
|
-
it 'allows to read data' do
|
83
|
-
result = '-' * 42
|
84
|
-
allow_any_instance_of(::Socket).to receive(:read)
|
85
|
-
.with(42)
|
86
|
-
.and_return(result)
|
87
|
-
expect(client.read(42)).to be result
|
88
|
-
end
|
89
|
-
|
90
|
-
it 'allows to write data' do
|
91
|
-
allow_any_instance_of(::Socket).to receive(:write)
|
92
|
-
.with('!' * 21)
|
93
|
-
.and_return(21)
|
94
|
-
expect(client.write('!' * 21)).to be 21
|
95
|
-
end
|
96
|
-
|
97
|
-
it 'can be closed' do
|
98
|
-
expect_any_instance_of(::Socket).to receive(:close)
|
99
|
-
expect(client.close).to be client
|
100
|
-
end
|
101
|
-
|
102
|
-
it 'can be flushed' do
|
103
|
-
expect_any_instance_of(::Socket).to receive(:flush)
|
104
|
-
expect(client.flush).to be client
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
108
|
-
describe 'an instance after #connect failed' do
|
109
|
-
subject(:client) do
|
110
|
-
TCPClient.new.tap do |instance|
|
111
|
-
instance.connect('', configuration)
|
112
|
-
rescue StandardError
|
113
|
-
Errno::EADDRNOTAVAIL
|
114
|
-
end
|
115
|
-
end
|
116
|
-
|
117
|
-
it 'is closed' do
|
118
|
-
expect(client).to be_closed
|
119
|
-
end
|
120
|
-
|
121
|
-
it 'has an address' do
|
122
|
-
expect(client.address).to be_a TCPClient::Address
|
123
|
-
end
|
124
|
-
|
125
|
-
it 'returns an address string' do
|
126
|
-
expect(client.to_s).to eq 'localhost:0'
|
127
|
-
end
|
128
|
-
|
129
|
-
it 'uses the given configuration' do
|
130
|
-
expect(client.configuration).to be configuration
|
131
|
-
end
|
132
|
-
|
133
|
-
it 'failes when read is called' do
|
134
|
-
expect { client.read(42) }.to raise_error(TCPClient::NotConnectedError)
|
135
|
-
end
|
136
|
-
|
137
|
-
it 'failes when write is called' do
|
138
|
-
expect { client.write('?!') }.to raise_error(TCPClient::NotConnectedError)
|
139
|
-
end
|
140
|
-
|
141
|
-
it 'can be closed' do
|
142
|
-
expect_any_instance_of(::Socket).not_to receive(:close)
|
143
|
-
expect(client.close).to be client
|
144
|
-
end
|
145
|
-
|
146
|
-
it 'can be flushed' do
|
147
|
-
expect_any_instance_of(::Socket).not_to receive(:flush)
|
148
|
-
expect(client.flush).to be client
|
149
|
-
end
|
150
|
-
end
|
151
|
-
|
152
|
-
xdescribe '.open' do
|
153
|
-
end
|
154
|
-
|
155
|
-
xdescribe '.with_deadline' do
|
156
|
-
end
|
157
|
-
|
158
|
-
context 'when not using SSL' do
|
159
|
-
describe '#connect' do
|
160
|
-
subject(:client) { TCPClient.new }
|
161
|
-
|
162
|
-
it 'configures the socket' do
|
163
|
-
expect_any_instance_of(::Socket).to receive(:sync=).once.with(true)
|
164
|
-
expect_any_instance_of(::Socket).to receive(:setsockopt)
|
165
|
-
.once
|
166
|
-
.with(:TCP, :NODELAY, 1)
|
167
|
-
expect_any_instance_of(::Socket).to receive(:setsockopt)
|
168
|
-
.once
|
169
|
-
.with(:SOCKET, :KEEPALIVE, 1)
|
170
|
-
expect_any_instance_of(::Socket).to receive(:do_not_reverse_lookup=)
|
171
|
-
.once
|
172
|
-
.with(false)
|
173
|
-
expect_any_instance_of(::Socket).to receive(:connect)
|
174
|
-
client.connect('localhost:1234', configuration)
|
175
|
-
end
|
176
|
-
|
177
|
-
context 'when a timeout is specified' do
|
178
|
-
it 'checks the time' do
|
179
|
-
expect_any_instance_of(::Socket).to receive(:connect_nonblock)
|
180
|
-
.once
|
181
|
-
.with(kind_of(String), exception: false)
|
182
|
-
client.connect('localhost:1234', configuration, timeout: 10)
|
183
|
-
end
|
184
|
-
|
185
|
-
it 'is returns itself' do
|
186
|
-
allow_any_instance_of(::Socket).to receive(:connect_nonblock).with(
|
187
|
-
kind_of(String),
|
188
|
-
exception: false
|
189
|
-
)
|
190
|
-
result = client.connect('localhost:1234', configuration, timeout: 10)
|
191
|
-
|
192
|
-
expect(result).to be client
|
193
|
-
end
|
194
|
-
|
195
|
-
it 'is not closed' do
|
196
|
-
allow_any_instance_of(::Socket).to receive(:connect_nonblock).with(
|
197
|
-
kind_of(String),
|
198
|
-
exception: false
|
199
|
-
)
|
200
|
-
client.connect('localhost:1234', configuration, timeout: 10)
|
201
|
-
expect(client).not_to be_closed
|
202
|
-
end
|
203
|
-
|
204
|
-
context 'when the connection can not be established in time' do
|
205
|
-
before do
|
206
|
-
allow_any_instance_of(::Socket).to receive(:connect_nonblock)
|
207
|
-
.and_return(:wait_writable)
|
208
|
-
end
|
209
|
-
|
210
|
-
it 'raises an exception' do
|
211
|
-
expect do
|
212
|
-
client.connect('localhost:1234', configuration, timeout: 0.1)
|
213
|
-
end.to raise_error(TCPClient::ConnectTimeoutError)
|
214
|
-
end
|
215
|
-
|
216
|
-
it 'allows to raise a custom exception' do
|
217
|
-
exception = Class.new(StandardError)
|
218
|
-
expect do
|
219
|
-
client.connect(
|
220
|
-
'localhost:1234',
|
221
|
-
configuration,
|
222
|
-
timeout: 0.1,
|
223
|
-
exception: exception
|
224
|
-
)
|
225
|
-
end.to raise_error(exception)
|
226
|
-
end
|
227
|
-
|
228
|
-
it 'is still closed' do
|
229
|
-
begin
|
230
|
-
client.connect('localhost:1234', configuration, timeout: 0.1)
|
231
|
-
rescue TCPClient::ConnectTimeoutError
|
232
|
-
end
|
233
|
-
expect(client).to be_closed
|
234
|
-
end
|
235
|
-
end
|
236
|
-
end
|
237
|
-
|
238
|
-
context 'when a SocketError appears' do
|
239
|
-
it 'does not handle it' do
|
240
|
-
allow_any_instance_of(::Socket).to receive(:connect) {
|
241
|
-
raise SocketError
|
242
|
-
}
|
243
|
-
expect do
|
244
|
-
TCPClient.new.connect('localhost:1234', configuration)
|
245
|
-
end.to raise_error(SocketError)
|
246
|
-
end
|
247
|
-
|
248
|
-
context 'when normalize_network_errors is configured' do
|
249
|
-
let(:configuration) do
|
250
|
-
TCPClient::Configuration.create(normalize_network_errors: true)
|
251
|
-
end
|
252
|
-
|
253
|
-
SOCKET_ERRORS.each do |error_class|
|
254
|
-
it "raises TCPClient::NetworkError when a #{error_class} appeared" do
|
255
|
-
allow_any_instance_of(::Socket).to receive(:connect) {
|
256
|
-
raise error_class
|
257
|
-
}
|
258
|
-
expect do
|
259
|
-
TCPClient.new.connect('localhost:1234', configuration)
|
260
|
-
end.to raise_error(TCPClient::NetworkError)
|
261
|
-
end
|
262
|
-
end
|
263
|
-
end
|
264
|
-
end
|
265
|
-
end
|
266
|
-
|
267
|
-
describe '#read' do
|
268
|
-
let(:data) { 'some bytes' }
|
269
|
-
let(:data_size) { data.bytesize }
|
270
|
-
|
271
|
-
before { allow_any_instance_of(::Socket).to receive(:connect) }
|
272
|
-
|
273
|
-
it 'reads from socket' do
|
274
|
-
expect_any_instance_of(::Socket).to receive(:read)
|
275
|
-
.once
|
276
|
-
.with(nil)
|
277
|
-
.and_return(data)
|
278
|
-
expect(client.read).to be data
|
279
|
-
end
|
280
|
-
|
281
|
-
context 'when a number of bytes is specified' do
|
282
|
-
it 'reads the requested number of bytes' do
|
283
|
-
expect_any_instance_of(::Socket).to receive(:read)
|
284
|
-
.once
|
285
|
-
.with(data_size)
|
286
|
-
.and_return(data)
|
287
|
-
expect(client.read(data_size)).to be data
|
288
|
-
end
|
289
|
-
end
|
290
|
-
|
291
|
-
context 'when a timeout is specified' do
|
292
|
-
it 'checks the time' do
|
293
|
-
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
294
|
-
.and_return(data)
|
295
|
-
expect(client.read(timeout: 10)).to be data
|
296
|
-
end
|
297
|
-
|
298
|
-
context 'when socket closed before any data can be read' do
|
299
|
-
it 'returns empty buffer' do
|
300
|
-
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
301
|
-
.and_return(nil)
|
302
|
-
expect(client.read(timeout: 10)).to be_empty
|
303
|
-
end
|
304
|
-
|
305
|
-
it 'is closed' do
|
306
|
-
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
307
|
-
.and_return(nil)
|
308
|
-
|
309
|
-
client.read(timeout: 10)
|
310
|
-
expect(client).to be_closed
|
311
|
-
end
|
312
|
-
end
|
313
|
-
|
314
|
-
context 'when data can not be fetched in a single chunk' do
|
315
|
-
it 'reads chunk by chunk' do
|
316
|
-
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
317
|
-
.once
|
318
|
-
.with(instance_of(Integer), exception: false)
|
319
|
-
.and_return(data)
|
320
|
-
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
321
|
-
.once
|
322
|
-
.with(instance_of(Integer), exception: false)
|
323
|
-
.and_return(data)
|
324
|
-
expect(client.read(data_size * 2, timeout: 10)).to eq data * 2
|
325
|
-
end
|
326
|
-
|
327
|
-
context 'when socket closed before enough data is avail' do
|
328
|
-
it 'returns available data only' do
|
329
|
-
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
330
|
-
.once
|
331
|
-
.with(instance_of(Integer), exception: false)
|
332
|
-
.and_return(data)
|
333
|
-
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
334
|
-
.once
|
335
|
-
.with(instance_of(Integer), exception: false)
|
336
|
-
.and_return(nil)
|
337
|
-
expect(client.read(data_size * 2, timeout: 10)).to eq data
|
338
|
-
end
|
339
|
-
|
340
|
-
it 'is closed' do
|
341
|
-
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
342
|
-
.once
|
343
|
-
.with(instance_of(Integer), exception: false)
|
344
|
-
.and_return(data)
|
345
|
-
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
346
|
-
.once
|
347
|
-
.with(instance_of(Integer), exception: false)
|
348
|
-
.and_return(nil)
|
349
|
-
client.read(data_size * 2, timeout: 10)
|
350
|
-
expect(client).to be_closed
|
351
|
-
end
|
352
|
-
end
|
353
|
-
end
|
354
|
-
|
355
|
-
context 'when the data can not be read in time' do
|
356
|
-
before do
|
357
|
-
allow_any_instance_of(::Socket).to receive(:read_nonblock)
|
358
|
-
.and_return(:wait_readable)
|
359
|
-
end
|
360
|
-
it 'raises an exception' do
|
361
|
-
expect { client.read(timeout: 0.25) }.to raise_error(
|
362
|
-
TCPClient::ReadTimeoutError
|
363
|
-
)
|
364
|
-
end
|
365
|
-
|
366
|
-
it 'allows to raise a custom exception' do
|
367
|
-
exception = Class.new(StandardError)
|
368
|
-
expect do
|
369
|
-
client.read(timeout: 0.25, exception: exception)
|
370
|
-
end.to raise_error(exception)
|
371
|
-
end
|
372
|
-
end
|
373
|
-
end
|
374
|
-
|
375
|
-
context 'when a SocketError appears' do
|
376
|
-
it 'does not handle it' do
|
377
|
-
allow_any_instance_of(::Socket).to receive(:read) {
|
378
|
-
raise SocketError
|
379
|
-
}
|
380
|
-
expect { client.read(10) }.to raise_error(SocketError)
|
381
|
-
end
|
382
|
-
|
383
|
-
context 'when normalize_network_errors is configured' do
|
384
|
-
let(:configuration) do
|
385
|
-
TCPClient::Configuration.create(normalize_network_errors: true)
|
386
|
-
end
|
387
|
-
|
388
|
-
SOCKET_ERRORS.each do |error_class|
|
389
|
-
it "raises a TCPClient::NetworkError when a #{error_class} appeared" do
|
390
|
-
allow_any_instance_of(::Socket).to receive(:read) {
|
391
|
-
raise error_class
|
392
|
-
}
|
393
|
-
expect { client.read(12) }.to raise_error(TCPClient::NetworkError)
|
394
|
-
end
|
395
|
-
end
|
396
|
-
end
|
397
|
-
end
|
398
|
-
end
|
399
|
-
|
400
|
-
describe '#readline' do
|
401
|
-
before { allow_any_instance_of(::Socket).to receive(:connect) }
|
402
|
-
|
403
|
-
it 'reads from socket' do
|
404
|
-
expect_any_instance_of(::Socket).to receive(:readline)
|
405
|
-
.once
|
406
|
-
.with($/, chomp: false)
|
407
|
-
.and_return("Hello World\n")
|
408
|
-
expect(client.readline).to eq "Hello World\n"
|
409
|
-
end
|
410
|
-
|
411
|
-
context 'when a separator is specified' do
|
412
|
-
it 'forwards the separator' do
|
413
|
-
expect_any_instance_of(::Socket).to receive(:readline)
|
414
|
-
.once
|
415
|
-
.with('/', chomp: false)
|
416
|
-
.and_return('Hello/')
|
417
|
-
expect(client.readline('/')).to eq 'Hello/'
|
418
|
-
end
|
419
|
-
end
|
420
|
-
|
421
|
-
context 'when chomp is true' do
|
422
|
-
it 'forwards the flag' do
|
423
|
-
expect_any_instance_of(::Socket).to receive(:readline)
|
424
|
-
.once
|
425
|
-
.with($/, chomp: true)
|
426
|
-
.and_return('Hello World')
|
427
|
-
expect(client.readline(chomp: true)).to eq 'Hello World'
|
428
|
-
end
|
429
|
-
end
|
430
|
-
|
431
|
-
context 'when a timeout is specified' do
|
432
|
-
it 'checks the time' do
|
433
|
-
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
434
|
-
.and_return("Hello World\nHello World\n")
|
435
|
-
expect(client.readline(timeout: 10)).to eq "Hello World\n"
|
436
|
-
end
|
437
|
-
|
438
|
-
it 'optional chomps the line' do
|
439
|
-
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
440
|
-
.and_return("Hello World\nHello World\n")
|
441
|
-
expect(client.readline(chomp: true, timeout: 10)).to eq 'Hello World'
|
442
|
-
end
|
443
|
-
|
444
|
-
it 'uses the given separator' do
|
445
|
-
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
446
|
-
.and_return("Hello/World\n")
|
447
|
-
expect(client.readline('/', timeout: 10)).to eq 'Hello/'
|
448
|
-
end
|
449
|
-
|
450
|
-
context 'when data can not be fetched in a single chunk' do
|
451
|
-
it 'reads chunk by chunk' do
|
452
|
-
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
453
|
-
.once
|
454
|
-
.with(instance_of(Integer), exception: false)
|
455
|
-
.and_return('Hello ')
|
456
|
-
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
457
|
-
.once
|
458
|
-
.with(instance_of(Integer), exception: false)
|
459
|
-
.and_return('World')
|
460
|
-
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
461
|
-
.once
|
462
|
-
.with(instance_of(Integer), exception: false)
|
463
|
-
.and_return("\nAnd so...")
|
464
|
-
expect(client.readline(timeout: 10)).to eq "Hello World\n"
|
465
|
-
end
|
466
|
-
|
467
|
-
context 'when socket closed before enough data is avail' do
|
468
|
-
it 'returns available data only' do
|
469
|
-
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
470
|
-
.once
|
471
|
-
.with(instance_of(Integer), exception: false)
|
472
|
-
.and_return('Hello ')
|
473
|
-
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
474
|
-
.once
|
475
|
-
.with(instance_of(Integer), exception: false)
|
476
|
-
.and_return(nil)
|
477
|
-
expect(client.readline(timeout: 10)).to eq "Hello "
|
478
|
-
end
|
479
|
-
|
480
|
-
it 'is closed' do
|
481
|
-
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
482
|
-
.once
|
483
|
-
.with(instance_of(Integer), exception: false)
|
484
|
-
.and_return('Hello ')
|
485
|
-
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
486
|
-
.once
|
487
|
-
.with(instance_of(Integer), exception: false)
|
488
|
-
.and_return(nil)
|
489
|
-
client.readline(timeout: 10)
|
490
|
-
expect(client).to be_closed
|
491
|
-
end
|
492
|
-
end
|
493
|
-
end
|
494
|
-
|
495
|
-
context 'when the data can not be read in time' do
|
496
|
-
before do
|
497
|
-
allow_any_instance_of(::Socket).to receive(:read_nonblock)
|
498
|
-
.and_return(:wait_readable)
|
499
|
-
end
|
500
|
-
it 'raises an exception' do
|
501
|
-
expect { client.readline(timeout: 0.25) }.to raise_error(
|
502
|
-
TCPClient::ReadTimeoutError
|
503
|
-
)
|
504
|
-
end
|
505
|
-
|
506
|
-
it 'allows to raise a custom exception' do
|
507
|
-
exception = Class.new(StandardError)
|
508
|
-
expect do
|
509
|
-
client.read(timeout: 0.25, exception: exception)
|
510
|
-
end.to raise_error(exception)
|
511
|
-
end
|
512
|
-
end
|
513
|
-
end
|
514
|
-
|
515
|
-
context 'when a SocketError appears' do
|
516
|
-
it 'does not handle it' do
|
517
|
-
allow_any_instance_of(::Socket).to receive(:read) {
|
518
|
-
raise SocketError
|
519
|
-
}
|
520
|
-
expect { client.read(10) }.to raise_error(SocketError)
|
521
|
-
end
|
522
|
-
|
523
|
-
context 'when normalize_network_errors is configured' do
|
524
|
-
let(:configuration) do
|
525
|
-
TCPClient::Configuration.create(normalize_network_errors: true)
|
526
|
-
end
|
527
|
-
|
528
|
-
SOCKET_ERRORS.each do |error_class|
|
529
|
-
it "raises a TCPClient::NetworkError when a #{error_class} appeared" do
|
530
|
-
allow_any_instance_of(::Socket).to receive(:read) {
|
531
|
-
raise error_class
|
532
|
-
}
|
533
|
-
expect { client.read(12) }.to raise_error(TCPClient::NetworkError)
|
534
|
-
end
|
535
|
-
end
|
536
|
-
end
|
537
|
-
end
|
538
|
-
end
|
539
|
-
|
540
|
-
describe '#write' do
|
541
|
-
let(:data) { 'some bytes' }
|
542
|
-
let(:data_size) { data.bytesize }
|
543
|
-
|
544
|
-
before { allow_any_instance_of(::Socket).to receive(:connect) }
|
545
|
-
|
546
|
-
it 'writes to the socket' do
|
547
|
-
expect_any_instance_of(::Socket).to receive(:write)
|
548
|
-
.once
|
549
|
-
.with(data)
|
550
|
-
.and_return(data_size)
|
551
|
-
expect(client.write(data)).to be data_size
|
552
|
-
end
|
553
|
-
|
554
|
-
context 'when multiple data chunks are given' do
|
555
|
-
it 'writes each chunk' do
|
556
|
-
expect_any_instance_of(::Socket).to receive(:write)
|
557
|
-
.once
|
558
|
-
.with(data, data)
|
559
|
-
.and_return(data_size * 2)
|
560
|
-
expect(client.write(data, data)).to be data_size * 2
|
561
|
-
end
|
562
|
-
end
|
563
|
-
|
564
|
-
context 'when a timeout is specified' do
|
565
|
-
it 'checks the time' do
|
566
|
-
expect_any_instance_of(::Socket).to receive(:write_nonblock)
|
567
|
-
.and_return(data_size)
|
568
|
-
expect(client.write(data, timeout: 10)).to be data_size
|
569
|
-
end
|
570
|
-
|
571
|
-
context 'when data can not be written in a single chunk' do
|
572
|
-
let(:chunk1) { '1234567890' }
|
573
|
-
let(:chunk2) { '12345' }
|
574
|
-
let(:data1) { chunk1 + chunk2 }
|
575
|
-
let(:chunk3) { 'abcdefghijklm' }
|
576
|
-
let(:chunk4) { 'ABCDE' }
|
577
|
-
let(:data2) { chunk3 + chunk4 }
|
578
|
-
|
579
|
-
it 'writes chunk by chunk and part by part' do
|
580
|
-
expect_any_instance_of(::Socket).to receive(:write_nonblock)
|
581
|
-
.once
|
582
|
-
.with(data1, exception: false)
|
583
|
-
.and_return(chunk1.bytesize)
|
584
|
-
expect_any_instance_of(::Socket).to receive(:write_nonblock)
|
585
|
-
.once
|
586
|
-
.with(chunk2, exception: false)
|
587
|
-
.and_return(chunk2.bytesize)
|
588
|
-
expect_any_instance_of(::Socket).to receive(:write_nonblock)
|
589
|
-
.once
|
590
|
-
.with(data2, exception: false)
|
591
|
-
.and_return(chunk3.bytesize)
|
592
|
-
expect_any_instance_of(::Socket).to receive(:write_nonblock)
|
593
|
-
.once
|
594
|
-
.with(chunk4, exception: false)
|
595
|
-
.and_return(chunk4.bytesize)
|
596
|
-
|
597
|
-
expect(client.write(data1, data2, timeout: 10)).to be(
|
598
|
-
data1.bytesize + data2.bytesize
|
599
|
-
)
|
600
|
-
end
|
601
|
-
end
|
602
|
-
|
603
|
-
context 'when the data can not be written in time' do
|
604
|
-
before do
|
605
|
-
allow_any_instance_of(::Socket).to receive(:write_nonblock)
|
606
|
-
.and_return(:wait_writable)
|
607
|
-
end
|
608
|
-
it 'raises an exception' do
|
609
|
-
expect { client.write(data, timeout: 0.25) }.to raise_error(
|
610
|
-
TCPClient::WriteTimeoutError
|
611
|
-
)
|
612
|
-
end
|
613
|
-
|
614
|
-
it 'allows to raise a custom exception' do
|
615
|
-
exception = Class.new(StandardError)
|
616
|
-
expect do
|
617
|
-
client.write(data, timeout: 0.25, exception: exception)
|
618
|
-
end.to raise_error(exception)
|
619
|
-
end
|
620
|
-
end
|
621
|
-
end
|
622
|
-
|
623
|
-
context 'when a SocketError appears' do
|
624
|
-
before { allow_any_instance_of(::Socket).to receive(:connect) }
|
625
|
-
|
626
|
-
it 'does not handle it' do
|
627
|
-
allow_any_instance_of(::Socket).to receive(:write) {
|
628
|
-
raise SocketError
|
629
|
-
}
|
630
|
-
expect { client.write('some data') }.to raise_error(SocketError)
|
631
|
-
end
|
632
|
-
|
633
|
-
context 'when normalize_network_errors is configured' do
|
634
|
-
let(:configuration) do
|
635
|
-
TCPClient::Configuration.create(normalize_network_errors: true)
|
636
|
-
end
|
637
|
-
|
638
|
-
SOCKET_ERRORS.each do |error_class|
|
639
|
-
it "raises a TCPClient::NetworkError when a #{error_class} appeared" do
|
640
|
-
allow_any_instance_of(::Socket).to receive(:write) {
|
641
|
-
raise error_class
|
642
|
-
}
|
643
|
-
expect { client.write('some data') }.to raise_error(
|
644
|
-
TCPClient::NetworkError
|
645
|
-
)
|
646
|
-
end
|
647
|
-
end
|
648
|
-
end
|
649
|
-
end
|
650
|
-
end
|
651
|
-
|
652
|
-
describe '#with_deadline' do
|
653
|
-
subject(:client) { TCPClient.new }
|
654
|
-
let(:configuration) { TCPClient::Configuration.create(timeout: 60) }
|
655
|
-
|
656
|
-
before do
|
657
|
-
allow_any_instance_of(::Socket).to receive(:connect_nonblock)
|
658
|
-
allow_any_instance_of(::Socket).to receive(:read_nonblock) do |_, size|
|
659
|
-
'r' * size
|
660
|
-
end
|
661
|
-
allow_any_instance_of(::Socket).to receive(
|
662
|
-
:write_nonblock
|
663
|
-
) do |_, data|
|
664
|
-
data.bytesize
|
665
|
-
end
|
666
|
-
end
|
667
|
-
|
668
|
-
it 'allows to use a timeout value for all actions in the given block' do
|
669
|
-
expect_any_instance_of(::Socket).to receive(:connect_nonblock)
|
670
|
-
.once
|
671
|
-
.with(kind_of(String), exception: false)
|
672
|
-
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
673
|
-
.once
|
674
|
-
.with(instance_of(Integer), exception: false)
|
675
|
-
.and_return('123456789012abcdefgAB')
|
676
|
-
expect_any_instance_of(::Socket).to receive(:write_nonblock)
|
677
|
-
.once
|
678
|
-
.with('123456', exception: false)
|
679
|
-
.and_return(6)
|
680
|
-
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
681
|
-
.once
|
682
|
-
.with(instance_of(Integer), exception: false)
|
683
|
-
.and_return('CDEFG')
|
684
|
-
expect_any_instance_of(::Socket).to receive(:write_nonblock)
|
685
|
-
.once
|
686
|
-
.with('abc', exception: false)
|
687
|
-
.and_return(3)
|
688
|
-
expect_any_instance_of(::Socket).to receive(:write_nonblock)
|
689
|
-
.once
|
690
|
-
.with('ABC', exception: false)
|
691
|
-
.and_return(3)
|
692
|
-
expect_any_instance_of(::Socket).to receive(:write_nonblock)
|
693
|
-
.once
|
694
|
-
.with('ABCDEF', exception: false)
|
695
|
-
.and_return(6)
|
696
|
-
|
697
|
-
client.with_deadline(10) do
|
698
|
-
expect(client.connect('localhost:1234', configuration)).to be client
|
699
|
-
expect(client.read(12)).to eq '123456789012'
|
700
|
-
expect(client.write('123456')).to be 6
|
701
|
-
expect(client.read(7)).to eq 'abcdefg'
|
702
|
-
expect(client.read(7)).to eq 'ABCDEFG'
|
703
|
-
expect(client.write('abc')).to be 3
|
704
|
-
expect(client.write('ABC', 'ABCDEF')).to be 9
|
705
|
-
end
|
706
|
-
end
|
707
|
-
|
708
|
-
context 'when called without a block' do
|
709
|
-
it 'raises an exception' do
|
710
|
-
expect { client.with_deadline(0.25) }.to raise_error(ArgumentError)
|
711
|
-
end
|
712
|
-
end
|
713
|
-
|
714
|
-
context 'when #connect fails' do
|
715
|
-
it 'raises an exception' do
|
716
|
-
expect_any_instance_of(::Socket).to receive(:connect_nonblock)
|
717
|
-
.and_return(:wait_writable)
|
718
|
-
expect do
|
719
|
-
client.with_deadline(0.25) do
|
720
|
-
client.connect('localhost:1234', configuration)
|
721
|
-
client.read(12)
|
722
|
-
client.write('Hello World!')
|
723
|
-
end
|
724
|
-
end.to raise_error(TCPClient::ConnectTimeoutError)
|
725
|
-
end
|
726
|
-
end
|
727
|
-
|
728
|
-
context 'when #read fails' do
|
729
|
-
it 'raises an exception' do
|
730
|
-
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
731
|
-
.and_return(:wait_readable)
|
732
|
-
expect do
|
733
|
-
client.with_deadline(0.25) do
|
734
|
-
client.connect('localhost:1234', configuration)
|
735
|
-
client.write('Hello World!')
|
736
|
-
client.read(12)
|
737
|
-
end
|
738
|
-
end.to raise_error(TCPClient::ReadTimeoutError)
|
739
|
-
end
|
740
|
-
end
|
741
|
-
|
742
|
-
context 'when #write fails' do
|
743
|
-
it 'raises an exception' do
|
744
|
-
expect_any_instance_of(::Socket).to receive(:write_nonblock)
|
745
|
-
.and_return(:wait_writable)
|
746
|
-
expect do
|
747
|
-
client.with_deadline(0.25) do
|
748
|
-
client.connect('localhost:1234', configuration)
|
749
|
-
client.read(12)
|
750
|
-
client.write('Hello World!')
|
751
|
-
end
|
752
|
-
end.to raise_error(TCPClient::WriteTimeoutError)
|
753
|
-
end
|
754
|
-
end
|
755
|
-
end
|
756
|
-
end
|
757
|
-
|
758
|
-
context 'when using SSL' do
|
759
|
-
let(:configuration) do
|
760
|
-
TCPClient::Configuration.create(
|
761
|
-
buffered: false,
|
762
|
-
reverse_lookup: false,
|
763
|
-
ssl: {
|
764
|
-
min_version: :TLS1_2,
|
765
|
-
max_version: :TLS1_3
|
766
|
-
}
|
767
|
-
)
|
768
|
-
end
|
769
|
-
|
770
|
-
before do
|
771
|
-
allow_any_instance_of(::Socket).to receive(:connect)
|
772
|
-
allow_any_instance_of(::OpenSSL::SSL::SSLSocket).to receive(:connect)
|
773
|
-
end
|
774
|
-
|
775
|
-
describe '#connect' do
|
776
|
-
it 'configures the SSL socket' do
|
777
|
-
# this produces a mock warning :(
|
778
|
-
# expect_any_instance_of(::OpenSSL::SSL::SSLContext).to receive(
|
779
|
-
# :set_params
|
780
|
-
# )
|
781
|
-
# .once
|
782
|
-
# .with(ssl_version: :TLSv1_2)
|
783
|
-
# .and_call_original
|
784
|
-
expect_any_instance_of(::OpenSSL::SSL::SSLSocket).to receive(
|
785
|
-
:sync_close=
|
786
|
-
)
|
787
|
-
.once
|
788
|
-
.with(true)
|
789
|
-
.and_call_original
|
790
|
-
expect_any_instance_of(::OpenSSL::SSL::SSLSocket).to receive(
|
791
|
-
:post_connection_check
|
792
|
-
)
|
793
|
-
.once
|
794
|
-
.with('localhost')
|
795
|
-
|
796
|
-
TCPClient.new.connect('localhost:1234', configuration)
|
797
|
-
end
|
798
|
-
end
|
799
|
-
end
|
800
|
-
end
|