test_archive_plate_validation.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. """Tests for the plate-validation helpers used by the print_start callback.
  2. Covers the #1204 fix: when two plates of the same model are printed back to
  3. back, MQTT subtask_name can lag and the FTP candidate built from it lands on
  4. the previous plate's still-resident upload. The fix peeks the slice_info
  5. plate index, compares it to the plate parsed from gcode_file, and (on
  6. mismatch) re-fetches with a corrected name.
  7. Pinned here:
  8. - ``peek_plate_index_in_3mf`` reads ONLY ``Metadata/slice_info.config`` and
  9. returns the integer plate index, or None on any failure (missing entry,
  10. malformed XML, unreadable zip, etc.). Cheap by design — the full parse
  11. runs later inside ArchiveService.
  12. - ``swap_plate_suffix`` rewrites the trailing plate number in a Bambu-style
  13. job name. Covers both the spaced "Project - Plate N" form and the
  14. underscored "project_plate_N" variant seen in real subtask_names.
  15. """
  16. import zipfile
  17. import pytest
  18. from backend.app.services.archive import peek_plate_index_in_3mf, swap_plate_suffix
  19. def _make_3mf(tmp_path, *, plate_index: int | None = None, malformed: bool = False):
  20. """Build a minimal 3MF with a single ``<plate>`` whose ``index`` metadata is set."""
  21. path = tmp_path / "test.3mf"
  22. if malformed:
  23. path.write_bytes(b"not a zip")
  24. return path
  25. with zipfile.ZipFile(path, "w") as zf:
  26. if plate_index is None:
  27. # plate present but with no index metadata — exercise the "no index" branch
  28. zf.writestr("Metadata/slice_info.config", "<config><plate></plate></config>")
  29. else:
  30. zf.writestr(
  31. "Metadata/slice_info.config",
  32. f'<config><plate><metadata key="index" value="{plate_index}" /></plate></config>',
  33. )
  34. return path
  35. class TestPeekPlateIndexIn3mf:
  36. def test_returns_index_for_valid_3mf(self, tmp_path):
  37. path = _make_3mf(tmp_path, plate_index=2)
  38. assert peek_plate_index_in_3mf(path) == 2
  39. def test_returns_none_when_index_missing(self, tmp_path):
  40. path = _make_3mf(tmp_path, plate_index=None)
  41. assert peek_plate_index_in_3mf(path) is None
  42. def test_returns_none_when_slice_info_absent(self, tmp_path):
  43. path = tmp_path / "noslice.3mf"
  44. with zipfile.ZipFile(path, "w") as zf:
  45. zf.writestr("3D/3dmodel.model", "<model/>")
  46. assert peek_plate_index_in_3mf(path) is None
  47. def test_returns_none_on_non_zip_file(self, tmp_path):
  48. path = _make_3mf(tmp_path, malformed=True)
  49. assert peek_plate_index_in_3mf(path) is None
  50. def test_returns_none_on_missing_file(self, tmp_path):
  51. assert peek_plate_index_in_3mf(tmp_path / "does-not-exist.3mf") is None
  52. def test_returns_none_on_non_integer_index(self, tmp_path):
  53. path = tmp_path / "bad.3mf"
  54. with zipfile.ZipFile(path, "w") as zf:
  55. zf.writestr(
  56. "Metadata/slice_info.config",
  57. '<config><plate><metadata key="index" value="abc" /></plate></config>',
  58. )
  59. assert peek_plate_index_in_3mf(path) is None
  60. class TestSwapPlateSuffix:
  61. @pytest.mark.parametrize(
  62. ("name", "target", "expected"),
  63. [
  64. # Bambu Studio's default form (spaces around hyphen, capitalised "Plate").
  65. ("MyModel - Plate 2", 1, "MyModel - Plate 1"),
  66. ("MyModel - Plate 1", 5, "MyModel - Plate 5"),
  67. # Hyphen variants without surrounding spaces should still match (regex
  68. # uses \s* — slicer output occasionally normalises spacing).
  69. ("Tight-Plate 3", 7, "Tight-Plate 7"),
  70. # Underscored form seen in real subtask_names (see
  71. # test_print_start_expected_promotion fixture "Box3.0_(2)_plate_5").
  72. ("Box3.0_(2)_plate_5", 1, "Box3.0_(2)_plate_1"),
  73. # Case-insensitive match — older exports occasionally use lowercase.
  74. ("model - plate 4", 2, "model - plate 2"),
  75. ],
  76. )
  77. def test_swaps_plate_number(self, name, target, expected):
  78. assert swap_plate_suffix(name, target) == expected
  79. @pytest.mark.parametrize(
  80. "name",
  81. [
  82. "JustAModelName", # No plate suffix at all — single-plate project.
  83. "Model with - Plate in middle of name", # "Plate" not at the end.
  84. "Plate 2", # Bare "Plate N" with no base — refuse rather than guess.
  85. "", # Empty string.
  86. ],
  87. )
  88. def test_returns_none_when_no_recognised_suffix(self, name):
  89. assert swap_plate_suffix(name, 1) is None
  90. def test_returns_none_for_none_input(self):
  91. assert swap_plate_suffix(None, 1) is None
  92. def test_preserves_separator_casing(self):
  93. # The replacement must not normalise " - Plate " to "_plate_" or vice versa.
  94. # Otherwise the corrected name won't match what BambuStudio actually uploaded.
  95. assert swap_plate_suffix("Model - Plate 1", 2) == "Model - Plate 2"
  96. assert swap_plate_suffix("model_plate_1", 2) == "model_plate_2"