tcp-client 0.1.0 → 0.2.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: bb5546419b47d6680b576f39d8c4b382c39d74f07ca43f6e782160122b6ba524
4
- data.tar.gz: 3e563cd2db3efc765e491ece2fc37a784cb55ef05f6390eb23d1322a926be49e
3
+ metadata.gz: 4738c2c07c1163d9a991fa08978d148ebdee182193a90103ecba277cd783f4b3
4
+ data.tar.gz: a69ecf05054b5260ab9a324226d5d0a592e337a09a68865c4702e37f4a948a4a
5
5
  SHA512:
6
- metadata.gz: 712f24a4dc3427cf37126681ba50d53f0aefc9f839920283f611ef22e923f65f53f09fc83ca66cb45930536ca3c13d630dc7b0c5a5f40c6fc6f91f82bf123c3c
7
- data.tar.gz: e97e52fc2aa1ad3cc548d2f561df97678e00c0e7e617c08afdbb9c6537709fc2743a07f26679cc8007124348cb32f1e8481d6417b7d3e28d247b504fcc951805
6
+ metadata.gz: 8c348cb8696ab9fec32321499b4fa62f169ce5b62e4d703df009a7762cd40e9c6e9cbe22c52affce4a493f4d473907f6b051a74e2a46e9de9ff5fc13d24bdd24
7
+ data.tar.gz: 40d45b9e44f4f7d6fd8e9514ae0e507c39e7177df8cca986b13f461753d00de579bae04578db49a0b5a67b86e6d9527e35cc8db87eb5ff4646b972c9ac4a6b90
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,46 +20,73 @@ class TCPClient
40
20
  attr_reader :address
41
21
 
42
22
  def initialize
43
- @socket = @address = @write_timeout = @read_timeout = nil
23
+ @socket = @address = @write_timeout = @read_timeout = @deadline = 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: ConnectTimeoutError)
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)
34
+ @socket = TCPSocket.new(@address, configuration, exception)
55
35
  configuration.ssl? &&
56
- @socket =
57
- SSLSocket.new(@socket, @address, configuration, ConnectTimeoutError)
36
+ @socket = SSLSocket.new(@socket, @address, configuration, exception)
58
37
  @write_timeout = configuration.write_timeout
59
38
  @read_timeout = configuration.read_timeout
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
+ NoBlockGiven.raise! unless block_given?
57
+ previous_deadline = @deadline
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: ReadTimeoutError)
67
+ NotConnected.raise! if closed?
68
+ if timeout.nil? && @deadline
69
+ return @socket.read_with_deadline(nbytes, @deadline, exception)
70
+ end
71
+ timeout = (timeout || @read_timeout).to_f
72
+ if timeout.positive?
73
+ @socket.read_with_deadline(nbytes, Time.now + timeout, exception)
74
+ else
75
+ @socket.read(nbytes)
76
+ end
78
77
  end
79
78
 
80
- def write(*msg, timeout: @write_timeout)
81
- NotConnected.raise!(self) if closed?
82
- @socket.write(*msg, timeout: timeout, exception: WriteTimeoutError)
79
+ def write(*msg, timeout: nil, exception: WriteTimeoutError)
80
+ NotConnected.raise! if closed?
81
+ if timeout.nil? && @deadline
82
+ return @socket.write_with_deadline(msg.join.b, @deadline, exception)
83
+ end
84
+ timeout = (timeout || @read_timeout).to_f
85
+ if timeout.positive?
86
+ @socket.write_with_deadline(msg.join.b, Time.now + timeout, exception)
87
+ else
88
+ @socket.write(*msg)
89
+ end
83
90
  end
84
91
 
85
92
  def flush
@@ -25,6 +25,19 @@ class TCPClient
25
25
  "#{@hostname}:#{@addrinfo.ip_port}"
26
26
  end
27
27
 
