Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.

Commit a0ca7ac

Browse files
mangelajogithub-actions[bot]
authored andcommitted
pyserial driver: pipe handle termination of incoming streams
(cherry picked from commit f18e909)
1 parent bc74d62 commit a0ca7ac

2 files changed

Lines changed: 123 additions & 12 deletions

File tree

packages/jumpstarter-driver-pyserial/jumpstarter_driver_pyserial/cli_test.py

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,21 @@
44
Tests the Click CLI interface including the pipe command.
55
"""
66

7-
from unittest.mock import patch
7+
from unittest.mock import AsyncMock, patch
88

99
import pytest
10+
from anyio import BrokenResourceError, EndOfStream
1011
from click.testing import CliRunner
1112

1213
from .driver import PySerial
1314
from jumpstarter.common.utils import serve
1415

1516

17+
@pytest.fixture
18+
def anyio_backend():
19+
return "asyncio"
20+
21+
1622
@pytest.fixture
1723
def pyserial_client():
1824
"""Fixture to create a PySerial client with loop:// URL for testing."""
@@ -241,3 +247,101 @@ def test_cli_base_command(pyserial_client):
241247
assert result.exit_code == 0
242248
assert "Serial port client" in result.output or "Commands:" in result.output
243249

250+
251+
@pytest.mark.anyio
252+
async def test_serial_to_output_handles_end_of_stream(pyserial_client):
253+
"""Test that _serial_to_output handles EndOfStream gracefully."""
254+
# Create a mock stream that raises EndOfStream
255+
mock_stream = AsyncMock()
256+
mock_stream.receive.side_effect = EndOfStream
257+
258+
# Capture the click output
259+
from click.testing import CliRunner
260+
261+
runner = CliRunner()
262+
with runner.isolated_filesystem():
263+
# Call _serial_to_output directly
264+
await pyserial_client._serial_to_output(mock_stream, None, False)
265+
266+
# The method should have caught EndOfStream and not raised it
267+
# (we're testing that it doesn't propagate)
268+
269+
270+
@pytest.mark.anyio
271+
async def test_serial_to_output_handles_broken_resource(pyserial_client):
272+
"""Test that _serial_to_output handles BrokenResourceError gracefully."""
273+
# Create a mock stream that raises BrokenResourceError
274+
mock_stream = AsyncMock()
275+
mock_stream.receive.side_effect = BrokenResourceError
276+
277+
# Capture the click output
278+
from click.testing import CliRunner
279+
280+
runner = CliRunner()
281+
with runner.isolated_filesystem():
282+
# Call _serial_to_output directly
283+
await pyserial_client._serial_to_output(mock_stream, None, False)
284+
285+
# The method should have caught BrokenResourceError and not raised it
286+
# (we're testing that it doesn't propagate)
287+
288+
289+
@pytest.mark.anyio
290+
async def test_serial_to_output_end_of_stream_with_file(pyserial_client):
291+
"""Test that _serial_to_output handles EndOfStream when writing to file."""
292+
# Create a mock stream that raises EndOfStream
293+
mock_stream = AsyncMock()
294+
mock_stream.receive.side_effect = EndOfStream
295+
296+
from click.testing import CliRunner
297+
298+
runner = CliRunner()
299+
with runner.isolated_filesystem():
300+
# Call _serial_to_output with a file output
301+
await pyserial_client._serial_to_output(mock_stream, "test.log", False)
302+
303+
# The file should have been created (even if empty)
304+
import os
305+
306+
assert os.path.exists("test.log")
307+
308+
309+
@pytest.mark.anyio
310+
async def test_serial_to_output_broken_resource_with_file(pyserial_client):
311+
"""Test that _serial_to_output handles BrokenResourceError when writing to file."""
312+
# Create a mock stream that raises BrokenResourceError
313+
mock_stream = AsyncMock()
314+
mock_stream.receive.side_effect = BrokenResourceError
315+
316+
from click.testing import CliRunner
317+
318+
runner = CliRunner()
319+
with runner.isolated_filesystem():
320+
# Call _serial_to_output with a file output
321+
await pyserial_client._serial_to_output(mock_stream, "test.log", False)
322+
323+
# The file should have been created (even if empty)
324+
import os
325+
326+
assert os.path.exists("test.log")
327+
328+
329+
@pytest.mark.anyio
330+
async def test_serial_to_output_receives_data_then_end_of_stream(pyserial_client):
331+
"""Test that _serial_to_output successfully receives data before EndOfStream."""
332+
# Create a mock stream that returns data then raises EndOfStream
333+
mock_stream = AsyncMock()
334+
mock_stream.receive.side_effect = [b"Hello", b"World", EndOfStream]
335+
336+
from click.testing import CliRunner
337+
338+
runner = CliRunner()
339+
with runner.isolated_filesystem():
340+
# Call _serial_to_output with a file output
341+
await pyserial_client._serial_to_output(mock_stream, "test.log", False)
342+
343+
# Verify the file contains the data
344+
with open("test.log", "rb") as f:
345+
content = f.read()
346+
assert content == b"HelloWorld"
347+

packages/jumpstarter-driver-pyserial/jumpstarter_driver_pyserial/client.py

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from typing import Optional
44

55
import click
6-
from anyio import EndOfStream, create_task_group, open_file, sleep
6+
from anyio import BrokenResourceError, EndOfStream, create_task_group, open_file, sleep
77
from anyio.streams.file import FileReadStream
88
from jumpstarter_driver_network.adapters import PexpectAdapter
99
from pexpect.fdpexpect import fdspawn
@@ -70,18 +70,25 @@ async def _pipe_serial(
7070

7171
async def _serial_to_output(self, stream, output_file: Optional[str], append: bool):
7272
"""Read from serial and write to file or stdout."""
73-
if output_file:
74-
mode = "ab" if append else "wb"
75-
async with await open_file(output_file, mode) as f:
73+
try:
74+
if output_file:
75+
mode = "ab" if append else "wb"
76+
async with await open_file(output_file, mode) as f:
77+
while True:
78+
data = await stream.receive()
79+
await f.write(data)
80+
await f.flush()
81+
else:
7682
while True:
7783
data = await stream.receive()
78-
await f.write(data)
79-
await f.flush()
80-
else:
81-
while True:
82-
data = await stream.receive()
83-
sys.stdout.buffer.write(data)
84-
sys.stdout.buffer.flush()
84+
sys.stdout.buffer.write(data)
85+
sys.stdout.buffer.flush()
86+
except EndOfStream:
87+
click.echo("\nSerial connection closed normally (end of stream).", err=True)
88+
except BrokenResourceError:
89+
click.echo(
90+
"\nSerial connection lost (broken resource). The connection may have been interrupted.", err=True
91+
)
8592

8693
async def _stdin_to_serial(self, stream):
8794
"""Read from stdin and write to serial. Returns when stdin reaches EOF."""

0 commit comments

Comments
 (0)