| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562 |
- """Each print of an archive keeps its own timelapse and photos (archive redesign P1b).
- Plates of one archive printed on two printers are two runs. Each run's scan,
- layer stitch and finish photo find its own video by the run's id, and the
- archive shows its newest run's. A reprint of a plate still replaces that
- plate's earlier video, as when the archive held the only one.
- """
- from __future__ import annotations
- import logging
- from datetime import datetime, timedelta
- from types import SimpleNamespace
- from unittest.mock import AsyncMock, MagicMock, patch
- import pytest
- from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
- from backend.app.core.config import settings as app_settings
- from backend.app.models.archive import PrintArchive
- from backend.app.models.print_log import INCLUDE_OPEN_RUNS, RUN_OPEN, PrintLogEntry
- pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
- T1 = datetime(2026, 10, 30, 9)
- T2 = T1 + timedelta(hours=1)
- ARCHIVE_DIR = "archives/test"
- logger = logging.getLogger(__name__)
- @pytest.fixture
- def files(tmp_path, monkeypatch):
- """The data directory, in tmp_path rather than the real one."""
- monkeypatch.setattr(app_settings, "base_dir", tmp_path)
- monkeypatch.setattr(app_settings, "archive_dir", tmp_path / "archive")
- (tmp_path / ARCHIVE_DIR).mkdir(parents=True)
- return tmp_path
- @pytest.fixture
- def sessions(test_engine):
- """The sessions main and the services open, on the test database."""
- maker = async_sessionmaker(test_engine, class_=AsyncSession, expire_on_commit=False)
- with (
- patch("backend.app.main.async_session", maker),
- patch("backend.app.core.database.async_session", maker),
- patch("backend.app.services.archive._announce_timelapse", AsyncMock()),
- patch("backend.app.main.ws_manager", MagicMock(send_archive_updated=AsyncMock())),
- ):
- yield maker
- @pytest.fixture
- async def two_plates(db_session, printer_factory, files, sessions):
- """One archive: plate 1 printed on A, then plate 2 started on B since.
- Neither has a video yet."""
- a, b = await printer_factory(name="A"), await printer_factory(name="B")
- archive = PrintArchive(
- printer_id=b.id,
- filename="two.gcode.3mf",
- file_path=f"{ARCHIVE_DIR}/two.gcode.3mf",
- file_size=1,
- print_name="two",
- status="completed",
- started_at=T2,
- )
- db_session.add(archive)
- await db_session.flush()
- first = PrintLogEntry(
- archive_id=archive.id, printer_id=a.id, plate_id=1, status="completed", started_at=T1, photos=[]
- )
- second = PrintLogEntry(
- archive_id=archive.id, printer_id=b.id, plate_id=2, status="completed", started_at=T2, photos=[]
- )
- db_session.add_all([first, second])
- await db_session.commit()
- return SimpleNamespace(archive=archive.id, a=a.id, b=b.id, first=first.id, second=second.id, files=files)
- async def _row(db_session, model, row_id):
- db_session.expire_all()
- return await db_session.get(model, row_id)
- async def _set(db_session, model, row_id, **values):
- row = await _row(db_session, model, row_id)
- for name, value in values.items():
- setattr(row, name, value)
- await db_session.commit()
- async def _attach(archive_id, data, name, **kwargs):
- from backend.app.core.database import async_session
- from backend.app.services.archive import ArchiveService
- async with async_session() as db:
- return await ArchiveService(db).attach_timelapse(archive_id, data, name, **kwargs)
- class TestAttach:
- async def test_an_earlier_plates_video_is_its_runs_only(self, db_session, two_plates):
- assert await _attach(two_plates.archive, b"one", "video_1.mp4", plate_id=1, run_id=two_plates.first)
- first = await _row(db_session, PrintLogEntry, two_plates.first)
- assert first.timelapse_path == f"{ARCHIVE_DIR}/video_1.mp4"
- archive = await _row(db_session, PrintArchive, two_plates.archive)
- assert archive.timelapse_path is None
- assert (await _row(db_session, PrintLogEntry, two_plates.second)).timelapse_path is None
- async def test_the_newest_runs_video_is_the_archives_too(self, db_session, two_plates):
- assert await _attach(two_plates.archive, b"two", "video_2.mp4", plate_id=2, run_id=two_plates.second)
- archive = await _row(db_session, PrintArchive, two_plates.archive)
- assert (archive.timelapse_path, archive.timelapse_plate_id) == (f"{ARCHIVE_DIR}/video_2.mp4", 2)
- assert (await _row(db_session, PrintLogEntry, two_plates.second)).timelapse_path == f"{ARCHIVE_DIR}/video_2.mp4"
- async def test_a_printing_run_is_the_newest(self, db_session, two_plates):
- """A print starting clears the archive's video: plate 2's landing
- after plate 3 started is not the archive's."""
- db_session.add(
- PrintLogEntry(
- archive_id=two_plates.archive,
- printer_id=two_plates.a,
- status=RUN_OPEN,
- started_at=T2 + timedelta(hours=1),
- )
- )
- await db_session.commit()
- assert await _attach(two_plates.archive, b"two", "video_2.mp4", run_id=two_plates.second)
- assert (await _row(db_session, PrintArchive, two_plates.archive)).timelapse_path is None
- async def test_one_attached_by_hand_is_the_newest_runs(self, db_session, two_plates):
- assert await _attach(two_plates.archive, b"hand", "picked.mp4")
- assert (await _row(db_session, PrintArchive, two_plates.archive)).timelapse_path == f"{ARCHIVE_DIR}/picked.mp4"
- assert (await _row(db_session, PrintLogEntry, two_plates.second)).timelapse_path == f"{ARCHIVE_DIR}/picked.mp4"
- assert (await _row(db_session, PrintLogEntry, two_plates.first)).timelapse_path is None
- async def test_a_gone_run_gets_nothing(self, db_session, two_plates):
- assert not await _attach(two_plates.archive, b"x", "video_9.mp4", run_id=999999)
- assert not (two_plates.files / ARCHIVE_DIR / "video_9.mp4").exists()
- async def test_does_not_write_over_another_runs_video(self, db_session, two_plates):
- """The external-camera stitch always names its output layer_timelapse.mp4."""
- assert await _attach(two_plates.archive, b"one", "layer_timelapse.mp4", run_id=two_plates.first)
- assert await _attach(two_plates.archive, b"two", "layer_timelapse.mp4", run_id=two_plates.second)
- assert (two_plates.files / ARCHIVE_DIR / "layer_timelapse.mp4").read_bytes() == b"one"
- second = await _row(db_session, PrintLogEntry, two_plates.second)
- assert second.timelapse_path == f"{ARCHIVE_DIR}/layer_timelapse_2.mp4"
- async def test_an_earlier_runs_stitch_keeps_the_archives_video(self, db_session, two_plates):
- """Plate 2's stitch is the archive's; plate 1's, done later under the
- same name, is plate 1's own file."""
- assert await _attach(two_plates.archive, b"two", "layer_timelapse.mp4", run_id=two_plates.second)
- assert await _attach(two_plates.archive, b"one", "layer_timelapse.mp4", run_id=two_plates.first)
- assert (two_plates.files / ARCHIVE_DIR / "layer_timelapse.mp4").read_bytes() == b"two"
- first = await _row(db_session, PrintLogEntry, two_plates.first)
- assert first.timelapse_path == f"{ARCHIVE_DIR}/layer_timelapse_2.mp4"
- class TestConversion:
- async def _convert(self, archive_id, source):
- from backend.app.services.archive import _convert_timelapse_to_mp4
- process = AsyncMock(returncode=0)
- process.communicate = AsyncMock(return_value=(b"", b""))
- source.with_suffix(".mp4").write_bytes(b"mp4")
- with (
- patch("backend.app.services.camera.get_ffmpeg_path", return_value="/usr/bin/ffmpeg"),
- patch("asyncio.create_subprocess_exec", AsyncMock(return_value=process)),
- ):
- await _convert_timelapse_to_mp4(archive_id, source)
- async def test_an_earlier_runs_video_is_converted_for_it(self, db_session, two_plates):
- source = two_plates.files / ARCHIVE_DIR / "video_1.avi"
- source.write_bytes(b"avi")
- await _set(db_session, PrintLogEntry, two_plates.first, timelapse_path=f"{ARCHIVE_DIR}/video_1.avi")
- await self._convert(two_plates.archive, source)
- first = await _row(db_session, PrintLogEntry, two_plates.first)
- assert first.timelapse_path == f"{ARCHIVE_DIR}/video_1.mp4"
- assert (two_plates.files / ARCHIVE_DIR / "video_1.mp4").exists()
- assert not source.exists()
- assert (await _row(db_session, PrintArchive, two_plates.archive)).timelapse_path is None
- async def test_the_archive_and_its_run_both_move(self, db_session, two_plates):
- source = two_plates.files / ARCHIVE_DIR / "video_2.avi"
- source.write_bytes(b"avi")
- await _set(db_session, PrintLogEntry, two_plates.second, timelapse_path=f"{ARCHIVE_DIR}/video_2.avi")
- await _set(db_session, PrintArchive, two_plates.archive, timelapse_path=f"{ARCHIVE_DIR}/video_2.avi")
- await self._convert(two_plates.archive, source)
- assert (await _row(db_session, PrintLogEntry, two_plates.second)).timelapse_path == f"{ARCHIVE_DIR}/video_2.mp4"
- assert (await _row(db_session, PrintArchive, two_plates.archive)).timelapse_path == f"{ARCHIVE_DIR}/video_2.mp4"
- class TestClaimed:
- async def test_another_plate_of_the_archive_holds_its_video(self, db_session, two_plates):
- from backend.app.services.print_log import claimed_timelapse_stems
- await _set(db_session, PrintLogEntry, two_plates.first, timelapse_path=f"{ARCHIVE_DIR}/video_1.mp4")
- third = PrintLogEntry(archive_id=two_plates.archive, printer_id=two_plates.a, status="completed", started_at=T2)
- db_session.add(third)
- await db_session.commit()
- claimed = await claimed_timelapse_stems(
- db_session, two_plates.a, archive_id=two_plates.archive, run_id=third.id
- )
- assert claimed == {"video_1"}
- own = await claimed_timelapse_stems(
- db_session, two_plates.a, archive_id=two_plates.archive, run_id=two_plates.first
- )
- assert own == set()
- async def test_without_a_run_the_archives_runs_are_its_own(self, db_session, two_plates):
- from backend.app.services.print_log import claimed_timelapse_stems
- await _set(db_session, PrintLogEntry, two_plates.first, timelapse_path=f"{ARCHIVE_DIR}/video_1.mp4")
- assert (
- await claimed_timelapse_stems(db_session, two_plates.a, archive_id=two_plates.archive, run_id=None) == set()
- )
- other = PrintArchive(filename="o.3mf", file_path="o/o.3mf", file_size=1, printer_id=two_plates.b)
- db_session.add(other)
- await db_session.commit()
- assert await claimed_timelapse_stems(db_session, two_plates.a, archive_id=other.id, run_id=None) == {"video_1"}
- def _video(name, size=3):
- return {"name": name, "size": size, "path": f"/timelapse/{name}"}
- class TestScan:
- async def _scan(self, plates, listings, *, baseline=None, on_list=None):
- from backend.app.main import _scan_for_timelapse_with_retries
- listed_printers = []
- async def list_videos(printer):
- listed_printers.append(printer.id)
- if on_list is not None:
- await on_list(len(listed_printers))
- return listings[min(len(listed_printers), len(listings)) - 1], "/timelapse"
- async def download(*_args, expected_size=None, **_kwargs):
- return b"v" * expected_size
- with (
- patch("backend.app.main._list_timelapse_videos", side_effect=list_videos),
- patch("backend.app.main.asyncio.sleep", AsyncMock()),
- patch("backend.app.main._timelapse_scan_max_attempts", return_value=3),
- patch("backend.app.services.bambu_ftp.download_file_bytes_async", side_effect=download),
- patch("backend.app.services.bambu_ftp.remote_file_settled", AsyncMock(return_value=True)),
- patch("backend.app.services.bambu_ftp.delete_archived_timelapse", AsyncMock()),
- ):
- await _scan_for_timelapse_with_retries(plates.archive, baseline, plate_id=1, run_id=plates.first)
- return listed_printers
- async def test_scans_the_printer_the_run_printed_on(self, db_session, two_plates):
- printers = await self._scan(two_plates, [[_video("new.mp4")]], baseline=set())
- assert printers == [two_plates.a]
- first = await _row(db_session, PrintLogEntry, two_plates.first)
- assert first.timelapse_path == f"{ARCHIVE_DIR}/new.mp4"
- async def test_the_next_plates_start_does_not_end_the_scan(self, db_session, two_plates):
- async def next_plate_starts(round_no):
- if round_no == 1:
- await _set(db_session, PrintArchive, two_plates.archive, started_at=T2 + timedelta(hours=1))
- await self._scan(two_plates, [[], [_video("new.mp4")]], baseline=set(), on_list=next_plate_starts)
- assert (await _row(db_session, PrintLogEntry, two_plates.first)).timelapse_path == f"{ARCHIVE_DIR}/new.mp4"
- async def test_reads_the_baseline_stored_on_its_run(self, db_session, two_plates):
- """After a restart: the archive's baseline is plate 2's start's."""
- await _set(
- db_session, PrintLogEntry, two_plates.first, timelapse_baseline=["old.mp4"], timelapse_baseline_unread=False
- )
- await _set(db_session, PrintArchive, two_plates.archive, timelapse_baseline=["old.mp4", "new.mp4"])
- await self._scan(two_plates, [[_video("old.mp4"), _video("new.mp4")]])
- assert (await _row(db_session, PrintLogEntry, two_plates.first)).timelapse_path == f"{ARCHIVE_DIR}/new.mp4"
- async def test_another_plates_video_on_the_archive_does_not_stop_it(self, db_session, two_plates):
- await _set(db_session, PrintArchive, two_plates.archive, timelapse_path=f"{ARCHIVE_DIR}/plate2.mp4")
- await self._scan(two_plates, [[_video("new.mp4")]], baseline=set())
- assert (await _row(db_session, PrintLogEntry, two_plates.first)).timelapse_path == f"{ARCHIVE_DIR}/new.mp4"
- async def _later_print(self, db_session, plates, *, printer_id, plate_id):
- db_session.add(
- PrintLogEntry(
- archive_id=plates.archive,
- printer_id=printer_id,
- plate_id=plate_id,
- status=RUN_OPEN,
- started_at=T2 + timedelta(hours=1),
- )
- )
- await db_session.commit()
- async def test_a_reprint_of_its_plate_ends_it(self, db_session, two_plates):
- """The reprint's video is the plate's one (it replaces this one)."""
- await self._later_print(db_session, two_plates, printer_id=two_plates.b, plate_id=1)
- await self._scan(two_plates, [[_video("new.mp4")]], baseline=set())
- assert (await _row(db_session, PrintLogEntry, two_plates.first)).timelapse_path is None
- async def test_the_next_print_on_its_printer_ends_it(self, db_session, two_plates):
- """A video landing now can be that print's."""
- await self._later_print(db_session, two_plates, printer_id=two_plates.a, plate_id=3)
- await self._scan(two_plates, [[_video("new.mp4")]], baseline=set())
- assert (await _row(db_session, PrintLogEntry, two_plates.first)).timelapse_path is None
- async def test_an_earlier_print_of_its_plate_does_not_end_it(self, db_session, two_plates):
- db_session.add(
- PrintLogEntry(
- archive_id=two_plates.archive,
- printer_id=two_plates.a,
- plate_id=1,
- status="completed",
- started_at=T1 - timedelta(hours=1),
- )
- )
- await db_session.commit()
- await self._scan(two_plates, [[_video("new.mp4")]], baseline=set())
- assert (await _row(db_session, PrintLogEntry, two_plates.first)).timelapse_path == f"{ARCHIVE_DIR}/new.mp4"
- async def test_another_plate_on_another_printer_does_not_end_it(self, db_session, two_plates):
- await self._later_print(db_session, two_plates, printer_id=two_plates.b, plate_id=3)
- await self._scan(two_plates, [[_video("new.mp4")]], baseline=set())
- assert (await _row(db_session, PrintLogEntry, two_plates.first)).timelapse_path == f"{ARCHIVE_DIR}/new.mp4"
- async def test_stops_once_its_run_has_a_video(self, db_session, two_plates):
- await _set(db_session, PrintLogEntry, two_plates.first, timelapse_path=f"{ARCHIVE_DIR}/kept.mp4")
- printers = await self._scan(two_plates, [[_video("new.mp4")]], baseline=set())
- assert printers == []
- assert (await _row(db_session, PrintLogEntry, two_plates.first)).timelapse_path == f"{ARCHIVE_DIR}/kept.mp4"
- class TestBaseline:
- async def test_print_start_writes_it_on_the_run(self, db_session, two_plates):
- from backend.app.main import _capture_timelapse_baseline_at_start
- run = PrintLogEntry(archive_id=two_plates.archive, printer_id=two_plates.b, status=RUN_OPEN, started_at=T2)
- db_session.add(run)
- await db_session.commit()
- run_id = run.id
- with patch("backend.app.main._read_timelapse_baseline", AsyncMock(return_value=({"a.mp4"}, True))):
- await _capture_timelapse_baseline_at_start(MagicMock(), two_plates.b, logger, archive_id=two_plates.archive)
- db_session.expire_all()
- run = await db_session.get(PrintLogEntry, run_id, execution_options={INCLUDE_OPEN_RUNS: True})
- assert (run.timelapse_baseline, run.timelapse_baseline_unread) == (["a.mp4"], False)
- assert (await _row(db_session, PrintArchive, two_plates.archive)).timelapse_baseline == ["a.mp4"]
- async def test_a_reread_fills_in_its_run(self, db_session, two_plates):
- from backend.app.main import _reread_unread_timelapse_baseline
- await _set(db_session, PrintLogEntry, two_plates.first, timelapse_baseline=[], timelapse_baseline_unread=True)
- with (
- patch("backend.app.main._TIMELAPSE_BASELINE_REREAD_INTERVAL_SECONDS", 0),
- patch("backend.app.main._unread_baseline_round", return_value=True),
- patch("backend.app.main._read_timelapse_baseline", AsyncMock(return_value=({"a.mp4"}, True))),
- ):
- await _reread_unread_timelapse_baseline(two_plates.a, set(), two_plates.archive, T1, two_plates.first)
- first = await _row(db_session, PrintLogEntry, two_plates.first)
- assert (first.timelapse_baseline, first.timelapse_baseline_unread) == (["a.mp4"], False)
- class TestLayerTimelapse:
- async def test_lands_on_its_run_after_the_next_plate_started(self, db_session, two_plates):
- from backend.app.main import _attach_layer_timelapse
- printer_video = two_plates.files / ARCHIVE_DIR / "video_1.mp4"
- printer_video.write_bytes(b"printer")
- await _set(db_session, PrintLogEntry, two_plates.first, timelapse_path=f"{ARCHIVE_DIR}/video_1.mp4")
- stitched = two_plates.files / "stitched.mp4"
- stitched.write_bytes(b"camera")
- await _attach_layer_timelapse(two_plates.archive, stitched, 1, T1, run_id=two_plates.first)
- first = await _row(db_session, PrintLogEntry, two_plates.first)
- assert first.timelapse_path == f"{ARCHIVE_DIR}/layer_timelapse.mp4"
- assert not printer_video.exists()
- assert not stitched.exists()
- assert (await _row(db_session, PrintArchive, two_plates.archive)).timelapse_path is None
- async def test_a_reprint_of_its_plate_drops_it(self, db_session, two_plates):
- from backend.app.main import _attach_layer_timelapse
- db_session.add(
- PrintLogEntry(
- archive_id=two_plates.archive,
- printer_id=two_plates.b,
- plate_id=1,
- status=RUN_OPEN,
- started_at=T2 + timedelta(hours=1),
- )
- )
- await db_session.commit()
- stitched = two_plates.files / "stitched.mp4"
- stitched.write_bytes(b"camera")
- await _attach_layer_timelapse(two_plates.archive, stitched, 1, T1, run_id=two_plates.first)
- assert (await _row(db_session, PrintLogEntry, two_plates.first)).timelapse_path is None
- assert not stitched.exists()
- class TestFinishPhoto:
- async def test_waits_for_its_runs_video_not_the_archives(self, db_session, two_plates):
- from backend.app.main import _capture_finish_photo_from_timelapse
- (two_plates.files / ARCHIVE_DIR / "video_2.mp4").write_bytes(b"plate 2")
- await _set(db_session, PrintArchive, two_plates.archive, timelapse_path=f"{ARCHIVE_DIR}/video_2.mp4")
- extract = AsyncMock(return_value=True)
- with patch("backend.app.services.camera.extract_video_last_frame", extract):
- photo, pending = await _capture_finish_photo_from_timelapse(
- two_plates.archive, two_plates.files / ARCHIVE_DIR, timeout=0, run_id=two_plates.first
- )
- assert (photo, pending) == (None, True)
- extract.assert_not_awaited()
- async def test_the_upgrade_leads_its_runs_photos(self, db_session, two_plates):
- from backend.app.main import _upgrade_finish_photo_from_timelapse
- await _set(db_session, PrintLogEntry, two_plates.first, photos=["live.jpg"])
- await _set(db_session, PrintArchive, two_plates.archive, photos=["live.jpg", "plate2.jpg"])
- with patch(
- "backend.app.main._capture_finish_photo_from_timelapse", AsyncMock(return_value=("frame.jpg", False))
- ) as capture:
- await _upgrade_finish_photo_from_timelapse(two_plates.archive, two_plates.files, run_id=two_plates.first)
- assert capture.await_args.kwargs["run_id"] == two_plates.first
- assert (await _row(db_session, PrintLogEntry, two_plates.first)).photos == ["frame.jpg", "live.jpg"]
- assert (await _row(db_session, PrintLogEntry, two_plates.second)).photos == []
- assert (await _row(db_session, PrintArchive, two_plates.archive)).photos == [
- "frame.jpg",
- "live.jpg",
- "plate2.jpg",
- ]
- class TestReprint:
- async def test_replaces_the_plates_earlier_videos_only(self, db_session, two_plates):
- from backend.app.main import _drop_replaced_plate_videos
- for run_id, name in ((two_plates.first, "video_1.mp4"), (two_plates.second, "video_2.mp4")):
- (two_plates.files / ARCHIVE_DIR / name).write_bytes(b"v")
- await _set(db_session, PrintLogEntry, run_id, timelapse_path=f"{ARCHIVE_DIR}/{name}")
- await _drop_replaced_plate_videos(db_session, two_plates.archive, 1, None, logger)
- assert (await _row(db_session, PrintLogEntry, two_plates.first)).timelapse_path is None
- assert not (two_plates.files / ARCHIVE_DIR / "video_1.mp4").exists()
- assert (await _row(db_session, PrintLogEntry, two_plates.second)).timelapse_path == f"{ARCHIVE_DIR}/video_2.mp4"
- assert (two_plates.files / ARCHIVE_DIR / "video_2.mp4").exists()
- async def test_the_archives_video_judged_this_plates_goes_with_its_run(self, db_session, two_plates):
- from backend.app.main import _drop_replaced_plate_videos
- (two_plates.files / ARCHIVE_DIR / "old.mp4").write_bytes(b"v")
- await _set(db_session, PrintLogEntry, two_plates.second, timelapse_path=f"{ARCHIVE_DIR}/old.mp4")
- await _drop_replaced_plate_videos(db_session, two_plates.archive, None, f"{ARCHIVE_DIR}/old.mp4", logger)
- assert (await _row(db_session, PrintLogEntry, two_plates.second)).timelapse_path is None
- assert not (two_plates.files / ARCHIVE_DIR / "old.mp4").exists()
- async def test_another_plates_video_is_kept_even_if_named(self, db_session, two_plates):
- from backend.app.main import _drop_replaced_plate_videos
- (two_plates.files / ARCHIVE_DIR / "video_2.mp4").write_bytes(b"v")
- await _set(db_session, PrintLogEntry, two_plates.second, timelapse_path=f"{ARCHIVE_DIR}/video_2.mp4")
- await _drop_replaced_plate_videos(db_session, two_plates.archive, 1, f"{ARCHIVE_DIR}/video_2.mp4", logger)
- assert (two_plates.files / ARCHIVE_DIR / "video_2.mp4").exists()
- assert (await _row(db_session, PrintLogEntry, two_plates.second)).timelapse_path == f"{ARCHIVE_DIR}/video_2.mp4"
- class TestRoutes:
- async def test_deleting_the_video_clears_its_run(self, async_client, db_session, two_plates):
- (two_plates.files / ARCHIVE_DIR / "video_2.mp4").write_bytes(b"v")
- await _set(db_session, PrintLogEntry, two_plates.second, timelapse_path=f"{ARCHIVE_DIR}/video_2.mp4")
- await _set(db_session, PrintArchive, two_plates.archive, timelapse_path=f"{ARCHIVE_DIR}/video_2.mp4")
- response = await async_client.delete(f"/api/v1/archives/{two_plates.archive}/timelapse")
- assert response.status_code == 200, response.text
- assert (await _row(db_session, PrintLogEntry, two_plates.second)).timelapse_path is None
- async def test_deleting_a_photo_takes_it_off_its_run(self, async_client, db_session, two_plates):
- await _set(db_session, PrintLogEntry, two_plates.first, photos=["a.jpg", "b.jpg"])
- await _set(db_session, PrintArchive, two_plates.archive, photos=["a.jpg", "b.jpg"])
- response = await async_client.delete(f"/api/v1/archives/{two_plates.archive}/photos/a.jpg")
- assert response.status_code == 200, response.text
- assert (await _row(db_session, PrintLogEntry, two_plates.first)).photos == ["b.jpg"]
- async def test_a_run_shows_its_own_video_and_photos(self, async_client, db_session, two_plates):
- await _set(db_session, PrintLogEntry, two_plates.first, photos=["one.jpg"], timelapse_path="a/one.mp4")
- await _set(
- db_session, PrintArchive, two_plates.archive, photos=["one.jpg", "two.jpg"], timelapse_path="a/two.mp4"
- )
- response = await async_client.get(f"/api/v1/archives/{two_plates.archive}?run_id={two_plates.first}")
- assert response.status_code == 200, response.text
- body = response.json()
- assert (body["photos"], body["timelapse_path"]) == (["one.jpg"], "a/one.mp4")
- none_yet = await async_client.get(f"/api/v1/archives/{two_plates.archive}?run_id={two_plates.second}")
- assert (none_yet.json()["photos"], none_yet.json()["timelapse_path"]) == (None, None)
- async def test_a_run_logged_before_keeps_the_archives_photos_with_a_video(
- self, async_client, db_session, two_plates
- ):
- """A video attached by hand to it is its own; photos it never had are the archive's."""
- await _set(db_session, PrintLogEntry, two_plates.second, photos=None, timelapse_path="a/picked.mp4")
- await _set(db_session, PrintArchive, two_plates.archive, photos=["two.jpg"], timelapse_path="a/picked.mp4")
- response = await async_client.get(f"/api/v1/archives/{two_plates.archive}?run_id={two_plates.second}")
- assert (response.json()["photos"], response.json()["timelapse_path"]) == (["two.jpg"], "a/picked.mp4")
- async def test_a_run_logged_before_shows_the_archives(self, async_client, db_session, two_plates):
- await _set(db_session, PrintLogEntry, two_plates.second, photos=None)
- await _set(db_session, PrintArchive, two_plates.archive, photos=["two.jpg"], timelapse_path="a/two.mp4")
- response = await async_client.get(f"/api/v1/archives/{two_plates.archive}?run_id={two_plates.second}")
- assert (response.json()["photos"], response.json()["timelapse_path"]) == (["two.jpg"], "a/two.mp4")
- async def test_a_run_opened_at_its_start_keeps_its_own_photos(db_session, two_plates):
- from backend.app.services.print_log import open_run
- archive = await _row(db_session, PrintArchive, two_plates.archive)
- archive.started_at = T2 + timedelta(hours=1)
- run = await open_run(db_session, archive, printer_id=two_plates.a)
- assert run is not None and run.photos == []
|