Only zero is empty, and the last day is worth reading
Two separate floors were collapsing a live cartridge into a spent one, and removing either alone changes nothing. EMPTY_LEVEL was 5, so everything at or below 5% was assigned daysleft 0 outright. The reasoning was that a printer at 1% is out of toner as far as anyone standing at it is concerned. But the row exists to say how long is left, and 2% draining a point a day has two days in it. Flooring put that cartridge beside ones that genuinely are empty with no way back to the difference. It is 0 now: empty means empty. int(level / rate) then truncated the division, so anything under a full day arrived as 0 whatever the floor did - a cartridge with six hours in it was indistinguishable from one with nothing, and the band read it as empty. daysleft is fractional now, rounded to two places, which is about a quarter of an hour: finer than the estimate deserves, but it costs nothing and keeps the ordering of two nearly-spent cartridges meaningful. daysText reads in whatever unit carries meaning: "6 hours", "1 hour", "29 min", "4 days". Below an hour it goes to minutes with a floor of one, because rounding hours would land back on "empty" - the same bug one rung down. BEHAVIOUR CHANGE worth knowing: a cartridge at 1-5% with NO history used to get daysleft 0 from the floor and land on the order list. A rate needs two readings; with none there is nothing to divide, and the old answer was right by accident - it said "empty" about a level nobody had watched move. It now reports 'no history' and shows under "No estimate yet". This reaches only printers newly added to Zabbix; anything that got to 3% the ordinary way has the history to forecast from. Five existing tests pinned the old rule. They recorded a real decision, so they are rewritten to the new one rather than deleted. One of them was passing for the wrong reason: its series (2.0, 1.7, 1.4, 1.1) drops 0.9, under MIN_DROP_FOR_ESTIMATE, so it never had a rate at all and only passed because the floor short-circuited ahead of the rate check. It now uses a real 20-day drop at a tenth of a point a day and asserts the ten days its docstring always described. The forecast fixture's black cartridge moves from 1% to 0% so the empty band keeps its API-level coverage, and a magenta at 3% covers hours-left end to end.
This commit is contained in:
@@ -8,6 +8,8 @@ purchasing decision.
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import pytest
|
||||
|
||||
from plugins.printers.services.supply_history import (
|
||||
analyse, band, burn_rate, current_run, find_replacements, normalise,
|
||||
orderlist, soonest,
|
||||
@@ -113,28 +115,69 @@ def test_live_level_drives_the_countdown_not_the_stored_one():
|
||||
assert result['daysleft'] == 1 # 20 / 20, not 40 / 20
|
||||
|
||||
|
||||
def test_a_nearly_empty_cartridge_reads_as_empty_not_as_weeks_away():
|
||||
"""1% draining a tenth of a point a day computes to ten days. It is out."""
|
||||
result = analyse(series([2.0, 1.7, 1.4, 1.1]), currentlevel=1)
|
||||
def test_a_nearly_empty_cartridge_keeps_its_real_countdown():
|
||||
"""Only zero is empty.
|
||||
|
||||
This used to floor everything at or below 5% to daysleft 0, so 1% draining
|
||||
a tenth of a point a day was reported as gone. It has ten days in it, and
|
||||
ten days is the only thing the row exists to say. Flooring it put the
|
||||
cartridge beside ones that genuinely are empty with no way back to the
|
||||
difference.
|
||||
"""
|
||||
# 3.0 down to 1.0 at a tenth of a point a day: a real, slow, measured drop.
|
||||
slow = [round(3.0 - 0.1 * i, 2) for i in range(21)]
|
||||
result = analyse(series(slow), currentlevel=1)
|
||||
|
||||
assert result['daysleft'] == pytest.approx(10, abs=0.1)
|
||||
|
||||
|
||||
def test_a_cartridge_with_hours_left_is_not_reported_as_empty():
|
||||
"""The other half of the floor: int() truncated the division, so anything
|
||||
under a full day became 0 and read as empty. A cartridge with six hours in
|
||||
it still prints."""
|
||||
result = analyse(series([100, 75, 50, 25]), currentlevel=6)
|
||||
|
||||
assert 0 < result['daysleft'] < 1
|
||||
assert round(result['daysleft'] * 24) == 6
|
||||
|
||||
|
||||
def test_an_actual_zero_is_still_empty():
|
||||
"""The half that must not regress. Zero means spent, and spent is ordered
|
||||
first."""
|
||||
result = analyse(series([2.0, 1.7, 1.4, 1.1]), currentlevel=0)
|
||||
|
||||
assert result['daysleft'] == 0
|
||||
|
||||
|
||||
def test_empty_sorts_ahead_of_a_fast_healthy_cartridge():
|
||||
empty = analyse(series([2.0, 1.7, 1.4, 1.1]), currentlevel=1)
|
||||
empty = analyse(series([2.0, 1.7, 1.4, 1.1]), currentlevel=0)
|
||||
healthy = analyse(series([100, 80, 60, 40]))
|
||||
|
||||
assert empty['daysleft'] < healthy['daysleft']
|
||||
|
||||
|
||||
def test_a_low_live_level_is_actionable_without_any_history():
|
||||
def test_an_empty_live_level_is_actionable_without_any_history():
|
||||
"""A printer new to Zabbix still reports it is out of toner."""
|
||||
result = analyse([], currentlevel=1)
|
||||
result = analyse([], currentlevel=0)
|
||||
|
||||
assert result['daysleft'] == 0
|
||||
assert result['reason'] is None
|
||||
|
||||
|
||||
def test_a_low_live_level_with_no_history_cannot_be_counted_down():
|
||||
"""A rate needs two readings. At 1% with none, there is nothing to divide.
|
||||
|
||||
Previously the 5% floor answered this by asserting daysleft 0, which was
|
||||
right by accident and wrong in general - it said "empty" about a level
|
||||
nobody had watched move. It reports no estimate and says why; the report
|
||||
lists it under "No estimate yet" rather than on the order list.
|
||||
"""
|
||||
result = analyse([], currentlevel=1)
|
||||
|
||||
assert result['daysleft'] is None
|
||||
assert result['reason'] == 'no history'
|
||||
|
||||
|
||||
def test_no_history_and_a_healthy_level_still_says_no_history():
|
||||
result = analyse([], currentlevel=80)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user