tcp-client 0.0.10 → 0.1.4

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: b04df2a87aaa1d7eea3322907e4f37c8ae228507474b6b59927617e12eb73d49
4
- data.tar.gz: 4454f0e6a53984627ffc3cb681a6b496f49306040d3a2bc0bfdabdf2405595a8
3
+ metadata.gz: a122629b36ac22b95c2ee7ed0d520391cfc00136982e9c0c4a8c7d0ea4dd5b47
4
+ data.tar.gz: a63612520461f6d84abffbe71c750158b2d7d0c5227d4ae9de16b9cac5071540
5
5
  SHA512:
6
- metadata.gz: 48e6bae6c20b9335593ca20d45f03884fd11db4fe2ff97b35766f17444f414f4cf15d5e187b02f059001e1bc819ed442cb4abd715de44bffbcac53f01dcacba5
7
- data.tar.gz: 912f1716f2bdeeb3e9bbdff18a5232c015d15c461f4aa4ba1919af2d7836f8f56e7916ee3800e7a26633dfee3c05257849a255669f9cb6eaf1a87c475f12a459
6
+ metadata.gz: 31d0760aa3f16564d171ba27dc0c3a919490356319265aa099762ef7f1e9ff57a20e968315d0d70beb291d62405040e01ec76aa4dfd78a229f3d91c0a7db029b
7
+ data.tar.gz: 265a8cda03d5c8f62138311f05a40756dd7e052c8b55d6e43412b3cf51dcea91e98c6ba38943bedf4b41de7af6b0d3dc86700197f7eafd4919093abd59d15351
data/README.md CHANGED
@@ -8,20 +8,22 @@ This gem implements a TCP client with (optional) SSL support. The motivation of
8
8
  ## Sample
9
9
 
10
10
  ```ruby
11
- configuration = TCPClient::Configuration.create do |cfg|
11
+ require 'tcp-client'
12
+
13
+ TCPClient.configure do |cfg|
12
14
  cfg.connect_timeout = 1 # second to connect the server
13
- cfg.write_timeout = 0.25 # seconds to write a single data junk
14
- cfg.read_timeout = 0.5 # seconds to read some bytes
15
- cfg.ssl_params = {ssl_version: :TLSv1_2} # use TLS 1.2
15
+ cfg.ssl_params = { ssl_version: :TLSv1_2 } # use TLS 1.2
16
16
  end
17
17
 
18
- # the following request sequence is not allowed to last longer than 2 seconds:
19
- # 1 second to connect (incl. SSL handshake etc.)
20
- # + 0.25 seconds to write data
21
- # + 0.5 seconds to read a response
22
- TCPClient.open('www.google.com:443', configuration) do |client|
23
- pp client.write("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n") # simple HTTP get request
24
- pp client.read(12) # "HTTP/1.1 " + 3 byte HTTP status code
18
+ TCPClient.open('www.google.com:443') do |client|
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
25
27
  end
26
28
  ```
27
29
 
data/gems.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- source('https://rubygems.org'){ gemspec }
3
+ source('https://rubygems.org') { gemspec }
data/lib/tcp-client.rb CHANGED
@@ -4,6 +4,7 @@ require_relative 'tcp-client/address'
4
4
  require_relative 'tcp-client/tcp_socket'
5
5
  require_relative 'tcp-client/ssl_socket'
6
6
  require_relative 'tcp-client/configuration'
7
+ require_relative 'tcp-client/default_configuration'
7
8
  require_relative 'tcp-client/version'
8
9
 
9
10
  class TCPClient
@@ -14,14 +15,20 @@ class TCPClient
14
15
  end
15
16
 
16
17
  class NotConnected < SocketError
17
- def self.raise!(which)
18
- raise(self, "client not connected - #{which}", caller(1))
18
+ def self.raise!(reason)
19
+ raise(self, "client not connected - #{reason}", caller(1))
19
20
  end
20
21
  end
21
22
 
22
- Timeout = Class.new(IOError)
23
+ TimeoutError = Class.new(IOError)
24
+ ConnectTimeoutError = Class.new(TimeoutError)
25
+ ReadTimeoutError = Class.new(TimeoutError)
26
+ WriteTimeoutError = Class.new(TimeoutError)
23
27
 
