"""A print started from the printer's own screen names its file in full (#3009). A job Bambuddy dispatches carries a bare subtask name, and the 3MF lookup appends the extensions to it. A job started on the printer's touchscreen reports the file's own name, extension included, so the lookup asked for ``X.3mf.gcode.3mf`` and ``X.3mf.3mf`` first -- six connections, about six seconds, that could not succeed -- and the existing-archive check compared against ``X.3mf.3mf`` and could never reattach such a print after a restart. The name is from a reporter's A1 log. """ import re from unittest.mock import AsyncMock, MagicMock, patch import pytest from backend.app.main import ( _active_prints, _expected_print_creators, _expected_print_registered_at, _expected_prints, _print_ams_mappings, _timelapse_baselines, ) pytestmark = pytest.mark.unit NAME = "A1-Siraya_Tech_TPU-64D-18JAN26" @pytest.fixture(autouse=True) def _clear_dicts(): dicts = ( _expected_prints, _expected_print_registered_at, _expected_print_creators, _print_ams_mappings, _active_prints, _timelapse_baselines, ) for d in dicts: d.clear() yield for d in dicts: d.clear() def _printer(): printer = MagicMock() printer.id = 1 printer.auto_archive = True printer.external_camera_enabled = False printer.external_camera_url = None printer.plate_detection_enabled = False printer.name = "A1" printer.model = "A1" printer.ip_address = "192.168.1.117" printer.access_code = "12345678" return printer async def _run_print_start(subtask_name, filename): """Drive on_print_start with every FTP path answering 550, and return the remote paths it tried, in order, and the SQL it ran.""" printer = _printer() statements = [] def execute_router(stmt, *args, **kwargs): statements.append(stmt) sql = str(stmt).lower() if "from printers" in sql or "from printer " in sql: return MagicMock( scalar_one_or_none=MagicMock(return_value=printer), scalars=MagicMock(return_value=MagicMock(all=MagicMock(return_value=[printer]))), ) return MagicMock( scalar_one_or_none=MagicMock(return_value=None), scalars=MagicMock(return_value=MagicMock(all=MagicMock(return_value=[]))), ) session = AsyncMock() session.__aenter__ = AsyncMock(return_value=session) session.__aexit__ = AsyncMock() session.execute = AsyncMock(side_effect=execute_router) session.commit = AsyncMock() session.refresh = AsyncMock() session.add = MagicMock() from backend.app.services.bambu_ftp import FileNotOnPrinterError download = AsyncMock(side_effect=FileNotOnPrinterError("550")) state = MagicMock(current_project_url=f"ftp://{filename}", sdcard=True, sdcard_reported=True) with ( patch("backend.app.main.async_session") as session_maker, patch("backend.app.main.notification_service") as notif, patch("backend.app.main.smart_plug_manager") as plug, patch("backend.app.main.ws_manager") as ws, patch("backend.app.main.mqtt_relay") as relay, patch("backend.app.main.printer_manager") as pm, patch("backend.app.main.download_file_async", new=download), patch("backend.app.main.download_file_try_paths_async", new=AsyncMock(return_value=None)), patch("backend.app.main.with_ftp_retry", new=AsyncMock(return_value=False)), patch("backend.app.main.get_cached_3mf", return_value=None), patch("backend.app.services.bambu_ftp.list_files_async", new=AsyncMock(return_value=[])), patch("backend.app.main.ftps_handshake_blocked", return_value=False), patch("backend.app.main.get_ftp_retry_settings", new=AsyncMock(return_value=(False, 3, 2.0, 30))), patch("backend.app.main._record_energy_start", new_callable=AsyncMock), patch("backend.app.main._send_print_start_notification", new_callable=AsyncMock), patch("backend.app.main._maybe_start_layer_timelapse"), patch("backend.app.main._capture_timelapse_baseline_at_start", new_callable=AsyncMock), patch("backend.app.main._schedule_fallback_3mf_retry", new=MagicMock()), ): session_maker.return_value = session notif.on_print_start = AsyncMock() plug.on_print_start = AsyncMock() ws.send_print_start = AsyncMock() ws.send_archive_updated = AsyncMock() ws.send_archive_created = AsyncMock() relay.on_print_start = AsyncMock() pm.get_status = MagicMock(return_value=state) pm.get_printer = MagicMock(return_value=MagicMock(serial_number="TEST3009")) from backend.app.main import on_print_start await on_print_start(1, {"filename": filename, "subtask_name": subtask_name}) tried = [call.args[2] for call in download.await_args_list] return tried, statements def _existing_archive_lookup(statements): """The literal SQL of the name-based check for a printing archive to reattach.""" for stmt in statements: try: sql = str(stmt.compile(compile_kwargs={"literal_binds": True})) except Exception: continue if "print_archives" in sql and "print_name" in sql and "printing" in sql: return sql return None @pytest.mark.asyncio async def test_a_subtask_with_its_extension_is_tried_as_the_file_itself_first(): tried, _ = await _run_print_start(f"{NAME}.3mf", f"{NAME}.3mf") assert tried[0] == f"/{NAME}.3mf" @pytest.mark.asyncio async def test_no_second_extension_is_appended_to_it(): tried, _ = await _run_print_start(f"{NAME}.3mf", f"{NAME}.3mf") assert not [p for p in tried if ".3mf.gcode.3mf" in p or ".3mf.3mf" in p] @pytest.mark.asyncio async def test_only_the_reported_file_is_tried(): """The printer named the file, so no other name is guessed at -- the list is the old one minus the two that could not exist.""" tried, _ = await _run_print_start(f"{NAME}.3mf", f"{NAME}.3mf") assert {p.rsplit("/", 1)[-1] for p in tried} == {f"{NAME}.3mf"} def _archive_filenames(sql): """The file names the existing-archive check accepts.""" match = re.search(r"filename IN \(([^)]*)\)", sql) assert match, sql return {name.strip().strip("'") for name in match.group(1).split(",")} @pytest.mark.asyncio async def test_the_existing_archive_check_also_accepts_the_file_itself(): _, statements = await _run_print_start(f"{NAME}.3mf", f"{NAME}.3mf") assert f"{NAME}.3mf" in _archive_filenames(_existing_archive_lookup(statements)) @pytest.mark.asyncio async def test_the_existing_archive_check_gets_nothing_looser(): """A leftover 'printing' archive of another file with the same base name must not swallow this print: a match with progress is taken as the same print and no new archive is made.""" _, statements = await _run_print_start(f"{NAME}.3mf", f"{NAME}.3mf") sql = _existing_archive_lookup(statements) assert f"{NAME}.gcode.3mf" not in _archive_filenames(sql) assert f"print_archives.print_name = '{NAME}.3mf'" in sql @pytest.mark.asyncio async def test_a_bare_subtask_is_looked_up_exactly_as_before(): """The dispatched case, which is every queue and slicer print.""" tried, _ = await _run_print_start(NAME, f"{NAME}.gcode.3mf") assert tried[0] == f"/{NAME}.gcode.3mf" assert f"/{NAME}.3mf" in tried @pytest.mark.asyncio async def test_a_dot_inside_the_model_name_is_kept(): tried, _ = await _run_print_start("My.Model", "My.Model.gcode.3mf") assert tried[0] == "/My.Model.gcode.3mf" @pytest.mark.asyncio async def test_a_dispatched_model_named_with_3mf_keeps_the_old_order(): """A model can be named "Foo.3mf"; its file is then Foo.3mf.gcode.3mf. The printer reports a different file than the subtask, so nothing about the name says the extension is the file's own, and a stale Foo.3mf on the card must not be tried first.""" tried, statements = await _run_print_start("Foo.3mf", "/data/Metadata/plate_1.gcode") assert tried[0] == "/Foo.3mf.gcode.3mf" assert "/Foo.gcode.3mf" not in tried assert _archive_filenames(_existing_archive_lookup(statements)) == {"Foo.3mf.3mf", "Foo.3mf.gcode.3mf"} class TestSubtaskIsTheFile: @pytest.mark.parametrize( "subtask,filename", [ (f"{NAME}.3mf", f"{NAME}.3mf"), (f"{NAME}.gcode.3mf", f"{NAME}.gcode.3mf"), ("Part.3mf", "/sdcard/Part.3mf"), ("Part.3mf", "ftp://Part.3mf"), ], ) def test_the_same_name_as_the_file(self, subtask, filename): from backend.app.main import _subtask_is_the_file assert _subtask_is_the_file(subtask, filename) @pytest.mark.parametrize( "subtask,filename", [ (NAME, f"{NAME}.gcode.3mf"), ("Foo.3mf", "/data/Metadata/plate_1.gcode"), ("Foo.3mf", "Bar.3mf"), ("Part.gcode", "Part.gcode"), ("", ""), ("Part.3mf", None), ], ) def test_anything_else(self, subtask, filename): from backend.app.main import _subtask_is_the_file assert not _subtask_is_the_file(subtask, filename)