| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195 |
- """A project saved as ``.gcode.3mf`` is still a project (#3435).
- Studio saves an unsliced project under whatever name it is given. One called
- ``Foo.gcode.3mf`` landed in the library as a printable file, and printing it to
- a cost centre was refused with "Estimated cost is required for cost center
- prints" -- eleven times, eleven duplicate uploads, nothing saying why. The file
- holds no G-code, so there is nothing to estimate and nothing to print.
- """
- import zipfile
- import pytest
- from sqlalchemy import text
- from backend.app.api.routes.library import classify_file_type
- from backend.app.models.library import LibraryFile
- from backend.app.utils.threemf_tools import is_unsliced_3mf
- SLICED = ["3D/3dmodel.model", "Metadata/plate_1.gcode", "Metadata/slice_info.config"]
- PROJECT = ["3D/3dmodel.model", "Metadata/slice_info.config", "Metadata/project_settings.config"]
- def _zip(path, names):
- with zipfile.ZipFile(path, "w") as zf:
- for name in names:
- zf.writestr(name, b"x")
- return path
- class TestClassification:
- def test_the_reported_file_is_a_project(self, tmp_path):
- path = _zip(tmp_path / "ImageToStl.com_Unbenannt.gcode.3mf", PROJECT)
- assert classify_file_type(path.name, path) == "3mf"
- def test_a_sliced_file_stays_sliced(self, tmp_path):
- path = _zip(tmp_path / "Benchy.gcode.3mf", SLICED)
- assert classify_file_type(path.name, path) == "gcode.3mf"
- def test_a_file_that_cannot_be_read_goes_by_its_name(self, tmp_path):
- broken = tmp_path / "x.gcode.3mf"
- broken.write_bytes(b"PK\x03\x04truncated")
- assert is_unsliced_3mf(broken) is False
- assert is_unsliced_3mf(tmp_path / "absent.gcode.3mf") is False
- assert classify_file_type("x.gcode.3mf", broken) == "gcode.3mf"
- async def _library_file(
- db_session, tmp_path, monkeypatch, *, names, filename="Unbenannt.gcode.3mf", file_type="gcode.3mf"
- ):
- from backend.app.core.config import settings as app_settings
- monkeypatch.setattr(app_settings, "base_dir", tmp_path)
- _zip(tmp_path / filename, names)
- lib_file = LibraryFile(
- filename=filename,
- file_path=filename,
- file_type=file_type,
- file_size=1,
- file_hash=filename,
- source_type="uploaded",
- file_metadata={"sliced_for_model": "H2D"},
- )
- db_session.add(lib_file)
- await db_session.commit()
- return lib_file
- @pytest.mark.asyncio
- @pytest.mark.integration
- class TestTheQueueSaysWhy:
- async def test_post_queue_names_the_cause(self, async_client, db_session, printer_factory, tmp_path, monkeypatch):
- printer = await printer_factory(model="H2D")
- lib_file = await _library_file(db_session, tmp_path, monkeypatch, names=PROJECT)
- response = await async_client.post(
- "/api/v1/queue/", json={"library_file_id": lib_file.id, "printer_id": printer.id}
- )
- assert response.status_code == 400
- assert (
- response.json()["detail"]
- == "Unbenannt.gcode.3mf isn't sliced yet. Slice it first, then print the sliced file."
- )
- async def test_ahead_of_the_cost_centre_message(
- self, async_client, db_session, printer_factory, tmp_path, monkeypatch
- ):
- """With billing on the estimate was the first thing missing."""
- from backend.app.models.settings import Settings
- db_session.add(Settings(key="billing_enabled", value="true"))
- await db_session.commit()
- printer = await printer_factory(model="H2D")
- lib_file = await _library_file(db_session, tmp_path, monkeypatch, names=PROJECT)
- response = await async_client.post(
- "/api/v1/queue/", json={"library_file_id": lib_file.id, "printer_id": printer.id, "cost_center_id": 1}
- )
- assert response.status_code == 400
- assert "isn't sliced yet" in response.json()["detail"]
- async def test_a_sliced_file_is_queued(self, async_client, db_session, printer_factory, tmp_path, monkeypatch):
- printer = await printer_factory(model="H2D")
- lib_file = await _library_file(db_session, tmp_path, monkeypatch, names=SLICED, filename="Benchy.gcode.3mf")
- response = await async_client.post(
- "/api/v1/queue/", json={"library_file_id": lib_file.id, "printer_id": printer.id}
- )
- assert response.status_code == 200, response.text
- async def test_bulk_add_judges_the_content(self, async_client, db_session, printer_factory, tmp_path, monkeypatch):
- printer = await printer_factory(model="H2D")
- project = await _library_file(db_session, tmp_path, monkeypatch, names=PROJECT)
- # A sliced plate exported under a plain .3mf name prints (#2993).
- exported = await _library_file(
- db_session, tmp_path, monkeypatch, names=SLICED, filename="Plate.3mf", file_type="gcode.3mf"
- )
- response = await async_client.post(
- "/api/v1/library/files/add-to-queue", json={"file_ids": [project.id, exported.id], "printer_id": printer.id}
- )
- assert response.status_code == 200, response.text
- body = response.json()
- assert [e["error"] for e in body["errors"]] == [
- "Unbenannt.gcode.3mf isn't sliced yet. Slice it first, then print the sliced file."
- ]
- assert [r["file_id"] for r in body["added"]] == [exported.id]
- @pytest.mark.asyncio
- class TestRowsAlreadyStored:
- @pytest.fixture(autouse=True)
- def _sqlite(self, monkeypatch):
- from backend.app.core import database as database_module, db_dialect
- monkeypatch.setattr(db_dialect, "is_sqlite", lambda: True)
- monkeypatch.setattr(db_dialect, "is_postgres", lambda: False)
- monkeypatch.setattr(database_module, "is_sqlite", lambda: True)
- @pytest.fixture
- async def engine(self):
- from sqlalchemy.ext.asyncio import create_async_engine
- from backend.app.core.database import Base
- from backend.tests.unit.test_library_file_type_backfill_migration import _register_all_models
- _register_all_models()
- eng = create_async_engine("sqlite+aiosqlite:///:memory:", echo=False)
- async with eng.begin() as conn:
- await conn.run_sync(Base.metadata.create_all)
- yield eng
- await eng.dispose()
- async def _insert(self, conn, file_id, filename, path, file_type="gcode.3mf", external=0):
- await conn.execute(
- text(
- "INSERT INTO library_files (id, filename, file_path, file_type, file_size, is_external, print_count) "
- "VALUES (:id, :f, :p, :t, 0, :e, 0)"
- ),
- {"id": file_id, "f": filename, "p": str(path), "t": file_type, "e": external},
- )
- async def _types(self, engine):
- async with engine.connect() as conn:
- return dict((await conn.execute(text("SELECT id, file_type FROM library_files ORDER BY id"))).fetchall())
- async def test_only_readable_projects_are_re_typed(self, engine, tmp_path):
- from backend.app.core.database import run_migrations
- async with engine.begin() as conn:
- await self._insert(conn, 1, "Unbenannt.gcode.3mf", _zip(tmp_path / "a.gcode.3mf", PROJECT))
- await self._insert(conn, 2, "Benchy.gcode.3mf", _zip(tmp_path / "b.gcode.3mf", SLICED))
- await self._insert(conn, 3, "Gone.gcode.3mf", tmp_path / "missing.gcode.3mf")
- await self._insert(conn, 4, "Ext.gcode.3mf", _zip(tmp_path / "c.gcode.3mf", PROJECT), external=1)
- async with engine.begin() as conn:
- await run_migrations(conn)
- assert await self._types(engine) == {1: "3mf", 2: "gcode.3mf", 3: "gcode.3mf", 4: "gcode.3mf"}
- async def test_a_later_boot_does_not_turn_them_back(self, engine, tmp_path):
- """The #1600 name backfill used to run on every boot and would flip a
- re-typed project straight back to printable."""
- from backend.app.core.database import run_migrations
- async with engine.begin() as conn:
- await self._insert(conn, 1, "Unbenannt.gcode.3mf", _zip(tmp_path / "a.gcode.3mf", PROJECT))
- async with engine.begin() as conn:
- await run_migrations(conn)
- async with engine.begin() as conn:
- await run_migrations(conn)
- assert await self._types(engine) == {1: "3mf"}
|