From 0d6c126c7b0cf674dbe189702de651196537f512 Mon Sep 17 00:00:00 2001 From: BabyChrist666 Date: Mon, 16 Feb 2026 20:06:18 -0500 Subject: [PATCH 1/2] fix: replace fixed sleeps with polling in TestChildProcessCleanup (#1775) The three flaky tests in TestChildProcessCleanup fail intermittently on macOS CI because fixed-duration sleeps (0.5s-1.0s) aren't enough for child processes to start writing on slow CI machines. Replace the fixed `anyio.sleep()` + size assertion pattern with a `_wait_for_file_growth()` helper that polls until the file exists, has content, and is actively growing, with a 10s timeout. This eliminates the race condition where assertions fire before processes have started. Co-Authored-By: Claude Opus 4.6 --- tests/client/test_stdio.py | 67 ++++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/tests/client/test_stdio.py b/tests/client/test_stdio.py index f70c24eee..ce57497e9 100644 --- a/tests/client/test_stdio.py +++ b/tests/client/test_stdio.py @@ -221,6 +221,32 @@ def sigint_handler(signum, frame): raise +async def _wait_for_file_growth( + file_path: str, + description: str = "process", + timeout: float = 10.0, + poll_interval: float = 0.2, +) -> None: + """Wait until a file exists, has content, and is actively growing. + + Polls the file size at regular intervals until it observes growth, + raising AssertionError if the timeout is exceeded. This replaces + fixed-duration sleeps that cause flaky tests on slow CI machines. + """ + deadline = time.monotonic() + timeout + prev_size = -1 + + while time.monotonic() < deadline: + if os.path.exists(file_path): + size = os.path.getsize(file_path) + if size > 0 and prev_size >= 0 and size > prev_size: + return # File is growing + prev_size = size + await anyio.sleep(poll_interval) + + raise AssertionError(f"{description} did not start writing within {timeout}s") + + class TestChildProcessCleanup: """Tests for child process cleanup functionality using _terminate_process_tree. @@ -296,19 +322,9 @@ async def test_basic_child_process_cleanup(self): # Start the parent process proc = await _create_platform_compatible_process(sys.executable, ["-c", parent_script]) - # Wait for processes to start - await anyio.sleep(0.5) - - # Verify parent started - assert os.path.exists(parent_marker), "Parent process didn't start" - - # Verify child is writing - if os.path.exists(marker_file): # pragma: no branch - initial_size = os.path.getsize(marker_file) - await anyio.sleep(0.3) - size_after_wait = os.path.getsize(marker_file) - assert size_after_wait > initial_size, "Child process should be writing" - print(f"Child is writing (file grew from {initial_size} to {size_after_wait} bytes)") + # Wait for parent and child to start (poll instead of fixed sleep) + await _wait_for_file_growth(parent_marker, "parent process") + await _wait_for_file_growth(marker_file, "child process") # Terminate using our function print("Terminating process and children...") @@ -398,16 +414,10 @@ async def test_nested_process_tree(self): # Start the parent process proc = await _create_platform_compatible_process(sys.executable, ["-c", parent_script]) - # Let all processes start - await anyio.sleep(1.0) - - # Verify all are writing - for file_path, name in [(parent_file, "parent"), (child_file, "child"), (grandchild_file, "grandchild")]: - if os.path.exists(file_path): # pragma: no branch - initial_size = os.path.getsize(file_path) - await anyio.sleep(0.3) - new_size = os.path.getsize(file_path) - assert new_size > initial_size, f"{name} process should be writing" + # Wait for all processes to start writing (poll instead of fixed sleep) + await _wait_for_file_growth(parent_file, "parent process") + await _wait_for_file_growth(child_file, "child process") + await _wait_for_file_growth(grandchild_file, "grandchild process") # Terminate the whole tree await _terminate_process_tree(proc) @@ -477,15 +487,8 @@ def handle_term(sig, frame): # Start the parent process proc = await _create_platform_compatible_process(sys.executable, ["-c", parent_script]) - # Let child start writing - await anyio.sleep(0.5) - - # Verify child is writing - if os.path.exists(marker_file): # pragma: no branch - size1 = os.path.getsize(marker_file) - await anyio.sleep(0.3) - size2 = os.path.getsize(marker_file) - assert size2 > size1, "Child should be writing" + # Wait for child to start writing (poll instead of fixed sleep) + await _wait_for_file_growth(marker_file, "child process") # Terminate - this will kill the process group even if parent exits first await _terminate_process_tree(proc) From ca3910a674395f64c560aa73778d4412b8d1c648 Mon Sep 17 00:00:00 2001 From: BabyChrist666 Date: Mon, 16 Feb 2026 20:14:58 -0500 Subject: [PATCH 2/2] fix: add coverage pragmas to _wait_for_file_growth safety paths The timeout raise and file-not-yet-exists branch are safety nets that don't execute in normal test runs. Mark them with coverage pragmas to satisfy the 100% coverage requirement. Co-Authored-By: Claude Opus 4.6 --- tests/client/test_stdio.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/client/test_stdio.py b/tests/client/test_stdio.py index ce57497e9..e240fb7e4 100644 --- a/tests/client/test_stdio.py +++ b/tests/client/test_stdio.py @@ -237,14 +237,14 @@ async def _wait_for_file_growth( prev_size = -1 while time.monotonic() < deadline: - if os.path.exists(file_path): + if os.path.exists(file_path): # pragma: no branch size = os.path.getsize(file_path) if size > 0 and prev_size >= 0 and size > prev_size: return # File is growing prev_size = size await anyio.sleep(poll_interval) - raise AssertionError(f"{description} did not start writing within {timeout}s") + raise AssertionError(f"{description} did not start writing within {timeout}s") # pragma: no cover class TestChildProcessCleanup: