"""A same-named 3MF is not automatically this print's 3MF (#2957).
When a print's own 3MF could not be fetched, the usage tracker looks for one in
the library or in a previous archive and matches on the filename stem. Bambu
Studio writes the printer-side filename from the project's ``Title`` metadata,
so every plate of a project reaches the printer under one name however the user
renamed the file on disk -- filename equality says almost nothing.
The reporter's archive 94 was handed archive 92's file that way. The real print
used one filament; the donor plate declared three, and three spools were debited
for material they never extruded. Nothing in the archive said the numbers were
someone else's.
The plate is the only sound discriminator available here, and these tests pin
both halves of it: a donor holding a different plate is refused, and an
all-plates export is refused unless it actually carries the plate that is
running. The tempting second check -- comparing the donor's filament count
against the slicer's ``ams_mapping`` -- is deliberately absent and has a test of
its own saying why: that field is indexed by the *project's* filament slots, so
a genuine single-filament print reports ``[0, -1, -1, -1]``.
Where the plate cannot be known at all, which is the reporter's own firmware,
the donor is still accepted on its name and a warning says so. That is the
honest limit of what the data supports.
"""
from __future__ import annotations
import zipfile
from pathlib import Path
import pytest
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from backend.app.models.archive import PrintArchive
from backend.app.models.library import LibraryFile
from backend.app.services.usage_tracker import (
_donor_3mf_conflicts,
_expected_plate_for_print,
_resolve_3mf_fallback,
)
def _write_3mf(path: Path, *, plate: int, filaments: int) -> Path:
"""A single-plate 3MF declaring *filaments* filaments on plate *plate*."""
path.parent.mkdir(parents=True, exist_ok=True)
rows = "".join(
f"" for i in range(filaments)
)
with zipfile.ZipFile(path, "w") as zf:
zf.writestr(
"Metadata/slice_info.config",
""
f""
f"{rows}"
"",
)
return path
def _write_multiplate_3mf(path: Path, plates: dict[int, int]) -> Path:
"""An all-plates export: ``{plate index: filament count}``."""
path.parent.mkdir(parents=True, exist_ok=True)
body = ""
for plate, filaments in plates.items():
rows = "".join(
f"" for i in range(filaments)
)
body += f"{rows}"
with zipfile.ZipFile(path, "w") as zf:
zf.writestr("Metadata/slice_info.config", f"{body}")
return path
class TestWhatRulesADonorOut:
def test_a_donor_holding_a_different_plate(self, tmp_path):
donor = _write_3mf(tmp_path / "donor.3mf", plate=2, filaments=1)
conflict = _donor_3mf_conflicts(donor, expected_plate=1)
assert conflict is not None
assert "plate 2" in conflict
def test_an_all_plates_export_without_the_running_plate(self, tmp_path):
"""Left alone this is the silent one: the plate is looked for
downstream, found missing, and every filament in the file is summed onto
a single plate's print."""
donor = _write_multiplate_3mf(tmp_path / "donor.3mf", {1: 1, 2: 3})
assert _donor_3mf_conflicts(donor, expected_plate=5) is not None
def test_an_unreadable_donor_is_not_rejected_on_that_alone(self, tmp_path):
"""Refusing a file we merely could not parse would take the fallback
away from every 3MF variant this parser does not understand -- and an
unreadable file is not evidence about which plate it holds. "No plates
found" must not be read as "not your plate". The parse failure surfaces
downstream as "no filament usage data" instead.
"""
donor = tmp_path / "broken.3mf"
donor.write_bytes(b"PK\x03\x04not-really-a-3mf")
assert _donor_3mf_conflicts(donor, expected_plate=None) is None
assert _donor_3mf_conflicts(donor, expected_plate=2) is None
class TestWhatMustStillBeAccepted:
def test_the_matching_plate(self, tmp_path):
donor = _write_3mf(tmp_path / "donor.3mf", plate=2, filaments=2)
assert _donor_3mf_conflicts(donor, expected_plate=2) is None
def test_an_all_plates_export_that_carries_the_running_plate(self, tmp_path):
"""``peek_plate_index_in_3mf`` returns None for a multi-plate file --
"which plate is this" has no answer (#2522) -- so the file is judged on
whether it holds the plate instead."""
donor = _write_multiplate_3mf(tmp_path / "donor.3mf", {1: 3, 2: 1})
assert _donor_3mf_conflicts(donor, expected_plate=2) is None
def test_nothing_known_accepts_anything(self, tmp_path):
"""The reporter's firmware echoes only the 3MF filename and the print
was not one Bambuddy dispatched, so the plate is unknowable. Accepting
is the pre-existing behaviour and stays -- refusing here would retire
the fallback recovery this same issue asked for -- but it is logged."""
donor = _write_3mf(tmp_path / "donor.3mf", plate=7, filaments=4)
assert _donor_3mf_conflicts(donor, expected_plate=None) is None
class TestTheCheckThatIsDeliberatelyNotMade:
def test_the_filament_count_is_not_compared(self, tmp_path):
"""A donor whose plate matches is accepted however many filaments it
declares, and this is the load-bearing reason why.
``ams_mapping`` is indexed by the *project's* filament slots, not the
plate's -- ``slot_to_tray[slot_id - 1]`` in the same module -- so a
genuine single-filament print publishes ``[0, -1, -1, -1]``. Comparing
its length against a plate's filament count would reject correct donors
far more often than wrong ones, on every multi-filament project.
"""
donor = _write_3mf(tmp_path / "donor.3mf", plate=2, filaments=3)
assert _donor_3mf_conflicts(donor, expected_plate=2) is None
class TestWhereTheExpectationsComeFrom:
def test_the_plate_column_wins_when_it_is_set(self):
assert _expected_plate_for_print(3, "Metadata/plate_1.gcode") == 3
def test_otherwise_the_gcode_path_the_printer_echoed(self):
assert _expected_plate_for_print(None, "Metadata/plate_2.gcode") == 2
def test_a_p1s_that_echoes_only_the_3mf_name_knows_no_plate(self):
"""Verbatim from the report: ``PRINT START detected - file:
Desktop_Goose.gcode.3mf``. There is no plate in that."""
assert _expected_plate_for_print(None, "Desktop_Goose.gcode.3mf") is None
@pytest.mark.asyncio
class TestTheLookupItself:
async def _seed(self, engine, tmp_path, *, donor_plate: int):
maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
donor_rel = "archive/1/donor.gcode.3mf"
_write_3mf(tmp_path / donor_rel, plate=donor_plate, filaments=1)
async with maker() as db:
# A successfully archived print keeps the 3MF's name; the fallback
# row keeps whatever the printer echoed, which here is the plate
# path that tells us which plate is running.
donor = PrintArchive(
printer_id=1,
filename="Trent.gcode.3mf",
file_path=donor_rel,
file_size=1,
print_name="Trent",
status="completed",
)
fallback = PrintArchive(
printer_id=1,
filename="Metadata/plate_2.gcode",
file_path="",
file_size=0,
print_name="Trent",
status="printing",
extra_data={"no_3mf_available": True},
)
db.add_all([donor, fallback])
await db.commit()
await db.refresh(donor)
await db.refresh(fallback)
return maker, donor.id, fallback.id
async def test_a_wrong_donor_is_refused(self, test_engine, tmp_path):
maker, _, fallback_id = await self._seed(test_engine, tmp_path, donor_plate=1)
async with maker() as db:
archive = await db.get(PrintArchive, fallback_id)
assert await _resolve_3mf_fallback(archive, db, tmp_path) is None
async def test_a_matching_donor_is_still_used(self, test_engine, tmp_path):
maker, _, fallback_id = await self._seed(test_engine, tmp_path, donor_plate=2)
async with maker() as db:
archive = await db.get(PrintArchive, fallback_id)
resolved = await _resolve_3mf_fallback(archive, db, tmp_path)
assert resolved is not None and resolved.name == "donor.gcode.3mf"
async def test_the_library_branch_is_guarded_too(self, test_engine, tmp_path):
"""A library upload can be the wrong plate for exactly the same reason a
previous archive can, and it is consulted first."""
maker = async_sessionmaker(test_engine, class_=AsyncSession, expire_on_commit=False)
_write_3mf(tmp_path / "library/Trent.3mf", plate=1, filaments=3)
async with maker() as db:
db.add(LibraryFile(filename="Trent.3mf", file_path="library/Trent.3mf", file_type="3mf", file_size=1))
# The printer echoes the plate path, so the plate is knowable; the
# search stem falls back to the print name, which is what finds the
# library upload in the first place.
archive = PrintArchive(
printer_id=1,
filename="Metadata/plate_2.gcode",
file_path="",
file_size=0,
print_name="Trent",
status="printing",
)
db.add(archive)
await db.commit()
await db.refresh(archive)
assert await _resolve_3mf_fallback(archive, db, tmp_path) is None