批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,六源明细化在闸内实跑=挂点真路径验证)

This commit is contained in:
DKChan committed 2026-11-06 03:21:10 +08:00
1 parent 07d4977f41
commit bb9ce8a117
7 files changed
+745

No files matched your search

+184
View File
@@ -0,0 +1,184 @@
#!/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())