Blank asset numbers, contested device claims, and honest toner forecasting
This commit is contained in:
@@ -102,8 +102,25 @@ def _controlled(pcname, label):
|
||||
|
||||
# --------------------------------------------------------------- part markers
|
||||
|
||||
def _retire(db, hostname):
|
||||
"""Move a PC off In Use, which is the one-step way to yield its device."""
|
||||
from shopdb.core.models import AssetStatus
|
||||
retired = AssetStatus.query.filter_by(status='Retired').first()
|
||||
if retired is None:
|
||||
retired = AssetStatus(status='Retired')
|
||||
db.session.add(retired)
|
||||
db.session.flush()
|
||||
pc = Asset.query.filter(Asset.assetnumber.ilike(hostname)).first()
|
||||
pc.statusid = retired.statusid
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def test_a_pc_swap_does_not_mint_a_second_marker(client, db, rig, collector_key):
|
||||
"""THE case this exists for. Same physical marker, two different PCs."""
|
||||
"""THE case this exists for. Same physical marker, two different PCs.
|
||||
|
||||
Neither PC mints a twin. Who HOLDS the marker is settled separately, by the
|
||||
two tests below - this one pins only that the physical unit stays one row.
|
||||
"""
|
||||
_asset(db, '0613')
|
||||
marker = _marker(db, 'PM-0613-A')
|
||||
|
||||
@@ -120,12 +137,51 @@ def test_a_pc_swap_does_not_mint_a_second_marker(client, db, rig, collector_key)
|
||||
|
||||
assert Asset.query.filter_by(assetnumber='FMARK100-PARTMARKER').first() is None
|
||||
assert Asset.query.filter_by(assetnumber='FMARK200-PARTMARKER').first() is None
|
||||
assert _controlled('FMARK200', 'collector:partmarker') == ['PM-0613-A']
|
||||
assert Asset.query.filter_by(assetnumber='PM-0613-A').count() == 1
|
||||
assert marker.assetid == Asset.query.filter_by(
|
||||
assetnumber='PM-0613-A').first().assetid
|
||||
|
||||
|
||||
def test_a_live_incumbent_keeps_the_marker_and_the_challenger_is_dormant(
|
||||
client, db, rig, collector_key):
|
||||
"""Two PCs naming one device must not both hold it actively.
|
||||
|
||||
Before this, neither device path looked at who else held the target, so a
|
||||
replaced PC kept its link forever and a copied asset-id.txt claimed the same
|
||||
marker from every bay, silently.
|
||||
"""
|
||||
_asset(db, '0613')
|
||||
_marker(db, 'PM-0613-A')
|
||||
_report(client, collector_key, 'FMARK100', pctype='gea-shopfloor-partmarker',
|
||||
machinenumber='0613', deviceid='PM-0613-A')
|
||||
resp = _report(client, collector_key, 'FMARK200',
|
||||
pctype='gea-shopfloor-partmarker',
|
||||
machinenumber='0613', deviceid='PM-0613-A')
|
||||
|
||||
assert _controlled('FMARK100', 'collector:partmarker') == ['PM-0613-A']
|
||||
assert _controlled('FMARK200', 'collector:partmarker') == []
|
||||
warnings = ' '.join(resp.get_json()['data'].get('warnings', []))
|
||||
assert 'FMARK100' in warnings and 'PM-0613-A' in warnings
|
||||
|
||||
|
||||
def test_handover_completes_once_the_incumbent_yields(client, db, rig,
|
||||
collector_key):
|
||||
"""The swap case as it actually happens: the old PC is retired or goes quiet."""
|
||||
_asset(db, '0613')
|
||||
_marker(db, 'PM-0613-A')
|
||||
_report(client, collector_key, 'FMARK100', pctype='gea-shopfloor-partmarker',
|
||||
machinenumber='0613', deviceid='PM-0613-A')
|
||||
_retire(db, 'FMARK100')
|
||||
|
||||
_report(client, collector_key, 'FMARK200', pctype='gea-shopfloor-partmarker',
|
||||
machinenumber='0613', deviceid='PM-0613-A')
|
||||
|
||||
assert _controlled('FMARK200', 'collector:partmarker') == ['PM-0613-A']
|
||||
# Archived, never deleted: "which PC drove this in June" stays answerable.
|
||||
assert _controlled('FMARK100', 'collector:partmarker') == []
|
||||
assert AssetRelationship.query.filter_by(label='collector:partmarker').count() >= 2
|
||||
|
||||
|
||||
def test_without_the_file_a_swap_still_mints_the_old_way(client, db, rig,
|
||||
collector_key):
|
||||
"""The unfixed behaviour, pinned so the file's value stays visible."""
|
||||
@@ -150,6 +206,11 @@ def test_an_unknown_device_warns_and_links_nothing(client, db, rig,
|
||||
assert Asset.query.filter_by(assetnumber='PM-TYPO').first() is None
|
||||
warnings = ' '.join(resp.get_json()['data'].get('warnings', []))
|
||||
assert 'PM-TYPO' in warnings
|
||||
# LINKS NOTHING, which is what the name claims. This used to warn and then
|
||||
# mint FMARK500-PARTMARKER anyway - the twin the whole path exists to stop -
|
||||
# and the test passed because it only checked that PM-TYPO was not created.
|
||||
assert Asset.query.filter_by(assetnumber='FMARK500-PARTMARKER').first() is None
|
||||
assert _controlled('FMARK500', 'collector:partmarker') == []
|
||||
|
||||
|
||||
def test_a_device_of_the_wrong_type_is_refused(client, db, rig, collector_key):
|
||||
@@ -160,7 +221,8 @@ def test_a_device_of_the_wrong_type_is_refused(client, db, rig, collector_key):
|
||||
pctype='gea-shopfloor-partmarker',
|
||||
machinenumber='0617', deviceid='PLAIN-MACHINE')
|
||||
assert resp.status_code in (200, 201)
|
||||
assert _controlled('FMARK600', 'collector:partmarker') != ['PLAIN-MACHINE']
|
||||
assert _controlled('FMARK600', 'collector:partmarker') == []
|
||||
assert Asset.query.filter_by(assetnumber='FMARK600-PARTMARKER').first() is None
|
||||
warnings = ' '.join(resp.get_json()['data'].get('warnings', []))
|
||||
assert 'PLAIN-MACHINE' in warnings
|
||||
|
||||
|
||||
@@ -267,3 +267,75 @@ def test_forecast_endpoint_says_so_when_zabbix_is_absent(client, db):
|
||||
assert 'not configured' in body['reason']
|
||||
assert body['cartridges'] == []
|
||||
assert body['orderlist'] == []
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Noise that used to read as cartridge changes, and bursts that used to bias
|
||||
# the rate for the life of the cartridge. Both were reported from the floor:
|
||||
# "5 changes in 90 days, that's hard to believe", and a cartridge that dropped
|
||||
# 20 percent in two days then barely moved.
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def test_a_poll_returning_zero_is_not_a_cartridge_change():
|
||||
"""0 then a real level is an SNMP error or a calibrating printer.
|
||||
|
||||
The rise is +60, which cleared the old threshold on its own. It does not
|
||||
land near full, so it is not a swap.
|
||||
"""
|
||||
points = normalise(series([70, 60, 0, 60, 55, 50]))
|
||||
assert find_replacements(points) == []
|
||||
|
||||
|
||||
def test_a_gauge_ticking_back_up_mid_range_is_not_a_change():
|
||||
"""A coarse gauge after a reseat or a power cycle. Lands at 55, not full."""
|
||||
points = normalise(series([70, 60, 45, 55, 50, 45]))
|
||||
assert find_replacements(points) == []
|
||||
|
||||
|
||||
def test_a_real_swap_is_still_counted():
|
||||
"""Near-empty to near-full. The shape a cartridge change actually makes."""
|
||||
points = normalise(series([30, 15, 5, 100, 95, 90]))
|
||||
assert len(find_replacements(points)) == 1
|
||||
|
||||
|
||||
def test_the_run_starts_at_the_real_swap_not_at_the_noise():
|
||||
"""current_run and find_replacements must agree on what a change is.
|
||||
|
||||
They read the same predicate now; when they did not, a phantom rise reset
|
||||
the run and threw away the history the estimate needed.
|
||||
"""
|
||||
points = normalise(series([90, 0, 85, 80, 75, 70]))
|
||||
assert find_replacements(points) == []
|
||||
assert len(current_run(points)) == len(points)
|
||||
|
||||
|
||||
def test_an_early_burst_does_not_dominate_the_rate_forever():
|
||||
"""20 percent in two days, then a month of almost nothing.
|
||||
|
||||
The endpoint slope reads the burst forever: (100-75)/30 = 0.83 %/day, so
|
||||
the report keeps promising the cartridge runs out long after printing
|
||||
stopped. The median sees one fast interval among many quiet ones.
|
||||
"""
|
||||
levels = [100, 90, 80] + [80 - i * 0.2 for i in range(1, 28)]
|
||||
rate = burn_rate(normalise(series(levels)))
|
||||
assert rate is not None
|
||||
assert rate < 1.0, rate
|
||||
detail = analyse(series(levels))
|
||||
assert detail['rateunstable'] is True
|
||||
|
||||
|
||||
def test_a_steady_cartridge_is_not_flagged_unstable():
|
||||
detail = analyse(series([100, 95, 90, 85, 80, 75, 70]))
|
||||
assert detail['burnrateperday'] == 5.0
|
||||
assert detail['rateunstable'] is False
|
||||
|
||||
|
||||
def test_a_real_near_empty_reading_before_a_swap_is_kept():
|
||||
"""30, 5, 100 is a cartridge run to the end and changed - not a spike.
|
||||
|
||||
The dip filter must not eat it: the 5 does not RECOVER to 30, it jumps to
|
||||
full, which is the shape of a swap rather than of a bad poll.
|
||||
"""
|
||||
points = normalise(series([40, 30, 5, 100, 95, 90]))
|
||||
assert 5.0 in [level for _, level in points]
|
||||
assert len(find_replacements(points)) == 1
|
||||
|
||||
Reference in New Issue
Block a user