Files
gplan-2/tools/coverage_receipts.py
T
DKChan bb9ce8a117 批2(QA harness·B覆盖总览面板+本体枚举器): 义务层=tools/extract_capability_list.py(README能力清单四列表按旅程段解析,7段103行,用例指针剥反引号); 执行层=tools/coverage_receipts.py六源回执明细化(冒烟/单元集成/系统/探针/渲染/静态各一.js,名×结果×计数粒度,日志缺失=not_run亮灰不冒充绿)+run_all_gates.sh末尾挂点(观测件非判定件,解析失败打红字不翻闸判)+probe_manifest.js(面板file://无法列目录,按现存登记文件落清单); 机器义务双源之本体源=tools/coverage_enum.py(两扫描器+比对:.tres行集/schema词表字段/tscn按钮/gui_input自绘类/屏模态清单×extract_checks断言文料,865实体/缺口212); 呈现层=tools/coverage_report.html(四节:六层闸况/能力行×六层灯按旅程段/控件下钻交互源+渲染golden展示源/本体对账缺口×探针命中降级探针承载;状态灯四态+数据时间戳陈旧可见+完备性边界页眉;JSONP直载file://即开); 豁免=tools/coverage_exemptions.js人工维护母版; 登记件扩seen面(场景树全量BaseButton+db布局表hotspot_key公共面枚举,seen-hit=存在但零真输入覆盖缺口数据源)。读数: 两探针窄跑绿(screen_flow 19命中/116存在,ui_click 27命中/91存在——停手钮/树购买重建代等真缺口在册,弃图刷新确认链走视图方法属衔接条目余项); 六源明细化对真实闸日志抽核(unit82套件/系统69检查/探针10段/静态13条/golden逐屏L1-L3); 面板浏览器真实入口四节渲染验过; 全量档闸【验收通过】无新增红(exit=0,回执commit 07d4977f,六源明细化在闸内实跑=挂点真路径验证)
2026-11-06 03:21:10 +08:00

185 lines
8.3 KiB
Python
Raw 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.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
r"""coverage_receipts.py — 六源回执明细化(B 覆盖总览面板·执行层)
各闸跑批把当场日志落 `.gates/*.log`;本工具在跑批末尾被闸壳调用一次,把六层日志解析成
「名×结果×计数」明细,按闸各写一个 JSONP .js 到 `.gates/coverage/`,供
tools/coverage_report.html 经 <script src> 直载。
六层与日志源(层键=面板消费键,层名=呈现名):
smoke 冒烟测试 smoke_boot.log 摘要「N 项检查,M 项失败」
unit 单元与集成测试 unit.log 每套件「[run_all] PASS/FAIL res://…(exit=N)」
system 系统测试 system_test.log 摘要「N 项检查,M 项失败」
probe 场景探针 probe.log 每探针段「======== 名(段)========」+该段 PASS/FAIL 行
render 渲染闸 render.log golden 清单与对比结果
static 静态扫描 static_scan.log 每个 S 系条目「[static_scan] S# ok/FAIL(…)」
判据口径:
- 明细粒度=「名×结果×计数」,不记逐条断言文本(契约 §4 成本口径);
- 日志缺失=该层 {"result": "not_run"}——面板亮灰灯,不冒充绿;
- 本工具失败不翻转闸判定(观测件非判定件),但必须打红字——静默丢数据等于断言蒸发。
用法: python tools/coverage_receipts.py [--stamp-dir <.gates 路径>]
产出: <stamp-dir>/coverage/gate_<层键>.js × 6
"""
from __future__ import annotations
import argparse
import datetime
import json
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
DEFAULT_STAMP = ROOT / "game" / "tests" / ".gates"
SUMMARY_RE = re.compile(r"摘要[::]\s*\[?[^\]]*\]?\s*摘要[::]\s*(\d+)\s*项检查,(\d+)\s*项失败")
SUITE_RE = re.compile(r"\[run_all\] (PASS|FAIL) (res://\S+?\.gd)(?:\s*\(exit=(-?\d+)\))?")
PROBE_SECTION_RE = re.compile(r"^======== (\S+)(?:\s+(.+?))? ========")
PROBE_RESULT_RE = re.compile(r"^\[[^\]]+\] (PASS|FAIL)")
GOLDEN_RE = re.compile(r"\[render_gate\] \[golden\] (PASS|FAIL)((\d+) 屏黄金图")
GOLDEN_CROP_RE = re.compile(r"\[render_gate\] \[golden_crop\] (\S+?) —")
STATIC_RE = re.compile(r"^\[static_scan\] (S\d+(?:/S\d+)*)\s+(ok|FAIL)", re.M)
STATIC_GDSTYLE_RE = re.compile(r"^\[gdstyle_gate\] (ok|FAIL)", re.M)
def read(stamp: Path, name: str) -> str:
p = stamp / name
if not p.is_file():
return ""
return p.read_text(encoding="utf-8", errors="replace")
def parse_summary_layer(log: str) -> dict:
if not log:
return {"result": "not_run"}
m = SUMMARY_RE.search(log)
verdict = "绿" if "[run_smoke] PASS" in log or "[run_system_test] PASS" in log else (
"红" if ("FAIL" in log or "摘要" in log and m and int(m.group(2)) > 0) else "灰")
if m:
return {"result": verdict, "checks": int(m.group(1)), "fails": int(m.group(2))}
return {"result": verdict}
def parse_unit(log: str) -> dict:
if not log:
return {"result": "not_run"}
items = [{"name": m.group(2), "result": m.group(1), "exit": int(m.group(3) or 0)}
for m in SUITE_RE.finditer(log)]
red = [i for i in items if i["result"] == "FAIL"]
verdict = "绿" if "[run_all] all green" in log and not red else ("红" if red or "FAIL" in log else "灰")
return {"result": verdict, "suites_total": len(items), "items": items}
def parse_probe(log: str) -> dict:
if not log:
return {"result": "not_run"}
items: list[dict] = []
cur: dict | None = None
for line in log.splitlines():
sm = PROBE_SECTION_RE.match(line.strip())
if sm:
seg = (sm.group(2) or "").strip()
cur = {"name": sm.group(1), "segment": seg, "result": "灰"}
items.append(cur)
continue
rm = PROBE_RESULT_RE.match(line.strip())
if rm and cur is not None and cur["result"] == "灰":
cur["result"] = rm.group(1)
judged = [i for i in items if i["result"] != "灰"]
verdict = "红" if (any(i["result"] == "FAIL" for i in judged) or "[probe_gate] 全绿" not in log) else "绿"
return {"result": verdict, "probes_total": len(judged), "items": items}
def parse_render(log: str) -> dict:
if not log:
return {"result": "not_run"}
items: list[dict] = []
gm = GOLDEN_RE.search(log)
if gm:
items.append({"name": "golden 逐像素对比", "result": gm.group(1), "screens": int(gm.group(2))})
screens: list[str] = []
for m in GOLDEN_CROP_RE.finditer(log):
if m.group(1) not in screens:
screens.append(m.group(1))
if screens:
items.append({"name": "golden 逐屏裁剪", "result": "PASS", "screens": screens})
verdict = "红" if "FAIL" in log or "[render_gate] 全绿" not in log else "绿"
return {"result": verdict, "items": items}
def parse_static(log: str) -> dict:
if not log:
return {"result": "not_run"}
items = [{"name": m.group(1), "result": m.group(2)} for m in STATIC_RE.finditer(log)]
gm = STATIC_GDSTYLE_RE.search(log)
if gm:
items.insert(0, {"name": "gdstyle", "result": gm.group(1)})
red = [i for i in items if i["result"] != "ok"]
verdict = "红" if red or "FAIL" in log else "绿"
return {"result": verdict, "items_total": len(items), "items": items}
LAYERS = [
("smoke", "冒烟测试", "smoke_boot.log", parse_summary_layer),
("unit", "单元与集成测试", "unit.log", parse_unit),
("system", "系统测试", "system_test.log", parse_summary_layer),
("probe", "场景探针", "probe.log", parse_probe),
("render", "渲染闸", "render.log", parse_render),
("static", "静态扫描", "static_scan.log", parse_static),
]
def main() -> int:
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
ap = argparse.ArgumentParser(description="六源回执明细化(B 面板·执行层)")
ap.add_argument("--stamp-dir", default=str(DEFAULT_STAMP))
args = ap.parse_args()
stamp = Path(args.stamp_dir)
out_dir = stamp / "coverage"
out_dir.mkdir(parents=True, exist_ok=True)
failed = False
wrote = 0
for key, label, log_name, fn in LAYERS:
try:
detail = fn(read(stamp, log_name))
detail["meta"] = {
"layer": key,
"label": label,
"source_log": log_name,
"generated_at": datetime.datetime.now().isoformat(timespec="seconds"),
}
payload = {"receipt": detail}
js = "/* 闸回执明细化——coverage_receipts.py 随每次跑批覆写,人工修订无效。 */\n"
js += "window.__QA_COVERAGE__ = window.__QA_COVERAGE__ || {};\n"
js += "window.__QA_COVERAGE__.gate_receipts = window.__QA_COVERAGE__.gate_receipts || {};\n"
js += "window.__QA_COVERAGE__.gate_receipts[%s] = %s;\n" % (
json.dumps(key), json.dumps(payload, ensure_ascii=False, indent=2))
(out_dir / ("gate_%s.js" % key)).write_text(js, encoding="utf-8", newline="\n")
wrote += 1
print("[receipts] %s(%s)=%s" % (label, log_name, detail.get("result", "?")))
except Exception as exc: # noqa: BLE001 —— 观测件逐层容错,红字可见不静默
failed = True
print("[receipts] 红:%s 层解析失败(%s):%r" % (label, log_name, exc))
if wrote == 0:
print("[receipts] 红:六层明细一件未落盘——禁静默")
return 1
# 探针登记文件清单(B 面板 file:// 无法列目录——按现存 probe_hits_*.js 落 manifest 供 <script src>)
probe_files = sorted(p.name for p in out_dir.glob("probe_hits_*.js"))
manifest = {
"meta": {"generated_at": datetime.datetime.now().isoformat(timespec="seconds")},
"probes": probe_files,
}
mjs = "/* 探针登记清单——coverage_receipts.py 随跑批按现存文件覆写,勿手编。 */\n"
mjs += "window.__QA_COVERAGE__ = window.__QA_COVERAGE__ || {};\n"
mjs += "window.__QA_COVERAGE__.probe_manifest = %s;\n" % json.dumps(manifest, ensure_ascii=False)
(out_dir / "probe_manifest.js").write_text(mjs, encoding="utf-8", newline="\n")
print("[receipts] 探针登记清单 %d 件 → probe_manifest.js" % len(probe_files))
return 1 if failed else 0
if __name__ == "__main__":
raise SystemExit(main())