| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266 |
- """What a VP in queue mode takes from the slicer onto the queue item.
- Nozzle offset calibration was never read, so every VP-queued print ran with
- "auto" whatever Bambu Studio or the default print options said. The slicer's
- nozzle positions were stored for an "Any <model>" VP too, and the dispatch
- sends a stored mapping as it is instead of resolving it against the rack of the
- printer it picked.
- """
- import json
- from unittest.mock import AsyncMock, MagicMock, patch
- import pytest
- from backend.app.services.virtual_printer.manager import VirtualPrinterInstance
- def _session(added: list, execute_results=None):
- db = AsyncMock()
- db.add = MagicMock(side_effect=lambda item: (added.append(item), setattr(item, "id", 100 + len(added)))[0])
- db.flush = AsyncMock()
- db.commit = AsyncMock()
- if execute_results is not None:
- db.execute = AsyncMock(side_effect=execute_results)
- ctx = AsyncMock()
- ctx.__aenter__ = AsyncMock(return_value=db)
- ctx.__aexit__ = AsyncMock(return_value=False)
- return MagicMock(return_value=ctx), db
- def _vp(tmp_path, factory, **kwargs):
- return VirtualPrinterInstance(
- vp_id=77,
- name="Intake",
- mode="queue",
- model="O1C2",
- access_code="12345678",
- serial_suffix="391800077",
- base_dir=tmp_path,
- session_factory=factory,
- **kwargs,
- )
- async def _enqueue(inst, tmp_path, slicer_opts: dict | None, settings: dict | None = None, announce=None):
- file_path = tmp_path / "test.3mf"
- file_path.write_bytes(b"fake3mf")
- if slicer_opts is not None:
- await inst.on_print_command(file_path.name, {"command": "project_file", **slicer_opts})
- archive = MagicMock(id=1, print_name="test")
- values = settings or {}
- async def _setting(_db, key):
- return values.get(key)
- with (
- patch("backend.app.api.routes.settings.get_setting", side_effect=_setting),
- patch(
- "backend.app.services.archive.ArchiveService.archive_print", new_callable=AsyncMock, return_value=archive
- ),
- patch("backend.app.services.virtual_printer.manager._SLICER_OPTIONS_WAIT_TIMEOUT", 0.05),
- # Its own query would take a result the session mock holds for the test.
- patch("backend.app.services.queue_announcements.announce_queue_jobs_added", announce or AsyncMock()),
- ):
- await inst._add_to_print_queue(file_path, "192.168.1.100")
- return file_path
- class TestNozzleOffsetCalibration:
- @pytest.mark.asyncio
- @pytest.mark.parametrize(("wire", "stored"), [(0, "off"), (1, "on"), (2, "auto")])
- async def test_the_slicers_choice_is_kept(self, tmp_path, wire, stored):
- added: list = []
- factory, _ = _session(added)
- inst = _vp(tmp_path, factory, target_printer_id=7)
- await _enqueue(inst, tmp_path, {"nozzle_offset_cali": wire}, {"default_nozzle_offset_cali": "on"})
- assert added[0].nozzle_offset_cali == stored
- @pytest.mark.asyncio
- async def test_without_a_slicer_choice_the_default_applies(self, tmp_path):
- added: list = []
- factory, _ = _session(added)
- inst = _vp(tmp_path, factory, target_printer_id=7)
- await _enqueue(inst, tmp_path, {"timelapse": True}, {"default_nozzle_offset_cali": "off"})
- assert added[0].nozzle_offset_cali == "off"
- @pytest.mark.asyncio
- async def test_a_late_slicer_choice_is_patched_on(self, tmp_path):
- added: list = []
- position_max = MagicMock(scalar=MagicMock(return_value=None))
- pending = MagicMock(all=MagicMock(return_value=[(101,)]))
- factory, db = _session(added, [position_max, pending, MagicMock()])
- inst = _vp(tmp_path, factory, target_printer_id=7)
- inst._mqtt = MagicMock()
- file_path = await _enqueue(inst, tmp_path, None)
- await inst.on_print_command(
- file_path.name, {"command": "project_file", "file": file_path.name, "nozzle_offset_cali": 0}
- )
- params = dict(db.execute.await_args_list[2].args[0].compile().params)
- assert params["nozzle_offset_cali"] == "off"
- class TestNozzlePositionsNeedOnePrinter:
- @pytest.mark.asyncio
- async def test_a_vp_bound_to_a_printer_keeps_them(self, tmp_path):
- added: list = []
- factory, _ = _session(added)
- inst = _vp(tmp_path, factory, target_printer_id=7)
- await _enqueue(inst, tmp_path, {"nozzle_mapping": [16, -1]})
- assert json.loads(added[0].nozzle_mapping) == [16, -1]
- @pytest.mark.asyncio
- async def test_an_any_model_vp_leaves_them_to_the_dispatch(self, tmp_path):
- added: list = []
- factory, _ = _session(added)
- inst = _vp(tmp_path, factory)
- await _enqueue(inst, tmp_path, {"nozzle_mapping": [16, -1]})
- assert added[0].target_model is not None
- assert added[0].nozzle_mapping is None
- @pytest.mark.asyncio
- async def test_an_any_model_vp_does_not_patch_them_on_late(self, tmp_path):
- added: list = []
- position_max = MagicMock(scalar=MagicMock(return_value=None))
- pending = MagicMock(all=MagicMock(return_value=[(101,)]))
- factory, db = _session(added, [position_max, pending, MagicMock()])
- inst = _vp(tmp_path, factory)
- inst._mqtt = MagicMock()
- file_path = await _enqueue(inst, tmp_path, None)
- await inst.on_print_command(
- file_path.name,
- {"command": "project_file", "file": file_path.name, "nozzle_mapping": [16, -1], "timelapse": True},
- )
- params = dict(db.execute.await_args_list[2].args[0].compile().params)
- assert params["timelapse"] is True
- assert "nozzle_mapping" not in params
- class TestAnyH2DKeepsNozzleIndices:
- @pytest.mark.asyncio
- async def test_extruder_indices_are_the_same_on_every_printer(self, tmp_path):
- """Off a rack model the mapping names extruders, not a printer's rack."""
- added: list = []
- factory, _ = _session(added)
- inst = _vp(tmp_path, factory)
- inst.model = "O1D" # H2D
- await _enqueue(inst, tmp_path, {"nozzle_mapping": [0, 1]})
- assert json.loads(added[0].nozzle_mapping) == [0, 1]
- class TestAmsOnOff:
- @pytest.mark.asyncio
- async def test_the_slicers_choice_is_kept_when_it_arrives_in_time(self, tmp_path):
- added: list = []
- factory, _ = _session(added)
- inst = _vp(tmp_path, factory, target_printer_id=7)
- await _enqueue(inst, tmp_path, {"use_ams": False})
- assert added[0].use_ams is False
- @pytest.mark.asyncio
- async def test_on_when_the_slicer_does_not_say(self, tmp_path):
- added: list = []
- factory, _ = _session(added)
- inst = _vp(tmp_path, factory, target_printer_id=7)
- await _enqueue(inst, tmp_path, {"timelapse": True})
- assert added[0].use_ams is True
- @pytest.mark.asyncio
- async def test_a_vp_for_any_printer_leaves_it_on(self, tmp_path):
- """The slicer saw the VP, which has no AMS; the printer the job goes to
- is picked later and may have one (the dispatch reconciles it, #2595)."""
- added: list = []
- factory, _ = _session(added)
- inst = _vp(tmp_path, factory)
- await _enqueue(inst, tmp_path, {"use_ams": False})
- assert added[0].use_ams is True
- @pytest.mark.asyncio
- async def test_a_vp_for_any_printer_does_not_patch_it_on_late(self, tmp_path):
- added: list = []
- position_max = MagicMock(scalar=MagicMock(return_value=None))
- pending = MagicMock(all=MagicMock(return_value=[(101,)]))
- factory, db = _session(added, [position_max, pending, MagicMock()])
- inst = _vp(tmp_path, factory)
- inst._mqtt = MagicMock()
- file_path = await _enqueue(inst, tmp_path, None)
- await inst.on_print_command(
- file_path.name,
- {"command": "project_file", "file": file_path.name, "use_ams": False, "timelapse": True},
- )
- params = dict(db.execute.await_args_list[2].args[0].compile().params)
- assert params["timelapse"] is True
- assert "use_ams" not in params
- class TestLateAmsMappingPerPlate:
- @pytest.mark.asyncio
- async def test_each_queued_plate_records_the_highest_slot_it_prints(self, tmp_path):
- added: list = []
- factory, _ = _session(added)
- inst = _vp(tmp_path, factory, target_printer_id=7)
- with patch(
- "backend.app.services.filament_requirements.extract_filament_requirements",
- return_value=[{"slot_id": 1, "type": "PLA"}, {"slot_id": 4, "type": "PLA"}],
- ):
- file_path = await _enqueue(inst, tmp_path, {"timelapse": True})
- assert inst._recent_queue_item_reach[file_path.name] == {added[0].id: 4}
- @pytest.mark.asyncio
- async def test_a_plate_the_mapping_cannot_reach_keeps_the_rest_of_the_patch(self, tmp_path):
- import time
- added: list = []
- pending = MagicMock(all=MagicMock(return_value=[(101, None), (102, None)]))
- factory, db = _session(added, [pending, MagicMock(), MagicMock()])
- inst = VirtualPrinterInstance(
- vp_id=78,
- name="LatePlates",
- mode="queue",
- model="X1C",
- access_code="12345678",
- serial_suffix="391800078",
- base_dir=tmp_path,
- session_factory=factory,
- save_ams_mapping=True,
- target_printer_id=7,
- )
- inst._recent_queue_items["test.3mf"] = ([101, 102], time.monotonic())
- inst._recent_queue_item_reach["test.3mf"] = {101: 2, 102: 6}
- await inst.on_print_command(
- "test.3mf",
- {"command": "project_file", "file": "test.3mf", "ams_mapping": [0, 1, -1], "timelapse": True},
- )
- reached, short = (call.args[0] for call in db.execute.await_args_list[1:3])
- reached_params = dict(reached.compile().params)
- short_params = dict(short.compile().params)
- assert json.loads(reached_params["ams_mapping"]) == [0, 1, -1]
- assert [101] in reached_params.values()
- assert "ams_mapping" not in short_params and short_params["timelapse"] is True
- assert [102] in short_params.values()
|