28
+ def to_h
29
+ { host: @hostname, port: @addrinfo.ip_port }
30
+ end
31
+
32
+ def ==(other)
33
+ to_h == other.to_h
34
+ end
35
+ alias eql? ==
36
+
37
+ def equal?(other)
38
+ self.class == other.class && self == other
39
+ end
40
+
28
41
  private
29
42
 
30
43
  def init_from_selfclass(address)
@@ -44,7 +57,7 @@ class TCPClient
44
57
  end
45
58
 
46
59
  def from_string(str)
47
- return nil, str.to_i unless idx = str.rindex(':')
60
+ idx = str.rindex(':') or return nil, str.to_i
48
61
  name = str[0, idx]
49
62
  name = name[1, name.size - 2] if name[0] == '[' && name[-1] == ']'
50
63
  [name, str[idx + 1, str.size - idx].to_i]
@@ -6,7 +6,7 @@ class TCPClient
6
6
  ret
7
7
  end
8
8
 
9
- attr_reader :buffered, :keep_alive, :reverse_lookup
9
+ attr_reader :buffered, :keep_alive, :reverse_lookup, :timeout
10
10
  attr_accessor :ssl_params
11
11
 
12
12
  def initialize(options = {})
@@ -19,22 +19,22 @@ class TCPClient
19
19
  @ssl_params ? true : false
20
20
  end
21
21
 
22
- def ssl=(yn)
23
- return @ssl_params = nil unless yn
24
- return @ssl_params = yn if Hash === yn
22
+ def ssl=(value)
23
+ return @ssl_params = nil unless value
24
+ return @ssl_params = value.dup if Hash === value
25
25
  @ssl_params ||= {}
26
26
  end
27
27
 
28
- def buffered=(yn)
29
- @buffered = yn ? true : false
28
+ def buffered=(value)
29
+ @buffered = value ? true : false
30
30
  end
31
31
 
32
- def keep_alive=(yn)
33
- @keep_alive = yn ? true : false
32
+ def keep_alive=(value)
33
+ @keep_alive = value ? true : false
34
34
  end
35
35
 
36
- def reverse_lookup=(yn)
37
- @reverse_lookup = yn ? true : false
36
+ def reverse_lookup=(value)
37
+ @reverse_lookup = value ? true : false
38
38
  end
39
39
 
40
40
  def timeout=(seconds)
@@ -66,6 +66,28 @@ class TCPClient
66
66
  @read_timeout = seconds(seconds)
67
67
  end
68
68
 
69
+ def to_h
70
+ {
71
+ buffered: @buffered,
72
+ keep_alive: @keep_alive,
73
+ reverse_lookup: @reverse_lookup,
74
+ timeout: @timeout,
75
+ connect_timeout: @connect_timeout,
76
+ read_timeout: @read_timeout,
77
+ write_timeout: @write_timeout,
78
+ ssl_params: @ssl_params
79
+ }
80
+ end
81
+
82
+ def ==(other)
83
+ to_h == other.to_h
84
+ end
85
+ alias eql? ==
86
+
87
+ def equal?(other)
88
+ self.class == other.class && self == other
89
+ end
90
+
69
91
  private
70
92
 
71
93
  def set(attribute, value)
@@ -75,7 +97,7 @@ class TCPClient
75
97
  end
76
98
 
77
99
  def seconds(value)
78
- value&.positive? ? value : nil
100
+ value&.to_f&.positive? ? value : nil
79
101
  end
80
102
  end
81
103
  end
@@ -0,0 +1,37 @@
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 NotConnected < SocketError
25
+ def self.raise!
26
+ raise(self, 'client not connected', caller(1))
27
+ end
28
+ end
29
+
30
+ TimeoutError = Class.new(IOError)
31
+ ConnectTimeoutError = Class.new(TimeoutError)
32
+ ReadTimeoutError = Class.new(TimeoutError)
33
+ WriteTimeoutError = Class.new(TimeoutError)
34
+
35
+ Timeout = TimeoutError # backward compatibility
36
+ deprecate_constant(:Timeout)
37
+ 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
@@ -59,9 +81,7 @@ module IOTimeoutMixin
59
81
  end
