tcp-client 0.0.10 → 0.1.4

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: 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