tcp-client 0.9.1 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -9,8 +9,8 @@ RSpec.describe TCPClient::Address do
9
9
 
10
10
  it 'points to the given port on localhost' do
11
11
  expect(address.hostname).to eq 'localhost'
12
+ expect(address.port).to be 42
12
13
  expect(address.to_s).to eq 'localhost:42'
13
- expect(address.addrinfo.ip_port).to be 42
14
14
  end
15
15
 
16
16
  it 'uses IPv6' do
@@ -29,8 +29,9 @@ RSpec.describe TCPClient::Address do
29
29
  end
30
30
 
31
31
  it 'points to the given host and port' do
32
- expect(address.hostname).to eq addrinfo.getnameinfo[0]
33
- expect(address.addrinfo.ip_port).to be 42
32
+ expect(address.hostname).to eq 'localhost'
33
+ expect(address.port).to be 42
34
+ expect(address.to_s).to eq 'localhost:42'
34
35
  end
35
36
 
36
37
  it 'uses IPv6' do
@@ -46,30 +47,21 @@ RSpec.describe TCPClient::Address do
46
47
 
47
48
  it 'points to the given host and port' do
48
49
  expect(address.hostname).to eq 'localhost'
50
+ expect(address.port).to be 42
49
51
  expect(address.to_s).to eq 'localhost:42'
50
- expect(address.addrinfo.ip_port).to be 42
51
- end
52
-
53
- it 'uses IPv6' do
54
52
  expect(address.addrinfo.ip?).to be true
55
- expect(address.addrinfo.ipv6?).to be true
56
- expect(address.addrinfo.ipv4?).to be false
57
53
  end
54
+
58
55
  end
59
56
 
60
57
  context 'when only a port is provided' do
61
- subject(:address) { TCPClient::Address.new(':21') }
58
+ subject(:address) { TCPClient::Address.new(':42') }
62
59
 
63
60
  it 'points to the given port on localhost' do
64
- expect(address.hostname).to eq ''
65
- expect(address.to_s).to eq ':21'
66
- expect(address.addrinfo.ip_port).to be 21
67
- end
68
-
69
- it 'uses IPv4' do
61
+ expect(address.hostname).to eq 'localhost'
62
+ expect(address.port).to be 42
63
+ expect(address.to_s).to eq 'localhost:42'
70
64
  expect(address.addrinfo.ip?).to be true
71
- expect(address.addrinfo.ipv6?).to be false
72
- expect(address.addrinfo.ipv4?).to be true
73
65
  end
74
66
  end
75
67
 
@@ -78,14 +70,9 @@ RSpec.describe TCPClient::Address do
78
70
 
79
71
  it 'points to the given port on localhost' do
80
72
  expect(address.hostname).to eq '::1'
73
+ expect(address.port).to be 42
81
74
  expect(address.to_s).to eq '[::1]:42'
82
- expect(address.addrinfo.ip_port).to be 42
83
- end
84
-
85
- it 'uses IPv6' do
86
75
  expect(address.addrinfo.ip?).to be true
87
- expect(address.addrinfo.ipv6?).to be true
88
- expect(address.addrinfo.ipv4?).to be false
89
76
  end
90
77
  end
91
78
  end
@@ -108,13 +95,13 @@ RSpec.describe TCPClient::Address do
108
95
  expect(address_a).to eq address_b
109
96
  end
110
97
 
111
- context 'using the == opperator' do
98
+ context 'using the == operator' do
112
99
  it 'compares to equal' do
113
100
  expect(address_a == address_b).to be true
114
101
  end
115
102
  end
116
103
 
117
- context 'using the === opperator' do
104
+ context 'using the === operator' do
118
105
  it 'compares to equal' do
119
106
  expect(address_a === address_b).to be true
120
107
  end
@@ -129,13 +116,13 @@ RSpec.describe TCPClient::Address do
129
116
  expect(address_a).not_to eq address_b
130
117
  end
131
118
 
132
- context 'using the == opperator' do
119
+ context 'using the == operator' do
133
120
  it 'compares not to equal' do
134
121
  expect(address_a == address_b).to be false
135
122
  end
136
123
  end
137
124
 
138
- context 'using the === opperator' do
125
+ context 'using the === operator' do
139
126
  it 'compares not to equal' do
140
127
  expect(address_a === address_b).to be false
141
128
  end
@@ -82,7 +82,7 @@ RSpec.describe TCPClient::Configuration do
82
82
  expect(configuration.keep_alive).to be false
83
83
  end
84
84
 
85
- it 'allows to configure reverse address lokup' do
85
+ it 'allows to configure reverse address lookup' do
86
86
  expect(configuration.reverse_lookup).to be false
87
87
  end
88
88
 
@@ -148,7 +148,7 @@ RSpec.describe TCPClient::Configuration do
148
148
  end
149
149
  end
150
150
 
151
- context 'with invalid attribte' do
151
+ context 'with invalid attribute' do
152
152
  it 'raises an error' do
