test_unsliced_named_gcode_3mf_3435.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. """A project saved as ``.gcode.3mf`` is still a project (#3435).
  2. Studio saves an unsliced project under whatever name it is given. One called
  3. ``Foo.gcode.3mf`` landed in the library as a printable file, and printing it to
  4. a cost centre was refused with "Estimated cost is required for cost center
  5. prints" -- eleven times, eleven duplicate uploads, nothing saying why. The file
  6. holds no G-code, so there is nothing to estimate and nothing to print.
  7. """
  8. import zipfile
  9. import pytest
  10. from sqlalchemy import text
  11. from backend.app.api.routes.library import classify_file_type
  12. from backend.app.models.library import LibraryFile
  13. from backend.app.utils.threemf_tools import is_unsliced_3mf
  14. SLICED = ["3D/3dmodel.model", "Metadata/plate_1.gcode", "Metadata/slice_info.config"]
  15. PROJECT = ["3D/3dmodel.model", "Metadata/slice_info.config", "Metadata/project_settings.config"]
  16. def _zip(path, names):
  17. with zipfile.ZipFile(path, "w") as zf:
  18. for name in names:
  19. zf.writestr(name, b"x")
  20. return path
  21. class TestClassification:
  22. def test_the_reported_file_is_a_project(self, tmp_path):
  23. path = _zip(tmp_path / "ImageToStl.com_Unbenannt.gcode.3mf", PROJECT)
  24. assert classify_file_type(path.name, path) == "3mf"
  25. def test_a_sliced_file_stays_sliced(self, tmp_path):
  26. path = _zip(tmp_path / "Benchy.gcode.3mf", SLICED)
  27. assert classify_file_type(path.name, path) == "gcode.3mf"
  28. def test_a_file_that_cannot_be_read_goes_by_its_name(self, tmp_path):
  29. broken = tmp_path / "x.gcode.3mf"
  30. broken.write_bytes(b"PK\x03\x04truncated")
  31. assert is_unsliced_3mf(broken) is False
  32. assert is_unsliced_3mf(tmp_path / "absent.gcode.3mf") is False
  33. assert classify_file_type("x.gcode.3mf", broken) == "gcode.3mf"
  34. async def _library_file(
  35. db_session, tmp_path, monkeypatch, *, names, filename="Unbenannt.gcode.3mf", file_type="gcode.3mf"
  36. ):
  37. from backend.app.core.config import settings as app_settings
  38. monkeypatch.setattr(app_settings, "base_dir", tmp_path)
  39. _zip(tmp_path / filename, names)
  40. lib_file = LibraryFile(
  41. filename=filename,
  42. file_path=filename,
  43. file_type=file_type,
  44. file_size=1,
  45. file_hash=filename,
  46. source_type="uploaded",
  47. file_metadata={"sliced_for_model": "H2D"},
  48. )
  49. db_session.add(lib_file)
  50. await db_session.commit()
  51. return lib_file
  52. @pytest.mark.asyncio
  53. @pytest.mark.integration
  54. class TestTheQueueSaysWhy:
  55. async def test_post_queue_names_the_cause(self, async_client, db_session, printer_factory, tmp_path, monkeypatch):
  56. printer = await printer_factory(model="H2D")
  57. lib_file = await _library_file(db_session, tmp_path, monkeypatch, names=PROJECT)
  58. response = await async_client.post(
  59. "/api/v1/queue/", json={"library_file_id": lib_file.id, "printer_id": printer.id}
  60. )
  61. assert response.status_code == 400
  62. assert (
  63. response.json()["detail"]
  64. == "Unbenannt.gcode.3mf isn't sliced yet. Slice it first, then print the sliced file."
  65. )
  66. async def test_ahead_of_the_cost_centre_message(
  67. self, async_client, db_session, printer_factory, tmp_path, monkeypatch
  68. ):
  69. """With billing on the estimate was the first thing missing."""
  70. from backend.app.models.settings import Settings
  71. db_session.add(Settings(key="billing_enabled", value="true"))
  72. await db_session.commit()
  73. printer = await printer_factory(model="H2D")
  74. lib_file = await _library_file(db_session, tmp_path, monkeypatch, names=PROJECT)
  75. response = await async_client.post(
  76. "/api/v1/queue/", json={"library_file_id": lib_file.id, "printer_id": printer.id, "cost_center_id": 1}
  77. )
  78. assert response.status_code == 400
  79. assert "isn't sliced yet" in response.json()["detail"]
  80. async def test_a_sliced_file_is_queued(self, async_client, db_session, printer_factory, tmp_path, monkeypatch):
  81. printer = await printer_factory(model="H2D")
  82. lib_file = await _library_file(db_session, tmp_path, monkeypatch, names=SLICED, filename="Benchy.gcode.3mf")
  83. response = await async_client.post(
  84. "/api/v1/queue/", json={"library_file_id": lib_file.id, "printer_id": printer.id}
  85. )
  86. assert response.status_code == 200, response.text
  87. async def test_bulk_add_judges_the_content(self, async_client, db_session, printer_factory, tmp_path, monkeypatch):
  88. printer = await printer_factory(model="H2D")
  89. project = await _library_file(db_session, tmp_path, monkeypatch, names=PROJECT)
  90. # A sliced plate exported under a plain .3mf name prints (#2993).
  91. exported = await _library_file(
  92. db_session, tmp_path, monkeypatch, names=SLICED, filename="Plate.3mf", file_type="gcode.3mf"
  93. )
  94. response = await async_client.post(
  95. "/api/v1/library/files/add-to-queue", json={"file_ids": [project.id, exported.id], "printer_id": printer.id}
  96. )
  97. assert response.status_code == 200, response.text
  98. body = response.json()
  99. assert [e["error"] for e in body["errors"]] == [
  100. "Unbenannt.gcode.3mf isn't sliced yet. Slice it first, then print the sliced file."
  101. ]
  102. assert [r["file_id"] for r in body["added"]] == [exported.id]
  103. @pytest.mark.asyncio
  104. class TestRowsAlreadyStored:
  105. @pytest.fixture(autouse=True)
  106. def _sqlite(self, monkeypatch):
  107. from backend.app.core import database as database_module, db_dialect
  108. monkeypatch.setattr(db_dialect, "is_sqlite", lambda: True)
  109. monkeypatch.setattr(db_dialect, "is_postgres", lambda: False)
  110. monkeypatch.setattr(database_module, "is_sqlite", lambda: True)
  111. @pytest.fixture
  112. async def engine(self):
  113. from sqlalchemy.ext.asyncio import create_async_engine
  114. from backend.app.core.database import Base
  115. from backend.tests.unit.test_library_file_type_backfill_migration import _register_all_models
  116. _register_all_models()
  117. eng = create_async_engine("sqlite+aiosqlite:///:memory:", echo=False)
  118. async with eng.begin() as conn:
  119. await conn.run_sync(Base.metadata.create_all)
  120. yield eng
  121. await eng.dispose()
  122. async def _insert(self, conn, file_id, filename, path, file_type="gcode.3mf", external=0):
  123. await conn.execute(
  124. text(
  125. "INSERT INTO library_files (id, filename, file_path, file_type, file_size, is_external, print_count) "
  126. "VALUES (:id, :f, :p, :t, 0, :e, 0)"
  127. ),
  128. {"id": file_id, "f": filename, "p": str(path), "t": file_type, "e": external},
  129. )
  130. async def _types(self, engine):
  131. async with engine.connect() as conn:
  132. return dict((await conn.execute(text("SELECT id, file_type FROM library_files ORDER BY id"))).fetchall())
  133. async def test_only_readable_projects_are_re_typed(self, engine, tmp_path):
  134. from backend.app.core.database import run_migrations
  135. async with engine.begin() as conn:
  136. await self._insert(conn, 1, "Unbenannt.gcode.3mf", _zip(tmp_path / "a.gcode.3mf", PROJECT))
  137. await self._insert(conn, 2, "Benchy.gcode.3mf", _zip(tmp_path / "b.gcode.3mf", SLICED))
  138. await self._insert(conn, 3, "Gone.gcode.3mf", tmp_path / "missing.gcode.3mf")
  139. await self._insert(conn, 4, "Ext.gcode.3mf", _zip(tmp_path / "c.gcode.3mf", PROJECT), external=1)
  140. async with engine.begin() as conn:
  141. await run_migrations(conn)
  142. assert await self._types(engine) == {1: "3mf", 2: "gcode.3mf", 3: "gcode.3mf", 4: "gcode.3mf"}
  143. async def test_a_later_boot_does_not_turn_them_back(self, engine, tmp_path):
  144. """The #1600 name backfill used to run on every boot and would flip a
  145. re-typed project straight back to printable."""
  146. from backend.app.core.database import run_migrations
  147. async with engine.begin() as conn:
  148. await self._insert(conn, 1, "Unbenannt.gcode.3mf", _zip(tmp_path / "a.gcode.3mf", PROJECT))
  149. async with engine.begin() as conn:
  150. await run_migrations(conn)
  151. async with engine.begin() as conn:
  152. await run_migrations(conn)
  153. assert await self._types(engine) == {1: "3mf"}