Files
maziggy 033eff1254 Show Bambu's own HMS descriptions and the real alert level (issue #2728)
Document HMS levels, Bambu's descriptions and uncounted faults (issue #2728)
2026-09-28 13:57:02 +02:00

140 lines
6.3 KiB
Python

"""The HMS catalogue generated from Bambu Studio's HMS files (#2728)."""
import json
import re
import pytest
from backend.app.services import hms_errors
from backend.app.services.hms_errors import (
alert_level_from_print_error,
describe_fault,
get_error_description,
lookup_fault,
)
CATALOG = json.loads(hms_errors._DATA_FILE.read_text(encoding="utf-8"))
EXTRA = json.loads((hms_errors._DATA_FILE.parent / "hms_catalog_extra.json").read_text(encoding="utf-8"))
class TestCatalogFile:
"""The file is generated by scripts/generate_hms_catalog.py and must stay in
the shape that script writes; these fail on a hand edit that breaks it."""
def test_records_where_it_came_from(self):
"""So "how old is our table" has an answer, and a regeneration shows up
in the diff as a new commit and new file versions."""
source = CATALOG["_source"]
assert source["generator"] == "scripts/generate_hms_catalog.py"
assert re.fullmatch(r"[0-9a-f]{7,40}|unknown", source["bambu_studio_commit"])
assert source["model_file_versions"]
assert all(isinstance(v, int) for v in source["model_file_versions"].values())
@pytest.mark.parametrize(("section", "length"), [("hms", 16), ("error", 8)])
def test_keys_are_uppercase_hex_of_the_right_length(self, section, length):
pattern = re.compile(rf"^[0-9A-F]{{{length}}}$")
assert CATALOG[section]
for code in CATALOG[section]:
assert pattern.match(code), code
for model, table in CATALOG[f"{section}_by_model"].items():
assert re.fullmatch(r"[0-9A-Z]{3}", model)
for code in table:
assert pattern.match(code), (model, code)
@pytest.mark.parametrize("section", ["hms", "error"])
def test_a_model_override_differs_from_the_merged_text(self, section):
"""Overrides hold only what differs; an identical one is dead weight and
means the file was not written by the script."""
for model, table in CATALOG[f"{section}_by_model"].items():
for code, text in table.items():
assert CATALOG[section].get(code) != text, (model, code)
def test_every_extra_code_is_in_the_catalogue(self):
"""The hand-kept extras are merged in where Studio has no entry, so a
regeneration cannot drop the texts Bambuddy already had."""
for code in EXTRA["error"]:
assert code in CATALOG["error"], code
def test_texts_are_strings(self):
for section in ("hms", "error"):
assert all(isinstance(t, str) for t in CATALOG[section].values())
class TestLookup:
def test_resolves_a_print_error_code(self):
assert describe_fault("03008004") == "Filament ran out. Please load new filament."
def test_resolves_an_hms_code_by_its_full_code(self):
"""A real P2S fault from #2728; the reporter traced it to a module and
firmware mismatch, which is what Bambu's text says."""
assert describe_fault("050003000002000E").startswith(
"Some modules are incompatible with the printer's firmware version"
)
def test_is_case_and_whitespace_tolerant(self):
assert describe_fault(" 0300400c ") == describe_fault("0300400C") is not None
def test_a_code_listed_without_text_is_empty_not_missing(self):
"""Two of the reporter's three P2S faults. Bambu lists them and publishes
no text; lookup_fault keeps that distinct from "not listed", and
describe_fault reads both as no description."""
assert lookup_fault("0500060000020070") == ""
assert lookup_fault("050002000003000A") == ""
assert describe_fault("0500060000020070") is None
def test_an_unlisted_code_is_none(self):
assert lookup_fault("99999999") is None
assert lookup_fault("FFFFFFFFFFFFFFFF") is None
def test_the_text_follows_the_printer_model(self):
"""0300_8001 is "paused by the user" on some models and "paused by a
pause command in the file" on others, and 0300_8013 the other way round.
A merged table alone would tell one set of owners the wrong thing."""
on_20p = describe_fault("03008001", "20P")
on_093 = describe_fault("03008001", "093")
assert "paused by the user" in on_20p
assert "pause command" in on_093
assert describe_fault("03008013", "20P") == on_093
def test_an_unknown_or_missing_model_uses_the_merged_text(self):
merged = CATALOG["error"]["03008001"]
assert describe_fault("03008001", "XXX") == merged
assert describe_fault("03008001") == merged
assert describe_fault("03008001", "") == merged
def test_a_sixteen_char_code_never_falls_back_to_a_print_error_key(self):
"""The old G1_G4 collapse looked an hms[] code up as a print_error. No
real hms[] code has an error group at or above 0x4000, where every
print_error key sits, so the collapse could only attach a neighbour's
sentence. Built here from a real print_error key to prove it's gone."""
key = "03004000"
assert describe_fault(key) is not None
forced = f"{key[:4]}02000001{key[4:]}"
assert forced not in CATALOG["hms"]
assert describe_fault(forced) is None
def test_no_real_hms_code_reaches_the_print_error_range(self):
"""The fact the previous test rests on, checked against the data."""
assert all(int(code[12:], 16) < 0x4000 for code in CATALOG["hms"])
def test_empty_missing_or_malformed_is_none(self):
assert describe_fault("") is None
assert describe_fault(None) is None
assert describe_fault("0300") is None
assert describe_fault("030080040") is None
def test_short_code_helper_matches_the_full_code(self):
assert get_error_description("0300_400c") == describe_fault("0300400C")
assert get_error_description("0300_8001", "20P") == describe_fault("03008001", "20P")
class TestPrintErrorAlertLevel:
@pytest.mark.parametrize(
("error", "level"),
[(0x4000, 1), (0x40FF, 1), (0x8004, 2), (0x8FFF, 2), (0xC003, 3), (0xCFFF, 3), (0x0002, 0), (0x5000, 0)],
)
def test_maps_the_first_hex_digit(self, error, level):
"""4xxx stops the task, 8xxx pauses it, Cxxx is a prompt -- the hms[]
levels 1, 2, 3. Anything else has no defined level."""
assert alert_level_from_print_error(error) == level