mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
The reduction that gave an AMS slot a material type read PLA-AERO, PLA-GF, ASA-GF and PPS-GF as their base material, so a slot loaded with foaming or glass-filled filament went out saying plain PLA or ASA. That is worse than the bug it replaced. "PLA-AERO" matched nothing before, which was useless but honest; "PLA" matches every PLA plate in the queue, so the dispatcher would have sent one to filament that will not print it -- and the contract the first fix claimed, that it could only ever repair a slot, no longer held. @doncaruana caught PLA Aero on the issue. All four are values Bambuddy itself offers: filament_fields.json is the material list the Profiles editor puts in a dropdown, and the reduction table was assembled from the cloud filament names and the frontend preset parser without ever being checked against it. It is checked now, so the next type added to one and not the other fails a test rather than a print. ASA-AERO joins them from the cloud catalogue (GFB02). The table hyphenates because the slicers do, while a spool says "PLA Aero" and every Bambu preset name says "Bambu PLA Aero". Adjacent words are joined and taken when the join is a type exactly -- exactly, because letting the prefix and suffix rules reach across a space would make "Support for PLA" a type by its tail. Also from @doncaruana, and the better half of his point: a preset is chosen from a list the slicer defines, so it already knows its own type and nothing has to be read out of a product name. The resolver now hands that answer back and both assign routes prefer it. It cannot be the only source -- material is required on a spool and slicer_filament is not, and the spool this issue was reported for had no preset at all -- so the reduction stays as the fallback for spools without one. Two things had to move with it. The auto-unlink guard compared the slot's reported type against the reduced material, so a spool whose preset outranked its material column would have been unlinked from the slot it had just been assigned to; it now accepts any type the assign path could have written. And two lookups keyed by material took the catch-all for a type they had no row for, which sent an ASA-GF spool out at 200/240 -- too cold to extrude -- and preheated its chamber to nothing. Both fall back to the base material last, so PLA-CF, PETG-CF and PA-CF keep the rows they are listed with, and ASA-CF and ABS-GF pick up ranges they had been missing all along. What counts as a material name is decided by the base for the same reason: saying yes throws the value away and rescues the slot from the generic-material fallback, so the answer has to be no when that fallback has nothing to offer. ABS-GF reduces to a generic ABS the printer can resolve; PPS-CF reduces to nothing and is left as it stands. Adding a type to the table therefore cannot quietly change that answer, which is how these five slipped through in the first place. ------ Hand the bundled chamber-preheat table back the way it is read Every lookup of the per-filament chamber map happens after the keys are upper-cased, and the parser documents exactly that: keys uppercased, DEFAULT always present so the resolution loop can index it unconditionally. The three fallback paths returned the bundled constant as declared, with the lowercase "default" row the Settings editor writes and displays, so an install that had never opened the setting got a dict the loop could not read its fallback out of and used a hardcoded 0 for any filament without a row of its own. It reported the right number only because that bundled default is 0. Raising it would have changed nothing for everyone who had not customised the map, with the map in Settings still showing the value that was not being used. The test that should have caught this asserted the fallback under either spelling, and its docstring contradicted itself between title and comment. It pins the contract now, over all four ways the parser can fall back.
263 lines
9.9 KiB
Python
263 lines
9.9 KiB
Python
"""Tests for ``resolve_slicer_filament`` (#1815).
|
|
|
|
The defensive filter at the end of the resolver clears ``tray_info_idx``
|
|
when its value isn't slicer-acceptable (literal material names + PFUS /
|
|
PFCN cloud-preset prefixes that the printer's calibration table can't
|
|
key on). Pre-#1815 it cleared ``setting_id`` alongside, which dropped
|
|
the slicer's only handle on the user's actual custom preset and forced
|
|
the caller into the generic-material fallback — Bambu Studio then
|
|
displayed "Generic <Material>" for spools whose Bambu Cloud detail
|
|
lookup didn't resolve a ``filament_id`` (cloud unauth on the on_ams_change
|
|
replay path, transient cloud failure, or custom presets whose detail
|
|
JSON omits ``filament_id``).
|
|
|
|
Post-#1815 the filter preserves a setting_id that's still a valid
|
|
slicer reference (PFUS / PFCN cloud user/shared preset, or GFS Bambu
|
|
official preset) even when ``tray_info_idx`` is cleared.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from backend.app.services.slicer_filament_resolver import resolve_slicer_filament
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_pfus_cloud_unavailable_preserves_setting_id():
|
|
"""Reporter scenario: PFUS cloud user preset, cloud lookup fails to
|
|
return a filament_id. setting_id must survive so the slicer can
|
|
still find the user's actual custom preset."""
|
|
db = MagicMock()
|
|
with patch(
|
|
"backend.app.api.routes.cloud.build_authenticated_cloud",
|
|
AsyncMock(return_value=None),
|
|
):
|
|
tray_info_idx, setting_id, sub_brand, _type_override = await resolve_slicer_filament(
|
|
db=db,
|
|
current_user=None,
|
|
slicer_filament="PFUS990b6e19965353",
|
|
slicer_filament_name="Jayo PETG HF",
|
|
material="PETG",
|
|
)
|
|
assert tray_info_idx == ""
|
|
assert setting_id == "PFUS990b6e19965353"
|
|
assert sub_brand is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_pfcn_cloud_unavailable_preserves_setting_id():
|
|
"""PFCN partner/shared cloud preset (e.g. Polymaker H2D variants,
|
|
#1648) shares the same shape problem as PFUS."""
|
|
db = MagicMock()
|
|
with patch(
|
|
"backend.app.api.routes.cloud.build_authenticated_cloud",
|
|
AsyncMock(return_value=None),
|
|
):
|
|
tray_info_idx, setting_id, sub_brand, _type_override = await resolve_slicer_filament(
|
|
db=db,
|
|
current_user=None,
|
|
slicer_filament="PFCN1234567890",
|
|
slicer_filament_name="Polymaker PolyTerra PLA",
|
|
material="PLA",
|
|
)
|
|
assert tray_info_idx == ""
|
|
assert setting_id == "PFCN1234567890"
|
|
assert sub_brand is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_pfus_cloud_resolves_filament_id_regression_guard():
|
|
"""When cloud auth works and returns a filament_id, the resolver
|
|
keeps its existing behaviour: tray_info_idx = real filament_id,
|
|
setting_id = original PFUS reference."""
|
|
db = MagicMock()
|
|
cloud_mock = MagicMock()
|
|
cloud_mock.is_authenticated = True
|
|
cloud_mock.get_setting_detail = AsyncMock(return_value={"filament_id": "P285e239", "name": "Jayo PETG HF @P1S"})
|
|
cloud_mock.close = AsyncMock()
|
|
with patch(
|
|
"backend.app.api.routes.cloud.build_authenticated_cloud",
|
|
AsyncMock(return_value=cloud_mock),
|
|
):
|
|
tray_info_idx, setting_id, sub_brand, _type_override = await resolve_slicer_filament(
|
|
db=db,
|
|
current_user=MagicMock(),
|
|
slicer_filament="PFUS990b6e19965353",
|
|
slicer_filament_name="Jayo PETG HF",
|
|
material="PETG",
|
|
)
|
|
assert tray_info_idx == "P285e239"
|
|
assert setting_id == "PFUS990b6e19965353"
|
|
assert sub_brand == "Jayo PETG HF"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_gfs_cloud_unavailable_resolves_via_normalize():
|
|
"""GFS Bambu official preset + cloud unavailable: normalize strips
|
|
the 'S' to give a real filament_id ('GFG02'), so tray_info_idx is
|
|
valid and the defensive filter doesn't trigger. setting_id stays as
|
|
the original GFS reference. Regression guard for the cloud-down
|
|
Bambu-official path."""
|
|
db = MagicMock()
|
|
with patch(
|
|
"backend.app.api.routes.cloud.build_authenticated_cloud",
|
|
AsyncMock(return_value=None),
|
|
):
|
|
tray_info_idx, setting_id, sub_brand, _type_override = await resolve_slicer_filament(
|
|
db=db,
|
|
current_user=None,
|
|
slicer_filament="GFSG02",
|
|
slicer_filament_name=None,
|
|
material="PETG",
|
|
)
|
|
assert tray_info_idx == "GFG02"
|
|
assert setting_id == "GFSG02"
|
|
assert sub_brand is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_literal_material_name_clears_both():
|
|
"""slicer_filament='PETG' (free-text material leak from legacy
|
|
spools): both tray_info_idx and setting_id must be cleared so the
|
|
caller's generic-material fallback rescues the slot. Regression
|
|
guard that the PFUS preservation doesn't accidentally preserve
|
|
literal material names."""
|
|
db = MagicMock()
|
|
with patch(
|
|
"backend.app.api.routes.cloud.build_authenticated_cloud",
|
|
AsyncMock(return_value=None),
|
|
):
|
|
tray_info_idx, setting_id, sub_brand, _type_override = await resolve_slicer_filament(
|
|
db=db,
|
|
current_user=None,
|
|
slicer_filament="PETG",
|
|
slicer_filament_name=None,
|
|
material="PETG",
|
|
)
|
|
assert tray_info_idx == ""
|
|
assert setting_id == ""
|
|
assert sub_brand is None
|
|
|
|
|
|
class TestThePresetsOwnType:
|
|
"""#2902: a preset is chosen from a list the slicer defines, so its
|
|
``filament_type`` is the slicer's own answer to what the material is --
|
|
no reading of a product name required. Raised by @doncaruana on the issue
|
|
after the first fix reduced "PLA Aero" to "PLA".
|
|
|
|
The resolver hands that answer back as the fourth element; the two assign
|
|
routes write it into ``tray_type`` in preference to reducing the spool's
|
|
material column. ``None`` means no preset said, and the reduction stands.
|
|
"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_a_local_presets_type_is_returned(self):
|
|
db = MagicMock()
|
|
lp = MagicMock()
|
|
lp.filament_type = "PLA-AERO"
|
|
lp.setting = None
|
|
lp.name = "Bambu PLA Aero @BBL X1C"
|
|
result = MagicMock()
|
|
result.scalar_one_or_none = MagicMock(return_value=lp)
|
|
db.execute = AsyncMock(return_value=result)
|
|
|
|
_idx, _sid, _brand, type_override = await resolve_slicer_filament(
|
|
db=db,
|
|
current_user=None,
|
|
slicer_filament="38",
|
|
slicer_filament_name=None,
|
|
material="PLA",
|
|
)
|
|
assert type_override == "PLA-AERO"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_a_cloud_presets_type_is_read_out_of_its_profile(self):
|
|
"""Both slicers store it as a one-element array, and the preset JSON
|
|
sits under ``setting`` in the cloud envelope."""
|
|
db = MagicMock()
|
|
cloud = MagicMock()
|
|
cloud.is_authenticated = True
|
|
cloud.get_setting_detail = AsyncMock(
|
|
return_value={
|
|
"filament_id": "GFA11",
|
|
"name": "Bambu PLA Aero @BBL X1C",
|
|
"setting": {"filament_type": ["PLA-AERO"]},
|
|
}
|
|
)
|
|
cloud.close = AsyncMock()
|
|
with patch(
|
|
"backend.app.api.routes.cloud.build_authenticated_cloud",
|
|
AsyncMock(return_value=cloud),
|
|
):
|
|
idx, _sid, _brand, type_override = await resolve_slicer_filament(
|
|
db=db,
|
|
current_user=None,
|
|
slicer_filament="GFSA11",
|
|
slicer_filament_name=None,
|
|
material="PLA",
|
|
)
|
|
assert idx == "GFA11"
|
|
assert type_override == "PLA-AERO"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_a_bare_string_filament_type_is_accepted_too(self):
|
|
"""Hand-written and older profiles store it unwrapped. ``orca_profiles``
|
|
accepts both forms, so this has to as well."""
|
|
db = MagicMock()
|
|
cloud = MagicMock()
|
|
cloud.is_authenticated = True
|
|
cloud.get_setting_detail = AsyncMock(
|
|
return_value={"filament_id": "GFG02", "setting": {"filament_type": "PETG"}}
|
|
)
|
|
cloud.close = AsyncMock()
|
|
with patch(
|
|
"backend.app.api.routes.cloud.build_authenticated_cloud",
|
|
AsyncMock(return_value=cloud),
|
|
):
|
|
_idx, _sid, _brand, type_override = await resolve_slicer_filament(
|
|
db=db,
|
|
current_user=None,
|
|
slicer_filament="GFSG02",
|
|
slicer_filament_name=None,
|
|
material="PETG",
|
|
)
|
|
assert type_override == "PETG"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_no_preset_means_no_answer(self):
|
|
"""A spool with no slicer_filament -- the case this issue was reported
|
|
for. ``material`` is required on a spool and ``slicer_filament`` is
|
|
not, so the reduction has to stay as the fallback."""
|
|
db = MagicMock()
|
|
_idx, _sid, _brand, type_override = await resolve_slicer_filament(
|
|
db=db,
|
|
current_user=None,
|
|
slicer_filament=None,
|
|
slicer_filament_name=None,
|
|
material="PLA+",
|
|
)
|
|
assert type_override is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_a_preset_that_does_not_say_gets_no_opinion(self):
|
|
db = MagicMock()
|
|
cloud = MagicMock()
|
|
cloud.is_authenticated = True
|
|
cloud.get_setting_detail = AsyncMock(return_value={"filament_id": "GFG02", "setting": {}})
|
|
cloud.close = AsyncMock()
|
|
with patch(
|
|
"backend.app.api.routes.cloud.build_authenticated_cloud",
|
|
AsyncMock(return_value=cloud),
|
|
):
|
|
_idx, _sid, _brand, type_override = await resolve_slicer_filament(
|
|
db=db,
|
|
current_user=None,
|
|
slicer_filament="GFSG02",
|
|
slicer_filament_name=None,
|
|
material="PETG",
|
|
)
|
|
assert type_override is None
|