批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:
1 parent
07d4977f41
commit
bb9ce8a117
7 files changed
+745
No files matched your search
@@ -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())
|
||||
Reference in new issue
Block a user