Found during a targeted code review, not from a user report: - _run_jpeg_loop()/_run_h264_loop() operated directly on the shared self._proc_jpeg/self._proc_h264 instance attributes in their cleanup, unlike _run_mjpeg_loop() (already fixed for exactly this) which uses a local `proc` reference. If a loop's task is cancelled - e.g. by CameraCache.reset() after the printer rotates its stream URL on reboot - while a new task has already started and assigned its own process to the shared attribute, the cancelled task's cleanup killed the NEWER process instead of its own, leaking its own ffmpeg child as an orphan. Applied the same local-variable + identity-check pattern already used by _run_mjpeg_loop. - handle_api_settings_post and handle_api_update_apply were the only two of ~84 handlers that called `await request.json()` without a try/except - every other handler follows the established pattern of returning a clean 400 for a malformed body. A trivial malformed request to either endpoint produced an unhandled 500 with a full traceback instead. New tests in tests/test_camera_process_race.py, tests/test_settings.py, and tests/test_update_check.py cover both.
67 lines
2.4 KiB
Python
67 lines
2.4 KiB
Python
"""Update-check regression for Issue #104.
|
|
|
|
STABLE_RELEASE_API used limit=1, so it only ever saw the single newest
|
|
release on Gitea regardless of type. Since nightly/dev prereleases publish
|
|
far more often than stable releases, that newest release is almost always a
|
|
prerelease - the stable_releases filter (not prerelease) then found nothing
|
|
and /api/update/check returned "no stable releases found" even though older
|
|
stable releases exist.
|
|
"""
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_apply_invalid_json_returns_400(client):
|
|
"""A malformed/non-JSON body must be a clean 400, not an unhandled 500
|
|
with a raw JSONDecodeError traceback (code review finding)."""
|
|
c, _ = client
|
|
resp = await c.post("/api/update/apply", data=b"not json", headers={"Content-Type": "application/json"})
|
|
assert resp.status == 400
|
|
|
|
|
|
def _fake_releases_response(payload):
|
|
resp = MagicMock()
|
|
resp.status = 200
|
|
resp.json = AsyncMock(return_value=payload)
|
|
ctx = MagicMock()
|
|
ctx.__aenter__ = AsyncMock(return_value=resp)
|
|
ctx.__aexit__ = AsyncMock(return_value=False)
|
|
return ctx
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_stable_update_check_finds_release_behind_newer_prereleases(client):
|
|
c, bridge = client
|
|
bridge._read_version = lambda: "0.9.27"
|
|
|
|
releases = (
|
|
[{"tag_name": f"nightly-0.9.30-nightly{i}", "prerelease": True} for i in range(1, 7)]
|
|
+ [{"tag_name": "v0.9.29", "prerelease": False, "body": "changelog"}]
|
|
)
|
|
|
|
with patch("aiohttp.ClientSession.get", return_value=_fake_releases_response(releases)):
|
|
resp = await c.get("/api/update/check")
|
|
data = await resp.json()
|
|
|
|
assert resp.status == 200
|
|
assert data["latest"] == "0.9.29"
|
|
assert data["update_available"] is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_stable_update_check_requests_enough_releases_to_skip_prereleases(client):
|
|
"""The API URL itself must ask for more than the single newest release -
|
|
a limit=1 request can never find a stable release behind a run of
|
|
prereleases no matter how the response is parsed."""
|
|
c, bridge = client
|
|
bridge._read_version = lambda: "0.9.27"
|
|
|
|
import re
|
|
assert not re.search(r"limit=1(?!\d)", bridge.STABLE_RELEASE_API), (
|
|
"STABLE_RELEASE_API must request more than 1 release, otherwise a "
|
|
"recent nightly/dev prerelease being the newest release hides all "
|
|
"stable releases behind it (Issue #104)"
|
|
)
|