| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109 |
- """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
|