Files
KX-Bridge-Release/tests/test_spoolman_unreported.py
viewit ef1956f423 fix(spoolman): stop deducting filament from spools not used in the print
User report: with 4 spools mapped, every print deducted the same
several-gram amount from ALL 4 spools regardless of which one was
actually printing with, adding up to ~150g of drift over time on an
unused spool.

Root cause: _spoolman_last_sync was reset to 0.0 (epoch) at print
start instead of the current time. The poll loop calls
_spoolman_sync_midprint() before _spoolman_attribute_tick() in the
same iteration, so with last_sync=0.0 the sync-due check was true on
the very first tick after print start - before any per-slot
attribution existed yet. _spoolman_unreported() then fell back to
splitting the printer's full (possibly already nonzero) supplies_usage
equally across every mapped spool, matching the reported log
(identical mm reported to all 4 spools, right after upload before the
purge even started).

Fixes:
- _on_print() now resets _spoolman_last_sync to time.time(), not 0.0,
  so the first sync check is only due after a real interval has
  passed with attribution data available.
- _spoolman_unreported()'s fallback now only applies with exactly one
  mapped slot (single-extruder, no ambiguity) - with multiple slots
  and no attribution data it reports nothing instead of guessing,
  since a wrong equal-split is worse than a temporarily-missed report.

Added tests/test_spoolman_unreported.py covering both fixes.

(Same fix as testing branch commit b44c902, applied manually here
since nightly still has the pre-mixin-split monolithic
kobrax_moonraker_bridge.py layout - bridge_mqtt.py/bridge_spoolman.py
don't exist on this branch yet.)
2026-08-24 19:17:02 +02:00

88 lines
3.2 KiB
Python

"""Regression test: Spoolman fallback split silently deducted filament from
spools not used in the current print.
_spoolman_unreported() falls back to an equal split across every mapped
AMS slot when per-slot attribution data (_spoolman_slot_usage) is still
empty - previously true right after print start, because
_spoolman_sync_midprint() ran (in the poll loop) before the first
_spoolman_attribute_tick() had a chance to populate any per-slot data,
since _spoolman_last_sync was reset to 0.0 (== "due immediately") instead
of the current time. With 4 spools mapped, that meant the full
(possibly already nonzero) supplies_usage got reported equally to all 4
spools regardless of which one was actually printing with.
The fix: only fall back to a full-credit report when there is exactly
one mapped slot (single-extruder, no ambiguity possible) - otherwise
report nothing until real attribution data exists. Additionally,
_on_print() now resets _spoolman_last_sync to the current time (not 0.0)
at print start, so the first mid-print sync check is due only after a
real sync_rate interval has passed.
"""
import time
def _configure(bridge, slot_spools):
bridge._spoolman_slot_spools = dict(slot_spools)
bridge._spoolman_slot_usage = {}
bridge._spoolman_slot_reported = {}
async def test_unreported_reports_nothing_for_multiple_slots_without_attribution(client):
_, bridge = client
_configure(bridge, {0: 11, 1: 15, 2: 17, 3: 23})
bridge._state["supplies_usage"] = 4369
result = bridge._spoolman_unreported()
assert result == {}
async def test_unreported_credits_the_single_slot_without_attribution(client):
_, bridge = client
_configure(bridge, {0: 42})
bridge._state["supplies_usage"] = 500
result = bridge._spoolman_unreported()
assert result == {0: 500}
async def test_unreported_single_slot_subtracts_already_reported(client):
_, bridge = client
_configure(bridge, {0: 42})
bridge._spoolman_slot_reported = {0: 200.0}
bridge._state["supplies_usage"] = 500
result = bridge._spoolman_unreported()
assert result == {0: 300.0}
async def test_unreported_uses_real_attribution_once_available(client):
_, bridge = client
_configure(bridge, {0: 11, 1: 15})
bridge._spoolman_slot_usage = {0: 120.0}
bridge._state["supplies_usage"] = 120
result = bridge._spoolman_unreported()
# Only the slot that was actually attributed usage gets a nonzero report;
# the untouched slot must not receive any share of it.
assert result[0] == 120.0
assert result[1] == 0.0
async def test_print_start_resets_last_sync_to_now_not_epoch(client):
"""_spoolman_last_sync=0.0 at print start meant the poll loop's mid-print
sync check (time.time() - last_sync >= sync_rate) was true on the very
first tick, before _spoolman_attribute_tick() ever ran once - triggering
the fallback-split bug above immediately after print start, before any
real per-slot usage existed yet."""
_, bridge = client
before = time.time()
bridge._on_print({"state": "printing", "data": {"filename": "test.gcode"}})
assert bridge._spoolman_last_sync >= before
assert bridge._spoolman_last_sync <= time.time()