mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 19:21:33 +02:00
The CI-only failure on test_layer_timelapse_expected_archive came from
the test driving the entire on_print_start flow through ~12 patches and
a MagicMock printer, which behaved differently between Python 3.11 (CI)
and 3.13 (local) — execution stopped silently somewhere in the
expected-archive path under CI's pytest-xdist parallelism but completed
locally.
Fix root-shape instead of fix the symptom:
1. Extract the three identical start_session call sites in on_print_start
(expected-archive promotion at 2030, fallback archive at 2554, fresh
archive at 2644) into one helper _maybe_start_layer_timelapse() with
the same external_camera_enabled / external_camera_url guard. The
three inline blocks had already started drifting (#1353 originally
only fixed one of them on the first pass) — the helper keeps them
locked together going forward.
2. Rewrite the test to call the helper directly. Uses SimpleNamespace
(strict attribute access) instead of MagicMock (default-truthy), no
DB mocking, no event loop, no parallel-state surface. Four small
cases instead of two integration-style ones: enabled→starts,
disabled→skips, URL-missing→skips, camera_type default 'mjpeg'.
110 lines
4.8 KiB
Python
110 lines
4.8 KiB
Python
"""Regression test for #1353: layer timelapse must start for queue/VP-dispatched prints.
|
|
|
|
Reporter @Andlar94 ran the external-camera flow on an A1 dispatched via the
|
|
print queue (so each print landed in the on_print_start "expected archive"
|
|
branch). Frames were never captured, no MP4 was produced, yet the post-print
|
|
log line said "Stitching layer timelapse for printer 1" — because
|
|
`tl_complete()` ran, found no active session, and silently returned None.
|
|
|
|
Root cause: only the two new-archive code paths in on_print_start
|
|
(`fallback_archive` + `archive_print`) called `layer_timelapse.start_session`.
|
|
The expected-archive branch — where reprints and queue dispatch land —
|
|
updated the existing archive's status to "printing" but never started a
|
|
timelapse session.
|
|
|
|
Fix: the three start_session call sites in on_print_start were unified
|
|
behind `_maybe_start_layer_timelapse(printer, printer_id, archive_id)`,
|
|
which gates on the same `external_camera_enabled and external_camera_url`
|
|
check. Testing the helper directly (instead of driving the whole
|
|
on_print_start flow) keeps this regression locked in without dragging in
|
|
unrelated side effects (plate detection, DB queries, MQTT relay, etc.).
|
|
"""
|
|
|
|
from types import SimpleNamespace
|
|
from unittest.mock import patch
|
|
|
|
from backend.app.main import _maybe_start_layer_timelapse
|
|
|
|
|
|
def _make_printer(*, external_camera_enabled: bool, external_camera_url: str | None):
|
|
"""Construct a minimal printer-shaped object with exactly the attributes
|
|
the helper reads. SimpleNamespace is used over MagicMock so attribute
|
|
access raises AttributeError on anything unexpected — keeps the test
|
|
honest about which fields the helper actually depends on.
|
|
"""
|
|
return SimpleNamespace(
|
|
external_camera_enabled=external_camera_enabled,
|
|
external_camera_url=external_camera_url,
|
|
external_camera_type="snapshot",
|
|
external_camera_snapshot_url=external_camera_url,
|
|
)
|
|
|
|
|
|
def test_starts_timelapse_when_external_camera_enabled():
|
|
"""Queue/VP-dispatched prints land in the expected-archive branch and must
|
|
start the timelapse session there (the #1353 root cause). The helper is
|
|
called from all three on_print_start paths (expected-archive promotion,
|
|
fallback archive, fresh archive) so testing it once covers all three."""
|
|
printer = _make_printer(
|
|
external_camera_enabled=True,
|
|
external_camera_url="http://camera.local:5000/snapshot.jpg",
|
|
)
|
|
|
|
with patch("backend.app.services.layer_timelapse.start_session") as mock_start_session:
|
|
started = _maybe_start_layer_timelapse(printer, printer_id=1, archive_id=42)
|
|
|
|
assert started is True
|
|
mock_start_session.assert_called_once_with(
|
|
1,
|
|
42,
|
|
"http://camera.local:5000/snapshot.jpg",
|
|
"snapshot",
|
|
snapshot_url="http://camera.local:5000/snapshot.jpg",
|
|
)
|
|
|
|
|
|
def test_skips_timelapse_when_external_camera_disabled():
|
|
"""The same guard that the new-archive paths use must hold here: no
|
|
external camera → no timelapse session. Otherwise we'd try to capture
|
|
from a None URL and crash the print-start flow."""
|
|
printer = _make_printer(external_camera_enabled=False, external_camera_url=None)
|
|
|
|
with patch("backend.app.services.layer_timelapse.start_session") as mock_start_session:
|
|
started = _maybe_start_layer_timelapse(printer, printer_id=1, archive_id=99)
|
|
|
|
assert started is False
|
|
mock_start_session.assert_not_called()
|
|
|
|
|
|
def test_skips_timelapse_when_url_missing_even_if_flag_set():
|
|
"""If the toggle is on but the URL field is empty (legacy / half-configured
|
|
install), the guard must still hold — calling start_session with an empty
|
|
URL would crash downstream when the capture thread tries to fetch frames."""
|
|
printer = _make_printer(external_camera_enabled=True, external_camera_url=None)
|
|
|
|
with patch("backend.app.services.layer_timelapse.start_session") as mock_start_session:
|
|
started = _maybe_start_layer_timelapse(printer, printer_id=1, archive_id=7)
|
|
|
|
assert started is False
|
|
mock_start_session.assert_not_called()
|
|
|
|
|
|
def test_camera_type_defaults_to_mjpeg_when_unset():
|
|
"""external_camera_type defaults to 'mjpeg' in start_session when the
|
|
printer column is None — pre-existing contract preserved by the helper."""
|
|
printer = SimpleNamespace(
|
|
external_camera_enabled=True,
|
|
external_camera_url="http://cam/feed",
|
|
external_camera_type=None,
|
|
external_camera_snapshot_url=None,
|
|
)
|
|
|
|
with patch("backend.app.services.layer_timelapse.start_session") as mock_start_session:
|
|
_maybe_start_layer_timelapse(printer, printer_id=2, archive_id=11)
|
|
|
|
assert mock_start_session.called
|
|
call_kwargs = mock_start_session.call_args.kwargs
|
|
call_args = mock_start_session.call_args.args
|
|
assert call_args[3] == "mjpeg"
|
|
assert call_kwargs["snapshot_url"] is None
|