mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-12 20:15:10 +00:00
Renames 314 defs and 41 module/class-level constants. Keeps 11 names underscored where the stripped form collides in scope, plus 5.5.0 shims for the three symbols external service repos call.
217 lines
6.7 KiB
Python
217 lines
6.7 KiB
Python
"""Shared fixtures + CLI flags for tests/remote/."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import socket
|
|
import subprocess
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
def pytest_configure(config: pytest.Config) -> None:
|
|
config.addinivalue_line("markers", "unit: fast, mocked tests (default)")
|
|
config.addinivalue_line("markers", "live: end-to-end tests against a running serve (opt-in via --live)")
|
|
config.addinivalue_line("markers", "slow: tests that hit real services and may take >10s")
|
|
|
|
|
|
def pytest_addoption(parser: pytest.Parser) -> None:
|
|
parser.addoption(
|
|
"--live",
|
|
action="store_true",
|
|
default=False,
|
|
help="Run e2e tests against a running 'unshackle serve' instance.",
|
|
)
|
|
parser.addoption(
|
|
"--server-url",
|
|
action="store",
|
|
default=os.environ.get("UNSHACKLE_SERVE_URL", ""),
|
|
help="Server URL for e2e tests. If empty, the suite will spawn its own serve "
|
|
"(via --spawn-serve, default on for --live). Override with $UNSHACKLE_SERVE_URL.",
|
|
)
|
|
parser.addoption(
|
|
"--secret-key",
|
|
action="store",
|
|
default=os.environ.get("UNSHACKLE_SECRET_KEY", ""),
|
|
help="X-Secret-Key for e2e tests. Empty for --no-key servers.",
|
|
)
|
|
parser.addoption(
|
|
"--services",
|
|
action="store",
|
|
default="",
|
|
help="Comma-separated service tags to run e2e against (default: all in fixtures.yaml).",
|
|
)
|
|
parser.addoption(
|
|
"--spawn-serve",
|
|
action="store",
|
|
default="auto",
|
|
choices=["auto", "always", "never"],
|
|
help="Whether to spawn 'unshackle serve' for e2e tests. "
|
|
"'auto' (default): spawn only when --server-url is empty. "
|
|
"'always': always spawn (kills any existing process is your problem). "
|
|
"'never': require an external serve.",
|
|
)
|
|
|
|
|
|
def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item]) -> None:
|
|
"""Skip live tests unless --live is passed."""
|
|
if config.getoption("--live"):
|
|
return
|
|
skip_live = pytest.mark.skip(reason="needs --live")
|
|
for item in items:
|
|
if "live" in item.keywords:
|
|
item.add_marker(skip_live)
|
|
|
|
|
|
def free_port() -> int:
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
s.bind(("127.0.0.1", 0))
|
|
return s.getsockname()[1]
|
|
|
|
|
|
def wait_for_health(url: str, timeout: float = 60.0) -> bool:
|
|
import requests
|
|
|
|
deadline = time.monotonic() + timeout
|
|
while time.monotonic() < deadline:
|
|
try:
|
|
r = requests.get(f"{url}/api/health", timeout=2)
|
|
if r.status_code == 200:
|
|
return True
|
|
except requests.RequestException:
|
|
pass
|
|
time.sleep(0.5)
|
|
return False
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def isolated_job_history(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Redirect job history writes to tmp_path so tests don't append to the server's api_history.jsonl."""
|
|
from unshackle.core.api import download_manager
|
|
|
|
monkeypatch.setattr(download_manager, "history_path", lambda: tmp_path / "api_history.jsonl")
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def secret_key(request: pytest.FixtureRequest) -> str:
|
|
return str(request.config.getoption("--secret-key"))
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def server_url(request: pytest.FixtureRequest):
|
|
"""Provide a base URL for the live serve.
|
|
|
|
- If --server-url is set: use it as-is (external server).
|
|
- Else (default) and --live is on: spawn 'unshackle serve' on a free
|
|
port, wait for /api/health, yield the URL, terminate at session end.
|
|
- Spawn mode controlled by --spawn-serve {auto, always, never}.
|
|
"""
|
|
cfg = request.config
|
|
explicit = str(cfg.getoption("--server-url")).rstrip("/")
|
|
mode = cfg.getoption("--spawn-serve")
|
|
is_live = cfg.getoption("--live")
|
|
|
|
if not is_live:
|
|
yield explicit or "http://localhost:8786"
|
|
return
|
|
|
|
if mode == "never":
|
|
if not explicit:
|
|
pytest.fail("--spawn-serve=never requires --server-url")
|
|
if not wait_for_health(explicit, timeout=10):
|
|
pytest.fail(f"External serve not reachable at {explicit}")
|
|
yield explicit
|
|
return
|
|
|
|
if mode == "auto" and explicit:
|
|
if not wait_for_health(explicit, timeout=10):
|
|
pytest.fail(f"External serve not reachable at {explicit}")
|
|
yield explicit
|
|
return
|
|
|
|
# Spawn our own serve.
|
|
port = free_port()
|
|
cmd = [
|
|
"uv",
|
|
"run",
|
|
"unshackle",
|
|
"serve",
|
|
"--host",
|
|
"127.0.0.1",
|
|
"--port",
|
|
str(port),
|
|
"--no-key",
|
|
"--remote-only",
|
|
]
|
|
proc = subprocess.Popen( # nosec B603
|
|
cmd,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
cwd=Path(__file__).resolve().parents[2],
|
|
)
|
|
url = f"http://127.0.0.1:{port}"
|
|
try:
|
|
if not wait_for_health(url, timeout=60):
|
|
proc.terminate()
|
|
pytest.fail(f"Spawned serve at {url} did not become healthy within 60s")
|
|
yield url
|
|
finally:
|
|
proc.terminate()
|
|
try:
|
|
proc.wait(timeout=10)
|
|
except subprocess.TimeoutExpired:
|
|
proc.kill()
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def selected_services(request: pytest.FixtureRequest) -> list[str]:
|
|
raw = str(request.config.getoption("--services")).strip()
|
|
return [s.strip().upper() for s in raw.split(",") if s.strip()] if raw else []
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def fixtures_dir() -> Path:
|
|
return Path(__file__).parent / "e2e" / "fixtures"
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def e2e_scenarios(fixtures_dir: Path, selected_services: list[str]) -> dict:
|
|
"""Load e2e service scenarios from fixtures.yaml.
|
|
|
|
See fixtures-example.yaml for the schema. fixtures.yaml is .gitignored
|
|
so each user keeps their own private scenario set locally.
|
|
"""
|
|
import yaml
|
|
|
|
path = fixtures_dir / "fixtures.yaml"
|
|
if not path.exists():
|
|
return {"services": {}}
|
|
data = yaml.safe_load(path.read_text()) or {}
|
|
services = data.get("services", {})
|
|
if selected_services:
|
|
services = {k: v for k, v in services.items() if k.upper() in selected_services}
|
|
data["services"] = services
|
|
return data
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def http_session(secret_key: str):
|
|
"""Plain requests.Session with X-Secret-Key wired in."""
|
|
import requests
|
|
|
|
s = requests.Session()
|
|
s.headers["User-Agent"] = "unshackle-tests/1.0"
|
|
if secret_key:
|
|
s.headers["X-Secret-Key"] = secret_key
|
|
return s
|
|
|
|
|
|
@pytest.fixture
|
|
def remote_client(server_url: str, secret_key: str):
|
|
"""RemoteClient pointed at the e2e serve instance."""
|
|
from unshackle.core.remote_service import RemoteClient
|
|
|
|
return RemoteClient(server_url=server_url, api_key=secret_key)
|