tcp-client 0.9.4 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 114da301d59fc9cf9c3d3bda508f4d23b5b4991877a5dc7504d4da97114f8e7f
4
- data.tar.gz: 9d0815501de340b485cba157aff05097597948d023a7eca6c45a8ead3a4e3d0d
3
+ metadata.gz: e88146d7f1f966d6c9e7dce6a876c17dca7383ee88eb76a8e5caf36076a03720
4
+ data.tar.gz: 493d8cbf748789df4efd0301679082023c140f23dbc2a3d52ccfcf8d71a4a1b4
5
5
  SHA512:
6
- metadata.gz: a560644d578cedccaf1e2665f518b3a6a4e225886cfc561a8b62087420d93b958ee4741d8efde3b456fa15ce24fd5ee2f6a1121b55855205ffae5abf17becf97
7
- data.tar.gz: f1f0ed0b87f8ee9c9b376b6b567c45aecb204c5b3de044c97f6ba080071c3c8e70da8bca8cca2484d5ddec06df181ee4b007e9ed826045e205d77df38eefbbbd
6
+ metadata.gz: 070ba42a0a185a67ae1ac4d1912220ab0aa297a6cdc4147d3afaf8593129f3b25da2ecad7a8569651c6d54dd1ba11c7705ebf8bcc1def2d525388025b45d12b3
7
+ data.tar.gz: 4c34127d89744a537d5b64c578ac4c1e2121c27e9412208f7c0abcf74999bb1e85c1c246bebfea83401c8caaf4c472daee7ff5e599989632076784881131b3ea
data/README.md CHANGED
@@ -27,11 +27,14 @@ cfg = TCPClient::Configuration.create(
27
27
  # - limit all network interactions to 1.5 seconds
28
28
  # - use the Configuration cfg
29
29
  # - send a simple HTTP get request
30
- # - read 12 byte: "HTTP/1.1 " + 3 byte HTTP status code
31
- TCPClient.with_deadline(1.5, 'www.google.com:443', cfg) do |client|
32
- client.write("GET / HTTP/1.1\r\nHost: www.google.com\r\n\r\n") # >= 40
33
- client.read(12) # => "HTTP/1.1 200"
34
- end
30
+ # - read the returned message and headers
31
+ response =
32
+ TCPClient.with_deadline(1.5, 'www.google.com:443', cfg) do |client|
33
+ client.write("GET / HTTP/1.1\r\nHost: www.google.com\r\n\r\n") #=> 40
34
+ client.readline("\r\n\r\n") #=> see response
35
+ end
36
+
37
+ puts(response)
35
38
  ```
36
39
 
37
40
  For more samples see [the samples dir](https://github.com/mblumtritt/tcp-client/tree/main/sample)
@@ -41,7 +41,7 @@ class TCPClient
41
41
  # @param addrinfo [Addrinfo] containing the addressed host and port
42
42
  #
43
43
  # @overload initialize(port)
44
- # Adresses the port on the local machine.
44
+ # Addresses the port on the local machine.
45
45
  #
46
46
  # @example create an Address for localhost on port 80
47
47
  # Address.new(80)
@@ -26,7 +26,7 @@ class TCPClient
26
26
  # @example
27
27
  # config = TCPClient::Configuration.create(buffered: false)
28
28
  #
29
- # @param options [Hash<Symbol,Object>] see {#initialize} for details
29
+ # @param options [{Symbol => Object}] see {#initialize} for details
30
30
  #
31
31
  # @return [Configuration] the initialized configuration
32
32
  #
@@ -37,13 +37,13 @@ class TCPClient
37
37
  end
38
38
 
39
39
  #
40
- # Intializes the instance with given options.
40
+ # Initializes the instance with given options.
41
41
  #
42
- # @param options [Hash<Symbol,Object>]
42
+ # @param options [{Symbol => Object}]
43
43
  # @option options [Boolean] :buffered, see {#buffered}
44
44
  # @option options [Boolean] :keep_alive, see {#keep_alive}
45
45
  # @option options [Boolean] :reverse_lookup, see {#reverse_lookup}
46
- # @option options [Hash<Symbol, Object>] :ssl_params, see {#ssl_params}
46
+ # @option options [{Symbol => Object}] :ssl_params, see {#ssl_params}
47
47
  # @option options [Numeric] :connect_timeout, see {#connect_timeout}
48
48
  # @option options [Class<Exception>] :connect_timeout_error, see
49
49
  # {#connect_timeout_error}
@@ -56,7 +56,6 @@ class TCPClient
56
56
  # @option options [Boolean] :normalize_network_errors, see
57
57
  # {#normalize_network_errors}
58
58
  #
59
- #
60
59
  def initialize(options = {})
61
60
  @buffered = @keep_alive = @reverse_lookup = true
62
61
  self.timeout = @ssl_params = nil
@@ -72,8 +71,8 @@ class TCPClient
72
71
  #
73
72
  # Enables/disables use of Socket-level buffering
74
73
  #
75
- # @return [Boolean] wheter the connection is allowed to use internal buffers
76
- # (default) or not
74
+ # @return [Boolean] whether the connection is allowed to use internal
75
+ # buffers (default) or not
77
76
  #
78
77
  attr_reader :buffered
79
78
 
@@ -84,7 +83,7 @@ class TCPClient
84
83
  #
85
84
  # Enables/disables use of Socket-level keep alive handling.
86
85
  #
87
- # @return [Boolean] wheter the connection is allowed to use keep alive
86
+ # @return [Boolean] whether the connection is allowed to use keep alive
88
87
  # signals (default) or not
89
88
  #
90
89
  attr_reader :keep_alive
@@ -96,7 +95,7 @@ class TCPClient
96
95
  #
97
96
  # Enables/disables address lookup.
98
97
  #
99
- # @return [Boolean] wheter the connection is allowed to lookup the address
98
+ # @return [Boolean] whether the connection is allowed to lookup the address
100
99
  # (default) or not
101
100
  #
102
101
  attr_reader :reverse_lookup
@@ -107,7 +106,7 @@ class TCPClient
107
106
 
108
107
  #
109
108
  # @!parse attr_reader :ssl?
110
- # @return [Boolean] wheter SSL is configured, see {#ssl_params}
109
+ # @return [Boolean] whether SSL is configured, see {#ssl_params}
111
110
  #
112
111
  def ssl?
113
112
  @ssl_params ? true : false
@@ -117,7 +116,7 @@ class TCPClient
117
116
  # Parameters used to initialize a SSL context. SSL/TLS will only be used if
118
117
  # this attribute is not `nil`.
119
118
  #
120
- # @return [Hash<Symbol, Object>] SSL parameters for the SSL context
119
+ # @return [{Symbol => Object}] SSL parameters for the SSL context
121
120
  # @return [nil] if no SSL should be used (default)
122
121
  #
123
122
  attr_reader :ssl_params
@@ -226,7 +225,7 @@ class TCPClient
226
225
  # @attribute [w] timeout
227
226
  # Shorthand to set maximum time in seconds for all timeout monitoring.
228
227
  #
229
- # @return [Numeric] maximum time in seconds for any actwion
228
+ # @return [Numeric] maximum time in seconds for any action
230
229
  # @return [nil] if all timeout monitoring should be disabled (default)
231
230
  #
232
231
  # @see #connect_timeout
@@ -239,7 +238,7 @@ class TCPClient
239
238
 
240
239
  #
241
240
  # @attribute [w] timeout_error
242
- # Shorthand to set the exception class wich will by raised by any reached
241
+ # Shorthand to set the exception class which will by raised by any reached
243
242
  # timeout.
244
243
  #
245
244
  # @return [Class<Exception>] exception class raised
@@ -266,7 +265,7 @@ class TCPClient
266
265
  # manner. If this option is set to true all these error cases are raised as
267
266
  # {NetworkError} and can be easily captured.
268
267
  #
269
- # @return [Boolean] wheter all network exceptions should be raised as
268
+ # @return [Boolean] whether all network exceptions should be raised as
270
269
  # {NetworkError}, or not (default)
271
270
  #
272
271
  attr_reader :normalize_network_errors
@@ -276,9 +275,7 @@ class TCPClient
276
275
  end
277
276
 
278
277
  #
279
- # Convert `self` to a Hash containing all attributes.
280
- #
281
- # @return [Hash<Symbol, Object>]
278
+ # @return [{Symbol => Object}] Hash containing all attributes
282
279
  #
283
280
  # @see #initialize
284
281
  #
@@ -2,7 +2,7 @@
2
2
 
3
3
  class TCPClient
4
4
  #
5
- # Raised when a SSL connection should be establshed but the OpenSSL gem is
5
+ # Raised when a SSL connection should be established but the OpenSSL gem is
6
6
  # not available.
7
7
  #
8
8
  class NoOpenSSLError < RuntimeError
@@ -38,7 +38,7 @@ class TCPClient
38
38
  #
39
39
  class UnknownAttributeError < ArgumentError
40
40
  #
41
- # @param attribute [Object] the undefined atttribute
41
+ # @param attribute [Object] the undefined attribute
42
42
  #
43
43
  def initialize(attribute)
44
44
  super("unknown attribute - #{attribute}")
@@ -13,25 +13,30 @@ module IOWithDeadlineMixin # :nodoc:
13
13
  end
14
14
  end
15
15
 
16
- def read_with_deadline(bytes_to_read, deadline, exception)
16
+ def read_with_deadline(nbytes, deadline, exception)
17
17
  raise(exception) unless deadline.remaining_time
18
- if bytes_to_read.nil?
19
- return(
20
- with_deadline(deadline, exception) do
21
- read_nonblock(65_536, exception: false)
22
- end
23
- )
18
+ return fetch_avail(deadline, exception) if nbytes.nil?
19
+ return ''.b if nbytes.zero?
20
+ @buf ||= ''.b
21
+ while @buf.bytesize < nbytes
22
+ read = fetch_next(deadline, exception) and next @buf << read
23
+ close
24
+ break
24
25
  end
25
- result = ''.b
26
- while result.bytesize < bytes_to_read
27
- read =
28
- with_deadline(deadline, exception) do
29
- read_nonblock(bytes_to_read - result.bytesize, exception: false)
30
- end
31
- next result += read if read
26
+ fetch_buffer_slice(nbytes)
27
+ end
28
+
29
+ def readto_with_deadline(sep, deadline, exception)
30
+ raise(exception) unless deadline.remaining_time
31
+ @buf ||= ''.b
32
+ while (index = @buf.index(sep)).nil?
33
+ read = fetch_next(deadline, exception) and next @buf << read
32
34
  close
33
35
  break
34
36
  end
37
+ index = @buf.index(sep) and return fetch_buffer_slice(index + sep.bytesize)
38
+ result = @buf
39
+ @buf = nil
35
40
  result
36
41
  end
37
42
 
@@ -44,12 +49,37 @@ module IOWithDeadlineMixin # :nodoc:
44
49
  with_deadline(deadline, exception) do
45
50
  write_nonblock(data, exception: false)
46
51
  end
47
- result += written
48
- return result if result >= size
52
+ (result += written) >= size and return result
49
53
  data = data.byteslice(written, data.bytesize - written)
50
54
  end
51
55
  end
52
56
 
57
+ private
58
+
59
+ def fetch_avail(deadline, exception)
60
+ if @buf.nil?
61
+ result = fetch_next(deadline, exception) and return result
62
+ close
63
+ return ''.b
64
+ end
65
+ result = @buf
66
+ @buf = nil
67
+ result
68
+ end
69
+
70
+ def fetch_buffer_slice(size)
71
+ result = @buf.byteslice(0, size)
72
+ rest = @buf.bytesize - result.bytesize
73
+ @buf = rest.zero? ? nil : @buf.byteslice(size, rest)
74
+ result
75
+ end
76
+
77
+ def fetch_next(deadline, exception)
78
+ with_deadline(deadline, exception) do
79
+ read_nonblock(65_536, exception: false)
80
+ end
81
+ end
82
+
53
83
  module ViaWaitMethod
54
84
  private def with_deadline(deadline, exception)
55
85
  loop do
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class TCPClient
4
- VERSION = '0.9.4'
4
+ # The current version number.
5
+ VERSION = '0.10.0'
5
6
  end
data/lib/tcp-client.rb CHANGED
@@ -16,12 +16,15 @@ require_relative 'tcp-client/version'
16
16
  # terminate before given time limits - or raise an exception.
17
17
  #
18
18
  # @example request to Google.com and limit network interactions to 1.5 seconds
19
- # TCPClient.with_deadline(1.5, 'www.google.com:443') do |client|
20
- # client.write("GET / HTTP/1.1\r\nHost: www.google.com\r\n\r\n")
21
- # client.read(12)
22
- # end
23
- # # => "HTTP/1.1 200"
19
+ # # create a configuration to use at least TLS 1.2
20
+ # cfg = TCPClient::Configuration.create(ssl_params: {min_version: :TLS1_2})
24
21
  #
22
+ # response =
23
+ # TCPClient.with_deadline(1.5, 'www.google.com:443', cfg) do |client|
24
+ # client.write("GET / HTTP/1.1\r\nHost: www.google.com\r\n\r\n") #=> 40
25
+ # client.readline("\r\n\r\n") #=> see response
26
+ # end
27
+ # # response contains the returned message and header
25
28
  #
26
29
  class TCPClient
27
30
  #
@@ -40,10 +43,10 @@ class TCPClient
40
43
  #
41
44
  # If an optional block is given, then the block's result is returned and the
42
45
  # connection will be closed when the block execution ends.
43
- # This can be used to create an ad-hoc connection which is garanteed to be
46
+ # This can be used to create an ad-hoc connection which is guaranteed to be
44
47
  # closed.
45
48
  #
46
- # If no block is giiven the connected client instance is returned.
49
+ # If no block is given the connected client instance is returned.
47
50
  # This can be used as a shorthand to create & connect a client.
48
51
  #
49
52
  # @param address [Address, String, Addrinfo, Integer] the target address see
@@ -55,7 +58,7 @@ class TCPClient
55
58
  #
56
59
  def self.open(address, configuration = nil)
57
60
  client = new
58
- client.connect(Address.new(address), configuration)
61
+ client.connect(address, configuration)
59
62
  block_given? ? yield(client) : client
60
63
  ensure
61
64
  client.close if block_given?
@@ -67,9 +70,9 @@ class TCPClient
67
70
  # the given time.
68
71
  #
69
72
  # It ensures to close the connection when the block execution ends and returns
70
- # the block`s result.
73
+ # the block's result.
71
74
  #
72
- # This can be used to create an ad-hoc connection which is garanteed to be
75
+ # This can be used to create an ad-hoc connection which is guaranteed to be
73
76
  # closed and which {#read}/{#write} call sequence should not last longer than
74
77
  # the `timeout` seconds.
75
78
  #
@@ -91,7 +94,6 @@ class TCPClient
91
94
  def self.with_deadline(timeout, address, configuration = nil)
92
95
  client = nil
93
96
  raise(NoBlockGivenError) unless block_given?
94
- address = Address.new(address)
95
97
  client = new
96
98
  client.with_deadline(timeout) do
97
99
  yield(client.connect(address, configuration))
@@ -112,7 +114,7 @@ class TCPClient
112
114
 
113
115
  #
114
116
  # @!parse attr_reader :closed?
115
- # @return [Boolean] wheter the connection is closed
117
+ # @return [Boolean] whether the connection is closed
116
118
  #
117
119
  def closed?
118
120
  @socket.nil? || @socket.closed?
@@ -121,7 +123,7 @@ class TCPClient
121
123
  #
122
124
  # Close the current connection if connected.
123
125
  #
124
- # @return [self]
126
+ # @return [TCPClient] itself
125
127
  #
126
128
  def close
127
129
  @socket&.close
@@ -133,7 +135,7 @@ class TCPClient
133
135
  end
134
136
 
135
137
  #
136
- # Establishes a new connection to a given `address`.
138
+ # Establishes a new connection to a server on given `address`.
137
139
  #
138
140
  # It accepts a connection-specific `configuration` or uses the
139
141
  # {.default_configuration}.
@@ -141,7 +143,7 @@ class TCPClient
141
143
  # The optional `timeout` and `exception` parameters allow to override the
142
144
  # `connect_timeout` and `connect_timeout_error` values.
143
145
  #
144
- # @param address [Address, String, Addrinfo, Integer] the target address see
146
+ # @param address [Address, String, Addrinfo, Integer] the target address, see
145
147
  # {Address#initialize} for valid formats
146
148
  # @param configuration [Configuration] the {Configuration} to be used for
147
149
  # this instance
@@ -149,7 +151,7 @@ class TCPClient
149
151
  # @param exception [Class<Exception>] exception class to be used when the
150
152
  # connect timeout reached
151
153
  #
152
- # @return [self]
154
+ # @return [TCPClient] itself
153
155
  #
154
156
  # @raise {NoOpenSSLError} if SSL should be used but OpenSSL is not avail
155
157
  #
@@ -157,17 +159,17 @@ class TCPClient
157
159
  #
158
160
  def connect(address, configuration = nil, timeout: nil, exception: nil)
159
161
  close if @socket
160
- @address = Address.new(address)
161
162
  @configuration = (configuration || Configuration.default).dup
162
163
  raise(NoOpenSSLError) if @configuration.ssl? && !defined?(SSLSocket)
164
+ @address = stem_errors { Address.new(address) }
163
165
  @socket = create_socket(timeout, exception)
164
166
  self
165
167
  end
166
168
 
167
169
  #
168
- # Flushes all internal buffers (write all through).
170
+ # Flushes all internal buffers (write all buffered data).
169
171
  #
170
- # @return [self]
172
+ # @return [TCPClient] itself
171
173
  #
172
174
  def flush
173
175
  stem_errors { @socket&.flush }
@@ -201,6 +203,39 @@ class TCPClient
201
203
  end
202
204
  end
203
205
 
206
+ #
207
+ # Reads the next line from server.
208
+ #
209
+ # The standard record separator is used as `separator`.
210
+ #
211
+ # The optional `timeout` and `exception` parameters allow to override the
212
+ # `read_timeout` and `read_timeout_error` values of the used {#configuration}.
213
+ #
214
+ # @param separator [String] the line separator to be used
215
+ # @param timeout [Numeric] maximum time in seconds to read
216
+ # @param exception [Class<Exception>] exception class to be used when the
217
+ # read timeout reached
218
+ #
219
+ # @return [String] the read line
220
+ #
221
+ # @raise [NotConnectedError] if {#connect} was not called before
222
+ #
223
+ # @see NetworkError
224
+ #
225
+ def readline(separator = $/, chomp: false, timeout: nil, exception: nil)
226
+ raise(NotConnectedError) if closed?
227
+ deadline = create_deadline(timeout, configuration.read_timeout)
228
+ unless deadline.valid?
229
+ return stem_errors { @socket.readline(separator, chomp: chomp) }
230
+ end
231
+ exception ||= configuration.read_timeout_error
232
+ line =
233
+ stem_errors(exception) do
234
+ @socket.readto_with_deadline(separator, deadline, exception)
235
+ end
236
+ chomp ? line.chomp : line
237
+ end
238
+
204
239
  #
205
240
  # @return [String] the currently used address as text.
206
241
  #
@@ -229,7 +264,7 @@ class TCPClient
229
264
  #
230
265
  # @yieldparam client [TCPClient] self
231
266
  #
232
- # @return [Object] the block`s result
267
+ # @return [Object] the block's result
233
268
  #
234
269
  # @raise [NoBlockGivenError] if the block is missing
235
270
  #
@@ -259,6 +294,8 @@ class TCPClient
259
294
  #
260
295
  # @raise [NotConnectedError] if {#connect} was not called before
261
296
  #
297
+ # @see NetworkError
298
+ #
262
299
  def write(*messages, timeout: nil, exception: nil)
263
300
  raise(NotConnectedError) if closed?
264
301
  deadline = create_deadline(timeout, configuration.write_timeout)
data/rakefile.rb CHANGED
@@ -6,7 +6,13 @@ require 'rspec/core/rake_task'
6
6
  require 'yard'
7
7
 
8
8
  $stdout.sync = $stderr.sync = true
9
- CLOBBER << 'prj' << 'doc' << '.yardoc'
9
+
10
+ CLEAN << 'prj' << 'doc'
11
+
12
+ CLOBBER << '.yardoc'
13
+
10
14
  task(:default) { exec('rake --tasks') }
15
+
11
16
  RSpec::Core::RakeTask.new { |task| task.ruby_opts = %w[-w] }
17
+
12
18
  YARD::Rake::YardocTask.new { |task| task.stats_options = %w[--list-undoc] }
data/sample/google_ssl.rb CHANGED
@@ -18,8 +18,11 @@ cfg =
18
18
  # - limit all network interactions to 1.5 seconds
19
19
  # - use the Configuration cfg
20
20
  # - send a simple HTTP get request
21
- # - read 12 byte: "HTTP/1.1 " + 3 byte HTTP status code
22
- TCPClient.with_deadline(1.5, 'www.google.com:443', cfg) do |client|
23
- p client.write("GET / HTTP/1.1\r\nHost: www.google.com\r\n\r\n")
24
- p client.read(12)
25
- end
21
+ # - read the returned message and headers
22
+ response =
23
+ TCPClient.with_deadline(1.5, 'www.google.com:443', cfg) do |client|
24
+ client.write("GET / HTTP/1.1\r\nHost: www.google.com\r\n\r\n") #=> 40
25
+ client.readline("\r\n\r\n") #=> see response
26
+ end
27
+
28
+ puts(response)
@@ -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)
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.4
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-16 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
@@ -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.2.32
132
+ rubygems_version: 3.3.3
133
133
  signing_key:
134
134
  specification_version: 4
135
135
  summary: A TCP client implementation with working timeout support.