"""Preheat heats the bed for the plate the queued job prints (PL-C7). The archive stores one bed temperature, the highest any filament of the project asks for, and preheat and keep-warm used it for every job: a PLA plate of a project that also holds PETG was preheated to the PETG temperature. They now read the job's own plate from the archive's file. """ import json import zipfile from pathlib import Path from types import SimpleNamespace import pytest from backend.app.services.archive import ThreeMFParser from backend.app.services.print_scheduler import _queued_plate_bed_temperature from backend.app.utils.threemf_tools import plate_bed_temperature PLA, PETG_PLATE_FILAMENTS = [1], [2, 3] def _3mf(path: Path, plates: dict[int, list[int]], gcode: tuple[int, ...] = ()) -> Path: """Plate N prints the filaments listed for it (1-based ids), PLA at 55, PLA at 60 and PETG at 70; ``gcode`` are the plates sliced.""" plate_xml = "".join( f"" + "".join(f"" for fid in filaments) + "" for index, filaments in plates.items() ) settings = {"curr_bed_type": "Textured PEI Plate", "textured_plate_temp": ["55", "60", "70"]} with zipfile.ZipFile(path, "w") as zf: zf.writestr("Metadata/slice_info.config", f"{plate_xml}") zf.writestr("Metadata/project_settings.config", json.dumps(settings)) zf.writestr("3D/3dmodel.model", "") for index in gcode: zf.writestr(f"Metadata/plate_{index}.gcode", "; plate") return path def test_each_plate_has_its_own_filaments_temperature(tmp_path): path = _3mf(tmp_path / "a.3mf", {1: PLA, 2: PETG_PLATE_FILAMENTS}) assert plate_bed_temperature(path, 1) == 55 assert plate_bed_temperature(path, 2) == 70 def test_without_a_plate_the_one_dispatched_decides(tmp_path): """A job without a plate prints the file's default (lowest sliced) plate.""" path = _3mf(tmp_path / "a.3mf", {1: PLA, 2: PETG_PLATE_FILAMENTS}, gcode=(1, 2)) assert plate_bed_temperature(path, None) == 55 assert plate_bed_temperature(path, 0) == 55 def test_a_plate_whose_filaments_are_unknown_takes_the_highest(tmp_path): assert plate_bed_temperature(_3mf(tmp_path / "a.3mf", {1: [], 2: []}), None) == 70 def test_an_unreadable_file_is_none(tmp_path): (tmp_path / "broken.3mf").write_bytes(b"not a zip") assert plate_bed_temperature(tmp_path / "broken.3mf", 1) is None assert plate_bed_temperature(tmp_path / "missing.3mf", 1) is None def test_a_damaged_member_is_none(tmp_path): """Corrupt deflate data raises zlib.error, which is no OSError: preheat runs this on every dispatch, so nothing may escape it.""" path = tmp_path / "a.3mf" with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as zf: zf.writestr("Metadata/project_settings.config", json.dumps({"bed_temperature": ["60"] * 200})) raw = bytearray(path.read_bytes()) start = raw.index(b"PK\x03\x04") + 30 + len("Metadata/project_settings.config") raw[start : start + 8] = b"\xff" * 8 path.write_bytes(bytes(raw)) assert plate_bed_temperature(path, 1) is None def test_an_infinite_temperature_is_none(tmp_path): path = tmp_path / "a.3mf" with zipfile.ZipFile(path, "w") as zf: zf.writestr("Metadata/project_settings.config", json.dumps({"bed_temperature": ["inf"]})) assert plate_bed_temperature(path, 1) is None def test_the_archive_keeps_the_projects_highest(tmp_path): """The stored value stays the safe upper bound for any plate of it.""" path = _3mf(tmp_path / "a.3mf", {1: PLA, 2: PETG_PLATE_FILAMENTS}) assert ThreeMFParser(path, plate_number=1).parse().get("bed_temperature") == 70 @pytest.fixture def base_dir(monkeypatch, tmp_path) -> Path: from backend.app.core.config import settings monkeypatch.setattr(settings, "base_dir", tmp_path) return tmp_path @pytest.mark.asyncio @pytest.mark.parametrize(("plate_id", "expected"), [(1, 55), (2, 70)]) async def test_a_job_is_heated_for_its_plate(base_dir, plate_id, expected): _3mf(base_dir / "a.3mf", {1: PLA, 2: PETG_PLATE_FILAMENTS}) archive = SimpleNamespace(file_path="a.3mf", bed_temperature=70) assert await _queued_plate_bed_temperature(SimpleNamespace(plate_id=plate_id), archive) == expected @pytest.mark.asyncio async def test_an_unreadable_file_falls_back_to_the_stored_value(base_dir): archive = SimpleNamespace(file_path="gone.3mf", bed_temperature=65) assert await _queued_plate_bed_temperature(SimpleNamespace(plate_id=2), archive) == 65 @pytest.mark.asyncio async def test_no_archive_is_zero(): assert await _queued_plate_bed_temperature(SimpleNamespace(plate_id=None), None) == 0 @pytest.mark.asyncio async def test_preheat_heats_for_the_job_s_plate(base_dir): from unittest.mock import AsyncMock, patch from backend.app.services.print_scheduler import PrintScheduler from backend.tests.unit.test_scheduler_preheat import _ints, _make_client, _make_printer, _make_state _3mf(base_dir / "a.3mf", {1: PLA, 2: PETG_PLATE_FILAMENTS}) scheduler = PrintScheduler() item = SimpleNamespace(id=42, plate_id=1, preheat_override="on", preheat_chamber_target_override=0) client = _make_client() with ( patch.object(scheduler, "_get_bool_setting", AsyncMock(return_value=False)), patch.object(scheduler, "_get_int_setting", _ints(preheat_soak_seconds=0)), patch("backend.app.services.print_scheduler.printer_manager") as pm, patch("backend.app.services.print_scheduler.asyncio.sleep", AsyncMock()), ): pm.get_client.return_value = client pm.get_status.return_value = _make_state(55.0, 0.0) await scheduler._preheat_and_soak( AsyncMock(), item, _make_printer("H2D"), SimpleNamespace(file_path="a.3mf", bed_temperature=70) ) client.set_bed_temperature.assert_called_once_with(55) @pytest.mark.asyncio async def test_keep_warm_holds_for_the_next_job_s_plate(base_dir): from unittest.mock import patch from backend.app.services.print_scheduler import PrintScheduler from backend.tests.unit.test_scheduler_keep_warm import _make_client, _make_item, _make_state, _run _3mf(base_dir / "a.3mf", {1: PLA, 2: PETG_PLATE_FILAMENTS}) item = _make_item() item.plate_id = 2 item.archive = SimpleNamespace(file_path="a.3mf", bed_temperature=110) client = _make_client() with patch("backend.app.services.print_scheduler.printer_manager") as pm: pm.get_status.return_value = _make_state() pm.get_client.return_value = client await _run(PrintScheduler(), items=[item], hold_temp=40) client.set_bed_temperature.assert_called_once_with(70)