mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-12 12:05:07 +00:00
Check a sequential download against the complete length whenever one is known, and settle the unknown-total case on the 416. An empty body of unknown length, a 206 with no complete length, and a 206 starting past the resume offset all passed the size checks before and were renamed into place. Four restart paths spent no attempt, so a host that always answered the same way cycled forever.
146 lines
5.3 KiB
Python
146 lines
5.3 KiB
Python
"""``dl.get_cdm`` builds each CDM once per run.
|
|
|
|
Regression: the callers ask for a CDM once per title, and the dedupe guard at the
|
|
call site compares object identity. No CDM class defines ``__eq__``, so the guard
|
|
was always true and every title paid a fresh build. A remote device costs an RSA
|
|
key pair and a blocking request to its host.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any, Optional
|
|
|
|
import pytest
|
|
|
|
import unshackle.core.cdm as cdm_mod
|
|
from unshackle.commands.dl import dl
|
|
from unshackle.core.cdm import loader
|
|
from unshackle.core.config import config
|
|
|
|
|
|
class StubCdm:
|
|
"""A distinct object per build, so a reused CDM is visible by identity."""
|
|
|
|
def __init__(self, cdm_name: str, service_name: str) -> None:
|
|
self.cdm_name = cdm_name
|
|
self.service_name = service_name
|
|
|
|
|
|
@pytest.fixture
|
|
def builds(monkeypatch) -> list[tuple[str, str]]:
|
|
"""Record every ``load_cdm`` call and hand back a fresh stub for each one."""
|
|
calls: list[tuple[str, str]] = []
|
|
|
|
def fake_load_cdm(cdm_name: str, *, service_name: str = "", vaults: Optional[Any] = None) -> StubCdm:
|
|
calls.append((cdm_name, service_name))
|
|
return StubCdm(cdm_name, service_name)
|
|
|
|
# The package resolves ``load_cdm`` lazily, so a ``setattr`` here would write the real
|
|
# function into the package dict on undo and shadow that lookup for the whole process.
|
|
monkeypatch.setitem(cdm_mod.__dict__, "load_cdm", fake_load_cdm)
|
|
return calls
|
|
|
|
|
|
def make_dl() -> dl:
|
|
# __new__ skips the CLI-driven __init__; get_cdm only needs these three.
|
|
instance = dl.__new__(dl)
|
|
instance.log = logging.getLogger("test_cdm_cache")
|
|
instance.cdm_override = None
|
|
instance.vaults = None
|
|
return instance
|
|
|
|
|
|
def test_repeated_calls_build_the_cdm_once(builds, monkeypatch) -> None:
|
|
monkeypatch.setattr(config, "cdm", {"EXAMPLE": "device_a"})
|
|
runner = make_dl()
|
|
|
|
first = runner.get_cdm("EXAMPLE", "default")
|
|
for _ in range(4):
|
|
assert runner.get_cdm("EXAMPLE", "default") is first
|
|
|
|
assert builds == [("device_a", "EXAMPLE")]
|
|
|
|
|
|
def test_the_title_loop_guard_sees_the_same_object(builds, monkeypatch) -> None:
|
|
"""The call site keeps its CDM when ``get_cdm`` returns what it already holds."""
|
|
monkeypatch.setattr(config, "cdm", {"EXAMPLE": "device_a"})
|
|
runner = make_dl()
|
|
|
|
held = runner.get_cdm("EXAMPLE", "default")
|
|
for _ in range(3):
|
|
quality_based_cdm = runner.get_cdm("EXAMPLE", "default", drm="widevine", quality=2160)
|
|
assert not (quality_based_cdm and quality_based_cdm != held)
|
|
|
|
assert len(builds) == 1
|
|
|
|
|
|
def test_a_different_service_gets_its_own_cdm(builds, monkeypatch) -> None:
|
|
"""A CDM is built with its ``service_name``, so two services must not share one."""
|
|
monkeypatch.setattr(config, "cdm", {"EXAMPLE": "device_a", "OTHER": "device_a"})
|
|
runner = make_dl()
|
|
|
|
example = runner.get_cdm("EXAMPLE", "default")
|
|
other = runner.get_cdm("OTHER", "default")
|
|
|
|
assert example is not other
|
|
assert builds == [("device_a", "EXAMPLE"), ("device_a", "OTHER")]
|
|
|
|
|
|
def test_a_different_profile_gets_its_own_cdm(builds, monkeypatch) -> None:
|
|
monkeypatch.setattr(config, "cdm", {"EXAMPLE": {"one": "device_one", "two": "device_two"}})
|
|
runner = make_dl()
|
|
|
|
one = runner.get_cdm("EXAMPLE", "one")
|
|
two = runner.get_cdm("EXAMPLE", "two")
|
|
|
|
assert one is not two
|
|
assert runner.get_cdm("EXAMPLE", "one") is one
|
|
assert builds == [("device_one", "EXAMPLE"), ("device_two", "EXAMPLE")]
|
|
|
|
|
|
def test_quality_selection_reuses_the_device_it_resolves_to(builds, monkeypatch) -> None:
|
|
"""Two spellings of the same device share one CDM; a different device does not."""
|
|
monkeypatch.setattr(config, "cdm", {"EXAMPLE": {">=1080": "device_hd", "<1080": "device_sd"}})
|
|
runner = make_dl()
|
|
|
|
hd = runner.get_cdm("EXAMPLE", "default", quality=2160)
|
|
assert runner.get_cdm("EXAMPLE", "default", quality=1080) is hd
|
|
sd = runner.get_cdm("EXAMPLE", "default", quality=720)
|
|
|
|
assert sd is not hd
|
|
assert builds == [("device_hd", "EXAMPLE"), ("device_sd", "EXAMPLE")]
|
|
|
|
|
|
def test_an_override_is_cached_under_the_name_it_resolves_to(builds, monkeypatch) -> None:
|
|
monkeypatch.setattr(config, "cdm", {"EXAMPLE": "device_a"})
|
|
runner = make_dl()
|
|
runner.cdm_override = "device_b"
|
|
|
|
override = runner.get_cdm("EXAMPLE", "default")
|
|
|
|
assert runner.get_cdm("EXAMPLE", "default") is override
|
|
assert builds == [("device_b", "EXAMPLE")]
|
|
|
|
|
|
@pytest.fixture
|
|
def leaked_shadow(monkeypatch) -> None:
|
|
"""Reproduce the residue that a ``setattr`` on the CDM package leaves behind.
|
|
|
|
``load_cdm`` reaches the package only through its lazy ``__getattr__``. A ``setattr`` on
|
|
the package writes the real function into the package dict when it undoes, which shadows
|
|
``__getattr__`` for the rest of the process. Two remote tests do exactly that.
|
|
"""
|
|
monkeypatch.setitem(cdm_mod.__dict__, "load_cdm", loader.load_cdm)
|
|
|
|
|
|
def test_a_shadowed_package_attribute_still_reaches_the_stub(leaked_shadow, builds, monkeypatch) -> None:
|
|
"""The stub intercepts ``get_cdm`` whatever an earlier test left on the CDM package."""
|
|
monkeypatch.setattr(config, "cdm", {"EXAMPLE": "device_a"})
|
|
runner = make_dl()
|
|
|
|
cdm = runner.get_cdm("EXAMPLE", "default")
|
|
|
|
assert isinstance(cdm, StubCdm)
|
|
assert builds == [("device_a", "EXAMPLE")]
|