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