tcp-client 0.9.4 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
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.