Files
MoviePilot/tests/test_scheduler_architecture.py
DDSRem 2de976f6b3 Merge pull request #7050 from DDSRem-Dev/codex-comment-migration
docs: 统一全项目中文注释和结构化契约说明
2026-10-17 19:26:29 +08:00

125 lines
3.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Scheduler 同名包职责与兼容边界门禁。
"""
import ast
from pathlib import Path
# 此用例覆盖运行时惰性导出,静态检查无法解析该公开入口。
from app.scheduler import Scheduler, SchedulerChain # pylint: disable=no-name-in-module
PROJECT_ROOT = Path(__file__).resolve().parents[1]
APP_ROOT = PROJECT_ROOT / "app"
SCHEDULER_ROOT = APP_ROOT / "scheduler"
def _tree(path: Path) -> ast.Module:
"""
解析指定 Python 文件。
"""
return ast.parse(path.read_text(encoding="utf-8-sig"), filename=str(path))
def test_scheduler_monolith_is_retired_and_package_owners_exist() -> None:
"""
旧单体必须消失,路线图声明的职责 owner 必须各有文件。
"""
assert not (APP_ROOT / "scheduler.py").exists()
assert {
"bridge.py",
"catalog.py",
"execution.py",
"facade.py",
"lifecycle.py",
"progress.py",
"reconcile.py",
"registry.py",
"services.py",
} <= {path.name for path in SCHEDULER_ROOT.glob("*.py")}
def test_scheduler_package_root_is_only_stable_legacy_abi() -> None:
"""
包根只延迟公开迁移前的两个类型,不重复承载实现。
"""
package_tree = _tree(SCHEDULER_ROOT / "__init__.py")
classes = [node.name for node in package_tree.body if isinstance(node, ast.ClassDef)]
assert classes == []
assert Scheduler.__module__ == "app.scheduler"
assert SchedulerChain.__module__ == "app.scheduler"
def test_scheduler_facade_remains_thin_and_composes_named_owners() -> None:
"""
Facade 只组合职责 owner,不重新吸收业务实现。
"""
facade_path = SCHEDULER_ROOT / "facade.py"
facade_tree = _tree(facade_path)
scheduler_class = next(
node
for node in facade_tree.body
if isinstance(node, ast.ClassDef) and node.name == "Scheduler"
)
methods = {
node.name
for node in scheduler_class.body
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
}
executable_statements = [
node
for node in ast.walk(facade_tree)
if isinstance(node, ast.stmt)
and not (
isinstance(node, ast.Expr)
and isinstance(node.value, ast.Constant)
and isinstance(node.value.value, str)
)
]
assert len(executable_statements) <= 70
assert methods == {
"__init__",
"_scheduler_services",
"configure_services",
"get_reload_name",
"on_plugin_reload",
"reset_runtime_bindings",
}
def test_scheduler_package_does_not_construct_chains() -> None:
"""
业务 Chain 只能由 startup 组合根构造后注入 Scheduler。
"""
violations: list[str] = []
for path in SCHEDULER_ROOT.glob("*.py"):
for node in ast.walk(_tree(path)):
if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Name):
continue
if node.func.id.endswith("Chain"):
violations.append(f"{path.name}:{node.lineno}:{node.func.id}")
assert violations == []
def test_host_code_does_not_import_scheduler_plugin_abi_root() -> None:
"""
除兼容包自身外,宿主只能使用 application 门面或 concrete 子模块。
"""
violations: list[str] = []
for path in APP_ROOT.rglob("*.py"):
if "plugins" in path.parts or path == SCHEDULER_ROOT / "__init__.py":
continue
for node in ast.walk(_tree(path)):
if isinstance(node, ast.ImportFrom) and node.module == "app.scheduler":
violations.append(f"{path.relative_to(PROJECT_ROOT)}:{node.lineno}")
if isinstance(node, ast.Import):
for alias in node.names:
if alias.name == "app.scheduler":
violations.append(
f"{path.relative_to(PROJECT_ROOT)}:{node.lineno}"
)
assert violations == []