|
|
@@ -0,0 +1,233 @@
|
|
|
+"""The printer kill switch acts on prints Bambuddy did not start.
|
|
|
+
|
|
|
+Two ways it did not:
|
|
|
+
|
|
|
+- The fast check counted every print in ``_active_prints`` as Bambuddy's, and
|
|
|
+ ``on_print_start`` puts every print it archives there, those started from
|
|
|
+ Bambu Studio, Handy or the printer's screen included. A print running when the
|
|
|
+ switch was turned on, or one archived before the switch looked, was never
|
|
|
+ stopped.
|
|
|
+- A LAN print reports ``subtask_id`` "0". The database check answered "not sure
|
|
|
+ yet" for it and waited, every frame, until the print was archived and the
|
|
|
+ first hole let it through.
|
|
|
+
|
|
|
+Bambuddy's own prints are now the ones it dispatched: an expected print, the
|
|
|
+print user it recorded, or an archive it adopted for a dispatch or that carries
|
|
|
+its dispatch marker. A "0" print is matched to Bambuddy's printing archive on
|
|
|
+that printer by file name; if there is none it waits only while a queue job of
|
|
|
+Bambuddy's is printing there.
|
|
|
+"""
|
|
|
+
|
|
|
+from types import SimpleNamespace
|
|
|
+from unittest.mock import AsyncMock
|
|
|
+
|
|
|
+import pytest
|
|
|
+
|
|
|
+from backend.app import main as main_module
|
|
|
+from backend.app.main import register_expected_print
|
|
|
+from backend.app.models.archive import PrintArchive
|
|
|
+from backend.tests.integration.test_external_print_confirmation_1898 import ( # noqa: F401
|
|
|
+ SUBTASK,
|
|
|
+ _clear_print_state,
|
|
|
+ _drive_print_start,
|
|
|
+)
|
|
|
+
|
|
|
+pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
|
|
|
+
|
|
|
+
|
|
|
+@pytest.fixture(autouse=True)
|
|
|
+def _clear_owned(monkeypatch):
|
|
|
+ main_module._owned_print_archive_ids.clear()
|
|
|
+ monkeypatch.setattr(main_module.printer_manager, "get_current_print_user", lambda printer_id: None)
|
|
|
+ yield
|
|
|
+ main_module._owned_print_archive_ids.clear()
|
|
|
+
|
|
|
+
|
|
|
+def _status(subtask_id="0"):
|
|
|
+ return SimpleNamespace(
|
|
|
+ current_print=None, subtask_name=SUBTASK, subtask_id=subtask_id, gcode_file=f"{SUBTASK}.gcode.3mf"
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+async def test_a_print_bambuddy_did_not_start_stays_unauthorized_once_archived(test_engine, printer_factory):
|
|
|
+ """The AP-1 scenario: archived by on_print_start, the print used to pass the
|
|
|
+ fast check from then on."""
|
|
|
+ printer_id = (await printer_factory()).id
|
|
|
+
|
|
|
+ await _drive_print_start(test_engine, SimpleNamespace(id=printer_id), download_ok=True)
|
|
|
+
|
|
|
+ assert any(key[0] == printer_id for key in main_module._active_prints)
|
|
|
+ assert main_module._is_bambuddy_authorized_print_in_memory(printer_id, _status()) is False
|
|
|
+
|
|
|
+
|
|
|
+async def test_a_print_bambuddy_dispatched_is_its_own_once_adopted(test_engine, db_session, printer_factory):
|
|
|
+ printer_id = (await printer_factory()).id
|
|
|
+ archive = PrintArchive(
|
|
|
+ printer_id=printer_id,
|
|
|
+ filename=f"{SUBTASK}.gcode.3mf",
|
|
|
+ file_path=f"archives/test/{SUBTASK}.gcode.3mf",
|
|
|
+ file_size=1,
|
|
|
+ print_name=SUBTASK,
|
|
|
+ status="pending",
|
|
|
+ )
|
|
|
+ db_session.add(archive)
|
|
|
+ await db_session.commit()
|
|
|
+ register_expected_print(printer_id, f"{SUBTASK}.gcode.3mf", archive.id)
|
|
|
+
|
|
|
+ await _drive_print_start(test_engine, SimpleNamespace(id=printer_id), download_ok=False)
|
|
|
+
|
|
|
+ # Without the expected entries (print start consumes the names the printer
|
|
|
+ # reported; the rest are evicted later), the print is still Bambuddy's.
|
|
|
+ main_module._expected_prints.clear()
|
|
|
+ assert archive.id in main_module._owned_print_archive_ids
|
|
|
+ assert main_module._is_bambuddy_authorized_print_in_memory(printer_id, _status()) is True
|
|
|
+
|
|
|
+
|
|
|
+def _db(archive, dispatched_queue_item_id=None):
|
|
|
+ """Both lookups: by subtask id (one row) and, for "0", Bambuddy's printing rows."""
|
|
|
+ rows = [archive] if archive is not None else []
|
|
|
+ return SimpleNamespace(
|
|
|
+ execute=AsyncMock(return_value=SimpleNamespace(scalar_one_or_none=lambda: archive, scalars=lambda: rows)),
|
|
|
+ scalar=AsyncMock(return_value=dispatched_queue_item_id),
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def _archive(archive_id, filename, *, marker="run-1", print_name=None):
|
|
|
+ return SimpleNamespace(
|
|
|
+ id=archive_id, filename=filename, print_name=print_name, billing_run_id=marker, created_by_id=None
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+async def test_a_lan_print_with_no_dispatch_behind_it_is_unauthorized():
|
|
|
+ assert await main_module._is_bambuddy_authorized_print(9, _status("0"), _db(None)) is False
|
|
|
+
|
|
|
+
|
|
|
+async def test_a_lan_print_matching_bambuddys_dispatched_archive_is_its_own():
|
|
|
+ """After a restart: no id to match, so the file name and the dispatch marker."""
|
|
|
+ archive = _archive(31, f"{SUBTASK}.gcode.3mf")
|
|
|
+
|
|
|
+ assert await main_module._is_bambuddy_authorized_print(9, _status("0"), _db(archive)) is True
|
|
|
+ assert 31 in main_module._owned_print_archive_ids
|
|
|
+
|
|
|
+
|
|
|
+async def test_a_lan_print_waits_while_bambuddy_has_a_job_printing_there():
|
|
|
+ """Nothing of Bambuddy's by that name, but a queue job is printing there."""
|
|
|
+ assert await main_module._is_bambuddy_authorized_print(9, _status("0"), _db(None, 401)) is None
|
|
|
+
|
|
|
+
|
|
|
+async def test_a_name_the_printer_echoes_with_underscores_still_matches():
|
|
|
+ """Bambuddy uploads "My Part" and the printer reports "My_Part"."""
|
|
|
+ archive = _archive(33, "My Part.gcode.3mf")
|
|
|
+ state = SimpleNamespace(current_print=None, subtask_name="My_Part", subtask_id="0", gcode_file="My_Part.gcode.3mf")
|
|
|
+
|
|
|
+ assert await main_module._is_bambuddy_authorized_print(9, state, _db(archive)) is True
|
|
|
+
|
|
|
+
|
|
|
+async def test_a_bambuddy_archive_of_another_name_does_not_cover_a_lan_print():
|
|
|
+ other = _archive(34, "Other_job.gcode.3mf")
|
|
|
+
|
|
|
+ assert await main_module._is_bambuddy_authorized_print(9, _status("0"), _db(other)) is False
|
|
|
+
|
|
|
+
|
|
|
+async def test_only_bambuddys_printing_archives_on_that_printer_are_read():
|
|
|
+ from sqlalchemy.dialects import sqlite
|
|
|
+
|
|
|
+ db = _db(None)
|
|
|
+ await main_module._is_bambuddy_authorized_print(9, _status("0"), db)
|
|
|
+
|
|
|
+ query = db.execute.await_args.args[0]
|
|
|
+ sql = str(query.compile(dialect=sqlite.dialect(), compile_kwargs={"literal_binds": True}))
|
|
|
+ assert "print_archives.printer_id = 9" in sql
|
|
|
+ assert "print_archives.status = 'printing'" in sql
|
|
|
+ assert "print_archives.billing_run_id IS NOT NULL" in sql
|
|
|
+
|
|
|
+
|
|
|
+async def test_a_lan_print_that_names_nothing_yet_waits():
|
|
|
+ state = SimpleNamespace(current_print=None, subtask_name="", subtask_id="0", gcode_file="")
|
|
|
+ db = _db(None)
|
|
|
+
|
|
|
+ assert await main_module._is_bambuddy_authorized_print(9, state, db) is None
|
|
|
+ db.execute.assert_not_awaited()
|
|
|
+
|
|
|
+
|
|
|
+async def test_the_printers_built_in_calibration_is_left_alone():
|
|
|
+ """Bambuddy's Calibrate and the printer's screen play gcode from the
|
|
|
+ printer's system partition (measured: /usr/etc/print/<model>/...)."""
|
|
|
+ state = SimpleNamespace(
|
|
|
+ current_print=None,
|
|
|
+ subtask_name="auto_cali_for_user_param.gcode",
|
|
|
+ subtask_id="0",
|
|
|
+ gcode_file="/usr/etc/print/O1C2/auto_cali_for_user_param.gcode",
|
|
|
+ )
|
|
|
+ db = _db(None)
|
|
|
+
|
|
|
+ assert await main_module._is_bambuddy_authorized_print(9, state, db) is True
|
|
|
+ db.execute.assert_not_awaited()
|
|
|
+
|
|
|
+
|
|
|
+async def test_a_print_named_like_a_calibration_is_still_a_print():
|
|
|
+ """Bambu Studio's pressure-advance print reports /data/Metadata/plate_1.gcode
|
|
|
+ and subtask auto_pa_line_calib_mode (measured on an H2C), exactly as a user
|
|
|
+ print given that name would."""
|
|
|
+ state = SimpleNamespace(
|
|
|
+ current_print=None,
|
|
|
+ subtask_name="pa_line_calib_mode",
|
|
|
+ subtask_id="0",
|
|
|
+ gcode_file="/data/Metadata/plate_1.gcode",
|
|
|
+ )
|
|
|
+
|
|
|
+ assert await main_module._is_bambuddy_authorized_print(9, state, _db(None)) is False
|
|
|
+
|
|
|
+
|
|
|
+async def test_a_lan_print_is_not_covered_by_an_archive_that_has_its_own_id():
|
|
|
+ from sqlalchemy.dialects import sqlite
|
|
|
+
|
|
|
+ db = _db(None)
|
|
|
+ await main_module._is_bambuddy_authorized_print(9, _status("0"), db)
|
|
|
+
|
|
|
+ sql = str(db.execute.await_args.args[0].compile(dialect=sqlite.dialect(), compile_kwargs={"literal_binds": True}))
|
|
|
+ assert "print_archives.subtask_id IS NULL" in sql
|
|
|
+
|
|
|
+
|
|
|
+async def test_a_missing_subtask_id_still_waits():
|
|
|
+ """The first frames after a restart can arrive before the field is filled."""
|
|
|
+ db = _db(None)
|
|
|
+
|
|
|
+ assert await main_module._is_bambuddy_authorized_print(9, _status(None), db) is None
|
|
|
+ db.execute.assert_not_awaited()
|
|
|
+
|
|
|
+
|
|
|
+async def test_a_finished_print_is_no_longer_owned():
|
|
|
+ main_module._owned_print_archive_ids.add(41)
|
|
|
+ main_module._active_prints[(9, f"{SUBTASK}.gcode.3mf")] = 41
|
|
|
+
|
|
|
+ main_module._active_prints.clear()
|
|
|
+ main_module._evict_stale_expected_prints()
|
|
|
+
|
|
|
+ assert 41 not in main_module._owned_print_archive_ids
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.parametrize(("marker", "owned"), [("run-7", True), (None, False)])
|
|
|
+async def test_a_print_resumed_after_a_restart_is_owned_only_with_bambuddys_marker(
|
|
|
+ test_engine, db_session, printer_factory, marker, owned
|
|
|
+):
|
|
|
+ """on_print_start resumes the printing archive it finds by name; that
|
|
|
+ archive is Bambuddy's only if Bambuddy dispatched it."""
|
|
|
+ printer_id = (await printer_factory()).id
|
|
|
+ archive = PrintArchive(
|
|
|
+ printer_id=printer_id,
|
|
|
+ filename=f"{SUBTASK}.gcode.3mf",
|
|
|
+ file_path=f"archives/test/{SUBTASK}.gcode.3mf",
|
|
|
+ file_size=1,
|
|
|
+ print_name=SUBTASK,
|
|
|
+ status="printing",
|
|
|
+ billing_run_id=marker,
|
|
|
+ )
|
|
|
+ db_session.add(archive)
|
|
|
+ await db_session.commit()
|
|
|
+
|
|
|
+ await _drive_print_start(
|
|
|
+ test_engine, SimpleNamespace(id=printer_id), download_ok=False, state_fields={"progress": 40}
|
|
|
+ )
|
|
|
+
|
|
|
+ assert (archive.id in main_module._owned_print_archive_ids) is owned
|