"""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