Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions tests/client/test_input_required.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@
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))
Expand Down
8 changes: 8 additions & 0 deletions tests/client/test_stdio.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@
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
# ---------------------------------------------------------------------------
Expand Down
30 changes: 27 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import os
from collections.abc import Iterator
from collections.abc import AsyncIterator, Iterator

import pytest

Expand All @@ -18,11 +18,35 @@
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.
Expand Down
5 changes: 5 additions & 0 deletions tests/interaction/lowlevel/test_timeouts.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
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:
Expand Down
5 changes: 5 additions & 0 deletions tests/server/test_streamable_http_modern.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@
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."""
Expand Down
5 changes: 5 additions & 0 deletions tests/shared/test_jsonrpc_dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@
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."""

Expand Down
5 changes: 5 additions & 0 deletions tests/transports/stdio/test_windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@
]


@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],
Expand Down
Loading