Files
gplan-2/tools/coverage_enum.py
T

176 lines
8.3 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_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 但断言弱=假覆盖(判据改造堵)。推荐约定「断言文本含被测实体 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)$")
EXTENDS_RE = re.compile(r"^extends\s+(\w+)", re.M)
CLASS_NAME_RE = re.compile(r"^class_name\s+(\w+)", re.M)
# 屏与模态按实体本相归类(QA覆盖余量收口批 2026-11-07 用户裁):
VIEW_NON_SCENE_BASES = {"RefCounted", "Resource"} # 绘制域件/数据件出圈(如 cell_mark_overlay),禁文件名后缀近似
DEV_FACE_CLASS_NAMES = {"DebugOverlay"} # 调试浮窗按 class_name 声明出圈——开发面不入玩家可达对账宇宙
#(登记件 probe_coverage_recorder 头注同款边界,两处单点同源)
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]:
# 屏与模态按实体本相归类:场景节点基类才入;RefCounted/Resource 绘制域件出圈;
# DebugOverlay 调试浮窗出圈(判据见 DEV_FACE_CLASS_NAMES 注)。
out: list[dict] = []
for f in sorted((GAME / "presentation" / "views").glob("*.gd")):
if not VIEW_KIND_RE.search(f.stem):
continue
text = f.read_text(encoding="utf-8", errors="replace")
m = EXTENDS_RE.search(text)
if m is not None and m.group(1) in VIEW_NON_SCENE_BASES:
continue
cm = CLASS_NAME_RE.search(text)
if cm is not None and cm.group(1) in DEV_FACE_CLASS_NAMES:
continue
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)
with open(str(out), "w", encoding="utf-8", newline="\n") as fh:
fh.write(js)
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())