11import io
22import sys
3+ import tempfile
34from io import TextIOWrapper
45
56import anyio
@@ -64,7 +65,7 @@ async def test_stdio_server():
6465
6566
6667@pytest .mark .anyio
67- async def test_stdio_server_invalid_utf8 (monkeypatch : pytest . MonkeyPatch ):
68+ async def test_stdio_server_invalid_utf8 ():
6869 """Non-UTF-8 bytes on stdin must not crash the server.
6970
7071 Invalid bytes are replaced with U+FFFD, which then fails JSON parsing and
@@ -73,22 +74,40 @@ async def test_stdio_server_invalid_utf8(monkeypatch: pytest.MonkeyPatch):
7374 """
7475 # \xff\xfe are invalid UTF-8 start bytes.
7576 valid = JSONRPCRequest (jsonrpc = "2.0" , id = 1 , method = "ping" )
76- raw_stdin = io .BytesIO (b"\xff \xfe \n " + valid .model_dump_json (by_alias = True , exclude_none = True ).encode () + b"\n " )
77-
78- # Replace sys.stdin with a wrapper whose .buffer is our raw bytes, so that
79- # stdio_server()'s default path wraps it with errors='replace'.
80- monkeypatch .setattr (sys , "stdin" , TextIOWrapper (raw_stdin , encoding = "utf-8" ))
81- monkeypatch .setattr (sys , "stdout" , TextIOWrapper (io .BytesIO (), encoding = "utf-8" ))
82-
83- with anyio .fail_after (5 ):
84- async with stdio_server () as (read_stream , write_stream ):
85- await write_stream .aclose ()
86- async with read_stream : # pragma: no branch
87- # First line: \xff\xfe -> U+FFFD U+FFFD -> JSON parse fails -> exception in stream
88- first = await read_stream .receive ()
89- assert isinstance (first , Exception )
90-
91- # Second line: valid message still comes through
92- second = await read_stream .receive ()
93- assert isinstance (second , SessionMessage )
94- assert second .message == valid
77+ raw_stdin = tempfile .TemporaryFile ()
78+ raw_stdin .write (b"\xff \xfe \n " + valid .model_dump_json (by_alias = True , exclude_none = True ).encode () + b"\n " )
79+ raw_stdin .seek (0 )
80+ raw_stdout = tempfile .TemporaryFile ()
81+
82+ # Replace sys.stdin/stdout with wrappers backed by real file descriptors so
83+ # stdio_server()'s default path can duplicate them without closing the
84+ # original process-level streams.
85+ original_stdin = sys .stdin
86+ original_stdout = sys .stdout
87+ test_stdin = TextIOWrapper (raw_stdin , encoding = "utf-8" )
88+ test_stdout = TextIOWrapper (raw_stdout , encoding = "utf-8" )
89+ sys .stdin = test_stdin
90+ sys .stdout = test_stdout
91+
92+ try :
93+ with anyio .fail_after (5 ):
94+ async with stdio_server () as (read_stream , write_stream ):
95+ await write_stream .aclose ()
96+ async with read_stream : # pragma: no branch
97+ # First line: \xff\xfe -> U+FFFD U+FFFD -> JSON parse fails -> exception in stream
98+ first = await read_stream .receive ()
99+ assert isinstance (first , Exception )
100+
101+ # Second line: valid message still comes through
102+ second = await read_stream .receive ()
103+ assert isinstance (second , SessionMessage )
104+ assert second .message == valid
105+
106+ assert not sys .stdin .closed
107+ assert not sys .stdout .closed
108+ sys .stdout .write ("stdio still open" )
109+ finally :
110+ sys .stdin = original_stdin
111+ sys .stdout = original_stdout
112+ test_stdin .close ()
113+ test_stdout .close ()
0 commit comments