24
- def self.open(addr, configuration = Configuration.new)
28
+ Timeout = TimeoutError # backward compatibility
29
+ deprecate_constant(:Timeout)
30
+
31
+ def self.open(addr, configuration = Configuration.default)
25
32
  addr = Address.new(addr)
26
33
  client = new
27
34
  client.connect(addr, configuration)
@@ -34,20 +41,20 @@ class TCPClient
34
41
 
35
42
  def initialize
36
43
  @socket = @address = @write_timeout = @read_timeout = nil
44
+ @deadline = nil
37
45
  end
38
46
 
39
47
  def to_s
40
48
  @address ? @address.to_s : ''
41
49
  end
42
50
 
43
- def connect(addr, configuration)
51
+ def connect(addr, configuration, exception: ConnectTimeoutError)
44
52
  close
45
53
  NoOpenSSL.raise! if configuration.ssl? && !defined?(SSLSocket)
46
54
  @address = Address.new(addr)
47
- @socket = TCPSocket.new(@address, configuration, Timeout)
48
- configuration.ssl? && @socket = SSLSocket.new(
49
- @socket, @address, configuration, Timeout
50
- )
55
+ @socket = TCPSocket.new(@address, configuration, exception)
56
+ configuration.ssl? &&
57
+ @socket = SSLSocket.new(@socket, @address, configuration, exception)
51
58
  @write_timeout = configuration.write_timeout
52
59
  @read_timeout = configuration.read_timeout
53
60
  self
@@ -59,24 +66,47 @@ class TCPClient
59
66
  self
60
67
  rescue IOError
61
68
  self
69
+ ensure
70
+ @deadline = nil
62
71
  end
63
72
 
64
73
  def closed?
65
74
  @socket.nil? || @socket.closed?
66
75
  end
67
76
 
68
- def read(nbytes, timeout: @read_timeout)
77
+ def with_deadline(timeout)
78
+ raise('no block given') unless block_given?
79
+ raise('deadline already used') if @deadline
80
+ tm = timeout&.to_f
81
+ raise(ArgumentError, "invalid deadline - #{timeout}") unless tm.positive?
82
+ @deadline = Time.now + tm
83
+ yield(self)
84
+ ensure
85
+ @deadline = nil
86
+ end
87
+
88
+ def read(nbytes, timeout: nil, exception: ReadTimeoutError)
69
89
  NotConnected.raise!(self) if closed?
70
- @socket.read(nbytes, timeout: timeout, exception: Timeout)
90
+ time = timeout || remaining_time(exception) || @read_timeout
91
+ @socket.read(nbytes, timeout: time, exception: exception)
71
92
  end
72
93
 
73
- def write(*msg, timeout: @write_timeout)
94
+ def write(*msg, timeout: nil, exception: WriteTimeoutError)
74
95
  NotConnected.raise!(self) if closed?
75
- @socket.write(*msg, timeout: timeout, exception: Timeout)
96
+ time = timeout || remaining_time(exception) || @write_timeout
97
+ @socket.write(*msg, timeout: time, exception: exception)
76
98
  end
77
99
 
78
100
  def flush
79
101
  @socket.flush unless closed?
80
102
  self
81
103
  end
104
+
105
+ private
106
+
107
+ def remaining_time(exception)
108
+ return unless @deadline
109
+ remaining_time = @deadline - Time.now
110
+ 0 < remaining_time ? remaining_time : raise(exception)
111
+ end
82
112
  end
@@ -4,7 +4,7 @@ require 'socket'
4
4
 
5
5
  class TCPClient
6
6
  class Address
7
- attr_reader :to_s, :hostname, :addrinfo
7
+ attr_reader :hostname, :addrinfo
8
8
 
9
9
  def initialize(addr)
10
10
  case addr
@@ -20,17 +20,33 @@ class TCPClient
20
20
  @addrinfo.freeze
21
21
  end
22
22
 
23
+ def to_s
24
+ return "[#{@hostname}]:#{@addrinfo.ip_port}" if @hostname.index(':') # IP6
25
+ "#{@hostname}:#{@addrinfo.ip_port}"
26
+ end
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
+
23
41
  private
24
42
 
25
43
  def init_from_selfclass(address)
26
- @to_s = address.to_s
27
44
  @hostname = address.hostname
28
45
  @addrinfo = address.addrinfo
29
46
  end