60
82
 
61
83
  module DeadlineMethods
62
- private
63
-
64
- def with_deadline(deadline, exclass)
84
+ private def with_deadline(deadline, exclass)
65
85
  loop do
66
86
  case ret = yield
67
87
  when :wait_writable
@@ -78,9 +98,7 @@ module IOTimeoutMixin
78
98
  end
79
99
 
80
100
  module DeadlineIO
81
- private
82
-
83
- def with_deadline(deadline, exclass)
101
+ private def with_deadline(deadline, exclass)
84
102
  loop do
85
103
  case ret = yield
86
104
  when :wait_writable
@@ -0,0 +1,79 @@
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
+ end
59
+ end
60
+
61
+ module ViaSelect
62
+ private def with_deadline(deadline, exclass)
63
+ loop do
64
+ case ret = yield
65
+ when :wait_writable
66
+ raise(exclass) if (remaining_time = deadline - Time.now) <= 0
67
+ raise(exclass) if ::IO.select(nil, [self], nil, remaining_time).nil?
68
+ when :wait_readable
69
+ raise(exclass) if (remaining_time = deadline - Time.now) <= 0
70
+ raise(exclass) if ::IO.select([self], nil, nil, remaining_time).nil?
71
+ else
72
+ return ret
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ private_constant(:ViaWaitMethod, :ViaSelect)
79
+ 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.0'.freeze
2
+ VERSION = '0.2.0'.freeze
3
3
  end
data/rakefile.rb CHANGED
@@ -4,7 +4,9 @@ require 'rake/clean'
4
4
  require 'rake/testtask'
5
5
  require 'bundler/gem_tasks'
6
6
 
7
- STDOUT.sync = STDERR.sync = true
7
+ $stdout.sync = $stderr.sync = true
8
+
9
+ task(:default) { exec('rake --tasks') }
8
10
 
9
11
  CLOBBER << 'prj'
10
12
 
@@ -13,7 +15,3 @@ Rake::TestTask.new(:test) do |t|
13
15
  t.verbose = true
14
16
  t.test_files = FileList['test/**/*_test.rb']
15
17
  end
16
-
17
- task :default do
18
- exec("#{$PROGRAM_NAME} --tasks")
19
- 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
data/tcp-client.gemspec CHANGED
@@ -5,6 +5,10 @@ require_relative './lib/tcp-client/version'
5
5
  GemSpec = Gem::Specification.new do |spec|
6
6
  spec.name = 'tcp-client'
7
7
  spec.version = TCPClient::VERSION
8
+ spec.author = 'Mike Blumtritt'
9
+
10
+ spec.required_ruby_version = '>= 2.7.0'
11
+
8
12
  spec.summary = 'A TCP client implementation with working timeout support.'
9
13
  spec.description = <<~DESCRIPTION
10
14
  This gem implements a TCP client with (optional) SSL support. The
@@ -13,27 +17,17 @@ GemSpec = Gem::Specification.new do |spec|
13
17
  other implementations this client respects given/configurable time
14
18
  limits for each method (`connect`, `read`, `write`).
15
19
  DESCRIPTION
16
- spec.author = 'Mike Blumtritt'
17
- spec.email = 'mike.blumtritt@pm.me'
18
20
  spec.homepage = 'https://github.com/mblumtritt/tcp-client'
19
- spec.metadata = {
20
- 'source_code_uri' => 'https://github.com/mblumtritt/tcp-client',
21
- 'bug_tracker_uri' => 'https://github.com/mblumtritt/tcp-client/issues'
22
- }
23
- spec.rubyforge_project = spec.name
21
+
22
+ spec.metadata['source_code_uri'] = 'https://github.com/mblumtritt/tcp-client'
23
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/mblumtritt/tcp-client/issues'
24
24
 
