mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 22:47:02 +01:00
./run.sh failed outright with ModuleNotFoundError, and once running it reported a sleeping printer as a critical paper jam. Both are fixed, plus the defects found while verifying them against the real HL-1110. Entry point: run.sh ran the script by path, so the absolute python_pkg.* imports could not resolve. Resolve the repo root and pass PYTHONPATH as a sudo argument, since sudo's env_reset strips an exported one. __main__.py had the wrong import prefix and ran main() on import. Status codes: the table claimed provenance from a "Brother PJL Technical Reference" that does not appear to exist, and 23 of its 25 entries were wrong. Rebuilt from the HP PJL Technical Reference Manual, Appendix D, and verified against six states induced on real hardware (10001 READY, 10003 WARMING UP, 10023 PRINTING, 40000 SLEEP, 40021 TOP COVER OPEN, 41213 NO PAPER) - all six matched the reference, none matched the table. 40000 is SLEEP MODE, which the reference explicitly flags "not an error"; the real jam code 40022 was absent entirely. The inverse also bit: 10006 is TONER LOW, previously shown as a benign "Processing". Codes in no reference are dropped and now report "Unknown status - check printer display" rather than a confident wrong answer, and states we infer ourselves no longer borrow PJL numbers to describe themselves. Page counts: estimates came from the CUPS page log, which only sees jobs CUPS logged and counts pages the printer never printed (it logged total=50 for a job that printed 3). Take them from @PJL INFO PAGECOUNT instead, migrating stored baselines onto the printer's scale once so the reported percentages do not jump. Reading status must not cost anything: a blocking open() hung forever whenever the printer was unwell - precisely when a status report is wanted - and the port-status probe stopped CUPS and reset the USB device, destroying the in-flight job and then misreporting the damage as a printer error. The fd is now non-blocking with deadline-bounded writes, and the probe reads the same byte via the free usblp ioctl, skips entirely while a job prints, and re-attaches the kernel driver. Dropped pages: the HL-1110 silently discards pages whose 600dpi raster exceeds its ~1MB memory, stays READY, and CUPS still calls the job successful - so nothing anywhere reports the failure. Compare CUPS's claim against the printer's counter between jobs and name the workaround (lp -o Resolution=300dpi). 402 tests, 100% branch coverage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RG3eEn8rwZbcXaJrKJxdv8
276 lines
9.3 KiB
Python
276 lines
9.3 KiB
Python
"""Tests for brother_printer.check_brother_printer module."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from io import StringIO
|
|
import subprocess
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from python_pkg.brother_printer.check_brother_printer import (
|
|
_discover_network_printer,
|
|
_no_printer_found,
|
|
_reset_consumable_from_printer,
|
|
_run_network_mode,
|
|
_run_usb_mode,
|
|
main,
|
|
)
|
|
from python_pkg.brother_printer.data_classes import USBResult
|
|
|
|
MOD = "python_pkg.brother_printer.check_brother_printer"
|
|
|
|
|
|
class TestDiscoverNetworkPrinter:
|
|
@patch(f"{MOD}.shutil.which", return_value=None)
|
|
def test_no_lpstat(self, m: MagicMock) -> None:
|
|
assert _discover_network_printer() == ""
|
|
|
|
@patch("python_pkg.brother_printer._query.subprocess.run")
|
|
@patch(f"{MOD}.shutil.which", return_value="/usr/bin/lpstat")
|
|
def test_found_ip(self, w: MagicMock, mock_run: MagicMock) -> None:
|
|
mock_run.return_value = MagicMock(
|
|
stdout="device for BrotherHL1110: ipp://192.168.1.100/ipp\n",
|
|
)
|
|
assert _discover_network_printer() == "192.168.1.100"
|
|
|
|
@patch("python_pkg.brother_printer._query.subprocess.run")
|
|
@patch(f"{MOD}.shutil.which", return_value="/usr/bin/lpstat")
|
|
def test_socket(self, w: MagicMock, mock_run: MagicMock) -> None:
|
|
mock_run.return_value = MagicMock(
|
|
stdout="device for BrotherHL1110: socket://10.0.0.5:9100\n",
|
|
)
|
|
assert _discover_network_printer() == "10.0.0.5"
|
|
|
|
@patch("python_pkg.brother_printer._query.subprocess.run")
|
|
@patch(f"{MOD}.shutil.which", return_value="/usr/bin/lpstat")
|
|
def test_no_match(self, w: MagicMock, mock_run: MagicMock) -> None:
|
|
mock_run.return_value = MagicMock(
|
|
stdout="device for BrotherHL1110: usb://Brother/HL-1110\n",
|
|
)
|
|
assert _discover_network_printer() == ""
|
|
|
|
@patch("python_pkg.brother_printer._query.subprocess.run")
|
|
@patch(f"{MOD}.shutil.which", return_value="/usr/bin/lpstat")
|
|
def test_timeout(self, w: MagicMock, mock_run: MagicMock) -> None:
|
|
mock_run.side_effect = subprocess.TimeoutExpired("lpstat", 5)
|
|
assert _discover_network_printer() == ""
|
|
|
|
@patch("python_pkg.brother_printer._query.subprocess.run")
|
|
@patch(f"{MOD}.shutil.which", return_value="/usr/bin/lpstat")
|
|
def test_oserror(self, w: MagicMock, mock_run: MagicMock) -> None:
|
|
mock_run.side_effect = OSError("fail")
|
|
assert _discover_network_printer() == ""
|
|
|
|
|
|
class TestRunNetworkMode:
|
|
@patch(f"{MOD}.shutil.which", return_value=None)
|
|
def test_no_snmpwalk(self, m: MagicMock) -> None:
|
|
with (
|
|
patch("sys.stdout", new_callable=StringIO),
|
|
pytest.raises(SystemExit),
|
|
):
|
|
_run_network_mode("1.2.3.4")
|
|
|
|
@patch(f"{MOD}.display_network_results")
|
|
@patch(f"{MOD}.query_network_snmp")
|
|
@patch(f"{MOD}.shutil.which", return_value="/usr/bin/snmpwalk")
|
|
def test_success(
|
|
self,
|
|
w: MagicMock,
|
|
mock_query: MagicMock,
|
|
mock_display: MagicMock,
|
|
) -> None:
|
|
from python_pkg.brother_printer.data_classes import NetworkResult
|
|
|
|
mock_query.return_value = NetworkResult(ip="1.2.3.4")
|
|
with patch("sys.stdout", new_callable=StringIO):
|
|
_run_network_mode("1.2.3.4")
|
|
mock_display.assert_called_once()
|
|
|
|
|
|
class TestRunUsbMode:
|
|
@patch(f"{MOD}.display_usb_results")
|
|
@patch(f"{MOD}.query_usb_pjl")
|
|
def test_success(
|
|
self,
|
|
mock_query: MagicMock,
|
|
mock_display: MagicMock,
|
|
) -> None:
|
|
mock_query.return_value = USBResult()
|
|
with patch("sys.stdout", new_callable=StringIO):
|
|
_run_usb_mode("Brother USB line")
|
|
mock_display.assert_called_once()
|
|
|
|
|
|
class TestNoPrinterFound:
|
|
def test_exits(self) -> None:
|
|
with (
|
|
patch("sys.stdout", new_callable=StringIO),
|
|
pytest.raises(SystemExit),
|
|
):
|
|
_no_printer_found()
|
|
|
|
|
|
class TestMain:
|
|
@patch(f"{MOD}.reset_consumable")
|
|
def test_reset_toner(self, mock_reset: MagicMock) -> None:
|
|
main(["--reset-toner"])
|
|
mock_reset.assert_called_once_with("toner")
|
|
|
|
@patch(f"{MOD}.reset_consumable")
|
|
def test_reset_drum(self, mock_reset: MagicMock) -> None:
|
|
main(["--reset-drum"])
|
|
mock_reset.assert_called_once_with("drum")
|
|
|
|
@patch(f"{MOD}.os.geteuid", return_value=1000)
|
|
def test_not_root(self, m: MagicMock) -> None:
|
|
with (
|
|
patch("sys.stdout", new_callable=StringIO),
|
|
pytest.raises(SystemExit),
|
|
):
|
|
main([])
|
|
|
|
@patch(f"{MOD}._run_network_mode")
|
|
@patch(f"{MOD}.os.geteuid", return_value=0)
|
|
def test_with_ip(self, g: MagicMock, mock_net: MagicMock) -> None:
|
|
main(["1.2.3.4"])
|
|
mock_net.assert_called_once_with("1.2.3.4")
|
|
|
|
@patch(f"{MOD}._run_usb_mode")
|
|
@patch(f"{MOD}.find_brother_usb", return_value="Brother USB")
|
|
@patch(f"{MOD}.os.geteuid", return_value=0)
|
|
def test_usb_found(
|
|
self,
|
|
g: MagicMock,
|
|
f: MagicMock,
|
|
mock_usb: MagicMock,
|
|
) -> None:
|
|
main([])
|
|
mock_usb.assert_called_once()
|
|
|
|
def test_network_discovered(self) -> None:
|
|
from python_pkg.brother_printer.data_classes import NetworkResult
|
|
|
|
with (
|
|
patch(f"{MOD}.os.geteuid", return_value=0),
|
|
patch(f"{MOD}.find_brother_usb", return_value=""),
|
|
patch(f"{MOD}._discover_network_printer", return_value="192.168.1.100"),
|
|
patch(f"{MOD}.shutil.which", return_value="/usr/bin/snmpwalk"),
|
|
patch(f"{MOD}.query_network_snmp") as mock_query,
|
|
patch(f"{MOD}.display_network_results") as mock_display,
|
|
patch("sys.stdout", new_callable=StringIO),
|
|
):
|
|
mock_query.return_value = NetworkResult(ip="192.168.1.100")
|
|
main([])
|
|
mock_display.assert_called_once()
|
|
|
|
@patch(f"{MOD}._no_printer_found")
|
|
@patch(f"{MOD}._discover_network_printer", return_value="")
|
|
@patch(f"{MOD}.find_brother_usb", return_value="")
|
|
@patch(f"{MOD}.os.geteuid", return_value=0)
|
|
def test_nothing_found(
|
|
self,
|
|
g: MagicMock,
|
|
f: MagicMock,
|
|
d: MagicMock,
|
|
mock_no: MagicMock,
|
|
) -> None:
|
|
main([])
|
|
mock_no.assert_called_once()
|
|
|
|
@patch(f"{MOD}._no_printer_found")
|
|
@patch(f"{MOD}.shutil.which", return_value=None)
|
|
@patch(f"{MOD}._discover_network_printer", return_value="192.168.1.100")
|
|
@patch(f"{MOD}.find_brother_usb", return_value="")
|
|
@patch(f"{MOD}.os.geteuid", return_value=0)
|
|
def test_network_discovered_no_snmpwalk(
|
|
self,
|
|
g: MagicMock,
|
|
f: MagicMock,
|
|
d: MagicMock,
|
|
w: MagicMock,
|
|
mock_no: MagicMock,
|
|
) -> None:
|
|
main([])
|
|
mock_no.assert_called_once()
|
|
|
|
def test_default_argv(self) -> None:
|
|
with (
|
|
patch(f"{MOD}.sys.argv", ["prog", "--reset-toner"]),
|
|
patch(f"{MOD}.reset_consumable") as mock_reset,
|
|
):
|
|
main()
|
|
mock_reset.assert_called_once_with("toner")
|
|
|
|
@patch(f"{MOD}.os.geteuid", return_value=1000)
|
|
def test_not_root_with_args(self, g: MagicMock) -> None:
|
|
with (
|
|
patch("sys.stdout", new_callable=StringIO),
|
|
pytest.raises(SystemExit),
|
|
):
|
|
main(["1.2.3.4"])
|
|
|
|
|
|
class TestResetConsumableFromPrinter:
|
|
"""Resetting a baseline must use the printer's own counter, or refuse.
|
|
|
|
Baselines live on the printer's lifetime-counter scale. Recording one from
|
|
any other source skews every later estimate, so a printer that cannot be
|
|
read is a hard error rather than a silent fallback.
|
|
"""
|
|
|
|
@patch(f"{MOD}.reset_consumable")
|
|
@patch(f"{MOD}.query_usb_pjl")
|
|
def test_uses_printer_page_count(
|
|
self,
|
|
mock_query: MagicMock,
|
|
mock_reset: MagicMock,
|
|
) -> None:
|
|
mock_query.return_value = USBResult(page_count="2017")
|
|
_reset_consumable_from_printer("toner")
|
|
mock_reset.assert_called_once_with("toner", 2017)
|
|
|
|
@patch(f"{MOD}._out")
|
|
@patch(f"{MOD}.reset_consumable")
|
|
@patch(f"{MOD}.query_usb_pjl")
|
|
def test_refuses_without_page_count(
|
|
self,
|
|
mock_query: MagicMock,
|
|
mock_reset: MagicMock,
|
|
mock_out: MagicMock,
|
|
) -> None:
|
|
mock_query.return_value = USBResult(page_count="")
|
|
with pytest.raises(SystemExit):
|
|
_reset_consumable_from_printer("toner")
|
|
mock_reset.assert_not_called()
|
|
|
|
@patch(f"{MOD}._out")
|
|
@patch(f"{MOD}.reset_consumable")
|
|
@patch(f"{MOD}.query_usb_pjl")
|
|
def test_reports_underlying_error(
|
|
self,
|
|
mock_query: MagicMock,
|
|
mock_reset: MagicMock,
|
|
mock_out: MagicMock,
|
|
) -> None:
|
|
mock_query.return_value = USBResult(page_count="", error="device busy")
|
|
with pytest.raises(SystemExit):
|
|
_reset_consumable_from_printer("drum")
|
|
printed = " ".join(str(c) for c in mock_out.call_args_list)
|
|
assert "device busy" in printed
|
|
|
|
@patch(f"{MOD}._out")
|
|
@patch(f"{MOD}.reset_consumable")
|
|
@patch(f"{MOD}.query_usb_pjl")
|
|
def test_non_numeric_page_count_refused(
|
|
self,
|
|
mock_query: MagicMock,
|
|
mock_reset: MagicMock,
|
|
mock_out: MagicMock,
|
|
) -> None:
|
|
mock_query.return_value = USBResult(page_count="???")
|
|
with pytest.raises(SystemExit):
|
|
_reset_consumable_from_printer("toner")
|
|
mock_reset.assert_not_called()
|