批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

+158
View File
@@ -0,0 +1,158 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
r"""coverage_enum.py — 本体枚举器(游戏内容↔测试验证对账,B 面板·义务层双源之机器源)
两个文本扫描器+一次比对,全程不依赖跑游戏(交互命中维度除外——由探针覆盖登记另行承载):
扫描器 1(游戏本体枚举)——「游戏有什么」:
game/data/tables/*.tres 行集主键(`"xxx_id": "yyy"` 形态,逐表逐行)
game/data/schemas/*.gd 词表常量与字段名(const 块内的引号 token+var 字段名)
game/presentation/**/*.tscn BaseButton 族节点(scene+节点名)
game/presentation/**/*.gd _gui_input/_input 自绘命中类(脚本名)
game/presentation/views/*.gd 屏与模态清单(*_screen/*_overlay/*_panel 视图类)
扫描器 2(验证关联)——「验证了什么」:复用 tools/extract_checks.py 的断言静态提取
(unit/**+integration/**+system_test.gd 全量),实体 id 在「断言标签+cond+源文件+函数」
文料中出现即记一次关联。
比对=差集:本体存在、验证零关联=缺口清单,按类别分组、标首见批次(本工具生成时间)。
诚实边界(契约 §7 在案):grep 关联有假阳假阴——断言测了实体但文本没带 id=假缺口(人工核销
兜);文本带 id 但断言弱=假覆盖(oracle 改造堵)。推荐约定「断言文本含被测实体 id」提高精度,
不立规约。交互点(按钮/物件被点过没有)不在本工具判据内——那归探针真输入覆盖登记。
用法: python tools/coverage_enum.py [--out <路径>]
产出: game/tests/.gates/coverage/ontology_gaps.js(JSONP,B 面板消费)
"""
from __future__ import annotations
import argparse
import datetime
import json
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
GAME = ROOT / "game"
DEFAULT_OUT = GAME / "tests" / ".gates" / "coverage" / "ontology_gaps.js"
TRES_ID_RE = re.compile(r'"(\w+_id)":\s*"([^"]+)"')
SCHEMA_TOKEN_RE = re.compile(r'"([a-z][a-z0-9_]{2,40})"')
SCHEMA_VAR_RE = re.compile(r"^@?export\s+var\s+(\w+)\s*:", re.M)
TSCN_NODE_RE = re.compile(r'\[node name="([^"]+)"[^>]*type="(\w*Button\w*)"')
GUI_INPUT_RE = re.compile(r"^func (_gui_input|_input)\(", re.M)
VIEW_KIND_RE = re.compile(r"(screen|overlay|panel)$")
def scan_tables() -> list[dict]:
out: list[dict] = []
tables = GAME / "data" / "tables"
for f in sorted(tables.glob("*.tres")):
text = f.read_text(encoding="utf-8", errors="replace")
seen: set[tuple[str, str]] = set()
for m in TRES_ID_RE.finditer(text):
key = (m.group(1), m.group(2))
if key not in seen:
seen.add(key)
out.append({"category": "表行:%s" % f.stem, "id": m.group(2), "id_field": m.group(1)})
return out
def scan_schemas() -> list[dict]:
out: list[dict] = []
schemas = GAME / "data" / "schemas"
for f in sorted(schemas.glob("*.gd")):
text = f.read_text(encoding="utf-8", errors="replace")
tokens = set(SCHEMA_TOKEN_RE.findall(text))
fields = set(SCHEMA_VAR_RE.findall(text))
for t in sorted(tokens):
out.append({"category": "词表/枚值:%s" % f.stem, "id": t, "id_field": "token"})
for fl in sorted(fields):
out.append({"category": "schema字段:%s" % f.stem, "id": fl, "id_field": "field"})
return out
def scan_tscn_buttons() -> list[dict]:
out: list[dict] = []
for f in sorted(GAME.glob("presentation/**/*.tscn")):
text = f.read_text(encoding="utf-8", errors="replace")
for m in TSCN_NODE_RE.finditer(text):
out.append({"category": "场景按钮", "id": "%s:%s" % (f.name, m.group(1)), "id_field": m.group(2)})
return out
def scan_gui_input_classes() -> list[dict]:
out: list[dict] = []
for f in sorted(GAME.glob("presentation/**/*.gd")):
text = f.read_text(encoding="utf-8", errors="replace")
kinds = set(GUI_INPUT_RE.findall(text))
for k in kinds:
out.append({"category": "自绘命中类", "id": f.stem, "id_field": k})
break # 一类一行(一个脚本同具两面也只记一次)
return out
def scan_screens() -> list[dict]:
out: list[dict] = []
for f in sorted((GAME / "presentation" / "views").glob("*.gd")):
if VIEW_KIND_RE.search(f.stem):
out.append({"category": "屏与模态", "id": f.stem, "id_field": "view"})
return out
def assertion_corpus() -> tuple[list[dict], str]:
sys.path.insert(0, str(ROOT / "tools"))
import extract_checks as ec # noqa: PLC0415 —— 复用现役提取机制(契约 §7 指定扩面)
rows, _problems = ec.extract_all(ec.default_sources())
parts: list[str] = []
for r in rows:
parts.append("%s %s %s %s" % (r.get("src", ""), r.get("func", ""),
r.get("label_literal", ""), r.get("cond", "")))
return rows, "\n".join(parts)
def main() -> int:
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
ap = argparse.ArgumentParser(description="本体枚举器(游戏内容↔测试验证对账)")
ap.add_argument("--out", default=str(DEFAULT_OUT))
args = ap.parse_args()
entities = scan_tables() + scan_schemas() + scan_tscn_buttons() + scan_gui_input_classes() + scan_screens()
_rows, corpus = assertion_corpus()
categories: dict[str, dict] = {}
for ent in entities:
cat = categories.setdefault(ent["category"], {"total": 0, "matched": 0, "gaps": []})
cat["total"] += 1
if ent["id"] and ent["id"] in corpus:
cat["matched"] += 1
else:
cat["gaps"].append({"id": ent["id"], "id_field": ent.get("id_field", "")})
payload = {
"meta": {
"generated_at": datetime.datetime.now().isoformat(timespec="seconds"),
"entities_total": len(entities),
"corpus_rows": len(_rows),
"honest_boundary": "grep 关联有假阳假阴——文本没带 id 的假缺口人工核销;交互命中归探针真输入登记",
},
"categories": [
{"category": k, "total": v["total"], "matched": v["matched"], "gaps": v["gaps"]}
for k, v in sorted(categories.items())
],
}
out = Path(args.out)
out.parent.mkdir(parents=True, exist_ok=True)
js = "/* 本体对账清单——coverage_enum.py 随跑重生成,人工核销走豁免清单(coverage_exemptions.js),不落本文件。 */\n"
js += "window.__QA_COVERAGE__ = window.__QA_COVERAGE__ || {};\n"
js += "window.__QA_COVERAGE__.ontology = %s;\n" % json.dumps(payload, ensure_ascii=False, indent=2)
out.write_text(js, encoding="utf-8", newline="\n")
gap_total = sum(len(c["gaps"]) for c in payload["categories"])
print("[ontology] 本体实体 %d / 断言文料行 %d / 缺口 %d(%d 类)→ %s" % (
len(entities), len(_rows), gap_total, len(payload["categories"]), out))
for c in payload["categories"]:
print(" %s:%d 实体 / %d 缺口" % (c["category"], c["total"], len(c["gaps"])))
return 0
if __name__ == "__main__":
raise SystemExit(main())