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