tcp-client 0.9.1 → 0.10.0

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.
@@ -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.