Files
gplan-2/tools/coverage_receipts.py
T
DKChan 8b09b2c952 追红(远端 run 95 两条红转绿·修复车道): 红因1=判定级——能力审计 G 道「oracle 注记函数 401·红项 1」, 红项原文「game/probe/probe_coverage_recorder.gd:201 验证函数 dump_js 缺 # oracle 注记」(dump_js 体内三处 push_error( 命中 capability_audit.py:341 VERDICT_RE → 属须注记的验证函数, :386 判红); 上一批「判据出处注记 402 函数全库铺开」的跑绿读数靠主工作区一行未提交注记撑过, 未随批 commit, 故本地 strict 绿而远端干净检出必红; 本批在该文件补同一行注记(放法照仓内既有形态——函数定义行下一行顶格, 样本 camp_scene_layout_test.gd:13/:18, 指针 design/engineering/test-conventions.md:30 与主区工作稿逐字节同, cmp 已核)。红因2=非判定级但在日志里成串报红——tools 四处五 sites 用 Path.write_text(..., newline="\n"), 该关键字 Python 3.13+ 才有(本机 3.13.14 绿), Mac runner 的 python3 更老 → TypeError("write_text() got an unexpected keyword argument 'newline'") → 六层明细回执在远端一件未落盘, run 93/95 日志同报此错(挂点按设计是观测件不翻闸判, 故它不是 run 95 的失败主因, 但等于覆盖总览面板的六层明细一直是空的); 五处改 open(str(p), "w", encoding="utf-8", newline="\n") 写法(newline 形参 builtin open 自 3.0 起有, LF 纪律不丢), 覆盖面=coverage_enum.py:148/coverage_receipts.py:160/:179/extract_capability_list.py:104/extract_checks.py:588; 注: tools/t17_fanout2_rebuild.py:140 与 t5_build_templates.py:237 本来就是 open(..., newline=) 形态, 不属坏件未动。本步凭据(全部区内实跑): ①修复前在干净检出复现红——capability_audit --strict 出「401 注记·红项 1」并打出上述红项原文; ②修复后同件 exit=0「402 注记·红项 0」; ③四件 py_compile OK 且 grep 全仓 write_text(.*newline= 零残留; ④runner 语义实证——把 Path.write_text 垫片成 3.12 行为(newline 一律 TypeError)再跑 coverage_receipts.main(), 六层与 probe_manifest 全部正常落盘、无一行「[receipts] 红」, 产物字节核对 gate_probe/gate_render/gate_smoke.js 均 CRLF=0 LF=14(行尾纪律未破); ⑤run_all_gates --docs 验收通过无新增红(基线 9e81408e)。引擎层本地未跑: 区内 --premerge 被素材齐盘预检拦在冷检出态(缺 .godot/uid_cache.bin 与字体 .import 边车, 处置需从主区拷素材再 --editor --quit 刷导入), 本批五处改动零游戏行为面, 引擎全套件由主区合回时的 pre-merge-commit 前置检查跑, 全量档归合回后远端 CI
2026-11-06 23:47:28 +08:00

187 lines
8.4 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))
with open(str(out_dir / ("gate_%s.js" % key)), "w", encoding="utf-8", newline="\n") as fh:
fh.write(js)
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)
with open(str(out_dir / "probe_manifest.js"), "w", encoding="utf-8", newline="\n") as fh:
fh.write(mjs)
print("[receipts] 探针登记清单 %d 件 → probe_manifest.js" % len(probe_files))
return 1 if failed else 0
if __name__ == "__main__":
raise SystemExit(main())