Share one event loop per test module to stop Windows socketpair churn (#3070)
This commit is contained in:
parent
d287c9868f
commit
867bba6263
7 changed files with 60 additions and 3 deletions
|
|
@ -37,6 +37,11 @@ from mcp.client._input_required import (
|
|||
pytestmark = pytest.mark.anyio
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _module_runner_lease() -> None:
|
||||
"""Opt out of the shared per-module event loop: this module parametrizes `anyio_backend`."""
|
||||
|
||||
|
||||
def _elicit(message: str = "What is your name?") -> ElicitRequest:
|
||||
schema = {"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]}
|
||||
return ElicitRequest(params=ElicitRequestFormParams(message=message, requested_schema=schema))
|
||||
|
|
|
|||
|
|
@ -44,6 +44,14 @@ from mcp.os.win32.utilities import FallbackProcess
|
|||
from mcp.shared.exceptions import MCPError
|
||||
from mcp.shared.message import SessionMessage
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _module_runner_lease() -> None:
|
||||
"""Opt out of the shared per-module event loop: this module parametrizes `anyio_backend`
|
||||
and calls `trio.run` directly (see the tests/conftest.py original for the Windows hazard).
|
||||
"""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# In-process fake of the spawned server process
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import os
|
||||
from collections.abc import Iterator
|
||||
from collections.abc import AsyncIterator, Iterator
|
||||
|
||||
import pytest
|
||||
|
||||
|
|
@ -18,11 +18,35 @@ from logfire.testing import CaptureLogfire # noqa: E402
|
|||
import mcp.shared._otel # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def anyio_backend():
|
||||
@pytest.fixture(scope="session")
|
||||
def anyio_backend() -> str:
|
||||
return "asyncio"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
async def _module_runner_lease(anyio_backend: str) -> AsyncIterator[None]:
|
||||
"""Share one event loop across each module's tests instead of one per test.
|
||||
|
||||
anyio's pytest plugin tears its runner down whenever the last lease is
|
||||
released, so with only function-scoped async fixtures every async test
|
||||
creates and destroys its own event loop. On Windows each loop's self-pipe
|
||||
is an emulated loopback-TCP socketpair, and churning thousands of those per
|
||||
run can transiently exhaust kernel socket buffers — surfacing in CI as
|
||||
`OSError: [WinError 10055]` raised from `asyncio.new_event_loop()` before
|
||||
an arbitrary test's body even starts. Holding a module-scoped lease caps
|
||||
the churn at one loop per module per xdist worker.
|
||||
|
||||
Modules that parametrize `anyio_backend` or call `trio.run(...)` directly
|
||||
must shadow this fixture with a sync no-op: a module-scoped lease cannot
|
||||
depend on the function-scoped parameter (pytest raises ScopeMismatch at
|
||||
setup), and the lease's live asyncio loop lingers over direct trio runs,
|
||||
whose signal handling collides with the loop's wakeup fd on Windows. The
|
||||
lease also makes sniffio report asyncio to the module's sync tests, so a
|
||||
sync test must not call `anyio.run()` itself.
|
||||
"""
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(name="capfire")
|
||||
def _capfire_isolated(capfire: CaptureLogfire) -> Iterator[CaptureLogfire]:
|
||||
"""Override of logfire's `capfire` that scopes the MCP tracer to the test.
|
||||
|
|
|
|||
|
|
@ -27,6 +27,11 @@ from tests.interaction._requirements import requirement
|
|||
pytestmark = pytest.mark.anyio
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _module_runner_lease() -> None:
|
||||
"""Opt out of the shared per-module event loop: this module parametrizes `anyio_backend`."""
|
||||
|
||||
|
||||
@requirement("protocol:timeout:basic")
|
||||
@requirement("protocol:timeout:sends-cancellation")
|
||||
async def test_request_timeout_fails_the_pending_call() -> None:
|
||||
|
|
|
|||
|
|
@ -55,6 +55,11 @@ from tests.interaction.transports import StreamingASGITransport
|
|||
pytestmark = pytest.mark.anyio
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _module_runner_lease() -> None:
|
||||
"""Opt out of the shared per-module event loop: this module parametrizes `anyio_backend`."""
|
||||
|
||||
|
||||
async def test_single_exchange_dispatch_context_has_no_back_channel() -> None:
|
||||
"""The per-request dispatch context refuses server-initiated requests; without an SSE sink,
|
||||
notify/progress are no-ops."""
|
||||
|
|
|
|||
|
|
@ -51,6 +51,11 @@ from .test_dispatcher import Recorder, echo_handlers, running_pair
|
|||
DCtx = DispatchContext[TransportContext]
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _module_runner_lease() -> None:
|
||||
"""Opt out of the shared per-module event loop: this module parametrizes `anyio_backend`."""
|
||||
|
||||
|
||||
class RecordingWriteStream:
|
||||
"""Records sends without a checkpoint, so a pending cancellation cannot interrupt the write or mask it."""
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,11 @@ pytestmark = [
|
|||
]
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _module_runner_lease() -> None:
|
||||
"""Opt out of the shared per-module event loop: this module parametrizes `anyio_backend`."""
|
||||
|
||||
|
||||
async def test_a_gracefully_exited_servers_child_is_reaped_when_the_job_handle_closes( # pragma: no cover
|
||||
tmp_path: Path,
|
||||
spawned_processes: list[anyio.abc.Process | FallbackProcess],
|
||||
|
|
|
|||
Loading…
Reference in a new issue