mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
Bambuddy has no run identifier to tie a completion to a queue row, so it
finds the row by printer and status='printing' alone. b5a34b7ba added a
check that the completion's subtask name agrees with the file the row was
dispatched with, so the printer's own calibration runs cannot close
someone's job early. It compared the two names verbatim.
The printer does not echo them verbatim. It substitutes underscores for
spaces, so 'H2D_Carbon_Filter_(V2)_Body & Solid Lid' came back as
'H2D_Carbon_Filter_(V2)_Body_&_Solid_Lid', the check refused it, and the
row stayed printing. check_queue counts every printing row as a busy
printer and nothing else ever closes one, so the printer's queue stopped
until someone cancelled by hand. It also truncates long names and marks
the cut with '...', which would have done the same to any long title.
Compare on the canonical form instead -- case, spaces and underscores --
which is the rule the 3MF lookup in this module has always used for the
same names, and treat a truncation marker as a prefix match. The check
keeps its purpose: the same printer the same day correctly refused a
completion for auto_pa_line_calib_mode.
One comparison being stricter than reality should not be able to stop a
queue indefinitely, so the scheduler now closes a row itself when it has
been printing for five minutes after its printer went terminal, with the
status that state implies. A real completion arrives within seconds, so
this only sees rows that were already stranded, and a disconnected
printer never qualifies. It restores the queue only -- notifications,
billing and auto-off are not replayed minutes late.
107 lines
4.2 KiB
Python
107 lines
4.2 KiB
Python
"""The completion guard itself, not just the name comparison (#2829).
|
|
|
|
There is a unit test for ``_subtask_names_match``. It is not enough on its own:
|
|
reverting ``_completion_belongs_to_queue_item`` to the strict equality that
|
|
caused the bug leaves every one of those tests green, because they never touch
|
|
the guard. So these drive the guard, with a real archive row behind a real
|
|
queue row, on the exact strings that stranded the maintainer's H2D.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
pytestmark = [pytest.mark.integration, pytest.mark.asyncio]
|
|
|
|
|
|
async def _archive(db_session, printer, filename):
|
|
from backend.app.models.archive import PrintArchive
|
|
|
|
archive = PrintArchive(
|
|
printer_id=printer.id,
|
|
filename=filename,
|
|
file_path=f"archives/{filename}",
|
|
file_size=1024,
|
|
status="printing",
|
|
)
|
|
db_session.add(archive)
|
|
await db_session.commit()
|
|
await db_session.refresh(archive)
|
|
return archive
|
|
|
|
|
|
async def _item(db_session, printer, archive):
|
|
from backend.app.models.print_queue import PrintQueueItem
|
|
|
|
item = PrintQueueItem(printer_id=printer.id, status="printing", archive_id=archive.id)
|
|
db_session.add(item)
|
|
await db_session.commit()
|
|
await db_session.refresh(item)
|
|
return item
|
|
|
|
|
|
async def _belongs(db_session, item, subtask_name):
|
|
from backend.app.main import _completion_belongs_to_queue_item
|
|
|
|
return await _completion_belongs_to_queue_item(db_session, item, {"subtask_name": subtask_name})
|
|
|
|
|
|
class TestTheReportedStranding:
|
|
async def test_the_completion_for_its_own_print_is_accepted(self, db_session, printer_factory):
|
|
"""Queue item 649 on the maintainer's H2D, verbatim. The printer echoes
|
|
the name back with underscores where the file has spaces; the guard
|
|
read that as a different print and left the row printing forever."""
|
|
printer = await printer_factory()
|
|
archive = await _archive(db_session, printer, "H2D_Carbon_Filter_(V2)_Body & Solid Lid.gcode.3mf")
|
|
item = await _item(db_session, printer, archive)
|
|
|
|
assert await _belongs(db_session, item, "H2D_Carbon_Filter_(V2)_Body_&_Solid_Lid")
|
|
|
|
async def test_a_truncated_echo_is_accepted(self, db_session, printer_factory):
|
|
printer = await printer_factory()
|
|
name = "169356_204314.STEP + 169356_204314.STEP + 169356_204314.STEP + 169356_204314.STEP + 169356_204314"
|
|
archive = await _archive(db_session, printer, f"{name}.gcode.3mf")
|
|
item = await _item(db_session, printer, archive)
|
|
|
|
assert await _belongs(db_session, item, f"{name[:70]}...")
|
|
|
|
|
|
class TestItStillRefuses:
|
|
"""The guard's reason for existing: a completion for something else must
|
|
not close a job that is still running."""
|
|
|
|
async def test_the_printers_own_calibration_run(self, db_session, printer_factory):
|
|
printer = await printer_factory()
|
|
archive = await _archive(db_session, printer, "H2D_Carbon_Filter_(V2)_Body & Solid Lid.gcode.3mf")
|
|
item = await _item(db_session, printer, archive)
|
|
|
|
assert not await _belongs(db_session, item, "auto_pa_line_calib_mode")
|
|
|
|
async def test_an_unrelated_print(self, db_session, printer_factory):
|
|
printer = await printer_factory()
|
|
archive = await _archive(db_session, printer, "Benchy.gcode.3mf")
|
|
item = await _item(db_session, printer, archive)
|
|
|
|
assert not await _belongs(db_session, item, "Calibration Cube")
|
|
|
|
|
|
class TestUnverifiableIsNotWrong:
|
|
"""Refusing what cannot be checked would strand the queue, which is the
|
|
worse of the two failures and the one this issue is about."""
|
|
|
|
async def test_no_subtask_name_in_the_event(self, db_session, printer_factory):
|
|
printer = await printer_factory()
|
|
archive = await _archive(db_session, printer, "Benchy.gcode.3mf")
|
|
item = await _item(db_session, printer, archive)
|
|
|
|
assert await _belongs(db_session, item, "")
|
|
|
|
async def test_a_row_with_no_archive(self, db_session, printer_factory):
|
|
from backend.app.models.print_queue import PrintQueueItem
|
|
|
|
printer = await printer_factory()
|
|
item = PrintQueueItem(printer_id=printer.id, status="printing")
|
|
db_session.add(item)
|
|
await db_session.commit()
|
|
await db_session.refresh(item)
|
|
|
|
assert await _belongs(db_session, item, "Anything At All")
|