mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-14 21:15:06 +00:00
style: clean up prose
This commit is contained in:
@@ -2,10 +2,10 @@
|
||||
|
||||
Test suite for the unshackle remote-services subsystem:
|
||||
|
||||
- `unshackle/core/remote_service.py` — `RemoteClient`, `RemoteService`, helpers
|
||||
- `unshackle/core/api/` — routes, handlers, session_store, input_bridge,
|
||||
- `unshackle/core/remote_service.py`: `RemoteClient`, `RemoteService`, helpers
|
||||
- `unshackle/core/api/`: routes, handlers, session_store, input_bridge,
|
||||
download_manager, errors, compression
|
||||
- `unshackle/commands/serve.py` — CLI surface
|
||||
- `unshackle/commands/serve.py`: CLI surface
|
||||
|
||||
Two tiers:
|
||||
|
||||
@@ -38,7 +38,7 @@ The suite can either:
|
||||
`--server-url http://host:port`.
|
||||
|
||||
Spawn mode is controlled by `--spawn-serve {auto, always, never}` (default
|
||||
`auto` — spawn only when `--server-url` is empty).
|
||||
`auto` spawns only when `--server-url` is empty).
|
||||
|
||||
### Auto-spawn (recommended)
|
||||
|
||||
@@ -78,7 +78,7 @@ services you have access to. Schema (see the example file for full docs):
|
||||
services:
|
||||
EXAMPLE:
|
||||
title_url: "..." # required
|
||||
series_url: "..." # optional — overrides movie target when set
|
||||
series_url: "..." # optional, overrides movie target when set
|
||||
target_season: 1
|
||||
target_episode: 1
|
||||
search_query: "..."
|
||||
@@ -114,9 +114,9 @@ uv run pytest tests/remote -v --live
|
||||
|
||||
## Markers
|
||||
|
||||
- `unit` — fast, mocked (default)
|
||||
- `live` — needs `--live`
|
||||
- `slow` — hits real services; combine with `--live`
|
||||
- `unit`: fast, mocked (default)
|
||||
- `live`: needs `--live`
|
||||
- `slow`: hits real services; combine with `--live`
|
||||
|
||||
Run only fast unit tests:
|
||||
|
||||
@@ -127,4 +127,4 @@ uv run pytest tests/remote -m "unit and not slow"
|
||||
## Adding a new service to e2e
|
||||
|
||||
1. Add a block under `services:` in your local `fixtures.yaml`.
|
||||
2. No new Python needed — every e2e test is parametrized over the YAML.
|
||||
2. No new Python needed; every e2e test is parametrized over the YAML.
|
||||
|
||||
@@ -8,39 +8,39 @@
|
||||
# unshackle (matches the service directory name, e.g. EXAMPLE). The keys
|
||||
# below are read by the e2e tests:
|
||||
#
|
||||
# title_url (required) — movie URL/slug/ID accepted by the service
|
||||
# series_url (optional) — series URL/slug/ID; when set, the
|
||||
# title_url (required): movie URL/slug/ID accepted by the service
|
||||
# series_url (optional): series URL/slug/ID; when set, the
|
||||
# quality + license + download tests
|
||||
# target target_season/target_episode
|
||||
# instead of the movie
|
||||
# target_season (optional, default 1)
|
||||
# target_episode (optional, default 1)
|
||||
# search_query (optional) — string to send to /api/search
|
||||
# requires_auth (optional, default true) — informational
|
||||
# server_cdm (optional, default false) — informational
|
||||
# drm (optional) — informational ("widevine" / "playready")
|
||||
# search_query (optional): string to send to /api/search
|
||||
# requires_auth (optional, default true): informational
|
||||
# server_cdm (optional, default false): informational
|
||||
# drm (optional): informational ("widevine" / "playready")
|
||||
#
|
||||
# expected_quality (optional) — when present, the quality test
|
||||
# expected_quality (optional): when present, the quality test
|
||||
# asserts these limits against the
|
||||
# discovered video tracks
|
||||
# min_height (optional) — max video.height >= this
|
||||
# min_codecs (optional) — codec names that must be present
|
||||
# min_height (optional): max video.height >= this
|
||||
# min_codecs (optional): codec names that must be present
|
||||
# (AVC, HEVC, VC1, VP8, VP9, AV1)
|
||||
# min_ranges (optional) — range names that must be present
|
||||
# min_ranges (optional): range names that must be present
|
||||
# (SDR, HDR10, HDR10P, DV, HLG, HYBRID)
|
||||
# min_track_count (optional) — at least this many video tracks
|
||||
# min_track_count (optional): at least this many video tracks
|
||||
#
|
||||
# runs_download_test (optional, default false) — when true, the
|
||||
# runs_download_test (optional, default false): when true, the
|
||||
# download smoke test
|
||||
# runs for this service
|
||||
# runs_license_test (optional, default false) — when true, the
|
||||
# runs_license_test (optional, default false): when true, the
|
||||
# license test runs for
|
||||
# this service
|
||||
# license_drm (optional, default "widevine") — DRM type the
|
||||
# license_drm (optional, default "widevine"): DRM type the
|
||||
# license test
|
||||
# requests ("widevine"
|
||||
# or "playready")
|
||||
# license_quality (optional, default 1080) — pixel height of the
|
||||
# license_quality (optional, default 1080): pixel height of the
|
||||
# track the license test
|
||||
# targets
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ For any fixture service with ``runs_download_test: true``, this test:
|
||||
4. Fetches the manifest URL with the resolved headers.
|
||||
5. Asserts the body is non-empty and looks like DASH/HLS.
|
||||
|
||||
It does NOT decrypt or mux — that requires a full local CDM. It proves
|
||||
It does NOT decrypt or mux; that requires a full local CDM. It proves
|
||||
the end-to-end pipeline up to CDN reachability for the selected quality.
|
||||
"""
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ def create_session(http_session, server_url: str, service: str, title_id: str) -
|
||||
payload = {
|
||||
"service": service,
|
||||
"title_id": title_id,
|
||||
# Quality knobs — send enum names the server's session_create parser accepts.
|
||||
# Quality knobs: send enum names the server's session_create parser accepts.
|
||||
"range_": ["SDR", "HDR10", "DV"],
|
||||
"vcodec": ["AVC", "HEVC"],
|
||||
"best_available": True,
|
||||
@@ -115,13 +115,13 @@ def test_track_quality_meets_expected(http_session, server_url: str, service_cas
|
||||
tracks = r.json()
|
||||
video = tracks.get("video") or []
|
||||
summary = summarize(video)
|
||||
# Print summary even when test passes — discovery aid.
|
||||
# Print summary even when the test passes, as a 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}")
|
||||
pytest.skip(f"{service}: no expected_quality in fixtures.yaml; discovered={summary}")
|
||||
|
||||
if "min_height" in expected:
|
||||
assert summary["max_height"] >= expected["min_height"], (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Unit tests for DownloadJob + DownloadQueueManager state machine.
|
||||
|
||||
These tests focus on the queue manager's data layer (create/get/list/cancel/
|
||||
cleanup/serialize) — they do not exercise the actual subprocess download path.
|
||||
cleanup/serialize); they do not exercise the actual subprocess download path.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -47,7 +47,7 @@ def test_api_error_default_http_status_per_code() -> None:
|
||||
APIErrorCode.NETWORK_ERROR: 503,
|
||||
APIErrorCode.SERVICE_UNAVAILABLE: 503,
|
||||
}
|
||||
# Lock that the cases above are the *complete* live set — a new/removed code trips this.
|
||||
# Lock that the cases above are the *complete* live set; a new/removed code trips this.
|
||||
assert {c.name for c in cases} == {c.name for c in APIErrorCode}
|
||||
for code, expected in cases.items():
|
||||
assert APIError(code, "x").http_status == expected, code
|
||||
@@ -142,7 +142,7 @@ def test_handle_api_exception_categorizes_generic() -> None:
|
||||
assert body["error_code"] == "NETWORK_ERROR"
|
||||
|
||||
|
||||
# Locks the FULL observable response — (http status, error_code, retryable) — that
|
||||
# Locks the FULL observable response (http status, error_code, retryable) that
|
||||
# a client sees for a generic (non-APIError) exception flowing through the
|
||||
# handle_api_exception funnel that every route's `except Exception` path calls.
|
||||
# This is the equivalence reference: after the categorize_exception collapse,
|
||||
|
||||
@@ -190,5 +190,5 @@ async def test_auth_middleware_accepts_known_key(make_app, aiohttp_client) -> No
|
||||
app["config"]["users"]["good-key"] = {"devices": []}
|
||||
client = await aiohttp_client(app)
|
||||
resp = await client.get("/api/session/nonexistent", headers={"X-Secret-Key": "good-key"})
|
||||
# Auth passed; handler then 404s the session — anything other than 401 is fine here.
|
||||
# Auth passed; handler then 404s the session; anything other than 401 is fine here.
|
||||
assert resp.status != 401
|
||||
|
||||
Reference in New Issue
Block a user