#!/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())