aps-agent/server/gateway/golden.py

78 lines
4.4 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.

# ============================================================
# 黄金测试看板数据源(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": []}