test_sliced_3mf_named_as_source_2993.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. """A sliced 3MF is sliced whatever it is called (#2993).
  2. Downloading an archive that shows the green GCODE badge and re-importing it
  3. produced, for some archives, a source-only project with no Print button. The
  4. G-code was never lost -- the download serves the stored file byte for byte.
  5. What differed was who was asked: the Archives card looked inside the zip, while
  6. the library decided from the filename alone.
  7. That splits on how the print reached the printer, which is why it looked
  8. random. Bambu Studio's LAN send names a file ``Foo.gcode.3mf``; a per-plate
  9. export or a cloud-dispatched print arrives as ``Foo.3mf``, G-code and all. Both
  10. archive fine, both badge fine, and only the second one came back as a project.
  11. These tests pin the two halves to one answer, and pin the escape hatch that
  12. keeps ingest cheap: the file is opened only when the name has not already
  13. settled it.
  14. """
  15. from __future__ import annotations
  16. import zipfile
  17. from pathlib import Path
  18. import pytest
  19. from backend.app.api.routes.library import classify_file_type
  20. from backend.app.utils.threemf_tools import carries_gcode, names_carry_gcode
  21. SLICED = ["3D/3dmodel.model", "Metadata/plate_3.gcode", "Metadata/plate_3.gcode.md5"]
  22. SOURCE = ["3D/3dmodel.model", "Metadata/plate_1.png", "Metadata/project_settings.config"]
  23. def _write_3mf(path: Path, names: list[str]) -> Path:
  24. with zipfile.ZipFile(path, "w") as zf:
  25. for name in names:
  26. zf.writestr(name, b"x")
  27. return path
  28. class TestTheSharedAnswer:
  29. def test_plate_gcode_makes_it_sliced(self):
  30. assert names_carry_gcode(SLICED) is True
  31. def test_a_project_export_is_not(self):
  32. assert names_carry_gcode(SOURCE) is False
  33. def test_a_slicer_that_does_not_use_plate_naming_still_counts(self):
  34. """Deferring to default_plate_gcode_name rather than matching
  35. ``Metadata/plate_<n>.gcode`` is the point: a file that is executable on
  36. a printer must not be filed as a model because of where its G-code
  37. member sits."""
  38. assert names_carry_gcode(["3D/3dmodel.model", "output.gcode"]) is True
  39. def test_an_unreadable_file_reads_as_not_sliced(self, tmp_path):
  40. """The pre-existing behaviour for anything that cannot be opened. An
  41. ingest path must not fail on a truncated upload."""
  42. broken = tmp_path / "broken.3mf"
  43. broken.write_bytes(b"PK\x03\x04not-a-zip")
  44. assert carries_gcode(broken) is False
  45. assert carries_gcode(tmp_path / "absent.3mf") is False
  46. class TestClassification:
  47. def test_the_reported_file(self, tmp_path):
  48. """The whole bug in one line: same bytes, name says project."""
  49. sliced = _write_3mf(tmp_path / "Labyrinth.3mf", SLICED)
  50. assert classify_file_type("Labyrinth.3mf") == "3mf"
  51. assert classify_file_type("Labyrinth.3mf", sliced) == "gcode.3mf"
  52. def test_a_genuine_project_stays_a_project(self, tmp_path):
  53. """The guard that keeps this from swallowing the model library: an
  54. unsliced 3MF must not gain a Print button."""
  55. source = _write_3mf(tmp_path / "Labyrinth.3mf", SOURCE)
  56. assert classify_file_type("Labyrinth.3mf", source) == "3mf"
  57. def test_a_name_that_already_says_sliced_needs_no_file(self):
  58. """Not a micro-optimisation: the upload path classifies before the
  59. bytes are anywhere, and the external scan runs over a mount. Neither
  60. may depend on the file being openable when the name is enough."""
  61. assert classify_file_type("Labyrinth.gcode.3mf", Path("/does/not/exist.3mf")) == "gcode.3mf"
  62. @pytest.mark.parametrize("filename", ["model.stl", "preview.png", "README", "model.gcode"])
  63. def test_nothing_else_is_sniffed(self, filename, tmp_path):
  64. """Only `.3mf` is ambiguous. Handing a path for anything else must not
  65. change its type or open the file."""
  66. before = classify_file_type(filename)
  67. assert classify_file_type(filename, _write_3mf(tmp_path / "z.3mf", SLICED)) == before
  68. class TestTheTwoSidesAgree:
  69. def test_the_archives_badge_and_the_library_now_say_the_same_thing(self, tmp_path):
  70. """The card promised G-code and the File Manager denied it. Asserted
  71. against the archive endpoint's own expression, so this fails if that
  72. side is ever pointed back at a private copy of the rule."""
  73. from backend.app.api.routes.archives import names_carry_gcode as archives_predicate
  74. sliced = _write_3mf(tmp_path / "Labyrinth.3mf", SLICED)
  75. with zipfile.ZipFile(sliced) as zf:
  76. badge = archives_predicate(zf.namelist())
  77. assert badge is True
  78. assert classify_file_type("Labyrinth.3mf", sliced) == "gcode.3mf"