mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
164 lines
6.5 KiB
Python
164 lines
6.5 KiB
Python
"""Regression for #1485 follow-up: timelapse baseline on restart-recovery.
|
|
|
|
When Bambuddy restarts mid-print, the first MQTT push has
|
|
``_previous_gcode_state = None`` which the #1304 guard treats as "first push
|
|
after Bambuddy startup, don't fire on_print_start" — avoiding duplicate
|
|
archive creation. But that path is also where ``_capture_timelapse_baseline_at_start``
|
|
lives, so without a separate hook the baseline is never captured. The
|
|
completion-time scan then falls into its "take baseline now" fallback
|
|
that snapshots the SD card AFTER the in-flight MP4 has landed, the new
|
|
file ends up in the baseline set, and no diff ever matches.
|
|
|
|
bambu_mqtt.py:_process_message now fires a sibling ``on_print_running_observed``
|
|
callback in this case. main.py wires it to ``on_print_running_observed``
|
|
which captures the baseline. These tests verify that handler.
|
|
"""
|
|
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from backend.app.main import _timelapse_baselines
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _clear_baselines():
|
|
_timelapse_baselines.clear()
|
|
yield
|
|
_timelapse_baselines.clear()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_running_observed_captures_baseline_on_restart_recovery():
|
|
"""The handler must capture the printer's existing-videos snapshot so
|
|
the completion-time scan has something to set-diff against. This is
|
|
the case the in-the-field pwostran report (#1485) hits: pre-reboot
|
|
baseline of 7 files lost on restart, post-reboot fallback baseline
|
|
sees the 8 files (including the just-uploaded one) → no new file."""
|
|
mock_printer = MagicMock()
|
|
mock_printer.id = 1
|
|
mock_printer.name = "TestP1S"
|
|
mock_printer.ip_address = "192.168.1.100"
|
|
mock_printer.access_code = "12345678"
|
|
mock_printer.model = "P1S"
|
|
|
|
existing_videos = [
|
|
{"name": "earlier_a.mp4", "is_directory": False, "path": "/timelapse/earlier_a.mp4"},
|
|
{"name": "earlier_b.mp4", "is_directory": False, "path": "/timelapse/earlier_b.mp4"},
|
|
{"name": "earlier_c.mp4", "is_directory": False, "path": "/timelapse/earlier_c.mp4"},
|
|
]
|
|
|
|
def execute_router(stmt, *args, **kwargs):
|
|
return MagicMock(scalar_one_or_none=MagicMock(return_value=mock_printer))
|
|
|
|
mock_session = AsyncMock()
|
|
mock_session.__aenter__ = AsyncMock(return_value=mock_session)
|
|
mock_session.__aexit__ = AsyncMock()
|
|
mock_session.execute = AsyncMock(side_effect=execute_router)
|
|
|
|
with (
|
|
patch("backend.app.main.async_session") as mock_session_maker,
|
|
patch(
|
|
"backend.app.main._list_timelapse_videos",
|
|
new=AsyncMock(return_value=(existing_videos, "/timelapse")),
|
|
),
|
|
):
|
|
mock_session_maker.return_value = mock_session
|
|
|
|
from backend.app.main import on_print_running_observed
|
|
|
|
await on_print_running_observed(
|
|
1,
|
|
{
|
|
"filename": "/data/Metadata/test_print.gcode",
|
|
"subtask_name": "Test_Print",
|
|
"remaining_time": 3600,
|
|
"raw_data": {},
|
|
"ams_mapping": None,
|
|
},
|
|
)
|
|
|
|
# Snapshot the dict state immediately after the handler returns —
|
|
# don't rely on _timelapse_baselines surviving outside the patches.
|
|
# CI intermittently saw the dict empty by the time a later top-level
|
|
# assert ran (likely an xdist-parallel teardown race on the session-
|
|
# scoped event_loop fixture in conftest.py). Capturing the value here
|
|
# is what the test actually wants to verify anyway: the handler set
|
|
# the baseline at the moment it returned.
|
|
captured = _timelapse_baselines.get(1)
|
|
|
|
assert captured == {"earlier_a.mp4", "earlier_b.mp4", "earlier_c.mp4"}, (
|
|
"restart-recovery handler must capture the printer's existing-videos "
|
|
"baseline so the completion-time scan can set-diff to find the new file"
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_running_observed_skips_when_baseline_already_present():
|
|
"""If on_print_start already ran in this Bambuddy process for the same
|
|
printer (the realistic same-session race), a second capture would
|
|
overwrite the correct pre-print baseline with one taken later — which
|
|
could include the in-flight MP4. The archive lookup still has to run so
|
|
restart recovery can restore durable print ownership for the kill switch."""
|
|
_timelapse_baselines[1] = {"pre_existing_a.mp4", "pre_existing_b.mp4"}
|
|
|
|
with (
|
|
patch("backend.app.main.async_session") as mock_session_maker,
|
|
patch("backend.app.main._list_timelapse_videos", new=AsyncMock()) as mock_list,
|
|
):
|
|
from backend.app.main import on_print_running_observed
|
|
|
|
await on_print_running_observed(
|
|
1,
|
|
{
|
|
"filename": "/data/Metadata/test_print.gcode",
|
|
"subtask_name": "Test_Print",
|
|
"remaining_time": 3600,
|
|
"raw_data": {},
|
|
"ams_mapping": None,
|
|
},
|
|
)
|
|
|
|
# Ownership reconciliation still consults the DB, but the expensive
|
|
# timelapse scan remains one-shot.
|
|
mock_session_maker.assert_called_once()
|
|
mock_list.assert_not_called()
|
|
|
|
# Original baseline preserved.
|
|
assert _timelapse_baselines[1] == {"pre_existing_a.mp4", "pre_existing_b.mp4"}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_running_observed_skips_when_printer_row_missing():
|
|
"""If the printer was deleted between the MQTT push and this handler
|
|
running, we can't capture anything — log and return without raising."""
|
|
mock_session = AsyncMock()
|
|
mock_session.__aenter__ = AsyncMock(return_value=mock_session)
|
|
mock_session.__aexit__ = AsyncMock()
|
|
mock_session.execute = AsyncMock(return_value=MagicMock(scalar_one_or_none=MagicMock(return_value=None)))
|
|
|
|
with (
|
|
patch("backend.app.main.async_session") as mock_session_maker,
|
|
patch("backend.app.main._list_timelapse_videos", new=AsyncMock()) as mock_list,
|
|
):
|
|
mock_session_maker.return_value = mock_session
|
|
|
|
from backend.app.main import on_print_running_observed
|
|
|
|
# Should not raise.
|
|
await on_print_running_observed(
|
|
999,
|
|
{
|
|
"filename": "/data/Metadata/test_print.gcode",
|
|
"subtask_name": "Test_Print",
|
|
"remaining_time": 3600,
|
|
"raw_data": {},
|
|
"ams_mapping": None,
|
|
},
|
|
)
|
|
|
|
# FTP scan must not run if the printer row didn't resolve.
|
|
mock_list.assert_not_called()
|
|
|
|
assert 999 not in _timelapse_baselines
|