Files
MoviePilot/tests/test_architecture_contract_baseline.py
T

157 lines
5.7 KiB
Python

import json
import subprocess
import sys
from pathlib import Path
from app.schemas.types import ChainEventType, EventType
PROJECT_ROOT = Path(__file__).parents[1]
BASELINE_ROOT = PROJECT_ROOT / "tests" / "fixtures" / "architecture"
def test_architecture_contract_baselines_match_current_source():
"""宿主依赖图和公开运行契约变化必须显式刷新基线。"""
result = subprocess.run(
[sys.executable, "scripts/architecture/baseline.py", "--check"],
cwd=PROJECT_ROOT,
capture_output=True,
text=True,
check=False,
)
assert result.returncode == 0, result.stderr
def test_official_plugin_baseline_records_external_source():
"""官方插件快照必须绑定独立仓提交,且不得引用宿主插件副本。"""
baseline_path = BASELINE_ROOT / "official-plugin-baseline.json"
baseline = json.loads(baseline_path.read_text(encoding="utf-8"))
assert baseline["source"]["repository"] == "MoviePilot-Plugins"
assert len(baseline["source"]["head"]) == 40
assert baseline["source"]["roots"] == ["plugins.v2", "plugins.v3"]
assert all(
not path.startswith("app/plugins/")
for contract in (*baseline["imports"].values(), *baseline["hooks"].values())
for path in contract["files"]
)
assert all(
not path.startswith("app/plugins/")
for path in baseline["api_routes"]
)
def test_official_discovery_plugins_explicitly_keep_host_page_envelope():
"""宿主探索页消费的官方插件 API 不得依赖动态路由隐式包装。"""
baseline_path = BASELINE_ROOT / "official-plugin-baseline.json"
baseline = json.loads(baseline_path.read_text(encoding="utf-8"))
routes = {
(path, route["path"]): route
for path, file_routes in baseline["api_routes"].items()
for route in file_routes
}
expected_paths = {
("plugins.v3/imdbsource/__init__.py", "/imdb-discover"),
("plugins.v3/imdbsource/__init__.py", "/imdb-top-250"),
("plugins.v3/imdbsource/__init__.py", "/imdb-trending"),
("plugins.v3/imdbsource/__init__.py", "/trending"),
("plugins.v3/tvdbdiscover/__init__.py", "/tvdb_discover"),
}
for route_key in expected_paths:
route = routes[route_key]
assert route["response_model"] == "schemas.Response[List[schemas.MediaInfo]]"
assert route["endpoint_return"] == "schemas.Response[List[schemas.MediaInfo]]"
def test_startup_performance_baseline_records_all_cold_import_targets():
"""启动性能基线必须包含关键入口的可比较冷导入采样。"""
baseline_path = BASELINE_ROOT / "startup-performance-baseline.json"
baseline = json.loads(baseline_path.read_text(encoding="utf-8"))
assert baseline["repeat"] >= 3
assert set(baseline["targets"]) == {
"app.startup.lifecycle",
"app.factory",
"app.main",
}
for contract in baseline["targets"].values():
assert len(contract["samples_ms"]) == baseline["repeat"]
assert contract["min_ms"] <= contract["median_ms"] <= contract["max_ms"]
assert contract["loaded_module_count"] > 0
def test_startup_performance_baseline_records_normal_and_safe_lifecycle_resources():
"""非功能基线必须同时记录正常/安全模式和隔离资源增量。"""
baseline_path = BASELINE_ROOT / "startup-performance-baseline.json"
baseline = json.loads(baseline_path.read_text(encoding="utf-8"))
lifecycle = baseline["lifecycle"]
assert "no-op" in lifecycle["scope"]
assert set(lifecycle["modes"]) == {"normal", "safe"}
normal = lifecycle["modes"]["normal"]
safe = lifecycle["modes"]["safe"]
assert normal["enabled_component_count"] > safe["enabled_component_count"]
for mode in (normal, safe):
assert len(mode["samples"]) == baseline["repeat"]
for sample in mode["samples"]:
assert sample["threads_after"] == sample["threads_before"]
assert sample["tasks_after"] == sample["tasks_before"]
assert sample["database_connections_started"] == 0
assert sample["stage_ms"]
def test_schema_export_manifest_matches_current_modules():
"""Schema 公开符号或冲突来源变化必须显式刷新生成清单。"""
result = subprocess.run(
[sys.executable, "scripts/schema/exports.py", "--check"],
cwd=PROJECT_ROOT,
capture_output=True,
text=True,
check=False,
)
assert result.returncode == 0, result.stdout + result.stderr
def test_schema_root_import_does_not_eagerly_load_schema_graph():
"""仅导入 schema 根包时不得加载任一业务 schema 子模块。"""
script = """
import sys
import app.schemas
loaded = sorted(
name
for name in sys.modules
if name.startswith('app.schemas.') and name != 'app.schemas.exports'
)
assert not loaded, loaded
assert len(app.schemas.__all__) >= 400
"""
result = subprocess.run(
[sys.executable, "-c", script],
cwd=PROJECT_ROOT,
capture_output=True,
text=True,
check=False,
)
assert result.returncode == 0, result.stderr
def test_event_contract_baseline_covers_every_public_event_enum() -> None:
"""事件生产者/消费者快照必须覆盖全部广播和链式事件枚举。"""
baseline_path = BASELINE_ROOT / "runtime-contract-baseline.json"
baseline = json.loads(baseline_path.read_text(encoding="utf-8"))
events = baseline["events"]
expected = {
*(f"EventType.{member.name}" for member in EventType),
*(f"ChainEventType.{member.name}" for member in ChainEventType),
}
assert set(events["events"]) == expected
assert events["event_count"] == len(expected)
assert events["producer_count"] > 0
assert events["consumer_count"] > 0