|
16 | 16 | import inspect
|
17 | 17 | from typing import Any, Callable, Mapping, Optional
|
18 | 18 | from unittest.mock import AsyncMock, Mock, patch
|
| 19 | +from asyncio import run_coroutine_threadsafe |
19 | 20 |
|
20 | 21 | import pytest
|
21 | 22 | from aioresponses import CallbackResult, aioresponses
|
|
27 | 28 |
|
28 | 29 | TEST_BASE_URL = "http://toolbox.example.com"
|
29 | 30 |
|
| 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) |
30 | 54 |
|
31 | 55 | @pytest.fixture
|
32 | 56 | def sync_client_environment():
|
@@ -288,30 +312,6 @@ def test_sync_client_creation_in_isolated_env(self, sync_client):
|
288 | 312 | sync_client._ToolboxSyncClient__async_client, ToolboxClient
|
289 | 313 | ), "Async client should be ToolboxClient instance"
|
290 | 314 |
|
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 |
| - |
315 | 315 | @pytest.mark.usefixtures("sync_client_environment")
|
316 | 316 | def test_sync_client_context_manager(self, aioresponses, tool_schema_minimal):
|
317 | 317 | """
|
|
0 commit comments