tcp-client 0.1.0 → 0.2.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: 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: