mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 21:47:02 +01:00
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
208 lines
7.0 KiB
Python
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()
|