testsAndMisc/python_pkg/brother_printer/tests/test_pjl_io.py
Krzysztof kuhy Rudnicki 74eee2b57f Split the brother_printer display and usb_query test modules
test_display.py 363 -> 204, with the supply maths and its display going
to test_supply_levels.py. test_usb_query.py 328 -> 126, with the retry
and sequencing tests going to test_usb_query_sequence.py.

That removed the last reader of the six _supply names through display,
so display.py's __all__ shrinks to the two public entry points and the
stale comment about tests reaching them there is gone. Checked by grep
across python_pkg and an import-time attribute probe, not by the suite.

The first run after the usb_query split reported 393: TestParsePagecount
sat between the two moved classes and travelled with them. Fixed.

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:23:47 +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()