Skip to content

Commit d0a1444

Browse files
committed
Added test suite for stream_io
1 parent fe90a96 commit d0a1444

File tree

1 file changed

+21
-82
lines changed

1 file changed

+21
-82
lines changed

tests/io/stream_io_test.py

Lines changed: 21 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@
22
import anyio
33

44
from lf_toolkit.io.stream_io import StreamIO, PrefixStreamIO, StreamServer
5+
from lf_toolkit.io.stdio_server import StdioServer
56

67

78
@pytest.fixture
89
def anyio_backend():
910
return "asyncio"
1011

1112

13+
# ---------------------------------------------------------------------------
14+
# Helpers
15+
# ---------------------------------------------------------------------------
1216

1317
def make_framed_message(payload: str) -> bytes:
1418
"""Wrap a JSON string in Content-Length framing."""
@@ -45,80 +49,29 @@ async def close(self):
4549
self.close_count += 1
4650

4751

48-
class EchoServer(StreamServer):
49-
"""
50-
Concrete StreamServer for testing.
51-
- run() is required by BaseServer (abstract) but not used in tests
52-
since we call _handle_client directly.
53-
- dispatch() is overridden to echo the raw request back, bypassing
54-
the real JsonRpcHandler so tests stay self-contained.
55-
"""
56-
57-
async def run(self):
58-
pass
59-
60-
async def dispatch(self, data: str) -> str:
61-
return data
62-
63-
64-
class BuggyStreamServer(StreamServer):
65-
"""
66-
Reproduces the original bug by overriding _handle_client with
67-
close() inside the finally block.
68-
"""
69-
70-
async def run(self):
71-
pass
72-
73-
async def dispatch(self, data: str) -> str:
74-
return data
75-
76-
async def _handle_client(self, client: StreamIO):
77-
io = self.wrap_io(client)
78-
while True:
79-
try:
80-
data = await io.read(4096)
81-
if not data:
82-
break
83-
response = await self.dispatch(data.decode("utf-8"))
84-
await io.write(response.encode("utf-8"))
85-
except anyio.EndOfStream:
86-
break
87-
except anyio.ClosedResourceError:
88-
break
89-
except Exception as e:
90-
print(f"Exception: {e}")
91-
finally:
92-
await client.close() # BUG: closes after every message
93-
94-
9552
# ---------------------------------------------------------------------------
9653
# Tests
9754
# ---------------------------------------------------------------------------
9855

99-
class TestStreamServer:
56+
class TestStdioServer:
10057

10158
@pytest.fixture
10259
def stream(self):
10360
return FakeStreamIO()
10461

10562
@pytest.fixture
10663
def server(self):
107-
return EchoServer()
108-
109-
@pytest.fixture
110-
def buggy_server(self):
111-
return BuggyStreamServer()
64+
return StdioServer()
11265

11366
@pytest.mark.anyio
11467
async def test_handles_multiple_messages(self, stream, server):
11568
"""
11669
Core fix test: the server must process multiple messages in a single
11770
session without closing the connection between them.
11871
"""
119-
stream.feed(make_framed_message('{"command": "eval", "id": 1}'))
120-
stream.feed(make_framed_message('{"command": "eval", "id": 2}'))
121-
stream.feed(make_framed_message('{"command": "eval", "id": 3}'))
72+
stream.feed(make_framed_message('{"jsonrpc":"2.0","method":"eval","params":{},"id":1}'))
73+
stream.feed(make_framed_message('{"jsonrpc":"2.0","method":"eval","params":{},"id":2}'))
74+
stream.feed(make_framed_message('{"jsonrpc":"2.0","method":"eval","params":{},"id":3}'))
12275

12376
await server._handle_client(stream)
12477

@@ -133,8 +86,8 @@ async def test_closes_only_once(self, stream, server):
13386
The client connection should be closed exactly once — after the loop
13487
exits — not once per message.
13588
"""
136-
stream.feed(make_framed_message('{"id": 1}'))
137-
stream.feed(make_framed_message('{"id": 2}'))
89+
stream.feed(make_framed_message('{"jsonrpc":"2.0","method":"eval","params":{},"id":1}'))
90+
stream.feed(make_framed_message('{"jsonrpc":"2.0","method":"eval","params":{},"id":2}'))
13891

13992
await server._handle_client(stream)
14093

@@ -143,32 +96,17 @@ async def test_closes_only_once(self, stream, server):
14396
f"{stream.close_count} times. This is the original bug."
14497
)
14598

146-
@pytest.mark.anyio
147-
async def test_buggy_server_closes_after_each_message(self, stream, buggy_server):
148-
"""
149-
Demonstrates the original bug: close() in the finally block causes
150-
the stream to be closed after every message, not just at the end.
151-
"""
152-
stream.feed(make_framed_message('{"id": 1}'))
153-
stream.feed(make_framed_message('{"id": 2}'))
154-
155-
await buggy_server._handle_client(stream)
156-
157-
assert stream.close_count > 1, (
158-
"Expected buggy server to call close() more than once, "
159-
"confirming the bug exists in the original code."
160-
)
161-
16299
@pytest.mark.anyio
163100
async def test_single_message(self, stream, server):
164101
"""A single message round-trip should work correctly."""
165-
payload = '{"command": "eval", "response": "test"}'
166-
stream.feed(make_framed_message(payload))
102+
stream.feed(make_framed_message('{"jsonrpc":"2.0","method":"eval","params":{},"id":1}'))
167103

168104
await server._handle_client(stream)
169105

170106
assert len(stream.responses) == 1
171-
assert payload.encode() in stream.responses[0]
107+
# Response is a framed JSON-RPC envelope
108+
assert b"Content-Length:" in stream.responses[0]
109+
assert b"jsonrpc" in stream.responses[0]
172110

173111
@pytest.mark.anyio
174112
async def test_closes_on_empty_stream(self, stream, server):
@@ -179,10 +117,10 @@ async def test_closes_on_empty_stream(self, stream, server):
179117

180118
@pytest.mark.anyio
181119
async def test_response_content(self, stream, server):
182-
"""Verify the actual response content is correct across messages."""
120+
"""Verify a response is returned for each message sent."""
183121
messages = [
184-
'{"id": 1, "command": "eval"}',
185-
'{"id": 2, "command": "preview"}',
122+
'{"jsonrpc":"2.0","method":"eval","params":{},"id":1}',
123+
'{"jsonrpc":"2.0","method":"preview","params":{},"id":2}',
186124
]
187125

188126
for msg in messages:
@@ -191,8 +129,9 @@ async def test_response_content(self, stream, server):
191129
await server._handle_client(stream)
192130

193131
assert len(stream.responses) == 2
194-
for i, msg in enumerate(messages):
195-
assert msg.encode() in stream.responses[i]
132+
for response in stream.responses:
133+
assert b"Content-Length:" in response
134+
assert b"jsonrpc" in response
196135

197136

198137
class TestPrefixStreamIO:

0 commit comments

Comments
 (0)