test_filament_ids_3387.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. """Filament ids that the "drop the S after GF" rule gets wrong (#3387).
  2. Bambu's public preset list carries each preset's setting_id next to the
  3. filament_id Bambu Studio sends to the printer. SUNLU's do not follow the rule
  4. ("GFSNLS03_04" is "GFSNL03"), and a few P1/X1 presets carry another
  5. filament's id altogether, decided by the full setting_id.
  6. """
  7. import re
  8. from pathlib import Path
  9. from unittest.mock import AsyncMock, MagicMock, patch
  10. import pytest
  11. from backend.app.api.routes.cloud import _BUILTIN_FILAMENT_NAMES, get_slicer_settings
  12. from backend.app.utils.filament_id_tables import ORCA_FILAMENT_IDS, SETTING_ID_FILAMENT_IDS
  13. from backend.app.utils.filament_ids import (
  14. derived_filament_id,
  15. filament_id_to_setting_id,
  16. normalize_slicer_filament,
  17. orca_to_bambu_filament_id,
  18. setting_id_to_filament_id,
  19. )
  20. REPO_ROOT = Path(__file__).resolve().parents[3]
  21. class TestSettingIdToFilamentId:
  22. @pytest.mark.parametrize(
  23. ("setting_id", "filament_id"),
  24. [
  25. ("GFSNLS03_04", "GFSNL03"), # SUNLU PLA+ @BBL A1, the reported one
  26. ("GFSNLS08", "GFSNL08"), # SUNLU PETG @BBL X1C, no suffix
  27. ("GFSA00_04", "GFA00"), # Bambu PLA Basic @BBL A1, the plain rule
  28. ("GFSA05_10", "GFA01"), # Bambu PLA Matte @BBL P1P
  29. ("GFSA05_02", "GFA05"), # Bambu PLA Silk @BBL X1: same base, own id
  30. ("GFSR99_10", "GFU99"), # Generic TPU @BBL P1P
  31. ("GFSR99_00", "GFR99"), # Generic EVA @BBL X1C
  32. ("GFSS02_10", "GFS00"), # Bambu Support W @BBL P1P
  33. ("GFSL23", "GFL00"), # PolyLite PLA @BBL X1; "GFL23" is no filament
  34. ],
  35. )
  36. def test_bambus_own_id(self, setting_id, filament_id):
  37. assert setting_id_to_filament_id(setting_id) == filament_id
  38. @pytest.mark.parametrize("filament_id", ["GFSNL03", "GFS99", "GFL05", "PFUS990b6e19965353", "P8a85d5a", ""])
  39. def test_a_filament_id_or_user_preset_is_left_alone(self, filament_id):
  40. assert setting_id_to_filament_id(filament_id) == filament_id
  41. def test_a_bare_id_bambuddy_builds_keeps_the_rule(self):
  42. """Bambuddy turns a filament id into a bare setting_id itself
  43. ("GFA05" -> "GFSA05"). Bambu also has a legacy preset of that bare id
  44. (Bambu PLA Matte @BBL X1, "GFA01"); following it would turn every such
  45. Bambu PLA Silk spool into Matte."""
  46. assert setting_id_to_filament_id(filament_id_to_setting_id("GFA05")) == "GFA05"
  47. assert setting_id_to_filament_id("GFSR99") == "GFR99"
  48. class TestFilamentIdToSettingId:
  49. @pytest.mark.parametrize(
  50. ("filament_id", "setting_id"),
  51. [("GFSNL03", "GFSNLS03"), ("GFSNLS03", "GFSNLS03"), ("GFA00", "GFSA00"), ("GFS99", "GFSS99")],
  52. )
  53. def test_converts(self, filament_id, setting_id):
  54. assert filament_id_to_setting_id(filament_id) == setting_id
  55. def test_normalize_reads_the_filament_id_from_the_full_id(self):
  56. assert normalize_slicer_filament("GFSA05_10") == ("GFA01", "GFSA05")
  57. assert normalize_slicer_filament("GFSNLS03_04") == ("GFSNL03", "GFSNLS03")
  58. assert normalize_slicer_filament("GFSL05_07") == ("GFL05", "GFSL05")
  59. def test_normalize_still_strips_the_suffix_of_anything_else(self):
  60. """Free text that starts with "P" must keep reducing to its material,
  61. which the resolver's guard then refuses, rather than going to the
  62. printer as a 9-character id."""
  63. assert normalize_slicer_filament("PLA_Basic") == ("PLA", "PLA")
  64. assert normalize_slicer_filament("PFUSabc_01") == ("PFUSabc", "PFUSabc")
  65. class TestOrcaIds:
  66. @pytest.mark.parametrize(
  67. ("value", "expected"),
  68. [("OFMUWNkp", "GFSNL03"), ("OFzzzzzz", ""), ("GFL99", "GFL99"), ("Pfc74047", "Pfc74047"), ("", "")],
  69. )
  70. def test_translates_orca_ids_only(self, value, expected):
  71. assert orca_to_bambu_filament_id(value) == expected
  72. def test_every_filament_orca_knows_has_a_builtin_name(self):
  73. """The Orca lookup finds a system parent's id by name in the builtin
  74. table; SUNLU missing from it sent those profiles out as Generic PLA."""
  75. assert set(ORCA_FILAMENT_IDS.values()) <= set(_BUILTIN_FILAMENT_NAMES)
  76. class TestTheGeneratedTables:
  77. def test_only_exceptions_are_listed(self):
  78. for setting_id, filament_id in SETTING_ID_FILAMENT_IDS.items():
  79. assert derived_filament_id(setting_id.split("_")[0]) != filament_id, setting_id
  80. def test_no_bare_entry_shadows_a_real_filament(self):
  81. """See test_a_bare_id_bambuddy_builds_keeps_the_rule."""
  82. for setting_id in SETTING_ID_FILAMENT_IDS:
  83. if "_" not in setting_id:
  84. assert derived_filament_id(setting_id) not in _BUILTIN_FILAMENT_NAMES, setting_id
  85. def test_the_frontend_copy_matches(self):
  86. ts = (REPO_ROOT / "frontend" / "src" / "utils" / "filamentIdTables.ts").read_text()
  87. assert dict(re.findall(r"^\s+(\w+): '(\w+)',$", ts, re.M)) == SETTING_ID_FILAMENT_IDS
  88. class TestTheCloudListCarriesTheId:
  89. @pytest.mark.asyncio
  90. async def test_filament_id_is_passed_through(self):
  91. cloud = MagicMock()
  92. cloud.is_authenticated = True
  93. cloud.close = AsyncMock()
  94. cloud.get_slicer_settings = AsyncMock(
  95. return_value={
  96. "filament": {
  97. "public": [
  98. {"setting_id": "GFSNLS03_04", "name": "SUNLU PLA+ @BBL A1", "filament_id": "GFSNL03"},
  99. {"setting_id": "GFSA00_04", "name": "Bambu PLA Basic @BBL A1", "filament_id": 7},
  100. ],
  101. "private": [{"setting_id": "PFUSfb87cd50b76616", "name": "Mine"}],
  102. }
  103. }
  104. )
  105. with patch("backend.app.api.routes.cloud.build_authenticated_cloud", AsyncMock(return_value=cloud)):
  106. result = await get_slicer_settings(version="2.0.0.0", db=MagicMock(), current_user=None)
  107. assert {s.setting_id: s.filament_id for s in result.filament} == {
  108. "PFUSfb87cd50b76616": None,
  109. "GFSNLS03_04": "GFSNL03",
  110. "GFSA00_04": None, # not a string: dropped, not a failed list
  111. }
  112. class TestOrcaProfiles:
  113. ORCA_ID = "34d8f588-860b-5be1-bcbe-c0d46d96324b"
  114. async def _lookup(self, profile, others=()):
  115. from backend.app.services.slicer_filament_resolver import lookup_orca_filament_id
  116. svc = MagicMock()
  117. svc.get_profile = AsyncMock(return_value=profile)
  118. svc.list_profiles = AsyncMock(return_value=[profile, *others])
  119. svc.close = AsyncMock()
  120. with patch(
  121. "backend.app.api.routes.orca_cloud._build_authenticated_service",
  122. AsyncMock(return_value=svc),
  123. ):
  124. return await lookup_orca_filament_id(MagicMock(), None, self.ORCA_ID)
  125. @pytest.mark.asyncio
  126. async def test_inheriting_a_sunlu_system_preset(self):
  127. child = {"id": self.ORCA_ID, "name": "My SUNLU", "content": {"inherits": "SUNLU PLA+ @BBL A1"}}
  128. found = await self._lookup(child)
  129. assert (found.filament_id, found.source) == ("GFSNL03", "inherited")
  130. assert filament_id_to_setting_id(found.filament_id) == "GFSNLS03"
  131. @pytest.mark.asyncio
  132. async def test_an_orca_id_of_its_own_is_translated(self):
  133. profile = {"id": self.ORCA_ID, "name": "Mine", "content": {"filament_id": "OFMUWNkp"}}
  134. found = await self._lookup(profile)
  135. assert (found.filament_id, found.source) == ("GFSNL03", "own")
  136. @pytest.mark.asyncio
  137. async def test_an_unknown_orca_id_never_reaches_the_printer(self):
  138. profile = {"id": self.ORCA_ID, "name": "Mine", "content": {"filament_id": "OFzzzzzz"}}
  139. found = await self._lookup(profile)
  140. assert (found.filament_id, found.reason) == ("", "no_filament_id")
  141. @pytest.mark.asyncio
  142. async def test_an_unknown_orca_id_still_lets_a_parent_answer(self):
  143. child = {"id": self.ORCA_ID, "name": "C", "content": {"filament_id": "OFzzzzzz", "inherits": "P"}}
  144. parent = {"id": "p", "name": "P", "content": {"filament_id": "OF2GCW1l"}}
  145. found = await self._lookup(child, [parent])
  146. assert (found.filament_id, found.source) == ("GFSNL08", "inherited")
  147. class TestSpoolAssignmentSignedIn:
  148. """With Bambu Cloud signed in, the resolver reads the detail of the
  149. suffix-free base, which for "GFSA05" is Bambu PLA Matte @BBL X1. The
  150. official id comes from the spool's full setting_id instead (#3387)."""
  151. MATTE_X1 = {"name": "Bambu PLA Matte @BBL X1", "filament_id": "GFA01", "setting": {"filament_type": ["PLA"]}}
  152. async def _resolve(self, slicer_filament, detail, name=None):
  153. from backend.app.services.slicer_filament_resolver import resolve_slicer_filament
  154. cloud = MagicMock()
  155. cloud.is_authenticated = True
  156. cloud.get_setting_detail = AsyncMock(return_value=detail)
  157. cloud.close = AsyncMock()
  158. with patch("backend.app.api.routes.cloud.build_authenticated_cloud", AsyncMock(return_value=cloud)):
  159. return await resolve_slicer_filament(
  160. db=MagicMock(),
  161. current_user=None,
  162. slicer_filament=slicer_filament,
  163. slicer_filament_name=name,
  164. material="PLA",
  165. )
  166. @pytest.mark.asyncio
  167. @pytest.mark.parametrize("slicer_filament", ["GFSA05_07", "GFSA05"])
  168. async def test_a_silk_spool_stays_silk(self, slicer_filament):
  169. idx, sid, brand, ftype = await self._resolve(slicer_filament, self.MATTE_X1)
  170. assert (idx, sid) == ("GFA05", "GFSA05")
  171. # Matte's name and type are another filament's.
  172. assert (brand, ftype) == (None, None)
  173. @pytest.mark.asyncio
  174. async def test_the_p1p_matte_spool_gets_matte(self):
  175. idx, _sid, brand, ftype = await self._resolve("GFSA05_10", self.MATTE_X1)
  176. assert (idx, brand, ftype) == ("GFA01", "Bambu PLA Matte", "PLA")
  177. @pytest.mark.asyncio
  178. async def test_a_spool_picked_as_the_legacy_bare_preset_gets_bambus_id(self):
  179. """Picked in the spool form, the spool carries the preset's own name;
  180. the bare id is then Bambu's preset, not one Bambuddy built."""
  181. detail = {"name": "Bambu TPU 95A @BBL X1C", "filament_id": "GFU01", "setting": {"filament_type": ["TPU"]}}
  182. idx, _sid, brand, ftype = await self._resolve("GFSU00", detail, name="Bambu TPU 95A @BBL X1C")
  183. assert (idx, brand, ftype) == ("GFU01", "Bambu TPU 95A", "TPU")
  184. @pytest.mark.asyncio
  185. async def test_a_silk_spool_named_silk_stays_silk(self):
  186. idx, *_ = await self._resolve("GFSA05", self.MATTE_X1, name="Bambu PLA Silk")
  187. assert idx == "GFA05"