30
47
 
31
48
  def init_from_addrinfo(addrinfo)
32
- @hostname, port = addrinfo.getnameinfo(Socket::NI_NUMERICSERV)
33
- @to_s = "#{@hostname}:#{port}"
49
+ @hostname, _port = addrinfo.getnameinfo(Socket::NI_NUMERICSERV)
34
50
  @addrinfo = addrinfo
35
51
  end
36
52
 
@@ -38,19 +54,13 @@ class TCPClient
38
54
  @hostname, port = from_string(str.to_s)
39
55
  return init_from_addrinfo(Addrinfo.tcp(nil, port)) unless @hostname
40
56
  @addrinfo = Addrinfo.tcp(@hostname, port)
41
- @to_s = as_str(@hostname, port)
42
57
  end
43
58
 
44
59
  def from_string(str)
45
- return [nil, str.to_i] unless idx = str.rindex(':')
60
+ idx = str.rindex(':') or return nil, str.to_i
46
61
  name = str[0, idx]
47
62
  name = name[1, name.size - 2] if name[0] == '[' && name[-1] == ']'
48
63
  [name, str[idx + 1, str.size - idx].to_i]
49
64
  end
50
-
51
- def as_str(hostname, port)
52
- return "[#{hostname}]:#{port}" if hostname.index(':') # IP6
53
- "#{hostname}:#{port}"
54
- end
55
65
  end
56
66
  end
@@ -1,39 +1,40 @@
1
1
  class TCPClient
2
2
  class Configuration
3
- def self.create
4
- ret = new
3
+ def self.create(options = {})
4
+ ret = new(options)
5
5
  yield(ret) if block_given?
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
- def initialize
12
+ def initialize(options = {})
13
13
  @buffered = @keep_alive = @reverse_lookup = true
14
14
  self.timeout = @ssl_params = nil
15
+ options.each_pair { |attribute, value| set(attribute, value) }
15
16
  end
16
17
 
17
18
  def ssl?
18
19
  @ssl_params ? true : false
19
20
  end
20
21
 
21
- def ssl=(yn)
22
- return @ssl_params = nil unless yn
23
- 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
24
25
  @ssl_params ||= {}
25
26
  end
26
27
 
27
- def buffered=(yn)
28
- @buffered = yn ? true : false
28
+ def buffered=(value)
29
+ @buffered = value ? true : false
29
30
  end
30
31
 
31
- def keep_alive=(yn)
32
- @keep_alive = yn ? true : false
32
+ def keep_alive=(value)
33
+ @keep_alive = value ? true : false
33
34
  end
34
35
 
35
- def reverse_lookup=(yn)
36
- @reverse_lookup = yn ? true : false
36
+ def reverse_lookup=(value)
37
+ @reverse_lookup = value ? true : false
37
38
  end
38
39
 
39
40
  def timeout=(seconds)
@@ -65,10 +66,38 @@ class TCPClient
65
66
  @read_timeout = seconds(seconds)
66
67
  end
67
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
+
68
91
  private
69
92
 
93
+ def set(attribute, value)
94
+ public_send("#{attribute}=", value)
95
+ rescue NoMethodError
96
+ raise(ArgumentError, "unknown attribute - #{attribute}")
97
+ end
98
+
70
99
  def seconds(value)
71
- value&.positive? ? value : nil
100
+ value&.to_f&.positive? ? value : nil
72
101
  end
73
102
  end
74
103
  end
@@ -0,0 +1,21 @@
1
+ require_relative 'configuration'
2
+
3
+ class TCPClient
4
+ @default_configuration = Configuration.new
5
+
6
+ class << self
7
+ attr_reader :default_configuration
8
+
9
+ def configure(options = {})
10
+ cfg = Configuration.new(options)
11
+ yield(cfg) if block_given?
12
+ @default_configuration = cfg
13
+ end
14
+ end
15
+
16
+ class Configuration
17
+ def self.default
18
+ TCPClient.default_configuration
19
+ end
20
+ end
21
+ end
@@ -12,7 +12,7 @@ module IOTimeoutMixin
12
12
 
13
13
  def read(nbytes, timeout: nil, exception: IOTimeoutError)
14
14
  timeout = timeout.to_f
