批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

+129
View File
@@ -0,0 +1,129 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
r"""extract_capability_list.py — 能力清单义务层提取器(B 覆盖总览面板·义务层)
把 game/tests/README.md「能力清单」的四列表(能力|系统|真实入口|代表用例)按旅程段
解析成机器可读 JSONP .js,供 tools/coverage_report.html 经 <script src> 直载
(file:// 下 fetch 本地 json 被 CORS 拦——JSONP 是 B 面板全数据源的统一形态)。
用法:
python tools/extract_capability_list.py [--readme <路径>] [--out <路径>]
判据口径:
- 只认表头恰为「能力|系统|真实入口|代表用例」的四列表(覆盖域表等五列表不取);
- 旅程段标题形态=「**旅程段N:名称**」,段名进每行的 segment 字段;
- 代表用例列按「;」切分成用例指针数组(`文件:函数` 形态逐字保真);
- 本工具是义务层的静态投影,产出随 README 变更重跑即得——面板只读本产物,禁在面板里改义务。
产出(JSONP):
window.__QA_COVERAGE__.capability_list = {
"meta": {"generated_at", "source", "rows_total", "segments_total"},
"segments": [{"segment": 名称, "rows": [{"ability", "system", "entry", "cases": []}]}],
}
"""
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_README = ROOT / "game" / "tests" / "README.md"
DEFAULT_OUT = ROOT / "game" / "tests" / ".gates" / "coverage" / "capability_list.js"
SEGMENT_RE = re.compile(r"^\*\*旅程段[一二三四五六七八九十]+[::](.+?)\*\*\s*$")
def split_row(line: str) -> list[str]:
body = line.strip()
if body.startswith("|"):
body = body[1:]
if body.endswith("|"):
body = body[:-1]
return [c.strip() for c in body.split("|")]
def parse(readme: Path) -> dict:
lines = readme.read_text(encoding="utf-8").splitlines()
segments: list[dict] = []
cur_seg: str | None = None
cur_rows: list[dict] | None = None
in_capability_table = False
for line in lines:
seg = SEGMENT_RE.match(line.strip())
if seg:
if cur_seg is not None:
segments.append({"segment": cur_seg, "rows": cur_rows or []})
cur_seg = seg.group(1).strip()
cur_rows = []
in_capability_table = False
continue
if cur_seg is None:
continue
if not line.strip().startswith("|"):
in_capability_table = False
continue
cells = split_row(line)
if all(not set(c) - {"-", " ", ":"} for c in cells) and cells:
continue # 表头分隔行 |---|---|
if (len(cells) == 4 and cells[0] == "能力" and cells[1] == "系统"
and cells[2] == "真实入口" and cells[3].startswith("代表用例")):
in_capability_table = True
continue
if in_capability_table and len(cells) == 4:
ability, system, entry, cases = cells
cur_rows.append({
"ability": ability.strip("`"),
"system": system,
"entry": entry,
"cases": [c.strip().strip("`") for c in cases.split(";") if c.strip()],
})
if cur_seg is not None:
segments.append({"segment": cur_seg, "rows": cur_rows or []})
rows_total = sum(len(s["rows"]) for s in segments)
return {
"meta": {
"generated_at": datetime.datetime.now().isoformat(timespec="seconds"),
"source": str(readme),
"rows_total": rows_total,
"segments_total": len(segments),
},
"segments": segments,
}
def dump_js(payload: dict, out: Path) -> None:
out.parent.mkdir(parents=True, exist_ok=True)
js = "/* 能力清单义务层投影——由 extract_capability_list.py 生成,随 README 变更重跑;人工修订无效(义务源=README 能力清单)。 */\n"
js += "window.__QA_COVERAGE__ = window.__QA_COVERAGE__ || {};\n"
js += "window.__QA_COVERAGE__.capability_list = %s;\n" % json.dumps(payload, ensure_ascii=False, indent=2)
out.write_text(js, encoding="utf-8", newline="\n")
def main() -> int:
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
ap = argparse.ArgumentParser(description="能力清单义务层提取(B 面板·义务层)")
ap.add_argument("--readme", default=str(DEFAULT_README))
ap.add_argument("--out", default=str(DEFAULT_OUT))
args = ap.parse_args()
readme = Path(args.readme)
if not readme.is_file():
print("[capability] README 不存在:%s" % readme)
return 1
payload = parse(readme)
dump_js(payload, Path(args.out))
print("[capability] 义务层投影已落盘 %s(旅程段 %d / 能力行 %d)" % (
args.out, payload["meta"]["segments_total"], payload["meta"]["rows_total"]))
if payload["meta"]["rows_total"] == 0:
print("[capability] 红:能力行数为 0——表头或旅程段标题形态不匹配,禁当空义务面用")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())