ruby/test/socket/test_tcp.rb
Misaki Shioi 8f57204c19
Avoid test failures on hosts that only support IPv4 (#12213)
To verify the behavior of HEv2, some tests were prepared. But unexpected failures occur in certain environments.
This happens in environments where "localhost" resolves only to an IPv4 address during tests that verify connections to IPv6.

For example, the following situation can occur:

- The server process is bound to ::1.
- The client socket always resolves "localhost" to 127.0.0.1 and attempts to connect to 127.0.0.1.
- Since no server is bound to 127.0.0.1, an ECONNREFUSED error is raised.

In such situations, the behavior of `TCPSocket.new` remains unchanged from before the introduction of HEv2.
(The failures occur because tests explicitly binding to ::1 were added to verify HEv2 behavior.)

This change ensures that the affected tests are skipped in environments of this kind.
2024-12-02 21:47:51 +09:00

410 lines
12 KiB
Ruby

# frozen_string_literal: true
begin
require "socket"
require "test/unit"
rescue LoadError
end
class TestSocket_TCPSocket < Test::Unit::TestCase
def test_inspect
TCPServer.open("localhost", 0) {|server|
assert_match(/AF_INET/, server.inspect)
TCPSocket.open("localhost", server.addr[1]) {|client|
assert_match(/AF_INET/, client.inspect)
}
}
end
def test_initialize_failure
# These addresses are chosen from TEST-NET-1, TEST-NET-2, and TEST-NET-3.
# [RFC 5737]
# They are chosen because probably they are not used as a host address.
# Anyway the addresses are used for bind() and should be failed.
# So no packets should be generated.
test_ip_addresses = [
'192.0.2.1', '192.0.2.42', # TEST-NET-1
'198.51.100.1', '198.51.100.42', # TEST-NET-2
'203.0.113.1', '203.0.113.42', # TEST-NET-3
]
begin
list = Socket.ip_address_list
rescue NotImplementedError
return
end
test_ip_addresses -= list.reject {|ai| !ai.ipv4? }.map {|ai| ai.ip_address }
if test_ip_addresses.empty?
return
end
client_addr = test_ip_addresses.first
client_port = 8000
server_addr = '127.0.0.1'
server_port = 80
begin
# Since client_addr is not an IP address of this host,
# bind() in TCPSocket.new should fail as EADDRNOTAVAIL.
t = TCPSocket.new(server_addr, server_port, client_addr, client_port)
flunk "expected SystemCallError"
rescue SystemCallError => e
assert_match "for \"#{client_addr}\" port #{client_port}", e.message
end
ensure
t.close if t && !t.closed?
end
def test_initialize_resolv_timeout
TCPServer.open("localhost", 0) do |svr|
th = Thread.new {
c = svr.accept
c.close
}
addr = svr.addr
s = TCPSocket.new(addr[3], addr[1], resolv_timeout: 10)
th.join
ensure
s.close()
end
end
def test_initialize_connect_timeout
assert_raise(IO::TimeoutError, Errno::ENETUNREACH, Errno::EACCES) do
TCPSocket.new("192.0.2.1", 80, connect_timeout: 0)
end
end
def test_recvfrom
TCPServer.open("localhost", 0) {|svr|
th = Thread.new {
c = svr.accept
c.write "foo"
c.close
}
addr = svr.addr
TCPSocket.open(addr[3], addr[1]) {|sock|
assert_equal(["foo", nil], sock.recvfrom(0x10000))
}
th.join
}
end
def test_encoding
TCPServer.open("localhost", 0) {|svr|
th = Thread.new {
c = svr.accept
c.write "foo\r\n"
c.close
}
addr = svr.addr
TCPSocket.open(addr[3], addr[1]) {|sock|
assert_equal(true, sock.binmode?)
s = sock.gets
assert_equal("foo\r\n", s)
assert_equal(Encoding.find("ASCII-8BIT"), s.encoding)
}
th.join
}
end
def test_accept_nonblock
TCPServer.open("localhost", 0) {|svr|
assert_raise(IO::WaitReadable) { svr.accept_nonblock }
assert_equal :wait_readable, svr.accept_nonblock(exception: false)
assert_raise(IO::WaitReadable) { svr.accept_nonblock(exception: true) }
}
end
def test_accept_multithread
attempts_count = 5
server_threads_count = 3
client_threads_count = 3
attempts_count.times do
server_threads = Array.new(server_threads_count) do
Thread.new do
TCPServer.open("localhost", 0) do |server|
accept_threads = Array.new(client_threads_count) do
Thread.new { server.accept.close }
end
client_threads = Array.new(client_threads_count) do
Thread.new { TCPSocket.open(server.addr[3], server.addr[1]) {} }
end
client_threads.each(&:join)
accept_threads.each(&:join)
end
end
end
server_threads.each(&:join)
end
end
def test_initialize_v6_hostname_resolved_earlier
# pend "to suppress the output of test failure logs in CI temporarily"
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
begin
# Verify that "localhost" can be resolved to an IPv6 address
Socket.getaddrinfo("localhost", 0, Socket::AF_INET6)
server = TCPServer.new("::1", 0)
rescue Socket::ResolutionError, Errno::EADDRNOTAVAIL # IPv6 is not supported
return
end
server_thread = Thread.new { server.accept }
port = server.addr[1]
socket = TCPSocket.new(
"localhost",
port,
fast_fallback: true,
test_mode_settings: { delay: { ipv4: 1000 } }
)
assert_true(socket.remote_address.ipv6?)
server_thread.value.close
server.close
socket.close if socket && !socket.closed?
end
def test_initialize_v4_hostname_resolved_earlier
# pend "to suppress the output of test failure logs in CI temporarily"
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
server = TCPServer.new("127.0.0.1", 0)
port = server.addr[1]
server_thread = Thread.new { server.accept }
socket = TCPSocket.new(
"localhost",
port,
fast_fallback: true,
test_mode_settings: { delay: { ipv6: 1000 } }
)
assert_true(socket.remote_address.ipv4?)
server_thread.value.close
server.close
socket.close if socket && !socket.closed?
end
def test_initialize_v6_hostname_resolved_in_resolution_delay
# pend "to suppress the output of test failure logs in CI temporarily"
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
begin
# Verify that "localhost" can be resolved to an IPv6 address
Socket.getaddrinfo("localhost", 0, Socket::AF_INET6)
server = TCPServer.new("::1", 0)
rescue Socket::ResolutionError, Errno::EADDRNOTAVAIL # IPv6 is not supported
return
end
port = server.addr[1]
delay_time = 25 # Socket::RESOLUTION_DELAY (private) is 50ms
server_thread = Thread.new { server.accept }
socket = TCPSocket.new(
"localhost",
port,
fast_fallback: true,
test_mode_settings: { delay: { ipv6: delay_time } }
)
assert_true(socket.remote_address.ipv6?)
server_thread.value.close
server.close
socket.close if socket && !socket.closed?
end
def test_initialize_v6_hostname_resolved_earlier_and_v6_server_is_not_listening
# pend "to suppress the output of test failure logs in CI temporarily"
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
ipv4_address = "127.0.0.1"
ipv4_server = Socket.new(Socket::AF_INET, :STREAM)
ipv4_server.bind(Socket.pack_sockaddr_in(0, ipv4_address))
port = ipv4_server.connect_address.ip_port
ipv4_server_thread = Thread.new { ipv4_server.listen(1); ipv4_server.accept }
socket = TCPSocket.new(
"localhost",
port,
fast_fallback: true,
test_mode_settings: { delay: { ipv4: 10 } }
)
assert_equal(ipv4_address, socket.remote_address.ip_address)
accepted, _ = ipv4_server_thread.value
accepted.close
ipv4_server.close
socket.close if socket && !socket.closed?
end
def test_initialize_v6_hostname_resolved_later_and_v6_server_is_not_listening
# pend "to suppress the output of test failure logs in CI temporarily"
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
ipv4_server = Socket.new(Socket::AF_INET, :STREAM)
ipv4_server.bind(Socket.pack_sockaddr_in(0, "127.0.0.1"))
port = ipv4_server.connect_address.ip_port
ipv4_server_thread = Thread.new { ipv4_server.listen(1); ipv4_server.accept }
socket = TCPSocket.new(
"localhost",
port,
fast_fallback: true,
test_mode_settings: { delay: { ipv6: 25 } }
)
assert_equal(
socket.remote_address.ipv4?,
true
)
accepted, _ = ipv4_server_thread.value
accepted.close
ipv4_server.close
socket.close if socket && !socket.closed?
end
def test_initialize_v6_hostname_resolution_failed_and_v4_hostname_resolution_is_success
# pend "to suppress the output of test failure logs in CI temporarily"
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
server = TCPServer.new("127.0.0.1", 0)
port = server.addr[1]
server_thread = Thread.new { server.accept }
socket = TCPSocket.new(
"localhost",
port,
fast_fallback: true,
test_mode_settings: { delay: { ipv4: 10 }, error: { ipv6: Socket::EAI_FAIL } }
)
assert_true(socket.remote_address.ipv4?)
server_thread.value.close
server.close
socket.close if socket && !socket.closed?
end
def test_initialize_resolv_timeout_with_connection_failure
# pend "to suppress the output of test failure logs in CI temporarily"
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
begin
server = TCPServer.new("::1", 0)
rescue Errno::EADDRNOTAVAIL # IPv6 is not supported
return
end
port = server.connect_address.ip_port
server.close
assert_raise(Errno::ETIMEDOUT) do
TCPSocket.new(
"localhost",
port,
resolv_timeout: 0.01,
fast_fallback: true,
test_mode_settings: { delay: { ipv4: 1000 } }
)
end
end
def test_initialize_with_hostname_resolution_failure_after_connection_failure
# pend "to suppress the output of test failure logs in CI temporarily"
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
begin
server = TCPServer.new("::1", 0)
rescue Errno::EADDRNOTAVAIL # IPv6 is not supported
return
end
port = server.connect_address.ip_port
server.close
assert_raise(Socket::ResolutionError) do
TCPSocket.new(
"localhost",
port,
fast_fallback: true,
test_mode_settings: { delay: { ipv4: 100 }, error: { ipv4: Socket::EAI_FAIL } }
)
end
end
def test_initialize_with_connection_failure_after_hostname_resolution_failure
# pend "to suppress the output of test failure logs in CI temporarily"
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
server = TCPServer.new("127.0.0.1", 0)
port = server.connect_address.ip_port
server.close
assert_raise(Errno::ECONNREFUSED) do
TCPSocket.new(
"localhost",
port,
fast_fallback: true,
test_mode_settings: { delay: { ipv4: 100 }, error: { ipv6: Socket::EAI_FAIL } }
)
end
end
def test_initialize_v6_connected_socket_with_v6_address
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
begin
server = TCPServer.new("::1", 0)
rescue Errno::EADDRNOTAVAIL # IPv6 is not supported
return
end
server_thread = Thread.new { server.accept }
port = server.addr[1]
socket = TCPSocket.new("::1", port)
assert_true(socket.remote_address.ipv6?)
ensure
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
server_thread.value.close
server.close
socket.close if socket && !socket.closed?
end
def test_initialize_v4_connected_socket_with_v4_address
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
server = TCPServer.new("127.0.0.1", 0)
server_thread = Thread.new { server.accept }
port = server.addr[1]
socket = TCPSocket.new("127.0.0.1", port)
assert_true(socket.remote_address.ipv4?)
ensure
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
server_thread.value.close
server.close
socket.close if socket && !socket.closed?
end
def test_initialize_fast_fallback_is_false
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
server = TCPServer.new("127.0.0.1", 0)
_, port, = server.addr
server_thread = Thread.new { server.accept }
socket = TCPSocket.new("127.0.0.1", port, fast_fallback: false)
assert_true(socket.remote_address.ipv4?)
ensure
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
server_thread.value.close
server.close
socket.close if socket && !socket.closed?
end
end if defined?(TCPSocket)