diff --git a/kubernetes/base/stream/ws_client.py b/kubernetes/base/stream/ws_client.py index 3031e01560..ffdd877ef4 100644 --- a/kubernetes/base/stream/ws_client.py +++ b/kubernetes/base/stream/ws_client.py @@ -128,6 +128,7 @@ def readline_channel(self, channel, timeout=None): return b"" if self.binary else "" self.update(timeout=(timeout - time.time() + start)) + return b"" if self.binary else "" def write_channel(self, channel, data): """Write data to a channel.""" @@ -216,11 +217,15 @@ def update(self, timeout=0): if hasattr(select, "poll"): poll = select.poll() poll.register(self.sock.sock, select.POLLIN) - if timeout is not None: + if timeout is not None and timeout != float("inf"): timeout *= 1_000 # poll method uses milliseconds as the time unit + else: + timeout = None r = poll.poll(timeout) poll.unregister(self.sock.sock) else: + if timeout == float("inf"): + timeout = None r, _, _ = select.select( (self.sock.sock, ), (), (), timeout) diff --git a/kubernetes/base/stream/ws_client_test.py b/kubernetes/base/stream/ws_client_test.py index 2785a831fb..80f9c692c5 100644 --- a/kubernetes/base/stream/ws_client_test.py +++ b/kubernetes/base/stream/ws_client_test.py @@ -342,6 +342,48 @@ def test_peek_channel_closed_with_leftover_data(self): self.assertEqual(data3, "") mock_update.assert_not_called() + def test_update_infinite_timeout_polls_without_overflow(self): + """Verify update maps an infinite (default) timeout to a blocking poll instead of overflowing""" + with patch.object(ws_client_module, 'create_websocket') as mock_create, \ + patch('select.poll') as mock_poll: + mock_poll.return_value.poll.return_value = [] + mock_ws = MagicMock() + mock_ws.subprotocol = V5_CHANNEL_PROTOCOL + mock_ws.connected = True + mock_ws.sock.fileno.return_value = 10 + mock_create.return_value = mock_ws + + client = WSClient(self.config_mock, "ws://test", headers=None, capture_all=True) + client.update(timeout=float("inf")) + + mock_poll.return_value.poll.assert_called_once_with(None) + + def test_readline_channel_returns_empty_string_on_expired_timeout(self): + """Verify readline_channel returns '' (not None) when a finite timeout expires""" + with patch.object(ws_client_module, 'create_websocket') as mock_create: + mock_ws = MagicMock() + mock_ws.subprotocol = V5_CHANNEL_PROTOCOL + mock_ws.connected = True + mock_create.return_value = mock_ws + + client = WSClient(self.config_mock, "ws://test", headers=None, capture_all=True, binary=False) + with patch.object(client, 'update'): + line = client.readline_channel(1, timeout=0.01) + self.assertEqual(line, "") + + def test_readline_channel_returns_empty_bytes_on_expired_timeout(self): + """Verify readline_channel returns b'' (not None) when a finite timeout expires in binary mode""" + with patch.object(ws_client_module, 'create_websocket') as mock_create: + mock_ws = MagicMock() + mock_ws.subprotocol = V5_CHANNEL_PROTOCOL + mock_ws.connected = True + mock_create.return_value = mock_ws + + client = WSClient(self.config_mock, "ws://test", headers=None, capture_all=True, binary=True) + with patch.object(client, 'update'): + line = client.readline_channel(1, timeout=0.01) + self.assertEqual(line, b"") + @pytest.fixture(scope="module")