mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-08 15:11:21 +02:00
When scheduling a print to "any printer" (model-based assignment), users can now override the 3MF's original filament choices per slot. The override dropdown shows compatible filaments loaded across all printers of the selected model, filtered by type and nozzle (H2D). The scheduler matches against overridden type/color instead of the original 3MF values, preferring printers with exact color matches. Backend: - New GET /printers/available-filaments endpoint (aggregates loaded filaments across printers of a model, includes extruder_id for dual-nozzle nozzle-aware filtering) - New filament_overrides JSON column on print_queue table - Scheduler applies overrides before AMS mapping, clears tray_info_idx on overridden slots so color matching takes priority - Printer selection prefers printers with most override color matches Frontend: - New FilamentOverride component with per-slot override dropdowns - Type-filtered options (PLA slots only show PLA) - Nozzle-aware filtering (H2D: only shows filaments on correct extruder) - Integrated into PrintModal for model-based queue mode Tests: - 11 backend unit tests (color match counting, override application) - 12 frontend tests (rendering, type/nozzle filtering, interactions) i18n: All 6 locales (en, de, fr, it, ja, pt-BR)
216 lines
9.9 KiB
Python
216 lines
9.9 KiB
Python
"""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]
|