| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363 |
- """A print started outside the cloud is linked to its archive (#3327).
- Bambu Studio over LAN, Handy and the printer's screen report subtask_id "0",
- which Bambuddy stores as no id at all, so status found the archive of such a
- print by subtask_id never: current_archive_id stayed null for the whole print,
- and the printer card could not name the plate of a multi-plate file. Status now
- falls back to the archive Bambuddy bound to the running print, and after a
- restart, which forgets that binding, to its printing archive by name. The
- cover reads the running print's 3MF from that archive too instead of the
- printer. Skip-objects does only through the binding: by name it could offer an
- earlier run's objects to skip.
- """
- import logging
- import zipfile
- from datetime import datetime
- from unittest.mock import AsyncMock, MagicMock, patch
- import pytest
- from backend.app.core.config import settings as app_settings
- from backend.app.main import _active_prints, _restore_printable_objects
- from backend.app.services.bambu_mqtt import PrinterState
- pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
- NAME = "Benchy plates"
- @pytest.fixture(autouse=True)
- def _bindings():
- _active_prints.clear()
- yield
- _active_prints.clear()
- def _state(run_state="RUNNING", subtask_id="0"):
- state = PrinterState()
- state.connected = True
- state.state = run_state
- state.subtask_id = subtask_id
- state.subtask_name = NAME
- state.gcode_file = "/data/Metadata/plate_5.gcode"
- return state
- async def _statuses(async_client, states):
- with patch("backend.app.api.routes.printers.printer_manager") as manager:
- manager.get_status = MagicMock(side_effect=lambda pid: states.get(pid))
- manager.is_awaiting_plate_clear = MagicMock(return_value=False)
- singles = {}
- for pid in states:
- response = await async_client.get(f"/api/v1/printers/{pid}/status")
- assert response.status_code == 200
- singles[pid] = response.json()["current_archive_id"]
- bulk = await async_client.get("/api/v1/bulk/printer-statuses", params={"ids": ",".join(map(str, states))})
- assert bulk.status_code == 200
- assert {row["id"]: row["current_archive_id"] for row in bulk.json()} == singles
- return singles
- async def test_a_lan_print_is_linked_to_its_archive(async_client, printer_factory, archive_factory):
- printer = await printer_factory(model="H2C")
- archive = await archive_factory(printer.id, status="printing", subtask_id=None, filename=f"{NAME}.3mf")
- _active_prints[(printer.id, f"{NAME}.3mf")] = archive.id
- linked = await _statuses(async_client, {printer.id: _state()})
- assert linked[printer.id] == archive.id
- async def test_also_while_paused(async_client, printer_factory, archive_factory):
- printer = await printer_factory()
- archive = await archive_factory(printer.id, status="printing", subtask_id=None)
- _active_prints[(printer.id, NAME)] = archive.id
- linked = await _statuses(async_client, {printer.id: _state("PAUSE")})
- assert linked[printer.id] == archive.id
- async def test_not_before_its_archive_exists(async_client, printer_factory, archive_factory):
- """Nor to an archive of an earlier print that also had no id."""
- printer = await printer_factory()
- await archive_factory(printer.id, status="printing", subtask_id=None)
- await archive_factory(printer.id, status="printing", subtask_id="0")
- linked = await _statuses(async_client, {printer.id: _state()})
- assert linked[printer.id] is None
- async def test_not_to_another_printers_print(async_client, printer_factory, archive_factory):
- printer, other = await printer_factory(), await printer_factory()
- archive = await archive_factory(other.id, status="printing", subtask_id=None)
- _active_prints[(other.id, NAME)] = archive.id
- linked = await _statuses(async_client, {printer.id: _state()})
- assert linked[printer.id] is None
- async def test_not_once_the_print_is_over(async_client, printer_factory, archive_factory):
- printer = await printer_factory()
- archive = await archive_factory(printer.id, status="printing", subtask_id=None)
- _active_prints[(printer.id, NAME)] = archive.id
- linked = await _statuses(async_client, {printer.id: _state("FINISH")})
- assert linked[printer.id] is None
- async def test_a_cloud_print_is_still_linked_by_its_id(async_client, printer_factory, archive_factory):
- printer = await printer_factory()
- by_id = await archive_factory(printer.id, status="printing", subtask_id="524288")
- bound = await archive_factory(printer.id, status="printing", subtask_id=None)
- _active_prints[(printer.id, NAME)] = bound.id
- linked = await _statuses(async_client, {printer.id: _state(subtask_id="524288")})
- assert linked[printer.id] == by_id.id
- async def test_a_cloud_print_without_an_archive_by_its_id_uses_the_binding(
- async_client, printer_factory, archive_factory
- ):
- printer = await printer_factory()
- bound = await archive_factory(printer.id, status="printing", subtask_id=None)
- _active_prints[(printer.id, NAME)] = bound.id
- linked = await _statuses(async_client, {printer.id: _state(subtask_id="524288")})
- assert linked[printer.id] == bound.id
- class TestAfterARestart:
- """The binding is gone and only Bambuddy's own prints are bound again."""
- async def test_a_lan_print_is_found_by_its_name(self, async_client, printer_factory, archive_factory):
- printer = await printer_factory(model="H2C")
- await archive_factory(printer.id, status="printing", subtask_id=None, filename="Older.3mf", print_name="Older")
- archive = await archive_factory(
- printer.id, status="printing", subtask_id=None, filename=f"{NAME}.3mf", print_name=NAME
- )
- linked = await _statuses(async_client, {printer.id: _state()})
- assert linked[printer.id] == archive.id
- async def test_as_the_printer_echoes_the_name(self, async_client, printer_factory, archive_factory):
- printer = await printer_factory()
- archive = await archive_factory(printer.id, status="printing", subtask_id=None, filename=f"{NAME}.3mf")
- state = _state()
- state.subtask_name = NAME.replace(" ", "_")
- linked = await _statuses(async_client, {printer.id: state})
- assert linked[printer.id] == archive.id
- async def test_not_a_finished_run_of_the_same_file(self, async_client, printer_factory, archive_factory):
- printer = await printer_factory()
- await archive_factory(printer.id, status="completed", subtask_id=None, filename=f"{NAME}.3mf")
- linked = await _statuses(async_client, {printer.id: _state()})
- assert linked[printer.id] is None
- async def test_not_a_print_that_had_an_id_of_its_own(self, async_client, printer_factory, archive_factory):
- printer = await printer_factory()
- await archive_factory(printer.id, status="printing", subtask_id="524288", filename=f"{NAME}.3mf")
- linked = await _statuses(async_client, {printer.id: _state()})
- assert linked[printer.id] is None
- async def test_not_for_a_cloud_print(self, async_client, printer_factory, archive_factory):
- """Its own id finds its archive; a name is no better than that."""
- printer = await printer_factory()
- await archive_factory(printer.id, status="printing", subtask_id=None, filename=f"{NAME}.3mf")
- linked = await _statuses(async_client, {printer.id: _state(subtask_id="524288")})
- assert linked[printer.id] is None
- async def test_not_a_deleted_archive(self, async_client, printer_factory, archive_factory):
- printer = await printer_factory()
- await archive_factory(
- printer.id, status="printing", subtask_id=None, filename=f"{NAME}.3mf", deleted_at=datetime.now()
- )
- linked = await _statuses(async_client, {printer.id: _state()})
- assert linked[printer.id] is None
- class TestADeletedArchive:
- """Deleting the archive of a running print leaves its binding behind."""
- async def test_is_not_named_through_the_binding(self, async_client, printer_factory, archive_factory, db_session):
- printer = await printer_factory()
- archive = await archive_factory(printer.id, status="printing", subtask_id=None, filename=f"{NAME}.3mf")
- _active_prints[(printer.id, NAME)] = archive.id
- await db_session.delete(archive)
- await db_session.commit()
- linked = await _statuses(async_client, {printer.id: _state()})
- assert linked[printer.id] is None
- async def test_nor_when_moved_to_the_bin(self, async_client, printer_factory, archive_factory):
- printer = await printer_factory()
- archive = await archive_factory(printer.id, status="printing", subtask_id=None, deleted_at=datetime.now())
- _active_prints[(printer.id, NAME)] = archive.id
- linked = await _statuses(async_client, {printer.id: _state()})
- assert linked[printer.id] is None
- async def test_nor_by_a_cloud_prints_id(self, async_client, printer_factory, archive_factory):
- printer = await printer_factory()
- await archive_factory(printer.id, status="printing", subtask_id="524288", deleted_at=datetime.now())
- linked = await _statuses(async_client, {printer.id: _state(subtask_id="524288")})
- assert linked[printer.id] is None
- def _job_3mf(tmp_path):
- """A real zip without a cover, so the cover route answers 404 from it."""
- path = tmp_path / "job.3mf"
- with zipfile.ZipFile(path, "w") as zf:
- zf.writestr("Metadata/slice_info.config", "<config/>")
- return str(path.relative_to(tmp_path))
- class TestTheRunningPrintsFileOnDisk:
- """What else finds the running print's archive finds a "0" print's too."""
- async def test_the_cover_comes_from_the_archive(self, async_client, printer_factory, archive_factory, tmp_path):
- printer = await printer_factory(model="H2C")
- await archive_factory(
- printer.id, status="printing", subtask_id=None, filename=f"{NAME}.3mf", file_path=_job_3mf(tmp_path)
- )
- state = _state()
- state.sdcard = True
- ftp = AsyncMock(return_value=None)
- with (
- patch.object(app_settings, "base_dir", tmp_path),
- patch("backend.app.api.routes.printers.printer_manager.get_status", return_value=state),
- patch("backend.app.api.routes.printers.get_cached_3mf", return_value=None),
- patch("backend.app.api.routes.printers.download_file_try_paths_async", new=ftp),
- ):
- response = await async_client.get(f"/api/v1/printers/{printer.id}/cover")
- assert response.status_code == 404, response.text
- ftp.assert_not_awaited()
- async def test_the_skip_objects_reload_reads_the_archive(self, async_client, printer_factory, archive_factory):
- printer = await printer_factory(model="H2C")
- archive = await archive_factory(printer.id, status="printing", subtask_id=None)
- _active_prints[(printer.id, NAME)] = archive.id
- client = MagicMock(state=_state())
- ftp = AsyncMock(return_value=None)
- with (
- patch("backend.app.api.routes.printers.printer_manager.get_client", return_value=client),
- patch(
- "backend.app.services.archive.extract_printable_objects_from_archive",
- return_value=({101: {"name": "left"}}, None),
- ) as from_disk,
- patch("backend.app.services.bambu_ftp.download_file_try_paths_async", new=ftp),
- ):
- response = await async_client.get(f"/api/v1/printers/{printer.id}/print/objects")
- assert response.status_code == 200, response.text
- assert response.json()["total"] == 1
- from_disk.assert_called_once()
- ftp.assert_not_awaited()
- async def test_the_object_list_comes_from_the_bound_archive(self, printer_factory, archive_factory, db_session):
- """A reconnect without a restart: the binding is still there."""
- printer = await printer_factory(model="H2C")
- archive = await archive_factory(printer.id, status="printing", subtask_id=None)
- _active_prints[(printer.id, NAME)] = archive.id
- with (
- patch("backend.app.main.printer_manager") as pm,
- patch("backend.app.main._load_objects_from_archive") as load,
- ):
- pm.get_client.return_value = MagicMock(state=MagicMock(printable_objects={}))
- await _restore_printable_objects(printer.id, _state(), db_session, logging.getLogger(__name__))
- load.assert_called_once()
- assert load.call_args.args[0].id == archive.id
- async def test_but_never_by_name_after_a_restart(self, printer_factory, archive_factory, db_session):
- """An earlier run of the same name, left printing by a completion
- missed while Bambuddy was down, cannot be told from this one."""
- printer = await printer_factory(model="H2C")
- await archive_factory(printer.id, status="printing", subtask_id=None, filename=f"{NAME}.3mf")
- with (
- patch("backend.app.main.printer_manager") as pm,
- patch("backend.app.main._load_objects_from_archive") as load,
- ):
- pm.get_client.return_value = MagicMock(state=MagicMock(printable_objects={}))
- await _restore_printable_objects(printer.id, _state(), db_session, logging.getLogger(__name__))
- load.assert_not_called()
- async def test_nor_does_the_reload(self, async_client, printer_factory, archive_factory):
- """It asks the printer for the file it is running instead."""
- printer = await printer_factory(model="H2C")
- await archive_factory(printer.id, status="printing", subtask_id=None, filename=f"{NAME}.3mf")
- client = MagicMock(state=_state())
- ftp = AsyncMock(return_value=None)
- with (
- patch("backend.app.api.routes.printers.printer_manager.get_client", return_value=client),
- patch(
- "backend.app.services.archive.extract_printable_objects_from_archive",
- return_value=({101: {"name": "left"}}, None),
- ) as from_disk,
- patch("backend.app.services.bambu_ftp.download_file_try_paths_async", new=ftp),
- ):
- response = await async_client.get(f"/api/v1/printers/{printer.id}/print/objects")
- assert response.status_code == 200, response.text
- from_disk.assert_not_called()
- ftp.assert_awaited()
- async def test_not_from_an_archive_without_its_3mf(self, printer_factory, archive_factory, db_session):
- printer = await printer_factory(model="H2C")
- archive = await archive_factory(
- printer.id, status="printing", subtask_id=None, filename=f"{NAME}.3mf", file_path=""
- )
- _active_prints[(printer.id, NAME)] = archive.id
- with (
- patch("backend.app.main.printer_manager") as pm,
- patch("backend.app.main._load_objects_from_archive") as load,
- ):
- pm.get_client.return_value = MagicMock(state=MagicMock(printable_objects={}))
- await _restore_printable_objects(printer.id, _state(), db_session, logging.getLogger(__name__))
- load.assert_not_called()
- async def test_nor_from_a_finished_archive_with_the_same_job_id(self, printer_factory, archive_factory, db_session):
- """A leftover row of a completion never seen must not lend its objects."""
- printer = await printer_factory(model="H2C")
- await archive_factory(printer.id, status="completed", subtask_id="524288", filename=f"{NAME}.3mf")
- with (
- patch("backend.app.main.printer_manager") as pm,
- patch("backend.app.main._load_objects_from_archive") as load,
- ):
- pm.get_client.return_value = MagicMock(state=MagicMock(printable_objects={}))
- await _restore_printable_objects(
- printer.id, _state(subtask_id="524288"), db_session, logging.getLogger(__name__)
- )
- load.assert_not_called()
|