Skip to content

Commit f54625c

Browse files
committed
fix(test): stabilize flaky Windows CI tests
On Windows CI runners, several tests fail intermittently due to timing sensitivity and unhandled warnings from TerminateProcess() cleanup. - Replace single-check file-growth assertions with polling loops inside anyio.fail_after(5) timeouts in process cleanup tests, so slow runners get multiple chances to observe the process has stopped - Increase subprocess.run() timeout from 20s to 60s in test_command_execution to accommodate slow CI runners - Add PytestUnraisableExceptionWarning filter on Windows alongside existing ResourceWarning filters, since Windows TerminateProcess() prevents graceful transport cleanup Github-Issue:#6
1 parent 6c8482c commit f54625c

3 files changed

Lines changed: 47 additions & 27 deletions

File tree

tests/client/test_config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def test_command_execution(mock_config_path: Path):
4444

4545
test_args = [command] + args + ["--help"]
4646

47-
result = subprocess.run(test_args, capture_output=True, text=True, timeout=20, check=False)
47+
result = subprocess.run(test_args, capture_output=True, text=True, timeout=60, check=False)
4848

4949
assert result.returncode == 0
5050
assert "usage" in result.stdout.lower()

tests/client/test_stdio.py

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,9 @@ class TestChildProcessCleanup:
247247

248248
@pytest.mark.anyio
249249
@pytest.mark.filterwarnings("ignore::ResourceWarning" if sys.platform == "win32" else "default")
250+
@pytest.mark.filterwarnings(
251+
"ignore::pytest.PytestUnraisableExceptionWarning" if sys.platform == "win32" else "default"
252+
)
250253
async def test_basic_child_process_cleanup(self):
251254
"""Test basic parent-child process cleanup.
252255
Parent spawns a single child process that writes continuously to a file.
@@ -314,17 +317,16 @@ async def test_basic_child_process_cleanup(self):
314317
print("Terminating process and children...")
315318
await _terminate_process_tree(proc)
316319

317-
# Verify processes stopped
318-
await anyio.sleep(0.5)
319-
if os.path.exists(marker_file): # pragma: no branch
320-
size_after_cleanup = os.path.getsize(marker_file)
321-
await anyio.sleep(0.5)
322-
final_size = os.path.getsize(marker_file)
323-
324-
print(f"After cleanup: file size {size_after_cleanup} -> {final_size}")
325-
assert final_size == size_after_cleanup, (
326-
f"Child process still running! File grew by {final_size - size_after_cleanup} bytes"
327-
)
320+
# Verify processes stopped — poll with retries for slow CI runners
321+
with anyio.fail_after(5):
322+
if os.path.exists(marker_file): # pragma: no branch
323+
while True:
324+
size_after_cleanup = os.path.getsize(marker_file)
325+
await anyio.sleep(0.5)
326+
final_size = os.path.getsize(marker_file)
327+
if final_size == size_after_cleanup:
328+
print(f"After cleanup: file stopped at {final_size} bytes")
329+
break
328330

329331
print("SUCCESS: Child process was properly terminated")
330332

@@ -338,6 +340,9 @@ async def test_basic_child_process_cleanup(self):
338340

339341
@pytest.mark.anyio
340342
@pytest.mark.filterwarnings("ignore::ResourceWarning" if sys.platform == "win32" else "default")
343+
@pytest.mark.filterwarnings(
344+
"ignore::pytest.PytestUnraisableExceptionWarning" if sys.platform == "win32" else "default"
345+
)
341346
async def test_nested_process_tree(self):
342347
"""Test nested process tree cleanup (parent → child → grandchild).
343348
Each level writes to a different file to verify all processes are terminated.
@@ -412,14 +417,20 @@ async def test_nested_process_tree(self):
412417
# Terminate the whole tree
413418
await _terminate_process_tree(proc)
414419

415-
# Verify all stopped
416-
await anyio.sleep(0.5)
417-
for file_path, name in [(parent_file, "parent"), (child_file, "child"), (grandchild_file, "grandchild")]:
418-
if os.path.exists(file_path): # pragma: no branch
419-
size1 = os.path.getsize(file_path)
420-
await anyio.sleep(0.3)
421-
size2 = os.path.getsize(file_path)
422-
assert size1 == size2, f"{name} still writing after cleanup!"
420+
# Verify all stopped — poll with retries for slow CI runners
421+
with anyio.fail_after(5):
422+
for file_path, name in [
423+
(parent_file, "parent"),
424+
(child_file, "child"),
425+
(grandchild_file, "grandchild"),
426+
]:
427+
if os.path.exists(file_path): # pragma: no branch
428+
while True:
429+
size1 = os.path.getsize(file_path)
430+
await anyio.sleep(0.5)
431+
size2 = os.path.getsize(file_path)
432+
if size1 == size2:
433+
break
423434

424435
print("SUCCESS: All processes in tree terminated")
425436

@@ -433,6 +444,9 @@ async def test_nested_process_tree(self):
433444

434445
@pytest.mark.anyio
435446
@pytest.mark.filterwarnings("ignore::ResourceWarning" if sys.platform == "win32" else "default")
447+
@pytest.mark.filterwarnings(
448+
"ignore::pytest.PytestUnraisableExceptionWarning" if sys.platform == "win32" else "default"
449+
)
436450
async def test_early_parent_exit(self):
437451
"""Test cleanup when parent exits during termination sequence.
438452
Tests the race condition where parent might die during our termination
@@ -490,13 +504,15 @@ def handle_term(sig, frame):
490504
# Terminate - this will kill the process group even if parent exits first
491505
await _terminate_process_tree(proc)
492506

493-
# Verify child stopped
494-
await anyio.sleep(0.5)
495-
if os.path.exists(marker_file): # pragma: no branch
496-
size3 = os.path.getsize(marker_file)
497-
await anyio.sleep(0.3)
498-
size4 = os.path.getsize(marker_file)
499-
assert size3 == size4, "Child should be terminated"
507+
# Verify child stopped — poll with retries for slow CI runners
508+
with anyio.fail_after(5):
509+
if os.path.exists(marker_file): # pragma: no branch
510+
while True:
511+
size3 = os.path.getsize(marker_file)
512+
await anyio.sleep(0.5)
513+
size4 = os.path.getsize(marker_file)
514+
if size3 == size4:
515+
break
500516

501517
print("SUCCESS: Child terminated even with parent exit during cleanup")
502518

tests/client/transports/test_memory.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Tests for InMemoryTransport."""
22

3+
import sys
4+
35
import pytest
46

57
from mcp import Client, types
@@ -81,6 +83,8 @@ async def test_list_tools(mcpserver_server: MCPServer):
8183
assert "greet" in tool_names
8284

8385

86+
@pytest.mark.filterwarnings("ignore::ResourceWarning" if sys.platform == "win32" else "default")
87+
@pytest.mark.filterwarnings("ignore::pytest.PytestUnraisableExceptionWarning" if sys.platform == "win32" else "default")
8488
async def test_call_tool(mcpserver_server: MCPServer):
8589
"""Test calling a tool through the transport."""
8690
async with Client(mcpserver_server) as client:

0 commit comments

Comments
 (0)