Skip to content

Commit

Permalink
Improve test coverage. (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
ioquatix authored Dec 16, 2023
1 parent 15b555a commit dd570f7
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 63 deletions.
2 changes: 1 addition & 1 deletion lib/io/endpoint/address_endpoint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def initialize(address, **options)
end

def to_s
"\#<#{self.class} #{@address.inspect}>"
"\#<#{self.class} address=#{@address.inspect}>"
end

attr :address
Expand Down
18 changes: 0 additions & 18 deletions lib/io/endpoint/generic.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,24 +83,6 @@ def accept(backlog: Socket::SOMAXCONN, &block)
end
end

# Map all endpoints by invoking `#bind`.
# @yield the bound wrapper.
def bound
wrappers = []

self.each do |endpoint|
wrapper = endpoint.bind
wrappers << wrapper

yield wrapper
end

success = true
return wrappers
ensure
wrappers.each(&:close) unless success
end

# Create an Endpoint instance by URI scheme. The host and port of the URI will be passed to the Endpoint factory method, along with any options.
#
# @param string [String] URI as string. Scheme will decide implementation used.
Expand Down
9 changes: 5 additions & 4 deletions lib/io/endpoint/host_endpoint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,14 @@ def hostname
# @yield [Socket] the socket which is being connected, may be invoked more than once
# @return [Socket] the connected socket
# @raise if no connection could complete successfully
def connect
def connect(wrapper = Wrapper.default, &block)
last_error = nil

Addrinfo.foreach(*@specification) do |address|
begin
socket = Socket.connect(address, **@options)
socket = wrapper.connect(@address, **@options)
rescue Errno::ECONNREFUSED, Errno::ENETUNREACH, Errno::EAGAIN => last_error
# Try again unless if possible, otherwise raise...
else
return socket unless block_given?

Expand All @@ -54,9 +55,9 @@ def connect
# Invokes the given block for every address which can be bound to.
# @yield [Socket] the bound socket
# @return [Array<Socket>] an array of bound sockets
def bind(&block)
def bind(wrapper = Wrapper.default, &block)
Addrinfo.foreach(*@specification).map do |address|
Socket.bind(address, **@options, &block)
wrapper.bind(address, **@options, &block)
end
end

Expand Down
83 changes: 59 additions & 24 deletions lib/io/endpoint/shared_endpoint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@

require_relative 'generic'
require_relative 'composite_endpoint'
require_relative 'socket_endpoint'

module IO::Endpoint
# Pre-connect and pre-bind sockets so that it can be used between processes.
class SharedEndpoint < Generic
# Create a new `SharedEndpoint` by binding to the given endpoint.
def self.bound(endpoint, backlog: Socket::SOMAXCONN, close_on_exec: false)
wrappers = endpoint.bound do |server|
def self.bound(endpoint, backlog: Socket::SOMAXCONN, close_on_exec: false, **options)
sockets = []

endpoint.each do |server_endpoint|
server = server_endpoint.bind(**options)

# This is somewhat optional. We want to have a generic interface as much as possible so that users of this interface can just call it without knowing a lot of internal details. Therefore, we ignore errors here if it's because the underlying socket does not support the operation.
begin
server.listen(backlog)
Expand All @@ -20,9 +25,11 @@ def self.bound(endpoint, backlog: Socket::SOMAXCONN, close_on_exec: false)
end

server.close_on_exec = close_on_exec

sockets << server
end

return self.new(endpoint, wrappers)
return self.new(endpoint, sockets)
end

# Create a new `SharedEndpoint` by connecting to the given endpoint.
Expand All @@ -34,70 +41,98 @@ def self.connected(endpoint, close_on_exec: false)
return self.new(endpoint, [wrapper])
end

def initialize(endpoint, wrappers, **options)
def initialize(endpoint, sockets, **options)
super(**options)

raise TypeError, "sockets must be an Array" unless sockets.is_a?(Array)

@endpoint = endpoint
@wrappers = wrappers
@sockets = sockets
end

attr :endpoint
attr :wrappers
attr :sockets

def local_address_endpoint(**options)
endpoints = @wrappers.map do |wrapper|
endpoints = @sockets.map do |wrapper|
AddressEndpoint.new(wrapper.to_io.local_address)
end

return CompositeEndpoint.new(endpoints, **options)
end

def remote_address_endpoint(**options)
endpoints = @wrappers.map do |wrapper|
endpoints = @sockets.map do |wrapper|
AddressEndpoint.new(wrapper.to_io.remote_address)
end

return CompositeEndpoint.new(endpoints, **options)
end

# Close all the internal wrappers.
# Close all the internal sockets.
def close
@wrappers.each(&:close)
@wrappers.clear
@sockets.each(&:close)
@sockets.clear
end

def each(&block)
return to_enum unless block_given?

@sockets.each do |socket|
yield SocketEndpoint.new(socket.dup)
end
end

def bind
@wrappers.each do |server|
def bind(wrapper = Wrapper.default, &block)
@sockets.each.map do |server|
server = server.dup

begin
yield server
ensure
server.close
if block_given?
wrapper.async do
begin
yield server
ensure
server.close
end
end
else
server
end
end
end

def connect
@wrappers.each do |peer|
peer = peer.dup
def connect(wrapper = Wrapper.default, &block)
@sockets.each do |socket|
socket = socket.dup

return socket unless block_given?

begin
yield peer
return yield(socket)
ensure
peer.close
socket.close
end
end
end

def accept(backlog = nil, &block)
def accept(**options, &block)
bind do |server|
server.accept(&block)
end
end

def to_s
"\#<#{self.class} #{@wrappers.size} descriptors for #{@endpoint}>"
"\#<#{self.class} #{@sockets.size} descriptors for #{@endpoint}>"
end
end

class Generic
def bound(**options)
SharedEndpoint.bound(self, **options)
end

def connected(**options)
SharedEndpoint.connected(self, **options)
end
end
end
6 changes: 6 additions & 0 deletions test/io/endpoint/address_endpoint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,10 @@
server&.close
thread&.join
end

with "#to_s" do
it "can generate a string representation" do
expect(endpoint.to_s).to be =~ /#<IO::Endpoint::AddressEndpoint address=/
end
end
end
47 changes: 31 additions & 16 deletions test/io/endpoint/host_endpoint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@
# Released under the MIT License.
# Copyright, 2023, by Samuel Williams.

require 'io/endpoint/address_endpoint'
require 'io/endpoint/host_endpoint'

describe IO::Endpoint::AddressEndpoint do
let(:options) {Hash.new}
let(:address) {Addrinfo.tcp('localhost', 0)}
let(:endpoint) {subject.new(address)}
describe IO::Endpoint::HostEndpoint do
let(:specification) {["localhost", 0, nil, ::Socket::SOCK_STREAM]}
let(:endpoint) {subject.new(specification)}

it "can bind to address" do
endpoint.bind do |socket|
Expand All @@ -17,27 +16,34 @@
end

it "can connect to address" do
server = endpoint.bind
expect(server).to be_a(Socket)
bound = endpoint.bound

server.listen(1)

thread = Thread.new do
bound.bind do |server|
expect(server).to be_a(Socket)
peer, address = server.accept
peer.close
end

subject.new(server.local_address).connect do |socket|
expect(socket).to be_a(Socket)
bound.sockets.each do |server|
server_endpoint = IO::Endpoint::AddressEndpoint.new(server.local_address)

client = server_endpoint.connect
expect(client).to be_a(Socket)

# Wait for the connection to be closed.
socket.wait_readable
client.wait_readable

socket.close
client.close
end
ensure
server&.close
thread&.join
bound&.close
end

with "#to_s" do
it "can generate a string representation" do
expect(endpoint.to_s).to be == "#<IO::Endpoint::HostEndpoint name=\"localhost\" service=0 family=nil type=1 protocol=nil flags=nil>"
end
end
end

Expand All @@ -50,4 +56,13 @@
expect(endpoint).to have_attributes(specification: be == ["localhost", 0, nil, ::Socket::SOCK_DGRAM])
end
end

with '.tcp' do
let(:endpoint) {subject.tcp("localhost", 0)}

it "can construct endpoint from path" do
expect(endpoint).to be_a(IO::Endpoint::HostEndpoint)
expect(endpoint).to have_attributes(specification: be == ["localhost", 0, nil, ::Socket::SOCK_STREAM])
end
end
end

0 comments on commit dd570f7

Please sign in to comment.