test(remote): add unit + e2e suite for remote-services subsystem

Covers RemoteClient/RemoteService, REST routes, handlers, SessionStore, InputBridge, DownloadQueueManager, errors, compression, and serve CLI. E2e tier opts in via --live and can auto-spawn its own serve.
This commit is contained in:
imSp4rky
2026-05-21 10:45:25 -06:00
parent 9c905ef7a3
commit 746b573711
29 changed files with 2541 additions and 0 deletions

View File

@@ -0,0 +1,141 @@
"""E2E: track quality probing per service.
Sends a session_create with the full track-selection knob set (range_,
vcodec, best_available) so the server returns every available track.
Picks S01E01 for series, first title for movies, then asserts that the
discovered video tracks meet the expected_quality limits declared in
fixtures.yaml. If no expected block is present, the test prints what was
discovered and skips so you can copy values back into the fixture.
"""
from __future__ import annotations
import time
from typing import Any, Optional
import pytest
pytestmark = [pytest.mark.live, pytest.mark.slow]
def _create_session(http_session, server_url: str, service: str, title_id: str) -> str:
payload = {
"service": service,
"title_id": title_id,
# Quality knobs — send enum names the server's session_create parser accepts.
"range_": ["SDR", "HDR10", "DV"],
"vcodec": ["AVC", "HEVC"],
"best_available": True,
}
r = http_session.post(f"{server_url}/api/session/create", json=payload, timeout=120)
if r.status_code >= 400:
pytest.skip(f"{service}: session create failed {r.status_code}: {r.text[:200]}")
return r.json()["session_id"]
def _wait_for_titles(http_session, server_url: str, sid: str, timeout: float = 120.0):
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
r = http_session.get(f"{server_url}/api/session/{sid}/titles", timeout=30)
if r.status_code == 200:
return r.status_code, r.json()
if r.status_code == 400:
try:
body = r.json()
except Exception:
body = {}
if (body.get("details") or {}).get("auth_status") in ("authenticating", "pending_input"):
time.sleep(2.0)
continue
return r.status_code, r.json() if r.text else {}
return 408, {"message": "auth timeout"}
def _pick_target_title(titles: list[dict[str, Any]], season: int = 1, episode: int = 1) -> Optional[dict[str, Any]]:
"""Pick the configured target episode; fall back to the first title."""
for t in titles:
if t.get("type") == "episode" and t.get("season") == season and t.get("number") == episode:
return t
return titles[0] if titles else None
def _summarize(video_tracks: list[dict[str, Any]]) -> dict[str, Any]:
max_height = max((v.get("height") or 0 for v in video_tracks), default=0)
max_width = max((v.get("width") or 0 for v in video_tracks), default=0)
codecs = sorted({v.get("codec") for v in video_tracks if v.get("codec")})
ranges = sorted({v.get("range") for v in video_tracks if v.get("range")})
bitrates = sorted({v.get("bitrate") for v in video_tracks if v.get("bitrate")})
return {
"track_count": len(video_tracks),
"max_width": max_width,
"max_height": max_height,
"codecs": codecs,
"ranges": ranges,
"bitrate_min_kbps": min(bitrates) if bitrates else None,
"bitrate_max_kbps": max(bitrates) if bitrates else None,
}
def _resolve_title_id(conf: dict) -> Optional[str]:
"""Prefer series_url so S01E01 logic kicks in; fall back to movie title_url."""
return conf.get("series_url") or conf.get("title_url")
def test_track_quality_meets_expected(http_session, server_url: str, service_case, capsys) -> None:
service, conf = service_case
title_input = _resolve_title_id(conf)
if not title_input:
pytest.skip(f"{service}: no title_url/series_url in fixture")
sid = _create_session(http_session, server_url, service, title_input)
try:
code, body = _wait_for_titles(http_session, server_url, sid)
if code != 200:
pytest.skip(f"{service}: titles fetch failed {code}: {str(body)[:200]}")
titles = body.get("titles") or []
target = _pick_target_title(titles, season=conf.get("target_season", 1), episode=conf.get("target_episode", 1))
if not target:
pytest.skip(f"{service}: no titles returned")
# Movies appear with type=='movie'; episodes with type=='episode'.
kind = target.get("type")
title_id = target.get("id")
if not title_id:
pytest.skip(f"{service}: target title has no id")
r = http_session.post(
f"{server_url}/api/session/{sid}/tracks",
json={"title_id": title_id},
timeout=300,
)
if r.status_code != 200:
pytest.skip(f"{service}: tracks fetch failed {r.status_code}: {r.text[:200]}")
tracks = r.json()
video = tracks.get("video") or []
summary = _summarize(video)
# Print summary even when test passes — discovery aid.
with capsys.disabled():
print(f"\n[{service}] target={kind} '{target.get('name')}' -> {summary}")
expected = conf.get("expected_quality") or {}
if not expected:
pytest.skip(f"{service}: no expected_quality in fixtures.yaml — discovered={summary}")
if "min_height" in expected:
assert summary["max_height"] >= expected["min_height"], (
f"{service}: max height {summary['max_height']} < expected min {expected['min_height']}"
)
if "min_codecs" in expected:
missing = set(expected["min_codecs"]) - set(summary["codecs"])
assert not missing, f"{service}: missing codecs {missing}, got {summary['codecs']}"
if "min_ranges" in expected:
missing = set(expected["min_ranges"]) - set(summary["ranges"])
assert not missing, f"{service}: missing ranges {missing}, got {summary['ranges']}"
if "min_track_count" in expected:
assert summary["track_count"] >= expected["min_track_count"], (
f"{service}: only {summary['track_count']} video tracks, expected >= {expected['min_track_count']}"
)
finally:
http_session.delete(f"{server_url}/api/session/{sid}", timeout=30)