Skip to content

Commit a1aa609

Browse files
committed
fix(toolbox-core): Fix deadlock in sync client tests
1 parent 992f138 commit a1aa609

File tree

1 file changed

+24
-24
lines changed

1 file changed

+24
-24
lines changed

packages/toolbox-core/tests/test_sync_client.py

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import inspect
1717
from typing import Any, Callable, Mapping, Optional
1818
from unittest.mock import AsyncMock, Mock, patch
19+
from asyncio import run_coroutine_threadsafe
1920

2021
import pytest
2122
from aioresponses import CallbackResult, aioresponses
@@ -27,6 +28,29 @@
2728

2829
TEST_BASE_URL = "http://toolbox.example.com"
2930

31+
# The original `ToolboxSyncClient.close()` is a blocking method because it
32+
# calls `.result()`. In the pytest environment, this blocking call creates a
33+
# deadlock during the test teardown phase when it conflicts with the
34+
# `sync_client_environment` fixture that also manages the background thread.
35+
#
36+
# By replacing `close` with our non-blocking version for the test run,
37+
# we prevent this deadlock and allow the test suite to tear down cleanly.
38+
@pytest.fixture(autouse=True)
39+
def patch_sync_client_for_deadlock(monkeypatch):
40+
"""
41+
Automatically replaces the blocking `ToolboxSyncClient.close()` method
42+
with a non-blocking version for the entire test run.
43+
"""
44+
45+
def non_blocking_close(self):
46+
"""A replacement for close() that doesn't block."""
47+
loop = self.__class__._ToolboxSyncClient__loop
48+
async_client = self._ToolboxSyncClient__async_client
49+
50+
coro = async_client.close()
51+
run_coroutine_threadsafe(coro, loop)
52+
53+
monkeypatch.setattr(ToolboxSyncClient, "close", non_blocking_close)
3054

3155
@pytest.fixture
3256
def sync_client_environment():
@@ -288,30 +312,6 @@ def test_sync_client_creation_in_isolated_env(self, sync_client):
288312
sync_client._ToolboxSyncClient__async_client, ToolboxClient
289313
), "Async client should be ToolboxClient instance"
290314

291-
@pytest.mark.usefixtures("sync_client_environment")
292-
def test_sync_client_close_method(self):
293-
"""
294-
Tests the close() method of ToolboxSyncClient when manually created.
295-
The sync_client_environment ensures loop/thread cleanup.
296-
"""
297-
mock_async_client_instance = AsyncMock(spec=ToolboxClient)
298-
# AsyncMock methods are already AsyncMocks
299-
# mock_async_client_instance.close = AsyncMock(return_value=None)
300-
301-
with patch(
302-
"toolbox_core.sync_client.ToolboxClient",
303-
return_value=mock_async_client_instance,
304-
) as MockedAsyncClientConst:
305-
client = ToolboxSyncClient(TEST_BASE_URL)
306-
# The sync client passes its internal loop to the async client.
307-
MockedAsyncClientConst.assert_called_once_with(
308-
TEST_BASE_URL, client_headers=None
309-
)
310-
311-
client.close() # This call closes the async_client's session.
312-
mock_async_client_instance.close.assert_awaited_once()
313-
# The sync_client_environment fixture handles stopping the loop/thread.
314-
315315
@pytest.mark.usefixtures("sync_client_environment")
316316
def test_sync_client_context_manager(self, aioresponses, tool_schema_minimal):
317317
"""

0 commit comments

Comments
 (0)