test_hms_errors.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. """The HMS catalogue generated from Bambu Studio's HMS files (#2728)."""
  2. import json
  3. import re
  4. import pytest
  5. from backend.app.services import hms_errors
  6. from backend.app.services.hms_errors import (
  7. alert_level_from_print_error,
  8. describe_fault,
  9. get_error_description,
  10. lookup_fault,
  11. )
  12. CATALOG = json.loads(hms_errors._DATA_FILE.read_text(encoding="utf-8"))
  13. EXTRA = json.loads((hms_errors._DATA_FILE.parent / "hms_catalog_extra.json").read_text(encoding="utf-8"))
  14. class TestCatalogFile:
  15. """The file is generated by scripts/generate_hms_catalog.py and must stay in
  16. the shape that script writes; these fail on a hand edit that breaks it."""
  17. def test_records_where_it_came_from(self):
  18. """So "how old is our table" has an answer, and a regeneration shows up
  19. in the diff as a new commit and new file versions."""
  20. source = CATALOG["_source"]
  21. assert source["generator"] == "scripts/generate_hms_catalog.py"
  22. assert re.fullmatch(r"[0-9a-f]{7,40}|unknown", source["bambu_studio_commit"])
  23. assert source["model_file_versions"]
  24. assert all(isinstance(v, int) for v in source["model_file_versions"].values())
  25. @pytest.mark.parametrize(("section", "length"), [("hms", 16), ("error", 8)])
  26. def test_keys_are_uppercase_hex_of_the_right_length(self, section, length):
  27. pattern = re.compile(rf"^[0-9A-F]{{{length}}}$")
  28. assert CATALOG[section]
  29. for code in CATALOG[section]:
  30. assert pattern.match(code), code
  31. for model, table in CATALOG[f"{section}_by_model"].items():
  32. assert re.fullmatch(r"[0-9A-Z]{3}", model)
  33. for code in table:
  34. assert pattern.match(code), (model, code)
  35. @pytest.mark.parametrize("section", ["hms", "error"])
  36. def test_a_model_override_differs_from_the_merged_text(self, section):
  37. """Overrides hold only what differs; an identical one is dead weight and
  38. means the file was not written by the script."""
  39. for model, table in CATALOG[f"{section}_by_model"].items():
  40. for code, text in table.items():
  41. assert CATALOG[section].get(code) != text, (model, code)
  42. def test_every_extra_code_is_in_the_catalogue(self):
  43. """The hand-kept extras are merged in where Studio has no entry, so a
  44. regeneration cannot drop the texts Bambuddy already had."""
  45. for code in EXTRA["error"]:
  46. assert code in CATALOG["error"], code
  47. def test_texts_are_strings(self):
  48. for section in ("hms", "error"):
  49. assert all(isinstance(t, str) for t in CATALOG[section].values())
  50. class TestLookup:
  51. def test_resolves_a_print_error_code(self):
  52. assert describe_fault("03008004") == "Filament ran out. Please load new filament."
  53. def test_resolves_an_hms_code_by_its_full_code(self):
  54. """A real P2S fault from #2728; the reporter traced it to a module and
  55. firmware mismatch, which is what Bambu's text says."""
  56. assert describe_fault("050003000002000E").startswith(
  57. "Some modules are incompatible with the printer's firmware version"
  58. )
  59. def test_is_case_and_whitespace_tolerant(self):
  60. assert describe_fault(" 0300400c ") == describe_fault("0300400C") is not None
  61. def test_a_code_listed_without_text_is_empty_not_missing(self):
  62. """Two of the reporter's three P2S faults. Bambu lists them and publishes
  63. no text; lookup_fault keeps that distinct from "not listed", and
  64. describe_fault reads both as no description."""
  65. assert lookup_fault("0500060000020070") == ""
  66. assert lookup_fault("050002000003000A") == ""
  67. assert describe_fault("0500060000020070") is None
  68. def test_an_unlisted_code_is_none(self):
  69. assert lookup_fault("99999999") is None
  70. assert lookup_fault("FFFFFFFFFFFFFFFF") is None
  71. def test_the_text_follows_the_printer_model(self):
  72. """0300_8001 is "paused by the user" on some models and "paused by a
  73. pause command in the file" on others, and 0300_8013 the other way round.
  74. A merged table alone would tell one set of owners the wrong thing."""
  75. on_20p = describe_fault("03008001", "20P")
  76. on_093 = describe_fault("03008001", "093")
  77. assert "paused by the user" in on_20p
  78. assert "pause command" in on_093
  79. assert describe_fault("03008013", "20P") == on_093
  80. def test_an_unknown_or_missing_model_uses_the_merged_text(self):
  81. merged = CATALOG["error"]["03008001"]
  82. assert describe_fault("03008001", "XXX") == merged
  83. assert describe_fault("03008001") == merged
  84. assert describe_fault("03008001", "") == merged
  85. def test_a_sixteen_char_code_never_falls_back_to_a_print_error_key(self):
  86. """The old G1_G4 collapse looked an hms[] code up as a print_error. No
  87. real hms[] code has an error group at or above 0x4000, where every
  88. print_error key sits, so the collapse could only attach a neighbour's
  89. sentence. Built here from a real print_error key to prove it's gone."""
  90. key = "03004000"
  91. assert describe_fault(key) is not None
  92. forced = f"{key[:4]}02000001{key[4:]}"
  93. assert forced not in CATALOG["hms"]
  94. assert describe_fault(forced) is None
  95. def test_no_real_hms_code_reaches_the_print_error_range(self):
  96. """The fact the previous test rests on, checked against the data."""
  97. assert all(int(code[12:], 16) < 0x4000 for code in CATALOG["hms"])
  98. def test_empty_missing_or_malformed_is_none(self):
  99. assert describe_fault("") is None
  100. assert describe_fault(None) is None
  101. assert describe_fault("0300") is None
  102. assert describe_fault("030080040") is None
  103. def test_short_code_helper_matches_the_full_code(self):
  104. assert get_error_description("0300_400c") == describe_fault("0300400C")
  105. assert get_error_description("0300_8001", "20P") == describe_fault("03008001", "20P")
  106. class TestPrintErrorAlertLevel:
  107. @pytest.mark.parametrize(
  108. ("error", "level"),
  109. [(0x4000, 1), (0x40FF, 1), (0x8004, 2), (0x8FFF, 2), (0xC003, 3), (0xCFFF, 3), (0x0002, 0), (0x5000, 0)],
  110. )
  111. def test_maps_the_first_hex_digit(self, error, level):
  112. """4xxx stops the task, 8xxx pauses it, Cxxx is a prompt -- the hms[]
  113. levels 1, 2, 3. Anything else has no defined level."""
  114. assert alert_level_from_print_error(error) == level