Clear the last three brother_printer test modules over the cap
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

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:
Krzysztof Rudnicki 2026-08-16 17:27:07 +02:00
parent 74eee2b57f
commit 2c01a0267e
9 changed files with 701 additions and 591 deletions

View File

@ -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."
}

View File

@ -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."
]
}

View File

@ -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()

View File

@ -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()

View 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

View File

@ -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

View File

@ -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

View 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

View 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()