test_scheduler_filament_override.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. """Tests for the filament override feature in the print scheduler."""
  2. from unittest.mock import MagicMock, patch
  3. import pytest
  4. from backend.app.services.print_scheduler import PrintScheduler
  5. class TestCountOverrideColorMatches:
  6. """Test the _count_override_color_matches method."""
  7. @pytest.fixture
  8. def scheduler(self):
  9. return PrintScheduler()
  10. @patch("backend.app.services.print_scheduler.printer_manager")
  11. def test_no_status_returns_zero(self, mock_pm, scheduler):
  12. """When printer_manager.get_status() returns None, should return 0."""
  13. mock_pm.get_status.return_value = None
  14. result = scheduler._count_override_color_matches(1, [{"type": "PLA", "color": "#FF0000"}])
  15. assert result == 0
  16. @patch("backend.app.services.print_scheduler.printer_manager")
  17. def test_exact_match(self, mock_pm, scheduler):
  18. """Override with matching type+color on printer returns 1."""
  19. mock_pm.get_status.return_value = MagicMock(
  20. raw_data={
  21. "ams": [{"id": 0, "tray": [{"id": 0, "tray_type": "PLA", "tray_color": "FF0000FF"}]}],
  22. }
  23. )
  24. result = scheduler._count_override_color_matches(1, [{"type": "PLA", "color": "#FF0000"}])
  25. assert result == 1
  26. @patch("backend.app.services.print_scheduler.printer_manager")
  27. def test_no_match(self, mock_pm, scheduler):
  28. """Override with type+color not on printer returns 0."""
  29. mock_pm.get_status.return_value = MagicMock(
  30. raw_data={
  31. "ams": [{"id": 0, "tray": [{"id": 0, "tray_type": "PLA", "tray_color": "FF0000FF"}]}],
  32. }
  33. )
  34. result = scheduler._count_override_color_matches(1, [{"type": "PETG", "color": "#00FF00"}])
  35. assert result == 0
  36. @patch("backend.app.services.print_scheduler.printer_manager")
  37. def test_multiple_overrides_partial_match(self, mock_pm, scheduler):
  38. """2 overrides, only 1 matching = returns 1."""
  39. mock_pm.get_status.return_value = MagicMock(
  40. raw_data={
  41. "ams": [{"id": 0, "tray": [{"id": 0, "tray_type": "PLA", "tray_color": "FF0000FF"}]}],
  42. }
  43. )
  44. overrides = [
  45. {"type": "PLA", "color": "#FF0000"}, # Matches
  46. {"type": "PETG", "color": "#00FF00"}, # Does not match
  47. ]
  48. result = scheduler._count_override_color_matches(1, overrides)
  49. assert result == 1
  50. @patch("backend.app.services.print_scheduler.printer_manager")
  51. def test_color_normalization(self, mock_pm, scheduler):
  52. """Override color '#FF0000' matches printer tray_color 'FF0000FF' (with alpha)."""
  53. mock_pm.get_status.return_value = MagicMock(
  54. raw_data={
  55. "ams": [{"id": 0, "tray": [{"id": 0, "tray_type": "PLA", "tray_color": "FF0000FF"}]}],
  56. }
  57. )
  58. # Override uses #-prefixed color; printer uses 8-char RGBA without hash
  59. result = scheduler._count_override_color_matches(1, [{"type": "PLA", "color": "#FF0000"}])
  60. assert result == 1
  61. @patch("backend.app.services.print_scheduler.printer_manager")
  62. def test_external_spool_match(self, mock_pm, scheduler):
  63. """Override matches filament in vt_tray."""
  64. mock_pm.get_status.return_value = MagicMock(
  65. raw_data={
  66. "ams": [],
  67. "vt_tray": [{"tray_type": "TPU", "tray_color": "0000FFFF"}],
  68. }
  69. )
  70. result = scheduler._count_override_color_matches(1, [{"type": "TPU", "color": "#0000FF"}])
  71. assert result == 1
  72. class TestFilamentOverrideInMatching:
  73. """Test that when overrides are applied to filament requirements, the matching uses overridden values."""
  74. @pytest.fixture
  75. def scheduler(self):
  76. return PrintScheduler()
  77. def _apply_overrides(self, filament_reqs, overrides):
  78. """Simulate override application as done in _compute_ams_mapping_for_printer."""
  79. override_map = {o["slot_id"]: o for o in overrides}
  80. for req in filament_reqs:
  81. if req["slot_id"] in override_map:
  82. override = override_map[req["slot_id"]]
  83. req["type"] = override["type"]
  84. req["color"] = override["color"]
  85. req["tray_info_idx"] = "" # Clear for override
  86. return filament_reqs
  87. def test_override_changes_color_match(self, scheduler):
  88. """Original req has color A, loaded has color B. Override to color B gives exact match."""
  89. filament_reqs = [{"slot_id": 1, "type": "PLA", "color": "#000000", "tray_info_idx": ""}]
  90. loaded = [
  91. {"type": "PLA", "color": "#FF0000", "global_tray_id": 0},
  92. ]
  93. # Without override: type-only match (colors differ)
  94. result_without = scheduler._match_filaments_to_slots(filament_reqs, loaded)
  95. assert result_without == [0] # Matches by type only
  96. # Now apply override changing color to match loaded
  97. overrides = [{"slot_id": 1, "type": "PLA", "color": "#FF0000"}]
  98. filament_reqs_overridden = [{"slot_id": 1, "type": "PLA", "color": "#000000", "tray_info_idx": ""}]
  99. self._apply_overrides(filament_reqs_overridden, overrides)
  100. result_with = scheduler._match_filaments_to_slots(filament_reqs_overridden, loaded)
  101. assert result_with == [0] # Exact color match now
  102. # Verify the override actually changed the color in the requirement
  103. assert filament_reqs_overridden[0]["color"] == "#FF0000"
  104. def test_override_clears_tray_info_idx(self, scheduler):
  105. """When tray_info_idx is cleared, matching falls to color-based instead of tray_info_idx-based."""
  106. loaded = [
  107. {"type": "PLA", "color": "#FF0000", "global_tray_id": 0, "tray_info_idx": "GFA00"},
  108. {"type": "PLA", "color": "#00FF00", "global_tray_id": 1, "tray_info_idx": "GFB00"},
  109. ]
  110. # Without override: tray_info_idx "GFA00" matches tray 0 (red)
  111. filament_reqs_original = [{"slot_id": 1, "type": "PLA", "color": "#FF0000", "tray_info_idx": "GFA00"}]
  112. result_original = scheduler._match_filaments_to_slots(filament_reqs_original, loaded)
  113. assert result_original == [0] # Matched by tray_info_idx
  114. # With override: tray_info_idx is cleared, color changed to green -> matches tray 1
  115. filament_reqs_overridden = [{"slot_id": 1, "type": "PLA", "color": "#FF0000", "tray_info_idx": "GFA00"}]
  116. overrides = [{"slot_id": 1, "type": "PLA", "color": "#00FF00"}]
  117. self._apply_overrides(filament_reqs_overridden, overrides)
  118. assert filament_reqs_overridden[0]["tray_info_idx"] == "" # Cleared
  119. result_overridden = scheduler._match_filaments_to_slots(filament_reqs_overridden, loaded)
  120. assert result_overridden == [1] # Now matches tray 1 by color
  121. def test_override_type_change(self, scheduler):
  122. """Override changes type from PLA to PETG, loaded has PETG -> matches."""
  123. loaded = [
  124. {"type": "PETG", "color": "#FF0000", "global_tray_id": 0},
  125. ]
  126. # Without override: PLA requirement, PETG loaded -> no match
  127. filament_reqs_original = [{"slot_id": 1, "type": "PLA", "color": "#FF0000", "tray_info_idx": ""}]
  128. result_original = scheduler._match_filaments_to_slots(filament_reqs_original, loaded)
  129. assert result_original == [-1] # Type mismatch
  130. # With override: type changed to PETG -> matches
  131. filament_reqs_overridden = [{"slot_id": 1, "type": "PLA", "color": "#FF0000", "tray_info_idx": ""}]
  132. overrides = [{"slot_id": 1, "type": "PETG", "color": "#FF0000"}]
  133. self._apply_overrides(filament_reqs_overridden, overrides)
  134. result_overridden = scheduler._match_filaments_to_slots(filament_reqs_overridden, loaded)
  135. assert result_overridden == [0] # Exact match now
  136. def test_partial_override(self, scheduler):
  137. """2 slots, only slot 1 overridden. Slot 1 uses override, slot 2 uses original."""
  138. loaded = [
  139. {"type": "PLA", "color": "#FF0000", "global_tray_id": 0},
  140. {"type": "PETG", "color": "#00FF00", "global_tray_id": 1},
  141. ]
  142. filament_reqs = [
  143. {"slot_id": 1, "type": "PLA", "color": "#000000", "tray_info_idx": "GFA00"},
  144. {"slot_id": 2, "type": "PETG", "color": "#00FF00", "tray_info_idx": "GFG02"},
  145. ]
  146. # Override only slot 1: change color to red
  147. overrides = [{"slot_id": 1, "type": "PLA", "color": "#FF0000"}]
  148. self._apply_overrides(filament_reqs, overrides)
  149. # Slot 1: overridden to PLA/#FF0000, tray_info_idx cleared -> matches tray 0 by exact color
  150. assert filament_reqs[0]["color"] == "#FF0000"
  151. assert filament_reqs[0]["tray_info_idx"] == ""
  152. # Slot 2: NOT overridden, retains original tray_info_idx
  153. assert filament_reqs[1]["color"] == "#00FF00"
  154. assert filament_reqs[1]["tray_info_idx"] == "GFG02"
  155. result = scheduler._match_filaments_to_slots(filament_reqs, loaded)
  156. assert result == [0, 1] # Slot 1 -> tray 0 (red PLA), slot 2 -> tray 1 (green PETG)
  157. def test_nozzle_filtering_with_override(self, scheduler):
  158. """Override to a type only available on the wrong nozzle returns -1."""
  159. loaded = [
  160. # PETG on RIGHT nozzle (extruder 0) only
  161. {"type": "PETG", "color": "#FF0000", "global_tray_id": 0, "extruder_id": 0},
  162. # PLA on LEFT nozzle (extruder 1) only
  163. {"type": "PLA", "color": "#00FF00", "global_tray_id": 4, "extruder_id": 1},
  164. ]
  165. # Override to PETG on LEFT nozzle — but PETG is only on RIGHT
  166. filament_reqs = [{"slot_id": 1, "type": "PLA", "color": "#000000", "tray_info_idx": "GFA00", "nozzle_id": 1}]
  167. overrides = [{"slot_id": 1, "type": "PETG", "color": "#FF0000"}]
  168. self._apply_overrides(filament_reqs, overrides)
  169. result = scheduler._match_filaments_to_slots(filament_reqs, loaded)
  170. # Nozzle filter limits to extruder 1 (LEFT) which only has PLA.
  171. # Override changed type to PETG, so no type match on LEFT nozzle -> -1
  172. assert result == [-1]