AutoTeam-F / tests /static /test_playwright_hygiene.py
ZRainbow's picture
feat(round-4 infra): docker-guard + playwright-hardening + 调研/PRD/SPEC 文档基线
d9d9e44
Raw
History Blame Contribute Delete
3.68 kB
"""SPEC-4 §4: Playwright import + async/sync 分离静态守卫。
通过 AST 扫描 src/autoteam/**/*.py,断言:
- 不出现 from playwright.async_api ...
- async def 函数体内不引用任何 playwright 符号
- playwright.sync_api 导入仅限白名单
- 已豁免的文件必须显式登记在 expected_exempt 集合
"""
from __future__ import annotations
import ast
from pathlib import Path
from autoteam._playwright_guard import (
ALLOWED_SYNC_NAMES,
EXEMPTION_MARKER,
FORBIDDEN_MODULES,
)
SRC_ROOT = Path(__file__).resolve().parents[2] / "src" / "autoteam"
PLAYWRIGHT_NAMESPACE = ALLOWED_SYNC_NAMES | {"async_playwright"}
def _is_exempt(path: Path) -> bool:
"""文件首 5 行内出现 EXEMPTION_MARKER 即视为豁免。"""
try:
head = "\n".join(path.read_text(encoding="utf-8").splitlines()[:5])
except OSError:
return False
return EXEMPTION_MARKER in head
def _iter_py_files():
for path in SRC_ROOT.rglob("*.py"):
if not _is_exempt(path):
yield path
def _parse(path: Path) -> ast.AST:
return ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
def test_no_forbidden_module_import():
"""A1+A2: 禁止 from playwright.async_api / import playwright.async_api"""
bad: list[str] = []
for path in _iter_py_files():
tree = _parse(path)
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom) and node.module in FORBIDDEN_MODULES:
bad.append(f"{path}:{node.lineno} from {node.module}")
elif isinstance(node, ast.Import):
for alias in node.names:
if alias.name in FORBIDDEN_MODULES:
bad.append(f"{path}:{node.lineno} import {alias.name}")
assert not bad, "禁止使用 playwright.async_api:\n" + "\n".join(bad)
def test_no_playwright_in_async_def():
"""A3: async def 函数体内不允许 playwright 符号"""
bad: list[str] = []
for path in _iter_py_files():
tree = _parse(path)
for fn in ast.walk(tree):
if not isinstance(fn, ast.AsyncFunctionDef):
continue
for child in ast.walk(fn):
name = getattr(child, "id", None) or getattr(child, "attr", None)
if name in PLAYWRIGHT_NAMESPACE:
bad.append(
f"{path}:{getattr(child, 'lineno', fn.lineno)} "
f"async def {fn.name} 内引用了 {name}"
)
assert not bad, "async def 函数体内禁止 playwright:\n" + "\n".join(bad)
def test_sync_import_whitelist():
"""A4: playwright.sync_api 导入仅限 ALLOWED_SYNC_NAMES"""
bad: list[str] = []
for path in _iter_py_files():
tree = _parse(path)
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom) and node.module == "playwright.sync_api":
for alias in node.names:
if alias.name not in ALLOWED_SYNC_NAMES:
bad.append(f"{path}:{node.lineno} 非白名单符号 {alias.name}")
assert not bad, "sync_api 导入不在白名单:\n" + "\n".join(bad)
def test_exemption_list_intentional():
"""SPEC-4 §5: 豁免文件需登记理由,避免静默扩散"""
exempt = [p for p in SRC_ROOT.rglob("*.py") if _is_exempt(p)]
expected_exempt: set[str] = set()
actual = {p.relative_to(SRC_ROOT).as_posix() for p in exempt}
unexpected = actual - expected_exempt
assert not unexpected, (
f"未登记的豁免文件:{unexpected}。"
f"如需新增,先更新 SPEC-4 §5 + 本测试 expected_exempt 集合"
)