tcp-client 0.9.3 → 0.10.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +8 -5
- data/lib/tcp-client/address.rb +16 -7
- data/lib/tcp-client/configuration.rb +37 -45
- data/lib/tcp-client/deadline.rb +1 -3
- data/lib/tcp-client/default_configuration.rb +1 -1
- data/lib/tcp-client/errors.rb +13 -7
- data/lib/tcp-client/mixin/io_with_deadline.rb +126 -88
- data/lib/tcp-client/ssl_socket.rb +1 -1
- data/lib/tcp-client/version.rb +2 -1
- data/lib/tcp-client.rb +87 -51
- data/rakefile.rb +7 -1
- data/sample/google_ssl.rb +8 -5
- data/spec/tcp-client/address_spec.rb +15 -28
- data/spec/tcp-client/configuration_spec.rb +4 -4
- data/spec/tcp_client_spec.rb +227 -23
- metadata +3 -3
data/spec/tcp_client_spec.rb
CHANGED
@@ -26,7 +26,7 @@ RSpec.describe TCPClient do
|
|
26
26
|
subject(:client) { TCPClient.new }
|
27
27
|
|
28
28
|
it 'is closed' do
|
29
|
-
expect(client
|
29
|
+
expect(client).to be_closed
|
30
30
|
end
|
31
31
|
|
32
32
|
it 'has no address' do
|
@@ -64,7 +64,7 @@ RSpec.describe TCPClient do
|
|
64
64
|
before { allow_any_instance_of(::Socket).to receive(:connect) }
|
65
65
|
|
66
66
|
it 'is not closed' do
|
67
|
-
expect(client
|
67
|
+
expect(client).not_to be_closed
|
68
68
|
end
|
69
69
|
|
70
70
|
it 'has an address' do
|
@@ -115,7 +115,7 @@ RSpec.describe TCPClient do
|
|
115
115
|
end
|
116
116
|
|
117
117
|
it 'is closed' do
|
118
|
-
expect(client
|
118
|
+
expect(client).to be_closed
|
119
119
|
end
|
120
120
|
|
121
121
|
it 'has an address' do
|
@@ -157,6 +157,8 @@ RSpec.describe TCPClient do
|
|
157
157
|
|
158
158
|
context 'when not using SSL' do
|
159
159
|
describe '#connect' do
|
160
|
+
subject(:client) { TCPClient.new }
|
161
|
+
|
160
162
|
it 'configures the socket' do
|
161
163
|
expect_any_instance_of(::Socket).to receive(:sync=).once.with(true)
|
162
164
|
expect_any_instance_of(::Socket).to receive(:setsockopt)
|
@@ -169,7 +171,7 @@ RSpec.describe TCPClient do
|
|
169
171
|
.once
|
170
172
|
.with(false)
|
171
173
|
expect_any_instance_of(::Socket).to receive(:connect)
|
172
|
-
|
174
|
+
client.connect('localhost:1234', configuration)
|
173
175
|
end
|
174
176
|
|
175
177
|
context 'when a timeout is specified' do
|
@@ -177,7 +179,26 @@ RSpec.describe TCPClient do
|
|
177
179
|
expect_any_instance_of(::Socket).to receive(:connect_nonblock)
|
178
180
|
.once
|
179
181
|
.with(kind_of(String), exception: false)
|
180
|
-
|
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
|
181
202
|
end
|
182
203
|
|
183
204
|
context 'when the connection can not be established in time' do
|
@@ -188,25 +209,29 @@ RSpec.describe TCPClient do
|
|
188
209
|
|
189
210
|
it 'raises an exception' do
|
190
211
|
expect do
|
191
|
-
|
192
|
-
'localhost:1234',
|
193
|
-
configuration,
|
194
|
-
timeout: 0.25
|
195
|
-
)
|
212
|
+
client.connect('localhost:1234', configuration, timeout: 0.1)
|
196
213
|
end.to raise_error(TCPClient::ConnectTimeoutError)
|
197
214
|
end
|
198
215
|
|
199
216
|
it 'allows to raise a custom exception' do
|
200
217
|
exception = Class.new(StandardError)
|
201
218
|
expect do
|
202
|
-
|
219
|
+
client.connect(
|
203
220
|
'localhost:1234',
|
204
221
|
configuration,
|
205
|
-
timeout: 0.
|
222
|
+
timeout: 0.1,
|
206
223
|
exception: exception
|
207
224
|
)
|
208
225
|
end.to raise_error(exception)
|
209
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
|
210
235
|
end
|
211
236
|
end
|
212
237
|
|
@@ -226,7 +251,7 @@ RSpec.describe TCPClient do
|
|
226
251
|
end
|
227
252
|
|
228
253
|
SOCKET_ERRORS.each do |error_class|
|
229
|
-
it "raises
|
254
|
+
it "raises TCPClient::NetworkError when a #{error_class} appeared" do
|
230
255
|
allow_any_instance_of(::Socket).to receive(:connect) {
|
231
256
|
raise error_class
|
232
257
|
}
|
@@ -270,18 +295,61 @@ RSpec.describe TCPClient do
|
|
270
295
|
expect(client.read(timeout: 10)).to be data
|
271
296
|
end
|
272
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
|
+
|
273
314
|
context 'when data can not be fetched in a single chunk' do
|
274
315
|
it 'reads chunk by chunk' do
|
275
316
|
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
276
317
|
.once
|
277
|
-
.with(
|
318
|
+
.with(instance_of(Integer), exception: false)
|
278
319
|
.and_return(data)
|
279
320
|
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
280
321
|
.once
|
281
|
-
.with(
|
322
|
+
.with(instance_of(Integer), exception: false)
|
282
323
|
.and_return(data)
|
283
324
|
expect(client.read(data_size * 2, timeout: 10)).to eq data * 2
|
284
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
|
285
353
|
end
|
286
354
|
|
287
355
|
context 'when the data can not be read in time' do
|
@@ -329,6 +397,146 @@ RSpec.describe TCPClient do
|
|
329
397
|
end
|
330
398
|
end
|
331
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
|
+
|
332
540
|
describe '#write' do
|
333
541
|
let(:data) { 'some bytes' }
|
334
542
|
let(:data_size) { data.bytesize }
|
@@ -463,20 +671,16 @@ RSpec.describe TCPClient do
|
|
463
671
|
.with(kind_of(String), exception: false)
|
464
672
|
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
465
673
|
.once
|
466
|
-
.with(
|
467
|
-
.and_return('
|
674
|
+
.with(instance_of(Integer), exception: false)
|
675
|
+
.and_return('123456789012abcdefgAB')
|
468
676
|
expect_any_instance_of(::Socket).to receive(:write_nonblock)
|
469
677
|
.once
|
470
678
|
.with('123456', exception: false)
|
471
679
|
.and_return(6)
|
472
680
|
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
473
681
|
.once
|
474
|
-
.with(
|
475
|
-
.and_return('
|
476
|
-
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
477
|
-
.once
|
478
|
-
.with(7, exception: false)
|
479
|
-
.and_return('ABCDEFG')
|
682
|
+
.with(instance_of(Integer), exception: false)
|
683
|
+
.and_return('CDEFG')
|
480
684
|
expect_any_instance_of(::Socket).to receive(:write_nonblock)
|
481
685
|
.once
|
482
686
|
.with('abc', exception: false)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tcp-client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.10.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mike Blumtritt
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-03-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -129,7 +129,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
129
129
|
- !ruby/object:Gem::Version
|
130
130
|
version: '0'
|
131
131
|
requirements: []
|
132
|
-
rubygems_version: 3.
|
132
|
+
rubygems_version: 3.3.7
|
133
133
|
signing_key:
|
134
134
|
specification_version: 4
|
135
135
|
summary: A TCP client implementation with working timeout support.
|