Skip to content

Commit 0e80918

Browse files
committed
Eliminate all coverage pragmas via AsyncExitStack + pytest.raises
Replaced 8 coverage pragmas with restructuring: - pytest.fail() # pragma: no cover → pytest.raises() — if the child survives, pytest.raises fails with "DID NOT RAISE" (library handles it) - if proc.stdin is not None: # pragma: no branch → assert proc.stdin is not None — satisfies pyright Optional narrowing; asserts are statements not branches, so coverage doesn't need both paths - if proc is not None: # pragma: no branch (×3 in finally blocks) → AsyncExitStack.push_async_callback(_terminate_and_reap, proc) immediately after proc is created. Cleanup is guaranteed, no None-check needed. - if stream is not None: # pragma: no branch (×2 in finally blocks) → Same: push stream.aclose as a callback right after accept() Also dropped the # noqa: PERF401 — the accept loop now does stream acquisition + cleanup registration + list append (3 ops), which doesn't match PERF401's single-append pattern. Net: all 3 tests are single-indent, zero try/finally boilerplate, zero pragmas, zero noqa. Still 100% coverage, ~0.5s for all 3 tests. Github-Issue: #1775
1 parent 160999f commit 0e80918

File tree

1 file changed

+28
-48
lines changed

1 file changed

+28
-48
lines changed

tests/client/test_stdio.py

Lines changed: 28 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import sys
44
import textwrap
55
import time
6+
from contextlib import AsyncExitStack
67

78
import anyio
89
import anyio.abc
@@ -302,14 +303,8 @@ async def _assert_stream_closed(stream: anyio.abc.SocketStream) -> None:
302303
Either is a deterministic, kernel-level signal that the process is dead —
303304
no sleeps or polling required.
304305
"""
305-
with anyio.fail_after(5.0):
306-
try:
307-
data = await stream.receive(1)
308-
except (anyio.EndOfStream, anyio.BrokenResourceError):
309-
return
310-
pytest.fail( # pragma: no cover
311-
f"subprocess still alive after _terminate_process_tree (received {data!r})"
312-
)
306+
with anyio.fail_after(5.0), pytest.raises((anyio.EndOfStream, anyio.BrokenResourceError)):
307+
await stream.receive(1)
313308

314309

315310
async def _terminate_and_reap(proc: anyio.abc.Process | FallbackProcess) -> None:
@@ -332,10 +327,10 @@ async def _terminate_and_reap(proc: anyio.abc.Process | FallbackProcess) -> None
332327
with anyio.move_on_after(5.0):
333328
await _terminate_process_tree(proc)
334329
await proc.wait()
335-
if proc.stdin is not None: # pragma: no branch
336-
await proc.stdin.aclose()
337-
if proc.stdout is not None: # pragma: no branch
338-
await proc.stdout.aclose()
330+
assert proc.stdin is not None
331+
assert proc.stdout is not None
332+
await proc.stdin.aclose()
333+
await proc.stdout.aclose()
339334

340335

341336
class TestChildProcessCleanup:
@@ -348,39 +343,34 @@ class TestChildProcessCleanup:
348343
@pytest.mark.filterwarnings("ignore::ResourceWarning" if sys.platform == "win32" else "default")
349344
async def test_basic_child_process_cleanup(self):
350345
"""Parent spawns one child; terminating the tree kills both."""
351-
sock, port = await _open_liveness_listener()
352-
stream: anyio.abc.SocketStream | None = None
353-
proc = None
354-
try:
346+
async with AsyncExitStack() as stack:
347+
sock, port = await _open_liveness_listener()
348+
stack.push_async_callback(sock.aclose)
349+
355350
# Parent spawns a child; the child connects back to us.
356351
parent_script = _spawn_then_block(_connect_back_script(port))
357352
proc = await _create_platform_compatible_process(sys.executable, ["-c", parent_script])
353+
stack.push_async_callback(_terminate_and_reap, proc)
358354

359355
# Deterministic: accept() blocks until the child connects. No sleep.
360356
with anyio.fail_after(10.0):
361357
stream = await _accept_alive(sock)
358+
stack.push_async_callback(stream.aclose)
362359

363360
# Terminate the process tree (the behavior under test).
364361
await _terminate_process_tree(proc)
365362

366363
# Deterministic: kernel closed child's socket when it died.
367364
await _assert_stream_closed(stream)
368365

369-
finally:
370-
if proc is not None: # pragma: no branch
371-
await _terminate_and_reap(proc)
372-
if stream is not None: # pragma: no branch
373-
await stream.aclose()
374-
await sock.aclose()
375-
376366
@pytest.mark.anyio
377367
@pytest.mark.filterwarnings("ignore::ResourceWarning" if sys.platform == "win32" else "default")
378368
async def test_nested_process_tree(self):
379369
"""Parent → child → grandchild; terminating the tree kills all three."""
380-
sock, port = await _open_liveness_listener()
381-
streams: list[anyio.abc.SocketStream] = []
382-
proc = None
383-
try:
370+
async with AsyncExitStack() as stack:
371+
sock, port = await _open_liveness_listener()
372+
stack.push_async_callback(sock.aclose)
373+
384374
# Build a three-level chain: parent spawns child, child spawns
385375
# grandchild. Every level connects back to our socket.
386376
grandchild = _connect_back_script(port)
@@ -393,13 +383,15 @@ async def test_nested_process_tree(self):
393383
f"subprocess.Popen([sys.executable, '-c', {child!r}])\n" + _connect_back_script(port)
394384
)
395385
proc = await _create_platform_compatible_process(sys.executable, ["-c", parent_script])
386+
stack.push_async_callback(_terminate_and_reap, proc)
396387

397388
# Deterministic: three blocking accepts, one per tree level.
389+
streams: list[anyio.abc.SocketStream] = []
398390
with anyio.fail_after(10.0):
399391
for _ in range(3):
400-
# Append-in-loop intentional: preserves partially-accepted
401-
# streams for cleanup in `finally` if a later accept fails.
402-
streams.append(await _accept_alive(sock)) # noqa: PERF401
392+
stream = await _accept_alive(sock)
393+
stack.push_async_callback(stream.aclose)
394+
streams.append(stream)
403395

404396
# Terminate the entire tree.
405397
await _terminate_process_tree(proc)
@@ -408,23 +400,16 @@ async def test_nested_process_tree(self):
408400
for stream in streams:
409401
await _assert_stream_closed(stream)
410402

411-
finally:
412-
if proc is not None: # pragma: no branch
413-
await _terminate_and_reap(proc)
414-
for stream in streams:
415-
await stream.aclose()
416-
await sock.aclose()
417-
418403
@pytest.mark.anyio
419404
@pytest.mark.filterwarnings("ignore::ResourceWarning" if sys.platform == "win32" else "default")
420405
async def test_early_parent_exit(self):
421406
"""Parent exits immediately on SIGTERM; process-group termination still
422407
catches the child (exercises the race where the parent dies mid-cleanup).
423408
"""
424-
sock, port = await _open_liveness_listener()
425-
stream: anyio.abc.SocketStream | None = None
426-
proc = None
427-
try:
409+
async with AsyncExitStack() as stack:
410+
sock, port = await _open_liveness_listener()
411+
stack.push_async_callback(sock.aclose)
412+
428413
# Parent installs a SIGTERM handler that exits immediately, spawns a
429414
# child that connects back to us, then blocks.
430415
child = _connect_back_script(port)
@@ -435,10 +420,12 @@ async def test_early_parent_exit(self):
435420
f"time.sleep(3600)\n"
436421
)
437422
proc = await _create_platform_compatible_process(sys.executable, ["-c", parent_script])
423+
stack.push_async_callback(_terminate_and_reap, proc)
438424

439425
# Deterministic: child connected means both parent and child are up.
440426
with anyio.fail_after(10.0):
441427
stream = await _accept_alive(sock)
428+
stack.push_async_callback(stream.aclose)
442429

443430
# Parent will sys.exit(0) on SIGTERM, but the process-group kill
444431
# (POSIX killpg / Windows Job Object) must still terminate the child.
@@ -447,13 +434,6 @@ async def test_early_parent_exit(self):
447434
# Child must be dead despite parent's early exit.
448435
await _assert_stream_closed(stream)
449436

450-
finally:
451-
if proc is not None: # pragma: no branch
452-
await _terminate_and_reap(proc)
453-
if stream is not None: # pragma: no branch
454-
await stream.aclose()
455-
await sock.aclose()
456-
457437

458438
@pytest.mark.anyio
459439
async def test_stdio_client_graceful_stdin_exit():

0 commit comments

Comments
 (0)