15
- return read_all(nbytes){ |junk_size| super(junk_size) } if timeout <= 0
15
+ return read_all(nbytes) { |junk_size| super(junk_size) } if timeout <= 0
16
16
  deadline = Time.now + timeout
17
17
  read_all(nbytes) do |junk_size|
18
18
  with_deadline(deadline, exception) do
@@ -23,9 +23,9 @@ module IOTimeoutMixin
23
23
 
24
24
  def write(*msgs, timeout: nil, exception: IOTimeoutError)
25
25
  timeout = timeout.to_f
26
- return write_all(msgs.join){ |junk| super(junk) } if timeout <= 0
26
+ return write_all(msgs.join.b) { |junk| super(junk) } if timeout <= 0
27
27
  deadline = Time.now + timeout
28
- write_all(msgs.join) do |junk|
28
+ write_all(msgs.join.b) do |junk|
29
29
  with_deadline(deadline, exception) do
30
30
  write_nonblock(junk, exception: false)
31
31
  end
@@ -59,9 +59,7 @@ module IOTimeoutMixin
59
59
  end
60
60
 
61
61
  module DeadlineMethods
62
- private
63
-
64
- def with_deadline(deadline, exclass)
62
+ private def with_deadline(deadline, exclass)
65
63
  loop do
66
64
  case ret = yield
67
65
  when :wait_writable
@@ -78,9 +76,7 @@ module IOTimeoutMixin
78
76
  end
79
77
 
80
78
  module DeadlineIO
81
- private
82
-
83
- def with_deadline(deadline, exclass)
79
+ private def with_deadline(deadline, exclass)
84
80
  loop do
85
81
  case ret = yield
86
82
  when :wait_writable
@@ -14,9 +14,11 @@ class TCPClient
14
14
  private
15
15
 
16
16
  def connect_to(address, timeout, exception)
17
- addr = ::Socket.pack_sockaddr_in(
18
- address.addrinfo.ip_port, address.addrinfo.ip_address
19
- )
17
+ addr =
18
+ ::Socket.pack_sockaddr_in(
19
+ address.addrinfo.ip_port,
20
+ address.addrinfo.ip_address
21
+ )
20
22
  return connect(addr) unless timeout
21
23
  with_deadline(Time.now + timeout, exception) do
22
24
  connect_nonblock(addr, exception: false)
@@ -1,3 +1,3 @@
1
1
  class TCPClient
2
- VERSION = '0.0.10'.freeze
2
+ VERSION = '0.1.4'.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.rb CHANGED
@@ -1,16 +1,21 @@
1
1
  require_relative '../lib/tcp-client'
2
2
 
3
- configuration = TCPClient::Configuration.create do |cfg|
4
- cfg.connect_timeout = 0.5 # seconds 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
- end
3
+ TCPClient.configure(
4
+ connect_timeout: 0.5, # seconds to connect the server
5
+ write_timeout: 0.25, # seconds to write a single data junk
6
+ read_timeout: 0.5 # seconds to read some bytes
7
+ )
8
8
 
9
- # the following request sequence is not allowed to last longer than 1.25 seconds:
10
- # 0.5 seconds to connect
9
+ # the following request sequence is not allowed
10
+ # to last longer than 1.25 seconds:
11
+ # 0.5 seconds to connect
11
12
  # + 0.25 seconds to write data
12
13
  # + 0.5 seconds to read a response
13
- TCPClient.open('www.google.com:80', configuration) do |client|
14
- pp client.write("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n") # simple HTTP get request
15
- pp client.read(12) # "HTTP/1.1 " + 3 byte HTTP status code
14
+
15
+ TCPClient.open('www.google.com:80') do |client|
16
+ # simple HTTP get request
17
+ pp client.write("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n")
18
+
19
+ # read "HTTP/1.1 " + 3 byte HTTP status code
20
+ pp client.read(12)
16
21
  end
data/sample/google_ssl.rb CHANGED
@@ -1,17 +1,17 @@
1
1
  require_relative '../lib/tcp-client'
2
2
 
3
- configuration = TCPClient::Configuration.create do |cfg|
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
- cfg.ssl_params = {ssl_version: :TLSv1_2} # use TLS 1.2
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
- TCPClient.open('www.google.com:443', configuration) do |client|
15
- pp client.write("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n") # simple HTTP get request
16
- pp client.read(12) # "HTTP/1.1 " + 3 byte HTTP status code
8
+ TCPClient.open('www.google.com:443') do |client|
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")
13
+
14
+ # read "HTTP/1.1 " + 3 byte HTTP status code
15
+ pp client.read(12)
16
+ end
17
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]
@@ -1,6 +1,8 @@
1
1
  require_relative '../test_helper'
2
2
 
3
- class AddressTest < Test
3
+ class AddressTest < MiniTest::Test
4
+ parallelize_me!
5
+
4
6
  def test_create_from_integer
5
7
  subject = TCPClient::Address.new(42)
6
8
  assert_equal('localhost:42', subject.to_s)
@@ -52,4 +54,12 @@ class AddressTest < Test
52
54
  assert(subject.addrinfo.ip?)
53
55
  assert(subject.addrinfo.ipv6?)
54
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
55
65
  end
@@ -1,6 +1,8 @@
1
1
  require_relative '../test_helper'
2
2
 
3
- class ConfigurationTest < Test
3
+ class ConfigurationTest < MiniTest::Test
4
+ parallelize_me!
5
+
4
6
  def test_defaults
5
7
  subject = TCPClient::Configuration.new
6
8
  assert(subject.buffered)
@@ -13,11 +15,12 @@ class ConfigurationTest < Test
13
15
  end
14
16
 
15
17
  def test_configure
16
- subject = TCPClient::Configuration.create do |cfg|
17
- cfg.buffered = cfg.keep_alive = cfg.reverse_lookup = false
18
- cfg.timeout = 42
19
- cfg.ssl = true
20
- end
18
+ subject =
19
+ TCPClient::Configuration.create do |cfg|
20
+ cfg.buffered = cfg.keep_alive = cfg.reverse_lookup = false
21
+ cfg.timeout = 42
22
+ cfg.ssl = true
23
+ end
21
24
  refute(subject.buffered)
22
25
  refute(subject.keep_alive)
23
26
  refute(subject.reverse_lookup)
@@ -27,6 +30,35 @@ class ConfigurationTest < Test
27
30
  assert(subject.ssl?)
28
31
  end
29
32
 
33
+ def test_options
34
+ subject =
35
+ TCPClient::Configuration.new(
36
+ buffered: false,
37
+ keep_alive: false,
38
+ reverse_lookup: false,
39
+ connect_timeout: 1,
40
+ read_timeout: 2,
41
+ write_timeout: 3,
42
+ ssl: true
43
+ )
44
+ refute(subject.buffered)
45
+ refute(subject.keep_alive)
46
+ refute(subject.reverse_lookup)
47
+ assert_same(1, subject.connect_timeout)
48
+ assert_same(2, subject.read_timeout)
49
+ assert_same(3, subject.write_timeout)
50
+ assert(subject.ssl?)
51
+ end
52
+
53
+ def test_invalid_option
54
+ err =
55
+ assert_raises(ArgumentError) do
56
+ TCPClient::Configuration.new(unknown_attr: :argument)
57
+ end
58
+ assert_includes(err.message, 'attribute')
59
+ assert_includes(err.message, 'unknown_attr')
60
+ end
61
+
30
62
  def test_ssl_params
31
63
  subject = TCPClient::Configuration.new
32
64
  refute(subject.ssl?)
@@ -40,11 +72,12 @@ class ConfigurationTest < Test
40
72
  end
41
73
 
42
74
  def test_timeout_overwrite
43
- subject = TCPClient::Configuration.create do |cfg|
44
- cfg.connect_timeout = 1
45
- cfg.read_timeout = 2
46
- cfg.write_timeout = 3
47
- end
75
+ subject =
76
+ TCPClient::Configuration.create do |cfg|
77
+ cfg.connect_timeout = 1
78
+ cfg.read_timeout = 2
79
+ cfg.write_timeout = 3
80
+ end
48
81
  assert_same(1, subject.connect_timeout)
49
82
  assert_same(2, subject.read_timeout)
50
83
  assert_same(3, subject.write_timeout)
@@ -54,4 +87,12 @@ class ConfigurationTest < Test
54
87
  assert_same(42, subject.read_timeout)
55
88
  assert_same(42, subject.write_timeout)
56
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
57
98
  end
