aps-agent/server/gateway/golden.py

78 lines
4.4 KiB
Python
Raw Permalink Normal View History

2026-07-21 11:05:57 +08:00
# ============================================================
# 黄金测试看板数据源(moduleId: gateway-golden, 可重生 ✅)
# plan.md §6.10.3 重生中心:读缓存展示最近一次 pytest 结果;
# ?run=true 时在子进程同步重跑(测试自隔离 tmp_path,不碰生产数据)。
# 权力等级:读缓存 P0;run=true 触发跑测仍无业务副作用(只写缓存文件)。
# ============================================================
from __future__ import annotations # 前向类型引用
import json # 缓存序列化
import os # 路径
import re # pytest 输出解析
import subprocess # 子进程跑 pytest
import sys # 当前解释器路径
from datetime import datetime # 跑测时间戳
from typing import Any # 类型标注
from server.timeutil import fmt_dt # 时间格式化
# 缓存文件路径(与世界状态同目录;进 .gitignore 的数据区)
_CACHE_PATH = os.environ.get("APS_GOLDEN_CACHE", "server/data/golden_tests.json")
# pytest 逐用例行:tests/golden/test_x.py::test_name PASSED/FAILED
_CASE_RE = re.compile(r"(tests[/\\]golden[/\\]\S+?)::(\S+?)\s+(PASSED|FAILED|ERROR)")
def _read_cache() -> dict[str, Any] | None:
"""读最近一次跑测缓存;缺失/损坏返回 None。"""
try:
with open(_CACHE_PATH, "r", encoding="utf-8") as f: # 读缓存
return json.load(f) # 解析
except (FileNotFoundError, json.JSONDecodeError): # 无缓存或损坏
return None
def _write_cache(data: dict[str, Any]) -> None:
"""落盘缓存(覆盖写;非关键数据无需原子写)。"""
os.makedirs(os.path.dirname(_CACHE_PATH) or ".", exist_ok=True) # 确保目录
with open(_CACHE_PATH, "w", encoding="utf-8") as f: # 覆盖写
json.dump(data, f, ensure_ascii=False, indent=1) # 序列化
def run_golden() -> dict[str, Any]:
"""子进程同步跑 `pytest tests/golden -v --tb=no` 并解析结果(更新缓存)。"""
proc = subprocess.run( # 独立进程(测试用 tmp_path 自隔离)
[sys.executable, "-m", "pytest", "tests/golden", "-v", "--tb=no"], # -v 输出逐用例行(勿加 -q 会压掉)
capture_output=True, text=True, timeout=120, # 限时防挂
)
out = proc.stdout + proc.stderr # 合并输出
cases = [{ # 逐用例结果
"file": m.group(1).replace("\\", "/"), # 测试文件
"name": m.group(2), # 用例名
"outcome": m.group(3), # PASSED/FAILED/ERROR
} for m in _CASE_RE.finditer(out)]
passed = sum(1 for c in cases if c["outcome"] == "PASSED") # 通过数
failed = len(cases) - passed # 未通过数
data = { # 看板数据结构
"ranAt": fmt_dt(datetime.now()), # 跑测时间
"passed": passed, "failed": failed, "total": len(cases), # 汇总
"exitCode": proc.returncode, # pytest 退出码(0=全绿)
"cases": cases, # 用例明细
}
_write_cache(data) # 更新缓存
return data # 返回
def golden_status(run: bool = False) -> dict[str, Any]:
"""看板入口:run=true 重跑;否则读缓存(无缓存时返回未运行占位)。"""
if run: # 显式要求重跑
try:
return run_golden() # 跑测并返回
except Exception as exc: # 跑测失败(环境问题)
return {"ranAt": None, "passed": 0, "failed": 0, "total": 0,
"exitCode": -1, "cases": [], "error": str(exc)}
cached = _read_cache() # 读缓存
if cached: # 有缓存直接用
return cached
return {"ranAt": None, "passed": 0, "failed": 0, "total": 0, # 从未跑过的占位
"exitCode": None, "cases": []}