test_idle_only_printer_actions.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. """Actions that only an idle printer takes: firmware upload and AMS slot reset.
  2. Firmware goes onto the SD card the printer prints from, and a slot reset
  3. clears the filament setting of a tray a running print may be feeding from.
  4. """
  5. from types import SimpleNamespace
  6. from unittest.mock import AsyncMock, MagicMock, patch
  7. import pytest
  8. from backend.app.services.firmware_update import FirmwareUpdateService, get_upload_state, reset_upload_state
  9. def _client(state: str) -> MagicMock:
  10. client = MagicMock()
  11. client.state = SimpleNamespace(state=state, firmware_version="01.06.00.00", sdcard=True, connected=True)
  12. client.reset_ams_slot.return_value = True
  13. return client
  14. def _firmware_service() -> MagicMock:
  15. service = MagicMock()
  16. service.check_for_update = AsyncMock(return_value={"update_available": True, "latest_version": "01.07.00.00"})
  17. service.get_firmware_file_info = AsyncMock(return_value={"filename": "fw.bin"})
  18. service.download_firmware = AsyncMock()
  19. service.get_latest_version = AsyncMock(return_value=None)
  20. return service
  21. def _patched(client: MagicMock, service: MagicMock):
  22. return (
  23. patch("backend.app.services.firmware_update.printer_manager.get_client", return_value=client),
  24. patch("backend.app.services.firmware_update.get_firmware_service", return_value=service),
  25. patch(
  26. "backend.app.services.firmware_update.get_storage_info_async",
  27. AsyncMock(return_value={"free_bytes": 10 * 1024**3}),
  28. ),
  29. )
  30. class TestFirmwareUpload:
  31. @pytest.mark.asyncio
  32. @pytest.mark.parametrize("state", ["RUNNING", "PAUSE", "PREPARE", "SLICING", "unknown"])
  33. async def test_a_busy_printer_is_refused_in_the_prepare_check(self, async_client, printer_factory, state):
  34. printer = await printer_factory()
  35. a, b, c = _patched(_client(state), _firmware_service())
  36. with a, b, c:
  37. response = await async_client.get(f"/api/v1/firmware/updates/{printer.id}/prepare")
  38. assert response.status_code == 200
  39. body = response.json()
  40. assert body["can_proceed"] is False
  41. assert any("idle" in error for error in body["errors"])
  42. @pytest.mark.asyncio
  43. @pytest.mark.parametrize("state", ["IDLE", "FINISH", "FAILED"])
  44. async def test_an_idle_printer_passes_the_prepare_check(self, async_client, printer_factory, state):
  45. printer = await printer_factory()
  46. a, b, c = _patched(_client(state), _firmware_service())
  47. with a, b, c:
  48. response = await async_client.get(f"/api/v1/firmware/updates/{printer.id}/prepare")
  49. assert response.json()["can_proceed"] is True, response.json()["errors"]
  50. @pytest.mark.asyncio
  51. async def test_the_upload_route_refuses_a_printing_printer(self, async_client, printer_factory):
  52. printer = await printer_factory()
  53. a, b, c = _patched(_client("RUNNING"), _firmware_service())
  54. with (
  55. a,
  56. b,
  57. c,
  58. patch.object(FirmwareUpdateService, "start_upload", AsyncMock(return_value=True)) as start,
  59. ):
  60. response = await async_client.post(f"/api/v1/firmware/updates/{printer.id}/upload")
  61. assert response.status_code == 400
  62. assert "idle" in response.json()["detail"]
  63. start.assert_not_called()
  64. @pytest.mark.asyncio
  65. async def test_a_print_started_during_the_download_gets_no_firmware(self, tmp_path):
  66. """The download can take minutes; the card is checked again before writing to it."""
  67. client = _client("IDLE")
  68. service = _firmware_service()
  69. firmware = tmp_path / "fw.bin"
  70. firmware.write_bytes(b"x")
  71. async def download(*_args, **_kwargs):
  72. client.state.state = "RUNNING"
  73. return firmware
  74. service.download_firmware = AsyncMock(side_effect=download)
  75. upload = AsyncMock(return_value=True)
  76. reset_upload_state(9101)
  77. with (
  78. patch("backend.app.services.firmware_update.printer_manager.get_client", return_value=client),
  79. patch("backend.app.services.firmware_update.get_firmware_service", return_value=service),
  80. patch("backend.app.services.firmware_update.upload_file_async", upload),
  81. patch(
  82. "backend.app.services.firmware_update.get_ftp_retry_settings",
  83. AsyncMock(return_value=(False, 0, 0, 30)),
  84. ),
  85. ):
  86. await FirmwareUpdateService()._do_upload(9101, "192.0.2.10", "12345678", "X1C", "01.07.00.00")
  87. upload.assert_not_called()
  88. state = get_upload_state(9101)
  89. assert state.status.value == "error"
  90. assert "idle" in (state.error or "")
  91. reset_upload_state(9101)
  92. class TestAmsSlotReset:
  93. @pytest.mark.asyncio
  94. @pytest.mark.parametrize("state", ["RUNNING", "PAUSE", "PREPARE", "SLICING"])
  95. async def test_refused_while_a_print_is_loaded(self, async_client, printer_factory, state):
  96. printer = await printer_factory()
  97. client = _client(state)
  98. with patch("backend.app.api.routes.printers.printer_manager.get_client", return_value=client):
  99. response = await async_client.post(f"/api/v1/printers/{printer.id}/ams/0/tray/1/reset")
  100. assert response.status_code == 409
  101. client.reset_ams_slot.assert_not_called()
  102. @pytest.mark.asyncio
  103. @pytest.mark.parametrize("state", ["IDLE", "FINISH", "FAILED"])
  104. async def test_sent_when_no_print_is_loaded(self, async_client, printer_factory, state):
  105. printer = await printer_factory()
  106. client = _client(state)
  107. with patch("backend.app.api.routes.printers.printer_manager.get_client", return_value=client):
  108. response = await async_client.post(f"/api/v1/printers/{printer.id}/ams/0/tray/1/reset")
  109. assert response.status_code == 200
  110. client.reset_ams_slot.assert_called_once_with(ams_id=0, tray_id=1)
  111. class TestQueueDuringFirmwareUpload:
  112. """A queued print does not start while firmware is being put on the printer's card."""
  113. @pytest.fixture
  114. def idle_printer(self):
  115. from backend.app.services.printer_manager import printer_manager
  116. printer_id = 9102
  117. client = MagicMock()
  118. client.state = SimpleNamespace(state="IDLE", connected=True)
  119. printer_manager._clients[printer_id] = client
  120. reset_upload_state(printer_id)
  121. try:
  122. with (
  123. patch.object(printer_manager, "is_connected", return_value=True),
  124. patch.object(printer_manager, "get_status", return_value=client.state),
  125. ):
  126. yield printer_id
  127. finally:
  128. printer_manager._clients.pop(printer_id, None)
  129. reset_upload_state(printer_id)
  130. @pytest.mark.parametrize("status", ["preparing", "downloading", "uploading"])
  131. def test_not_idle_while_an_upload_runs(self, idle_printer, status):
  132. from backend.app.services.firmware_update import FirmwareUploadStatus
  133. from backend.app.services.print_scheduler import PrintScheduler
  134. get_upload_state(idle_printer).status = FirmwareUploadStatus(status)
  135. assert PrintScheduler()._is_printer_idle(idle_printer, require_plate_clear=False) is False
  136. @pytest.mark.parametrize("status", ["idle", "complete", "error"])
  137. def test_idle_again_once_it_is_over(self, idle_printer, status):
  138. from backend.app.services.firmware_update import FirmwareUploadStatus
  139. from backend.app.services.print_scheduler import PrintScheduler
  140. get_upload_state(idle_printer).status = FirmwareUploadStatus(status)
  141. assert PrintScheduler()._is_printer_idle(idle_printer, require_plate_clear=False) is True