Skip to content

Commit ba0c95d

Browse files
author
Sebastian
committed
Async safety unit test
Signed-off-by: Sebastian <[email protected]>
1 parent 3d8fe70 commit ba0c95d

File tree

1 file changed

+139
-0
lines changed

1 file changed

+139
-0
lines changed

tests/async/test_async_safety.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
"""
2+
Comprehensive async safety tests for mcpgateway.
3+
"""
4+
5+
import pytest
6+
import asyncio
7+
import warnings
8+
import time
9+
from unittest.mock import AsyncMock, patch
10+
11+
12+
class TestAsyncSafety:
13+
"""Test async safety and proper coroutine handling."""
14+
15+
def test_no_unawaited_coroutines(self):
16+
"""Test that no coroutines are left unawaited."""
17+
18+
# Capture async warnings
19+
with warnings.catch_warnings(record=True) as caught_warnings:
20+
warnings.simplefilter("always")
21+
22+
# Run async code that might have unawaited coroutines
23+
asyncio.run(self._test_async_operations())
24+
25+
# Check for unawaited coroutine warnings
26+
unawaited_warnings = [w for w in caught_warnings if "coroutine" in str(w.message) and "never awaited" in str(w.message)]
27+
28+
assert len(unawaited_warnings) == 0, f"Found {len(unawaited_warnings)} unawaited coroutines"
29+
30+
async def _test_async_operations(self):
31+
"""Test various async operations for safety."""
32+
33+
# Test WebSocket operations
34+
await self._test_websocket_safety()
35+
36+
# Test database operations
37+
await self._test_database_safety()
38+
39+
# Test MCP operations
40+
await self._test_mcp_safety()
41+
42+
async def _test_websocket_safety(self):
43+
"""Test WebSocket async safety."""
44+
45+
# Mock WebSocket operations
46+
with patch("websockets.connect") as mock_connect:
47+
mock_websocket = AsyncMock()
48+
mock_connect.return_value.__aenter__.return_value = mock_websocket
49+
50+
# Test proper awaiting
51+
async with mock_connect("ws://test") as websocket:
52+
await websocket.send("test")
53+
await websocket.recv()
54+
55+
async def _test_database_safety(self):
56+
"""Test database async safety."""
57+
58+
# Mock database operations
59+
with patch("asyncpg.connect") as mock_connect:
60+
mock_connection = AsyncMock()
61+
mock_connect.return_value = mock_connection
62+
63+
# Test proper connection handling
64+
connection = await mock_connect("postgresql://test")
65+
await connection.execute("SELECT 1")
66+
await connection.close()
67+
68+
async def _test_mcp_safety(self):
69+
"""Test MCP async safety."""
70+
71+
# Mock MCP operations
72+
with patch("aiohttp.ClientSession") as mock_session:
73+
mock_response = AsyncMock()
74+
mock_session.return_value.post.return_value.__aenter__.return_value = mock_response
75+
76+
# Test proper session handling
77+
async with mock_session() as session:
78+
async with session.post("http://test") as response:
79+
await response.json()
80+
81+
@pytest.mark.asyncio
82+
async def test_concurrent_operations_performance(self):
83+
"""Test performance of concurrent async operations."""
84+
85+
async def mock_operation():
86+
await asyncio.sleep(0.01) # 10ms operation
87+
return "result"
88+
89+
# Test concurrent execution
90+
start_time = time.time()
91+
92+
tasks = [mock_operation() for _ in range(100)]
93+
results = await asyncio.gather(*tasks)
94+
95+
end_time = time.time()
96+
97+
# Should complete in roughly 10ms, not 1000ms (100 * 10ms)
98+
assert end_time - start_time < 0.1, "Concurrent operations not properly parallelized"
99+
assert len(results) == 100, "Not all operations completed"
100+
101+
@pytest.mark.asyncio
102+
async def test_task_cleanup(self):
103+
"""Test proper task cleanup and no task leaks."""
104+
105+
initial_tasks = len(asyncio.all_tasks())
106+
107+
async def background_task():
108+
await asyncio.sleep(0.1)
109+
110+
# Create and properly manage tasks
111+
tasks = []
112+
for _ in range(10):
113+
task = asyncio.create_task(background_task())
114+
tasks.append(task)
115+
116+
# Wait for completion
117+
await asyncio.gather(*tasks)
118+
119+
# Check no leaked tasks
120+
final_tasks = len(asyncio.all_tasks())
121+
122+
# Allow for some variation but no significant leaks
123+
assert final_tasks <= initial_tasks + 2, "Task leak detected"
124+
125+
@pytest.mark.asyncio
126+
async def test_exception_handling_in_async(self):
127+
"""Test proper exception handling in async operations."""
128+
129+
async def failing_operation():
130+
await asyncio.sleep(0.01)
131+
raise ValueError("Test error")
132+
133+
# Test exception handling doesn't break event loop
134+
with pytest.raises(ValueError):
135+
await failing_operation()
136+
137+
# Event loop should still be functional
138+
await asyncio.sleep(0.01)
139+
assert True, "Event loop functional after exception"

0 commit comments

Comments
 (0)