Skip to content

Commit dcc8afa

Browse files
committed
Detect event-loop blocking in tests
1 parent 9972c21 commit dcc8afa

4 files changed

Lines changed: 54 additions & 3 deletions

File tree

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ dev = [
7777
"strict-no-cover",
7878
"logfire>=3.0.0",
7979
"opentelemetry-sdk>=1.39.1",
80+
# BlockBuster minor releases may add breaking detection rules.
81+
"blockbuster>=1.5.26,<1.6",
8082
]
8183
docs = [
8284
# Zensical is the Material team's successor to MkDocs; it natively

src/mcp/client/session.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import anyio
1313
import anyio.abc
1414
import anyio.lowlevel
15+
import anyio.to_thread
1516
import mcp_types as types
1617
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
1718
from mcp_types import (
@@ -1133,12 +1134,16 @@ async def validate_tool_result(self, name: str, result: types.CallToolResult) ->
11331134
logger.warning(f"Tool {name} not listed by server, cannot validate any structured content")
11341135

11351136
if output_schema is not None:
1137+
if result.structured_content is None:
1138+
raise RuntimeError(f"Tool {name} has an output schema but did not return structured content")
1139+
validator = self._tool_output_validators.get(name)
1140+
if validator is None:
1141+
# First compilation lazily reads jsonschema's bundled schemas.
1142+
validator = await anyio.to_thread.run_sync(self._output_schema_validator, name, output_schema)
1143+
11361144
from jsonschema import exceptions as jsonschema_exceptions
11371145
from referencing.exceptions import Unresolvable
11381146

1139-
if result.structured_content is None:
1140-
raise RuntimeError(f"Tool {name} has an output schema but did not return structured content")
1141-
validator = self._output_schema_validator(name, output_schema)
11421147
# `best_match` picks the same error the previous `jsonschema.validate()` call raised,
11431148
# so the message a caller sees is unchanged. It is untyped upstream.
11441149
errors = validator.iter_errors(result.structured_content)

tests/conftest.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import os
22
from collections.abc import AsyncIterator, Iterator
33

4+
import httpcore2 as _httpcore2
45
import pytest
6+
from blockbuster import BlockBuster
57

68
# OpenTelemetry's `set_tracer_provider` is set-once per process, so the suite
79
# uses a single span-capture mechanism: logfire's `capfire` fixture (its
@@ -17,12 +19,34 @@
1719

1820
import mcp.shared._otel # noqa: E402
1921

22+
# Load httpx2's lazy default transport before BlockBuster starts.
23+
del _httpcore2
24+
2025

2126
@pytest.fixture(scope="session")
2227
def anyio_backend() -> str:
2328
return "asyncio"
2429

2530

31+
_BLOCKBUSTER = BlockBuster(["mcp", "mcp_types"])
32+
# Coverage reads source files while collecting data.
33+
_BLOCKBUSTER.functions["os.stat"].can_block_in("coverage/python.py", "get_python_source")
34+
_BLOCKBUSTER.functions["io.BufferedReader.read"].can_block_in("coverage/python.py", "read_python_source")
35+
# These public synchronous conversions read the media file by design.
36+
_BLOCKBUSTER.functions["io.BufferedReader.read"].can_block_in(
37+
"mcp/server/mcpserver/utilities/types.py", ("to_image_content", "to_audio_content")
38+
)
39+
40+
41+
@pytest.fixture(autouse=True)
42+
def blockbuster() -> Iterator[BlockBuster]:
43+
try:
44+
_BLOCKBUSTER.activate()
45+
yield _BLOCKBUSTER
46+
finally:
47+
_BLOCKBUSTER.deactivate()
48+
49+
2650
@pytest.fixture(scope="module", autouse=True)
2751
async def _module_runner_lease(anyio_backend: str) -> AsyncIterator[None]:
2852
"""Share one event loop across each module's tests instead of one per test.

uv.lock

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)