test_layer_timelapse_attach.py 3.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
  1. """The external camera's timelapse and the printer's own (TL-3).
  2. With both on, both videos arrive for one archive. The camera's, the one set
  3. up in Bambuddy, wins: it replaces the printer's (usually there first, since
  4. stitching takes minutes), and the replaced file is deleted instead of being
  5. left in the archive folder with nothing pointing at it. Frames taken for one
  6. run never go to a newer run of the same archive.
  7. """
  8. from datetime import datetime
  9. from unittest.mock import AsyncMock, patch
  10. import pytest
  11. from backend.app.models.archive import PrintArchive
  12. from backend.tests.unit.test_fallback_timelapse_baseline_2957 import _seed
  13. pytestmark = pytest.mark.asyncio
  14. RUN = datetime(2026, 10, 19, 8, 0)
  15. async def _attach(test_engine, tmp_path, monkeypatch, *, existing: str | None, run_started_at=RUN):
  16. from backend.app import main as main_module
  17. from backend.app.core.config import settings as app_config
  18. monkeypatch.setattr(app_config, "base_dir", tmp_path)
  19. monkeypatch.setattr(app_config, "archive_dir", tmp_path / "archive")
  20. maker, _printer_id, archive_id = await _seed(test_engine)
  21. async with maker() as db:
  22. archive = await db.get(PrintArchive, archive_id)
  23. archive.started_at = RUN
  24. archive.timelapse_path = existing
  25. await db.commit()
  26. if existing:
  27. (tmp_path / existing).parent.mkdir(parents=True, exist_ok=True)
  28. (tmp_path / existing).write_bytes(b"printer video")
  29. stitched = tmp_path / "stitched.mp4"
  30. stitched.write_bytes(b"layer video")
  31. announce = AsyncMock()
  32. with (
  33. patch.object(main_module, "async_session", maker),
  34. patch("backend.app.services.archive._announce_timelapse", announce),
  35. ):
  36. await main_module._attach_layer_timelapse(archive_id, stitched, None, run_started_at)
  37. async with maker() as db:
  38. attached = (await db.get(PrintArchive, archive_id)).timelapse_path
  39. return attached, stitched, announce
  40. async def test_it_replaces_the_printer_s_video_and_deletes_that_file(test_engine, tmp_path, monkeypatch):
  41. attached, stitched, announce = await _attach(test_engine, tmp_path, monkeypatch, existing="archive/1/video_a.mp4")
  42. assert attached and attached.endswith("layer_timelapse.mp4")
  43. assert not (tmp_path / "archive/1/video_a.mp4").exists(), "the replaced video would be left orphaned"
  44. assert not stitched.exists(), "the stitched temporary is removed either way"
  45. assert announce.await_args.kwargs == {"attached": True}
  46. async def test_without_one_it_is_attached(test_engine, tmp_path, monkeypatch):
  47. attached, stitched, _announce = await _attach(test_engine, tmp_path, monkeypatch, existing=None)
  48. assert attached and attached.endswith("layer_timelapse.mp4")
  49. assert not stitched.exists()
  50. async def test_a_newer_run_of_the_archive_does_not_get_it(test_engine, tmp_path, monkeypatch):
  51. """The stitch took long enough for the archive to start printing again."""
  52. attached, stitched, announce = await _attach(
  53. test_engine,
  54. tmp_path,
  55. monkeypatch,
  56. existing="archive/1/video_a.mp4",
  57. run_started_at=datetime(2026, 10, 19, 7, 0),
  58. )
  59. assert attached == "archive/1/video_a.mp4"
  60. assert (tmp_path / "archive/1/video_a.mp4").exists()
  61. assert not stitched.exists()
  62. announce.assert_not_awaited()