test_plate_gcode_selection.py 4.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. """Which ``.gcode`` member of a 3MF a plate resolves to.
  2. A sliced multi-plate 3MF holds one toolpath per plate, and the order they sit
  3. in the zip is whatever the slicer wrote — not plate order. A real two-plate
  4. export measured for this fix stores ``Metadata/plate_2.gcode`` ahead of
  5. ``Metadata/plate_1.gcode``, so every caller that took the first member was
  6. opening plate 2 on a file whose first plate is plate 1.
  7. """
  8. from backend.app.utils.threemf_tools import (
  9. default_plate_gcode_name,
  10. default_plate_number,
  11. select_plate_gcode_name,
  12. )
  13. # The exact member order of the reporter's AMS_Rack.gcode.3mf.
  14. REVERSED_ORDER = ["Metadata/plate_2.gcode", "Metadata/plate_1.gcode"]
  15. class TestDefaultPlateGcodeName:
  16. def test_picks_the_lowest_plate_not_the_first_member(self):
  17. assert default_plate_gcode_name(REVERSED_ORDER) == "Metadata/plate_1.gcode"
  18. def test_ignores_non_gcode_members(self):
  19. names = ["Metadata/plate_1.png", "Metadata/plate_2.gcode", "3D/3dmodel.model", "Metadata/plate_1.gcode"]
  20. assert default_plate_gcode_name(names) == "Metadata/plate_1.gcode"
  21. def test_a_gcode_md5_sidecar_is_not_mistaken_for_the_toolpath(self):
  22. # Bambu writes plate_N.gcode.md5 next to each plate; it ends in .md5,
  23. # so it must not win the lowest-plate sort.
  24. names = ["Metadata/plate_1.gcode.md5", "Metadata/plate_2.gcode", "Metadata/plate_1.gcode"]
  25. assert default_plate_gcode_name(names) == "Metadata/plate_1.gcode"
  26. def test_falls_back_to_first_member_when_nothing_is_plate_numbered(self):
  27. # Slicers that don't use the convention have no numbering to sort by.
  28. assert default_plate_gcode_name(["out.gcode", "other.gcode"]) == "out.gcode"
  29. def test_double_digit_plates_sort_numerically_not_lexically(self):
  30. names = ["Metadata/plate_10.gcode", "Metadata/plate_2.gcode"]
  31. assert default_plate_gcode_name(names) == "Metadata/plate_2.gcode"
  32. def test_returns_none_for_an_unsliced_file(self):
  33. assert default_plate_gcode_name(["3D/3dmodel.model"]) is None
  34. class TestDefaultPlateNumber:
  35. """The integer a dispatch caller needs when no ``plate_id`` was asked for
  36. (#2947). Composes ``default_plate_gcode_name`` with the numeric index its
  37. chosen member encodes.
  38. """
  39. def test_picks_the_lowest_plate_not_the_first_member(self):
  40. assert default_plate_number(REVERSED_ORDER) == 1
  41. def test_single_plate_file_numbered_two_resolves_to_two(self):
  42. # The exact shape of a plate cut out of a multi-plate project: one
  43. # G-code member, keeping its ORIGINAL (non-1) plate number. Dispatch
  44. # hardcoding a plate-1 fallback here is #2947.
  45. assert default_plate_number(["Metadata/plate_2.gcode"]) == 2
  46. def test_a_gcode_md5_sidecar_is_not_mistaken_for_the_toolpath(self):
  47. names = ["Metadata/plate_1.gcode.md5", "Metadata/plate_2.gcode", "Metadata/plate_1.gcode"]
  48. assert default_plate_number(names) == 1
  49. def test_double_digit_plates_sort_numerically_not_lexically(self):
  50. names = ["Metadata/plate_10.gcode", "Metadata/plate_2.gcode"]
  51. assert default_plate_number(names) == 2
  52. def test_returns_none_for_an_unsliced_file(self):
  53. assert default_plate_number(["3D/3dmodel.model"]) is None
  54. def test_returns_none_when_the_default_member_has_no_plate_number(self):
  55. # default_plate_gcode_name falls back to the first .gcode member for
  56. # a slicer that doesn't use the plate_N convention; that member
  57. # carries no parseable number, and this must not invent one — the
  58. # dispatch caller is the one that falls back to plate 1.
  59. assert default_plate_number(["out.gcode", "other.gcode"]) is None
  60. class TestSelectPlateGcodeName:
  61. def test_selects_the_named_plate_regardless_of_zip_order(self):
  62. assert select_plate_gcode_name(REVERSED_ORDER, 1) == "Metadata/plate_1.gcode"
  63. assert select_plate_gcode_name(REVERSED_ORDER, 2) == "Metadata/plate_2.gcode"
  64. def test_zero_padded_names_match_the_index_the_plates_endpoint_reports(self):
  65. assert select_plate_gcode_name(["Metadata/plate_01.gcode"], 1) == "Metadata/plate_01.gcode"
  66. def test_returns_none_for_a_plate_the_file_does_not_hold(self):
  67. # Never a silent fallback: the caller asked for a specific plate, and
  68. # serving a different one is how the viewer showed the wrong toolpath.
  69. assert select_plate_gcode_name(REVERSED_ORDER, 3) is None
  70. def test_returns_none_without_a_plate_id(self):
  71. assert select_plate_gcode_name(REVERSED_ORDER, None) is None
  72. def test_does_not_match_a_prefix_of_a_longer_number(self):
  73. assert select_plate_gcode_name(["Metadata/plate_12.gcode"], 1) is None