mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-13 20:10:48 +01:00
Clear the last three brother_printer test modules over the cap
test_consumables_part2.py 273 -> 133 (renamed test_counter_scales.py), test_consumables.py 272 -> 123, test_check_brother_printer.py 275 -> 112. python_pkg/brother_printer is now entirely under the 250-line cap, from fifteen violations at the start. The consumables split lands exactly on the MOD/MOD_PD boundary an earlier commit introduced, so the two-constant workaround dissolves instead of being carried forward - each file has one module under test again. Pass count is 402, unchanged across all eight commits of this pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
This commit is contained in:
parent
74eee2b57f
commit
2c01a0267e
@ -0,0 +1,18 @@
|
||||
{
|
||||
"title": "Clear the last three brother_printer test modules over the 250-line cap",
|
||||
"objective": "Bring test_check_brother_printer.py (275 lines), test_consumables.py (272) and test_consumables_part2.py (273) under the repo-wide 250-line cap, taking python_pkg/brother_printer from fifteen violations to zero.",
|
||||
"acceptance_criteria": [
|
||||
"bash ~/utils/scripts/check_file_length.sh --all lists no file under python_pkg/brother_printer.",
|
||||
"The brother_printer suite still reports exactly 402 passing tests -- the same count as at the start of this whole package pass, proving no test was dropped or silently skipped across any of the eight commits.",
|
||||
"The package keeps 100% branch coverage of the brother_printer source modules.",
|
||||
"test_consumables_part2.py's split removes the need for its second MOD_PD constant: the tests that needed it are exactly the ones that move, so each resulting file has one module under test again.",
|
||||
"No assertion is changed and no test is deleted, skipped or weakened; whole classes move verbatim and only the import blocks are adjusted.",
|
||||
"No suppression is added and no coverage exclusion is introduced."
|
||||
],
|
||||
"out_of_scope": [
|
||||
"Changing what any test asserts.",
|
||||
"Touching the brother_printer source, which is already under the cap.",
|
||||
"Other packages' violations, which are separate commits."
|
||||
],
|
||||
"verifier": "PYTHONPATH=. python3 -m pytest python_pkg/brother_printer/tests -q (expects exactly 402 passed), plus bash ~/utils/scripts/check_file_length.sh --all showing no brother_printer entry, plus pre-commit run --files on the touched files."
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
{
|
||||
"intent": "Clear the last three over-cap brother_printer test modules, taking the package from fifteen violations to zero.",
|
||||
"scope": [
|
||||
"python_pkg/brother_printer/tests/test_consumables_part2.py -> test_counter_scales.py (273 -> 133)",
|
||||
"python_pkg/brother_printer/tests/test_page_delivery.py (new)",
|
||||
"python_pkg/brother_printer/tests/test_consumables.py (272 -> 123)",
|
||||
"python_pkg/brother_printer/tests/test_consumable_estimate.py (new)",
|
||||
"python_pkg/brother_printer/tests/test_check_brother_printer.py (275 -> 112)",
|
||||
"python_pkg/brother_printer/tests/test_check_brother_printer_main.py (new)",
|
||||
"Non-goal: no assertion changed, no test deleted, skipped or weakened"
|
||||
],
|
||||
"changes": [
|
||||
"test_consumables_part2.py split at TestCheckPageDelivery, which is exactly the MOD/MOD_PD boundary an earlier commit introduced. The two classes that needed MOD_PD move to test_page_delivery.py and use a plain MOD there, so the two-constant workaround dissolves rather than being carried forward: each file has one module under test again.",
|
||||
"test_consumables.py split at TestEstimateConsumableLife: the state file and CUPS page-log tests stay, the life estimate and replacement resets go to test_consumable_estimate.py.",
|
||||
"test_check_brother_printer.py split at TestMain: discovery and the network/USB mode selection stay, the CLI entry point and the reset flags go to test_check_brother_printer_main.py.",
|
||||
"Renamed test_consumables_part2.py to test_counter_scales.py and narrowed its docstring, which had claimed to cover dropped-page detection that now lives in test_page_delivery.py."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "PYTHONPATH=. python3 -m pytest python_pkg/brother_printer/tests -q",
|
||||
"result": "pass",
|
||||
"evidence": "402 passed - the same count as before the first of the eight commits in this package pass, so nothing was lost anywhere along the way."
|
||||
},
|
||||
{
|
||||
"command": "PYTHONPATH=. python3 -m pytest python_pkg/brother_printer/tests -q (first run after the check_brother_printer split)",
|
||||
"result": "fail then pass",
|
||||
"evidence": "Reported 4 failed, 398 passed: TestResetConsumableFromPrinter travelled with TestMain and needed USBResult, which my hand-written import block had omitted. Added; the count returned to 402."
|
||||
},
|
||||
{
|
||||
"command": "bash ~/utils/scripts/check_file_length.sh --all | grep brother_printer",
|
||||
"result": "pass",
|
||||
"evidence": "No output - python_pkg/brother_printer is entirely under the 250-line cap, down from fifteen violations at the start of the session."
|
||||
},
|
||||
{
|
||||
"command": "pre-commit run --files <the touched test files and artifacts>",
|
||||
"result": "pass",
|
||||
"evidence": "ruff, ruff format, mypy, pylint, bandit, name-tests-test and pytest-with-coverage all pass."
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"Hand-trimmed import blocks remain the recurring risk in test splits; this pass hit it three times and the pass-count check caught it every time. A test that was already skipped would not be caught this way, but the package has none.",
|
||||
"Several test modules are now named after the private module they cover (_page_delivery, _pjl_io, _queue_fix). If a future split renames one of those source modules, the test file names go stale even though nothing breaks."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit; the three originals return at 275, 272 and 273 lines and the three new modules disappear.",
|
||||
"After rollback, re-run the package suite and confirm it still reports 402 passed."
|
||||
]
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
"""Tests for brother_printer.check_brother_printer module."""
|
||||
"""Tests for brother_printer.check_brother_printer - discovery and modes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@ -11,10 +11,8 @@ 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
|
||||
|
||||
@ -110,166 +108,3 @@ class TestNoPrinterFound:
|
||||
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()
|
||||
|
||||
@ -0,0 +1,183 @@
|
||||
"""Tests for brother_printer.check_brother_printer - the CLI entry point.
|
||||
|
||||
Split from test_check_brother_printer.py under the 250-line cap. These cover
|
||||
argument handling, mode selection and the consumable reset flags.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from io import StringIO
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from python_pkg.brother_printer.check_brother_printer import (
|
||||
_reset_consumable_from_printer,
|
||||
main,
|
||||
)
|
||||
from python_pkg.brother_printer.data_classes import USBResult
|
||||
|
||||
MOD = "python_pkg.brother_printer.check_brother_printer"
|
||||
|
||||
|
||||
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()
|
||||
167
python_pkg/brother_printer/tests/test_consumable_estimate.py
Normal file
167
python_pkg/brother_printer/tests/test_consumable_estimate.py
Normal file
@ -0,0 +1,167 @@
|
||||
"""Tests for brother_printer.consumables - the toner and drum life estimate.
|
||||
|
||||
Split from test_consumables.py under the 250-line cap. These cover which
|
||||
counter the estimate is based on and how replacement resets rebase it.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from python_pkg.brother_printer.consumables import (
|
||||
STATE_SCHEMA_CUPS_SCALE,
|
||||
STATE_SCHEMA_PRINTER_SCALE,
|
||||
estimate_consumable_life,
|
||||
reset_consumable,
|
||||
)
|
||||
|
||||
MOD = "python_pkg.brother_printer.consumables"
|
||||
|
||||
|
||||
class TestEstimateConsumableLife:
|
||||
@patch(f"{MOD}._load_consumable_state")
|
||||
@patch(f"{MOD}._get_cups_total_pages", return_value=0)
|
||||
def test_no_pages(self, p: MagicMock, mock_load: MagicMock) -> None:
|
||||
result = estimate_consumable_life()
|
||||
assert result.total_pages == 0
|
||||
|
||||
@patch(f"{MOD}._load_consumable_state")
|
||||
@patch(f"{MOD}._get_cups_total_pages", return_value=500)
|
||||
def test_mid_life(self, p: MagicMock, mock_load: MagicMock) -> None:
|
||||
mock_load.return_value = {
|
||||
"toner_replaced_at": 0,
|
||||
"drum_replaced_at": 0,
|
||||
"last_printer_count": 0,
|
||||
"last_cups_total": 0,
|
||||
}
|
||||
result = estimate_consumable_life()
|
||||
assert result.total_pages == 500
|
||||
assert result.toner_pct_remaining == 50
|
||||
assert result.toner_exhausted is False
|
||||
assert result.toner_low is False
|
||||
|
||||
@patch(f"{MOD}._load_consumable_state")
|
||||
@patch(f"{MOD}._get_cups_total_pages", return_value=1000)
|
||||
def test_toner_exhausted(self, p: MagicMock, mock_load: MagicMock) -> None:
|
||||
mock_load.return_value = {
|
||||
"toner_replaced_at": 0,
|
||||
"drum_replaced_at": 0,
|
||||
"last_printer_count": 0,
|
||||
"last_cups_total": 0,
|
||||
}
|
||||
result = estimate_consumable_life()
|
||||
assert result.toner_exhausted is True
|
||||
|
||||
@patch(f"{MOD}._load_consumable_state")
|
||||
@patch(f"{MOD}._get_cups_total_pages", return_value=800)
|
||||
def test_toner_low(self, p: MagicMock, mock_load: MagicMock) -> None:
|
||||
mock_load.return_value = {
|
||||
"toner_replaced_at": 0,
|
||||
"drum_replaced_at": 0,
|
||||
"last_printer_count": 0,
|
||||
"last_cups_total": 0,
|
||||
}
|
||||
result = estimate_consumable_life()
|
||||
assert result.toner_low is True
|
||||
|
||||
@patch(f"{MOD}._load_consumable_state")
|
||||
@patch(f"{MOD}._get_cups_total_pages", return_value=9000)
|
||||
def test_drum_near_end(self, p: MagicMock, mock_load: MagicMock) -> None:
|
||||
mock_load.return_value = {
|
||||
"toner_replaced_at": 8500,
|
||||
"drum_replaced_at": 0,
|
||||
"last_printer_count": 0,
|
||||
"last_cups_total": 0,
|
||||
}
|
||||
result = estimate_consumable_life()
|
||||
assert result.drum_near_end is True
|
||||
|
||||
|
||||
class TestEstimateConsumableLifeCounterSource:
|
||||
"""Which counter the estimate came from, and whether it says so."""
|
||||
|
||||
@patch(f"{MOD}._migrate_state_to_printer_scale")
|
||||
@patch(f"{MOD}._load_consumable_state")
|
||||
@patch(f"{MOD}._get_cups_total_pages", return_value=1658)
|
||||
def test_printer_counter_is_authoritative(
|
||||
self,
|
||||
mock_cups: MagicMock,
|
||||
mock_load: MagicMock,
|
||||
mock_migrate: MagicMock,
|
||||
) -> None:
|
||||
mock_load.return_value = {
|
||||
"toner_replaced_at": 1767,
|
||||
"drum_replaced_at": 0,
|
||||
"last_printer_count": 0,
|
||||
"last_cups_total": 0,
|
||||
}
|
||||
mock_migrate.side_effect = lambda state, total: state
|
||||
estimate = estimate_consumable_life(2017)
|
||||
assert estimate.total_pages == 2017
|
||||
assert estimate.approximate is False
|
||||
assert estimate.toner_pages == 250
|
||||
|
||||
@patch(f"{MOD}._load_consumable_state")
|
||||
@patch(f"{MOD}._get_cups_total_pages", return_value=1658)
|
||||
def test_falls_back_to_cups_log_and_flags_it(
|
||||
self,
|
||||
mock_cups: MagicMock,
|
||||
mock_load: MagicMock,
|
||||
) -> None:
|
||||
"""Without the printer's counter, say the number is approximate."""
|
||||
mock_load.return_value = {
|
||||
"toner_replaced_at": 0,
|
||||
"drum_replaced_at": 0,
|
||||
"last_printer_count": 0,
|
||||
"last_cups_total": 0,
|
||||
}
|
||||
estimate = estimate_consumable_life(0)
|
||||
assert estimate.total_pages == 1658
|
||||
assert estimate.approximate is True
|
||||
|
||||
@patch(f"{MOD}._get_cups_total_pages", return_value=0)
|
||||
def test_no_counter_at_all(self, mock_cups: MagicMock) -> None:
|
||||
assert estimate_consumable_life(0).total_pages == 0
|
||||
|
||||
|
||||
class TestResetConsumableScale:
|
||||
@patch(f"{MOD}._out")
|
||||
@patch(f"{MOD}._save_consumable_state")
|
||||
@patch(f"{MOD}._load_consumable_state")
|
||||
def test_printer_total_marks_state_migrated(
|
||||
self,
|
||||
mock_load: MagicMock,
|
||||
mock_save: MagicMock,
|
||||
mock_out: MagicMock,
|
||||
) -> None:
|
||||
"""A baseline written from the printer is already on the new scale."""
|
||||
mock_load.return_value = {
|
||||
"toner_replaced_at": 0,
|
||||
"drum_replaced_at": 0,
|
||||
"schema": STATE_SCHEMA_CUPS_SCALE,
|
||||
}
|
||||
reset_consumable("toner", 2017)
|
||||
saved = mock_save.call_args[0][0]
|
||||
assert saved["toner_replaced_at"] == 2017
|
||||
assert saved["schema"] == STATE_SCHEMA_PRINTER_SCALE
|
||||
|
||||
@patch(f"{MOD}._out")
|
||||
@patch(f"{MOD}._save_consumable_state")
|
||||
@patch(f"{MOD}._load_consumable_state")
|
||||
@patch(f"{MOD}._get_cups_total_pages", return_value=1658)
|
||||
def test_without_printer_total_uses_cups_log(
|
||||
self,
|
||||
mock_cups: MagicMock,
|
||||
mock_load: MagicMock,
|
||||
mock_save: MagicMock,
|
||||
mock_out: MagicMock,
|
||||
) -> None:
|
||||
mock_load.return_value = {
|
||||
"toner_replaced_at": 0,
|
||||
"drum_replaced_at": 0,
|
||||
"schema": STATE_SCHEMA_CUPS_SCALE,
|
||||
}
|
||||
reset_consumable("drum")
|
||||
saved = mock_save.call_args[0][0]
|
||||
assert saved["drum_replaced_at"] == 1658
|
||||
assert saved["schema"] == STATE_SCHEMA_CUPS_SCALE
|
||||
@ -1,4 +1,4 @@
|
||||
"""Tests for brother_printer.consumables - state, estimates, resets."""
|
||||
"""Tests for brother_printer.consumables - the state file and page log."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@ -7,11 +7,9 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
from python_pkg.brother_printer.consumables import (
|
||||
STATE_SCHEMA_CUPS_SCALE,
|
||||
STATE_SCHEMA_PRINTER_SCALE,
|
||||
_get_cups_total_pages,
|
||||
_load_consumable_state,
|
||||
_save_consumable_state,
|
||||
estimate_consumable_life,
|
||||
reset_consumable,
|
||||
)
|
||||
|
||||
@ -121,152 +119,3 @@ class TestResetConsumable:
|
||||
reset_consumable("toner")
|
||||
saved_state = mock_save.call_args[0][0]
|
||||
assert saved_state["toner_replaced_at"] == 500
|
||||
|
||||
|
||||
class TestEstimateConsumableLife:
|
||||
@patch(f"{MOD}._load_consumable_state")
|
||||
@patch(f"{MOD}._get_cups_total_pages", return_value=0)
|
||||
def test_no_pages(self, p: MagicMock, mock_load: MagicMock) -> None:
|
||||
result = estimate_consumable_life()
|
||||
assert result.total_pages == 0
|
||||
|
||||
@patch(f"{MOD}._load_consumable_state")
|
||||
@patch(f"{MOD}._get_cups_total_pages", return_value=500)
|
||||
def test_mid_life(self, p: MagicMock, mock_load: MagicMock) -> None:
|
||||
mock_load.return_value = {
|
||||
"toner_replaced_at": 0,
|
||||
"drum_replaced_at": 0,
|
||||
"last_printer_count": 0,
|
||||
"last_cups_total": 0,
|
||||
}
|
||||
result = estimate_consumable_life()
|
||||
assert result.total_pages == 500
|
||||
assert result.toner_pct_remaining == 50
|
||||
assert result.toner_exhausted is False
|
||||
assert result.toner_low is False
|
||||
|
||||
@patch(f"{MOD}._load_consumable_state")
|
||||
@patch(f"{MOD}._get_cups_total_pages", return_value=1000)
|
||||
def test_toner_exhausted(self, p: MagicMock, mock_load: MagicMock) -> None:
|
||||
mock_load.return_value = {
|
||||
"toner_replaced_at": 0,
|
||||
"drum_replaced_at": 0,
|
||||
"last_printer_count": 0,
|
||||
"last_cups_total": 0,
|
||||
}
|
||||
result = estimate_consumable_life()
|
||||
assert result.toner_exhausted is True
|
||||
|
||||
@patch(f"{MOD}._load_consumable_state")
|
||||
@patch(f"{MOD}._get_cups_total_pages", return_value=800)
|
||||
def test_toner_low(self, p: MagicMock, mock_load: MagicMock) -> None:
|
||||
mock_load.return_value = {
|
||||
"toner_replaced_at": 0,
|
||||
"drum_replaced_at": 0,
|
||||
"last_printer_count": 0,
|
||||
"last_cups_total": 0,
|
||||
}
|
||||
result = estimate_consumable_life()
|
||||
assert result.toner_low is True
|
||||
|
||||
@patch(f"{MOD}._load_consumable_state")
|
||||
@patch(f"{MOD}._get_cups_total_pages", return_value=9000)
|
||||
def test_drum_near_end(self, p: MagicMock, mock_load: MagicMock) -> None:
|
||||
mock_load.return_value = {
|
||||
"toner_replaced_at": 8500,
|
||||
"drum_replaced_at": 0,
|
||||
"last_printer_count": 0,
|
||||
"last_cups_total": 0,
|
||||
}
|
||||
result = estimate_consumable_life()
|
||||
assert result.drum_near_end is True
|
||||
|
||||
|
||||
class TestEstimateConsumableLifeCounterSource:
|
||||
"""Which counter the estimate came from, and whether it says so."""
|
||||
|
||||
@patch(f"{MOD}._migrate_state_to_printer_scale")
|
||||
@patch(f"{MOD}._load_consumable_state")
|
||||
@patch(f"{MOD}._get_cups_total_pages", return_value=1658)
|
||||
def test_printer_counter_is_authoritative(
|
||||
self,
|
||||
mock_cups: MagicMock,
|
||||
mock_load: MagicMock,
|
||||
mock_migrate: MagicMock,
|
||||
) -> None:
|
||||
mock_load.return_value = {
|
||||
"toner_replaced_at": 1767,
|
||||
"drum_replaced_at": 0,
|
||||
"last_printer_count": 0,
|
||||
"last_cups_total": 0,
|
||||
}
|
||||
mock_migrate.side_effect = lambda state, total: state
|
||||
estimate = estimate_consumable_life(2017)
|
||||
assert estimate.total_pages == 2017
|
||||
assert estimate.approximate is False
|
||||
assert estimate.toner_pages == 250
|
||||
|
||||
@patch(f"{MOD}._load_consumable_state")
|
||||
@patch(f"{MOD}._get_cups_total_pages", return_value=1658)
|
||||
def test_falls_back_to_cups_log_and_flags_it(
|
||||
self,
|
||||
mock_cups: MagicMock,
|
||||
mock_load: MagicMock,
|
||||
) -> None:
|
||||
"""Without the printer's counter, say the number is approximate."""
|
||||
mock_load.return_value = {
|
||||
"toner_replaced_at": 0,
|
||||
"drum_replaced_at": 0,
|
||||
"last_printer_count": 0,
|
||||
"last_cups_total": 0,
|
||||
}
|
||||
estimate = estimate_consumable_life(0)
|
||||
assert estimate.total_pages == 1658
|
||||
assert estimate.approximate is True
|
||||
|
||||
@patch(f"{MOD}._get_cups_total_pages", return_value=0)
|
||||
def test_no_counter_at_all(self, mock_cups: MagicMock) -> None:
|
||||
assert estimate_consumable_life(0).total_pages == 0
|
||||
|
||||
|
||||
class TestResetConsumableScale:
|
||||
@patch(f"{MOD}._out")
|
||||
@patch(f"{MOD}._save_consumable_state")
|
||||
@patch(f"{MOD}._load_consumable_state")
|
||||
def test_printer_total_marks_state_migrated(
|
||||
self,
|
||||
mock_load: MagicMock,
|
||||
mock_save: MagicMock,
|
||||
mock_out: MagicMock,
|
||||
) -> None:
|
||||
"""A baseline written from the printer is already on the new scale."""
|
||||
mock_load.return_value = {
|
||||
"toner_replaced_at": 0,
|
||||
"drum_replaced_at": 0,
|
||||
"schema": STATE_SCHEMA_CUPS_SCALE,
|
||||
}
|
||||
reset_consumable("toner", 2017)
|
||||
saved = mock_save.call_args[0][0]
|
||||
assert saved["toner_replaced_at"] == 2017
|
||||
assert saved["schema"] == STATE_SCHEMA_PRINTER_SCALE
|
||||
|
||||
@patch(f"{MOD}._out")
|
||||
@patch(f"{MOD}._save_consumable_state")
|
||||
@patch(f"{MOD}._load_consumable_state")
|
||||
@patch(f"{MOD}._get_cups_total_pages", return_value=1658)
|
||||
def test_without_printer_total_uses_cups_log(
|
||||
self,
|
||||
mock_cups: MagicMock,
|
||||
mock_load: MagicMock,
|
||||
mock_save: MagicMock,
|
||||
mock_out: MagicMock,
|
||||
) -> None:
|
||||
mock_load.return_value = {
|
||||
"toner_replaced_at": 0,
|
||||
"drum_replaced_at": 0,
|
||||
"schema": STATE_SCHEMA_CUPS_SCALE,
|
||||
}
|
||||
reset_consumable("drum")
|
||||
saved = mock_save.call_args[0][0]
|
||||
assert saved["drum_replaced_at"] == 1658
|
||||
assert saved["schema"] == STATE_SCHEMA_CUPS_SCALE
|
||||
|
||||
@ -1,273 +0,0 @@
|
||||
"""Tests for brother_printer.consumables - counter scales and dropped pages.
|
||||
|
||||
The printer's counter and the CUPS page log measure different things. These
|
||||
tests pin the rebasing between them and the dropped-page detection that the
|
||||
gap makes possible.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from python_pkg.brother_printer._page_delivery import (
|
||||
_snapshot_counters,
|
||||
check_page_delivery,
|
||||
)
|
||||
from python_pkg.brother_printer.consumables import (
|
||||
STATE_SCHEMA_CUPS_SCALE,
|
||||
STATE_SCHEMA_PRINTER_SCALE,
|
||||
_cups_total_on_printer_scale,
|
||||
_migrate_state_to_printer_scale,
|
||||
)
|
||||
|
||||
MOD = "python_pkg.brother_printer.consumables"
|
||||
# check_page_delivery and _snapshot_counters moved to _page_delivery, so the
|
||||
# state-file helpers they call resolve through that module's globals instead.
|
||||
MOD_PD = "python_pkg.brother_printer._page_delivery"
|
||||
|
||||
|
||||
class TestMigrateStateToPrinterScale:
|
||||
"""Rebasing replacement baselines from the CUPS page log onto the printer.
|
||||
|
||||
The baselines are recorded against whatever counter was in use when they
|
||||
were written. Switching counters without rebasing them silently changes
|
||||
every reported percentage, so this is the migration that keeps the numbers
|
||||
meaning the same thing before and after.
|
||||
"""
|
||||
|
||||
@patch(f"{MOD}._save_consumable_state")
|
||||
@patch(f"{MOD}._get_cups_total_pages", return_value=1658)
|
||||
def test_shifts_baseline_by_offset(
|
||||
self,
|
||||
mock_total: MagicMock,
|
||||
mock_save: MagicMock,
|
||||
) -> None:
|
||||
state = {
|
||||
"toner_replaced_at": 1408,
|
||||
"drum_replaced_at": 0,
|
||||
"schema": STATE_SCHEMA_CUPS_SCALE,
|
||||
}
|
||||
result = _migrate_state_to_printer_scale(state, 2017)
|
||||
# Offset is 2017 - 1658 = 359.
|
||||
assert result["toner_replaced_at"] == 1767
|
||||
assert result["schema"] == STATE_SCHEMA_PRINTER_SCALE
|
||||
mock_save.assert_called_once()
|
||||
|
||||
@patch(f"{MOD}._save_consumable_state")
|
||||
@patch(f"{MOD}._get_cups_total_pages", return_value=1658)
|
||||
def test_zero_baseline_left_alone(
|
||||
self,
|
||||
mock_total: MagicMock,
|
||||
mock_save: MagicMock,
|
||||
) -> None:
|
||||
"""Zero means 'never replaced', not 'replaced at page zero'.
|
||||
|
||||
On the printer's scale zero already means "as old as the printer",
|
||||
which is the truthful reading; shifting it would re-hide the pages the
|
||||
CUPS log never saw.
|
||||
"""
|
||||
state = {
|
||||
"toner_replaced_at": 0,
|
||||
"drum_replaced_at": 0,
|
||||
"schema": STATE_SCHEMA_CUPS_SCALE,
|
||||
}
|
||||
result = _migrate_state_to_printer_scale(state, 2017)
|
||||
assert result["drum_replaced_at"] == 0
|
||||
assert result["toner_replaced_at"] == 0
|
||||
|
||||
@patch(f"{MOD}._save_consumable_state")
|
||||
@patch(f"{MOD}._get_cups_total_pages", return_value=1658)
|
||||
def test_runs_only_once(
|
||||
self,
|
||||
mock_total: MagicMock,
|
||||
mock_save: MagicMock,
|
||||
) -> None:
|
||||
"""A second run must not shift the baselines again."""
|
||||
state = {
|
||||
"toner_replaced_at": 1767,
|
||||
"drum_replaced_at": 0,
|
||||
"schema": STATE_SCHEMA_PRINTER_SCALE,
|
||||
}
|
||||
result = _migrate_state_to_printer_scale(state, 2017)
|
||||
assert result["toner_replaced_at"] == 1767
|
||||
mock_save.assert_not_called()
|
||||
|
||||
@patch(f"{MOD}._save_consumable_state")
|
||||
@patch(f"{MOD}._get_cups_total_pages", return_value=2100)
|
||||
def test_negative_offset_leaves_baselines(
|
||||
self,
|
||||
mock_total: MagicMock,
|
||||
mock_save: MagicMock,
|
||||
) -> None:
|
||||
"""If the CUPS log somehow exceeds the printer, do not corrupt state."""
|
||||
state = {
|
||||
"toner_replaced_at": 1408,
|
||||
"drum_replaced_at": 0,
|
||||
"schema": STATE_SCHEMA_CUPS_SCALE,
|
||||
}
|
||||
result = _migrate_state_to_printer_scale(state, 2017)
|
||||
assert result["toner_replaced_at"] == 1408
|
||||
assert result["schema"] == STATE_SCHEMA_PRINTER_SCALE
|
||||
|
||||
|
||||
class TestCheckPageDelivery:
|
||||
"""Spotting pages the printer silently dropped.
|
||||
|
||||
CUPS calls a job successful once the data leaves the machine, so this
|
||||
comparison against the printer's own counter is the only signal that a page
|
||||
never actually came out.
|
||||
"""
|
||||
|
||||
@patch(f"{MOD_PD}._snapshot_counters")
|
||||
@patch(f"{MOD_PD}._get_cups_total_pages", return_value=1841)
|
||||
@patch(f"{MOD_PD}._load_consumable_state")
|
||||
def test_detects_dropped_pages(
|
||||
self,
|
||||
mock_load: MagicMock,
|
||||
mock_cups: MagicMock,
|
||||
mock_snap: MagicMock,
|
||||
) -> None:
|
||||
"""Replay of the real failure: CUPS sent 63, printer printed none."""
|
||||
mock_load.return_value = {
|
||||
"last_printer_count": 2087,
|
||||
"last_cups_total": 1778,
|
||||
}
|
||||
check = check_page_delivery(2087, queue_idle=True)
|
||||
assert check.cups_pages == 63
|
||||
assert check.printer_pages == 0
|
||||
assert check.dropped == 63
|
||||
assert check.suspected is True
|
||||
|
||||
@patch(f"{MOD_PD}._snapshot_counters")
|
||||
@patch(f"{MOD_PD}._get_cups_total_pages", return_value=1841)
|
||||
@patch(f"{MOD_PD}._load_consumable_state")
|
||||
def test_healthy_job_does_not_warn(
|
||||
self,
|
||||
mock_load: MagicMock,
|
||||
mock_cups: MagicMock,
|
||||
mock_snap: MagicMock,
|
||||
) -> None:
|
||||
mock_load.return_value = {
|
||||
"last_printer_count": 2024,
|
||||
"last_cups_total": 1778,
|
||||
}
|
||||
check = check_page_delivery(2087, queue_idle=True)
|
||||
assert check.dropped == 0
|
||||
assert check.suspected is False
|
||||
|
||||
@patch(f"{MOD_PD}._snapshot_counters")
|
||||
@patch(f"{MOD_PD}._get_cups_total_pages", return_value=1780)
|
||||
@patch(f"{MOD_PD}._load_consumable_state")
|
||||
def test_small_gap_is_not_a_warning(
|
||||
self,
|
||||
mock_load: MagicMock,
|
||||
mock_cups: MagicMock,
|
||||
mock_snap: MagicMock,
|
||||
) -> None:
|
||||
"""A page or two of drift is timing, not a fault."""
|
||||
mock_load.return_value = {
|
||||
"last_printer_count": 2087,
|
||||
"last_cups_total": 1778,
|
||||
}
|
||||
check = check_page_delivery(2087, queue_idle=True)
|
||||
assert check.dropped == 2
|
||||
assert check.suspected is False
|
||||
|
||||
def test_skipped_while_printing(self) -> None:
|
||||
"""Mid-job the counters legitimately disagree, so never compare."""
|
||||
assert check_page_delivery(2087, queue_idle=False).suspected is False
|
||||
|
||||
def test_skipped_without_printer_count(self) -> None:
|
||||
assert check_page_delivery(0, queue_idle=True).suspected is False
|
||||
|
||||
@patch(f"{MOD_PD}._snapshot_counters")
|
||||
@patch(f"{MOD_PD}._get_cups_total_pages", return_value=1841)
|
||||
@patch(f"{MOD_PD}._load_consumable_state")
|
||||
def test_first_run_only_establishes_baseline(
|
||||
self,
|
||||
mock_load: MagicMock,
|
||||
mock_cups: MagicMock,
|
||||
mock_snap: MagicMock,
|
||||
) -> None:
|
||||
mock_load.return_value = {"last_printer_count": 0, "last_cups_total": 0}
|
||||
check = check_page_delivery(2087, queue_idle=True)
|
||||
assert check.suspected is False
|
||||
mock_snap.assert_called_once()
|
||||
|
||||
@patch(f"{MOD_PD}._snapshot_counters")
|
||||
@patch(f"{MOD_PD}._get_cups_total_pages", return_value=10)
|
||||
@patch(f"{MOD_PD}._load_consumable_state")
|
||||
def test_log_rotation_draws_no_conclusion(
|
||||
self,
|
||||
mock_load: MagicMock,
|
||||
mock_cups: MagicMock,
|
||||
mock_snap: MagicMock,
|
||||
) -> None:
|
||||
"""A rotated page log makes the delta negative; do not cry wolf."""
|
||||
mock_load.return_value = {
|
||||
"last_printer_count": 2087,
|
||||
"last_cups_total": 1778,
|
||||
}
|
||||
check = check_page_delivery(2087, queue_idle=True)
|
||||
assert check.suspected is False
|
||||
|
||||
@patch(f"{MOD_PD}._snapshot_counters")
|
||||
@patch(f"{MOD_PD}._get_cups_total_pages", return_value=1841)
|
||||
@patch(f"{MOD_PD}._load_consumable_state")
|
||||
def test_printer_counter_reset_draws_no_conclusion(
|
||||
self,
|
||||
mock_load: MagicMock,
|
||||
mock_cups: MagicMock,
|
||||
mock_snap: MagicMock,
|
||||
) -> None:
|
||||
mock_load.return_value = {
|
||||
"last_printer_count": 5000,
|
||||
"last_cups_total": 1778,
|
||||
}
|
||||
check = check_page_delivery(2087, queue_idle=True)
|
||||
assert check.suspected is False
|
||||
|
||||
|
||||
class TestSnapshotCounters:
|
||||
@patch(f"{MOD_PD}._save_consumable_state")
|
||||
def test_writes_new_snapshot(self, mock_save: MagicMock) -> None:
|
||||
state = {"last_printer_count": 2000, "last_cups_total": 1700}
|
||||
_snapshot_counters(state, 2087, 1778)
|
||||
saved = mock_save.call_args[0][0]
|
||||
assert saved["last_printer_count"] == 2087
|
||||
assert saved["last_cups_total"] == 1778
|
||||
|
||||
@patch(f"{MOD_PD}._save_consumable_state")
|
||||
def test_unchanged_counters_skip_the_write(self, mock_save: MagicMock) -> None:
|
||||
"""Do not rewrite the file on every run when nothing moved."""
|
||||
state = {"last_printer_count": 2087, "last_cups_total": 1778}
|
||||
_snapshot_counters(state, 2087, 1778)
|
||||
mock_save.assert_not_called()
|
||||
|
||||
|
||||
class TestCupsTotalOnPrinterScale:
|
||||
"""The CUPS figure must be shifted onto the scale the baselines use.
|
||||
|
||||
Without the shift the raw CUPS total sits below a printer-scale baseline,
|
||||
the subtraction clamps at zero, and a part-used cartridge reads 100%.
|
||||
"""
|
||||
|
||||
@patch(f"{MOD}._get_cups_total_pages", return_value=1778)
|
||||
def test_applies_offset(self, mock_cups: MagicMock) -> None:
|
||||
state = {"last_printer_count": 2087, "last_cups_total": 1778}
|
||||
assert _cups_total_on_printer_scale(state) == 2087
|
||||
|
||||
@patch(f"{MOD}._get_cups_total_pages", return_value=1778)
|
||||
def test_no_snapshot_returns_raw(self, mock_cups: MagicMock) -> None:
|
||||
state = {"last_printer_count": 0, "last_cups_total": 0}
|
||||
assert _cups_total_on_printer_scale(state) == 1778
|
||||
|
||||
@patch(f"{MOD}._get_cups_total_pages", return_value=1778)
|
||||
def test_negative_offset_returns_raw(self, mock_cups: MagicMock) -> None:
|
||||
state = {"last_printer_count": 1700, "last_cups_total": 1778}
|
||||
assert _cups_total_on_printer_scale(state) == 1778
|
||||
|
||||
@patch(f"{MOD}._get_cups_total_pages", return_value=0)
|
||||
def test_no_cups_pages(self, mock_cups: MagicMock) -> None:
|
||||
state = {"last_printer_count": 2087, "last_cups_total": 1778}
|
||||
assert _cups_total_on_printer_scale(state) == 0
|
||||
131
python_pkg/brother_printer/tests/test_counter_scales.py
Normal file
131
python_pkg/brother_printer/tests/test_counter_scales.py
Normal file
@ -0,0 +1,131 @@
|
||||
"""Tests for brother_printer.consumables - rebasing the counter scales.
|
||||
|
||||
The printer's counter and the CUPS page log measure different things. These
|
||||
tests pin the rebasing between them; the dropped-page detection that the gap
|
||||
makes possible is covered in test_page_delivery.py.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from python_pkg.brother_printer.consumables import (
|
||||
STATE_SCHEMA_CUPS_SCALE,
|
||||
STATE_SCHEMA_PRINTER_SCALE,
|
||||
_cups_total_on_printer_scale,
|
||||
_migrate_state_to_printer_scale,
|
||||
)
|
||||
|
||||
MOD = "python_pkg.brother_printer.consumables"
|
||||
|
||||
|
||||
class TestMigrateStateToPrinterScale:
|
||||
"""Rebasing replacement baselines from the CUPS page log onto the printer.
|
||||
|
||||
The baselines are recorded against whatever counter was in use when they
|
||||
were written. Switching counters without rebasing them silently changes
|
||||
every reported percentage, so this is the migration that keeps the numbers
|
||||
meaning the same thing before and after.
|
||||
"""
|
||||
|
||||
@patch(f"{MOD}._save_consumable_state")
|
||||
@patch(f"{MOD}._get_cups_total_pages", return_value=1658)
|
||||
def test_shifts_baseline_by_offset(
|
||||
self,
|
||||
mock_total: MagicMock,
|
||||
mock_save: MagicMock,
|
||||
) -> None:
|
||||
state = {
|
||||
"toner_replaced_at": 1408,
|
||||
"drum_replaced_at": 0,
|
||||
"schema": STATE_SCHEMA_CUPS_SCALE,
|
||||
}
|
||||
result = _migrate_state_to_printer_scale(state, 2017)
|
||||
# Offset is 2017 - 1658 = 359.
|
||||
assert result["toner_replaced_at"] == 1767
|
||||
assert result["schema"] == STATE_SCHEMA_PRINTER_SCALE
|
||||
mock_save.assert_called_once()
|
||||
|
||||
@patch(f"{MOD}._save_consumable_state")
|
||||
@patch(f"{MOD}._get_cups_total_pages", return_value=1658)
|
||||
def test_zero_baseline_left_alone(
|
||||
self,
|
||||
mock_total: MagicMock,
|
||||
mock_save: MagicMock,
|
||||
) -> None:
|
||||
"""Zero means 'never replaced', not 'replaced at page zero'.
|
||||
|
||||
On the printer's scale zero already means "as old as the printer",
|
||||
which is the truthful reading; shifting it would re-hide the pages the
|
||||
CUPS log never saw.
|
||||
"""
|
||||
state = {
|
||||
"toner_replaced_at": 0,
|
||||
"drum_replaced_at": 0,
|
||||
"schema": STATE_SCHEMA_CUPS_SCALE,
|
||||
}
|
||||
result = _migrate_state_to_printer_scale(state, 2017)
|
||||
assert result["drum_replaced_at"] == 0
|
||||
assert result["toner_replaced_at"] == 0
|
||||
|
||||
@patch(f"{MOD}._save_consumable_state")
|
||||
@patch(f"{MOD}._get_cups_total_pages", return_value=1658)
|
||||
def test_runs_only_once(
|
||||
self,
|
||||
mock_total: MagicMock,
|
||||
mock_save: MagicMock,
|
||||
) -> None:
|
||||
"""A second run must not shift the baselines again."""
|
||||
state = {
|
||||
"toner_replaced_at": 1767,
|
||||
"drum_replaced_at": 0,
|
||||
"schema": STATE_SCHEMA_PRINTER_SCALE,
|
||||
}
|
||||
result = _migrate_state_to_printer_scale(state, 2017)
|
||||
assert result["toner_replaced_at"] == 1767
|
||||
mock_save.assert_not_called()
|
||||
|
||||
@patch(f"{MOD}._save_consumable_state")
|
||||
@patch(f"{MOD}._get_cups_total_pages", return_value=2100)
|
||||
def test_negative_offset_leaves_baselines(
|
||||
self,
|
||||
mock_total: MagicMock,
|
||||
mock_save: MagicMock,
|
||||
) -> None:
|
||||
"""If the CUPS log somehow exceeds the printer, do not corrupt state."""
|
||||
state = {
|
||||
"toner_replaced_at": 1408,
|
||||
"drum_replaced_at": 0,
|
||||
"schema": STATE_SCHEMA_CUPS_SCALE,
|
||||
}
|
||||
result = _migrate_state_to_printer_scale(state, 2017)
|
||||
assert result["toner_replaced_at"] == 1408
|
||||
assert result["schema"] == STATE_SCHEMA_PRINTER_SCALE
|
||||
|
||||
|
||||
class TestCupsTotalOnPrinterScale:
|
||||
"""The CUPS figure must be shifted onto the scale the baselines use.
|
||||
|
||||
Without the shift the raw CUPS total sits below a printer-scale baseline,
|
||||
the subtraction clamps at zero, and a part-used cartridge reads 100%.
|
||||
"""
|
||||
|
||||
@patch(f"{MOD}._get_cups_total_pages", return_value=1778)
|
||||
def test_applies_offset(self, mock_cups: MagicMock) -> None:
|
||||
state = {"last_printer_count": 2087, "last_cups_total": 1778}
|
||||
assert _cups_total_on_printer_scale(state) == 2087
|
||||
|
||||
@patch(f"{MOD}._get_cups_total_pages", return_value=1778)
|
||||
def test_no_snapshot_returns_raw(self, mock_cups: MagicMock) -> None:
|
||||
state = {"last_printer_count": 0, "last_cups_total": 0}
|
||||
assert _cups_total_on_printer_scale(state) == 1778
|
||||
|
||||
@patch(f"{MOD}._get_cups_total_pages", return_value=1778)
|
||||
def test_negative_offset_returns_raw(self, mock_cups: MagicMock) -> None:
|
||||
state = {"last_printer_count": 1700, "last_cups_total": 1778}
|
||||
assert _cups_total_on_printer_scale(state) == 1778
|
||||
|
||||
@patch(f"{MOD}._get_cups_total_pages", return_value=0)
|
||||
def test_no_cups_pages(self, mock_cups: MagicMock) -> None:
|
||||
state = {"last_printer_count": 2087, "last_cups_total": 1778}
|
||||
assert _cups_total_on_printer_scale(state) == 0
|
||||
152
python_pkg/brother_printer/tests/test_page_delivery.py
Normal file
152
python_pkg/brother_printer/tests/test_page_delivery.py
Normal file
@ -0,0 +1,152 @@
|
||||
"""Tests for brother_printer._page_delivery - dropped-page detection.
|
||||
|
||||
Split from test_consumables_part2.py under the 250-line cap. These are exactly
|
||||
the tests whose functions moved to _page_delivery, so the file needs only that
|
||||
module's patch target rather than the two constants the combined file carried.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from python_pkg.brother_printer._page_delivery import (
|
||||
_snapshot_counters,
|
||||
check_page_delivery,
|
||||
)
|
||||
|
||||
MOD = "python_pkg.brother_printer._page_delivery"
|
||||
|
||||
|
||||
class TestCheckPageDelivery:
|
||||
"""Spotting pages the printer silently dropped.
|
||||
|
||||
CUPS calls a job successful once the data leaves the machine, so this
|
||||
comparison against the printer's own counter is the only signal that a page
|
||||
never actually came out.
|
||||
"""
|
||||
|
||||
@patch(f"{MOD}._snapshot_counters")
|
||||
@patch(f"{MOD}._get_cups_total_pages", return_value=1841)
|
||||
@patch(f"{MOD}._load_consumable_state")
|
||||
def test_detects_dropped_pages(
|
||||
self,
|
||||
mock_load: MagicMock,
|
||||
mock_cups: MagicMock,
|
||||
mock_snap: MagicMock,
|
||||
) -> None:
|
||||
"""Replay of the real failure: CUPS sent 63, printer printed none."""
|
||||
mock_load.return_value = {
|
||||
"last_printer_count": 2087,
|
||||
"last_cups_total": 1778,
|
||||
}
|
||||
check = check_page_delivery(2087, queue_idle=True)
|
||||
assert check.cups_pages == 63
|
||||
assert check.printer_pages == 0
|
||||
assert check.dropped == 63
|
||||
assert check.suspected is True
|
||||
|
||||
@patch(f"{MOD}._snapshot_counters")
|
||||
@patch(f"{MOD}._get_cups_total_pages", return_value=1841)
|
||||
@patch(f"{MOD}._load_consumable_state")
|
||||
def test_healthy_job_does_not_warn(
|
||||
self,
|
||||
mock_load: MagicMock,
|
||||
mock_cups: MagicMock,
|
||||
mock_snap: MagicMock,
|
||||
) -> None:
|
||||
mock_load.return_value = {
|
||||
"last_printer_count": 2024,
|
||||
"last_cups_total": 1778,
|
||||
}
|
||||
check = check_page_delivery(2087, queue_idle=True)
|
||||
assert check.dropped == 0
|
||||
assert check.suspected is False
|
||||
|
||||
@patch(f"{MOD}._snapshot_counters")
|
||||
@patch(f"{MOD}._get_cups_total_pages", return_value=1780)
|
||||
@patch(f"{MOD}._load_consumable_state")
|
||||
def test_small_gap_is_not_a_warning(
|
||||
self,
|
||||
mock_load: MagicMock,
|
||||
mock_cups: MagicMock,
|
||||
mock_snap: MagicMock,
|
||||
) -> None:
|
||||
"""A page or two of drift is timing, not a fault."""
|
||||
mock_load.return_value = {
|
||||
"last_printer_count": 2087,
|
||||
"last_cups_total": 1778,
|
||||
}
|
||||
check = check_page_delivery(2087, queue_idle=True)
|
||||
assert check.dropped == 2
|
||||
assert check.suspected is False
|
||||
|
||||
def test_skipped_while_printing(self) -> None:
|
||||
"""Mid-job the counters legitimately disagree, so never compare."""
|
||||
assert check_page_delivery(2087, queue_idle=False).suspected is False
|
||||
|
||||
def test_skipped_without_printer_count(self) -> None:
|
||||
assert check_page_delivery(0, queue_idle=True).suspected is False
|
||||
|
||||
@patch(f"{MOD}._snapshot_counters")
|
||||
@patch(f"{MOD}._get_cups_total_pages", return_value=1841)
|
||||
@patch(f"{MOD}._load_consumable_state")
|
||||
def test_first_run_only_establishes_baseline(
|
||||
self,
|
||||
mock_load: MagicMock,
|
||||
mock_cups: MagicMock,
|
||||
mock_snap: MagicMock,
|
||||
) -> None:
|
||||
mock_load.return_value = {"last_printer_count": 0, "last_cups_total": 0}
|
||||
check = check_page_delivery(2087, queue_idle=True)
|
||||
assert check.suspected is False
|
||||
mock_snap.assert_called_once()
|
||||
|
||||
@patch(f"{MOD}._snapshot_counters")
|
||||
@patch(f"{MOD}._get_cups_total_pages", return_value=10)
|
||||
@patch(f"{MOD}._load_consumable_state")
|
||||
def test_log_rotation_draws_no_conclusion(
|
||||
self,
|
||||
mock_load: MagicMock,
|
||||
mock_cups: MagicMock,
|
||||
mock_snap: MagicMock,
|
||||
) -> None:
|
||||
"""A rotated page log makes the delta negative; do not cry wolf."""
|
||||
mock_load.return_value = {
|
||||
"last_printer_count": 2087,
|
||||
"last_cups_total": 1778,
|
||||
}
|
||||
check = check_page_delivery(2087, queue_idle=True)
|
||||
assert check.suspected is False
|
||||
|
||||
@patch(f"{MOD}._snapshot_counters")
|
||||
@patch(f"{MOD}._get_cups_total_pages", return_value=1841)
|
||||
@patch(f"{MOD}._load_consumable_state")
|
||||
def test_printer_counter_reset_draws_no_conclusion(
|
||||
self,
|
||||
mock_load: MagicMock,
|
||||
mock_cups: MagicMock,
|
||||
mock_snap: MagicMock,
|
||||
) -> None:
|
||||
mock_load.return_value = {
|
||||
"last_printer_count": 5000,
|
||||
"last_cups_total": 1778,
|
||||
}
|
||||
check = check_page_delivery(2087, queue_idle=True)
|
||||
assert check.suspected is False
|
||||
|
||||
|
||||
class TestSnapshotCounters:
|
||||
@patch(f"{MOD}._save_consumable_state")
|
||||
def test_writes_new_snapshot(self, mock_save: MagicMock) -> None:
|
||||
state = {"last_printer_count": 2000, "last_cups_total": 1700}
|
||||
_snapshot_counters(state, 2087, 1778)
|
||||
saved = mock_save.call_args[0][0]
|
||||
assert saved["last_printer_count"] == 2087
|
||||
assert saved["last_cups_total"] == 1778
|
||||
|
||||
@patch(f"{MOD}._save_consumable_state")
|
||||
def test_unchanged_counters_skip_the_write(self, mock_save: MagicMock) -> None:
|
||||
"""Do not rewrite the file on every run when nothing moved."""
|
||||
state = {"last_printer_count": 2087, "last_cups_total": 1778}
|
||||
_snapshot_counters(state, 2087, 1778)
|
||||
mock_save.assert_not_called()
|
||||
Loading…
Reference in New Issue
Block a user