tcp-client 0.1.3 → 0.2.3

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: bc62bca2078a00db24dc861aa876a6e2acac79dd1f2eb0cafbd29840d5169704
4
- data.tar.gz: c784e00a152bcf7ac5c1246517483a39f55beb3f7591881be58a4edeb847097e
3
+ metadata.gz: 2791642003fb16623a27988401ff3bb532194032a5bbd4f62b853aae94790055
4
+ data.tar.gz: 030a2b3ebecf1334dea84e2c8f18aecd3b900c7c467c10c624c1354429a66195
5
5
  SHA512:
6
- metadata.gz: 77ff9adf39b16aab0ca83cd77c01f688a77efd675c287095e5b67c1b52f89cf0528ddbc0a1c206330703cf430c4f457ddbdcb369f5a0a8f57a6c143ee5266f7e
7
- data.tar.gz: 27df4b59116a68cf8232f471214e305d74f650b4e1be6cbd59302660f8d2998beef1fd7c382079311f9139c3465bda20981f3ff0993659f74807c00bb3942959
6
+ metadata.gz: e4ef14d49cd323a418190e62bc190af01fcc9be1b7008b7fd27c8fa7b8779ae29947045a660080e9403ba1d932e1318a6f98c819006d816f91eb093caf6d7873
7
+ data.tar.gz: 29617e860ad0b419638b54a8258541ccf3c668530a38aa2e629badcb373b94df71c33e538d54faf12d732ffd51670e1ad57cc8fabcaac11bb2d4de3d28cd29b1
data/README.md CHANGED
@@ -12,22 +12,18 @@ require 'tcp-client'
12
12
 
13
13
  TCPClient.configure do |cfg|
14
14
  cfg.connect_timeout = 1 # second to connect the server
15
- cfg.write_timeout = 0.25 # seconds to write a single data junk
16
- cfg.read_timeout = 0.5 # seconds to read some bytes
17
15
  cfg.ssl_params = { ssl_version: :TLSv1_2 } # use TLS 1.2
18
16
  end
19
17
 
20
- # the following request sequence is not allowed to last longer than 2 seconds:
21
- # 1 second to connect (incl. SSL handshake etc.)
22
- # + 0.25 seconds to write data
23
- # + 0.5 seconds to read a response
24
-
25
18
  TCPClient.open('www.google.com:443') do |client|
26
- # simple HTTP get request
27
- pp client.write("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n")
28
-
29
- # read "HTTP/1.1 " + 3 byte HTTP status code
30
- pp client.read(12)
19
+ # query should not last longer than 0.5 seconds
20
+ client.with_deadline(0.5) do
21
+ # simple HTTP get request
22
+ pp client.write("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n")
23
+
24
+ # read "HTTP/1.1 " + 3 byte HTTP status code
25
+ pp client.read(12)
26
+ end
31
27
  end
