|
|
@@ -0,0 +1,268 @@
|
|
|
+"""An Orca Cloud profile reaches the AMS slot with its own filament id (#3216).
|
|
|
+
|
|
|
+OrcaSlicer's "Sync filaments" resolves a slot to a preset by ``filament_id``
|
|
|
+alone. The Configure dialog used to look the id up in the browser and, when
|
|
|
+that came back empty, quietly sent the generic for the material -- so every
|
|
|
+Orca custom filament reached the slicer as "Generic <material>", and nothing
|
|
|
+in the log said why. The lookup now runs in ``configure`` and its outcome is
|
|
|
+reported back.
|
|
|
+
|
|
|
+The slot preset row also records the filament id it was written with, so the
|
|
|
+slot card can tell when OrcaSlicer's Device tab re-configured the slot.
|
|
|
+"""
|
|
|
+
|
|
|
+from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
+
|
|
|
+import pytest
|
|
|
+from httpx import AsyncClient
|
|
|
+
|
|
|
+from backend.app.services.slicer_filament_resolver import OrcaFilamentLookup
|
|
|
+
|
|
|
+ORCA_ID = "34d8f588-860b-5be1-bcbe-c0d46d96324b"
|
|
|
+
|
|
|
+
|
|
|
+def _client():
|
|
|
+ client = MagicMock()
|
|
|
+ client.ams_set_filament_setting.return_value = True
|
|
|
+ client.extrusion_cali_sel.return_value = True
|
|
|
+ client.request_status_update.return_value = True
|
|
|
+ return client
|
|
|
+
|
|
|
+
|
|
|
+def _status(tray_info_idx: str = "", tray_type: str = ""):
|
|
|
+ status = MagicMock()
|
|
|
+ status.raw_data = {
|
|
|
+ "ams": {
|
|
|
+ "ams": [
|
|
|
+ {"id": 0, "tray": [{"id": 2, "tray_info_idx": tray_info_idx, "tray_type": tray_type}]},
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return status
|
|
|
+
|
|
|
+
|
|
|
+async def _configure(async_client: AsyncClient, printer_id: int, **extra):
|
|
|
+ params = {
|
|
|
+ "tray_info_idx": "",
|
|
|
+ "tray_type": "PLA",
|
|
|
+ "tray_sub_brands": "XXXXX EXAMPLE PLA Example",
|
|
|
+ "tray_color": "FCECD6FF",
|
|
|
+ "nozzle_temp_min": 190,
|
|
|
+ "nozzle_temp_max": 230,
|
|
|
+ "orca_profile_id": ORCA_ID,
|
|
|
+ }
|
|
|
+ params.update(extra)
|
|
|
+ return await async_client.post(f"/api/v1/printers/{printer_id}/slots/0/2/configure", params=params)
|
|
|
+
|
|
|
+
|
|
|
+class TestConfigureWithOrcaProfile:
|
|
|
+ @pytest.mark.asyncio
|
|
|
+ @pytest.mark.integration
|
|
|
+ async def test_the_profiles_own_filament_id_goes_to_the_slot(self, async_client: AsyncClient, printer_factory):
|
|
|
+ """The id OrcaSlicer itself sends from its Device tab: Pfc74047 in the
|
|
|
+ reporter's log, where Bambuddy sent GFL99 for the same profile."""
|
|
|
+ printer = await printer_factory(name="H2D")
|
|
|
+ client = _client()
|
|
|
+ lookup = AsyncMock(return_value=OrcaFilamentLookup("Pfc74047", source="own"))
|
|
|
+
|
|
|
+ with (
|
|
|
+ patch("backend.app.api.routes.printers.printer_manager") as pm,
|
|
|
+ patch("backend.app.api.routes.printers.lookup_orca_filament_id", lookup),
|
|
|
+ ):
|
|
|
+ pm.get_client.return_value = client
|
|
|
+ pm.get_status.return_value = _status()
|
|
|
+ response = await _configure(async_client, printer.id)
|
|
|
+
|
|
|
+ assert response.status_code == 200
|
|
|
+ body = response.json()
|
|
|
+ assert body["tray_info_idx"] == "Pfc74047"
|
|
|
+ assert body["orca_fallback_reason"] == ""
|
|
|
+ assert lookup.await_args.args[2] == ORCA_ID
|
|
|
+ kwargs = client.ams_set_filament_setting.call_args.kwargs
|
|
|
+ assert kwargs["tray_info_idx"] == "Pfc74047"
|
|
|
+ # Never the profile UUID: it is foreign to the printer and to the slicer.
|
|
|
+ assert ORCA_ID not in kwargs["setting_id"]
|
|
|
+
|
|
|
+ @pytest.mark.asyncio
|
|
|
+ @pytest.mark.integration
|
|
|
+ async def test_no_id_falls_back_to_the_generic_and_says_so(self, async_client: AsyncClient, printer_factory):
|
|
|
+ """Not the slot's previous filament: reusing it would put the previous
|
|
|
+ spool's preset in front of the slicer for a profile the user just chose."""
|
|
|
+ printer = await printer_factory(name="H2D")
|
|
|
+ client = _client()
|
|
|
+ lookup = AsyncMock(return_value=OrcaFilamentLookup(reason="lookup_failed"))
|
|
|
+
|
|
|
+ with (
|
|
|
+ patch("backend.app.api.routes.printers.printer_manager") as pm,
|
|
|
+ patch("backend.app.api.routes.printers.lookup_orca_filament_id", lookup),
|
|
|
+ ):
|
|
|
+ pm.get_client.return_value = client
|
|
|
+ pm.get_status.return_value = _status("P4d64437", "PLA")
|
|
|
+ response = await _configure(async_client, printer.id)
|
|
|
+
|
|
|
+ assert response.status_code == 200
|
|
|
+ body = response.json()
|
|
|
+ assert body["tray_info_idx"] == "GFL99"
|
|
|
+ assert body["orca_fallback_reason"] == "lookup_failed"
|
|
|
+ assert client.ams_set_filament_setting.call_args.kwargs["tray_info_idx"] == "GFL99"
|
|
|
+
|
|
|
+ @pytest.mark.asyncio
|
|
|
+ @pytest.mark.integration
|
|
|
+ async def test_an_explicit_filament_id_skips_the_lookup(self, async_client: AsyncClient, printer_factory):
|
|
|
+ printer = await printer_factory(name="H2D")
|
|
|
+ client = _client()
|
|
|
+ lookup = AsyncMock()
|
|
|
+
|
|
|
+ with (
|
|
|
+ patch("backend.app.api.routes.printers.printer_manager") as pm,
|
|
|
+ patch("backend.app.api.routes.printers.lookup_orca_filament_id", lookup),
|
|
|
+ ):
|
|
|
+ pm.get_client.return_value = client
|
|
|
+ pm.get_status.return_value = _status()
|
|
|
+ response = await _configure(async_client, printer.id, tray_info_idx="Pad3f856")
|
|
|
+
|
|
|
+ assert response.status_code == 200
|
|
|
+ lookup.assert_not_awaited()
|
|
|
+ assert client.ams_set_filament_setting.call_args.kwargs["tray_info_idx"] == "Pad3f856"
|
|
|
+
|
|
|
+ @pytest.mark.asyncio
|
|
|
+ @pytest.mark.integration
|
|
|
+ async def test_other_presets_report_no_fallback(self, async_client: AsyncClient, printer_factory):
|
|
|
+ printer = await printer_factory(name="H2D")
|
|
|
+ client = _client()
|
|
|
+
|
|
|
+ with patch("backend.app.api.routes.printers.printer_manager") as pm:
|
|
|
+ pm.get_client.return_value = client
|
|
|
+ pm.get_status.return_value = _status()
|
|
|
+ response = await _configure(async_client, printer.id, tray_info_idx="GFA00", orca_profile_id="")
|
|
|
+
|
|
|
+ assert response.status_code == 200
|
|
|
+ assert response.json()["orca_fallback_reason"] == ""
|
|
|
+ assert response.json()["tray_info_idx"] == "GFA00"
|
|
|
+
|
|
|
+
|
|
|
+class TestOrcaFallbackKeepsTheDialogsGenerics:
|
|
|
+ """The fallback the Configure dialog used to pick in the browser, kept when
|
|
|
+ the lookup moved to the server: its table has Silk, PCTG, PP and PE."""
|
|
|
+
|
|
|
+ @pytest.mark.asyncio
|
|
|
+ @pytest.mark.integration
|
|
|
+ @pytest.mark.parametrize(
|
|
|
+ ("material", "expected"),
|
|
|
+ [
|
|
|
+ ("PLA SILK", "GFL96"),
|
|
|
+ ("PCTG", "GFG97"),
|
|
|
+ ("PP", "GFP97"),
|
|
|
+ ("PETG-CF", "GFG98"),
|
|
|
+ ("ASA-CF", "GFB98"),
|
|
|
+ ("PLA+", "GFL99"),
|
|
|
+ ],
|
|
|
+ )
|
|
|
+ async def test_material_generic(self, async_client: AsyncClient, printer_factory, material, expected):
|
|
|
+ printer = await printer_factory(name="H2D")
|
|
|
+ client = _client()
|
|
|
+ lookup = AsyncMock(return_value=OrcaFilamentLookup(reason="no_filament_id"))
|
|
|
+
|
|
|
+ with (
|
|
|
+ patch("backend.app.api.routes.printers.printer_manager") as pm,
|
|
|
+ patch("backend.app.api.routes.printers.lookup_orca_filament_id", lookup),
|
|
|
+ ):
|
|
|
+ pm.get_client.return_value = client
|
|
|
+ pm.get_status.return_value = _status()
|
|
|
+ response = await _configure(async_client, printer.id, tray_type=material)
|
|
|
+
|
|
|
+ assert response.status_code == 200
|
|
|
+ assert client.ams_set_filament_setting.call_args.kwargs["tray_info_idx"] == expected
|
|
|
+ assert response.json()["orca_fallback_reason"] == "no_filament_id"
|
|
|
+
|
|
|
+ @pytest.mark.asyncio
|
|
|
+ @pytest.mark.integration
|
|
|
+ async def test_an_unstorable_looked_up_id_is_refused(self, async_client: AsyncClient, printer_factory):
|
|
|
+ """Same refusal as the #3003 guard: an 18-character PFUS would be
|
|
|
+ stored truncated and resolve nowhere."""
|
|
|
+ printer = await printer_factory(name="H2D")
|
|
|
+ client = _client()
|
|
|
+ lookup = AsyncMock(return_value=OrcaFilamentLookup("PFUS9ac902733670a9", source="own"))
|
|
|
+
|
|
|
+ with (
|
|
|
+ patch("backend.app.api.routes.printers.printer_manager") as pm,
|
|
|
+ patch("backend.app.api.routes.printers.lookup_orca_filament_id", lookup),
|
|
|
+ ):
|
|
|
+ pm.get_client.return_value = client
|
|
|
+ pm.get_status.return_value = _status()
|
|
|
+ response = await _configure(async_client, printer.id)
|
|
|
+
|
|
|
+ assert client.ams_set_filament_setting.call_args.kwargs["tray_info_idx"] == "GFL99"
|
|
|
+ assert response.json()["orca_fallback_reason"] == "no_filament_id"
|
|
|
+
|
|
|
+ @pytest.mark.asyncio
|
|
|
+ @pytest.mark.integration
|
|
|
+ async def test_a_kprofile_realignment_clears_the_warning(self, async_client: AsyncClient, printer_factory):
|
|
|
+ """The slot ends up on the K-profile's own filament, not a generic."""
|
|
|
+ printer = await printer_factory(name="H2D")
|
|
|
+ client = _client()
|
|
|
+ lookup = AsyncMock(return_value=OrcaFilamentLookup(reason="no_filament_id"))
|
|
|
+
|
|
|
+ with (
|
|
|
+ patch("backend.app.api.routes.printers.printer_manager") as pm,
|
|
|
+ patch("backend.app.api.routes.printers.lookup_orca_filament_id", lookup),
|
|
|
+ ):
|
|
|
+ pm.get_client.return_value = client
|
|
|
+ pm.get_status.return_value = _status()
|
|
|
+ response = await _configure(async_client, printer.id, kprofile_filament_id="P4d64437")
|
|
|
+
|
|
|
+ assert response.status_code == 200
|
|
|
+ assert response.json()["tray_info_idx"] == "P4d64437"
|
|
|
+ assert response.json()["orca_fallback_reason"] == ""
|
|
|
+
|
|
|
+
|
|
|
+class TestSlotPresetRecordsItsFilamentId:
|
|
|
+ @pytest.mark.asyncio
|
|
|
+ @pytest.mark.integration
|
|
|
+ async def test_saved_and_returned(self, async_client: AsyncClient, printer_factory):
|
|
|
+ printer = await printer_factory(name="H2D")
|
|
|
+ url = f"/api/v1/printers/{printer.id}/slot-presets/0/2"
|
|
|
+ response = await async_client.put(
|
|
|
+ url,
|
|
|
+ params={
|
|
|
+ "preset_id": f"orca_{ORCA_ID}",
|
|
|
+ "preset_name": "XXXXX EXAMPLE PLA Example",
|
|
|
+ "preset_source": "orca_cloud",
|
|
|
+ "tray_info_idx": "Pfc74047",
|
|
|
+ },
|
|
|
+ )
|
|
|
+ assert response.status_code == 200
|
|
|
+ assert response.json()["tray_info_idx"] == "Pfc74047"
|
|
|
+
|
|
|
+ assert (await async_client.get(url)).json()["tray_info_idx"] == "Pfc74047"
|
|
|
+ listed = (await async_client.get(f"/api/v1/printers/{printer.id}/slot-presets")).json()
|
|
|
+ assert listed["2"]["tray_info_idx"] == "Pfc74047"
|
|
|
+
|
|
|
+ @pytest.mark.asyncio
|
|
|
+ @pytest.mark.integration
|
|
|
+ async def test_a_save_without_one_clears_the_old_one(self, async_client: AsyncClient, printer_factory):
|
|
|
+ """A stale id must not outlive the preset it came with: the slot card
|
|
|
+ would compare the new preset against the old preset's filament."""
|
|
|
+ printer = await printer_factory(name="H2D")
|
|
|
+ url = f"/api/v1/printers/{printer.id}/slot-presets/0/2"
|
|
|
+ await async_client.put(
|
|
|
+ url, params={"preset_id": "GFSL99", "preset_name": "Generic PLA", "tray_info_idx": "GFL99"}
|
|
|
+ )
|
|
|
+ response = await async_client.put(
|
|
|
+ url, params={"preset_id": "local_5", "preset_name": "Mine", "preset_source": "local"}
|
|
|
+ )
|
|
|
+
|
|
|
+ assert response.status_code == 200
|
|
|
+ assert response.json()["tray_info_idx"] is None
|
|
|
+ assert (await async_client.get(url)).json()["tray_info_idx"] is None
|
|
|
+
|
|
|
+ @pytest.mark.asyncio
|
|
|
+ @pytest.mark.integration
|
|
|
+ async def test_an_overlong_id_is_dropped_not_refused(self, async_client: AsyncClient, printer_factory):
|
|
|
+ """Refusing it would lose the preset save itself, which the dialog only
|
|
|
+ logs; recording nothing keeps the row shown as before."""
|
|
|
+ printer = await printer_factory(name="H2D")
|
|
|
+ url = f"/api/v1/printers/{printer.id}/slot-presets/0/2"
|
|
|
+ response = await async_client.put(url, params={"preset_id": "x", "preset_name": "X", "tray_info_idx": "P" * 40})
|
|
|
+ assert response.status_code == 200
|
|
|
+ assert response.json()["tray_info_idx"] is None
|