Skip to content

Commit e602516

Browse files
committed
add more test coverage
1 parent 75d9c08 commit e602516

2 files changed

Lines changed: 84 additions & 3 deletions

File tree

shotgun_api3/shotgun.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,10 +161,12 @@ def _set_socket_keepalive(sock) -> None:
161161
return
162162

163163
# Windows exposes the timers through an ioctl rather than socket options.
164-
if hasattr(socket, "SIO_KEEPALIVE_VALS") and hasattr(sock, "ioctl"):
164+
# Read via getattr so this branch stays reachable in tests on any platform.
165+
keepalive_vals = getattr(socket, "SIO_KEEPALIVE_VALS", None)
166+
if keepalive_vals is not None and hasattr(sock, "ioctl"):
165167
try:
166168
sock.ioctl(
167-
socket.SIO_KEEPALIVE_VALS,
169+
keepalive_vals,
168170
(1, KEEPALIVE_IDLE_SECS * 1000, KEEPALIVE_INTERVAL_SECS * 1000),
169171
)
170172
except OSError:

tests/test_unit.py

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)