test_no_3mf_archive_paths.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. """A no-3MF archive still owns a directory, and still explains itself (#2843).
  2. Both cases below are the same underlying situation: an H2-series or P2S print
  3. sent from the slicer goes to internal eMMC, Bambuddy cannot fetch the 3MF, and
  4. the archive is created with ``file_path == ""``.
  5. """
  6. import logging
  7. from datetime import datetime, timezone
  8. from types import SimpleNamespace
  9. from unittest.mock import MagicMock
  10. import pytest
  11. # Their rows refer to printer 1.
  12. pytestmark = pytest.mark.usefixtures("printer_1")
  13. @pytest.fixture
  14. def data_dirs(monkeypatch, tmp_path):
  15. """Point base_dir/archive_dir at a scratch tree, as a real install has them."""
  16. from backend.app.core.config import settings
  17. base = tmp_path / "data"
  18. (base / "archive").mkdir(parents=True)
  19. monkeypatch.setattr(settings, "base_dir", base)
  20. monkeypatch.setattr(settings, "archive_dir", base / "archive")
  21. return base
  22. class TestTimelapseDestination:
  23. """``attach_timelapse`` must stay inside the data directory."""
  24. @staticmethod
  25. async def _archive(db_session, file_path: str):
  26. from backend.app.models.archive import PrintArchive
  27. archive = PrintArchive(
  28. printer_id=1,
  29. print_name="Cube",
  30. filename="/data/Metadata/plate_1.gcode" if not file_path else "Cube.gcode.3mf",
  31. file_path=file_path,
  32. file_size=0,
  33. status="completed",
  34. )
  35. db_session.add(archive)
  36. await db_session.commit()
  37. return archive
  38. @pytest.mark.asyncio
  39. async def test_no_3mf_archive_writes_under_the_data_dir(self, db_session, data_dirs):
  40. """Regression: this resolved to ``base_dir.parent`` — /app in Docker, so
  41. the write failed EACCES; where the parent was writable it dropped a stray
  42. video beside the install and then failed on relative_to() anyway."""
  43. from backend.app.services.archive import ArchiveService
  44. archive = await self._archive(db_session, "")
  45. ok = await ArchiveService(db_session).attach_timelapse(archive.id, b"video-bytes", "video_2026.mp4")
  46. assert ok is True
  47. written = data_dirs / "archive" / str(archive.id) / "video_2026.mp4"
  48. assert written.read_bytes() == b"video-bytes"
  49. # Nothing may appear above the data directory.
  50. assert not list(data_dirs.parent.glob("*.mp4"))
  51. @pytest.mark.asyncio
  52. async def test_timelapse_path_is_stored_relative_to_base_dir(self, db_session, data_dirs):
  53. """The old path could not be made relative to base_dir at all, which is
  54. what raised ValueError and lost the video after a successful download."""
  55. from backend.app.services.archive import ArchiveService
  56. archive = await self._archive(db_session, "")
  57. await ArchiveService(db_session).attach_timelapse(archive.id, b"video-bytes", "video_2026.mp4")
  58. assert archive.timelapse_path == f"archive/{archive.id}/video_2026.mp4"
  59. @pytest.mark.asyncio
  60. async def test_a_normal_archive_is_unaffected(self, db_session, data_dirs):
  61. """An archive with a 3MF keeps writing beside it, exactly as before."""
  62. from backend.app.services.archive import ArchiveService
  63. archive = await self._archive(db_session, "archive/1/20260817_Cube/Cube.gcode.3mf")
  64. (data_dirs / "archive" / "1" / "20260817_Cube").mkdir(parents=True)
  65. ok = await ArchiveService(db_session).attach_timelapse(archive.id, b"video-bytes", "video_2026.mp4")
  66. assert ok is True
  67. assert archive.timelapse_path == "archive/1/20260817_Cube/video_2026.mp4"
  68. class TestUnchargeableTrayIsAnnounced:
  69. """A tray the print used but could not be charged must say so."""
  70. @pytest.fixture(autouse=True)
  71. def _clear_sessions(self):
  72. from backend.app.services.usage_tracker import _active_sessions
  73. _active_sessions.clear()
  74. yield
  75. _active_sessions.clear()
  76. @pytest.mark.asyncio
  77. async def test_used_tray_with_no_start_remain_is_logged(self, db_session, caplog):
  78. """Non-RFID spools report remain = -1, so they never enter
  79. ``tray_remain_start`` — and the loop skipped them with a bare
  80. ``continue``. Nothing deducted, no reason given anywhere."""
  81. from backend.app.services.usage_tracker import PrintSession, _active_sessions, on_print_complete
  82. _active_sessions[1] = PrintSession(
  83. printer_id=1,
  84. print_name="Cube",
  85. started_at=datetime.now(timezone.utc),
  86. # AMS0-T0 is missing: it read -1 when the print began.
  87. tray_remain_start={(1, 0): 50},
  88. tray_now_at_start=0,
  89. )
  90. printer_manager = MagicMock()
  91. printer_manager.get_status.return_value = SimpleNamespace(
  92. raw_data={"ams": [{"id": 0, "tray": [{"id": 0, "remain": 40}]}]},
  93. progress=100,
  94. layer_num=50,
  95. tray_now=0,
  96. tray_change_log=[],
  97. )
  98. with caplog.at_level(logging.INFO, logger="backend.app.services.usage_tracker"):
  99. await on_print_complete(
  100. printer_id=1,
  101. data={"status": "completed"},
  102. printer_manager=printer_manager,
  103. db=db_session,
  104. archive_id=None,
  105. ams_mapping=[0],
  106. )
  107. assert "AMS0-T0: no valid remain% at print start" in caplog.text
  108. @pytest.mark.asyncio
  109. async def test_a_tray_the_print_never_touched_stays_quiet(self, db_session, caplog):
  110. """The loop walks every tray on the printer, so logging unconditionally
  111. would narrate slots that had nothing to do with this print."""
  112. from backend.app.services.usage_tracker import PrintSession, _active_sessions, on_print_complete
  113. _active_sessions[1] = PrintSession(
  114. printer_id=1,
  115. print_name="Cube",
  116. started_at=datetime.now(timezone.utc),
  117. tray_remain_start={(1, 0): 50},
  118. tray_now_at_start=0,
  119. )
  120. printer_manager = MagicMock()
  121. printer_manager.get_status.return_value = SimpleNamespace(
  122. raw_data={"ams": [{"id": 3, "tray": [{"id": 2, "remain": 40}]}]},
  123. progress=100,
  124. layer_num=50,
  125. tray_now=0,
  126. tray_change_log=[],
  127. )
  128. with caplog.at_level(logging.INFO, logger="backend.app.services.usage_tracker"):
  129. await on_print_complete(
  130. printer_id=1,
  131. data={"status": "completed"},
  132. printer_manager=printer_manager,
  133. db=db_session,
  134. archive_id=None,
  135. ams_mapping=[0],
  136. )
  137. assert "AMS3-T2: no valid remain%" not in caplog.text