159 lines
7.2 KiB
Python
159 lines
7.2 KiB
Python
#!/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())
|