|
@@ -25,6 +25,7 @@ import pytest
|
|
|
|
|
|
|
|
from backend.app import main as main_module
|
|
from backend.app import main as main_module
|
|
|
from backend.app.main import _cleanup_forced_timelapse
|
|
from backend.app.main import _cleanup_forced_timelapse
|
|
|
|
|
+from backend.app.services.bambu_ftp import DeleteResult
|
|
|
|
|
|
|
|
|
|
|
|
|
def _fake_session_factory(rows: dict):
|
|
def _fake_session_factory(rows: dict):
|
|
@@ -92,7 +93,7 @@ async def test_not_forced_is_noop(monkeypatch, tmp_path):
|
|
|
video_path.parent.mkdir(parents=True, exist_ok=True)
|
|
video_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
video_path.write_bytes(b"x" * 100)
|
|
video_path.write_bytes(b"x" * 100)
|
|
|
|
|
|
|
|
- delete_mock = AsyncMock(return_value=True)
|
|
|
|
|
|
|
+ delete_mock = AsyncMock(return_value=DeleteResult.DELETED)
|
|
|
with patch("backend.app.services.bambu_ftp.delete_file_async", new=delete_mock):
|
|
with patch("backend.app.services.bambu_ftp.delete_file_async", new=delete_mock):
|
|
|
await _cleanup_forced_timelapse(archive_id=99, printer_id=10)
|
|
await _cleanup_forced_timelapse(archive_id=99, printer_id=10)
|
|
|
|
|
|
|
@@ -123,7 +124,7 @@ async def test_forced_deletes_local_and_remote(monkeypatch, tmp_path):
|
|
|
video_path.write_bytes(b"x" * 100)
|
|
video_path.write_bytes(b"x" * 100)
|
|
|
|
|
|
|
|
# FTP DELE succeeds on the first directory we try.
|
|
# FTP DELE succeeds on the first directory we try.
|
|
|
- delete_mock = AsyncMock(return_value=True)
|
|
|
|
|
|
|
+ delete_mock = AsyncMock(return_value=DeleteResult.DELETED)
|
|
|
with patch("backend.app.services.bambu_ftp.delete_file_async", new=delete_mock):
|
|
with patch("backend.app.services.bambu_ftp.delete_file_async", new=delete_mock):
|
|
|
await _cleanup_forced_timelapse(archive_id=99, printer_id=10)
|
|
await _cleanup_forced_timelapse(archive_id=99, printer_id=10)
|
|
|
|
|
|
|
@@ -158,9 +159,10 @@ async def test_forced_walks_alternate_dirs_when_first_fails(monkeypatch, tmp_pat
|
|
|
video_path.parent.mkdir(parents=True, exist_ok=True)
|
|
video_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
video_path.write_bytes(b"x" * 100)
|
|
video_path.write_bytes(b"x" * 100)
|
|
|
|
|
|
|
|
- # First two attempts fail (False), third succeeds (True). Cleanup
|
|
|
|
|
- # should stop after the third.
|
|
|
|
|
- delete_mock = AsyncMock(side_effect=[False, False, True])
|
|
|
|
|
|
|
+ # First two dirs report NOT_FOUND (file not there), third succeeds.
|
|
|
|
|
+ # Cleanup should stop after the third — and crucially must NOT WARN
|
|
|
|
|
+ # because no real network/auth failure happened (#1721).
|
|
|
|
|
+ delete_mock = AsyncMock(side_effect=[DeleteResult.NOT_FOUND, DeleteResult.NOT_FOUND, DeleteResult.DELETED])
|
|
|
with patch("backend.app.services.bambu_ftp.delete_file_async", new=delete_mock):
|
|
with patch("backend.app.services.bambu_ftp.delete_file_async", new=delete_mock):
|
|
|
await _cleanup_forced_timelapse(archive_id=99, printer_id=10)
|
|
await _cleanup_forced_timelapse(archive_id=99, printer_id=10)
|
|
|
|
|
|
|
@@ -203,3 +205,87 @@ async def test_forced_local_cleanup_runs_even_if_ftp_unreachable(monkeypatch, tm
|
|
|
assert archive.timelapse_path is None
|
|
assert archive.timelapse_path is None
|
|
|
# All four dirs were attempted before giving up.
|
|
# All four dirs were attempted before giving up.
|
|
|
assert delete_mock.await_count == 4
|
|
assert delete_mock.await_count == 4
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@pytest.mark.asyncio
|
|
|
|
|
+async def test_forced_no_warning_when_every_dir_returns_not_found(monkeypatch, tmp_path, caplog):
|
|
|
|
|
+ """#1721: when every candidate dir returns 550 (file not there) the
|
|
|
|
|
+ helper used to emit "Could not delete printer-side timelapse ...
|
|
|
|
|
+ (file may already be gone)" at WARNING. That message landed in support
|
|
|
|
|
+ bundles for healthy printers whose firmware swept the SD card itself.
|
|
|
|
|
+ With DeleteResult.NOT_FOUND signalling, no real failure happened →
|
|
|
|
|
+ must be DEBUG, not WARNING.
|
|
|
|
|
+ """
|
|
|
|
|
+ import logging
|
|
|
|
|
+
|
|
|
|
|
+ archive = SimpleNamespace(
|
|
|
|
|
+ bambuddy_forced_timelapse=True,
|
|
|
|
|
+ timelapse_path="archive/1/myprint.mp4",
|
|
|
|
|
+ )
|
|
|
|
|
+ printer = SimpleNamespace(ip_address="10.0.0.5", access_code="12345678", model="N2S")
|
|
|
|
|
+ monkeypatch.setattr(
|
|
|
|
|
+ main_module,
|
|
|
|
|
+ "async_session",
|
|
|
|
|
+ _fake_session_factory({"PrintArchive": archive, "Printer": printer}),
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ video_path = tmp_path / archive.timelapse_path
|
|
|
|
|
+ video_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
+ video_path.write_bytes(b"x" * 100)
|
|
|
|
|
+
|
|
|
|
|
+ delete_mock = AsyncMock(return_value=DeleteResult.NOT_FOUND)
|
|
|
|
|
+ with (
|
|
|
|
|
+ caplog.at_level(logging.DEBUG, logger="backend.app.main"),
|
|
|
|
|
+ patch("backend.app.services.bambu_ftp.delete_file_async", new=delete_mock),
|
|
|
|
|
+ ):
|
|
|
|
|
+ await _cleanup_forced_timelapse(archive_id=99, printer_id=10)
|
|
|
|
|
+
|
|
|
|
|
+ assert delete_mock.await_count == 4
|
|
|
|
|
+ warnings = [r for r in caplog.records if r.levelno >= logging.WARNING and "[FORCED-TIMELAPSE]" in r.message]
|
|
|
|
|
+ assert warnings == [], f"unexpected WARNING(s): {[w.message for w in warnings]}"
|
|
|
|
|
+ debugs = [
|
|
|
|
|
+ r for r in caplog.records if r.levelno == logging.DEBUG and "No printer-side timelapse to delete" in r.message
|
|
|
|
|
+ ]
|
|
|
|
|
+ assert len(debugs) == 1, "expected the 'nothing to delete' debug summary"
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@pytest.mark.asyncio
|
|
|
|
|
+async def test_forced_warns_when_any_dir_returns_failed(monkeypatch, tmp_path, caplog):
|
|
|
|
|
+ """Counterpart to the above: a real network/auth/transient FAILED on any
|
|
|
|
|
+ dir keeps the WARNING — that's the signal the maintainer actually wants
|
|
|
|
|
+ to see.
|
|
|
|
|
+ """
|
|
|
|
|
+ import logging
|
|
|
|
|
+
|
|
|
|
|
+ archive = SimpleNamespace(
|
|
|
|
|
+ bambuddy_forced_timelapse=True,
|
|
|
|
|
+ timelapse_path="archive/1/myprint.mp4",
|
|
|
|
|
+ )
|
|
|
|
|
+ printer = SimpleNamespace(ip_address="10.0.0.5", access_code="12345678", model="O1C")
|
|
|
|
|
+ monkeypatch.setattr(
|
|
|
|
|
+ main_module,
|
|
|
|
|
+ "async_session",
|
|
|
|
|
+ _fake_session_factory({"PrintArchive": archive, "Printer": printer}),
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ video_path = tmp_path / archive.timelapse_path
|
|
|
|
|
+ video_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
+ video_path.write_bytes(b"x" * 100)
|
|
|
|
|
+
|
|
|
|
|
+ delete_mock = AsyncMock(
|
|
|
|
|
+ side_effect=[
|
|
|
|
|
+ DeleteResult.NOT_FOUND,
|
|
|
|
|
+ DeleteResult.FAILED,
|
|
|
|
|
+ DeleteResult.NOT_FOUND,
|
|
|
|
|
+ DeleteResult.NOT_FOUND,
|
|
|
|
|
+ ]
|
|
|
|
|
+ )
|
|
|
|
|
+ with (
|
|
|
|
|
+ caplog.at_level(logging.WARNING, logger="backend.app.main"),
|
|
|
|
|
+ patch("backend.app.services.bambu_ftp.delete_file_async", new=delete_mock),
|
|
|
|
|
+ ):
|
|
|
|
|
+ await _cleanup_forced_timelapse(archive_id=99, printer_id=10)
|
|
|
|
|
+
|
|
|
|
|
+ warnings = [r for r in caplog.records if r.levelno >= logging.WARNING and "[FORCED-TIMELAPSE]" in r.message]
|
|
|
|
|
+ assert len(warnings) == 1
|
|
|
|
|
+ assert "network/auth/transient" in warnings[0].message
|