Skip to content

test: replace subprocess servers with in-process threaded servers for coverage tracking#2341

Open
perhapzz wants to merge 3 commits intomodelcontextprotocol:mainfrom
perhapzz:fix/1678-streamable-http-test-coverage
Open

test: replace subprocess servers with in-process threaded servers for coverage tracking#2341
perhapzz wants to merge 3 commits intomodelcontextprotocol:mainfrom
perhapzz:fix/1678-streamable-http-test-coverage

Conversation

@perhapzz
Copy link

@perhapzz perhapzz commented Mar 25, 2026

Motivation

Part of #1678

The Streamable HTTP test fixtures launch servers via multiprocessing.Process. Since coverage.py cannot track code in child processes by default, server-side code paths appear uncovered despite being exercised by tests, requiring # pragma: no cover workarounds in both test and source files.

Changes

Test refactoring

Replace multiprocessing.Process with in-process threading.Thread + uvicorn.Server for the three main server fixtures (basic_server, json_server, resumable_server). coverage.py tracks all threads in the same process by default, so these code paths are now properly covered.

  • Add _start_server_thread(): launches uvicorn.Server on a daemon thread with graceful shutdown (server.should_exit = True)
  • Remove dead run_server() function and unused traceback import
  • Remove 8 # pragma: no cover markers from test server code
  • Add 13 new tests covering previously-unreachable branches:
    • replay_events_after (non-existent event ID, None-message skipping)
    • slow:// resource read
    • long_running_with_checkpoints tool
    • Sampling with non-text content
    • multi_close_tool / tool_with_standalone_stream_close with and without event store
    • POST with invalid Content-Type (400 from transport security)
    • POST/GET/DELETE with mismatched session ID (404 from session manager)
    • PUT unsupported HTTP method (405)
  • Remove unused http_client fixture

Source file pragma adjustment

Since the test servers now run in-process, coverage.py now tracks source code that was previously unreachable. This requires adjusting coverage annotations to satisfy both coverage --fail-under=100 and strict-no-cover:

  • Remove # pragma: no cover from lines now deterministically covered
  • Replace with # pragma: lax no cover on non-deterministic except handlers (e.g. race-condition-dependent BrokenResourceError, ClosedResourceError) — these are sometimes covered depending on thread timing; lax is excluded by coverage.py (via exclude_lines) but not flagged by strict-no-cover
  • Add # pragma: no branch for branch partials that are never taken in tests (e.g. mcp_session_id checks that are always True in the stateful manager architecture)

Files modified (annotation changes only, no logic changes):

  • src/mcp/server/streamable_http.py
  • src/mcp/server/transport_security.py
  • src/mcp/server/session.py

Results

  • All 74 Streamable HTTP tests pass
  • pyright 0 errors, ruff check + ruff format clean
  • Coverage: 100.00%
  • strict-no-cover: ✅ no wrongly marked lines

perhapzz added a commit to perhapzz/python-sdk that referenced this pull request Mar 25, 2026
- Replace multiprocessing.Process with threading.Thread for
  context_aware_server fixture (same pattern as PR modelcontextprotocol#2341)
- Remove 5 pragma: no cover markers from context-aware server code
- Remove dead run_server() function and unused imports
- Remaining 5 pragmas are on genuinely unreachable defensive code
@perhapzz perhapzz force-pushed the fix/1678-streamable-http-test-coverage branch 2 times, most recently from c3ef81a to 7a3fb21 Compare March 25, 2026 13:23
@perhapzz perhapzz marked this pull request as draft March 25, 2026 13:45
@perhapzz perhapzz force-pushed the fix/1678-streamable-http-test-coverage branch 3 times, most recently from 522bd42 to 9c0d31f Compare March 25, 2026 15:28
Replace multiprocessing.Process with threading.Thread + uvicorn.Server
for test server fixtures (basic_server, json_server, resumable_server)
so coverage.py can track server-side code in the same process.

Changes:
- Add _start_server_thread() helper using uvicorn.Server in a daemon thread
- Graceful shutdown via server.should_exit instead of proc.kill()
- Remove 8 pragma: no cover from test fixtures (no longer needed)
- Add 8 new tests covering previously-uncovered branches
- Remove dead run_server() function and unused http_client fixture
- Convert pragma: no cover to pragma: lax no cover in source files
  for non-deterministic coverage lines (thread timing dependent)
- Add pragma: no branch for partial branch coverage on guard lines
@perhapzz perhapzz force-pushed the fix/1678-streamable-http-test-coverage branch from 9c0d31f to f4ea245 Compare March 25, 2026 15:38
@perhapzz perhapzz marked this pull request as ready for review March 25, 2026 16:01
@perhapzz perhapzz marked this pull request as draft March 25, 2026 17:29
@perhapzz perhapzz marked this pull request as ready for review March 25, 2026 17:29
- Add warnings.filterwarnings in _run_server thread to suppress
  asyncio.iscoroutinefunction deprecation (Python 3.14+)
- Add ResourceWarning filter for unclosed sockets during teardown (Windows)
- Add pytest filterwarnings for DeprecationWarning and
  PytestUnhandledThreadExceptionWarning in pyproject.toml
Add pragma annotations for 3 remaining uncovered paths:
- pragma: no branch on mcp_session_id checks in _create_error_response
  and initialization handler (always True in stateful manager)
- pragma: lax no cover on ClosedResourceError handler (non-deterministic)

Add 5 integration tests for transport validation:
- POST with invalid Content-Type (400)
- POST/GET/DELETE with mismatched session ID (404)
- PUT unsupported method (405)

All 74 tests pass with 100% coverage and strict-no-cover clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant