diff --git a/stdlib/socket/0/basic_socket.rbs b/stdlib/socket/0/basic_socket.rbs index a69132467b..63552bd49c 100644 --- a/stdlib/socket/0/basic_socket.rbs +++ b/stdlib/socket/0/basic_socket.rbs @@ -237,7 +237,7 @@ class BasicSocket < IO # onoff, linger = optval.unpack "ii" # onoff = onoff == 0 ? false : true # - def getsockopt: (Symbol | Integer, Symbol | Integer) -> (Integer | boolish | String) + def getsockopt: (interned | Integer level, interned | Integer optname) -> Socket::Option # + # Returns whether Happy Eyeballs Version 2 ([RFC + # 8305](https://datatracker.ietf.org/doc/html/rfc8305)), which is provided + # starting from Ruby 3.4 when using TCPSocket.new and Socket.tcp, is enabled or + # disabled. + # + # If true, it is enabled for TCPSocket.new and Socket.tcp. (Note: Happy Eyeballs + # Version 2 is not provided when using TCPSocket.new on Windows.) + # + # If false, Happy Eyeballs Version 2 is disabled. + # + # For details on Happy Eyeballs Version 2, see + # [Socket.tcp_fast_fallback=](rdoc-ref:Socket.tcp_fast_fallback=). + # + def self.tcp_fast_fallback: () -> bool + + # + # Enable or disable Happy Eyeballs Version 2 ([RFC + # 8305](https://datatracker.ietf.org/doc/html/rfc8305)) globally, which is + # provided starting from Ruby 3.4 when using TCPSocket.new and Socket.tcp. + # + # When set to true, the feature is enabled for both TCPSocket.new + # and Socket.tcp. (Note: This feature is not available when using + # TCPSocket.new on Windows.) + # + # When set to false, the behavior reverts to that of Ruby 3.3 or earlier. + # + # The default value is true if no value is explicitly set by calling this + # method. However, when the environment variable RUBY_TCP_NO_FAST_FALLBACK=1 is + # set, the default is false. + # + # To control the setting on a per-method basis, use the fast_fallback keyword + # argument for each method. + # + # ### Happy Eyeballs Version 2 + # Happy Eyeballs Version 2 ([RFC + # 8305](https://datatracker.ietf.org/doc/html/rfc8305)) is an algorithm designed + # to improve client socket connectivity. + # It aims for more reliable and efficient connections by performing hostname + # resolution and connection attempts in parallel, instead of serially. + # + # Starting from Ruby 3.4, this method operates as follows with this algorithm: + # + # 1. Start resolving both IPv6 and IPv4 addresses concurrently. + # 2. Start connecting to the one of the addresses that are obtained first. + # If IPv4 addresses are obtained first, the method waits 50 ms for IPv6 name + # resolution to prioritize IPv6 connections. + # 3. After starting a connection attempt, wait 250 ms for the connection to be + # established. + # If no connection is established within this time, a new connection is + # started every 250 ms + # until a connection is established or there are no more candidate + # addresses. + # (Although RFC 8305 strictly specifies sorting addresses, + # this method only alternates between IPv6 / IPv4 addresses due to the + # performance concerns) + # 4. Once a connection is established, all remaining connection attempts are + # canceled. + # + def self.tcp_fast_fallback=: (bool value) -> bool + # +# Socket::Option represents a socket option used by BasicSocket#getsockopt and +# BasicSocket#setsockopt. A socket option contains the socket #family, protocol +# #level, option name #optname and option value #data. +# +class Socket::Option + # + # Returns a new Socket::Option object. + # + # sockopt = Socket::Option.new(:INET, :SOCKET, :KEEPALIVE, [1].pack("i")) + # p sockopt #=> # + # + def initialize: (interned | Integer family, interned | Integer level, interned | Integer optname, String data) -> void + + # + # Creates a new Socket::Option object which contains boolean as data. Actually 0 + # or 1 as int is used. + # + # require 'socket' + # + # p Socket::Option.bool(:INET, :SOCKET, :KEEPALIVE, true) + # #=> # + # + # p Socket::Option.bool(:INET, :SOCKET, :KEEPALIVE, false) + # #=> # + # + def self.bool: (interned | Integer family, interned | Integer level, interned | Integer optname, boolish bool) -> instance + + # + # Creates a new Socket::Option object which contains a byte as data. + # + # p Socket::Option.byte(:INET, :SOCKET, :KEEPALIVE, 1) + # #=> # + # + def self.byte: (interned | Integer family, interned | Integer level, interned | Integer optname, Integer integer) -> instance + + # + # Creates a new Socket::Option object which contains an int as data. + # + # The size and endian is dependent on the platform. + # + # p Socket::Option.int(:INET, :SOCKET, :KEEPALIVE, 1) + # #=> # + # + def self.int: (interned | Integer family, interned | Integer level, interned | Integer optname, Integer integer) -> instance + + # + # Creates a new Socket::Option object for IP_MULTICAST_LOOP. + # + # The size is dependent on the platform. + # + # sockopt = Socket::Option.int(:INET, :IPPROTO_IP, :IP_MULTICAST_LOOP, 1) + # p sockopt.int => 1 + # + # p Socket::Option.ipv4_multicast_loop(10) + # #=> # + # + def self.ipv4_multicast_loop: (Integer integer) -> instance + + # + # Creates a new Socket::Option object for IP_MULTICAST_TTL. + # + # The size is dependent on the platform. + # + # p Socket::Option.ipv4_multicast_ttl(10) + # #=> # + # + def self.ipv4_multicast_ttl: (Integer integer) -> instance + + # + # Creates a new Socket::Option object for SOL_SOCKET/SO_LINGER. + # + # *onoff* should be an integer or a boolean. + # + # *secs* should be the number of seconds. + # + # p Socket::Option.linger(true, 10) + # #=> # + # + def self.linger: (boolish | Integer onoff, Integer secs) -> instance + + # + # Returns the data in *sockopt* as an boolean value. + # + # sockopt = Socket::Option.int(:INET, :SOCKET, :KEEPALIVE, 1) + # p sockopt.bool => true + # + def bool: () -> bool + + # + # Returns the data in *sockopt* as an byte. + # + # sockopt = Socket::Option.byte(:INET, :SOCKET, :KEEPALIVE, 1) + # p sockopt.byte => 1 + # + def byte: () -> Integer + + # + # returns the socket option data as a string. + # + # p Socket::Option.new(:INET6, :IPV6, :RECVPKTINFO, [1].pack("i!")).data + # #=> "\x01\x00\x00\x00" + # + def data: () -> String + + # + # returns the socket family as an integer. + # + # p Socket::Option.new(:INET6, :IPV6, :RECVPKTINFO, [1].pack("i!")).family + # #=> 10 + # + def family: () -> Integer + + # + # Returns a string which shows sockopt in human-readable form. + # + # p Socket::Option.new(:INET, :SOCKET, :KEEPALIVE, [1].pack("i")).inspect + # #=> "#" + # + def inspect: () -> String + + # + # Returns the data in *sockopt* as an int. + # + # The size and endian is dependent on the platform. + # + # sockopt = Socket::Option.int(:INET, :SOCKET, :KEEPALIVE, 1) + # p sockopt.int => 1 + # + def int: () -> Integer + + # + # Returns the ipv4_multicast_loop data in *sockopt* as an integer. + # + # sockopt = Socket::Option.ipv4_multicast_loop(10) + # p sockopt.ipv4_multicast_loop => 10 + # + def ipv4_multicast_loop: () -> Integer + + # + # Returns the ipv4_multicast_ttl data in *sockopt* as an integer. + # + # sockopt = Socket::Option.ipv4_multicast_ttl(10) + # p sockopt.ipv4_multicast_ttl => 10 + # + def ipv4_multicast_ttl: () -> Integer + + # + # returns the socket level as an integer. + # + # p Socket::Option.new(:INET6, :IPV6, :RECVPKTINFO, [1].pack("i!")).level + # #=> 41 + # + def level: () -> Integer + + # + # Returns the linger data in *sockopt* as a pair of boolean and integer. + # + # sockopt = Socket::Option.linger(true, 10) + # p sockopt.linger => [true, 10] + # + def linger: () -> [ bool, Integer ] + + # + # returns the socket option name as an integer. + # + # p Socket::Option.new(:INET6, :IPV6, :RECVPKTINFO, [1].pack("i!")).optname + # #=> 2 + # + def optname: () -> Integer + + # + # returns the socket option data as a string. + # + # p Socket::Option.new(:INET6, :IPV6, :RECVPKTINFO, [1].pack("i!")).data + # #=> "\x01\x00\x00\x00" + # + alias to_s data + + # + # Calls String#unpack on sockopt.data. + # + # sockopt = Socket::Option.new(:INET, :SOCKET, :KEEPALIVE, [1].pack("i")) + # p sockopt.unpack("i") #=> [1] + # p sockopt.data.unpack("i") #=> [1] + # + def unpack: (String template) -> Array[untyped] +end diff --git a/stdlib/socket/0/socket_error.rbs b/stdlib/socket/0/socket_error.rbs index 02095c7feb..466e575c27 100644 --- a/stdlib/socket/0/socket_error.rbs +++ b/stdlib/socket/0/socket_error.rbs @@ -3,3 +3,27 @@ # class SocketError < StandardError end + +# +# Socket::ResolutionError is the error class for hostname resolution. +# +class Socket::ResolutionError < SocketError + # + # Returns the raw error code indicating the cause of the hostname resolution + # failure. + # + # begin + # Addrinfo.getaddrinfo("ruby-lang.org", nil) + # rescue Socket::ResolutionError => e + # if e.error_code == Socket::EAI_AGAIN + # puts "Temporary failure in name resolution." + # end + # end + # + # Note that error codes depend on the operating system. + # + def error_code: () -> Integer +end diff --git a/test/stdlib/socket/BasicSocket_sockopt_test.rb b/test/stdlib/socket/BasicSocket_sockopt_test.rb new file mode 100644 index 0000000000..8e886a1b88 --- /dev/null +++ b/test/stdlib/socket/BasicSocket_sockopt_test.rb @@ -0,0 +1,44 @@ +require_relative "../test_helper" + +require "socket" + +class BasicSocketSockoptTest < Test::Unit::TestCase + include TestHelper + + library "socket" + testing "::BasicSocket" + + def test_getsockopt + Socket.open(:INET, :STREAM) do |socket| + assert_send_type( + "(::Symbol, ::Symbol) -> ::Socket::Option", + socket, :getsockopt, :SOCKET, :KEEPALIVE + ) + assert_send_type( + "(::Integer, ::Integer) -> ::Socket::Option", + socket, :getsockopt, Socket::SOL_SOCKET, Socket::SO_KEEPALIVE + ) + end + end + + def test_setsockopt + Socket.open(:INET, :STREAM) do |socket| + assert_send_type( + "(::Socket::Option) -> void", + socket, :setsockopt, Socket::Option.bool(:INET, :SOCKET, :KEEPALIVE, true) + ) + assert_send_type( + "(::Symbol, ::Symbol, bool) -> void", + socket, :setsockopt, :SOCKET, :KEEPALIVE, true + ) + assert_send_type( + "(::Symbol, ::Symbol, ::Integer) -> void", + socket, :setsockopt, :SOCKET, :KEEPALIVE, 1 + ) + assert_send_type( + "(::Integer, ::Integer, ::String) -> void", + socket, :setsockopt, Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, [1].pack("i") + ) + end + end +end diff --git a/test/stdlib/socket/SocketOption_test.rb b/test/stdlib/socket/SocketOption_test.rb new file mode 100644 index 0000000000..b821e98b06 --- /dev/null +++ b/test/stdlib/socket/SocketOption_test.rb @@ -0,0 +1,130 @@ +require_relative "../test_helper" + +require "socket" + +class SocketOptionSingletonTest < Test::Unit::TestCase + include TestHelper + + library "socket" + testing "singleton(::Socket::Option)" + + def test_new + assert_send_type( + "(::Symbol, ::Symbol, ::Symbol, ::String) -> ::Socket::Option", + Socket::Option, :new, :INET, :SOCKET, :KEEPALIVE, [1].pack("i") + ) + assert_send_type( + "(::String, ::Integer, ::Integer, ::String) -> ::Socket::Option", + Socket::Option, :new, "INET", Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, [1].pack("i") + ) + end + + def test_bool + assert_send_type( + "(::Symbol, ::Symbol, ::Symbol, bool) -> ::Socket::Option", + Socket::Option, :bool, :INET, :SOCKET, :KEEPALIVE, true + ) + end + + def test_byte + assert_send_type( + "(::Symbol, ::Symbol, ::Symbol, ::Integer) -> ::Socket::Option", + Socket::Option, :byte, :INET, :IP, :TTL, 3 + ) + end + + def test_int + assert_send_type( + "(::Symbol, ::Symbol, ::Symbol, ::Integer) -> ::Socket::Option", + Socket::Option, :int, :INET, :SOCKET, :KEEPALIVE, 1 + ) + end + + def test_ipv4_multicast_loop + assert_send_type( + "(::Integer) -> ::Socket::Option", + Socket::Option, :ipv4_multicast_loop, 1 + ) + end + + def test_ipv4_multicast_ttl + assert_send_type( + "(::Integer) -> ::Socket::Option", + Socket::Option, :ipv4_multicast_ttl, 1 + ) + end + + def test_linger + assert_send_type( + "(bool, ::Integer) -> ::Socket::Option", + Socket::Option, :linger, true, 10 + ) + assert_send_type( + "(::Integer, ::Integer) -> ::Socket::Option", + Socket::Option, :linger, 1, 10 + ) + end +end + +class SocketOptionTest < Test::Unit::TestCase + include TestHelper + + library "socket" + testing "::Socket::Option" + + def keepalive + Socket::Option.new(:INET, :SOCKET, :KEEPALIVE, [1].pack("i")) + end + + def test_bool + assert_send_type "() -> bool", keepalive, :bool + end + + def test_byte + assert_send_type "() -> ::Integer", Socket::Option.byte(:INET, :IP, :TTL, 3), :byte + end + + def test_data + assert_send_type "() -> ::String", keepalive, :data + end + + def test_to_s + assert_send_type "() -> ::String", keepalive, :to_s + end + + def test_family + assert_send_type "() -> ::Integer", keepalive, :family + end + + def test_inspect + assert_send_type "() -> ::String", keepalive, :inspect + end + + def test_int + assert_send_type "() -> ::Integer", keepalive, :int + end + + def test_ipv4_multicast_loop + assert_send_type "() -> ::Integer", Socket::Option.ipv4_multicast_loop(1), :ipv4_multicast_loop + end + + def test_ipv4_multicast_ttl + assert_send_type "() -> ::Integer", Socket::Option.ipv4_multicast_ttl(1), :ipv4_multicast_ttl + end + + def test_level + assert_send_type "() -> ::Integer", keepalive, :level + end + + def test_linger + assert_send_type "() -> [bool, ::Integer]", Socket::Option.linger(true, 10), :linger + end + + def test_optname + assert_send_type "() -> ::Integer", keepalive, :optname + end + + def test_unpack + assert_send_type "(::String) -> ::Array[untyped]", keepalive, :unpack, "i" + end +end diff --git a/test/stdlib/socket/SocketResolutionError_test.rb b/test/stdlib/socket/SocketResolutionError_test.rb new file mode 100644 index 0000000000..4aa2ccebba --- /dev/null +++ b/test/stdlib/socket/SocketResolutionError_test.rb @@ -0,0 +1,23 @@ +require_relative "../test_helper" + +require "socket" + +class SocketResolutionErrorTest < Test::Unit::TestCase + include TestHelper + + library "socket" + testing "::Socket::ResolutionError" + + def resolution_error + Addrinfo.getaddrinfo("nonexistent.invalid", nil) + rescue Socket::ResolutionError => error + error + end + + def test_error_code + assert_send_type( + "() -> ::Integer", + resolution_error, :error_code + ) + end +end diff --git a/test/stdlib/socket/Socket_test.rb b/test/stdlib/socket/Socket_test.rb index f024c89456..2c103ddac3 100644 --- a/test/stdlib/socket/Socket_test.rb +++ b/test/stdlib/socket/Socket_test.rb @@ -66,6 +66,23 @@ def test_tcp ) end end + + def test_tcp_fast_fallback + assert_send_type( + "() -> bool", + Socket, :tcp_fast_fallback + ) + end + + def test_tcp_fast_fallback= + original = Socket.tcp_fast_fallback + assert_send_type( + "(bool) -> bool", + Socket, :tcp_fast_fallback=, false + ) + ensure + Socket.tcp_fast_fallback = original + end end class SocketInstanceTest < Test::Unit::TestCase