Files
MoviePilot/tests/test_complexity_gate.py

123 lines
4.4 KiB
Python

"""复杂度只降不增 ratchet 测试。"""
from pathlib import Path
from scripts.architecture.complexity import (
collect_complexity_v2,
compare_complexity,
compare_complexity_v2,
)
def _assignments(indent: str, count: int) -> str:
"""生成足够长的临时语句体,精确触发复杂度阈值。"""
return "\n".join(f"{indent}value_{index} = {index}" for index in range(count))
def test_complexity_ratchet_allows_removal_and_reduction() -> None:
"""既有超限入口被删除或缩短必须允许合并。"""
baseline = {
"api_endpoint": {"app/api/endpoints/a.py:large": 100},
"application_public": {"app/application/a.py:Service.run": 180},
"chain_public": {},
}
current = {
"api_endpoint": {"app/api/endpoints/a.py:large": 90},
"application_public": {},
"chain_public": {},
}
assert compare_complexity(baseline, current) == []
def test_complexity_ratchet_rejects_growth_and_new_oversize() -> None:
"""既有入口增长和新增超预算入口必须同时给出精确诊断。"""
baseline = {
"api_endpoint": {"app/api/endpoints/a.py:large": 100},
"application_public": {},
"chain_public": {},
}
current = {
"api_endpoint": {
"app/api/endpoints/a.py:large": 101,
"app/api/endpoints/b.py:new_endpoint": 81,
},
"application_public": {},
"chain_public": {},
}
problems = compare_complexity(baseline, current)
assert any("既有超限增长" in problem for problem in problems)
assert any("新增超限" in problem for problem in problems)
def test_complexity_v2_ratchet_checks_categories_outside_public_rules() -> None:
"""v2 基线必须实际比较私有方法、类和文件类别。"""
baseline = {"method": {"app/scheduler/a.py:Runner.run": 160}, "class": {}, "file": {}}
current = {
"method": {"app/scheduler/a.py:Runner.run": 161},
"class": {"app/scheduler/a.py:Runner": 600},
"file": {"app/scheduler/a.py": 1200},
}
problems = compare_complexity_v2(baseline, current)
assert any("method: 既有超限增长" in problem for problem in problems)
assert any("class: 新增超限" in problem for problem in problems)
assert any("file: 新增超限" in problem for problem in problems)
def test_complexity_v2_walks_dunder_match_trystar_and_nested_class_owners(
tmp_path: Path,
) -> None:
"""完整 AST 遍历必须覆盖 dunder、Match、TryStar 和函数内嵌套类。"""
scheduler = tmp_path / "app/scheduler"
scheduler.mkdir(parents=True)
(scheduler / "dunder.py").write_text(
"class Runner:\n"
" def __hidden(self):\n"
f"{_assignments(' ', 151)}\n",
encoding="utf-8",
)
(scheduler / "control_flow.py").write_text(
"def dispatch(value):\n"
" match value:\n"
" case 1:\n"
" def matched():\n"
f"{_assignments(' ', 151)}\n"
" return matched()\n\n"
"def guarded():\n"
" try:\n"
" raise ExceptionGroup('failure', [ValueError()])\n"
" except* ValueError:\n"
" def recovered():\n"
f"{_assignments(' ', 151)}\n"
" return recovered()\n",
encoding="utf-8",
)
nested_classes = (
"class Outer:\n"
" def first(self):\n"
" class Local:\n"
f"{_assignments(' ', 501)}\n\n"
" def second(self):\n"
" class Local:\n"
f"{_assignments(' ', 501)}\n"
)
(scheduler / "nested_classes.py").write_text(nested_classes, encoding="utf-8")
(scheduler / "conditional.py").write_text(
"if enabled:\n"
" class Conditional:\n"
f"{_assignments(' ', 501)}\n",
encoding="utf-8",
)
report = collect_complexity_v2(tmp_path)
assert "app/scheduler/dunder.py:Runner.__hidden" in report["method"]
assert "app/scheduler/control_flow.py:dispatch.matched" in report["method"]
assert "app/scheduler/control_flow.py:guarded.recovered" in report["method"]
assert "app/scheduler/conditional.py:Conditional" in report["class"]
assert "app/scheduler/nested_classes.py:Outer.first.Local" in report["class"]
assert "app/scheduler/nested_classes.py:Outer.second.Local" in report["class"]