@@ -1021,13 +1021,23 @@ class TestSocketKeepalive(unittest.TestCase):
10211021 network requests are made.
10221022 """
10231023
1024+ # Stand-in for the Windows-only socket.SIO_KEEPALIVE_VALS constant.
1025+ SIO_SENTINEL = 2550136836
1026+
10241027 def _keepalive_calls (self , sock ):
10251028 return [
10261029 call
10271030 for call in sock .setsockopt .call_args_list
10281031 if call [0 ][:2 ] == (socket .SOL_SOCKET , socket .SO_KEEPALIVE )
10291032 ]
10301033
1034+ def _tcp_option_calls (self , sock ):
1035+ return [
1036+ call
1037+ for call in sock .setsockopt .call_args_list
1038+ if call [0 ][0 ] == socket .IPPROTO_TCP
1039+ ]
1040+
10311041 def _addrinfo (self , port ):
10321042 return [(socket .AF_INET , socket .SOCK_STREAM , 6 , "" , ("127.0.0.1" , port ))]
10331043
@@ -1039,14 +1049,83 @@ def test_keepalive_enabled_on_socket(self):
10391049 self .assertEqual (self ._keepalive_calls (sock )[0 ][0 ][2 ], 1 )
10401050
10411051 def test_unsupported_options_are_ignored (self ):
1042- """Platform tuning is best effort; a rejecting OS must not raise."""
1052+ """A socket that refuses keepalive outright must not raise."""
10431053 sock = mock .MagicMock ()
10441054 sock .setsockopt .side_effect = OSError ("unsupported" )
10451055 sock .ioctl .side_effect = OSError ("unsupported" )
10461056
10471057 # Must not raise.
10481058 shotgun ._set_socket_keepalive (sock )
10491059
1060+ # Nothing is tuned once the socket has rejected SO_KEEPALIVE.
1061+ sock .ioctl .assert_not_called ()
1062+ self .assertEqual (len (self ._keepalive_calls (sock )), 1 )
1063+
1064+ def test_windows_timers_tuned_via_ioctl (self ):
1065+ """
1066+ On Windows the timers are set with an ioctl rather than socket options.
1067+ SIO_KEEPALIVE_VALS is patched in so the branch runs on any platform.
1068+ """
1069+ sock = mock .MagicMock ()
1070+ with mock .patch .object (
1071+ socket , "SIO_KEEPALIVE_VALS" , self .SIO_SENTINEL , create = True
1072+ ):
1073+ shotgun ._set_socket_keepalive (sock )
1074+
1075+ sock .ioctl .assert_called_once_with (
1076+ self .SIO_SENTINEL ,
1077+ (
1078+ 1 ,
1079+ shotgun .KEEPALIVE_IDLE_SECS * 1000 ,
1080+ shotgun .KEEPALIVE_INTERVAL_SECS * 1000 ,
1081+ ),
1082+ )
1083+ # The POSIX socket options must not also be attempted.
1084+ self .assertEqual (len (self ._tcp_option_calls (sock )), 0 )
1085+
1086+ def test_windows_ioctl_failure_is_ignored (self ):
1087+ """Keepalive stays enabled even if the timers cannot be tuned."""
1088+ sock = mock .MagicMock ()
1089+ sock .ioctl .side_effect = OSError ("unsupported" )
1090+ with mock .patch .object (
1091+ socket , "SIO_KEEPALIVE_VALS" , self .SIO_SENTINEL , create = True
1092+ ):
1093+ # Must not raise.
1094+ shotgun ._set_socket_keepalive (sock )
1095+
1096+ self .assertEqual (len (self ._keepalive_calls (sock )), 1 )
1097+
1098+ def test_timers_tuned_via_socket_options (self ):
1099+ """
1100+ Off Windows the timers are socket options. SIO_KEEPALIVE_VALS is patched
1101+ out so the branch runs there too.
1102+ """
1103+ sock = mock .MagicMock ()
1104+ with mock .patch .object (socket , "SIO_KEEPALIVE_VALS" , None , create = True ):
1105+ shotgun ._set_socket_keepalive (sock )
1106+
1107+ sock .ioctl .assert_not_called ()
1108+ # Which timers exist is platform dependent, but at least the idle timer
1109+ # is available everywhere this library is supported.
1110+ self .assertGreater (len (self ._tcp_option_calls (sock )), 0 )
1111+
1112+ def test_rejected_timer_options_are_ignored (self ):
1113+ """A platform that rejects the timers must still get keepalive."""
1114+ sock = mock .MagicMock ()
1115+
1116+ def reject_tcp_options (level , option , value ):
1117+ if level == socket .IPPROTO_TCP :
1118+ raise OSError ("unsupported" )
1119+ return None
1120+
1121+ sock .setsockopt .side_effect = reject_tcp_options
1122+ with mock .patch .object (socket , "SIO_KEEPALIVE_VALS" , None , create = True ):
1123+ # Must not raise.
1124+ shotgun ._set_socket_keepalive (sock )
1125+
1126+ self .assertEqual (len (self ._keepalive_calls (sock )), 1 )
1127+ self .assertGreater (len (self ._tcp_option_calls (sock )), 0 )
1128+
10501129 def test_http_connection_enables_keepalive (self ):
10511130 sock = mock .MagicMock ()
10521131 with mock .patch ("socket.socket" , return_value = sock ), mock .patch (
0 commit comments