153
153
  expect { TCPClient::Configuration.new(invalid: :value) }.to raise_error(
154
154
  TCPClient::UnknownAttributeError
@@ -182,6 +182,7 @@ RSpec.describe TCPClient::Configuration do
182
182
  read_timeout_error: TCPClient::ReadTimeoutError,
183
183
  write_timeout: 3,
184
184
  write_timeout_error: TCPClient::WriteTimeoutError,
185
+ normalize_network_errors: false,
185
186
  ssl_params: {
186
187
  min_version: :TLS1_2,
187
188
  max_version: :TLS1_3
@@ -232,13 +233,13 @@ RSpec.describe TCPClient::Configuration do
232
233
  expect(config_a).to eq config_b
233
234
  end
234
235
 
235
- context 'using the == opperator' do
236
+ context 'using the == operator' do
236
237
  it 'compares to equal' do
237
238
  expect(config_a == config_b).to be true
238
239
  end
239
240
  end
240
241
 
241
- context 'using the === opperator' do
242
+ context 'using the === operator' do
242
243
  it 'compares to equal' do
243
244
  expect(config_a === config_b).to be true
244
245
  end
@@ -253,13 +254,13 @@ RSpec.describe TCPClient::Configuration do
253
254
  expect(config_a).not_to eq config_b
254
255
  end
255
256
 
256
- context 'using the == opperator' do
257
+ context 'using the == operator' do
257
258
  it 'compares not to equal' do
258
259
  expect(config_a == config_b).to be false
259
260
  end
260
261
  end
261
262
 
262
- context 'using the === opperator' do
263
+ context 'using the === operator' do
263
264
  it 'compares not to equal' do
264
265
  expect(config_a === config_b).to be false
265
266
  end
@@ -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.closed?).to be true
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.closed?).to be false
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.closed?).to be true
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
- TCPClient.new.connect('localhost:1234', configuration)
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
- TCPClient.new.connect('localhost:1234', configuration, timeout: 10)
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(client).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
- TCPClient.new.connect(
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
- TCPClient.new.connect(
219
+ client.connect(
203
220
  'localhost:1234',
204
221
  configuration,
205
- timeout: 0.25,
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 a TCPClient::NetworkError when a #{error_class} appeared" do
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(data_size * 2, exception: false)
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(data_size, exception: false)
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(12, exception: false)
467
- .and_return('123456789012')
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(7, exception: false)
475
- .and_return('abcdefg')
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)
data/tcp-client.gemspec CHANGED
@@ -5,12 +5,11 @@ require_relative './lib/tcp-client/version'
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = 'tcp-client'
7
7
  spec.version = TCPClient::VERSION
8
- spec.author = 'Mike Blumtritt'
9
-
10
8
  spec.required_ruby_version = '>= 2.7.0'
11
9
 
10
+ spec.author = 'Mike Blumtritt'
12
11
  spec.summary = 'A TCP client implementation with working timeout support.'
13
- spec.description = <<~DESCRIPTION
12
+ spec.description = <<~description
14
13
  This Gem implements a TCP client with (optional) SSL support.
15
14
  It is an easy to use, versatile configurable client that can correctly
16
15
  handle time limits.
@@ -18,15 +17,15 @@ Gem::Specification.new do |spec|
18
17
  predefined/configurable time limits for each method
19
18
  (`connect`, `read`, `write`). Deadlines for a sequence of read/write
20
19
  actions can also be monitored.
21
- DESCRIPTION
20
+ description
21
+
22
22
  spec.homepage = 'https://github.com/mblumtritt/tcp-client'
23
23
  spec.license = 'BSD-3-Clause'
24
-
25
- spec.metadata['source_code_uri'] = 'https://github.com/mblumtritt/tcp-client'
26
- spec.metadata['documentation_uri'] =
27
- 'https://rubydoc.info/github/mblumtritt/tcp-client'
28
- spec.metadata['bug_tracker_uri'] =
29
- 'https://github.com/mblumtritt/tcp-client/issues'
24
+ spec.metadata.merge!(
25
+ 'source_code_uri' => 'https://github.com/mblumtritt/tcp-client',
26
+ 'bug_tracker_uri' => 'https://github.com/mblumtritt/tcp-client/issues',
27
+ 'documentation_uri' => 'https://rubydoc.info/github/mblumtritt/tcp-client'
28
+ )
30
29
 
31
30
  spec.add_development_dependency 'bundler'
32
31
  spec.add_development_dependency 'rake'
@@ -36,6 +35,5 @@ Gem::Specification.new do |spec|
36
35
  all_files = Dir.chdir(__dir__) { `git ls-files -z`.split(0.chr) }
37
36
  spec.test_files = all_files.grep(%r{^spec/})
38
37
  spec.files = all_files - spec.test_files
39
-
40
38
  spec.extra_rdoc_files = %w[README.md LICENSE]
41
39
  end
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.9.1
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Blumtritt
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-12-01 00:00:00.000000000 Z
11
+ date: 2022-01-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -82,6 +82,7 @@ extra_rdoc_files:
82
82
  - LICENSE
83
83
  files:
84
84
  - ".gitignore"
85
+ - ".yardopts"
85
86
  - LICENSE
86
87
  - README.md
87
88
  - gems.rb
@@ -111,8 +112,8 @@ licenses:
111
112
  - BSD-3-Clause
112
113
  metadata:
113
114
  source_code_uri: https://github.com/mblumtritt/tcp-client
114
- documentation_uri: https://rubydoc.info/github/mblumtritt/tcp-client
115
115
  bug_tracker_uri: https://github.com/mblumtritt/tcp-client/issues
116
+ documentation_uri: https://rubydoc.info/github/mblumtritt/tcp-client
116
117
  post_install_message:
117
118
  rdoc_options: []
118
119
  require_paths:
@@ -128,7 +129,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
128
129
  - !ruby/object:Gem::Version
129
130
  version: '0'
130
131
  requirements: []
131
- rubygems_version: 3.2.28
132
+ rubygems_version: 3.3.3
132
133
  signing_key:
133
134
  specification_version: 4
134
135
  summary: A TCP client implementation with working timeout support.