32
28
  ```
33
29
 
data/lib/tcp-client.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'tcp-client/errors'
3
4
  require_relative 'tcp-client/address'
4
5
  require_relative 'tcp-client/tcp_socket'
5
6
  require_relative 'tcp-client/ssl_socket'
@@ -8,30 +9,9 @@ require_relative 'tcp-client/default_configuration'
8
9
  require_relative 'tcp-client/version'
9
10
 
10
11
  class TCPClient
11
- class NoOpenSSL < RuntimeError
12
- def self.raise!
13
- raise(self, 'OpenSSL is not avail', caller(1))
14
- end
15
- end
16
-
17
- class NotConnected < SocketError
18
- def self.raise!(reason)
19
- raise(self, "client not connected - #{reason}", caller(1))
20
- end
21
- end
22
-
23
- TimeoutError = Class.new(IOError)
24
- ConnectTimeoutError = Class.new(TimeoutError)
25
- ReadTimeoutError = Class.new(TimeoutError)
26
- WriteTimeoutError = Class.new(TimeoutError)
27
-
28
- Timeout = TimeoutError # backward compatibility
29
- deprecate_constant(:Timeout)
30
-
31
12
  def self.open(addr, configuration = Configuration.default)
32
- addr = Address.new(addr)
33
13
  client = new
34
- client.connect(addr, configuration)
14
+ client.connect(Address.new(addr), configuration)
35
15
  block_given? ? yield(client) : client
36
16
  ensure
37
17
  client&.close if block_given?
@@ -40,50 +20,88 @@ class TCPClient
40
20
  attr_reader :address
41
21
 
42
22
  def initialize
43
- @socket = @address = @write_timeout = @read_timeout = nil
23
+ @socket = @address = @deadline = @cfg = nil
44
24
  end
45
25
 
46
26
  def to_s
47
27
  @address ? @address.to_s : ''
48
28
  end
49
29
 
50
- def connect(addr, configuration)
30
+ def connect(addr, configuration, exception: nil)
51
31
  close
52
32
  NoOpenSSL.raise! if configuration.ssl? && !defined?(SSLSocket)
53
33
  @address = Address.new(addr)
54
- @socket = TCPSocket.new(@address, configuration, ConnectTimeoutError)
55
- configuration.ssl? &&
56
- @socket =
57
- SSLSocket.new(@socket, @address, configuration, ConnectTimeoutError)
58
- @write_timeout = configuration.write_timeout
59
- @read_timeout = configuration.read_timeout
34
+ @cfg = configuration.dup
35
+ exception ||= configuration.connect_timeout_error
36
+ @socket = TCPSocket.new(@address, @cfg, exception)
37
+ @cfg.ssl? &&
38
+ @socket = SSLSocket.new(@socket, @address, configuration, exception)
60
39
  self
61
40
  end
62
41
 
63
42
  def close
64
- socket, @socket = @socket, nil
65
- socket&.close
43
+ @socket&.close
66
44
  self
67
45
  rescue IOError
68
46
  self
47
+ ensure
48
+ @socket = @deadline = nil
69
49
  end
70
50
 
71
51
  def closed?
72
52
  @socket.nil? || @socket.closed?
73
53
  end
74
54
 
75
- def read(nbytes, timeout: @read_timeout)
76
- NotConnected.raise!(self) if closed?
77
- @socket.read(nbytes, timeout: timeout, exception: ReadTimeoutError)
55
+ def with_deadline(timeout)
56
+ previous_deadline = @deadline
57
+ NoBlockGiven.raise! unless block_given?
58
+ tm = timeout&.to_f
59
+ InvalidDeadLine.raise! unless tm&.positive?
60
+ @deadline = Time.now + tm
61
+ yield(self)
62
+ ensure
63
+ @deadline = previous_deadline
64
+ end
65
+
66
+ def read(nbytes, timeout: nil, exception: nil)
67
+ NotConnected.raise! if closed?
68
+ timeout.nil? && @deadline and
69
+ return read_with_deadline(nbytes, @deadline, exception)
70
+ timeout = (timeout || @cfg.read_timeout).to_f
71
+ return @socket.read(nbytes) unless timeout.positive?
72
+ read_with_deadline(nbytes, Time.now + timeout, exception)
78
73
  end
79
74
 
80
- def write(*msg, timeout: @write_timeout)
81
- NotConnected.raise!(self) if closed?
82
- @socket.write(*msg, timeout: timeout, exception: WriteTimeoutError)
75
+ def write(*msg, timeout: nil, exception: nil)
76
+ NotConnected.raise! if closed?
77
+ timeout.nil? && @deadline and
78
+ return write_with_deadline(msg, @deadline, exception)
79
+ timeout = (timeout || @cfg.write_timeout).to_f
80
+ return @socket.write(*msg) unless timeout.positive?
81
+ write_with_deadline(msg, Time.now + timeout, exception)
83
82
  end
84
83
 
85
84
  def flush
86
85
  @socket.flush unless closed?
87
86
  self
88
87
  end
88
+
89
+ private
90
+
91
+ def read_with_deadline(nbytes, deadline, exception)
92
+ @socket.read_with_deadline(
93
+ nbytes,
94
+ deadline,
95
+ exception || @cfg.read_timeout_error
96
+ )
97
+ end
98
+
99
+ def write_with_deadline(msg, deadline, exception)
100
+ exception ||= @cfg.write_timeout_error
101
+ result = 0
102
+ msg.each do |chunk|
103
+ result += @socket.write_with_deadline(chunk.b, deadline, exception)
104
+ end
105
+ result
106
+ end
89
107
  end
@@ -26,7 +26,7 @@ class TCPClient
26
26
  end
27
27
 
28
28
  def to_h
29
- {host: @hostname, port: @addrinfo.ip_port}
29
+ { host: @hostname, port: @addrinfo.ip_port }
30
30
  end
31
31
 
32
32
  def ==(other)
@@ -1,3 +1,5 @@
1
+ require_relative 'errors'
2
+
1
3
  class TCPClient
2
4
  class Configuration
3
5
  def self.create(options = {})
@@ -6,15 +8,38 @@ class TCPClient
6
8
  ret
7
9
  end
8
10
 
9
- attr_reader :buffered, :keep_alive, :reverse_lookup, :timeout
11
+ attr_reader :buffered,
12
+ :keep_alive,
13
+ :reverse_lookup,
14
+ :timeout,
15
+ :connect_timeout,
16
+ :read_timeout,
17
+ :write_timeout,
18
+ :connect_timeout_error,
19
+ :read_timeout_error,
20
+ :write_timeout_error
10
21
  attr_accessor :ssl_params
11
22
 
12
23
  def initialize(options = {})
13
24
  @buffered = @keep_alive = @reverse_lookup = true
14
25
  self.timeout = @ssl_params = nil
26
+ @connect_timeout_error = ConnectTimeoutError
27
+ @read_timeout_error = ReadTimeoutError
28
+ @write_timeout_error = WriteTimeoutError
15
29
  options.each_pair { |attribute, value| set(attribute, value) }
16
30
  end
17
31
 
32
+ def freeze
33
+ @ssl_params.freeze
34
+ super
35
+ end
36
+
37
+ def initialize_copy(_org)
38
+ super
39
+ @ssl_params = @ssl_params.dup
40
+ self
41
+ end
42
+
18
43
  def ssl?
19
44
  @ssl_params ? true : false
20
45
  end
@@ -38,32 +63,41 @@ class TCPClient
38
63
  end
39
64
 
40
65
  def timeout=(seconds)
41
- @timeout = seconds(seconds)
42
- @connect_timeout = @write_timeout = @read_timeout = nil
43
- end
44
-
45
- def connect_timeout
46
- @connect_timeout || @timeout
66
+ @timeout =
67
+ @connect_timeout = @write_timeout = @read_timeout = seconds(seconds)
47
68
  end
48
69
 
49
70
  def connect_timeout=(seconds)
50
71
  @connect_timeout = seconds(seconds)
51
72
  end
52
73
 
53
- def write_timeout
54
- @write_timeout || @timeout
74
+ def read_timeout=(seconds)
75
+ @read_timeout = seconds(seconds)
55
76
  end
56
77
 
57
78
  def write_timeout=(seconds)
58
79
  @write_timeout = seconds(seconds)
59
80
  end
60
81
 
61
- def read_timeout
62
- @read_timeout || @timeout
82
+ def timeout_error=(exception)
83
+ NotAnException.raise!(exception) unless exception_class?(exception)
84
+ @connect_timeout_error =
85
+ @read_timeout_error = @write_timeout_error = exception
63
86
  end
64
87
 
65
- def read_timeout=(seconds)
66
- @read_timeout = seconds(seconds)
88
+ def connect_timeout_error=(exception)
89
+ NotAnException.raise!(exception) unless exception_class?(exception)
90
+ @connect_timeout_error = exception
91
+ end
92
+
93
+ def read_timeout_error=(exception)
94
+ NotAnException.raise!(exception) unless exception_class?(exception)
95
+ @read_timeout_error = exception
96
+ end
97
+
98
+ def write_timeout_error=(exception)
99
+ NotAnException.raise!(exception) unless exception_class?(exception)
100
+ @write_timeout_error = exception
67
101
  end
68
102
 
69
103
  def to_h
@@ -75,6 +109,9 @@ class TCPClient
75
109
  connect_timeout: @connect_timeout,
76
110
  read_timeout: @read_timeout,
77
111
  write_timeout: @write_timeout,
112
+ connect_timeout_error: @connect_timeout_error,
113
+ read_timeout_error: @read_timeout_error,
114
+ write_timeout_error: @write_timeout_error,
78
115
  ssl_params: @ssl_params
79
116
  }
80
117
  end
@@ -90,10 +127,14 @@ class TCPClient
90
127
 
91
128
  private
92
129
 
130
+ def exception_class?(value)
131
+ value.is_a?(Class) && value < Exception
132
+ end
133
+
93
134
  def set(attribute, value)
94
135
  public_send("#{attribute}=", value)
95
136
  rescue NoMethodError
96
- raise(ArgumentError, "unknown attribute - #{attribute}")
137
+ UnknownAttribute.raise!(attribute)
97
138
  end
98
139
 
99
140
  def seconds(value)
@@ -6,10 +6,8 @@ class TCPClient
6
6
  class << self
7
7
  attr_reader :default_configuration
8
8
 
9
- def configure(options = {})
10
- cfg = Configuration.new(options)
11
- yield(cfg) if block_given?
12
- @default_configuration = cfg
9
+ def configure(options = {}, &block)
10
+ @default_configuration = Configuration.create(options, &block)
13
11
  end
14
12
  end
15
13
 
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'socket'
4
+
5
+ class TCPClient
6
+ class NoOpenSSL < RuntimeError
7
+ def self.raise!
8
+ raise(self, 'OpenSSL is not avail', caller(1))
9
+ end
10
+ end
11
+
12
+ class NoBlockGiven < RuntimeError
13
+ def self.raise!
14
+ raise(self, 'no block given', caller(1))
15
+ end
16
+ end
17
+
18
+ class InvalidDeadLine < ArgumentError
19
+ def self.raise!(timeout)
20
+ raise(self, "invalid deadline - #{timeout}", caller(1))
21
+ end
22
+ end
23
+
24
+ class UnknownAttribute < ArgumentError
25
+ def self.raise!(attribute)
26
+ raise(self, "unknown attribute - #{attribute}", caller(1))
27
+ end
28
+ end
29
+
30
+ class NotAnException < TypeError
31
+ def self.raise!(object)
32
+ raise(self, "not a valid exception class - #{object.inspect}", caller(1))
33
+ end
34
+ end
35
+
36
+ class NotConnected < SocketError
37
+ def self.raise!
38
+ raise(self, 'client not connected', caller(1))
39
+ end
40
+ end
41
+
42
+ TimeoutError = Class.new(IOError)
43
+ ConnectTimeoutError = Class.new(TimeoutError)
44
+ ReadTimeoutError = Class.new(TimeoutError)
45
+ WriteTimeoutError = Class.new(TimeoutError)
46
+
47
+ Timeout = TimeoutError # backward compatibility
48
+ deprecate_constant(:Timeout)
49
+ end
@@ -1,3 +1,7 @@
1
+ # I keep this file for backward compatibility.
2
+ # This may be removed in one of the next versions.
3
+ # Let me know if you like to use it elsewhere...
4
+
1
5
  IOTimeoutError = Class.new(IOError) unless defined?(IOTimeoutError)
2
6
 
3
7
  module IOTimeoutMixin
@@ -10,6 +14,24 @@ module IOTimeoutMixin
10
14
  end
11
15
  end
12
16
 
17
+ def read_with_deadline(nbytes, deadline, exception)
18
+ result = ''.b
19
+ return result if nbytes.zero?
20
+ loop do
21
+ junk_size = nbytes - result.bytesize
22
+ read =
23
+ with_deadline(deadline, exception) do
24
+ read_nonblock(junk_size, exception: false)
25
+ end
26
+ unless read
27
+ close
28
+ return result
29
+ end
30
+ result += read
31
+ return result if result.bytesize >= nbytes
32
+ end
33
+ end
34
+
13
35
  def read(nbytes, timeout: nil, exception: IOTimeoutError)
14
36
  timeout = timeout.to_f
15
37
  return read_all(nbytes) { |junk_size| super(junk_size) } if timeout <= 0
@@ -35,8 +57,8 @@ module IOTimeoutMixin
35
57
  private
36
58
 
37
59
  def read_all(nbytes)
38
- return '' if nbytes.zero?
39
- result = ''
60
+ return ''.b if nbytes.zero?
61
+ result = ''.b
40
62
  loop do
41
63
  unless read = yield(nbytes - result.bytesize)
42
64
  close
@@ -0,0 +1,83 @@
1
+ module IOWithDeadlineMixin
2
+ def self.included(mod)
3
+ im = mod.instance_methods
4
+ if im.index(:wait_writable) && im.index(:wait_readable)
5
+ mod.include(ViaWaitMethod)
6
+ else
7
+ mod.include(ViaSelect)
8
+ end
9
+ end
10
+
11
+ def read_with_deadline(nbytes, deadline, exclass)
12
+ raise(exclass) if Time.now > deadline
13
+ result = ''.b
14
+ return result if nbytes.zero?
15
+ loop do
16
+ read =
17
+ with_deadline(deadline, exclass) do
18
+ read_nonblock(nbytes - result.bytesize, exception: false)
19
+ end
20
+ unless read
21
+ close
22
+ return result
23
+ end
24
+ result += read
25
+ return result if result.bytesize >= nbytes
26
+ end
27
+ end
28
+
29
+ def write_with_deadline(data, deadline, exclass)
30
+ raise(exclass) if Time.now > deadline
31
+ return 0 if (size = data.bytesize).zero?
32
+ result = 0
33
+ loop do
34
+ written =
35
+ with_deadline(deadline, exclass) do
36
+ write_nonblock(data, exception: false)
37
+ end
38
+ result += written
39
+ return result if result >= size
40
+ data = data.byteslice(written, data.bytesize - written)
41
+ end
42
+ end
43
+
44
+ module ViaWaitMethod
45
+ private def with_deadline(deadline, exclass)
46
+ loop do
47
+ case ret = yield
48
+ when :wait_writable
49
+ raise(exclass) if (remaining_time = deadline - Time.now) <= 0
50
+ raise(exclass) if wait_writable(remaining_time).nil?
51
+ when :wait_readable
52
+ raise(exclass) if (remaining_time = deadline - Time.now) <= 0
53
+ raise(exclass) if wait_readable(remaining_time).nil?
54
+ else
55
+ return ret
56
+ end
57
+ end
58
+ rescue Errno::ETIMEDOUT
59
+ raise(exclass)
60
+ end
61
+ end
62
+
63
+ module ViaSelect
64
+ private def with_deadline(deadline, exclass)
65
+ loop do
66
+ case ret = yield
67
+ when :wait_writable
68
+ raise(exclass) if (remaining_time = deadline - Time.now) <= 0
69
+ raise(exclass) if ::IO.select(nil, [self], nil, remaining_time).nil?
70
+ when :wait_readable
71
+ raise(exclass) if (remaining_time = deadline - Time.now) <= 0
72
+ raise(exclass) if ::IO.select([self], nil, nil, remaining_time).nil?
73
+ else
74
+ return ret
75
+ end
76
+ end
77
+ rescue Errno::ETIMEDOUT
78
+ raise(exclass)
79
+ end
80
+ end
81
+
82
+ private_constant(:ViaWaitMethod, :ViaSelect)
83
+ end
@@ -4,11 +4,11 @@ rescue LoadError
4
4
  return
5
5
  end
6
6
 
7
- require_relative 'mixin/io_timeout'
7
+ require_relative 'mixin/io_with_deadline'
8
8
 
9
9
  class TCPClient
10
10
  class SSLSocket < ::OpenSSL::SSL::SSLSocket
11
- include IOTimeoutMixin
11
+ include IOWithDeadlineMixin
12
12
 
13
13
  def initialize(socket, address, configuration, exception)
14
14
  ssl_params = Hash[configuration.ssl_params]
@@ -31,12 +31,13 @@ class TCPClient
31
31
 
32
32
  def connect_to(address, check, timeout, exception)
33
33
  self.hostname = address.hostname
34
- if timeout
34
+ timeout = timeout.to_f
35
+ if timeout.zero?
36
+ connect
37
+ else
35
38
  with_deadline(Time.now + timeout, exception) do
36
39
  connect_nonblock(exception: false)
37
40
  end
38
- else
39
- connect
40
41
  end
41
42
  post_connection_check(address.hostname) if check
42
43
  end
@@ -1,9 +1,9 @@
1
1
  require 'socket'
2
- require_relative 'mixin/io_timeout'
2
+ require_relative 'mixin/io_with_deadline'
3
3
 
4
4
  class TCPClient
5
5
  class TCPSocket < ::Socket
6
- include IOTimeoutMixin
6
+ include IOWithDeadlineMixin
7
7
 
8
8
  def initialize(address, configuration, exception)
9
9
  super(address.addrinfo.ipv6? ? :INET6 : :INET, :STREAM)
@@ -19,7 +19,8 @@ class TCPClient
19
19
  address.addrinfo.ip_port,
20
20
  address.addrinfo.ip_address
21
21
  )
22
- return connect(addr) unless timeout
22
+ timeout = timeout.to_f
23
+ return connect(addr) if timeout.zero?
23
24
  with_deadline(Time.now + timeout, exception) do
24
25
  connect_nonblock(addr, exception: false)
25
26
  end
@@ -1,3 +1,3 @@
1
1
  class TCPClient
2
- VERSION = '0.1.3'.freeze
2
+ VERSION = '0.2.3'.freeze
3
3
  end
data/rakefile.rb CHANGED
@@ -6,12 +6,12 @@ require 'bundler/gem_tasks'
6
6
 
7
7
  $stdout.sync = $stderr.sync = true
8
8
 
9
- task(:default) { exec('rake --tasks') }
10
-
11
9
  CLOBBER << 'prj'
12
10
 
13
- Rake::TestTask.new(:test) do |t|
14
- t.ruby_opts = %w[-w]
15
- t.verbose = true
16
- t.test_files = FileList['test/**/*_test.rb']
11
+ task(:default) { exec('rake --tasks') }
12
+
13
+ Rake::TestTask.new(:test) do |task|
14
+ task.test_files = FileList['test/**/*_test.rb']
15
+ task.ruby_opts = %w[-w]
16
+ task.verbose = true
17
17
  end
data/sample/google_ssl.rb CHANGED
@@ -2,20 +2,16 @@ require_relative '../lib/tcp-client'
2
2
 
3
3
  TCPClient.configure do |cfg|
4
4
  cfg.connect_timeout = 1 # second to connect the server
5
- cfg.write_timeout = 0.25 # seconds to write a single data junk
6
- cfg.read_timeout = 0.5 # seconds to read some bytes
7
5
  cfg.ssl_params = { ssl_version: :TLSv1_2 } # use TLS 1.2
8
6
  end
9
7
 
10
- # the following request sequence is not allowed to last longer than 2 seconds:
11
- # 1 second to connect (incl. SSL handshake etc.)
12
- # + 0.25 seconds to write data
13
- # + 0.5 seconds to read a response
14
-
15
8
  TCPClient.open('www.google.com:443') do |client|
16
- # simple HTTP get request
17
- pp client.write("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n")
9
+ # query should not last longer than 0.5 seconds
10
+ client.with_deadline(0.5) do
11
+ # simple HTTP get request
12
+ pp client.write("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n")
18
13
 
19
- # read "HTTP/1.1 " + 3 byte HTTP status code
20
- pp client.read(12)
14
+ # read "HTTP/1.1 " + 3 byte HTTP status code
15
+ pp client.read(12)
16
+ end
21
17
  end
@@ -13,7 +13,7 @@ class AddressTest < MiniTest::Test
13
13
  end
14
14
 
15
15
  def test_create_from_addrinfo
16
- addrinfo = Addrinfo.tcp('google.com', 42)
16
+ addrinfo = Addrinfo.tcp('localhost', 42)
17
17
  subject = TCPClient::Address.new(addrinfo)
18
18
  assert_equal(addrinfo.getnameinfo[0], subject.hostname)
19
19
  assert_equal(addrinfo, subject.addrinfo)
@@ -62,5 +62,4 @@ class AddressTest < MiniTest::Test
62
62
  assert(a == b)
63
63
  assert(a === b)
64
64
  end
65
-
66
65
  end
@@ -39,7 +39,8 @@ class ConfigurationTest < MiniTest::Test
39
39
  connect_timeout: 1,
40
40
  read_timeout: 2,
41
41
  write_timeout: 3,
42
- ssl: true
42
+ ssl: true,
43
+ connect_timeout_error: IOError
43
44
  )
44
45
  refute(subject.buffered)
45
46
  refute(subject.keep_alive)
@@ -48,6 +49,8 @@ class ConfigurationTest < MiniTest::Test
48
49
  assert_same(2, subject.read_timeout)
49
50
  assert_same(3, subject.write_timeout)
50
51
  assert(subject.ssl?)
52
+ assert_same(IOError, subject.connect_timeout_error)
53
+ assert_same(TCPClient::ReadTimeoutError, subject.read_timeout_error)
51
54
  end
52
55
 
53
56
  def test_invalid_option
@@ -88,6 +91,18 @@ class ConfigurationTest < MiniTest::Test
88
91
  assert_same(42, subject.write_timeout)
89
92
  end
90
93
 
94
+ def test_timeout_error_overwrite
95
+ subject = TCPClient::Configuration.new
96
+ assert_same(TCPClient::ConnectTimeoutError, subject.connect_timeout_error)
97
+ assert_same(TCPClient::ReadTimeoutError, subject.read_timeout_error)
98
+ assert_same(TCPClient::WriteTimeoutError, subject.write_timeout_error)
99
+
100
+ subject.timeout_error = IOError
101
+ assert_same(IOError, subject.connect_timeout_error)
102
+ assert_same(IOError, subject.read_timeout_error)
103
+ assert_same(IOError, subject.write_timeout_error)
104
+ end
105
+
91
106
  def test_compare
92
107
  a = TCPClient::Configuration.new
93
108
  b = TCPClient::Configuration.new
@@ -95,4 +110,32 @@ class ConfigurationTest < MiniTest::Test
95
110
  assert(a == b)
96
111
  assert(a === b)
97
112
  end
113
+
114
+ def test_dup
115
+ source =
116
+ TCPClient::Configuration.new(
117
+ buffered: false,
118
+ keep_alive: false,
119
+ reverse_lookup: false,
120
+ connect_timeout: 1,
121
+ read_timeout: 2,
122
+ write_timeout: 3,
123
+ ssl: {
124
+ ssl_version: :TLSv1_2
125
+ }
126
+ )
127
+ shadow = source.dup.freeze
128
+
129
+ # some changes
130
+ source.buffered = true
131
+ source.write_timeout = 5
132
+ source.ssl_params[:err] = true
133
+ source.timeout_error = IOError
134
+
135
+ refute_equal(source.__id__, shadow.__id__)
136
+ refute(shadow.buffered)
137
+ assert_equal(3, shadow.write_timeout)
138
+ assert_equal({ ssl_version: :TLSv1_2 }, shadow.ssl_params)
139
+ assert_same(TCPClient::ReadTimeoutError, shadow.read_timeout_error)
140
+ end
98
141
  end
@@ -3,6 +3,8 @@ require_relative 'test_helper'
3
3
  class TCPClientTest < MiniTest::Test
4
4
  parallelize_me!
5
5
 
6
+ HUGE_AMOUNT_OF_DATA = Array.new(2024, '?' * 1024).freeze
7
+
6
8
  attr_reader :config
7
9
 
8
10
  def setup
@@ -65,7 +67,7 @@ class TCPClientTest < MiniTest::Test
65
67
  start_time = Time.now
66
68
  subject.read(42, timeout: timeout)
67
69
  end
68
- assert_in_delta(timeout, Time.now - start_time, 0.02)
70
+ assert_in_delta(timeout, Time.now - start_time, 0.15)
69
71
  end
70
72
  end
71
73
 
@@ -81,27 +83,63 @@ class TCPClientTest < MiniTest::Test
81
83
  start_time = nil
82
84
  assert_raises(TCPClient::WriteTimeoutError) do
83
85
  start_time = Time.now
84
-
85
- # send 1MB to avoid any TCP stack buffering
86
- args = Array.new(2024, '?' * 1024)
87
- subject.write(*args, timeout: timeout)
86
+ subject.write(*HUGE_AMOUNT_OF_DATA, timeout: timeout)
88
87
  end
89
- assert_in_delta(timeout, Time.now - start_time, 0.02)
88
+ assert_in_delta(timeout, Time.now - start_time, 0.15)
90
89
  end
91
90
  end
92
91
 
93
92
  def test_write_timeout
94
- check_write_timeout(0.1)
93
+ check_write_timeout(0.01)
95
94
  check_write_timeout(0.25)
96
95
  end
97
96
 
97
+ def test_write_deadline
98
+ TCPClient.open('localhost:1234', config) do |subject|
99
+ refute(subject.closed?)
100
+ assert_raises(TCPClient::WriteTimeoutError) do
101
+ subject.with_deadline(0.25) do |*args|
102
+ assert_equal([subject], args)
103
+ loop { subject.write('some data here') }
104
+ end
105
+ end
106
+ end
107
+ end
108
+
109
+ def test_read_deadline
110
+ TCPClient.open('localhost:1234', config) do |subject|
111
+ refute(subject.closed?)
112
+ assert_raises(TCPClient::ReadTimeoutError) do
113
+ subject.with_deadline(0.25) do |*args|
114
+ assert_equal([subject], args)
115
+ loop { subject.read(0) }
116
+ end
117
+ end
118
+ end
119
+ end
120
+
121
+ def xtest_read_write_deadline
122
+ TCPClient.open('localhost:1234', config) do |subject|
123
+ refute(subject.closed?)
124
+ assert_raises(TCPClient::TimeoutError) do
125
+ subject.with_deadline(0.25) do |*args|
126
+ assert_equal([subject], args)
127
+ loop do
128
+ subject.write('HUGE_AMOUNT_OF_DATA')
129
+ subject.read(0)
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
135
+
98
136
  def check_connect_timeout(ssl_config)
99
137
  start_time = nil
100
138
  assert_raises(TCPClient::ConnectTimeoutError) do
101
139
  start_time = Time.now
102
140
  TCPClient.new.connect('localhost:1234', ssl_config)
103
141
  end
104
- assert_in_delta(ssl_config.connect_timeout, Time.now - start_time, 0.02)
142
+ assert_in_delta(ssl_config.connect_timeout, Time.now - start_time, 0.11)
105
143
  end
106
144
 
107
145
  def test_connect_ssl_timeout
data/test/test_helper.rb CHANGED
@@ -4,8 +4,6 @@ require 'minitest/autorun'
4
4
  require 'minitest/parallel'
5
5
  require_relative '../lib/tcp-client'
6
6
 
7
- $stdout.sync = $stderr.sync = true
8
-
9
7
  # this pseudo-server never reads or writes anything
10
- DummyServer = TCPServer.new('localhost', 1234)
11
- Minitest.after_run { DummyServer.close }
8
+ PseudoServer = TCPServer.new('localhost', 1234)
9
+ Minitest.after_run { PseudoServer.close }
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.1.3
4
+ version: 0.2.3
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-02-12 00:00:00.000000000 Z
11
+ date: 2021-05-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -71,7 +71,9 @@ files:
71
71
  - lib/tcp-client/address.rb
72
72
  - lib/tcp-client/configuration.rb
73
73
  - lib/tcp-client/default_configuration.rb
74
+ - lib/tcp-client/errors.rb
74
75
  - lib/tcp-client/mixin/io_timeout.rb
76
+ - lib/tcp-client/mixin/io_with_deadline.rb
75
77
  - lib/tcp-client/ssl_socket.rb
76
78
  - lib/tcp-client/tcp_socket.rb
77
79
  - lib/tcp-client/version.rb
@@ -106,7 +108,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
106
108
  - !ruby/object:Gem::Version
107
109
  version: '0'
108
110
  requirements: []
109
- rubygems_version: 3.2.9
111
+ rubygems_version: 3.2.15
110
112
  signing_key:
111
113
  specification_version: 4
112
114
  summary: A TCP client implementation with working timeout support.