| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186 |
- """Deleting a printer drops what Bambuddy kept for its id (SQLite gives the id
- to the next printer created). See unit/test_forget_printer.py for the full list."""
- import pytest
- from httpx import AsyncClient
- from backend.app import main
- from backend.app.models.printer import Printer
- from backend.app.services.bambu_mqtt import PrinterState
- from backend.app.services.printer_manager import printer_manager
- pytestmark = [pytest.mark.integration]
- async def test_the_next_printer_given_the_id_starts_clean(async_client: AsyncClient, db_session, tmp_path, monkeypatch):
- from backend.app.services import plate_detection
- monkeypatch.setattr(plate_detection, "_get_calibration_dir", lambda: tmp_path)
- printer = Printer(name="A", serial_number="LEFT-A", ip_address="192.0.2.41", access_code="x")
- db_session.add(printer)
- await db_session.commit()
- pid = printer.id
- # Mid-print, dispatched for alice, plate calibrated.
- printer_manager.set_current_print_user(pid, 1, "alice")
- printer_manager._awaiting_plate_clear.add(pid)
- main._active_prints[(pid, "cube.3mf")] = 42
- (tmp_path / f"printer_{pid}_ref_0.jpg").write_bytes(b"jpg")
- (tmp_path / f"printer_{pid}_metadata.json").write_text("{}")
- try:
- response = await async_client.delete(f"/api/v1/printers/{pid}")
- assert response.status_code == 200, response.text
- assert printer_manager.get_current_print_user(pid) is None
- assert not printer_manager.is_awaiting_plate_clear(pid)
- state = PrinterState(subtask_name="cube", gcode_file="cube.3mf")
- assert not main._is_bambuddy_authorized_print_in_memory(pid, state) # the kill switch stops it
- assert list(tmp_path.iterdir()) == []
- finally:
- printer_manager._current_print_user.pop(pid, None)
- printer_manager._awaiting_plate_clear.discard(pid)
- main._active_prints.pop((pid, "cube.3mf"), None)
- async def _print_sessions(db, printer_id: int) -> None:
- from datetime import datetime
- from backend.app.models.active_print_session import ActivePrintSession
- from backend.app.models.active_print_spoolman import ActivePrintSpoolman
- from backend.app.models.archive import PrintArchive
- archive = PrintArchive(filename="cube.3mf", file_path="x", file_size=1, printer_id=printer_id)
- db.add(archive)
- await db.commit()
- db.add_all(
- [
- ActivePrintSession(printer_id=printer_id, print_name="cube", started_at=datetime(2026, 1, 1)),
- ActivePrintSpoolman(printer_id=printer_id, archive_id=archive.id, ams_trays={}),
- ]
- )
- await db.commit()
- async def _session_rows(db) -> tuple[list, list]:
- from sqlalchemy import select
- from backend.app.models.active_print_session import ActivePrintSession
- from backend.app.models.active_print_spoolman import ActivePrintSpoolman
- sessions = (await db.execute(select(ActivePrintSession.printer_id))).scalars().all()
- spoolman = (await db.execute(select(ActivePrintSpoolman.printer_id))).scalars().all()
- return list(sessions), list(spoolman)
- async def test_the_print_being_tracked_goes_with_the_printer(async_client: AsyncClient, db_session):
- """A new printer given the id and already printing would restore it, and its
- filament would be charged to the deleted printer's spools."""
- printer = Printer(name="A", serial_number="LEFT-B", ip_address="192.0.2.42", access_code="x")
- db_session.add(printer)
- await db_session.commit()
- await _print_sessions(db_session, printer.id)
- response = await async_client.delete(f"/api/v1/printers/{printer.id}", params={"delete_archives": "false"})
- assert response.status_code == 200, response.text
- assert await _session_rows(db_session) == ([], [])
- @pytest.mark.sqlite_only # stages stale ids with a raw delete, which foreign keys refuse or null out
- async def test_the_startup_repair_drops_print_tracking_of_deleted_printers(db_session):
- from sqlalchemy import text
- from backend.app.core.database import _repair_sqlite_orphans
- printer = Printer(name="A", serial_number="LEFT-C", ip_address="192.0.2.43", access_code="x")
- db_session.add(printer)
- await db_session.commit()
- await _print_sessions(db_session, printer.id)
- await db_session.execute(text("DELETE FROM printers WHERE id = :id"), {"id": printer.id}) # the old way
- await db_session.commit()
- await _repair_sqlite_orphans(await db_session.connection())
- await db_session.commit()
- assert await _session_rows(db_session) == ([], [])
- async def test_an_external_camera_stream_of_the_printer_ends(async_client: AsyncClient, db_session): # noqa: ARG001 - patches the session factory
- """An MJPEG or snapshot camera runs no ffmpeg the stop could find: a viewer
- left open kept writing the old camera's frames under the id, which plate
- detection and the finish photo of the next printer given it would read."""
- import asyncio
- from unittest.mock import MagicMock, patch
- from backend.app.api.routes import camera
- from backend.app.core.auth import StreamOpener
- printer = Printer(
- name="A",
- serial_number="LEFT-D",
- ip_address="192.0.2.44",
- access_code="x",
- external_camera_enabled=True,
- external_camera_url="http://192.0.2.51/mjpeg",
- external_camera_type="mjpeg",
- )
- db_session.add(printer)
- await db_session.commit()
- async def _frames(*_args, on_frame=None, **_kwargs):
- while True:
- on_frame(b"\xff\xd8old\xff\xd9")
- yield b"--frame\r\n"
- await asyncio.sleep(0)
- with patch("backend.app.services.external_camera.generate_mjpeg_stream", _frames):
- response = await camera.camera_stream(printer.id, MagicMock(), fps=5, opener=StreamOpener(auth_on=False))
- stream = response.body_iterator
- await anext(stream)
- assert camera._last_frames.get(printer.id)
- await camera.forget_printer(printer.id)
- with pytest.raises(StopAsyncIteration):
- await asyncio.wait_for(anext(stream), 2)
- assert printer.id not in camera._last_frames
- async def test_a_new_printer_starts_without_plate_references(
- async_client: AsyncClient, db_session, tmp_path, monkeypatch
- ):
- """Ones left under its id by a delete before deletes removed them would show
- as its calibration and be compared with its plate."""
- from unittest.mock import AsyncMock, patch
- from backend.app.services import plate_detection
- monkeypatch.setattr(plate_detection, "_get_calibration_dir", lambda: tmp_path)
- existing = Printer(name="A", serial_number="LEFT-E", ip_address="192.0.2.45", access_code="x")
- db_session.add(existing)
- await db_session.commit()
- reused = existing.id + 1 # SQLite: the highest id plus one
- for printer_id in (existing.id, reused):
- (tmp_path / f"printer_{printer_id}_ref_0.jpg").write_bytes(b"jpg")
- (tmp_path / f"printer_{printer_id}_metadata.json").write_text("{}")
- with patch(
- "backend.app.api.routes.printers.printer_manager.test_connection",
- AsyncMock(return_value={"success": True}),
- ):
- response = await async_client.post(
- "/api/v1/printers/",
- json={
- "name": "B",
- "serial_number": "LEFT-F",
- "ip_address": "192.0.2.46",
- "access_code": "12345678",
- "is_active": False,
- },
- )
- assert response.status_code == 200, response.text
- assert response.json()["id"] == reused
- assert sorted(p.name for p in tmp_path.iterdir()) == [
- f"printer_{existing.id}_metadata.json",
- f"printer_{existing.id}_ref_0.jpg",
- ]
|