From f53793d354268dd7621cec2bb9e8fcb956785a00 Mon Sep 17 00:00:00 2001 From: perhapzz Date: Wed, 25 Mar 2026 10:34:35 +0000 Subject: [PATCH 1/6] test: convert context_aware_server from multiprocessing to in-process threads Replace multiprocessing.Process with threading.Thread for the context_aware_server fixture so coverage.py can track server-side code. - Remove 5 pragma: no cover markers now reachable by coverage - Graceful shutdown via server.should_exit instead of proc.kill() - All 61 tests pass, pyright/ruff clean Part of #1678 --- tests/shared/test_streamable_http.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/tests/shared/test_streamable_http.py b/tests/shared/test_streamable_http.py index f8ca30441..c730268fa 100644 --- a/tests/shared/test_streamable_http.py +++ b/tests/shared/test_streamable_http.py @@ -8,6 +8,7 @@ import json import multiprocessing import socket +import threading import time import traceback from collections.abc import AsyncIterator, Generator @@ -1462,7 +1463,7 @@ async def sampling_callback( # Context-aware server implementation for testing request context propagation -async def _handle_context_list_tools( # pragma: no cover +async def _handle_context_list_tools( ctx: ServerRequestContext, params: PaginatedRequestParams | None ) -> ListToolsResult: return ListToolsResult( @@ -1487,9 +1488,7 @@ async def _handle_context_list_tools( # pragma: no cover ) -async def _handle_context_call_tool( # pragma: no cover - ctx: ServerRequestContext, params: CallToolRequestParams -) -> CallToolResult: +async def _handle_context_call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult: name = params.name args = params.arguments or {} @@ -1517,8 +1516,8 @@ async def _handle_context_call_tool( # pragma: no cover # Server runner for context-aware testing -def run_context_aware_server(port: int): # pragma: no cover - """Run the context-aware test server.""" +def _create_context_aware_server(port: int) -> uvicorn.Server: + """Create the context-aware test server app and uvicorn.Server.""" server = Server( "ContextAwareServer", on_list_tools=_handle_context_list_tools, @@ -1547,24 +1546,22 @@ def run_context_aware_server(port: int): # pragma: no cover log_level="error", ) ) - server_instance.run() + return server_instance @pytest.fixture def context_aware_server(basic_server_port: int) -> Generator[None, None, None]: - """Start the context-aware server in a separate process.""" - proc = multiprocessing.Process(target=run_context_aware_server, args=(basic_server_port,), daemon=True) - proc.start() + """Start the context-aware server on a background thread (in-process for coverage).""" + server_instance = _create_context_aware_server(basic_server_port) + thread = threading.Thread(target=server_instance.run, daemon=True) + thread.start() - # Wait for server to be running wait_for_server(basic_server_port) yield - proc.kill() - proc.join(timeout=2) - if proc.is_alive(): # pragma: no cover - print("Context-aware server process failed to terminate") + server_instance.should_exit = True + thread.join(timeout=5) @pytest.mark.anyio From 375e86240b6ace4770b27a9a40d8f55a5cb105ed Mon Sep 17 00:00:00 2001 From: perhapzz Date: Wed, 25 Mar 2026 16:29:54 +0000 Subject: [PATCH 2/6] fix: add pragma markers for unreachable defensive branches in context_aware handler - Add pragma: no branch for ctx.request guards (always truthy in test env) - Add pragma: no cover for unknown tool fallback return (never reached in tests) - Fixes coverage failure: these branches are defensive code paths that cannot be exercised through the test fixtures --- tests/shared/test_streamable_http.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/shared/test_streamable_http.py b/tests/shared/test_streamable_http.py index c730268fa..7541ddfc0 100644 --- a/tests/shared/test_streamable_http.py +++ b/tests/shared/test_streamable_http.py @@ -1494,7 +1494,7 @@ async def _handle_context_call_tool(ctx: ServerRequestContext, params: CallToolR if name == "echo_headers": headers_info: dict[str, Any] = {} - if ctx.request and isinstance(ctx.request, Request): + if ctx.request and isinstance(ctx.request, Request): # pragma: no branch headers_info = dict(ctx.request.headers) return CallToolResult(content=[TextContent(type="text", text=json.dumps(headers_info))]) @@ -1505,14 +1505,14 @@ async def _handle_context_call_tool(ctx: ServerRequestContext, params: CallToolR "method": None, "path": None, } - if ctx.request and isinstance(ctx.request, Request): + if ctx.request and isinstance(ctx.request, Request): # pragma: no branch request = ctx.request context_data["headers"] = dict(request.headers) context_data["method"] = request.method context_data["path"] = request.url.path return CallToolResult(content=[TextContent(type="text", text=json.dumps(context_data))]) - return CallToolResult(content=[TextContent(type="text", text=f"Unknown tool: {name}")]) + return CallToolResult(content=[TextContent(type="text", text=f"Unknown tool: {name}")]) # pragma: no cover # Server runner for context-aware testing From ff4239de473d72e16358c16abb320f0576342f48 Mon Sep 17 00:00:00 2001 From: perhapzz Date: Thu, 26 Mar 2026 03:06:59 +0000 Subject: [PATCH 3/6] fix: suppress DeprecationWarning in threaded uvicorn server Python 3.14 deprecates asyncio.iscoroutinefunction(), which uvicorn calls internally. With pytest's filterwarnings=['error'], this DeprecationWarning becomes an exception that kills the server thread before it can start listening. In multiprocessing mode this was hidden because child processes don't inherit pytest's warning filters. Threading shares the same process, so we need to explicitly suppress DeprecationWarnings in the server thread. --- tests/shared/test_streamable_http.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/shared/test_streamable_http.py b/tests/shared/test_streamable_http.py index 7541ddfc0..2a5ddd1b9 100644 --- a/tests/shared/test_streamable_http.py +++ b/tests/shared/test_streamable_http.py @@ -11,6 +11,7 @@ import threading import time import traceback +import warnings from collections.abc import AsyncIterator, Generator from contextlib import asynccontextmanager from dataclasses import dataclass, field @@ -1553,7 +1554,13 @@ def _create_context_aware_server(port: int) -> uvicorn.Server: def context_aware_server(basic_server_port: int) -> Generator[None, None, None]: """Start the context-aware server on a background thread (in-process for coverage).""" server_instance = _create_context_aware_server(basic_server_port) - thread = threading.Thread(target=server_instance.run, daemon=True) + + def _run() -> None: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + server_instance.run() + + thread = threading.Thread(target=_run, daemon=True) thread.start() wait_for_server(basic_server_port) From efe06317fffd511c213d39b1c6d4d2f8264e70af Mon Sep 17 00:00:00 2001 From: perhapzz Date: Thu, 26 Mar 2026 03:18:24 +0000 Subject: [PATCH 4/6] fix: suppress Windows ProactorBasePipeTransport teardown warning On Windows Python 3.13, the ProactorBasePipeTransport.__del__ fires during GC after the threaded uvicorn server shuts down, raising a PytestUnraisableExceptionWarning due to filterwarnings=['error']. Force a GC collection with warnings suppressed so the transport finalizer runs before pytest's unraisable-exception hook can catch it. --- tests/shared/test_streamable_http.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/shared/test_streamable_http.py b/tests/shared/test_streamable_http.py index 2a5ddd1b9..11889c7dc 100644 --- a/tests/shared/test_streamable_http.py +++ b/tests/shared/test_streamable_http.py @@ -1570,6 +1570,14 @@ def _run() -> None: server_instance.should_exit = True thread.join(timeout=5) + # Force GC and suppress Windows ProactorBasePipeTransport.__del__ warnings + # that surface when the event loop is torn down in a thread. + import gc + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=pytest.PytestUnraisableExceptionWarning) + gc.collect() + @pytest.mark.anyio async def test_streamablehttp_request_context_propagation(context_aware_server: None, basic_server_url: str) -> None: From 578d132016793a4bab2a764c5b50d9d405f30939 Mon Sep 17 00:00:00 2001 From: perhapzz Date: Thu, 26 Mar 2026 03:27:39 +0000 Subject: [PATCH 5/6] fix: suppress ProactorEventLoop teardown warnings on Windows On Windows Python 3.13, the ProactorBasePipeTransport finalizer fires during GC after the threaded uvicorn server shuts down, raising PytestUnraisableExceptionWarning. Add a filterwarnings marker to all tests that use the threaded context_aware_server fixture. --- tests/shared/test_streamable_http.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/tests/shared/test_streamable_http.py b/tests/shared/test_streamable_http.py index 11889c7dc..fbcf877ed 100644 --- a/tests/shared/test_streamable_http.py +++ b/tests/shared/test_streamable_http.py @@ -1552,12 +1552,20 @@ def _create_context_aware_server(port: int) -> uvicorn.Server: @pytest.fixture def context_aware_server(basic_server_port: int) -> Generator[None, None, None]: - """Start the context-aware server on a background thread (in-process for coverage).""" + """Start the context-aware server on a background thread (in-process for coverage). + + Unlike multiprocessing, threads share the host process's warning filters. + Uvicorn and the Windows ProactorEventLoop emit DeprecationWarning / + ResourceWarning during startup and teardown that pytest's + ``filterwarnings = ["error"]`` would otherwise promote to hard failures. + We therefore run the server with all warnings suppressed (mirroring + the implicit isolation that multiprocessing provided). + """ server_instance = _create_context_aware_server(basic_server_port) def _run() -> None: with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) + warnings.simplefilter("ignore") server_instance.run() thread = threading.Thread(target=_run, daemon=True) @@ -1570,15 +1578,14 @@ def _run() -> None: server_instance.should_exit = True thread.join(timeout=5) - # Force GC and suppress Windows ProactorBasePipeTransport.__del__ warnings - # that surface when the event loop is torn down in a thread. - import gc - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=pytest.PytestUnraisableExceptionWarning) - gc.collect() +# Marker to suppress Windows ProactorEventLoop teardown warnings on threaded servers. +# When uvicorn runs in a thread (instead of a subprocess), transport finalizers fire +# during GC in the main process and trigger PytestUnraisableExceptionWarning. +_suppress_transport_teardown = pytest.mark.filterwarnings("ignore::pytest.PytestUnraisableExceptionWarning") +@_suppress_transport_teardown @pytest.mark.anyio async def test_streamablehttp_request_context_propagation(context_aware_server: None, basic_server_url: str) -> None: """Test that request context is properly propagated through StreamableHTTP.""" @@ -1612,6 +1619,7 @@ async def test_streamablehttp_request_context_propagation(context_aware_server: assert headers_data.get("x-trace-id") == "trace-123" +@_suppress_transport_teardown @pytest.mark.anyio async def test_streamablehttp_request_context_isolation(context_aware_server: None, basic_server_url: str) -> None: """Test that request contexts are isolated between StreamableHTTP clients.""" @@ -1650,6 +1658,7 @@ async def test_streamablehttp_request_context_isolation(context_aware_server: No assert ctx["headers"].get("authorization") == f"Bearer token-{i}" +@_suppress_transport_teardown @pytest.mark.anyio async def test_client_includes_protocol_version_header_after_init(context_aware_server: None, basic_server_url: str): """Test that client includes mcp-protocol-version header after initialization.""" @@ -2263,6 +2272,7 @@ async def test_streamable_http_client_does_not_mutate_provided_client( assert custom_client.headers.get("Authorization") == "Bearer test-token" +@_suppress_transport_teardown @pytest.mark.anyio async def test_streamable_http_client_mcp_headers_override_defaults( context_aware_server: None, basic_server_url: str @@ -2294,6 +2304,7 @@ async def test_streamable_http_client_mcp_headers_override_defaults( assert headers_data["content-type"] == "application/json" +@_suppress_transport_teardown @pytest.mark.anyio async def test_streamable_http_client_preserves_custom_with_mcp_headers( context_aware_server: None, basic_server_url: str From 0007aa58734bfb42d48e4efcf71645bdf43e0705 Mon Sep 17 00:00:00 2001 From: perhapzz Date: Thu, 26 Mar 2026 03:45:23 +0000 Subject: [PATCH 6/6] ci: re-trigger CI (flaky Windows tests)