test_vp_queue_intake_d2.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. """What a VP in queue mode takes from the slicer onto the queue item.
  2. Nozzle offset calibration was never read, so every VP-queued print ran with
  3. "auto" whatever Bambu Studio or the default print options said. The slicer's
  4. nozzle positions were stored for an "Any <model>" VP too, and the dispatch
  5. sends a stored mapping as it is instead of resolving it against the rack of the
  6. printer it picked.
  7. """
  8. import json
  9. from unittest.mock import AsyncMock, MagicMock, patch
  10. import pytest
  11. from backend.app.services.virtual_printer.manager import VirtualPrinterInstance
  12. def _session(added: list, execute_results=None):
  13. db = AsyncMock()
  14. db.add = MagicMock(side_effect=lambda item: (added.append(item), setattr(item, "id", 100 + len(added)))[0])
  15. db.flush = AsyncMock()
  16. db.commit = AsyncMock()
  17. if execute_results is not None:
  18. db.execute = AsyncMock(side_effect=execute_results)
  19. ctx = AsyncMock()
  20. ctx.__aenter__ = AsyncMock(return_value=db)
  21. ctx.__aexit__ = AsyncMock(return_value=False)
  22. return MagicMock(return_value=ctx), db
  23. def _vp(tmp_path, factory, **kwargs):
  24. return VirtualPrinterInstance(
  25. vp_id=77,
  26. name="Intake",
  27. mode="queue",
  28. model="O1C2",
  29. access_code="12345678",
  30. serial_suffix="391800077",
  31. base_dir=tmp_path,
  32. session_factory=factory,
  33. **kwargs,
  34. )
  35. async def _enqueue(inst, tmp_path, slicer_opts: dict | None, settings: dict | None = None):
  36. file_path = tmp_path / "test.3mf"
  37. file_path.write_bytes(b"fake3mf")
  38. if slicer_opts is not None:
  39. await inst.on_print_command(file_path.name, {"command": "project_file", **slicer_opts})
  40. archive = MagicMock(id=1, print_name="test")
  41. values = settings or {}
  42. async def _setting(_db, key):
  43. return values.get(key)
  44. with (
  45. patch("backend.app.api.routes.settings.get_setting", side_effect=_setting),
  46. patch(
  47. "backend.app.services.archive.ArchiveService.archive_print", new_callable=AsyncMock, return_value=archive
  48. ),
  49. patch("backend.app.services.virtual_printer.manager._SLICER_OPTIONS_WAIT_TIMEOUT", 0.05),
  50. ):
  51. await inst._add_to_print_queue(file_path, "192.168.1.100")
  52. return file_path
  53. class TestNozzleOffsetCalibration:
  54. @pytest.mark.asyncio
  55. @pytest.mark.parametrize(("wire", "stored"), [(0, "off"), (1, "on"), (2, "auto")])
  56. async def test_the_slicers_choice_is_kept(self, tmp_path, wire, stored):
  57. added: list = []
  58. factory, _ = _session(added)
  59. inst = _vp(tmp_path, factory, target_printer_id=7)
  60. await _enqueue(inst, tmp_path, {"nozzle_offset_cali": wire}, {"default_nozzle_offset_cali": "on"})
  61. assert added[0].nozzle_offset_cali == stored
  62. @pytest.mark.asyncio
  63. async def test_without_a_slicer_choice_the_default_applies(self, tmp_path):
  64. added: list = []
  65. factory, _ = _session(added)
  66. inst = _vp(tmp_path, factory, target_printer_id=7)
  67. await _enqueue(inst, tmp_path, {"timelapse": True}, {"default_nozzle_offset_cali": "off"})
  68. assert added[0].nozzle_offset_cali == "off"
  69. @pytest.mark.asyncio
  70. async def test_a_late_slicer_choice_is_patched_on(self, tmp_path):
  71. added: list = []
  72. position_max = MagicMock(scalar=MagicMock(return_value=None))
  73. pending = MagicMock(all=MagicMock(return_value=[(101,)]))
  74. factory, db = _session(added, [position_max, pending, MagicMock()])
  75. inst = _vp(tmp_path, factory, target_printer_id=7)
  76. inst._mqtt = MagicMock()
  77. file_path = await _enqueue(inst, tmp_path, None)
  78. await inst.on_print_command(
  79. file_path.name, {"command": "project_file", "file": file_path.name, "nozzle_offset_cali": 0}
  80. )
  81. params = dict(db.execute.await_args_list[2].args[0].compile().params)
  82. assert params["nozzle_offset_cali"] == "off"
  83. class TestNozzlePositionsNeedOnePrinter:
  84. @pytest.mark.asyncio
  85. async def test_a_vp_bound_to_a_printer_keeps_them(self, tmp_path):
  86. added: list = []
  87. factory, _ = _session(added)
  88. inst = _vp(tmp_path, factory, target_printer_id=7)
  89. await _enqueue(inst, tmp_path, {"nozzle_mapping": [16, -1]})
  90. assert json.loads(added[0].nozzle_mapping) == [16, -1]
  91. @pytest.mark.asyncio
  92. async def test_an_any_model_vp_leaves_them_to_the_dispatch(self, tmp_path):
  93. added: list = []
  94. factory, _ = _session(added)
  95. inst = _vp(tmp_path, factory)
  96. await _enqueue(inst, tmp_path, {"nozzle_mapping": [16, -1]})
  97. assert added[0].target_model is not None
  98. assert added[0].nozzle_mapping is None
  99. @pytest.mark.asyncio
  100. async def test_an_any_model_vp_does_not_patch_them_on_late(self, tmp_path):
  101. added: list = []
  102. position_max = MagicMock(scalar=MagicMock(return_value=None))
  103. pending = MagicMock(all=MagicMock(return_value=[(101,)]))
  104. factory, db = _session(added, [position_max, pending, MagicMock()])
  105. inst = _vp(tmp_path, factory)
  106. inst._mqtt = MagicMock()
  107. file_path = await _enqueue(inst, tmp_path, None)
  108. await inst.on_print_command(
  109. file_path.name,
  110. {"command": "project_file", "file": file_path.name, "nozzle_mapping": [16, -1], "timelapse": True},
  111. )
  112. params = dict(db.execute.await_args_list[2].args[0].compile().params)
  113. assert params["timelapse"] is True
  114. assert "nozzle_mapping" not in params
  115. class TestAnyH2DKeepsNozzleIndices:
  116. @pytest.mark.asyncio
  117. async def test_extruder_indices_are_the_same_on_every_printer(self, tmp_path):
  118. """Off a rack model the mapping names extruders, not a printer's rack."""
  119. added: list = []
  120. factory, _ = _session(added)
  121. inst = _vp(tmp_path, factory)
  122. inst.model = "O1D" # H2D
  123. await _enqueue(inst, tmp_path, {"nozzle_mapping": [0, 1]})
  124. assert json.loads(added[0].nozzle_mapping) == [0, 1]
  125. class TestAmsOnOff:
  126. @pytest.mark.asyncio
  127. async def test_the_slicers_choice_is_kept_when_it_arrives_in_time(self, tmp_path):
  128. added: list = []
  129. factory, _ = _session(added)
  130. inst = _vp(tmp_path, factory, target_printer_id=7)
  131. await _enqueue(inst, tmp_path, {"use_ams": False})
  132. assert added[0].use_ams is False
  133. @pytest.mark.asyncio
  134. async def test_on_when_the_slicer_does_not_say(self, tmp_path):
  135. added: list = []
  136. factory, _ = _session(added)
  137. inst = _vp(tmp_path, factory, target_printer_id=7)
  138. await _enqueue(inst, tmp_path, {"timelapse": True})
  139. assert added[0].use_ams is True
  140. class TestLateAmsMappingPerPlate:
  141. @pytest.mark.asyncio
  142. async def test_each_queued_plate_records_the_highest_slot_it_prints(self, tmp_path):
  143. added: list = []
  144. factory, _ = _session(added)
  145. inst = _vp(tmp_path, factory, target_printer_id=7)
  146. with patch(
  147. "backend.app.services.filament_requirements.extract_filament_requirements",
  148. return_value=[{"slot_id": 1, "type": "PLA"}, {"slot_id": 4, "type": "PLA"}],
  149. ):
  150. file_path = await _enqueue(inst, tmp_path, {"timelapse": True})
  151. assert inst._recent_queue_item_reach[file_path.name] == {added[0].id: 4}
  152. @pytest.mark.asyncio
  153. async def test_a_plate_the_mapping_cannot_reach_keeps_the_rest_of_the_patch(self, tmp_path):
  154. import time
  155. added: list = []
  156. pending = MagicMock(all=MagicMock(return_value=[(101, None), (102, None)]))
  157. factory, db = _session(added, [pending, MagicMock(), MagicMock()])
  158. inst = VirtualPrinterInstance(
  159. vp_id=78,
  160. name="LatePlates",
  161. mode="queue",
  162. model="X1C",
  163. access_code="12345678",
  164. serial_suffix="391800078",
  165. base_dir=tmp_path,
  166. session_factory=factory,
  167. save_ams_mapping=True,
  168. target_printer_id=7,
  169. )
  170. inst._recent_queue_items["test.3mf"] = ([101, 102], time.monotonic())
  171. inst._recent_queue_item_reach["test.3mf"] = {101: 2, 102: 6}
  172. await inst.on_print_command(
  173. "test.3mf",
  174. {"command": "project_file", "file": "test.3mf", "ams_mapping": [0, 1, -1], "timelapse": True},
  175. )
  176. reached, short = (call.args[0] for call in db.execute.await_args_list[1:3])
  177. reached_params = dict(reached.compile().params)
  178. short_params = dict(short.compile().params)
  179. assert json.loads(reached_params["ams_mapping"]) == [0, 1, -1]
  180. assert [101] in reached_params.values()
  181. assert "ams_mapping" not in short_params and short_params["timelapse"] is True
  182. assert [102] in short_params.values()