@@ -0,0 +1,59 @@
1
+ require_relative '../test_helper'
2
+
3
+ class DefauktConfigurationTest < MiniTest::Test
4
+ def test_default
5
+ subject = TCPClient.configure # reset to defaults
6
+
7
+ assert_same(
8
+ TCPClient.default_configuration,
9
+ TCPClient::Configuration.default
10
+ )
11
+ assert(subject.buffered)
12
+ assert(subject.keep_alive)
13
+ assert(subject.reverse_lookup)
14
+ refute(subject.ssl?)
15
+ assert_nil(subject.connect_timeout)
16
+ assert_nil(subject.read_timeout)
17
+ assert_nil(subject.write_timeout)
18
+ end
19
+
20
+ def test_configure_options
21
+ TCPClient.configure(
22
+ buffered: false,
23
+ keep_alive: false,
24
+ reverse_lookup: false,
25
+ ssl: true,
26
+ connect_timeout: 1,
27
+ read_timeout: 2,
28
+ write_timeout: 3
29
+ )
30
+ subject = TCPClient.default_configuration
31
+ refute(subject.buffered)
32
+ refute(subject.keep_alive)
33
+ refute(subject.reverse_lookup)
34
+ assert(subject.ssl?)
35
+ assert_same(1, subject.connect_timeout)
36
+ assert_same(2, subject.read_timeout)
37
+ assert_same(3, subject.write_timeout)
38
+ end
39
+
40
+ def test_configure_block
41
+ TCPClient.configure do |cfg|
42
+ cfg.buffered = false
43
+ cfg.keep_alive = false
44
+ cfg.reverse_lookup = false
45
+ cfg.ssl = true
46
+ cfg.connect_timeout = 1
47
+ cfg.read_timeout = 2
48
+ cfg.write_timeout = 3
49
+ end
50
+ subject = TCPClient.default_configuration
51
+ refute(subject.buffered)
52
+ refute(subject.keep_alive)
53
+ refute(subject.reverse_lookup)
54
+ assert(subject.ssl?)
55
+ assert_same(1, subject.connect_timeout)
56
+ assert_same(2, subject.read_timeout)
57
+ assert_same(3, subject.write_timeout)
58
+ end
59
+ end
@@ -1,6 +1,8 @@
1
1
  require_relative '../test_helper'
2
2
 
3
- class VersionTest < Test
3
+ class VersionTest < MiniTest::Test
4
+ parallelize_me!
5
+
4
6
  def test_format
5
7
  assert_match(/\d+\.\d+\.\d+/, TCPClient::VERSION)
6
8
  end
@@ -1,26 +1,32 @@
1
1
  require_relative 'test_helper'
2
2
 
3
- class TCPClientTest < Test
3
+ class TCPClientTest < MiniTest::Test
4
+ parallelize_me!
5
+
6
+ HUGE_AMOUNT_OF_DATA = Array.new(2024, '?' * 1024).freeze
7
+
8
+ attr_reader :config
9
+
10
+ def setup
11
+ @config = TCPClient::Configuration.create(buffered: false)
12
+ end
13
+
4
14
  def test_defaults
5
15
  subject = TCPClient.new
6
16
  assert(subject.closed?)
7
17
  assert_equal('', subject.to_s)
8
18
  assert_nil(subject.address)
9
19
  subject.close
10
- assert_raises(TCPClient::NotConnected) do
11
- subject.write('hello world!')
12
- end
13
- assert_raises(TCPClient::NotConnected) do
14
- subject.read(42)
15
- end
20
+ assert_raises(TCPClient::NotConnected) { subject.write('hello world!') }
21
+ assert_raises(TCPClient::NotConnected) { subject.read(42) }
16
22
  end
17
23
 
18
24
  def create_nonconnected_client
19
25
  client = TCPClient.new
20
- client.connect('', TCPClient::Configuration.new)
26
+ client.connect('', config)
27
+ client
21
28
  rescue Errno::EADDRNOTAVAIL
22
- ensure
23
- return client
29
+ client
24
30
  end
25
31
 
26
32
  def test_failed_state
@@ -32,84 +38,120 @@ class TCPClientTest < Test
32
38
  assert_equal('localhost', subject.address.hostname)
33
39
  assert_instance_of(Addrinfo, subject.address.addrinfo)
34
40
  assert_same(0, subject.address.addrinfo.ip_port)
