fix(r5): address the third /code-review high round — 7 findings
- the sweep guard anchors the rating_key to digits: the round-2 widening had degenerated to "anything ending in _<digits>" (backup_2024, release_10) - RSS counts apply-side failures too, so a read-only database no longer reports a wholly failed poll as "ok — new=0, failed=0" - the player reads/writes its volume directly (readAccount/writeAccount): the persisted-object hook rendered nothing and cost a second write per settle - new tests: the sweep guard's accept/reject table, and run_ffmpeg's threading (drain, cancel while silent, stall watchdog, stderr tail on a nonzero exit) - the breaker sentinel is checked one way (isinstance) at all four sites - run_rss_poll is typed dict[str, int] All three new test groups were mutation-checked: reverting each fix turns them red (or, for the cancel/stall pair, hangs — which is the bug itself).
This commit is contained in:
@@ -3,6 +3,7 @@ playlist-push circuit breaker, and RSS telling a failure apart from an empty fee
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from app.sync import runner
|
||||
from app.sync.playlists import FAILED, _Breaker
|
||||
from app.sync.subscriptions import should_prune
|
||||
from app.youtube import rss
|
||||
@@ -85,6 +86,55 @@ class TestBreakerRun:
|
||||
_Breaker().run(boom, lambda: None)
|
||||
|
||||
|
||||
class _StubDb:
|
||||
"""Just enough Session for run_rss_poll when the caller supplies the channel list."""
|
||||
|
||||
def __init__(self):
|
||||
self.rollbacks = 0
|
||||
|
||||
def rollback(self):
|
||||
self.rollbacks += 1
|
||||
|
||||
|
||||
class _StubChannel:
|
||||
def __init__(self, cid):
|
||||
self.id = cid
|
||||
|
||||
|
||||
class TestRssPollResult:
|
||||
"""`failed` must count every channel that did not get polled — feed-side AND apply-side.
|
||||
Counting only the fetch failures would move the "a broken run looks like a quiet hour" blind
|
||||
spot rather than remove it."""
|
||||
|
||||
def _run(self, monkeypatch, *, fetch, apply_):
|
||||
monkeypatch.setattr(runner, "fetch_channel_feed", fetch)
|
||||
monkeypatch.setattr(runner, "apply_rss_feed", apply_)
|
||||
return runner.run_rss_poll(_StubDb(), [_StubChannel("UC1"), _StubChannel("UC2")])
|
||||
|
||||
def test_a_healthy_run_reports_no_failures(self, monkeypatch):
|
||||
out = self._run(monkeypatch, fetch=lambda cid: [{"id": "v"}], apply_=lambda db, ch, e: 1)
|
||||
assert out == {"new": 2, "failed": 0}
|
||||
|
||||
def test_unreadable_feeds_are_counted(self, monkeypatch):
|
||||
def boom(cid):
|
||||
raise rss.RssError("404")
|
||||
|
||||
out = self._run(monkeypatch, fetch=boom, apply_=lambda db, ch, e: 1)
|
||||
assert out == {"new": 0, "failed": 2}
|
||||
|
||||
def test_apply_failures_are_counted_too(self, monkeypatch):
|
||||
# A read-only database: every feed reads fine, nothing can be stored.
|
||||
def boom(db, ch, entries):
|
||||
raise RuntimeError("read-only transaction")
|
||||
|
||||
out = self._run(monkeypatch, fetch=lambda cid: [{"id": "v"}], apply_=boom)
|
||||
assert out == {"new": 0, "failed": 2}, "a run where nothing was stored must not read as ok"
|
||||
|
||||
def test_a_quiet_hour_is_distinguishable_from_an_outage(self, monkeypatch):
|
||||
quiet = self._run(monkeypatch, fetch=lambda cid: [], apply_=lambda db, ch, e: 0)
|
||||
assert quiet == {"new": 0, "failed": 0} # same "new", different "failed" — that's the point
|
||||
|
||||
|
||||
class TestRssErrorVsEmpty:
|
||||
"""A failed poll must RAISE (so the caller leaves `last_rss_at` alone); only a genuinely
|
||||
empty feed may return []."""
|
||||
|
||||
Reference in New Issue
Block a user