test_bundled_compatible_printers_2982.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. """Standard-tier presets carry the slicer's own ``compatible_printers`` (#2982).
  2. The sidecar's ``/profiles/bundled`` listing used to report only a name and a
  3. ``base_id``, which left the SliceModal inferring a preset's printer from its
  4. NAME. That inference cannot work for several Bambu printers, because the bundle
  5. ships no preset named after them: all ten of a P1S's process presets are named
  6. ``@BBL X1C`` and name the P1S only in ``compatible_printers``. Reading the name
  7. classified every one of them as belonging to an X1 Carbon, so a P1S had zero
  8. compatible processes, the dropdown hid all 198, and the auto-pick fell through
  9. to an alphabetically-first ``0.06mm Fine @BBL A1 0.2 nozzle`` that the CLI then
  10. refused.
  11. These pin the pass-through, including the graceful degrade for a sidecar too
  12. old to report the field.
  13. """
  14. from unittest.mock import AsyncMock, MagicMock, patch
  15. import pytest
  16. from backend.app.api.routes import slicer_presets as sp
  17. def _sidecar(payload: dict) -> MagicMock:
  18. svc = MagicMock()
  19. svc.list_bundled_profiles = AsyncMock(return_value=payload)
  20. svc.__aenter__ = AsyncMock(return_value=svc)
  21. svc.__aexit__ = AsyncMock(return_value=False)
  22. return svc
  23. async def _fetch(payload: dict) -> dict:
  24. sp._bundled_cache = None
  25. svc = _sidecar(payload)
  26. with (
  27. patch.object(sp, "_resolve_slicer_api_url", AsyncMock(return_value="http://ok")),
  28. patch.object(sp, "SlicerApiService", return_value=svc),
  29. ):
  30. return await sp._fetch_bundled_presets(MagicMock())
  31. P1S = "Bambu Lab P1S 0.4 nozzle"
  32. # The real shape of the shipped bundle: a process preset named for one printer
  33. # that names several others, the P1S among them.
  34. X1C_PROCESS = {
  35. "name": "0.20mm Standard @BBL X1C",
  36. "base_id": "fdm_process_single_0.20",
  37. "compatible_printers": [
  38. "Bambu Lab X1 Carbon 0.4 nozzle",
  39. "Bambu Lab X1 0.4 nozzle",
  40. P1S,
  41. "Bambu Lab X1E 0.4 nozzle",
  42. ],
  43. }
  44. A1_FILAMENT = {
  45. "name": "Bambu ABS @BBL A1",
  46. "base_id": "Bambu ABS @base",
  47. "compatible_printers": ["Bambu Lab A1 0.4 nozzle", "Bambu Lab A1 0.6 nozzle"],
  48. "filament_type": "ABS",
  49. "filament_colour": None,
  50. }
  51. def _payload(**slots) -> dict:
  52. base: dict = {"printer": [], "process": [], "filament": []}
  53. base.update(slots)
  54. return base
  55. class TestTheProcessSlot:
  56. @pytest.mark.asyncio
  57. async def test_carries_the_declared_printer_list(self):
  58. slots = await _fetch(_payload(process=[X1C_PROCESS]))
  59. assert slots["process"][0].compatible_printers == X1C_PROCESS["compatible_printers"]
  60. @pytest.mark.asyncio
  61. async def test_keeps_a_printer_no_preset_is_named_after(self):
  62. slots = await _fetch(_payload(process=[X1C_PROCESS]))
  63. assert P1S in (slots["process"][0].compatible_printers or [])
  64. @pytest.mark.asyncio
  65. async def test_an_older_sidecar_leaves_the_field_unset(self):
  66. """No field is not an empty list: unset means "said nothing", which
  67. keeps the name matcher in play, while an empty list would read as
  68. "compatible with no printer at all" and hide the preset everywhere."""
  69. slots = await _fetch(
  70. _payload(process=[{"name": "0.20mm Standard @BBL X1C", "base_id": None}]),
  71. )
  72. assert slots["process"][0].compatible_printers is None
  73. @pytest.mark.asyncio
  74. async def test_normalises_a_bare_string(self):
  75. slots = await _fetch(
  76. _payload(process=[{"name": "Solo", "base_id": None, "compatible_printers": P1S}]),
  77. )
  78. assert slots["process"][0].compatible_printers == [P1S]
  79. @pytest.mark.asyncio
  80. async def test_an_empty_list_reads_as_no_data(self):
  81. slots = await _fetch(
  82. _payload(process=[{"name": "Solo", "base_id": None, "compatible_printers": []}]),
  83. )
  84. assert slots["process"][0].compatible_printers is None
  85. @pytest.mark.asyncio
  86. async def test_a_malformed_value_reads_as_no_data(self):
  87. slots = await _fetch(
  88. _payload(process=[{"name": "Solo", "base_id": None, "compatible_printers": 7}]),
  89. )
  90. assert slots["process"][0].compatible_printers is None
  91. @pytest.mark.asyncio
  92. async def test_drops_non_string_entries_but_keeps_the_rest(self):
  93. slots = await _fetch(
  94. _payload(
  95. process=[
  96. {"name": "Solo", "base_id": None, "compatible_printers": [P1S, None, 3, " "]},
  97. ],
  98. ),
  99. )
  100. assert slots["process"][0].compatible_printers == [P1S]
  101. class TestTheFilamentSlot:
  102. @pytest.mark.asyncio
  103. async def test_carries_both_the_printer_list_and_the_material(self):
  104. slots = await _fetch(_payload(filament=[A1_FILAMENT]))
  105. preset = slots["filament"][0]
  106. assert preset.compatible_printers == A1_FILAMENT["compatible_printers"]
  107. assert preset.filament_type == "ABS"
  108. @pytest.mark.asyncio
  109. async def test_a_colourless_bundled_profile_stays_colourless(self):
  110. """True of the whole BBL tree at every inheritance depth — colour is a
  111. spool attribute, not a profile one — so this must not be invented."""
  112. slots = await _fetch(_payload(filament=[A1_FILAMENT]))
  113. assert slots["filament"][0].filament_colour is None
  114. @pytest.mark.asyncio
  115. async def test_an_unresolvable_material_stays_none(self):
  116. """32 shipped filament profiles inherit from a parent the bundle does
  117. not contain, so the sidecar reports no material for them. They must
  118. still be listed — the picker treats "unknown" as eligible."""
  119. slots = await _fetch(
  120. _payload(
  121. filament=[
  122. {
  123. "name": "PolyLite PLA @BBL H2S",
  124. "base_id": "PolyLite PLA @base",
  125. "filament_type": None,
  126. "compatible_printers": ["Bambu Lab H2S 0.4 nozzle"],
  127. },
  128. ],
  129. ),
  130. )
  131. assert len(slots["filament"]) == 1
  132. assert slots["filament"][0].filament_type is None
  133. assert slots["filament"][0].compatible_printers == ["Bambu Lab H2S 0.4 nozzle"]
  134. class TestThePrinterSlot:
  135. @pytest.mark.asyncio
  136. async def test_printer_presets_carry_no_compatibility_of_their_own(self):
  137. """A printer is what compatibility is measured against; a list on one
  138. would be meaningless, and the SliceModal never filters that dropdown."""
  139. slots = await _fetch(
  140. _payload(
  141. printer=[
  142. {"name": P1S, "base_id": None, "compatible_printers": ["nonsense"]},
  143. ],
  144. ),
  145. )
  146. assert slots["printer"][0].compatible_printers is None
  147. assert slots["printer"][0].name == P1S