Skip to content

Commit a57411a

Browse files
committed
chore(toolbox-core): Fix deadlock issue in e2e tests
1 parent 215d6d1 commit a57411a

File tree

2 files changed

+35
-26
lines changed

2 files changed

+35
-26
lines changed

packages/toolbox-core/tests/conftest.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,16 @@
2222
import subprocess
2323
import tempfile
2424
import time
25+
from asyncio import run_coroutine_threadsafe
2526
from typing import Generator
2627

2728
import google
2829
import pytest_asyncio
2930
from google.auth import compute_engine
3031
from google.cloud import secretmanager, storage
3132

33+
from toolbox_core import ToolboxSyncClient
34+
3235

3336
#### Define Utility Functions
3437
def get_env_var(key: str) -> str:
@@ -92,6 +95,38 @@ def get_auth_token(client_id: str) -> str:
9295

9396

9497
#### Define Fixtures
98+
@pytest_asyncio.fixture(autouse=True)
99+
def patch_sync_client_for_deadlock(monkeypatch):
100+
"""
101+
Automatically replaces the blocking `ToolboxSyncClient.close()` method
102+
with a non-blocking version for the entire test run.
103+
104+
The original `ToolboxSyncClient.close()` is a blocking method because it
105+
calls `.result()`. In the pytest environment, this blocking call creates a
106+
deadlock during the test teardown phase when it conflicts with other fixtures
107+
(like `sync_client_environment` or `toolbox_server`) that are also managing
108+
background processes and threads.
109+
110+
By replacing `close` with this safe, non-blocking version, we prevent the
111+
deadlock and allow the test suite's fixtures to tear down cleanly.
112+
This change is only active during the test run.
113+
"""
114+
115+
def non_blocking_close(self):
116+
"""A replacement for close() that doesn't block."""
117+
if hasattr(self.__class__, "_ToolboxSyncClient__loop") and hasattr(
118+
self, "_ToolboxSyncClient__async_client"
119+
):
120+
loop = self.__class__._ToolboxSyncClient__loop
121+
async_client = self._ToolboxSyncClient__async_client
122+
123+
if loop and loop.is_running():
124+
coro = async_client.close()
125+
run_coroutine_threadsafe(coro, loop)
126+
127+
monkeypatch.setattr(ToolboxSyncClient, "close", non_blocking_close)
128+
129+
95130
@pytest_asyncio.fixture(scope="session")
96131
def project_id() -> str:
97132
return get_env_var("GOOGLE_CLOUD_PROJECT")

packages/toolbox-core/tests/test_sync_client.py

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515

1616
import inspect
17-
from asyncio import run_coroutine_threadsafe
1817
from typing import Any, Callable, Mapping, Optional
1918
from unittest.mock import AsyncMock, Mock, patch
2019

@@ -29,31 +28,6 @@
2928
TEST_BASE_URL = "http://toolbox.example.com"
3029

3130

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

0 commit comments

Comments
 (0)