test_plate_check_chamber_light.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. """Plate check: the chamber light it switched on is always switched back off.
  2. on_print_start turns the chamber light on for plate detection when it was
  3. off. It must go back off as soon as the camera is done with it -- before the
  4. broadcast and the provider sends -- and also when anything on the way raises,
  5. or the printer is left lit for the rest of the print.
  6. """
  7. from types import SimpleNamespace
  8. from unittest.mock import AsyncMock, MagicMock, patch
  9. import pytest
  10. from backend.app.main import (
  11. _active_prints,
  12. _expected_print_creators,
  13. _expected_print_registered_at,
  14. _expected_prints,
  15. _print_ams_mappings,
  16. _timelapse_baselines,
  17. )
  18. @pytest.fixture(autouse=True)
  19. def _clear_dicts():
  20. dicts = (
  21. _expected_prints,
  22. _expected_print_registered_at,
  23. _expected_print_creators,
  24. _print_ams_mappings,
  25. _active_prints,
  26. _timelapse_baselines,
  27. )
  28. for d in dicts:
  29. d.clear()
  30. yield
  31. for d in dicts:
  32. d.clear()
  33. def _printer():
  34. printer = MagicMock()
  35. printer.id = 1
  36. printer.name = "TestP1S"
  37. printer.plate_detection_enabled = True
  38. printer.plate_detection_roi_x = None
  39. printer.plate_detection_roi_y = None
  40. printer.plate_detection_roi_w = None
  41. printer.plate_detection_roi_h = None
  42. printer.auto_archive = False
  43. printer.external_camera_enabled = False
  44. printer.external_camera_url = None
  45. return printer
  46. async def _run_print_start(events: list[str], *, check_plate_empty, pause_side_effect=None):
  47. printer = _printer()
  48. client = MagicMock()
  49. client.state = SimpleNamespace(chamber_light=False)
  50. client.set_chamber_light = MagicMock(side_effect=lambda on: events.append(f"light:{'on' if on else 'off'}"))
  51. client.pause_print = MagicMock(side_effect=pause_side_effect or (lambda: events.append("pause")))
  52. def execute_router(stmt, *args, **kwargs):
  53. sql = str(stmt).lower()
  54. found = printer if "from printers" in sql else None
  55. return MagicMock(
  56. scalar_one_or_none=MagicMock(return_value=found),
  57. scalars=MagicMock(return_value=MagicMock(all=MagicMock(return_value=[found] if found else []))),
  58. )
  59. session = AsyncMock()
  60. session.__aenter__ = AsyncMock(return_value=session)
  61. session.__aexit__ = AsyncMock()
  62. session.execute = AsyncMock(side_effect=execute_router)
  63. async def broadcast(_message):
  64. events.append("broadcast")
  65. async def notify(**_kwargs):
  66. events.append("notify")
  67. with (
  68. patch("backend.app.main.async_session", return_value=session),
  69. patch("backend.app.main.notification_service") as mock_notif,
  70. patch("backend.app.main.smart_plug_manager") as mock_plug,
  71. patch("backend.app.main.ws_manager") as mock_ws,
  72. patch("backend.app.main.printer_manager") as mock_pm,
  73. patch("backend.app.main.mqtt_relay") as mock_relay,
  74. patch("backend.app.main.asyncio.sleep", new_callable=AsyncMock),
  75. patch("backend.app.main._capture_snapshot_for_notification", new_callable=AsyncMock, return_value=b"jpeg"),
  76. patch("backend.app.main._record_energy_start", new_callable=AsyncMock),
  77. patch("backend.app.main._load_objects_from_archive"),
  78. patch("backend.app.main._store_spoolman_print_data", new_callable=AsyncMock),
  79. patch("backend.app.main._send_print_start_notification", new_callable=AsyncMock),
  80. patch("backend.app.services.plate_detection.check_plate_empty", check_plate_empty),
  81. ):
  82. mock_notif.on_plate_not_empty = AsyncMock(side_effect=notify)
  83. mock_notif.on_print_start = AsyncMock()
  84. mock_plug.on_print_start = AsyncMock()
  85. mock_ws.broadcast = AsyncMock(side_effect=broadcast)
  86. mock_ws.send_print_start = AsyncMock()
  87. mock_ws.send_archive_updated = AsyncMock()
  88. mock_relay.on_print_start = AsyncMock()
  89. mock_pm.get_client = MagicMock(return_value=client)
  90. mock_pm.get_printer = MagicMock(return_value=MagicMock(name="Test", serial_number="TEST123"))
  91. from backend.app.main import on_print_start
  92. await on_print_start(1, {"filename": "Benchy.3mf", "subtask_name": "Benchy"})
  93. def _objects_on_plate():
  94. return AsyncMock(
  95. return_value=SimpleNamespace(needs_calibration=False, is_empty=False, confidence=0.9, difference_percent=12.0)
  96. )
  97. @pytest.mark.asyncio
  98. async def test_light_goes_off_before_the_notifications_are_sent():
  99. events: list[str] = []
  100. await _run_print_start(events, check_plate_empty=_objects_on_plate())
  101. assert events.index("light:on") < events.index("light:off")
  102. assert events.index("light:off") < events.index("broadcast")
  103. assert events.index("light:off") < events.index("notify")
  104. @pytest.mark.asyncio
  105. async def test_light_goes_off_when_detection_raises():
  106. events: list[str] = []
  107. await _run_print_start(events, check_plate_empty=AsyncMock(side_effect=RuntimeError("camera gone")))
  108. assert events == ["light:on", "light:off"]
  109. @pytest.mark.asyncio
  110. async def test_light_goes_off_when_pausing_raises():
  111. events: list[str] = []
  112. await _run_print_start(
  113. events, check_plate_empty=_objects_on_plate(), pause_side_effect=RuntimeError("mqtt disconnected")
  114. )
  115. assert "light:off" in events
  116. assert "notify" not in events