35
- assert_raises(TCPClient::NotConnected) do
36
- subject.write('hello world!')
37
- end
38
- assert_raises(TCPClient::NotConnected) do
39
- subject.read(42)
40
- end
41
+ assert_raises(TCPClient::NotConnected) { subject.write('hello world!') }
42
+ assert_raises(TCPClient::NotConnected) { subject.read(42) }
41
43
  end
42
44
 
43
- def with_dummy_server(port)
44
- # this server will never receive or send any data
45
- server = TCPServer.new('localhost', port)
46
- yield
47
- ensure
48
- server&.close
45
+ def test_connected_state
46
+ TCPClient.open('localhost:1234', config) do |subject|
47
+ refute(subject.closed?)
48
+ assert_equal('localhost:1234', subject.to_s)
49
+ refute_nil(subject.address)
50
+ address_when_opened = subject.address
51
+ assert_equal('localhost:1234', subject.address.to_s)
52
+ assert_equal('localhost', subject.address.hostname)
53
+ assert_instance_of(Addrinfo, subject.address.addrinfo)
54
+ assert_same(1234, subject.address.addrinfo.ip_port)
55
+
56
+ subject.close
57
+ assert(subject.closed?)
58
+ assert_same(address_when_opened, subject.address)
59
+ end
49
60
  end
50
61
 
51
- def test_connected_state
52
- with_dummy_server(1234) do
53
- TCPClient.open('localhost:1234') do |subject|
54
- refute(subject.closed?)
55
- assert_equal('localhost:1234', subject.to_s)
56
- refute_nil(subject.address)
57
- address_when_opened = subject.address
58
- assert_equal('localhost:1234', subject.address.to_s)
59
- assert_equal('localhost', subject.address.hostname)
60
- assert_instance_of(Addrinfo, subject.address.addrinfo)
61
- assert_same(1234, subject.address.addrinfo.ip_port)
62
-
63
- subject.close
64
- assert(subject.closed?)
65
- assert_same(address_when_opened, subject.address)
62
+ def check_read_timeout(timeout)
63
+ TCPClient.open('localhost:1234', config) do |subject|
64
+ refute(subject.closed?)
65
+ start_time = nil
66
+ assert_raises(TCPClient::ReadTimeoutError) do
67
+ start_time = Time.now
68
+ subject.read(42, timeout: timeout)
66
69
  end
70
+ assert_in_delta(timeout, Time.now - start_time, 0.11)
67
71
  end
68
72
  end
69
73
 
70
- def check_read_write_timeout(addr, timeout)
71
- TCPClient.open(addr) do |subject|
74
+ def test_read_timeout
75
+ check_read_timeout(0.5)
76
+ check_read_timeout(1)
77
+ check_read_timeout(1.5)
78
+ end
79
+
80
+ def check_write_timeout(timeout)
81
+ TCPClient.open('localhost:1234', config) do |subject|
72
82
  refute(subject.closed?)
73
83
  start_time = nil
74
- assert_raises(TCPClient::Timeout) do
84
+ assert_raises(TCPClient::WriteTimeoutError) do
75
85
  start_time = Time.now
76
- # send 1MB to avoid any TCP stack buffering
77
- subject.write('?' * (1024 * 1024), timeout: timeout)
86
+ subject.write(*HUGE_AMOUNT_OF_DATA, timeout: timeout)
78
87
  end
79
88
  assert_in_delta(timeout, Time.now - start_time, 0.02)
80
- assert_raises(TCPClient::Timeout) do
81
- start_time = Time.now
82
- subject.read(42, timeout: timeout)
89
+ end
90
+ end
91
+
92
+ def test_write_timeout
93
+ check_write_timeout(0.01)
94
+ check_write_timeout(0.25)
95
+ end
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
83
117
  end
84
- assert_in_delta(timeout, Time.now - start_time, 0.02)
85
118
  end
86
119
  end
87
120
 
88
- def test_read_write_timeout
89
- with_dummy_server(1235) do
90
- [0.5, 1, 1.5].each do |timeout|
91
- check_read_write_timeout('localhost:1235', timeout)
121
+ def test_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
92
132
  end
93
133
  end
94
134
  end
95
135
 
96
- def check_connect_timeout(addr, config)
136
+ def check_connect_timeout(ssl_config)
97
137
  start_time = nil
