| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169 |
- """A no-3MF archive still owns a directory, and still explains itself (#2843).
- Both cases below are the same underlying situation: an H2-series or P2S print
- sent from the slicer goes to internal eMMC, Bambuddy cannot fetch the 3MF, and
- the archive is created with ``file_path == ""``.
- """
- import logging
- from datetime import datetime, timezone
- from types import SimpleNamespace
- from unittest.mock import MagicMock
- import pytest
- @pytest.fixture
- def data_dirs(monkeypatch, tmp_path):
- """Point base_dir/archive_dir at a scratch tree, as a real install has them."""
- from backend.app.core.config import settings
- base = tmp_path / "data"
- (base / "archive").mkdir(parents=True)
- monkeypatch.setattr(settings, "base_dir", base)
- monkeypatch.setattr(settings, "archive_dir", base / "archive")
- return base
- class TestTimelapseDestination:
- """``attach_timelapse`` must stay inside the data directory."""
- @staticmethod
- async def _archive(db_session, file_path: str):
- from backend.app.models.archive import PrintArchive
- archive = PrintArchive(
- printer_id=1,
- print_name="Cube",
- filename="/data/Metadata/plate_1.gcode" if not file_path else "Cube.gcode.3mf",
- file_path=file_path,
- file_size=0,
- status="completed",
- )
- db_session.add(archive)
- await db_session.commit()
- return archive
- @pytest.mark.asyncio
- async def test_no_3mf_archive_writes_under_the_data_dir(self, db_session, data_dirs):
- """Regression: this resolved to ``base_dir.parent`` — /app in Docker, so
- the write failed EACCES; where the parent was writable it dropped a stray
- video beside the install and then failed on relative_to() anyway."""
- from backend.app.services.archive import ArchiveService
- archive = await self._archive(db_session, "")
- ok = await ArchiveService(db_session).attach_timelapse(archive.id, b"video-bytes", "video_2026.mp4")
- assert ok is True
- written = data_dirs / "archive" / str(archive.id) / "video_2026.mp4"
- assert written.read_bytes() == b"video-bytes"
- # Nothing may appear above the data directory.
- assert not list(data_dirs.parent.glob("*.mp4"))
- @pytest.mark.asyncio
- async def test_timelapse_path_is_stored_relative_to_base_dir(self, db_session, data_dirs):
- """The old path could not be made relative to base_dir at all, which is
- what raised ValueError and lost the video after a successful download."""
- from backend.app.services.archive import ArchiveService
- archive = await self._archive(db_session, "")
- await ArchiveService(db_session).attach_timelapse(archive.id, b"video-bytes", "video_2026.mp4")
- assert archive.timelapse_path == f"archive/{archive.id}/video_2026.mp4"
- @pytest.mark.asyncio
- async def test_a_normal_archive_is_unaffected(self, db_session, data_dirs):
- """An archive with a 3MF keeps writing beside it, exactly as before."""
- from backend.app.services.archive import ArchiveService
- archive = await self._archive(db_session, "archive/1/20260817_Cube/Cube.gcode.3mf")
- (data_dirs / "archive" / "1" / "20260817_Cube").mkdir(parents=True)
- ok = await ArchiveService(db_session).attach_timelapse(archive.id, b"video-bytes", "video_2026.mp4")
- assert ok is True
- assert archive.timelapse_path == "archive/1/20260817_Cube/video_2026.mp4"
- class TestUnchargeableTrayIsAnnounced:
- """A tray the print used but could not be charged must say so."""
- @pytest.fixture(autouse=True)
- def _clear_sessions(self):
- from backend.app.services.usage_tracker import _active_sessions
- _active_sessions.clear()
- yield
- _active_sessions.clear()
- @pytest.mark.asyncio
- async def test_used_tray_with_no_start_remain_is_logged(self, db_session, caplog):
- """Non-RFID spools report remain = -1, so they never enter
- ``tray_remain_start`` — and the loop skipped them with a bare
- ``continue``. Nothing deducted, no reason given anywhere."""
- from backend.app.services.usage_tracker import PrintSession, _active_sessions, on_print_complete
- _active_sessions[1] = PrintSession(
- printer_id=1,
- print_name="Cube",
- started_at=datetime.now(timezone.utc),
- # AMS0-T0 is missing: it read -1 when the print began.
- tray_remain_start={(1, 0): 50},
- tray_now_at_start=0,
- )
- printer_manager = MagicMock()
- printer_manager.get_status.return_value = SimpleNamespace(
- raw_data={"ams": [{"id": 0, "tray": [{"id": 0, "remain": 40}]}]},
- progress=100,
- layer_num=50,
- tray_now=0,
- tray_change_log=[],
- )
- with caplog.at_level(logging.INFO, logger="backend.app.services.usage_tracker"):
- await on_print_complete(
- printer_id=1,
- data={"status": "completed"},
- printer_manager=printer_manager,
- db=db_session,
- archive_id=None,
- ams_mapping=[0],
- )
- assert "AMS0-T0: no valid remain% at print start" in caplog.text
- @pytest.mark.asyncio
- async def test_a_tray_the_print_never_touched_stays_quiet(self, db_session, caplog):
- """The loop walks every tray on the printer, so logging unconditionally
- would narrate slots that had nothing to do with this print."""
- from backend.app.services.usage_tracker import PrintSession, _active_sessions, on_print_complete
- _active_sessions[1] = PrintSession(
- printer_id=1,
- print_name="Cube",
- started_at=datetime.now(timezone.utc),
- tray_remain_start={(1, 0): 50},
- tray_now_at_start=0,
- )
- printer_manager = MagicMock()
- printer_manager.get_status.return_value = SimpleNamespace(
- raw_data={"ams": [{"id": 3, "tray": [{"id": 2, "remain": 40}]}]},
- progress=100,
- layer_num=50,
- tray_now=0,
- tray_change_log=[],
- )
- with caplog.at_level(logging.INFO, logger="backend.app.services.usage_tracker"):
- await on_print_complete(
- printer_id=1,
- data={"status": "completed"},
- printer_manager=printer_manager,
- db=db_session,
- archive_id=None,
- ams_mapping=[0],
- )
- assert "AMS3-T2: no valid remain%" not in caplog.text
|