| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165 |
- """Tests for HMS error code translations."""
- from backend.app.services.hms_errors import HMS_ERROR_DESCRIPTIONS, describe_fault, get_error_description
- class TestHMSErrorDescriptions:
- """Tests for the HMS error descriptions dictionary."""
- def test_dictionary_is_not_empty(self):
- """Verify the error descriptions dictionary has entries."""
- assert len(HMS_ERROR_DESCRIPTIONS) > 0
- def test_dictionary_has_expected_count(self):
- """Verify we have the expected number of error codes."""
- # Should have 853 error codes from the frontend
- assert len(HMS_ERROR_DESCRIPTIONS) == 853
- def test_all_keys_are_valid_format(self):
- """Verify all keys follow the XXXX_YYYY format."""
- import re
- pattern = re.compile(r"^[0-9A-F]{4}_[0-9A-F]{4}$")
- for code in HMS_ERROR_DESCRIPTIONS:
- assert pattern.match(code), f"Invalid error code format: {code}"
- def test_all_values_are_non_empty_strings(self):
- """Verify all descriptions are non-empty strings."""
- for code, description in HMS_ERROR_DESCRIPTIONS.items():
- assert isinstance(description, str), f"Description for {code} is not a string"
- assert len(description) > 0, f"Description for {code} is empty"
- class TestGetErrorDescription:
- """Tests for the get_error_description function."""
- def test_returns_description_for_known_code(self):
- """Verify known error codes return their descriptions."""
- # 0300_400C = "The task was canceled."
- result = get_error_description("0300_400C")
- assert result == "The task was canceled."
- def test_returns_description_for_ams_error(self):
- """Verify AMS error codes return their descriptions."""
- # 0700_8010 = AMS assist motor overloaded
- result = get_error_description("0700_8010")
- assert "AMS assist motor" in result
- def test_returns_none_for_unknown_code(self):
- """Verify unknown error codes return None."""
- result = get_error_description("XXXX_YYYY")
- assert result is None
- def test_handles_lowercase_input(self):
- """Verify function handles lowercase input."""
- result = get_error_description("0300_400c")
- assert result == "The task was canceled."
- def test_handles_mixed_case_input(self):
- """Verify function handles mixed case input."""
- result = get_error_description("0300_400C")
- assert result == "The task was canceled."
- def test_common_error_codes_have_descriptions(self):
- """Verify common error codes have descriptions."""
- common_codes = [
- "0300_4000", # Z axis homing failed
- "0300_4006", # Nozzle clogged
- "0300_8004", # Filament ran out
- "0500_4001", # Failed to connect to Bambu Cloud
- "0700_8010", # AMS assist motor overloaded
- ]
- for code in common_codes:
- result = get_error_description(code)
- assert result is not None, f"Missing description for common code: {code}"
- class TestDescribeFault:
- """`describe_fault` maps a fault's canonical `full_code` onto the catalogue,
- so every surface that reports a fault resolves it the same way (#2926)."""
- def test_resolves_an_eight_char_print_error_code(self):
- """The parser derives full_code and the catalogue key from the same
- 32-bit value, so the split is exact rather than a guess."""
- assert describe_fault("03008004") == "Filament ran out. Please load new filament."
- def test_resolves_regardless_of_case(self):
- """Firmware-facing code is uppercase, but a client echoing a value back
- from its own store may not be."""
- assert describe_fault("0300400c") == "The task was canceled."
- def test_tolerates_surrounding_whitespace(self):
- assert describe_fault(" 03008004 ") == "Filament ran out. Please load new filament."
- def test_returns_none_for_an_hms_code_outside_the_catalogue(self):
- """A real P2S fault from #2728. Neither the whole 16-char key nor its
- G1_G4 collapse ("0500_000A") is in the catalogue — no catalogue key has
- an error group below 0x4000, and this family's is 0x000A."""
- assert describe_fault("050002000003000A") is None
- def test_collapses_a_sixteen_char_code_to_its_g1_g4_short_key(self):
- """Lossy, and kept deliberately: this is how the notification path, the
- queue's failure-reason helper and the frontend modal have always
- resolved `hms[]` faults, and it resolves real ones. Refusing would stop
- describing faults that are described today (see the module docstring)."""
- key = next(iter(HMS_ERROR_DESCRIPTIONS)) # e.g. "0300_4000"
- module, error = key.split("_")
- forced = f"{module}02000003{error}" # four 4-hex groups; G1 and G4 are the key
- assert len(forced) == 16
- assert describe_fault(forced) == HMS_ERROR_DESCRIPTIONS[key]
- def test_prefers_the_whole_sixteen_char_key_over_the_collapse(self):
- """The full identifier is lossless, so it wins when the catalogue has
- both. No 16-char keys ship today; this pins the order for when they do."""
- key = next(iter(HMS_ERROR_DESCRIPTIONS))
- module, error = key.split("_")
- forced = f"{module}02000003{error}"
- HMS_ERROR_DESCRIPTIONS[forced] = "specific variant"
- try:
- assert describe_fault(forced) == "specific variant"
- finally:
- del HMS_ERROR_DESCRIPTIONS[forced]
- def test_matches_the_derivation_it_replaced(
- self,
- ):
- """The regression guard for the consolidation: for every fault shape the
- codebase can produce, `describe_fault` returns exactly what the
- attr/code short-code lookup in the notification path returned before it.
- Covers both families and all three alert levels a real `hms[]` code
- carries — a divergence here means notifications silently stop firing for
- faults that used to raise them."""
- for key, expected in HMS_ERROR_DESCRIPTIONS.items():
- module, error = int(key[:4], 16), int(key[5:], 16)
- # print_error: attr is the whole 32-bit value, code its low half.
- print_error = (module << 16) | error
- assert describe_fault(f"{print_error:08X}") == expected
- # hms[]: attr is groups 1-2, code is groups 3-4 (alert level + id).
- for alert_level in (0x0000, 0x0002, 0x0003):
- attr = (module << 16) | 0x0200
- code = (alert_level << 16) | error
- legacy = get_error_description(f"{(attr >> 16) & 0xFFFF:04X}_{code & 0xFFFF:04X}")
- assert describe_fault(f"{attr:08X}{code:08X}") == legacy == expected
- def test_returns_none_for_an_unknown_eight_char_code(self):
- assert describe_fault("99999999") is None
- def test_returns_none_for_empty_or_missing(self):
- """The dataclass default is "" and the field is optional on the wire."""
- assert describe_fault("") is None
- assert describe_fault(None) is None
- def test_returns_none_for_a_malformed_length(self):
- """Neither 8 nor 16 chars — no shape to interpret, so no guess."""
- assert describe_fault("0300") is None
- assert describe_fault("030080040") is None
- def test_agrees_with_the_short_code_lookup_for_print_error_codes(self):
- """Pins the equivalence the consolidation rests on: for every 8-char
- code the catalogue covers, describe_fault returns exactly what the
- pre-existing short-code lookup did."""
- for key, expected in HMS_ERROR_DESCRIPTIONS.items():
- assert describe_fault(key.replace("_", "")) == expected
- assert get_error_description(key) == expected
|