testsAndMisc/python_pkg/brother_printer/tests/test_usb_query_part2.py
Krzysztof kuhy Rudnicki ad3d70f197
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Split brother_printer PJL wire transport out of usb_query
usb_query.py 267 -> 180. The non-blocking fd handling - _drain_buffer,
_write_with_deadline, _read_nonblocking, _wait_for_pjl_response and
pjl_query - moves to _pjl_io.py.

usb_query imports the three names test_usb_query.py patches back, which
is safe here only because all three still have real call sites in the
code that stayed; an import-only name would be stripped by ruff's F401
sweep and break the patch. os and time also stay: they are patched on
usb_query as well as on _pjl_io.

test_usb_query.py needs no edit. Pass count is 402 before and after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-16 17:11:15 +02:00

208 lines
7.0 KiB
Python

"""Tests for brother_printer._pjl_io - the PJL wire protocol.
The printer's fd is non-blocking throughout: usblp blocks forever on open
and on writes whenever the printer is unwell, which is exactly when a status
report is wanted. These tests pin that every read and write is bounded.
"""
from __future__ import annotations
from unittest.mock import MagicMock, patch
from python_pkg.brother_printer._pjl_io import (
_drain_buffer,
_read_nonblocking,
_wait_for_pjl_response,
_write_with_deadline,
pjl_query,
)
MOD = "python_pkg.brother_printer._pjl_io"
class TestDrainBuffer:
@patch(f"{MOD}.os.read")
def test_drain(self, mock_read: MagicMock) -> None:
mock_read.side_effect = [b"data", OSError("done")]
_drain_buffer(42)
assert mock_read.called
@patch(f"{MOD}.os.read")
def test_drain_empty_buffer(self, mock_read: MagicMock) -> None:
"""Buffer is already empty - os.read returns b'' immediately."""
mock_read.return_value = b""
_drain_buffer(42)
mock_read.assert_called_once()
class TestReadNonblocking:
@patch(f"{MOD}.os.read")
def test_reads_chunks(self, mock_read: MagicMock) -> None:
mock_read.side_effect = [b"hello", b"", OSError]
assert _read_nonblocking(42) == b"hello"
@patch(f"{MOD}.os.read")
def test_oserror_suppressed(self, mock_read: MagicMock) -> None:
mock_read.side_effect = OSError("would block")
assert _read_nonblocking(42) == b""
class TestWriteWithDeadline:
"""Writes must never block forever on a printer that is not listening."""
@patch(f"{MOD}.os.write", return_value=5)
@patch(f"{MOD}.time.time", return_value=0.0)
def test_writes_all(self, t: MagicMock, mock_write: MagicMock) -> None:
assert _write_with_deadline(42, b"hello", 10.0) is True
@patch(f"{MOD}.os.write")
@patch(f"{MOD}.time.time", return_value=0.0)
def test_partial_writes_resume(self, t: MagicMock, mock_write: MagicMock) -> None:
mock_write.side_effect = [2, 3]
assert _write_with_deadline(42, b"hello", 10.0) is True
assert mock_write.call_count == 2
@patch(f"{MOD}.time.time")
def test_deadline_already_passed(self, mock_time: MagicMock) -> None:
mock_time.return_value = 20.0
assert _write_with_deadline(42, b"hello", 10.0) is False
@patch(f"{MOD}.select.select", return_value=([], [], []))
@patch(f"{MOD}.os.write", side_effect=BlockingIOError())
@patch(f"{MOD}.time.time")
def test_blocked_until_deadline(
self,
mock_time: MagicMock,
mock_write: MagicMock,
mock_select: MagicMock,
) -> None:
"""A printer that never accepts data times out instead of hanging."""
mock_time.side_effect = [0.0, 1.0, 20.0]
assert _write_with_deadline(42, b"hello", 10.0) is False
@patch(f"{MOD}.select.select", return_value=([], [42], []))
@patch(f"{MOD}.os.write")
@patch(f"{MOD}.time.time", return_value=0.0)
def test_blocked_then_writable(
self,
t: MagicMock,
mock_write: MagicMock,
mock_select: MagicMock,
) -> None:
mock_write.side_effect = [BlockingIOError(), 5]
assert _write_with_deadline(42, b"hello", 10.0) is True
@patch(f"{MOD}.os.write", side_effect=OSError("gone"))
@patch(f"{MOD}.time.time", return_value=0.0)
def test_oserror_gives_up(self, t: MagicMock, mock_write: MagicMock) -> None:
assert _write_with_deadline(42, b"hello", 10.0) is False
class TestWaitForPjlResponse:
@patch(f"{MOD}._read_nonblocking")
@patch(f"{MOD}.select.select")
@patch(f"{MOD}.time.time")
def test_response_with_equals(
self,
mock_time: MagicMock,
mock_select: MagicMock,
mock_read: MagicMock,
) -> None:
mock_time.side_effect = [0.0, 0.5, 1.0]
mock_select.return_value = ([42], [], [])
mock_read.return_value = b"CODE=10001"
assert b"CODE=10001" in _wait_for_pjl_response(42, 5.0)
@patch(f"{MOD}._read_nonblocking")
@patch(f"{MOD}.select.select")
@patch(f"{MOD}.time.time")
def test_response_with_pjl(
self,
mock_time: MagicMock,
mock_select: MagicMock,
mock_read: MagicMock,
) -> None:
mock_time.side_effect = [0.0, 0.5, 1.0]
mock_select.return_value = ([42], [], [])
mock_read.return_value = b"@PJL INFO"
assert b"@PJL" in _wait_for_pjl_response(42, 5.0)
@patch(f"{MOD}.select.select")
@patch(f"{MOD}.time.time")
def test_timeout_no_data(
self,
mock_time: MagicMock,
mock_select: MagicMock,
) -> None:
mock_time.side_effect = [10.0, 11.0]
assert _wait_for_pjl_response(42, 5.0) == b""
@patch(f"{MOD}._read_nonblocking")
@patch(f"{MOD}.select.select")
@patch(f"{MOD}.time.time")
def test_not_readable_then_timeout(
self,
mock_time: MagicMock,
mock_select: MagicMock,
mock_read: MagicMock,
) -> None:
mock_time.side_effect = [0.0, 0.5, 6.0]
mock_select.return_value = ([], [], [])
assert _wait_for_pjl_response(42, 5.0) == b""
@patch(f"{MOD}._read_nonblocking")
@patch(f"{MOD}.select.select")
@patch(f"{MOD}.time.time")
def test_remaining_lte_zero(
self,
mock_time: MagicMock,
mock_select: MagicMock,
mock_read: MagicMock,
) -> None:
"""Inner remaining check triggers break."""
mock_time.side_effect = [0.0, 6.0, 6.0]
assert _wait_for_pjl_response(42, 5.0) == b""
mock_select.assert_not_called()
@patch(f"{MOD}._read_nonblocking")
@patch(f"{MOD}.select.select")
@patch(f"{MOD}.time.time")
def test_response_no_eq_or_pjl(
self,
mock_time: MagicMock,
mock_select: MagicMock,
mock_read: MagicMock,
) -> None:
"""Data read but no '=' or '@PJL' -> continues loop then times out."""
mock_time.side_effect = [0.0, 0.5, 1.0, 6.0]
mock_select.return_value = ([42], [], [])
mock_read.return_value = b"garbage"
assert _wait_for_pjl_response(42, 5.0) == b"garbage"
class TestPjlQuery:
@patch(f"{MOD}._wait_for_pjl_response")
@patch(f"{MOD}._write_with_deadline", return_value=True)
@patch(f"{MOD}.time.time", return_value=100.0)
def test_query(
self,
t: MagicMock,
mock_write: MagicMock,
mock_wait: MagicMock,
) -> None:
mock_wait.return_value = b"CODE=10001"
assert "CODE=10001" in pjl_query(42, "@PJL INFO STATUS")
@patch(f"{MOD}._wait_for_pjl_response")
@patch(f"{MOD}._write_with_deadline", return_value=False)
@patch(f"{MOD}.time.time", return_value=100.0)
def test_write_timeout_returns_empty(
self,
t: MagicMock,
mock_write: MagicMock,
mock_wait: MagicMock,
) -> None:
"""If we cannot even send the command, do not wait for a reply."""
assert pjl_query(42, "@PJL INFO STATUS") == ""
mock_wait.assert_not_called()