test_scheduler_clear_plate.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. """Tests for the clear plate queue flow in the print scheduler."""
  2. import logging
  3. from unittest.mock import AsyncMock, MagicMock, patch
  4. import pytest
  5. from backend.app.services.print_scheduler import PrintScheduler
  6. from backend.app.services.printer_manager import PrinterManager
  7. class TestPrinterManagerPlateCleared:
  8. """Test the plate-cleared flag management in PrinterManager."""
  9. @pytest.fixture
  10. def manager(self):
  11. return PrinterManager()
  12. def test_plate_cleared_initially_false(self, manager):
  13. """No printers should have plate cleared by default."""
  14. assert not manager.is_plate_cleared(1)
  15. assert not manager.is_plate_cleared(999)
  16. def test_set_plate_cleared(self, manager):
  17. """Setting plate cleared should make is_plate_cleared return True."""
  18. manager.set_plate_cleared(1)
  19. assert manager.is_plate_cleared(1)
  20. assert not manager.is_plate_cleared(2)
  21. def test_consume_plate_cleared(self, manager):
  22. """Consuming plate cleared should reset the flag."""
  23. manager.set_plate_cleared(1)
  24. assert manager.is_plate_cleared(1)
  25. manager.consume_plate_cleared(1)
  26. assert not manager.is_plate_cleared(1)
  27. def test_consume_plate_cleared_idempotent(self, manager):
  28. """Consuming when not set should not raise."""
  29. manager.consume_plate_cleared(1) # Should not raise
  30. assert not manager.is_plate_cleared(1)
  31. def test_set_plate_cleared_multiple_printers(self, manager):
  32. """Plate cleared should be tracked per printer."""
  33. manager.set_plate_cleared(1)
  34. manager.set_plate_cleared(3)
  35. assert manager.is_plate_cleared(1)
  36. assert not manager.is_plate_cleared(2)
  37. assert manager.is_plate_cleared(3)
  38. def test_consume_only_affects_target_printer(self, manager):
  39. """Consuming plate cleared for one printer should not affect others."""
  40. manager.set_plate_cleared(1)
  41. manager.set_plate_cleared(2)
  42. manager.consume_plate_cleared(1)
  43. assert not manager.is_plate_cleared(1)
  44. assert manager.is_plate_cleared(2)
  45. class TestSchedulerIdleCheckWithPlateCleared:
  46. """Test _is_printer_idle with plate-cleared flag interactions."""
  47. @pytest.fixture
  48. def scheduler(self):
  49. return PrintScheduler()
  50. @patch("backend.app.services.print_scheduler.printer_manager")
  51. def test_idle_state_is_idle(self, mock_pm, scheduler):
  52. """Printer in IDLE state should be considered idle."""
  53. mock_pm.is_connected.return_value = True
  54. mock_pm.get_status.return_value = MagicMock(state="IDLE")
  55. assert scheduler._is_printer_idle(1) is True
  56. @patch("backend.app.services.print_scheduler.printer_manager")
  57. def test_running_state_not_idle(self, mock_pm, scheduler):
  58. """Printer in RUNNING state should not be idle."""
  59. mock_pm.is_connected.return_value = True
  60. mock_pm.get_status.return_value = MagicMock(state="RUNNING")
  61. assert scheduler._is_printer_idle(1) is False
  62. @patch("backend.app.services.print_scheduler.printer_manager")
  63. def test_finish_state_not_idle_without_plate_cleared(self, mock_pm, scheduler):
  64. """Printer in FINISH state should NOT be idle without plate cleared."""
  65. mock_pm.is_connected.return_value = True
  66. mock_pm.get_status.return_value = MagicMock(state="FINISH")
  67. mock_pm.is_plate_cleared.return_value = False
  68. assert scheduler._is_printer_idle(1) is False
  69. @patch("backend.app.services.print_scheduler.printer_manager")
  70. def test_finish_state_idle_with_plate_cleared(self, mock_pm, scheduler):
  71. """Printer in FINISH state should be idle when plate is cleared."""
  72. mock_pm.is_connected.return_value = True
  73. mock_pm.get_status.return_value = MagicMock(state="FINISH")
  74. mock_pm.is_plate_cleared.return_value = True
  75. assert scheduler._is_printer_idle(1) is True
  76. @patch("backend.app.services.print_scheduler.printer_manager")
  77. def test_failed_state_not_idle_without_plate_cleared(self, mock_pm, scheduler):
  78. """Printer in FAILED state should NOT be idle without plate cleared."""
  79. mock_pm.is_connected.return_value = True
  80. mock_pm.get_status.return_value = MagicMock(state="FAILED")
  81. mock_pm.is_plate_cleared.return_value = False
  82. assert scheduler._is_printer_idle(1) is False
  83. @patch("backend.app.services.print_scheduler.printer_manager")
  84. def test_failed_state_idle_with_plate_cleared(self, mock_pm, scheduler):
  85. """Printer in FAILED state should be idle when plate is cleared."""
  86. mock_pm.is_connected.return_value = True
  87. mock_pm.get_status.return_value = MagicMock(state="FAILED")
  88. mock_pm.is_plate_cleared.return_value = True
  89. assert scheduler._is_printer_idle(1) is True
  90. @patch("backend.app.services.print_scheduler.printer_manager")
  91. def test_disconnected_printer_not_idle(self, mock_pm, scheduler):
  92. """Disconnected printer should never be idle."""
  93. mock_pm.is_connected.return_value = False
  94. assert scheduler._is_printer_idle(1) is False
  95. @patch("backend.app.services.print_scheduler.printer_manager")
  96. def test_no_status_not_idle(self, mock_pm, scheduler):
  97. """Printer with no status should not be idle."""
  98. mock_pm.is_connected.return_value = True
  99. mock_pm.get_status.return_value = None
  100. assert scheduler._is_printer_idle(1) is False
  101. @patch("backend.app.services.print_scheduler.printer_manager")
  102. def test_finish_state_idle_when_require_plate_clear_disabled(self, mock_pm, scheduler):
  103. """FINISH state should be idle when require_plate_clear=False, regardless of plate cleared."""
  104. mock_pm.is_connected.return_value = True
  105. mock_pm.get_status.return_value = MagicMock(state="FINISH")
  106. mock_pm.is_plate_cleared.return_value = False
  107. assert scheduler._is_printer_idle(1, require_plate_clear=False) is True
  108. @patch("backend.app.services.print_scheduler.printer_manager")
  109. def test_failed_state_idle_when_require_plate_clear_disabled(self, mock_pm, scheduler):
  110. """FAILED state should be idle when require_plate_clear=False."""
  111. mock_pm.is_connected.return_value = True
  112. mock_pm.get_status.return_value = MagicMock(state="FAILED")
  113. mock_pm.is_plate_cleared.return_value = False
  114. assert scheduler._is_printer_idle(1, require_plate_clear=False) is True
  115. @patch("backend.app.services.print_scheduler.printer_manager")
  116. def test_running_state_not_idle_even_when_require_plate_clear_disabled(self, mock_pm, scheduler):
  117. """RUNNING state should NOT be idle even with require_plate_clear=False."""
  118. mock_pm.is_connected.return_value = True
  119. mock_pm.get_status.return_value = MagicMock(state="RUNNING")
  120. assert scheduler._is_printer_idle(1, require_plate_clear=False) is False
  121. @patch("backend.app.services.print_scheduler.printer_manager")
  122. def test_idle_state_unaffected_by_require_plate_clear(self, mock_pm, scheduler):
  123. """IDLE state should always be idle regardless of require_plate_clear."""
  124. mock_pm.is_connected.return_value = True
  125. mock_pm.get_status.return_value = MagicMock(state="IDLE")
  126. assert scheduler._is_printer_idle(1, require_plate_clear=False) is True
  127. @patch("backend.app.services.print_scheduler.printer_manager")
  128. def test_finish_state_still_needs_plate_cleared_when_setting_enabled(self, mock_pm, scheduler):
  129. """FINISH + require_plate_clear=True + plate not cleared → NOT idle (default behavior)."""
  130. mock_pm.is_connected.return_value = True
  131. mock_pm.get_status.return_value = MagicMock(state="FINISH")
  132. mock_pm.is_plate_cleared.return_value = False
  133. assert scheduler._is_printer_idle(1, require_plate_clear=True) is False
  134. class TestSchedulerQueueCheckLogging:
  135. """Test queue check logging when pending items are found (#374)."""
  136. @pytest.fixture
  137. def scheduler(self):
  138. return PrintScheduler()
  139. @pytest.mark.asyncio
  140. @patch("backend.app.services.print_scheduler.printer_manager")
  141. async def test_check_queue_logs_pending_items(self, mock_pm, scheduler, caplog):
  142. """Verify pending items are logged when found in check_queue."""
  143. mock_item = MagicMock()
  144. mock_item.id = 42
  145. mock_item.printer_id = 1
  146. mock_item.archive_id = 100
  147. mock_item.library_file_id = None
  148. mock_item.scheduled_time = None
  149. mock_item.manual_start = False
  150. mock_item.target_model = None
  151. mock_pm.is_connected.return_value = True
  152. mock_pm.get_status.return_value = MagicMock(state="RUNNING")
  153. mock_result = MagicMock()
  154. mock_result.scalars.return_value.all.return_value = [mock_item]
  155. with (
  156. patch("backend.app.services.print_scheduler.async_session") as mock_session_ctx,
  157. caplog.at_level(logging.INFO, logger="backend.app.services.print_scheduler"),
  158. ):
  159. mock_db = AsyncMock()
  160. mock_db.execute = AsyncMock(return_value=mock_result)
  161. mock_session_ctx.return_value.__aenter__ = AsyncMock(return_value=mock_db)
  162. mock_session_ctx.return_value.__aexit__ = AsyncMock(return_value=False)
  163. await scheduler.check_queue()
  164. queue_logs = [r for r in caplog.records if "Queue check" in r.message]
  165. assert len(queue_logs) == 1
  166. assert "1 pending items" in queue_logs[0].message
  167. assert "42" in queue_logs[0].message # item ID
  168. @pytest.mark.asyncio
  169. async def test_check_queue_no_log_when_empty(self, scheduler, caplog):
  170. """Verify no queue log when no pending items found."""
  171. mock_result = MagicMock()
  172. mock_result.scalars.return_value.all.return_value = []
  173. with (
  174. patch("backend.app.services.print_scheduler.async_session") as mock_session_ctx,
  175. caplog.at_level(logging.INFO, logger="backend.app.services.print_scheduler"),
  176. ):
  177. mock_db = AsyncMock()
  178. mock_db.execute = AsyncMock(return_value=mock_result)
  179. mock_session_ctx.return_value.__aenter__ = AsyncMock(return_value=mock_db)
  180. mock_session_ctx.return_value.__aexit__ = AsyncMock(return_value=False)
  181. await scheduler.check_queue()
  182. queue_logs = [r for r in caplog.records if "Queue check" in r.message]
  183. assert len(queue_logs) == 0