77
88import json
99import multiprocessing
10+ import threading
1011import socket
1112import time
1213import traceback
@@ -108,7 +109,7 @@ async def store_event(self, stream_id: StreamId, message: types.JSONRPCMessage |
108109 self ._events .append ((stream_id , event_id , message ))
109110 return event_id
110111
111- async def replay_events_after ( # pragma: no cover
112+ async def replay_events_after (
112113 self ,
113114 last_event_id : EventId ,
114115 send_callback : EventCallback ,
@@ -144,11 +145,11 @@ class ServerState:
144145
145146
146147@asynccontextmanager
147- async def _server_lifespan (_server : Server [ServerState ]) -> AsyncIterator [ServerState ]: # pragma: no cover
148+ async def _server_lifespan (_server : Server [ServerState ]) -> AsyncIterator [ServerState ]:
148149 yield ServerState ()
149150
150151
151- async def _handle_read_resource ( # pragma: no cover
152+ async def _handle_read_resource (
152153 ctx : ServerRequestContext [ServerState ], params : ReadResourceRequestParams
153154) -> ReadResourceResult :
154155 uri = str (params .uri )
@@ -163,7 +164,7 @@ async def _handle_read_resource( # pragma: no cover
163164 return ReadResourceResult (contents = [TextResourceContents (uri = uri , text = text , mime_type = "text/plain" )])
164165
165166
166- async def _handle_list_tools ( # pragma: no cover
167+ async def _handle_list_tools (
167168 ctx : ServerRequestContext [ServerState ], params : PaginatedRequestParams | None
168169) -> ListToolsResult :
169170 return ListToolsResult (
@@ -228,7 +229,7 @@ async def _handle_list_tools( # pragma: no cover
228229 )
229230
230231
231- async def _handle_call_tool ( # pragma: no cover
232+ async def _handle_call_tool (
232233 ctx : ServerRequestContext [ServerState ], params : CallToolRequestParams
233234) -> CallToolResult :
234235 name = params .name
@@ -382,7 +383,7 @@ async def _handle_call_tool( # pragma: no cover
382383 return CallToolResult (content = [TextContent (type = "text" , text = f"Called { name } " )])
383384
384385
385- def _create_server () -> Server [ServerState ]: # pragma: no cover
386+ def _create_server () -> Server [ServerState ]:
386387 return Server (
387388 SERVER_NAME ,
388389 lifespan = _server_lifespan ,
@@ -396,7 +397,7 @@ def create_app(
396397 is_json_response_enabled : bool = False ,
397398 event_store : EventStore | None = None ,
398399 retry_interval : int | None = None ,
399- ) -> Starlette : # pragma: no cover
400+ ) -> Starlette :
400401 """Create a Starlette application for testing using the session manager.
401402
402403 Args:
@@ -437,7 +438,7 @@ def run_server(
437438 event_store : EventStore | None = None ,
438439 retry_interval : int | None = None ,
439440) -> None : # pragma: no cover
440- """Run the test server.
441+ """Run the test server in a subprocess (used only by context_aware_server) .
441442
442443 Args:
443444 port: Port to listen on.
@@ -468,6 +469,33 @@ def run_server(
468469 traceback .print_exc ()
469470
470471
472+ def _start_server_thread (
473+ port : int ,
474+ is_json_response_enabled : bool = False ,
475+ event_store : EventStore | None = None ,
476+ retry_interval : int | None = None ,
477+ ) -> tuple [threading .Thread , uvicorn .Server ]:
478+ """Start a test server in a background thread (in-process for coverage).
479+
480+ Returns:
481+ A tuple of (thread, uvicorn_server) for cleanup.
482+ """
483+ app = create_app (is_json_response_enabled , event_store , retry_interval )
484+ config = uvicorn .Config (
485+ app = app ,
486+ host = "127.0.0.1" ,
487+ port = port ,
488+ log_level = "info" ,
489+ limit_concurrency = 10 ,
490+ timeout_keep_alive = 5 ,
491+ access_log = False ,
492+ )
493+ server = uvicorn .Server (config = config )
494+ thread = threading .Thread (target = server .run , daemon = True )
495+ thread .start ()
496+ return thread , server
497+
498+
471499# Test fixtures - using same approach as SSE tests
472500@pytest .fixture
473501def basic_server_port () -> int :
@@ -487,18 +515,17 @@ def json_server_port() -> int:
487515
488516@pytest .fixture
489517def basic_server (basic_server_port : int ) -> Generator [None , None , None ]:
490- """Start a basic server."""
491- proc = multiprocessing .Process (target = run_server , kwargs = {"port" : basic_server_port }, daemon = True )
492- proc .start ()
518+ """Start a basic server in a background thread (in-process for coverage)."""
519+ thread , server = _start_server_thread (port = basic_server_port )
493520
494521 # Wait for server to be running
495522 wait_for_server (basic_server_port )
496523
497524 yield
498525
499526 # Clean up
500- proc . kill ()
501- proc .join (timeout = 2 )
527+ server . should_exit = True
528+ thread .join (timeout = 5 )
502529
503530
504531@pytest .fixture
@@ -519,42 +546,36 @@ def event_server_port() -> int:
519546def event_server (
520547 event_server_port : int , event_store : SimpleEventStore
521548) -> Generator [tuple [SimpleEventStore , str ], None , None ]:
522- """Start a server with event store and retry_interval enabled."""
523- proc = multiprocessing .Process (
524- target = run_server ,
525- kwargs = {"port" : event_server_port , "event_store" : event_store , "retry_interval" : 500 },
526- daemon = True ,
549+ """Start a server with event store and retry_interval enabled (in-process for coverage)."""
550+ thread , server = _start_server_thread (
551+ port = event_server_port , event_store = event_store , retry_interval = 500
527552 )
528- proc .start ()
529553
530554 # Wait for server to be running
531555 wait_for_server (event_server_port )
532556
533557 yield event_store , f"http://127.0.0.1:{ event_server_port } "
534558
535559 # Clean up
536- proc . kill ()
537- proc .join (timeout = 2 )
560+ server . should_exit = True
561+ thread .join (timeout = 5 )
538562
539563
540564@pytest .fixture
541565def json_response_server (json_server_port : int ) -> Generator [None , None , None ]:
542- """Start a server with JSON response enabled."""
543- proc = multiprocessing .Process (
544- target = run_server ,
545- kwargs = {"port" : json_server_port , "is_json_response_enabled" : True },
546- daemon = True ,
566+ """Start a server with JSON response enabled (in-process for coverage)."""
567+ thread , server = _start_server_thread (
568+ port = json_server_port , is_json_response_enabled = True
547569 )
548- proc .start ()
549570
550571 # Wait for server to be running
551572 wait_for_server (json_server_port )
552573
553574 yield
554575
555576 # Clean up
556- proc . kill ()
557- proc .join (timeout = 2 )
577+ server . should_exit = True
578+ thread .join (timeout = 5 )
558579
559580
560581@pytest .fixture
@@ -1044,7 +1065,7 @@ def test_get_validation(basic_server: None, basic_server_url: str):
10441065
10451066# Client-specific fixtures
10461067@pytest .fixture
1047- async def http_client (basic_server : None , basic_server_url : str ): # pragma: no cover
1068+ async def http_client (basic_server : None , basic_server_url : str ):
10481069 """Create test client matching the SSE test pattern."""
10491070 async with httpx .AsyncClient (base_url = basic_server_url ) as client :
10501071 yield client
0 commit comments