Detect event-loop blocking in tests (#3510)
Some checks failed
Conformance Tests / server-conformance (push) Has been cancelled
Conformance Tests / client-conformance (push) Has been cancelled
Deploy Docs / deploy-docs (push) Has been cancelled
CI / checks (push) Has been cancelled
GitHub Actions Security Analysis / zizmor (push) Has been cancelled
CI / all-green (push) Has been cancelled

This commit is contained in:
Marcelo Trylesinski 2026-09-16 12:12:27 +02:00 • committed by GitHub
commit 6affe5c0d3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -14,14 +14,18 @@ import math
import os
import signal
import sys
import threading
from collections.abc import Callable
from contextlib import AsyncExitStack, suppress
from pathlib import Path
from types import SimpleNamespace
from typing import TextIO, cast
import anyio
import anyio.abc
import anyio.from_thread
import anyio.lowlevel
import anyio.to_thread
import pytest
import trio
import trio.testing
@ -199,6 +203,9 @@ def install_fake_process(
"""
terminated: list[FakeProcess] = []
async def fake_get_executable_command(command: str) -> str:
return command
async def fake_spawn(
command: str,
args: list[str],
@ -212,6 +219,7 @@ def install_fake_process(
terminated.append(proc)
proc.exit(-15)
monkeypatch.setattr(stdio, "_get_executable_command", fake_get_executable_command)
monkeypatch.setattr(stdio, "_create_platform_compatible_process", fake_spawn)
monkeypatch.setattr(stdio, "_terminate_process_tree", fake_terminate_tree)
if grace_period is not None:
@ -568,6 +576,45 @@ async def test_a_command_that_cannot_be_execed_raises_enoent() -> None:
assert exc_info.value.errno == errno.ENOENT
@pytest.mark.anyio
async def test_cancellation_during_windows_command_resolution_returns_before_resolution_finishes(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Cancelling `stdio_client` does not wait for blocked Windows command resolution."""
resolution_started = anyio.Event()
resolution_release = threading.Event()
resolution_finished = threading.Event()
def blocking_resolver(command: str) -> str:
anyio.from_thread.run_sync(resolution_started.set)
resolution_release.wait()
resolution_finished.set()
return command
monkeypatch.setattr(stdio, "sys", SimpleNamespace(platform="win32"))
monkeypatch.setattr(stdio, "get_windows_executable_command", blocking_resolver)
cancel_scope = anyio.CancelScope()
client_stopped = anyio.Event()
async def run_client() -> None:
with cancel_scope:
async with AsyncExitStack() as stack:
await stack.enter_async_context(stdio_client(FAKE_PARAMS))
client_stopped.set()
with anyio.fail_after(5):
async with anyio.create_task_group() as tg:
tg.start_soon(run_client)
await resolution_started.wait()
cancel_scope.cancel()
try:
await client_stopped.wait()
finally:
resolution_release.set()
await anyio.to_thread.run_sync(resolution_finished.wait)
@pytest.mark.anyio
async def test_cancellation_during_spawn_leaks_no_streams(monkeypatch: pytest.MonkeyPatch) -> None:
"""Cancellation while the spawn is still in flight must not leak the internal streams.