25
25
  spec.add_development_dependency 'bundler'
26
26
  spec.add_development_dependency 'minitest'
27
27
  spec.add_development_dependency 'rake'
28
28
 
29
- spec.platform = Gem::Platform::RUBY
30
- spec.required_ruby_version = '>= 2.5.0'
31
- spec.required_rubygems_version = Gem::Requirement.new('>= 1.3.6')
32
-
33
- spec.require_paths = %w[lib]
34
-
35
- all_files = %x(git ls-files -z).split(0.chr)
36
- spec.test_files = all_files.grep(%r{^(spec|test)/})
29
+ all_files = Dir.chdir(__dir__) { `git ls-files -z`.split(0.chr) }
30
+ spec.test_files = all_files.grep(%r{^test/})
37
31
  spec.files = all_files - spec.test_files
38
32
 
39
33
  spec.extra_rdoc_files = %w[README.md]
@@ -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)
@@ -54,4 +54,12 @@ class AddressTest < MiniTest::Test
54
54
  assert(subject.addrinfo.ip?)
55
55
  assert(subject.addrinfo.ipv6?)
56
56
  end
57
+
58
+ def test_compare
59
+ a = TCPClient::Address.new('localhost:42')
60
+ b = TCPClient::Address.new('localhost:42')
61
+ assert_equal(a, b)
62
+ assert(a == b)
63
+ assert(a === b)
64
+ end
57
65
  end
@@ -87,4 +87,12 @@ class ConfigurationTest < MiniTest::Test
87
87
  assert_same(42, subject.read_timeout)
88
88
  assert_same(42, subject.write_timeout)
89
89
  end
90
+
91
+ def test_compare
92
+ a = TCPClient::Configuration.new
93
+ b = TCPClient::Configuration.new
94
+ assert_equal(a, b)
95
+ assert(a == b)
96
+ assert(a === b)
97
+ end
90
98
  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
@@ -22,9 +24,9 @@ class TCPClientTest < MiniTest::Test
22
24
  def create_nonconnected_client
23
25
  client = TCPClient.new
24
26
  client.connect('', config)
27
+ client
25
28
  rescue Errno::EADDRNOTAVAIL
26
- ensure
27
- return client
29
+ client
28
30
  end
29
31
 
30
32
  def test_failed_state
@@ -41,7 +43,7 @@ class TCPClientTest < MiniTest::Test
41
43
  end
42
44
 
43
45
  def test_connected_state
44
- TCPClient.open('localhost:1234') do |subject|
46
+ TCPClient.open('localhost:1234', config) do |subject|
45
47
  refute(subject.closed?)
46
48
  assert_equal('localhost:1234', subject.to_s)
47
49
  refute_nil(subject.address)
@@ -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.11)
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.11)
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'minitest/autorun'
2
4
  require 'minitest/parallel'
3
5
  require_relative '../lib/tcp-client'
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.0
4
+ version: 0.2.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-02-11 00:00:00.000000000 Z
11
+ date: 2021-02-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -58,7 +58,7 @@ description: |
58
58
  easy to use client which can handle time limits correctly. Unlike
59
59
  other implementations this client respects given/configurable time
60
60
  limits for each method (`connect`, `read`, `write`).
61
- email: mike.blumtritt@pm.me
61
+ email:
62
62
  executables: []
63
63
  extensions: []
64
64
  extra_rdoc_files:
@@ -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
@@ -99,12 +101,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
99
101
  requirements:
100
102
  - - ">="
101
103
  - !ruby/object:Gem::Version
102
- version: 2.5.0
104
+ version: 2.7.0
103
105
  required_rubygems_version: !ruby/object:Gem::Requirement
104
106
  requirements:
105
107
  - - ">="
106
108
  - !ruby/object:Gem::Version
107
- version: 1.3.6
109
+ version: '0'
108
110
  requirements: []
109
111
  rubygems_version: 3.2.9
110
112
  signing_key: