|
|
@@ -0,0 +1,215 @@
|
|
|
+"""Tests for the filament override feature in the print scheduler."""
|
|
|
+
|
|
|
+from unittest.mock import MagicMock, patch
|
|
|
+
|
|
|
+import pytest
|
|
|
+
|
|
|
+from backend.app.services.print_scheduler import PrintScheduler
|
|
|
+
|
|
|
+
|
|
|
+class TestCountOverrideColorMatches:
|
|
|
+ """Test the _count_override_color_matches method."""
|
|
|
+
|
|
|
+ @pytest.fixture
|
|
|
+ def scheduler(self):
|
|
|
+ return PrintScheduler()
|
|
|
+
|
|
|
+ @patch("backend.app.services.print_scheduler.printer_manager")
|
|
|
+ def test_no_status_returns_zero(self, mock_pm, scheduler):
|
|
|
+ """When printer_manager.get_status() returns None, should return 0."""
|
|
|
+ mock_pm.get_status.return_value = None
|
|
|
+
|
|
|
+ result = scheduler._count_override_color_matches(1, [{"type": "PLA", "color": "#FF0000"}])
|
|
|
+ assert result == 0
|
|
|
+
|
|
|
+ @patch("backend.app.services.print_scheduler.printer_manager")
|
|
|
+ def test_exact_match(self, mock_pm, scheduler):
|
|
|
+ """Override with matching type+color on printer returns 1."""
|
|
|
+ mock_pm.get_status.return_value = MagicMock(
|
|
|
+ raw_data={
|
|
|
+ "ams": [{"id": 0, "tray": [{"id": 0, "tray_type": "PLA", "tray_color": "FF0000FF"}]}],
|
|
|
+ }
|
|
|
+ )
|
|
|
+
|
|
|
+ result = scheduler._count_override_color_matches(1, [{"type": "PLA", "color": "#FF0000"}])
|
|
|
+ assert result == 1
|
|
|
+
|
|
|
+ @patch("backend.app.services.print_scheduler.printer_manager")
|
|
|
+ def test_no_match(self, mock_pm, scheduler):
|
|
|
+ """Override with type+color not on printer returns 0."""
|
|
|
+ mock_pm.get_status.return_value = MagicMock(
|
|
|
+ raw_data={
|
|
|
+ "ams": [{"id": 0, "tray": [{"id": 0, "tray_type": "PLA", "tray_color": "FF0000FF"}]}],
|
|
|
+ }
|
|
|
+ )
|
|
|
+
|
|
|
+ result = scheduler._count_override_color_matches(1, [{"type": "PETG", "color": "#00FF00"}])
|
|
|
+ assert result == 0
|
|
|
+
|
|
|
+ @patch("backend.app.services.print_scheduler.printer_manager")
|
|
|
+ def test_multiple_overrides_partial_match(self, mock_pm, scheduler):
|
|
|
+ """2 overrides, only 1 matching = returns 1."""
|
|
|
+ mock_pm.get_status.return_value = MagicMock(
|
|
|
+ raw_data={
|
|
|
+ "ams": [{"id": 0, "tray": [{"id": 0, "tray_type": "PLA", "tray_color": "FF0000FF"}]}],
|
|
|
+ }
|
|
|
+ )
|
|
|
+
|
|
|
+ overrides = [
|
|
|
+ {"type": "PLA", "color": "#FF0000"}, # Matches
|
|
|
+ {"type": "PETG", "color": "#00FF00"}, # Does not match
|
|
|
+ ]
|
|
|
+ result = scheduler._count_override_color_matches(1, overrides)
|
|
|
+ assert result == 1
|
|
|
+
|
|
|
+ @patch("backend.app.services.print_scheduler.printer_manager")
|
|
|
+ def test_color_normalization(self, mock_pm, scheduler):
|
|
|
+ """Override color '#FF0000' matches printer tray_color 'FF0000FF' (with alpha)."""
|
|
|
+ mock_pm.get_status.return_value = MagicMock(
|
|
|
+ raw_data={
|
|
|
+ "ams": [{"id": 0, "tray": [{"id": 0, "tray_type": "PLA", "tray_color": "FF0000FF"}]}],
|
|
|
+ }
|
|
|
+ )
|
|
|
+
|
|
|
+ # Override uses #-prefixed color; printer uses 8-char RGBA without hash
|
|
|
+ result = scheduler._count_override_color_matches(1, [{"type": "PLA", "color": "#FF0000"}])
|
|
|
+ assert result == 1
|
|
|
+
|
|
|
+ @patch("backend.app.services.print_scheduler.printer_manager")
|
|
|
+ def test_external_spool_match(self, mock_pm, scheduler):
|
|
|
+ """Override matches filament in vt_tray."""
|
|
|
+ mock_pm.get_status.return_value = MagicMock(
|
|
|
+ raw_data={
|
|
|
+ "ams": [],
|
|
|
+ "vt_tray": [{"tray_type": "TPU", "tray_color": "0000FFFF"}],
|
|
|
+ }
|
|
|
+ )
|
|
|
+
|
|
|
+ result = scheduler._count_override_color_matches(1, [{"type": "TPU", "color": "#0000FF"}])
|
|
|
+ assert result == 1
|
|
|
+
|
|
|
+
|
|
|
+class TestFilamentOverrideInMatching:
|
|
|
+ """Test that when overrides are applied to filament requirements, the matching uses overridden values."""
|
|
|
+
|
|
|
+ @pytest.fixture
|
|
|
+ def scheduler(self):
|
|
|
+ return PrintScheduler()
|
|
|
+
|
|
|
+ def _apply_overrides(self, filament_reqs, overrides):
|
|
|
+ """Simulate override application as done in _compute_ams_mapping_for_printer."""
|
|
|
+ override_map = {o["slot_id"]: o for o in overrides}
|
|
|
+ for req in filament_reqs:
|
|
|
+ if req["slot_id"] in override_map:
|
|
|
+ override = override_map[req["slot_id"]]
|
|
|
+ req["type"] = override["type"]
|
|
|
+ req["color"] = override["color"]
|
|
|
+ req["tray_info_idx"] = "" # Clear for override
|
|
|
+ return filament_reqs
|
|
|
+
|
|
|
+ def test_override_changes_color_match(self, scheduler):
|
|
|
+ """Original req has color A, loaded has color B. Override to color B gives exact match."""
|
|
|
+ filament_reqs = [{"slot_id": 1, "type": "PLA", "color": "#000000", "tray_info_idx": ""}]
|
|
|
+ loaded = [
|
|
|
+ {"type": "PLA", "color": "#FF0000", "global_tray_id": 0},
|
|
|
+ ]
|
|
|
+
|
|
|
+ # Without override: type-only match (colors differ)
|
|
|
+ result_without = scheduler._match_filaments_to_slots(filament_reqs, loaded)
|
|
|
+ assert result_without == [0] # Matches by type only
|
|
|
+
|
|
|
+ # Now apply override changing color to match loaded
|
|
|
+ overrides = [{"slot_id": 1, "type": "PLA", "color": "#FF0000"}]
|
|
|
+ filament_reqs_overridden = [{"slot_id": 1, "type": "PLA", "color": "#000000", "tray_info_idx": ""}]
|
|
|
+ self._apply_overrides(filament_reqs_overridden, overrides)
|
|
|
+
|
|
|
+ result_with = scheduler._match_filaments_to_slots(filament_reqs_overridden, loaded)
|
|
|
+ assert result_with == [0] # Exact color match now
|
|
|
+ # Verify the override actually changed the color in the requirement
|
|
|
+ assert filament_reqs_overridden[0]["color"] == "#FF0000"
|
|
|
+
|
|
|
+ def test_override_clears_tray_info_idx(self, scheduler):
|
|
|
+ """When tray_info_idx is cleared, matching falls to color-based instead of tray_info_idx-based."""
|
|
|
+ loaded = [
|
|
|
+ {"type": "PLA", "color": "#FF0000", "global_tray_id": 0, "tray_info_idx": "GFA00"},
|
|
|
+ {"type": "PLA", "color": "#00FF00", "global_tray_id": 1, "tray_info_idx": "GFB00"},
|
|
|
+ ]
|
|
|
+
|
|
|
+ # Without override: tray_info_idx "GFA00" matches tray 0 (red)
|
|
|
+ filament_reqs_original = [{"slot_id": 1, "type": "PLA", "color": "#FF0000", "tray_info_idx": "GFA00"}]
|
|
|
+ result_original = scheduler._match_filaments_to_slots(filament_reqs_original, loaded)
|
|
|
+ assert result_original == [0] # Matched by tray_info_idx
|
|
|
+
|
|
|
+ # With override: tray_info_idx is cleared, color changed to green -> matches tray 1
|
|
|
+ filament_reqs_overridden = [{"slot_id": 1, "type": "PLA", "color": "#FF0000", "tray_info_idx": "GFA00"}]
|
|
|
+ overrides = [{"slot_id": 1, "type": "PLA", "color": "#00FF00"}]
|
|
|
+ self._apply_overrides(filament_reqs_overridden, overrides)
|
|
|
+
|
|
|
+ assert filament_reqs_overridden[0]["tray_info_idx"] == "" # Cleared
|
|
|
+ result_overridden = scheduler._match_filaments_to_slots(filament_reqs_overridden, loaded)
|
|
|
+ assert result_overridden == [1] # Now matches tray 1 by color
|
|
|
+
|
|
|
+ def test_override_type_change(self, scheduler):
|
|
|
+ """Override changes type from PLA to PETG, loaded has PETG -> matches."""
|
|
|
+ loaded = [
|
|
|
+ {"type": "PETG", "color": "#FF0000", "global_tray_id": 0},
|
|
|
+ ]
|
|
|
+
|
|
|
+ # Without override: PLA requirement, PETG loaded -> no match
|
|
|
+ filament_reqs_original = [{"slot_id": 1, "type": "PLA", "color": "#FF0000", "tray_info_idx": ""}]
|
|
|
+ result_original = scheduler._match_filaments_to_slots(filament_reqs_original, loaded)
|
|
|
+ assert result_original == [-1] # Type mismatch
|
|
|
+
|
|
|
+ # With override: type changed to PETG -> matches
|
|
|
+ filament_reqs_overridden = [{"slot_id": 1, "type": "PLA", "color": "#FF0000", "tray_info_idx": ""}]
|
|
|
+ overrides = [{"slot_id": 1, "type": "PETG", "color": "#FF0000"}]
|
|
|
+ self._apply_overrides(filament_reqs_overridden, overrides)
|
|
|
+
|
|
|
+ result_overridden = scheduler._match_filaments_to_slots(filament_reqs_overridden, loaded)
|
|
|
+ assert result_overridden == [0] # Exact match now
|
|
|
+
|
|
|
+ def test_partial_override(self, scheduler):
|
|
|
+ """2 slots, only slot 1 overridden. Slot 1 uses override, slot 2 uses original."""
|
|
|
+ loaded = [
|
|
|
+ {"type": "PLA", "color": "#FF0000", "global_tray_id": 0},
|
|
|
+ {"type": "PETG", "color": "#00FF00", "global_tray_id": 1},
|
|
|
+ ]
|
|
|
+
|
|
|
+ filament_reqs = [
|
|
|
+ {"slot_id": 1, "type": "PLA", "color": "#000000", "tray_info_idx": "GFA00"},
|
|
|
+ {"slot_id": 2, "type": "PETG", "color": "#00FF00", "tray_info_idx": "GFG02"},
|
|
|
+ ]
|
|
|
+
|
|
|
+ # Override only slot 1: change color to red
|
|
|
+ overrides = [{"slot_id": 1, "type": "PLA", "color": "#FF0000"}]
|
|
|
+ self._apply_overrides(filament_reqs, overrides)
|
|
|
+
|
|
|
+ # Slot 1: overridden to PLA/#FF0000, tray_info_idx cleared -> matches tray 0 by exact color
|
|
|
+ assert filament_reqs[0]["color"] == "#FF0000"
|
|
|
+ assert filament_reqs[0]["tray_info_idx"] == ""
|
|
|
+
|
|
|
+ # Slot 2: NOT overridden, retains original tray_info_idx
|
|
|
+ assert filament_reqs[1]["color"] == "#00FF00"
|
|
|
+ assert filament_reqs[1]["tray_info_idx"] == "GFG02"
|
|
|
+
|
|
|
+ result = scheduler._match_filaments_to_slots(filament_reqs, loaded)
|
|
|
+ assert result == [0, 1] # Slot 1 -> tray 0 (red PLA), slot 2 -> tray 1 (green PETG)
|
|
|
+
|
|
|
+ def test_nozzle_filtering_with_override(self, scheduler):
|
|
|
+ """Override to a type only available on the wrong nozzle returns -1."""
|
|
|
+ loaded = [
|
|
|
+ # PETG on RIGHT nozzle (extruder 0) only
|
|
|
+ {"type": "PETG", "color": "#FF0000", "global_tray_id": 0, "extruder_id": 0},
|
|
|
+ # PLA on LEFT nozzle (extruder 1) only
|
|
|
+ {"type": "PLA", "color": "#00FF00", "global_tray_id": 4, "extruder_id": 1},
|
|
|
+ ]
|
|
|
+
|
|
|
+ # Override to PETG on LEFT nozzle — but PETG is only on RIGHT
|
|
|
+ filament_reqs = [{"slot_id": 1, "type": "PLA", "color": "#000000", "tray_info_idx": "GFA00", "nozzle_id": 1}]
|
|
|
+ overrides = [{"slot_id": 1, "type": "PETG", "color": "#FF0000"}]
|
|
|
+ self._apply_overrides(filament_reqs, overrides)
|
|
|
+
|
|
|
+ result = scheduler._match_filaments_to_slots(filament_reqs, loaded)
|
|
|
+ # Nozzle filter limits to extruder 1 (LEFT) which only has PLA.
|
|
|
+ # Override changed type to PETG, so no type match on LEFT nozzle -> -1
|
|
|
+ assert result == [-1]
|