test(批B): 断言层级重构大提交——1065 条断言自 smoke_all.gd 全量迁入 16 标准宿主(冒烟/单元/集成/系统+UI 探针);smoke_all.gd 整体退役;变异引擎按用户令删除、验证改核代码(三冷启动审核员 1065 全审:1047 有效,17 存疑+2 段落级,其中 2 段落级经今日闸记录实证解决);10 条核代码修复(恒真/名实/静默跳过/键名等——grilling 一轮修订);验证集清单回写 1068 条三对齐零差异;底册去向补正与笔误清账

This commit is contained in:
DKChan committed 2026-10-23 20:48:20 +08:00
1 parent 8fbd2dd875
commit 957391e13b
29 files changed
+6759 -8446

No files matched your search

+137 -69
View File
@@ -1,24 +1,30 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
r"""extract_checks.py — smoke_all.gd 断言静态提取器(验证集清单生成与出口对账)
r"""extract_checks.py — 断言静态提取器(验证集清单生成与出口对账)
机器提取 game/smoke/smoke_all.gd 的全部静态 check( 调用点(每个调用点记录:所在函数、
机器提取宿主脚本的全部静态 check( 调用点(每个调用点记录:所在文件、所在函数、
行号、标签字符串字面量原文、cond 表达式原文),提供两种模式:
用法:
python tools/extract_checks.py --extract [--src <源文件>] [--out <清单文件>] [--force]
python tools/extract_checks.py --extract [--src <源文件>...] [--out <清单文件>] [--force]
生成验证集清单(已存在时拒写,除非 --force——冻结参照物防误覆)
python tools/extract_checks.py --diff <清单文件> [--src <源文件>]
python tools/extract_checks.py --diff <清单文件> [--src <源文件>...]
对当前源码再提取,与清单逐条比对——批 B 出口对账唯一工具
python tools/extract_checks.py --list-funcs [--src <源文件>]
python tools/extract_checks.py --list-funcs [--src <源文件>...]
只打印函数清单与各函数条数(编账本辅助,不写文件)
断言源(--src 可多个;缺省=三宿主遍历):
game/tests/unit/**/*.gd + game/tests/integration/**/*.gd + game/tests/system_test.gd
批 B 出口对账用显式模式(--src 列全本批宿主文件)——缺省遍历含既有套件的 Kit.check
断言,对账时以「新增」出现(非迁移面),出口判定以显式模式为准。
提取口径:
- 静态 check( 调用点。与行级 grep(grep -c "check(")的差:grep 数含 func check 定义行
与一处注释行(「禁恒真 check(true)」),行级 grep=1067、真实调用点=1065(2026-10-22 基点实测)。
- 静态 check( 调用点,两种通道都认:smoke/系统宿主的 check(cond, label)(cond 在前)
与单元/集成宿主的 Kit.check("label", cond)(点前缀=标签在前);`_check(` 等同名
前缀调用非标准通道,不取。
- 调用点可跨行:从 `check(` 起括号配平归集完整实参文本(() [] {} 三种括号都计深度);
字符串字面量内的括号/逗号/井号、行尾注释、转义序列均不参与结构判定(码位掩码单趟预扫)。
- cond 摘要=cond 原文跨行折叠为单行(连续空白并一空格);标签原文=第二个实参里的字符串
- cond 摘要=cond 原文跨行折叠为单行(连续空白并一空格);标签原文=标签实参里的字符串
字面量逐字保真(含引号与转义序列原样,禁改写);标签实参若带格式化/拼接后缀
(`"..." % [...]`),后缀折叠后以「↩」接排在标签列。
- md 表格单元格内 `|` 转义为 `\|`、`\` 转义为 `\\`(源码串内无这两序列的歧义形态,读回可还原)。
@@ -37,9 +43,26 @@ import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
DEFAULT_SRC = ROOT / "game" / "smoke" / "smoke_all.gd"
# 断言源缺省=三宿主遍历:unit/**+integration/** 两目录递归 + 系统宿主单文件。
# 遍历面含既有套件的 Kit.check 断言(非本批迁移面,对账呈「新增」)——出口对账用 --src 显式列宿主。
DEFAULT_TREES = (
ROOT / "game" / "tests" / "unit",
ROOT / "game" / "tests" / "integration",
ROOT / "game" / "tests" / "system_test.gd",
)
DEFAULT_OUT = ROOT / "game" / "tests" / "验证集清单.md"
def default_sources():
"""三宿主遍历:两目录递归 *.gd + 系统宿主单文件,路径排序定序。"""
files = []
for p in DEFAULT_TREES:
if p.is_file():
files.append(p)
elif p.is_dir():
files.extend(sorted(p.rglob("*.gd")))
return sorted(files)
CALL_RE = re.compile(r"check\(")
DEF_RE = re.compile(r"^\s*(static\s+)?func\s+check\s*\(")
FUNC_RE = re.compile(r"^(static\s+)?func\s+([A-Za-z_][A-Za-z0-9_]*)")
@@ -204,7 +227,7 @@ def collect_call(joined, start):
def extract(src_path: Path):
"""提取全部静态 check( 调用点。返回(条目列表, 函数表),条目保持源码顺序。"""
"""提取单文件全部静态 check( 调用点。返回(条目列表, 函数表),条目保持源码顺序。"""
src = src_path.read_text(encoding="utf-8")
lines = src.split("\n")
funcs = func_table(lines)
@@ -227,21 +250,28 @@ def extract(src_path: Path):
ln = line_of(m.start())
if DEF_RE.match(lines[ln - 1]):
continue # func check 定义行
prev = joined[m.start() - 1] if m.start() > 0 else ""
if prev and (prev.isalnum() or prev == "_"):
continue # `_check(` 等同名前缀——非标准通道
label_first = prev == "." # Kit.check("label", cond)——点前缀=标签在前
body, end_pos = collect_call(joined, m.start())
if body is None:
raise SystemExit("[extract_checks] FAIL:行 %d 起的 check( 括号不配平到文件尾——先查解析缺口" % ln)
raise SystemExit("[extract_checks] FAIL:%s 行 %d 起的 check( 括号不配平到文件尾——先查解析缺口"
% (src_path.name, ln))
args = split_args(body)
if len(args) not in (2, 3):
raise SystemExit(
"[extract_checks] FAIL:行 %d 的 check( 拆出 %d 个实参(预期 2/3)——解析缺口,先查再跑"
% (ln, len(args)))
cond = args[0]
label_expr = args[1]
"[extract_checks] FAIL:%s 行 %d 的 check( 拆出 %d 个实参(预期 2/3)——解析缺口,先查再跑"
% (src_path.name, ln, len(args)))
if label_first:
label_expr, cond = args[0], args[1]
else:
cond, label_expr = args[0], args[1]
quiet = args[2] if len(args) == 3 else ""
lm = LITERAL_RE.search(label_expr)
if lm is None:
raise SystemExit(
"[extract_checks] FAIL:行 %d 的标签实参不含字符串字面量——解析缺口,先查再跑" % ln)
continue # 标签实参无字面量(wrapper 直传变量——既有套件先例 reward_overlay_test _req)
# 不取:无可指纹化面;冻结面对账不受影响(字面量损坏会以「消失」呈现)
entries.append({
"line": ln,
"end_line": line_of(min(end_pos, len(joined) - 1)),
@@ -254,6 +284,20 @@ def extract(src_path: Path):
return entries, funcs
def extract_all(src_paths):
"""多文件提取合并。返回(条目列表, {相对路径: 函数表});条目附加 src=相对路径。"""
entries = []
funcs_by_src = {}
for p in src_paths:
es, funcs = extract(p)
rel = p.relative_to(ROOT).as_posix() if p.is_relative_to(ROOT) else p.as_posix()
for e in es:
e["src"] = rel
entries.extend(es)
funcs_by_src[rel] = funcs
return entries, funcs_by_src
def label_cell(e):
"""标签列:字面量 ↩ 后缀(后缀空则只有字面量)。"""
sfx = fold(e["label_suffix"])
@@ -261,8 +305,10 @@ def label_cell(e):
def entry_key(e):
"""对账指纹:cond 折叠 + 标签字面量(逐字)+ 后缀折叠 + quiet。"""
return (fold(e["cond"]), e["label_literal"], fold(e["label_suffix"]), fold(e["quiet"]))
"""对账指纹=合同 §五三要素:cond 折叠 + 标签字面量(逐字)+ 后缀折叠。
quiet 不入指纹——它是 smoke 通道的打印档位(只影响 PASS 行打印,计数/红绿同口径),
Kit 通道无此参,迁移时按接线注记落通道转换;quiet 列保留展示,不参与对账。"""
return (fold(e["cond"]), e["label_literal"], fold(e["label_suffix"]))
def pad3(n):
@@ -280,13 +326,14 @@ def base_commit():
return "<git 不可用,未取得基点 commit>"
def render_list(entries, funcs, src_rel: str) -> str:
"""生成验证集清单骨架。白话释义/处置两列留待回填(回填不动冻结列)。"""
def render_list(entries, funcs_by_src, src_names) -> str:
"""生成验证集清单骨架。白话释义/处置两列留待回填(回填不动冻结列)。
多源按(源文件, 函数)分章——跨文件同名函数不并章;章头行号=该源文件内行号。"""
out = []
out.append("# 验证集清单(迁移参照物·冻结)")
out.append("")
out.append("> 生成日期:2026-10-22 · 基点 commit:`%s` · 源:`%s`" % (base_commit(), src_rel))
out.append("> 提取口径:静态 `check(` 调用点,括号配平归集(可跨行)。基点实测调用点 1065 处;")
out.append("> 生成日期:2026-10-22 · 基点 commit:`%s` · 源:%s" % (base_commit(), "、".join(src_names)))
out.append("> 提取口径:静态 `check(` 调用点,括号配平归集(可跨行)。批 B 基点实测 smoke_all.gd 调用点 1065 处;")
out.append("> 行级 `grep -c \"check(\"` = 1067 是另一口径——多出的 2 行为 `func check(` 定义行与")
out.append("> 一处注释行(「禁恒真 check(true)」),非调用点。运行时计数大于静态计数=循环展开条目,")
out.append("> 两口径各自浮动,禁互相换算(tests/README「断言计数双口径」)。")
@@ -294,20 +341,21 @@ def render_list(entries, funcs, src_rel: str) -> str:
out.append("> 冻结纪律:每条的「标签原文」「cond 摘要」「quiet」三列为机器提取逐字保真,禁任何改写;")
out.append("> cond 摘要=原文跨行折叠单行(连续空白并一空格),单元格内 `|`→`\\|`、`\\`→`\\\\` 转义,读回还原。")
out.append("> 「白话释义」「处置」两列由维护回填,不属冻结面。")
out.append("> 出口对账:`python tools/extract_checks.py --diff game/tests/验证集清单.md`")
out.append("> 出口对账:`python tools/extract_checks.py --diff game/tests/验证集清单.md --src <宿主文件>...`")
out.append("> ——标签/cond/计数三对齐才算迁移完(合同 §五出口机检)。")
out.append("> 维护义务:此后断言新增/删除/改判必须同步本清单(test-conventions §9.3 锚)。")
out.append("")
seq = 0
by_func = []
by_gf = []
for e in entries:
if by_func and by_func[-1][0] == e["func"]:
by_func[-1][1].append(e)
key = (e["src"], e["func"])
if by_gf and by_gf[-1][0] == key:
by_gf[-1][1].append(e)
else:
by_func.append((e["func"], [e]))
for idx, (fn, es) in enumerate(by_func, 1):
by_gf.append((key, [e]))
for idx, ((src, fn), es) in enumerate(by_gf, 1):
start, end = es[0]["line"], es[-1]["end_line"]
out.append("## %s %s(行 %d–%d · %d 条 · 去向:待分类回填)" % (pad3(idx), fn, start, end, len(es)))
out.append("## %s %s(行 %d–%d · %d 条 · 源 %s · 去向:待分类回填)" % (pad3(idx), fn, start, end, len(es), src))
out.append("")
out.append("| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet |")
out.append("|---|---|---|---|---|---|---|")
@@ -317,7 +365,7 @@ def render_list(entries, funcs, src_rel: str) -> str:
seq, e["line"], esc_cell(label_cell(e)), esc_cell(fold(e["cond"])), esc_cell(fold(e["quiet"]))))
out.append("")
out.append("---")
out.append("全源共 %d 章、%d 条(静态调用点口径)。" % (len(by_func), seq))
out.append("全源共 %d 章、%d 条(静态调用点口径)。" % (len(by_gf), seq))
return "\n".join(out) + "\n"
@@ -361,8 +409,8 @@ def _short(s, n=80):
return s if len(s) <= n else s[:n] + "…"
def diff_report(list_path: Path, src_path: Path) -> int:
cur, _funcs = extract(src_path)
def diff_report(list_path: Path, src_paths) -> int:
cur, _funcs = extract_all(src_paths)
frozen = parse_list(list_path)
cur_keys = {}
for e in cur:
@@ -377,7 +425,7 @@ def diff_report(list_path: Path, src_path: Path) -> int:
used_added = set()
rest_missing = []
for k in missing:
cond, lit, sfx, _q = k
cond, lit, sfx = k
pair = next((ka for ka in added if ka not in used_added and ka[0] == cond), None)
if pair is not None:
used_added.add(pair)
@@ -391,12 +439,12 @@ def diff_report(list_path: Path, src_path: Path) -> int:
rest_missing.append(k)
rest_added = [k for k in added if k not in used_added]
ok = True
print("[extract_checks] 对账:%s vs 现提取 %s" % (list_path.name, src_path.name))
print("[extract_checks] 对账:%s vs 现提取 %d 个源文件" % (list_path.name, len(src_paths)))
print(" 清单条数=%d 现提取条数=%d %s" % (
len(frozen), len(cur), "(计数对齐)" if len(frozen) == len(cur) else "(计数不齐)"))
def show(k, tag):
cond, lit, sfx, q = k
def show(k, tag, q=""):
cond, lit, sfx = k
s = " %s cond=%s label=%s" % (tag, _short(cond), _short(lit))
if sfx:
s += " suffix=%s" % _short(fold(sfx))
@@ -408,27 +456,27 @@ def diff_report(list_path: Path, src_path: Path) -> int:
ok = False
print(" 标签字面变更 %d 对(cond 同、标签异):" % len(label_changes))
for old, new in label_changes:
show(old, "-")
show(new, "+")
show(old, "-", frozen_keys[old][0].get("quiet", ""))
show(new, "+", cur_keys[new][0].get("quiet", ""))
if cond_changes:
ok = False
print(" cond 字面变更 %d 对(标签同、cond 异):" % len(cond_changes))
for old, new in cond_changes:
show(old, "-")
show(new, "+")
show(old, "-", frozen_keys[old][0].get("quiet", ""))
show(new, "+", cur_keys[new][0].get("quiet", ""))
if rest_missing:
ok = False
print(" 消失 %d 条(清单有、现码无):" % len(rest_missing))
for k in rest_missing:
print(" - [%s]" % frozen_keys[k][0].get("section", "?"), end="")
show(k, "")
show(k, "", frozen_keys[k][0].get("quiet", ""))
if rest_added:
ok = False
print(" 新增 %d 条(现码有、清单无):" % len(rest_added))
for k in rest_added:
es = cur_keys[k][0]
print(" + [func %s @%d]" % (es["func"], es["line"]), end="")
show(k, "")
print(" + [src %s func %s @%d]" % (es["src"], es["func"], es["line"]), end="")
show(k, "", es.get("quiet", ""))
if ok:
print("[extract_checks] PASS——标签/cond/计数三对齐,零差异")
return 0
@@ -436,57 +484,77 @@ def diff_report(list_path: Path, src_path: Path) -> int:
return 1
def list_funcs(entries, funcs):
by_func = []
for e in entries:
if by_func and by_func[-1][0] == e["func"]:
by_func[-1][1] = by_func[-1][1] + 1
else:
by_func.append([e["func"], 1])
checked = {f for f, _ in by_func}
for fn, n in by_func:
print("%s\t%d" % (fn, n))
print("--- 无断言函数(helper,随套迁移)---")
for fn, ln in funcs:
if fn not in checked:
print("%s\t@%d" % (fn, ln))
def list_funcs(entries, funcs_by_src):
for src, funcs in funcs_by_src.items():
by_func = []
for e in entries:
if e["src"] != src:
continue
if by_func and by_func[-1][0] == e["func"]:
by_func[-1][1] = by_func[-1][1] + 1
else:
by_func.append([e["func"], 1])
print("=== %s ===" % src)
checked = {f for f, _ in by_func}
for fn, n in by_func:
print("%s\t%d" % (fn, n))
print("--- 无断言函数(helper,随套迁移)---")
for fn, ln in funcs:
if fn not in checked:
print("%s\t@%d" % (fn, ln))
def resolve_sources(src_args):
"""--src 列表 → Path 列表(相对路径按仓库根解析);缺省=三宿主遍历。任一文件不可读即 None。"""
if src_args:
paths = [Path(s) if Path(s).is_absolute() else ROOT / s for s in src_args]
else:
paths = default_sources()
if not paths:
print("[extract_checks] FAIL:断言源为空(三宿主遍历无命中?)")
return None
for p in paths:
if not p.is_file():
print("[extract_checks] FAIL:源文件不可读:%s" % p)
return None
return paths
def main() -> int:
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
ap = argparse.ArgumentParser(description="smoke_all.gd 断言静态提取器(清单生成/对账)")
ap = argparse.ArgumentParser(description="断言静态提取器(清单生成/对账)")
ap.add_argument("--extract", action="store_true", help="生成验证集清单")
ap.add_argument("--diff", metavar="清单文件", help="与清单逐条对账(批 B 出口机检)")
ap.add_argument("--list-funcs", action="store_true", help="打印函数清单与条数(编账本辅助)")
ap.add_argument("--src", default=str(DEFAULT_SRC), help="断言源文件")
ap.add_argument("--src", nargs="+", default=None,
help="断言源文件(可多个;缺省=三宿主遍历 unit/**+integration/**+system_test.gd)")
ap.add_argument("--out", default=str(DEFAULT_OUT), help="--extract 输出路径")
ap.add_argument("--force", action="store_true", help="输出文件已存在时允许覆盖")
args = ap.parse_args()
if sum(bool(x) for x in (args.extract, args.diff, args.list_funcs)) != 1:
print("[extract_checks] 用法:--extract / --diff <清单文件> / --list-funcs 三选一")
return 2
src_path = Path(args.src)
if not src_path.is_file():
print("[extract_checks] FAIL:源文件不可读:%s" % src_path)
src_paths = resolve_sources(args.src)
if src_paths is None:
return 2
if args.extract:
out = Path(args.out)
if out.exists() and not args.force:
print("[extract_checks] FAIL:清单已存在(%s)——冻结参照物防误覆,重生成须加 --force" % out)
return 2
entries, _funcs = extract(src_path)
out.write_text(render_list(entries, _funcs, src_path.name), encoding="utf-8", newline="\n")
print("[extract_checks] 已生成 %s(%d 个调用点)" % (out, len(entries)))
entries, funcs_by_src = extract_all(src_paths)
out.write_text(render_list(entries, funcs_by_src, [p.name for p in src_paths]), encoding="utf-8", newline="\n")
print("[extract_checks] 已生成 %s(%d 个源文件、%d 个调用点)" % (out, len(src_paths), len(entries)))
return 0
if args.diff:
lp = Path(args.diff)
if not lp.is_file():
print("[extract_checks] FAIL:清单不可读:%s" % lp)
return 2
return diff_report(lp, src_path)
entries, funcs = extract(src_path)
list_funcs(entries, funcs)
return diff_report(lp, src_paths)
entries, funcs_by_src = extract_all(src_paths)
list_funcs(entries, funcs_by_src)
return 0
+1 -1
View File
@@ -163,7 +163,7 @@ gate_unit() { # run_all 是 --script 模式,退出码为唯一判据
if in_domain code; then
# ── 0. 冒烟测试(极小启动场景:真实开机路径存活检查,零逻辑判定,秒级)──
gate "00 冒烟测试" "smoke_boot.log" "$PY" "$REPO/tools/run_smoke.py"
# ── 1. 系统测试(真实玩法路径断言面;过渡期宿主=game/smoke/smoke_all.gd,断言迁移归批 B)──
# ── 1. 系统测试(真实玩法路径断言面;宿主=game/tests/system_test.gd,缺省 --script 由 run_system_test.py 持有)──
gate "01 系统测试" "system_test.log" "$PY" "$REPO/tools/run_system_test.py"
# ── 2. 单元与集成测试 ──
gate_unit
+4 -6
View File
@@ -2,14 +2,12 @@
r"""run_system_test.py — 系统测试运行器(脚本错误外部门禁)
用法:
python tools/run_system_test.py # 跑系统测试(过渡期宿主 res://smoke/smoke_all.gd)
python tools/run_system_test.py --script res://tests/system_test.gd
python tools/run_system_test.py # 跑系统测试(宿主 res://tests/system_test.gd)
python tools/run_system_test.py --script <宿主脚本>
python tools/run_system_test.py --godot <引擎路径> --path <切片目录> --timeout 300
系统测试 = 启动真实工程走真实玩法路径的断言面(标准四级测试层之一)。
宿主过渡态:断言现全部住在 res://smoke/smoke_all.gd,按「只剪切禁改写」纪律迁往
res://tests/system_test.gd(宿主薄壳已在位)——迁移完成后本件缺省 --script 改指新宿主,
闸 01 系统测试随迁。
宿主:res://tests/system_test.gd(--script 薄壳自持 check 通道;断言面自 smoke_all.gd 迁入——沿革查 git log)。
背景: GDScript 运行时脚本错误(SCRIPT ERROR)不改变进程退出码、不计入失败数,
且会静默中断所在断言函数的剩余断言——摘要零失败也可能断言未跑全。
@@ -29,7 +27,7 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
DEFAULT_GODOT = r"D:\code\godot\Godot_v4.7.2-stable_win64_console.exe"
DEFAULT_SLICE = ROOT / "game"
DEFAULT_SCRIPT = "res://smoke/smoke_all.gd" # 过渡期宿主;断言迁移批后改 res://tests/system_test.gd
DEFAULT_SCRIPT = "res://tests/system_test.gd" # 系统测试宿主
SUMMARY_RE = re.compile(r"(?:冒烟完成|\[system_test\] 摘要):(\d+) 项检查,(\d+) 项失败")
ERROR_MARKERS = ("SCRIPT ERROR", "Parse Error")