98
- assert_raises(TCPClient::Timeout) do
138
+ assert_raises(TCPClient::ConnectTimeoutError) do
99
139
  start_time = Time.now
100
- TCPClient.new.connect(addr, config)
140
+ TCPClient.new.connect('localhost:1234', ssl_config)
101
141
  end
102
- assert_in_delta(config.connect_timeout, Time.now - start_time, 0.02)
142
+ assert_in_delta(ssl_config.connect_timeout, Time.now - start_time, 0.11)
103
143
  end
104
144
 
105
145
  def test_connect_ssl_timeout
106
- with_dummy_server(1236) do
107
- config = TCPClient::Configuration.new
108
- config.ssl = true
109
- [0.5, 1, 1.5].each do |timeout|
110
- config.timeout = timeout
111
- check_connect_timeout('localhost:1236', config)
112
- end
113
- end
146
+ ssl_config = TCPClient::Configuration.new(ssl: true)
147
+
148
+ ssl_config.connect_timeout = 0.5
149
+ check_connect_timeout(ssl_config)
150
+
151
+ ssl_config.connect_timeout = 1
152
+ check_connect_timeout(ssl_config)
153
+
154
+ ssl_config.connect_timeout = 1.5
155
+ check_connect_timeout(ssl_config)
114
156
  end
115
157
  end
data/test/test_helper.rb CHANGED
@@ -1,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'minitest/autorun'
2
4
  require 'minitest/parallel'
3
5
  require_relative '../lib/tcp-client'
4
6
 
5
7
  $stdout.sync = $stderr.sync = true
6
8
 
7
- class Test < Minitest::Test
8
- parallelize_me!
9
- end
9
+ # this pseudo-server never reads or writes anything
10
+ DummyServer = TCPServer.new('localhost', 1234)
11
+ Minitest.after_run { DummyServer.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.0.10
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Blumtritt
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-05-01 00:00:00.000000000 Z
11
+ date: 2021-02-12 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:
@@ -70,6 +70,7 @@ files:
70
70
  - lib/tcp-client.rb
71
71
  - lib/tcp-client/address.rb
72
72
  - lib/tcp-client/configuration.rb
73
+ - lib/tcp-client/default_configuration.rb
73
74
  - lib/tcp-client/mixin/io_timeout.rb
74
75
  - lib/tcp-client/ssl_socket.rb
75
76
  - lib/tcp-client/tcp_socket.rb
@@ -81,6 +82,7 @@ files:
81
82
  - tcp-client.gemspec
82
83
  - test/tcp-client/address_test.rb
83
84
  - test/tcp-client/configuration_test.rb
85
+ - test/tcp-client/default_configuration_test.rb
84
86
  - test/tcp-client/version_test.rb
85
87
  - test/tcp_client_test.rb
86
88
  - test/test_helper.rb
@@ -89,7 +91,7 @@ licenses: []
89
91
  metadata:
90
92
  source_code_uri: https://github.com/mblumtritt/tcp-client
91
93
  bug_tracker_uri: https://github.com/mblumtritt/tcp-client/issues
92
- post_install_message:
94
+ post_install_message:
93
95
  rdoc_options: []
94
96
  require_paths:
95
97
  - lib
@@ -97,20 +99,21 @@ required_ruby_version: !ruby/object:Gem::Requirement
97
99
  requirements:
98
100
  - - ">="
99
101
  - !ruby/object:Gem::Version
100
- version: 2.5.0
102
+ version: 2.7.0
101
103
  required_rubygems_version: !ruby/object:Gem::Requirement
102
104
  requirements:
103
105
  - - ">="
104
106
  - !ruby/object:Gem::Version
105
- version: 1.3.6
107
+ version: '0'
106
108
  requirements: []
107
- rubygems_version: 3.0.3
108
- signing_key:
109
+ rubygems_version: 3.2.9
110
+ signing_key:
109
111
  specification_version: 4
110
112
  summary: A TCP client implementation with working timeout support.
111
113
  test_files:
112
114
  - test/tcp-client/address_test.rb
113
115
  - test/tcp-client/configuration_test.rb
116
+ - test/tcp-client/default_configuration_test.rb
114
117
  - test/tcp-client/version_test.rb
115
118
  - test/tcp_client_test.rb
116
119
  - test/test_helper.rb