mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 19:21:33 +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.
128 lines
5.2 KiB
Python
128 lines
5.2 KiB
Python
"""Matching a completion event to the queue row it belongs to (#2829).
|
|
|
|
``on_print_complete`` finds its row by printer and ``status='printing'`` alone,
|
|
so #b5a34b7ba added a check that the completion's subtask name agrees with the
|
|
file the row was dispatched with -- otherwise the printer's own calibration
|
|
runs close whoever's job happens to be printing.
|
|
|
|
The check compared the two names with plain equality, and the printer does not
|
|
echo the name back verbatim. Three days later two users had queues that would
|
|
not advance: the row stayed ``printing``, ``check_queue`` counts every such row
|
|
as a busy printer, and nothing anywhere ever closes it. Cancelling by hand was
|
|
the only way out.
|
|
|
|
The strings below are the real ones from the maintainer's own H2D, queue item
|
|
649, and from a support bundle showing the truncation case.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from backend.app.main import _normalise_subtask_name, _subtask_name_from_filename, _subtask_names_match
|
|
|
|
pytestmark = pytest.mark.unit
|
|
|
|
|
|
class TestTheReportedCase:
|
|
def test_spaces_come_back_as_underscores(self):
|
|
"""Queue item 649, verbatim from the warning it logged twice."""
|
|
dispatched = "H2D_Carbon_Filter_(V2)_Body & Solid Lid"
|
|
reported = "H2D_Carbon_Filter_(V2)_Body_&_Solid_Lid"
|
|
|
|
assert _subtask_names_match(dispatched, reported)
|
|
|
|
def test_from_the_archive_filename_it_was_dispatched_with(self):
|
|
"""End to end from the stored filename, which is where the check
|
|
actually gets its side of the comparison."""
|
|
expected = _subtask_name_from_filename("H2D_Carbon_Filter_(V2)_Body & Solid Lid.gcode.3mf")
|
|
|
|
assert _subtask_names_match(expected, "H2D_Carbon_Filter_(V2)_Body_&_Solid_Lid")
|
|
|
|
|
|
class TestTruncation:
|
|
"""The printer cuts long names and marks the cut with '...'.
|
|
|
|
Observed at ~100 characters, but not a fixed count -- a name with multibyte
|
|
characters came back at 98 -- so the marker is what is matched, not a
|
|
length. Without this every print with a long name strands its row the same
|
|
way the space substitution did.
|
|
"""
|
|
|
|
def test_a_truncated_echo_matches_the_full_name(self):
|
|
full = (
|
|
"169356_204314.STEP + 169356_204314.STEP + 169356_204314.STEP + "
|
|
"169356_204314.STEP + 169356_204314.STEP + 169356_204314.STEP"
|
|
)
|
|
truncated = (
|
|
"169356_204314.STEP + 169356_204314.STEP + 169356_204314.STEP + 169356_204314.STEP + 169356_204314..."
|
|
)
|
|
|
|
assert _subtask_names_match(full, truncated)
|
|
|
|
def test_a_truncated_name_on_the_archive_side_matches_too(self):
|
|
"""An archive whose filename was recorded from an earlier truncated
|
|
echo carries the marker itself, so the cut can be on either side."""
|
|
stored = "EXXXX-A001-Barriere Mundstück.STEP + EXXXX-A001-Barriere M..."
|
|
reported = "EXXXX-A001-Barriere_Mundstück.STEP_+_EXXXX-A001-Barriere_Mundstück.STEP"
|
|
|
|
assert _subtask_names_match(stored, reported)
|
|
|
|
def test_truncation_does_not_match_a_different_print(self):
|
|
"""The prefix still has to agree -- '...' is not a wildcard."""
|
|
assert not _subtask_names_match("Benchy_Calibration_Cube_Large", "Something_Else_Entirely...")
|
|
|
|
|
|
class TestItStillRefusesADifferentPrint:
|
|
"""The check has to keep doing its job, or #b5a34b7ba's bug comes back:
|
|
a completion for another print closing a job that is still running.
|
|
"""
|
|
|
|
def test_the_printers_own_calibration_run(self):
|
|
"""The second rejection on queue item 649, and a correct one."""
|
|
assert not _subtask_names_match("H2D_Carbon_Filter_(V2)_Body & Solid Lid", "auto_pa_line_calib_mode")
|
|
|
|
def test_an_unrelated_print(self):
|
|
assert not _subtask_names_match("Benchy", "Calibration Cube")
|
|
|
|
def test_a_name_that_merely_starts_the_same(self):
|
|
assert not _subtask_names_match("Bracket_v1", "Bracket_v2")
|
|
|
|
|
|
class TestNormalisation:
|
|
def test_case_is_ignored(self):
|
|
assert _subtask_names_match("BENCHY BOAT", "benchy_boat")
|
|
|
|
def test_surrounding_whitespace_is_ignored(self):
|
|
assert _subtask_names_match(" Benchy ", "Benchy")
|
|
|
|
@pytest.mark.parametrize(
|
|
"raw,expected",
|
|
[
|
|
("A B", "a_b"),
|
|
("A_B", "a_b"),
|
|
(" A B ", "a__b"),
|
|
("Mundstück", "mundstück"),
|
|
],
|
|
)
|
|
def test_canonical_form(self, raw, expected):
|
|
assert _normalise_subtask_name(raw) == expected
|
|
|
|
def test_spaces_and_underscores_are_the_same_rule_the_3mf_lookup_uses(self):
|
|
"""The 3MF lookup has always built space-to-underscore variants of its
|
|
candidates. The completion check growing its own comparison instead of
|
|
reading the same rule is how the two came to disagree."""
|
|
assert _normalise_subtask_name("My Model") == _normalise_subtask_name("My_Model")
|
|
|
|
|
|
class TestFilenameDerivation:
|
|
@pytest.mark.parametrize(
|
|
"filename,expected",
|
|
[
|
|
("Benchy.gcode.3mf", "Benchy"),
|
|
("Benchy.3mf", "Benchy"),
|
|
("My.Model.3mf", "My.Model"),
|
|
("/cache/Nested Path/Benchy.gcode.3mf", "Benchy"),
|
|
],
|
|
)
|
|
def test_extensions_come_off_and_nothing_else_does(self, filename, expected):
|
|
assert _subtask_name_from_filename(filename) == expected
|