Files
gplan-2/game/tests/mutation/mutation_engine.gd
T

2174 lines
80 KiB
GDScript
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.
# tests/mutation/mutation_engine.gd — 按条变异验证引擎(单进程·内存内变异·零触盘)
# 干什么:游戏环境启动一次,对每条断言执行「内存里改坏 → 只重跑该断言所在套函数 →
# 确认这条断言报警(FAIL)→ 撤销改坏 → 记录结果」,一条接一条;全程不改任何文件。
#
# 三件武器(按套配置适配,见 mutation_plan.gd):
# table 表/资源改写——改已加载 Resource 的内存字段,跑完按原值回写;
# inject 注入编译——被测生产脚本源码字符串在内存做最小变换(数值±1/比较符翻转/
# 布尔翻转/键错位/置空等形态),GDScript.reload() 原地重编译(零触盘),
# 套函数内 load()/preload() 到的类即变异体,跑完全量还原源码再 reload;
# state 状态篡改——直接改运行中对象的内存状态:宿主长活成员(_db 等)、
# 生产脚本静态变量与成员初值、autoload 实例成员与场景树节点名。
# 改坏点自动生成:按计划里该套守的生产模块枚举全部候选改坏,逐个试跑该套函数,
# 扫到能让目标断言翻 FAIL 的改坏即记(摊销扫描:一次试跑同时判定套内全部未杀条目);
# 随后逐条复验(该断言的改坏点单独跑一轮确认红)出逐条台账记录。
#
# 用法(工作目录 = game/):
# godot --headless --path . --script res://tests/mutation/mutation_engine.gd -- --all
# godot ... -- --host=board_gen_reward # 指定宿主全部套
# godot ... -- --suite=seal_polarity:_smoke_seal_ap_exhausted
# godot ... -- --snap=seal_polarity=<快照绝对或 res 路径> # 物化迁移中宿主的提交快照
# godot ... -- --ledger --out=res://reports/批B-变异统一台账.md
# 可选:--resume(跳过已有结果 JSON 的套)· --budget=N · --cap-ms=N
# 退出码:0=全部条目验红(打不红的条全部进异常清单)· 1=存在未验红且不在异常清单
# · 2=零条目(含无参数用法打印)。
extends SceneTree
const Plan := preload("res://tests/mutation/mutation_plan.gd")
const _USER_GUARD_FILES := ["save.json", "save_slot_1.json", "save_slot_2.json", "save_slot_3.json"]
const _RESULT_DIR := "user://mutation_work/results"
var _args := {}
var _jobs: Array = [] # [[host_id, suite_name], ...]
var _adhoc_hosts := {} # id -> cfg(--adhoc 零配置宿主:套/表/模块从宿主源码自动推导)
var _orig_src := {} # 路径 -> 原始源码(宿主与模块件,FileAccess 原文)
var _orig_scripts := {} # 路径 -> 原件 GDScript 对象(进程内首捕;终态夺回路径用)
var _orig_variant := {} # 路径 -> 原件源码变体(无 class_name,编译一次反复接管——还原通道)
var _mod_cand_cache := {} # 模块路径 -> Array[候选](含 func 归属,未排序)
var _host_states := {} # host_id -> {cfg, scan, gds, inst}
var _user_backup := {} # user 档备份(文件名 -> 字节或 null)
var _mut_proxy: Object = null # 活树代理(root 引流+套创建节点记录)
var _mut_tops: Array = [] # 当前 trial 经代理入树的顶层节点(顺序)
var _mut_pending: Array = [] # 待施加树形模板(trial 内节点入树时按路径匹配施加)
var _mut_tree_templates: Array = [] # 基线轮挖出的树形状态模板(候选来源)
var _mut_recording := false # 基线轮记录开关
var _started := false
var _done := false
var _t0_ms := 0
# ============================ 主循环骨架 ============================
func _process(_delta: float) -> bool:
if not _started:
_started = true
_t0_ms = Time.get_ticks_msec()
_main()
return false
if _done:
return true
if Time.get_ticks_msec() - _t0_ms > Plan.GLOBAL_WATCHDOG_MS:
push_error("[mut] 全局看门狗到点,强制收束(已完成套的结果已落盘 %s)" % _RESULT_DIR)
quit(1)
return true
return false
func _main() -> void:
_parse_args()
if _args.has("help") or (_args.is_empty() and not _args.has("ledger-only")):
_print_usage()
quit(2)
_done = true
return
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(_RESULT_DIR))
var ledger_only: bool = _args.has("ledger-only")
if not ledger_only:
_build_adhoc_hosts()
_parse_jobs()
_apply_snaps()
_backup_user_save()
var n := _jobs.size()
var done := 0
for job in _jobs:
done += 1
print("[mut] ===== 套 %d/%d %s:%s =====" % [done, n, job[0], job[1]])
await _run_job(String(job[0]), String(job[1]))
_restore_user_save()
var integrity := _verify_scripts_restored()
print("[mut] 全部套完成:jobs=%d · 生产/宿主源码还原核验=%s" % [n, "全还原" if integrity == "" else "异常:" + integrity])
var code := 2
if _args.has("ledger") or ledger_only:
code = _build_ledger(_ledger_out_path())
else:
code = _exit_code_from_results()
quit(code)
_done = true
func _parse_args() -> void:
for a in OS.get_cmdline_user_args():
if not a.begins_with("--"):
continue
var body := a.substr(2)
var eq := body.find("=")
var k := body if eq < 0 else body.substr(0, eq)
var v := "true" if eq < 0 else body.substr(eq + 1)
if k == "suite":
if not _args.has("suites"):
_args["suites"] = []
_args["suites"].append(v)
elif k == "snap":
if not _args.has("snaps"):
_args["snaps"] = []
_args["snaps"].append(v)
elif k == "adhoc":
if not _args.has("adhocs"):
_args["adhocs"] = []
_args["adhocs"].append(v)
elif k == "excl-func":
if not _args.has("excl-funcs"):
_args["excl-funcs"] = []
_args["excl-funcs"].append(v)
elif k == "excl-line":
if not _args.has("excl-lines"):
_args["excl-lines"] = []
_args["excl-lines"].append(v)
else:
_args[k] = v
# --adhoc=<id>=<宿主 res 路径>:零配置宿主(主会话统一验证跑计划外宿主用)。
# 套 = 有断言点且零必填参的函数(_init/_initialize 除外);表 = 源码 _db.<名> 访问词;
# 模块 = 源码 load/preload 的 res://*.gd;通道 Kit.check 在场=kit 否则 self;含 await=异步。
# 武器三件全开(table 候选按推导表、inject 按推导模块;细化归计划条目——候选招数不全的
# 套按台账异常清单补计划配置后重跑)。
func _build_adhoc_hosts() -> void:
for spec in _args.get("adhocs", []):
var parts := String(spec).split("=", true, 1)
if parts.size() != 2:
push_error("[mut] --adhoc 规格非法(要 id=res://路径):%s" % String(spec))
continue
var hid := String(parts[0])
var src_path := String(parts[1])
var src := FileAccess.get_file_as_string(src_path)
if src == "":
push_error("[mut] adhoc 宿主源码为空/缺失:%s" % src_path)
continue
var scan := _scan_source(src)
var re_table := RegEx.create_from_string("_db\\.([a-z][a-z_0-9]*)")
var tables := {}
for m in re_table.search_all(src):
tables[String(m.get_string(1))] = true
var re_mod := RegEx.create_from_string("(?:load|preload)\\(\\s*\"(res://[^\"]+\\.gd)\"")
var modules := {}
for m2 in re_mod.search_all(src):
var mp := String(m2.get_string(1))
if mp != src_path:
modules[mp] = true
var by_func := {}
for s in scan["sites"]:
var fn := String(s["func"])
if fn in ["_init", "_initialize", ""]:
continue
by_func[fn] = int(by_func.get(fn, 0)) + 1
var suites := {}
for fn2 in by_func.keys():
if not scan["func_ranges"].has(fn2) or _func_required_args(scan, fn2) > 0:
continue
suites[fn2] = {"count": int(by_func[fn2]), "weapons": ["table", "inject", "state"],
"tables": tables.keys(), "modules": modules.keys()}
if suites.is_empty():
push_error("[mut] adhoc 宿主无可运行套(断言点全在有参 helper 里?):%s" % src_path)
continue
_adhoc_hosts[hid] = {"id": hid, "src": src_path,
"channel": "kit" if src.find("Kit.check") >= 0 else "self",
"async": src.find("await ") >= 0, "budget": 800,
"cap_ms": Plan.SUITE_CAP_MS_DEFAULT, "state_autoloads": [], "suites": suites}
var n := 0
for s2 in suites.values():
n += int(s2["count"])
print("[mut] adhoc 宿主 %s(%d 套 %d 点 · 表 %d · 模块 %d)" % [hid, suites.size(), n, tables.size(), modules.size()])
# 函数必填参数个数(带默认值的不计——零必填参才可作套函数直呼)
func _func_required_args(scan: Dictionary, fname: String) -> int:
var r: Dictionary = scan["func_ranges"][fname]
var head := String(scan["lines"][int(r["start"])])
var open_i := head.find("(")
var close_i := head.rfind(")")
if open_i < 0 or close_i <= open_i:
return 0
var n := 0
for seg in head.substr(open_i + 1, close_i - open_i - 1).split(","):
var s := String(seg).strip_edges()
if s != "" and s.find("=") < 0:
n += 1
return n
func _parse_jobs() -> void:
_jobs.clear()
if _args.has("all"):
for h in Plan.hosts():
for s in h["suites"].keys():
_jobs.append([h["id"], s])
elif _args.has("host"):
var h := _plan_cfg(String(_args["host"]))
if h.is_empty():
push_error("[mut] 未知宿主:%s" % String(_args["host"]))
else:
for s in h["suites"].keys():
_jobs.append([h["id"], s])
elif _args.has("suites"):
for spec in _args["suites"]:
var parts := String(spec).split(":", true, 1)
if parts.size() != 2 or _plan_cfg(parts[0]).is_empty():
push_error("[mut] --suite 规格非法(要 宿主:套名):%s" % String(spec))
continue
_jobs.append([parts[0], parts[1]])
# --adhoc 宿主整host入队(仅在无 --all/--host/--suite 显式选面时;显式 --suite 仍可单挑其套)
var explicit := _args.has("all") or _args.has("host") or _args.has("suites")
if not explicit:
for hid in _adhoc_hosts.keys():
for s in _adhoc_hosts[hid]["suites"].keys():
_jobs.append([String(hid), String(s)])
if String(_args.get("resume", "false")) == "true":
var kept: Array = []
for job in _jobs:
if FileAccess.file_exists(_result_path(String(job[0]), String(job[1]))):
print("[mut] resume 跳过已有结果:%s:%s" % [job[0], job[1]])
else:
kept.append(job)
_jobs = kept
func _apply_snaps() -> void:
for spec in _args.get("snaps", []):
var parts := String(spec).split("=", true, 1)
if parts.size() != 2:
continue
var h := _plan_cfg(parts[0])
if h.is_empty():
continue
var content := FileAccess.get_file_as_string(parts[1])
if content == "":
push_error("[mut] 快照源为空:%s" % parts[1])
continue
var dst: String = h["src"]
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(dst).get_base_dir())
var f := FileAccess.open(dst, FileAccess.WRITE)
f.store_string(content)
f.close()
print("[mut] 已物化宿主快照 %s <- %s(%d 行)" % [dst, parts[1], content.count("\n") + 1])
func _print_usage() -> void:
print("按条变异验证引擎——用法:")
print(" godot --headless --path game --script res://tests/mutation/mutation_engine.gd -- --all")
print(" 可选:--host=<id> · --suite=<id>:<套名>(可多次)· --snap=<宿主id>=<快照路径>")
print(" --adhoc=<id>=<宿主 res:// 路径>(零配置宿主:套/表/模块自动推导,可多次)")
print(" --excl-func=<文件名>:<函数名>(函数级候选排除——挂死/崩溃型毒候选隔离阀,可多次)")
print(" --excl-line=<文件名>:<1基行号>(行级毒候选定点排除,可多次)")
print(" --resume · --budget=N · --cap-ms=N · --ledger --out=<res 路径> · --ledger-only")
print("计划内宿主×套×静态断言数(合计 %d):" % Plan.total_static_checks())
for h in Plan.hosts():
for s in h["suites"].keys():
print(" %s : %s = %d 条" % [h["id"], s, h["suites"][s]["count"]])
# ============================ 源码扫描 ============================
# 宿主配置查找:--adhoc 注册的零配置宿主优先,其次计划条目
func _plan_cfg(host_id: String) -> Dictionary:
if _adhoc_hosts.has(host_id):
return _adhoc_hosts[host_id]
return Plan.host_by_id(host_id)
func _host_state(host_id: String) -> Dictionary:
if _host_states.has(host_id):
return _host_states[host_id]
var cfg := _plan_cfg(host_id)
if cfg.is_empty():
return {}
var src := FileAccess.get_file_as_string(String(cfg["src"]))
if src == "":
push_error("[mut] 宿主源码为空/缺失:%s(迁移中宿主需先 --snap 物化)" % String(cfg["src"]))
return {}
_orig_src[String(cfg["src"])] = src
var scan := _scan_source(src)
var st := {"cfg": cfg, "src": src, "scan": scan, "gds": null, "inst": null}
_host_states[host_id] = st
return st
func _scan_source(src: String) -> Dictionary:
var lines := src.split("\n")
var codes: Array[String] = []
var in_tqs := false
for raw in lines:
var tq := raw.count("\"\"\"")
if in_tqs:
if tq % 2 == 1:
in_tqs = false
codes.append("")
continue
if tq % 2 == 1:
in_tqs = true
codes.append(_strip_comment(raw))
var func_ranges := {} # 函数名 -> {start, end}
var func_of_line := {}
var cur := ""
for i in lines.size():
var s := codes[i].strip_edges()
if s.begins_with("func ") or s.begins_with("static func "):
var head := s.substr(s.find("func ") + 5)
var nm := head.substr(0, head.find("(")).strip_edges()
if cur != "":
func_ranges[cur]["end"] = i
cur = nm
func_ranges[cur] = {"start": i, "end": lines.size() - 1}
func_of_line[i] = cur
var sites := []
var idx := 0
for i in lines.size():
var is_def := codes[i].strip_edges().begins_with("func ") or codes[i].strip_edges().begins_with("static func ")
var col := 0
while true:
var hit := _find_check_call(codes[i], col)
if hit["col"] < 0:
break
col = int(hit["col"]) + int(hit["len"])
if is_def and hit["kind"] == "self":
continue
sites.append({"idx": idx, "line": i, "col": int(hit["col"]), "len": int(hit["len"]),
"kind": hit["kind"], "func": func_of_line[i]})
idx += 1
return {"lines": lines, "codes": codes, "func_ranges": func_ranges,
"func_of_line": func_of_line, "sites": sites}
func _find_check_call(code: String, from: int) -> Dictionary:
var found := {"col": -1, "len": 0, "kind": ""}
var probes := [["Kit.check_quiet(", "kit_q", 1], ["Kit.check(", "kit", 0], ["check(", "self", 1]]
for p in probes:
var token: String = p[0]
var c := code.find(token, from)
while c >= 0:
var prev := " " if c == 0 else code[c - 1]
var ok_site := true
if p[1] == "self":
if prev == "." or prev == "_":
ok_site = false
if c >= 5 and code.substr(c - 5, 5) == "func ":
ok_site = false
else:
if not (prev in [" ", "\t", "(", "[", "=", ",", "+"]):
ok_site = false
if ok_site:
if found["col"] < 0 or c < int(found["col"]):
found = {"col": c, "len": token.length(), "kind": p[1], "arg": p[2]}
break
c = code.find(token, c + 1)
return found
func _extract_label(scan: Dictionary, site: Dictionary) -> String:
var lines: Array = scan["lines"]
var start_line: int = site["line"]
var start_col: int = int(site["col"]) + int(site["len"])
var depth := 0
var args: Array[String] = []
var cur_arg := ""
var in_str := false
for li in range(start_line, lines.size()):
var raw: String = lines[li]
var ci := 0
if li == start_line:
ci = start_col
while ci < raw.length():
var ch := raw[ci]
if in_str:
cur_arg += ch
if ch == "\\" and ci + 1 < raw.length():
ci += 1
cur_arg += raw[ci]
elif ch == "\"":
in_str = false
ci += 1
continue
if ch == "\"":
in_str = true
cur_arg += ch
elif ch == "(" or ch == "[" or ch == "{":
depth += 1
cur_arg += ch
elif ch == ")" or ch == "]" or ch == "}":
depth -= 1
if depth < 0:
args.append(cur_arg)
return _pick_label(args, int(site.get("arg", 0)))
cur_arg += ch
elif ch == "," and depth == 0:
args.append(cur_arg)
cur_arg = ""
else:
cur_arg += ch
ci += 1
cur_arg += " "
return _pick_label(args, int(site.get("arg", 0)))
func _pick_label(args: Array[String], want: int) -> String:
if want >= args.size():
return "<取参失败>"
var a := args[want].strip_edges().replace("\t", " ")
while a.find(" ") >= 0:
a = a.replace(" ", " ")
return a
func _strip_comment(line: String) -> String:
var out := ""
var in_str := false
var i := 0
while i < line.length():
var c := line[i]
if in_str:
out += c
if c == "\\" and i + 1 < line.length():
out += line[i + 1]
i += 2
continue
if c == "\"":
in_str = false
i += 1
else:
if c == "#":
break
if c == "\"":
in_str = true
out += c
i += 1
if in_str:
return "" # 引号不闭合——保守跳过该行
return out
# ============================ 宿主源码内存变换与编译 ============================
func _compile_variant(host_id: String, run_fn: String, strip_siblings: bool) -> Dictionary:
var st := _host_state(host_id)
if st.is_empty():
return {}
var scan: Dictionary = st["scan"]
var lines: Array = scan["lines"]
var new_lines: Array = []
for l in lines:
new_lines.append(String(l))
# ① 断言点改写 _mut_check(idx, ...)(自后向前替换,列号不漂移)
var sites: Array = scan["sites"]
for si in range(sites.size() - 1, -1, -1):
var s: Dictionary = sites[si]
var token := "Kit.check_quiet(" if s["kind"] == "kit_q" else ("Kit.check(" if s["kind"] == "kit" else "check(")
var raw := String(new_lines[int(s["line"])])
new_lines[int(s["line"])] = raw.substr(0, int(s["col"])) + "_mut_check(%d, " % int(s["idx"]) + raw.substr(int(s["col"]) + token.length())
# ② 中和 _init/_initialize(保夹具,删套调用与 quit);_split_tail 形态另删兄弟套调用
var suite_names: Array = st["cfg"]["suites"].keys()
for fname in ["_init", "_initialize"]:
if not scan["func_ranges"].has(fname):
continue
var r: Dictionary = scan["func_ranges"][fname]
for i in range(int(r["start"]) + 1, int(r["end"]) + 1):
var code := String(scan["codes"][i]).strip_edges()
if code == "":
continue
var m := _simple_call_name(code)
if m != "" and scan["func_ranges"].has(m) and m != fname:
new_lines[i] = "\tpass # [mut 变换] 套调用摘除"
elif code.begins_with("quit("):
new_lines[i] = "\tpass # [mut 变换] quit 摘除"
if strip_siblings and scan["func_ranges"].has(run_fn):
var r2: Dictionary = scan["func_ranges"][run_fn]
for i in range(int(r2["start"]) + 1, int(r2["end"]) + 1):
var code2 := String(scan["codes"][i]).strip_edges()
var m2 := _simple_call_name(code2)
if code2.begins_with("await ") and m2 != "" and m2 in suite_names and m2 != run_fn:
new_lines[i] = "\t\tpass # [mut 变换] 兄弟套调用摘除"
elif code2.begins_with("quit("):
new_lines[i] = "\tpass # [mut 变换] quit 摘除"
# ③ 活树/帧信号改道(root/get_root/process_frame → 引擎注入方法;按变换后行重取代码面前缀)
for i in new_lines.size():
var raw2 := String(new_lines[i])
var cpart := _strip_comment(raw2)
if cpart == "":
continue
var r1 := _replace_word(raw2, cpart, "get_root()", "_mut_root()")
var r2b := _replace_word(r1["line"], r1["scan"], "process_frame", "_mut_pf()")
var r3 := _replace_word(r2b["line"], r2b["scan"], "root", "_mut_root()")
new_lines[i] = r3["line"]
# ④ 注入预置(断言记点 / 活树改道)
var prelude := "\n\n# ---- [mut 变换] 引擎注入预置 ----\n"
prelude += "var _mut_engine = null\nvar _mut_sites = {}\n"
prelude += "func _mut_check(idx, a, b) -> void:\n"
prelude += "\tif not _mut_sites.has(idx):\n\t\t_mut_sites[idx] = []\n"
prelude += "\t_mut_sites[idx].append(bool(b) if (b is bool) else bool(a))\n"
prelude += "func _mut_root():\n\treturn _mut_engine.mut_root()\n"
prelude += "func _mut_pf():\n\treturn _mut_engine.process_frame\n"
var gds := GDScript.new()
gds.source_code = "\n".join(new_lines) + prelude
if gds.reload() != OK:
push_error("[mut] 宿主变换编译失败:%s run_fn=%s" % [host_id, run_fn])
return {}
return {"gds": gds}
func _simple_call_name(code: String) -> String:
var c := code
if c.begins_with("await "):
c = c.substr(6).strip_edges()
if not c.ends_with("()"):
return ""
var nm := c.substr(0, c.length() - 2)
if nm == "" or nm.find("(") >= 0 or nm.find(" ") >= 0 or not nm.is_valid_identifier():
return ""
return nm
func _replace_word(raw: String, scan: String, token: String, repl: String) -> Dictionary:
var out := raw
var s := scan
var pos := s.find(token)
while pos >= 0:
var before_ok := pos == 0 or not _is_word_char(s[pos - 1])
var after := pos + token.length()
var after_ok := after >= s.length() or not _is_word_char(s[after])
var dotted := pos > 0 and s[pos - 1] == "."
if before_ok and after_ok and not dotted and not _inside_string_span(s, pos):
out = out.substr(0, pos) + repl + out.substr(pos + token.length())
s = s.substr(0, pos) + repl + s.substr(pos + token.length())
pos = s.find(token, pos + repl.length())
else:
pos = s.find(token, pos + token.length())
return {"line": out, "scan": s}
func _is_word_char(c: String) -> bool:
if c == "_":
return true
return (c >= "a" and c <= "z") or (c >= "A" and c <= "Z") or (c >= "0" and c <= "9")
# ============================ 套执行 ============================
func _run_job(host_id: String, suite_name: String) -> void:
var st := _host_state(host_id)
if st.is_empty():
_write_result(host_id, suite_name, {"host": host_id, "suite": suite_name, "complete": true,
"rows": [], "anomalies": [{"scope": "suite", "reason": "宿主源码缺失"}], "meta": {}})
return
var senv: Dictionary = st["cfg"]["suites"][suite_name]
var run_fn := String(senv.get("run_fn", suite_name))
var variant := _compile_variant(host_id, run_fn, bool(senv.get("strip_siblings", false)))
if variant.is_empty():
_write_result(host_id, suite_name, {"host": host_id, "suite": suite_name, "complete": true,
"rows": [], "anomalies": [{"scope": "suite", "reason": "宿主变换编译失败"}], "meta": {}})
return
var gds: GDScript = variant["gds"]
var inst: Object = gds.new()
inst.set("_mut_engine", self)
st["gds"] = gds
st["inst"] = inst
var scan: Dictionary = st["scan"]
var targets: Array = []
var label_of := {}
for s in scan["sites"]:
if s["func"] == suite_name:
var s2 := {"idx": s["idx"], "line": s["line"], "col": s["col"], "len": s["len"],
"kind": s["kind"], "arg": s.get("arg", 0), "func": s["func"]}
targets.append(s2)
label_of[int(s["idx"])] = _extract_label(scan, s2)
var anomalies: Array = []
var expect := int(senv["count"])
if targets.size() != expect:
anomalies.append({"scope": "suite", "reason": "静态断言点账实不符:计划 %d 实扫 %d(以实扫为准)" % [expect, targets.size()]})
var budget := int(_args.get("budget", st["cfg"]["budget"]))
var cap_ms := int(_args.get("cap-ms", st["cfg"].get("cap_ms", Plan.SUITE_CAP_MS_DEFAULT)))
# ---- 基线(未执行 = 条件分支未走到,视为可杀形态;执行了必须全真) ----
_mut_tree_templates = []
_mut_recording = true
var base = await _run_suite_fn(st, run_fn)
_mut_recording = false
var base_ok := {}
for t in targets:
var idx := int(t["idx"])
if base["sites"].has(idx):
var any_false := false
for v in base["sites"][idx]:
if not v:
any_false = true
break
if any_false:
anomalies.append({"scope": "assert", "idx": idx, "label": label_of[idx],
"reason": "基线即红——未变异已失败(死断言候选或环境污染)"})
base_ok[idx] = false
else:
base_ok[idx] = true
else:
base_ok[idx] = true
var pending := 0
for t in targets:
if base_ok[int(t["idx"])]:
pending += 1
# ---- 候选改坏点(按武器优先序;摊销扫描) ----
var weapon_cands := _build_candidates(st, senv, suite_name)
if String(_args.get("debug", "false")) == "true":
for w3 in ["table", "inject", "state"]:
print("[mut-debug] %s 候选数=%d" % [w3, weapon_cands[w3].size()])
for di in range(0, mini(12, weapon_cands["inject"].size())):
print("[mut-debug] inject#%d %s" % [di, String(weapon_cands["inject"][di]["desc"])])
var killers := {}
for t in targets:
killers[int(t["idx"])] = []
var tried := 0
var suite_t0 := Time.get_ticks_msec()
for w in senv["weapons"]:
for c in weapon_cands[String(w)]:
if pending <= 0 or tried >= budget or Time.get_ticks_msec() - suite_t0 > cap_ms:
break
var _trace := String(_args.get("trace", "false")) == "true"
if _trace:
print("[mut-trace] try #%d %s" % [tried + 1, String(c.get("desc", ""))])
if not _apply_candidate(c):
_restore_candidate(c) # 施加失败也还原——失败施加可能已动了一半内存态
tried += 1
if _trace:
print("[mut-trace] #%d apply失败 %s" % [tried, String(c.get("desc", ""))])
continue
var r = await _run_suite_fn(st, run_fn)
_restore_candidate(c)
tried += 1
if _trace:
var nred := 0
for t9 in targets:
var i9 := int(t9["idx"])
if r["sites"].has(i9):
for v9 in r["sites"][i9]:
if not v9:
nred += 1
break
print("[mut-trace] #%d %s 红数=%d" % [tried, String(c.get("desc", "")), nred])
for t in targets:
var idx2 := int(t["idx"])
if not base_ok[idx2] or not killers[idx2].is_empty():
continue
if r["sites"].has(idx2):
var bad := false
for v in r["sites"][idx2]:
if not v:
bad = true
break
if bad:
killers[idx2].append(c)
pending -= 1
if pending <= 0 or tried >= budget or Time.get_ticks_msec() - suite_t0 > cap_ms:
break
# ---- 逐条复验(改坏 → 跑该断言所在函数 → 验 FAIL → 撤销 → 记录) ----
var rows: Array = []
for t in targets:
var idx3 := int(t["idx"])
if not base_ok[idx3]:
rows.append({"idx": idx3, "label": label_of[idx3], "status": "anomaly",
"reason": _anomaly_reason(anomalies, idx3), "weapon": "", "cand": "",
"cand_file": "", "cand_line": "", "cand_kind": "", "ms": 0, "collateral": -1})
continue
var verified := false
for c in killers[idx3]:
if not _apply_candidate(c):
continue
var rv = await _run_suite_fn(st, run_fn)
_restore_candidate(c)
if rv["sites"].has(idx3):
var red := false
for v in rv["sites"][idx3]:
if not v:
red = true
break
if red:
var coll := 0
for other_idx in rv["sites"].keys():
if int(other_idx) == idx3:
continue
for v2 in rv["sites"][other_idx]:
if not v2:
coll += 1
break
rows.append({"idx": idx3, "label": label_of[idx3], "status": "verified",
"weapon": String(c["weapon"]), "cand": String(c["desc"]),
"cand_file": String(c.get("file", "")), "cand_line": str(int(c.get("line", -1))),
"cand_kind": String(c.get("kind", "")), "ms": int(rv["us"] / 1000.0),
"collateral": coll, "killers_available": killers[idx3].size()})
verified = true
break
if not verified:
var reason := ""
if killers[idx3].is_empty():
reason = ("预算内未找到能翻红的改坏(试跑 %d 轮,武器=%s)——死断言候选或改坏面缺形" % [tried, ", ".join(PackedStringArray(senv["weapons"]))]) if tried > 0 else "零轮试跑(预算/上限异常)"
reason += _env_read_hint(scan, t)
if String(label_of[idx3]).find("幂等") >= 0:
reason += ";标签含幂等语义——确定性快照的内容等值在单行确定性变异下难破,疑似结构性打不红,须人工研判改坏面"
else:
reason = "复验未红(改坏候选 %d 个均不复现——疑似非确定断言)" % killers[idx3].size()
anomalies.append({"scope": "assert", "idx": idx3, "label": label_of[idx3], "reason": reason})
rows.append({"idx": idx3, "label": label_of[idx3], "status": "anomaly", "reason": reason,
"weapon": "", "cand": "", "cand_file": "", "cand_line": "", "cand_kind": "",
"ms": 0, "collateral": -1})
# ---- 收尾:跑后基线复绿(零残留证明) ----
var rend = await _run_suite_fn(st, run_fn)
var end_green := true
for t in targets:
var idx4 := int(t["idx"])
if rend["sites"].has(idx4):
for v in rend["sites"][idx4]:
if not v:
end_green = false
break
if not end_green:
anomalies.append({"scope": "suite", "reason": "跑后基线未复绿——存在跨试跑状态残留,须人工排查"})
var verify_ms_total := 0
for r in rows:
verify_ms_total += int(r.get("ms", 0))
_write_result(host_id, suite_name, {"host": host_id, "suite": suite_name, "complete": true,
"run_fn": run_fn, "budget": budget, "tried": tried, "targets": targets.size(),
"static_plan": expect, "rows": rows, "anomalies": anomalies,
"end_baseline_green": end_green,
"meta": {"verify_ms_total": verify_ms_total}})
inst.free()
st["inst"] = null
st["gds"] = null
func _anomaly_reason(anomalies: Array, idx: int) -> String:
for a in anomalies:
if int(a.get("idx", -1)) == idx:
return String(a["reason"])
return "基线异常"
# 断言表达式环境面识别(断言行连读三行——多行 cond 的续行):命中 FileAccess/DirAccess
# (磁盘读取)或「大写成员常量读取」(schema 词表 const 经宿主 preload 编译期绑定原件——
# 路径接管不可达且 const 深冻结)的打不红条目注明归因——台账异常清单用(禁触盘/禁 reload
# 原件是引擎红线,此类条目只能如实进异常清单,不为验红改文件)
func _env_read_hint(scan: Dictionary, t: Dictionary) -> String:
var lines: Array = scan["lines"]
var re_const := RegEx.create_from_string("\\.[A-Z][A-Z0-9_]{2,}\\b")
for li in range(int(t["line"]), mini(int(t["line"]) + 3, lines.size())):
var s := _strip_comment(String(lines[li]))
if s.find("FileAccess") >= 0 or s.find("DirAccess") >= 0:
return ";断言表达式含磁盘读取(FileAccess/DirAccess)——内存内改坏不可达,归环境面死断言候选"
if re_const != null and re_const.search(s) != null:
return ";断言表达式读 schema 常量(大写成员,宿主 preload 编译期绑定原件)——路径接管不可达且 const 深冻结,归结构性打不红候选"
return ""
func _run_suite_fn(st: Dictionary, run_fn: String) -> Dictionary:
var inst: Object = st["inst"]
inst.set("_mut_sites", {})
_mut_tops = []
var t0 := Time.get_ticks_usec()
if bool(st["cfg"]["async"]):
await inst.call(run_fn)
else:
inst.call(run_fn)
var us := Time.get_ticks_usec() - t0
var out := {}
var sites: Dictionary = inst.get("_mut_sites")
for k in sites.keys():
out[int(k)] = sites[k].duplicate()
return {"sites": out, "us": us}
# ============================ 候选改坏点生成 ============================
func _build_candidates(st: Dictionary, senv: Dictionary, suite_name: String) -> Dictionary:
var by_weapon := {"table": [], "inject": [], "state": []}
var tokens := _suite_tokens(st, String(senv.get("run_fn", suite_name)))
for w in senv["weapons"]:
match String(w):
"table":
by_weapon["table"] = _table_candidates(st, senv.get("tables", []),
_suite_literals(st, String(senv.get("run_fn", suite_name))))
"inject":
for mp in senv.get("modules", []):
for c in _module_candidates(String(mp)):
c["weapon"] = "inject"
by_weapon["inject"].append(c)
# 相关性置前:套函数体内出现过的标识符(被测入口/协作者名)命中的候选优先试跑——
# 大模块按行序扫会把深处守卫排在预算外,套源码词面+计划 funcs 提示是仅有的自动定位信号
var scored: Array = []
var hinted_all: Dictionary = senv.get("funcs", {})
for ci in by_weapon["inject"].size():
var c2: Dictionary = by_weapon["inject"][ci]
var sc := 0
var mp_file := String(String(c2.get("file", "")).get_file())
var hinted_names: Array = hinted_all.get(mp_file, [])
if hinted_names.has("*") or hinted_names.has(String(c2.get("func", ""))):
sc += 4
if tokens.has(String(c2.get("func", ""))):
sc += 2
for t in _word_tokens(String(c2.get("line_text", ""))):
if t.length() >= 4 and tokens.has(t):
sc += 1
break
scored.append(sc)
var order := range(by_weapon["inject"].size())
order.sort_custom(func(a: int, b: int) -> bool:
if scored[a] != scored[b]:
return int(scored[a]) > int(scored[b])
return a < b)
var reordered: Array = []
for oi in order:
reordered.append(by_weapon["inject"][oi])
by_weapon["inject"] = reordered
"state":
by_weapon["state"] = _state_candidates(st, senv)
for tc in _tree_state_candidates():
by_weapon["state"].append(tc)
# 跨武器去重(同文件同行同形态只留先到者;注入/初值候选无 set_key——以行号+形态区分;
# 按套的武器优先序迭代——先到武器保留该行候选的武器归属)
var seen := {}
var worder: Array = []
for w4 in senv["weapons"]:
worder.append(String(w4))
for w5 in ["table", "inject", "state"]:
if not worder.has(w5):
worder.append(w5)
for w2 in worder:
var kept: Array = []
for c in by_weapon[w2]:
var key := "%s|%s|%s|%s" % [c.get("file", ""), c.get("kind", ""), c.get("set_key", ""), str(c.get("line", -1))]
if seen.has(key):
continue
seen[key] = true
kept.append(c)
by_weapon[w2] = kept
return by_weapon
# 词法切分(标识符提取用——4.7 RegEx 无 split,手扫字符)
func _word_tokens(code: String) -> PackedStringArray:
var out := PackedStringArray()
var cur := ""
for i in code.length():
var ch := code[i]
if _is_word_char(ch):
cur += ch
elif cur != "":
out.append(cur)
cur = ""
if cur != "":
out.append(cur)
return out
# 套函数体标识符集合(注释剔除后取词;run_fn 本体及其调用的套内私有 helper 一并纳入——
# 供注入候选按「被套源码词面命中」排序)
func _suite_tokens(st: Dictionary, run_fn: String) -> Dictionary:
var scan: Dictionary = st["scan"]
var tokens := {}
var want: Array = [run_fn]
var seen_fns := {}
while not want.is_empty():
var fn := String(want.pop_back())
if fn == "" or seen_fns.has(fn) or not scan["func_ranges"].has(fn):
continue
seen_fns[fn] = true
var r: Dictionary = scan["func_ranges"][fn]
for i in range(int(r["start"]), int(r["end"]) + 1):
var code := String(scan["codes"][i])
if code.strip_edges() == "":
continue
for t in _word_tokens(code):
if t.length() >= 4:
tokens[t] = true
var m := _simple_call_name(code.strip_edges())
if m != "" and scan["func_ranges"].has(m):
want.append(m)
return tokens
func _table_candidates(st: Dictionary, table_names: Array, lits: Array) -> Array:
var cands: Array = []
var inst: Object = st["inst"]
if inst == null or inst.get("_db") == null:
return cands
var db = inst.get("_db")
for tname in table_names:
var t = db.get(String(tname))
if t == null or not (t is Resource):
continue
var before := cands.size()
_walk_table(t, String(tname), cands, 0, lits)
if String(_args.get("debug", "false")) == "true":
print("[mut-debug] 表 %s 候选 %d(累计 %d)" % [String(tname), cands.size() - before, cands.size()])
return cands
# 套函数体字符串字面量(注释剔除后引号内文本;套内私有 helper 一并纳入)。
# 供表武器「字面量键位插行/加键」候选——断言点名的业务键(如 defense_up)就是套源码
# 字面量,账本 §九 G2 旧法「补立已退役行/加键」形态的自动取材通道。
func _suite_literals(st: Dictionary, run_fn: String) -> Array:
var scan: Dictionary = st["scan"]
var out: Array = []
var seen := {}
var want: Array = [run_fn]
var seen_fns := {}
while not want.is_empty():
var fn := String(want.pop_back())
if fn == "" or seen_fns.has(fn) or not scan["func_ranges"].has(fn):
continue
seen_fns[fn] = true
var r: Dictionary = scan["func_ranges"][fn]
for i in range(int(r["start"]), int(r["end"]) + 1):
var code := String(scan["codes"][i])
if code.strip_edges() == "":
continue
var ci := 0
while ci < code.length():
if code[ci] == "\"":
var cj := ci + 1
var buf := ""
while cj < code.length() and code[cj] != "\"":
if code[cj] == "\\" and cj + 1 < code.length():
cj += 2
continue
buf += code[cj]
cj += 1
if cj >= code.length():
break
var lit := buf
if lit.length() >= 3 and lit.length() <= 40 and not seen.has(lit) \
and _is_ident_like(lit):
seen[lit] = true
out.append(lit)
ci = cj + 1
continue
ci += 1
var m := _simple_call_name(code.strip_edges())
if m != "" and scan["func_ranges"].has(m):
want.append(m)
if out.size() >= 24:
break
return out
func _is_ident_like(s: String) -> bool:
if s == "" or (s[0] >= "0" and s[0] <= "9"):
return false
for i in s.length():
if not _is_word_char(s[i]):
return false
return true
func _walk_table(res: Resource, label: String, cands: Array, depth: int, lits: Array) -> void:
var n := 0
for p in res.get_property_list():
if n >= 96 or cands.size() >= 2600:
return
var pname := String(p["name"])
if pname.begins_with("resource_") or pname in ["script", "metadata", "resource_name", "reference"]:
continue
var v = res.get(pname)
match typeof(v):
TYPE_OBJECT:
if v is Resource and depth < 2:
_walk_table(v, label + "." + pname, cands, depth + 1, lits)
TYPE_INT:
for nv in [v + 1, v - 1, 0, v * 10]:
if nv != v and nv >= 0:
cands.append(_val_cand(res, label + "." + pname, pname, nv))
n += 1
TYPE_FLOAT:
for fv in [v + 1.0, v * 10.0]:
cands.append(_val_cand(res, label + "." + pname, pname, fv))
n += 1
TYPE_BOOL:
cands.append(_val_cand(res, label + "." + pname, pname, not v))
n += 1
TYPE_STRING:
if v != "":
cands.append(_val_cand(res, label + "." + pname, pname, v + "_x"))
cands.append(_val_cand(res, label + "." + pname, pname, ""))
n += 2
TYPE_DICTIONARY:
# 字典值属性(如 settlement.effect_pool_map):现键值改写+套字面量加键——
# has/键查通道的击穿面(「无 X 键」类断言须造出同名键才翻红)
if depth < 2:
_dict_prop_candidates(v, label + "." + pname, cands, lits, depth)
TYPE_ARRAY:
if depth < 2:
_array_prop_candidates(v, label + "." + pname, cands, lits, depth)
n += 1
# 数组值属性(行集为主):结构性改坏置前(账本 §九 G2 旧法主力形态:复插/删行/加键/删键)
# +套字面量键位插行(已退役行/清单规模类断言)+空表插行+逐键值改写(逐键上限,防
# 前排字符串键吃光配额把纵深数值键排到预算外)
func _array_prop_candidates(arr: Array, label: String, cands: Array, lits: Array, depth: int) -> void:
var row_cap := mini(arr.size(), 16)
if String(_args.get("debug", "false")) == "true":
print("[mut-debug] 数组 %s 行数=%d row_cap=%d lits=%d" % [label, arr.size(), row_cap, lits.size()])
for ei in row_cap:
var el0 = arr[ei]
if el0 is Dictionary:
cands.append(_dict_addkey_cand(el0, "%s[%d]" % [label, ei]))
var kk := 0
for key0 in el0.keys():
if kk >= 2:
break
cands.append(_dict_erase_cand(el0, "%s[%d].%s" % [label, ei, str(key0)], key0))
kk += 1
if cands.size() >= 2600:
return
if arr.size() > 0:
cands.append(_row_dup_cand(arr, label))
cands.append(_row_dup_cand(arr, label, mini(2, arr.size() - 1)))
cands.append(_row_del_cand(arr, label, 0))
if arr.size() > 2:
cands.append(_row_del_cand(arr, label, arr.size() - 1))
# 字面量键位插行:键名取行首键,值=套源码字面量、其余字段抄行0(append 表尾——
# 规模/成员/退役行查通道全翻红;还原按持引用 erase,跑后基线复绿闸兜底残留)
var el_first = arr[0]
var idk = null
if el_first is Dictionary and el_first.size() > 0:
idk = el_first.keys()[0]
var idv = el_first.get(idk) if idk != null else null
var lit_n := 0
for lit in lits:
if lit_n >= 8 or cands.size() >= 2600:
break
# 键值非字符串不插(int 键位插字符串值必畸形行);键值==字面量无新信息不插
if idk == null or typeof(idv) != TYPE_STRING or str(idv) == lit:
continue
cands.append(_row_lit_insert_cand(arr, label, idk, lit))
lit_n += 1
# 新键值插行(每行集一挺——「十型封闭/成员集合」类断言要一个词表外的键值才翻红;
# 键值型非字符串不生成——typed 行插异型必 apply 失败白轮)
if idk != null and typeof(idv) == TYPE_STRING:
cands.append(_row_lit_insert_cand(arr, label, idk, "mut_seed"))
elif arr.is_empty():
cands.append(_row_synth_insert_cand(arr, label))
for ei in row_cap:
var el = arr[ei]
if el is Dictionary:
# 每键候选各自成列再轮转并取——宽行(如 enemy 行 15 键、armor_class 殿后)
# 不被前排键的累计配额挤掉:先让每个键拿到第一形态,再谈第二三形态
var per_key: Array = []
for key in el.keys():
var ev = el[key]
var tag := "%s[%d].%s" % [label, ei, str(key)]
var l: Array = []
_dict_value_candidates(el, tag, key, ev, l, depth, lits)
per_key.append(l)
var added := 0
var round_i := 0
while round_i < 4 and added < 48 and cands.size() < 2600:
for l2 in per_key:
if round_i < l2.size():
cands.append(l2[round_i])
added += 1
if added >= 48 or cands.size() >= 2600:
break
round_i += 1
elif el is Resource:
_walk_table(el, "%s[%d]" % [label, ei], cands, depth + 1, lits)
else:
match typeof(el):
TYPE_INT:
for nv3 in [el + 1, el - 1, 0]:
if nv3 != el and nv3 >= 0:
cands.append(_arr_elem_cand(arr, label, ei, nv3))
TYPE_FLOAT:
cands.append(_arr_elem_cand(arr, label, ei, el + 1.0))
TYPE_BOOL:
cands.append(_arr_elem_cand(arr, label, ei, not el))
TYPE_STRING:
if el != "":
cands.append(_arr_elem_cand(arr, label, ei, el + "_x"))
# 字典值属性候选:现键值改写(逐键上限)+套字面量加键
func _dict_prop_candidates(dd: Dictionary, label: String, cands: Array, lits: Array, depth: int) -> void:
var kc := 0
for key in dd.keys():
if kc >= 6 or cands.size() >= 2600:
break
var ev = dd[key]
kc += _dict_value_candidates(dd, "%s.%s" % [label, str(key)], key, ev, cands, depth, lits)
var v0 = null
for k0 in dd.keys():
v0 = dd[k0]
break
if v0 == null:
v0 = true
for lit in lits:
if cands.size() >= 2600:
break
if dd.has(lit):
continue
cands.append(_dict_lit_addkey_cand(dd, label, lit, v0))
# 单键值改写候选(每键上限 2——纵深键不再被前排键的配额挤掉;返回追加数)
func _dict_value_candidates(dict: Dictionary, tag: String, key, ev, cands: Array, depth: int, lits: Array) -> int:
var kc := 0
match typeof(ev):
TYPE_INT:
for nv2 in [ev - 1, ev + 1, 0]:
if kc >= 3:
break
if nv2 != ev and nv2 >= 0:
cands.append(_dict_cand(dict, tag, key, nv2))
kc += 1
TYPE_FLOAT:
for fv2 in [ev + 1.0, ev * 10.0]:
if kc >= 2:
break
cands.append(_dict_cand(dict, tag, key, fv2))
kc += 1
TYPE_STRING:
if ev != "":
cands.append(_dict_cand(dict, tag, key, ev + "_x"))
kc += 1
TYPE_BOOL:
cands.append(_dict_cand(dict, tag, key, not ev))
kc += 1
TYPE_OBJECT:
if ev is Resource and depth < 1:
_walk_table(ev, tag, cands, depth + 1, lits)
return kc
var _rm_slot = null # 删行候选的暂存行(施加→还原窗口内持有)
func _dict_addkey_cand(dict: Dictionary, tag: String) -> Dictionary:
var apply_f := func() -> bool:
if dict.has("mut_seed"):
return false
dict["mut_seed"] = true
return true
var restore_f := func() -> void:
dict.erase("mut_seed")
return {"weapon": "table", "file": tag, "line": -1, "kind": "table 加键",
"set_key": tag + ".mut_seed", "desc": "表行加键 %s: mut_seed=true" % tag,
"apply": apply_f, "restore": restore_f}
func _dict_erase_cand(dict: Dictionary, tag: String, key) -> Dictionary:
var has_old: bool = dict.has(key)
var old = dict.get(key)
var apply_f := func() -> bool:
if not dict.has(key):
return false
dict.erase(key)
return true
var restore_f := func() -> void:
if has_old:
dict[key] = old
return {"weapon": "table", "file": tag, "line": -1, "kind": "table 删键",
"set_key": tag + ".erase", "desc": "表行删键 %s" % tag,
"apply": apply_f, "restore": restore_f}
func _row_dup_cand(arr: Array, label: String, ei: int = 0) -> Dictionary:
var apply_f := func() -> bool:
if ei >= arr.size():
return false
var ins = arr[ei].duplicate(true)
arr.insert(0, ins)
_rm_slot = ins
return true
var restore_f := func() -> void:
# erase 按内容等价删首个命中——副本与原行同内容,删哪个对断言面等价;
# apply 失败时 _rm_slot 恒 null,restore 空操作(不误删真行)
if _rm_slot != null:
arr.erase(_rm_slot)
_rm_slot = null
return {"weapon": "table", "file": label, "line": -1, "kind": "table 复插行",
"set_key": label + ".dup" + str(ei), "desc": "表复插行 %s[%d] → 表首" % [label, ei],
"apply": apply_f, "restore": restore_f}
func _row_del_cand(arr: Array, label: String, ei: int) -> Dictionary:
var apply_f := func() -> bool:
if ei >= arr.size():
return false
_rm_slot = arr[ei]
arr.remove_at(ei)
return true
var restore_f := func() -> void:
if _rm_slot != null:
arr.insert(mini(ei, arr.size()), _rm_slot)
_rm_slot = null
return {"weapon": "table", "file": label, "line": -1, "kind": "table 删行",
"set_key": label + ".del" + str(ei), "desc": "表删行 %s[%d]" % [label, ei],
"apply": apply_f, "restore": restore_f}
# 字面量键位插行(行首键=套源码字面量、其余字段抄行0)——「已退役行不存在/清单规模/
# 成员封闭」类断言的击穿面
func _row_lit_insert_cand(arr: Array, label: String, idk, lit: String) -> Dictionary:
var proto: Dictionary = arr[0]
var row: Dictionary = proto.duplicate(true)
row[idk] = lit
var apply_f := func() -> bool:
if arr.has(row):
return false
arr.append(row)
return true
var restore_f := func() -> void:
arr.erase(row)
return {"weapon": "table", "file": "%s.%s=%s" % [label, str(idk), lit], "line": -1,
"kind": "table 字面量插行", "set_key": "%s.%s=%s" % [label, str(idk), lit],
"desc": "表插行 %s:%s=%s(其余字段抄行0)" % [label, str(idk), lit],
"apply": apply_f, "restore": restore_f}
# 空表插行(空行集的「空表加载/零行为」类断言击穿面;值型按数组元素型适配,
# 不认识的元素型不生成——apply 必失败的白轮)
func _row_synth_insert_cand(arr: Array, label: String) -> Dictionary:
var row = {"mut_seed": true}
var typed_ok := true
if arr.is_typed():
match arr.get_typed_builtin():
TYPE_STRING:
row = "mut_seed"
TYPE_INT, TYPE_FLOAT:
row = 1
TYPE_BOOL:
row = true
TYPE_DICTIONARY, TYPE_NIL, TYPE_OBJECT:
pass
_:
typed_ok = false
var apply_f := func() -> bool:
if not typed_ok:
return false
arr.append(row)
return true
var restore_f := func() -> void:
arr.erase(row)
return {"weapon": "table", "file": label, "line": -1, "kind": "table 空表插行",
"set_key": label + ".synth", "desc": "空表插行 %s(合成行灭 is_empty)" % label,
"apply": apply_f, "restore": restore_f}
# 字典字面量加键(断言点名的键名——「无 X 键/has 通道」类断言的击穿面;值抄现键值型)
func _dict_lit_addkey_cand(dd: Dictionary, label: String, lit: String, v0) -> Dictionary:
var apply_f := func() -> bool:
if dd.has(lit):
return false
dd[lit] = v0
return true
var restore_f := func() -> void:
dd.erase(lit)
return {"weapon": "table", "file": "%s.%s" % [label, lit], "line": -1,
"kind": "table 字典加键", "set_key": "%s.%s" % [label, lit],
"desc": "字典加键 %s:%s(值型抄现键)" % [label, lit],
"apply": apply_f, "restore": restore_f}
func _arr_elem_cand(arr: Array, label: String, ei: int, nv) -> Dictionary:
var old = arr[ei]
var apply_f := func() -> bool:
arr[ei] = nv
return true
var restore_f := func() -> void:
arr[ei] = old
return {"weapon": "table", "file": "%s[%d]" % [label, ei], "line": -1, "kind": "table_elem",
"set_key": "%s[%d]=%s" % [label, ei, _fmt_val(nv)], "desc": "表元素改写 %s[%d]: %s → %s" % [label, ei, _fmt_val(old), _fmt_val(nv)],
"apply": apply_f, "restore": restore_f}
func _val_cand(res: Resource, path_label: String, pname: String, nv) -> Dictionary:
var old = res.get(pname)
var apply_f := func() -> bool:
res.set(pname, nv)
return true
var restore_f := func() -> void:
res.set(pname, old)
return {"weapon": "table", "file": path_label, "line": -1, "kind": "table_set",
"set_key": path_label + "=" + _fmt_val(nv), "desc": "表改写 %s: %s → %s" % [path_label, _fmt_val(old), _fmt_val(nv)],
"apply": apply_f, "restore": restore_f}
func _dict_cand(dict: Dictionary, path_label: String, key, nv) -> Dictionary:
var old = dict.get(key)
var apply_f := func() -> bool:
dict[key] = nv
return true
var restore_f := func() -> void:
dict[key] = old
return {"weapon": "table", "file": path_label, "line": -1, "kind": "table_set",
"set_key": path_label + "=" + _fmt_val(nv), "desc": "表行改写 %s: %s → %s" % [path_label, _fmt_val(old), _fmt_val(nv)],
"apply": apply_f, "restore": restore_f}
func _fmt_val(v) -> String:
var s := str(v)
return s.substr(0, 40)
# 模块源码行级候选(注释/字符串感知;loop 行跳过防挂死;含 func 归属供套级提示排序)
func _module_candidates(path: String) -> Array:
if _mod_cand_cache.has(path):
return _mod_cand_cache[path]
var src := FileAccess.get_file_as_string(path)
if src == "":
var s = load(path)
src = String(s.source_code) if s != null else ""
if src == "":
push_error("[mut] 模块源码缺失:%s" % path)
_mod_cand_cache[path] = []
return []
_orig_src[path] = src
if not _orig_scripts.has(path):
var so = load(path)
if so != null:
_orig_scripts[path] = so # 原件对象捕获(还原通道+终态核验基准)
var lines := src.split("\n")
var codes: Array[String] = []
var in_tqs := false
for raw in lines:
var tq := raw.count("\"\"\"")
if in_tqs:
if tq % 2 == 1:
in_tqs = false
codes.append("")
continue
if tq % 2 == 1:
in_tqs = true
codes.append(_strip_comment(raw))
var cur_func := ""
var all: Array = []
for i in lines.size():
var s := codes[i].strip_edges()
if s.begins_with("func ") or s.begins_with("static func "):
var head := s.substr(s.find("func ") + 5)
cur_func = head.substr(0, head.find("(")).strip_edges()
if s == "":
continue
if _func_excluded(path, cur_func):
continue
var scope := "body" if cur_func != "" else "init"
all.append_array(_line_candidates(codes[i], i, String(lines[i]), path, scope, cur_func))
_mod_cand_cache[path] = all
return all
# 函数级候选排除(--excl-func=<文件名>:<函数名>,可多次):挂死/崩溃型变异体的隔离阀——
# 有毒候选(如把带重试环的 generate 改成无限递归→栈溢出杀进程)按函数整段剔除,
# 同模块其余函数的候选照常生成
func _func_excluded(path: String, fname: String) -> bool:
for spec in _args.get("excl-funcs", []):
var parts := String(spec).split(":", true, 1)
if parts.size() == 2 and String(path).get_file() == parts[0] and fname == parts[1]:
return true
return false
# 行级候选排除(--excl-line=<文件名>:<1 基行号>,可多次):单行毒候选(如 BFS 邻接条件
# 取反→队列无界增长→OOM 杀进程)的精确定点阀,同函数其余行照常
func _line_excluded(path: String, i: int) -> bool:
for spec in _args.get("excl-lines", []):
var parts := String(spec).split(":", true, 1)
if parts.size() == 2 and String(path).get_file() == parts[0] and int(parts[1]) == i + 1:
return true
return false
func _line_candidates(code: String, i: int, orig_line: String, path: String, scope: String, cur_func: String) -> Array:
var cands: Array = []
var stripped := code.strip_edges()
if stripped == "" or stripped.begins_with("class_name") or stripped.begins_with("extends"):
return cands
var loop_risk := stripped.begins_with("while") or stripped.begins_with("for ") \
or code.find(" range(") >= 0 or code.find("\tfor ") >= 0
var fname := String(path).get_file()
var muts: Array = []
if _line_excluded(path, i):
return cands
if not loop_risk:
muts.append_array(_op_muts(code))
muts.append_array(_token_muts(code, 2))
muts.append_array(_empty_coll_muts(code))
muts.append_array(_num_muts(code, 5))
if code.find(" :=") < 0:
muts.append_array(_assign_null_muts(code))
muts.append_array(_negret_muts(code))
muts.append_array(_return_swap_muts(code))
muts.append_array(_if_negate_muts(code))
muts.append_array(_string_muts(code, 2))
muts.append_array(_const_coll_append_muts(code))
for m in muts:
var new_line := String(m["line"])
if new_line == orig_line:
continue
var apply_f := func() -> bool:
# 变体编译+路径接管(原件零 reload——GDScript.reload 在原件有循环引用实例时
# 恒返 ERR_ALREADY_IN_USE,进程内原地重编译对战斗核心件不可行)
var src := String(_orig_src.get(path, ""))
if src == "":
return false
var ls := src.split("\n")
if i >= ls.size() or ls[i] != orig_line:
return false
ls[i] = new_line
return _take_over_variant(path, "\n".join(ls))
var restore_f := func() -> void:
var orig_s: GDScript = _orig_scripts.get(path)
if orig_s != null:
orig_s.take_over_path(path) # 原件对象夺回路径——缓存映射回到开工态(零重编译)
cands.append({"weapon": "inject", "file": path, "line": i, "kind": String(m["kind"]),
"scope": scope, "func": cur_func, "line_text": code,
"desc": "%s:%d %s [%s]" % [fname, i + 1, String(m["kind"]), _snippet_pair(orig_line, new_line)],
"apply": apply_f, "restore": restore_f})
return cands
func _snippet_pair(a: String, b: String) -> String:
var sa := a.strip_edges()
var sb := b.strip_edges()
if sa.length() > 46:
sa = sa.substr(0, 46) + "…"
if sb.length() > 46:
sb = sb.substr(0, 46) + "…"
return sa + " → " + sb
func _op_muts(code: String) -> Array:
var flips := [["==", "!="], ["!=", "=="], [">=", "<"], ["<=", ">"], [">", "<="], ["<", ">="]]
var done: Array = []
var out: Array = []
for f in flips:
var from: String = f[0]
var to: String = f[1]
var pos := code.find(from)
while pos >= 0:
var ok := true
var ch_before := " " if pos == 0 else code[pos - 1]
var after_i := pos + from.length()
var ch_after := " " if after_i >= code.length() else code[after_i]
if ch_before in ["=", "!", "<", ">", "+", "-", "*", "/", "%", "&", "|", ":"]:
ok = false
if ch_after in ["=", "<", ">"]:
ok = false
if ok and _inside_string_span(code, pos):
ok = false
if ok:
var nl := code.substr(0, pos) + to + code.substr(pos + from.length())
if not done.has(nl):
done.append(nl)
out.append({"kind": "op %s→%s" % [from, to], "line": nl})
pos = code.find(from, pos + 1)
return out
func _inside_string_span(code: String, pos: int) -> bool:
var in_str := false
var i := 0
while i < pos:
if code[i] == "\\":
i += 2
continue
if code[i] == "\"":
in_str = not in_str
i += 1
return in_str
func _token_muts(code: String, cap: int) -> Array:
var out: Array = []
for t in [["true", "false"], ["false", "true"]]:
var pos := code.find(t[0])
while pos >= 0 and out.size() < cap:
var before_ok := pos == 0 or not _is_word_char(code[pos - 1])
var after: int = pos + String(t[0]).length()
var after_ok := after >= code.length() or not _is_word_char(code[after])
if before_ok and after_ok and not _inside_string_span(code, pos):
out.append({"kind": "bool %s→%s" % [t[0], t[1]], "line": code.substr(0, pos) + t[1] + code.substr(after)})
pos = code.find(t[0], pos + 1)
return out
func _num_muts(code: String, cap: int) -> Array:
var out: Array = []
var i := 0
while i < code.length() and out.size() < cap:
var c := code[i]
if c >= "0" and c <= "9":
var before_ok := i == 0 or not (_is_word_char(code[i - 1]) or code[i - 1] == ".")
var hexlike := c == "0" and i + 1 < code.length() and code[i + 1] in ["x", "b", "X", "B"]
if before_ok and not hexlike:
var j := i
var dot := false
while j < code.length() and ((code[j] >= "0" and code[j] <= "9") or (code[j] == "." and not dot)):
if code[j] == ".":
dot = true
j += 1
var lit := code.substr(i, j - i)
var head := code.substr(0, i)
var tail := code.substr(j)
if not dot:
var iv := lit.to_int()
for nv in [iv + 1, iv - 1]:
if nv >= 0:
out.append({"kind": "num %d→%d" % [iv, nv], "line": head + str(nv) + tail})
# 折半——±1 够不着的钳制带/阈值断言(旧法 46→25 类大跳)的最小通用替代形态
var hv := iv / 2
if hv != iv and hv >= 0:
out.append({"kind": "num %d→%d(折半)" % [iv, hv], "line": head + str(hv) + tail})
else:
var fv := lit.to_float()
out.append({"kind": "num %s→%s" % [lit, _fmt_val(fv + 1.0)], "line": head + _fmt_val(fv + 1.0) + tail})
var hv2 := fv * 0.5
out.append({"kind": "num %s→%s(折半)" % [lit, _fmt_val(hv2)], "line": head + _fmt_val(hv2) + tail})
i = j
continue
i += 1
return out
func _empty_coll_muts(code: String) -> Array:
var out: Array = []
for pair in [["= {}", "{\"mut_seed\": true}", "coll {}→注脏"], ["= []", "[\"mut_seed\"]", "coll []→注脏"]]:
var pos := code.find(pair[0])
if pos >= 0 and not _inside_string_span(code, pos):
if pos > 0 and code[pos - 1] == ":":
continue # :={} 推断赋值——跳过避免生成非法行
# 带类型标注的声明(var x: T = …)换异形字面量必编译失败——只吃无标注的普通赋值
if code.substr(0, pos).contains(":"):
continue
var nl: String = code.substr(0, pos) + String(pair[1]) + code.substr(pos + String(pair[0]).length())
out.append({"kind": pair[2], "line": nl})
return out
func _assign_null_muts(code: String) -> Array:
var out: Array = []
var pos := code.find(" = ")
while pos >= 0 and out.size() < 1:
var ch_before := "" if pos == 0 else code[pos - 1]
if not (ch_before in ["=", "!", "<", ">", "+", "-", "*", "/", "%", ":"]):
# 类型标注(var x: T = …)与 const 置 null 必编译失败——只吃无标注赋值
if not code.substr(0, pos).contains(":") and not code.strip_edges().begins_with("const"):
var rhs := code.substr(pos + 3).strip_edges()
if rhs != "" and not rhs.begins_with("null"):
out.append({"kind": "asgn 置null", "line": code.substr(0, pos) + " = null"})
pos = code.find(" = ", pos + 1)
return out
func _negret_muts(code: String) -> Array:
var out: Array = []
var stripped := code.strip_edges()
if stripped.begins_with("return ") and not stripped.begins_with("return not"):
var expr := stripped.substr(7).strip_edges()
if expr != "":
var cut := code.find("return")
var indent := code.substr(0, cut)
out.append({"kind": "return 取反", "line": indent + "return not (" + expr + ")"})
var key := ": return "
var pos := code.find(key)
if pos >= 0:
var expr2 := code.substr(pos + key.length()).strip_edges()
if expr2 != "" and not expr2.begins_with("not"):
out.append({"kind": "lambda 取反", "line": code.substr(0, pos) + key + "not (" + expr2 + ")"})
return out
# return 置换(守卫击穿的最小通用形态):非平凡 return 表达式换成常量假/空——
# 与返回类型不合的形态 reload 即拒、apply 内自动回写原件(廉价废轮,不跑套)
func _return_swap_muts(code: String) -> Array:
var out: Array = []
var stripped := code.strip_edges()
if not stripped.begins_with("return "):
return out
var expr := stripped.substr(7).strip_edges()
if expr == "" or expr.begins_with("not") or expr in ["false", "0"]:
return out
var cut := code.find("return")
var indent := code.substr(0, cut)
if expr == "null":
out.append({"kind": "return null→{}", "line": indent + "return {}"})
return out
if expr == "{}":
# 空字典返回=「清单外查空」类断言的守卫面——非空化击穿(读取通道在位类)
out.append({"kind": "return {}→非空", "line": indent + "return {\"mut_seed\": true}"})
return out
if expr == "[]":
out.append({"kind": "return []→非空", "line": indent + "return [\"mut_seed\"]"})
return out
out.append({"kind": "return 置假", "line": indent + "return false"})
out.append({"kind": "return 置空", "line": indent + "return []"})
return out
# if/elif 条件击穿两面:正条件取反;not 条件去 not(not(not X) ≡ X——对 not 形态取反
# 无义不生成)。守卫与幂等缓存命中的通用击穿形态(账本旧法「has 恒真/恒假」类的自动替身;
# 跨行条件不处理——行尾无冒号即跳过)
func _if_negate_muts(code: String) -> Array:
var out: Array = []
var stripped := code.strip_edges()
var head := ""
if stripped.begins_with("if "):
head = "if "
elif stripped.begins_with("elif "):
head = "elif "
if head == "":
return out
var cond := stripped.substr(head.length())
if not cond.ends_with(":"):
return out
var body := cond.substr(0, cond.length() - 1).strip_edges()
var ci := code.find(head)
var indent := code.substr(0, ci)
if body.begins_with("not "):
out.append({"kind": head.strip_edges() + " 去not", "line": indent + head + body.substr(4) + ":"})
else:
out.append({"kind": head.strip_edges() + " 取反", "line": indent + head + "not (" + body + "):"})
return out
# const 数组追加元素(词表封闭类断言的击穿面——CELL_TYPES 等词表加一个词表外成员,
# size 与 has 双面翻红;单行字面量数组限定,跨行不处理)
func _const_coll_append_muts(code: String) -> Array:
var out: Array = []
var stripped := code.strip_edges()
if not stripped.begins_with("const "):
return out
var open_i := code.find("[")
var close_i := code.rfind("]")
if open_i < 0 or close_i <= open_i:
return out
var body := code.substr(open_i + 1, close_i - open_i - 1)
if body.find("[") >= 0 or body.find("]") >= 0:
return out
var nl := code.substr(0, close_i) + ", \"mut_seed\"" + code.substr(close_i)
out.append({"kind": "const 数组追加", "line": nl})
return out
func _string_muts(code: String, cap: int) -> Array:
var out: Array = []
var i := 0
while i < code.length() and out.size() < cap:
if code[i] == "\"":
var j := i + 1
while j < code.length() and code[j] != "\"":
if code[j] == "\\":
j += 1
j += 1
if j < code.length():
var content := code.substr(i + 1, j - i - 1)
if content != "" and not content.contains("res://") and not content.contains("user://") \
and not content.contains("%") and not content.contains("$"):
var nl := code.substr(0, i + 1) + content + "_x" + code.substr(j)
out.append({"kind": "key 错位:\"%s\"" % content.substr(0, 20), "line": nl})
i = j + 1
continue
i += 1
return out
# 状态篡改候选:宿主长活成员 / 模块静态变量与成员初值 / autoload 实例与树状态
func _state_candidates(st: Dictionary, senv: Dictionary) -> Array:
var cands: Array = []
var inst: Object = st["inst"]
# (a) 宿主长活成员
if inst != null and inst.get("_db") != null:
var db = inst.get("_db")
if typeof(db.get("_fatal_flag")) == TYPE_BOOL:
cands.append(_obj_prop_cand(db, "_fatal_flag", true, "宿主._db._fatal_flag"))
if typeof(db.get("is_ready")) == TYPE_BOOL:
cands.append(_obj_prop_cand(db, "is_ready", false, "宿主._db.is_ready"))
# (b) 模块成员初值行(对象出生状态)+ 静态变量运行态改写
for mp in senv.get("modules", []):
var mps := String(mp)
for c in _module_candidates(mps):
if c.get("scope", "body") == "init":
var c2: Dictionary = c.duplicate(true)
c2["weapon"] = "state"
c2["desc"] = "[状态·初值] " + String(c["desc"])
cands.append(c2)
var msrc := FileAccess.get_file_as_string(mps)
if msrc == "" and load(mps) != null:
msrc = String(load(mps).source_code)
if msrc == "":
continue
var script = load(mps)
if script == null:
continue
var mlines := msrc.split("\n")
var static_found := 0
for mi in mlines.size():
if static_found >= 6:
break
var s := _strip_comment(String(mlines[mi])).strip_edges()
if not s.begins_with("static var "):
continue
var decl := s.substr(11)
var sp := decl.find(" ")
if sp <= 0:
continue
var vname := decl.substr(0, sp)
var old = script.get(vname)
if old == null:
continue
var nv = null
if typeof(old) == TYPE_BOOL:
nv = not old
elif typeof(old) == TYPE_INT:
nv = int(old) + 1
elif typeof(old) == TYPE_FLOAT:
nv = float(old) + 1.0
elif typeof(old) == TYPE_STRING and String(old) != "":
nv = String(old) + "_x"
if nv == null:
continue
var mfile := mps.get_file()
var sname := vname
var ov = old
var apply_f := func() -> bool:
script.set(sname, nv)
return true
var restore_f := func() -> void:
script.set(sname, ov)
cands.append({"weapon": "state", "file": mps, "line": mi, "kind": "static_var",
"desc": "[状态·静态] %s:%d %s → %s" % [mfile, mi + 1, sname, _fmt_val(nv)],
"apply": apply_f, "restore": restore_f})
static_found += 1
# (c) autoload 实例与树状态
for aname in st["cfg"].get("state_autoloads", []):
var nm := String(aname)
var node := get_root().get_node_or_null(NodePath("/root/" + nm))
if node == null:
continue
var nobj: Object = node
var apply_r := func() -> bool:
nobj.set("name", nm + "__mut")
return true
var restore_r := func() -> void:
nobj.set("name", nm)
cands.append({"weapon": "state", "file": "/root/" + nm, "line": -1, "kind": "tree_rename",
"desc": "[状态·树] autoload 节点 /root/%s 改名(装配链就绪护栏击穿)" % nm,
"apply": apply_r, "restore": restore_r})
var bound := 0
for p in nobj.get_property_list():
if bound >= 12:
break
var pname := String(p["name"])
if pname.begins_with("resource_") or pname in ["script", "metadata", "name", "owner",
"process_mode", "process_priority", "physics_process_priority",
"scene_file_path", "unique_name_in_owner"]:
continue
if (int(p.get("usage", 0)) & PROPERTY_USAGE_STORAGE) == 0:
continue
var pv = nobj.get(pname)
if typeof(pv) == TYPE_INT:
var o1: int = pv
var p1 := pname
var apply_p1 := func() -> bool:
nobj.set(p1, o1 + 1)
return true
var restore_p1 := func() -> void:
nobj.set(p1, o1)
cands.append({"weapon": "state", "file": "/root/" + nm, "line": -1, "kind": "obj_prop",
"set_key": p1, "desc": "[状态·实例] %s.%s: %d → %d" % [nm, p1, o1, o1 + 1],
"apply": apply_p1, "restore": restore_p1})
bound += 1
elif typeof(pv) == TYPE_BOOL:
var o2: bool = pv
var p2 := pname
var apply_p2 := func() -> bool:
nobj.set(p2, not o2)
return true
var restore_p2 := func() -> void:
nobj.set(p2, o2)
cands.append({"weapon": "state", "file": "/root/" + nm, "line": -1, "kind": "obj_prop",
"set_key": p2, "desc": "[状态·实例] %s.%s: %s → %s" % [nm, p2, _fmt_val(o2), _fmt_val(not o2)],
"apply": apply_p2, "restore": restore_p2})
bound += 1
return cands
func _obj_prop_cand(obj: Object, pname: String, nv, label: String) -> Dictionary:
var old = obj.get(pname)
var apply_f := func() -> bool:
obj.set(pname, nv)
return true
var restore_f := func() -> void:
obj.set(pname, old)
return {"weapon": "state", "file": label, "line": -1, "kind": "obj_prop", "set_key": pname,
"desc": "[状态·实例] %s: %s → %s" % [label, _fmt_val(old), _fmt_val(nv)],
"apply": apply_f, "restore": restore_f}
# ============================ 改坏施加与还原 ============================
# 活树代理:套代码里 root/get_root() 经编译变换改道至此(_mut_root())。代理把 add_child
# 引流到真实 root 并记录入树顶层节点——供「树形状态模板」武器按基线树形在 trial 内改写
# 场景承载脚本的节点属性(.tscn 经 uid 直引脚本对象+场景缓存,路径接管不可达——这是
# 呈现类断言唯一可达的改坏通道)。套代码用到的 root 面仅 add_child/get_node_or_null/
# get_children(system_test 七处 add_child+autoload 探针;其余方法按需在此扩)。
func mut_root() -> Object:
if _mut_proxy == null:
_mut_proxy = _MutRootProxy.new(self)
return _mut_proxy
class _MutRootProxy:
var eng
func _init(e) -> void:
eng = e
func add_child(n: Node, legacy_argument: bool = false, legible: bool = false) -> void:
eng.root.add_child(n)
eng._on_mut_add(n)
func remove_child(n: Node) -> void:
eng.root.remove_child(n)
func get_children() -> Array:
return eng.root.get_children()
func get_child(i: int) -> Node:
return eng.root.get_child(i)
func get_node_or_null(p) -> Node:
return eng.root.get_node_or_null(p)
func has_node(p) -> bool:
return eng.root.has_node(p)
func _to_string() -> String:
return str(eng.root)
func _on_mut_add(top: Node) -> void:
_mut_tops.append(top)
var top_i := _mut_tops.size() - 1
if _mut_recording:
_mine_tree_templates(top, top_i)
for p in _mut_pending:
if int(p["top"]) != top_i:
continue
var n: Node = top
var ok := true
for seg in p["path"]:
n = n.get_node_or_null(NodePath(String(seg)))
if n == null:
ok = false
break
if ok:
n.set(String(p["prop"]), p["val"])
# 树形模板挖掘(基线轮、入树即时):子树展平至深 3,Control 取 visible 翻转+mouse_filter
# 轮转,全部节点取 name 加尾——场景承载断言的默认态/在位链改坏面。路径=自顶的名字链。
func _mine_tree_templates(top: Node, top_i: int) -> void:
if _mut_tree_templates.size() >= 24:
return
var stack: Array = [[top, []]]
while not stack.is_empty() and _mut_tree_templates.size() < 24:
var e: Array = stack.pop_front()
var n: Node = e[0]
var path: Array = e[1]
if not path.is_empty():
if n is Control and typeof(n.get("visible")) == TYPE_BOOL:
_mut_tree_templates.append({"top": top_i, "path": path, "prop": "visible",
"val": not bool(n.get("visible")), "desc": "树态 %s.visible → %s" % ["/".join(PackedStringArray(path)), str(not bool(n.get("visible")))]})
if n is Control and typeof(n.get("mouse_filter")) == TYPE_INT:
var mf: int = int(n.get("mouse_filter"))
_mut_tree_templates.append({"top": top_i, "path": path, "prop": "mouse_filter",
"val": (mf + 1) % 3, "desc": "树态 %s.mouse_filter → %d" % ["/".join(PackedStringArray(path)), (mf + 1) % 3]})
if n.get_parent() != null:
_mut_tree_templates.append({"top": top_i, "path": path, "prop": "name",
"val": String(n.name) + "__mut", "desc": "树态 %s.name 改名" % "/".join(PackedStringArray(path))})
if path.size() < 3:
for c in n.get_children():
var np: Array = path.duplicate()
np.append(String(c.name))
stack.push_back([c, np])
func _tree_state_candidates() -> Array:
var cands: Array = []
for t in _mut_tree_templates:
var tmpl: Dictionary = t
var apply_f := func() -> bool:
_mut_pending = [tmpl]
return true
var restore_f := func() -> void:
_mut_pending = []
cands.append({"weapon": "state", "file": "/root(树态)", "line": -1, "kind": "tree_prop",
"desc": "[状态·树] %s" % String(tmpl["desc"]), "apply": apply_f, "restore": restore_f})
return cands
func _apply_candidate(c: Dictionary) -> bool:
var f: Callable = c["apply"]
return bool(f.call())
func _restore_candidate(c: Dictionary) -> void:
var f: Callable = c["restore"]
f.call()
# 变体编译+路径接管:变体为无路径 GDScript(去掉 class_name 行——重名注册会 hides-a-global-class
# 编译失败),编译成功后 take_over_path 让后续 load(路径) 取到变体;原件对象零 reload 零触碰
func _take_over_variant(path: String, src: String) -> bool:
var v := GDScript.new()
v.source_code = _strip_class_name_line(src)
if v.reload() != OK:
return false
v.take_over_path(path)
return true
func _strip_class_name_line(src: String) -> String:
var ls := src.split("\n")
for i in ls.size():
if String(ls[i]).strip_edges().begins_with("class_name "):
ls.remove_at(i)
break
return "\n".join(ls)
func _verify_scripts_restored() -> String:
var bad: Array[String] = []
for p in _orig_scripts.keys():
_orig_scripts[p].take_over_path(String(p)) # 兜底夺回(正常已由逐候选还原完成)
for p in _orig_scripts.keys():
if load(String(p)) != _orig_scripts[p]:
bad.append(String(p) + "(缓存映射非原件)")
for p in _orig_src.keys():
if FileAccess.get_file_as_string(String(p)) != String(_orig_src[p]):
bad.append(String(p) + "(磁盘源码变动!)")
if bad.is_empty():
return ""
return "、".join(bad)
# ============================ user:// 真实档守卫 ============================
func _backup_user_save() -> void:
_user_backup.clear()
for p in _USER_GUARD_FILES:
var path := "user://" + String(p)
if FileAccess.file_exists(path):
var f := FileAccess.open(path, FileAccess.READ)
_user_backup[p] = f.get_buffer(f.getLength())
f.close()
else:
_user_backup[p] = null
var cnt := 0
for p in _USER_GUARD_FILES:
if _user_backup.get(p, null) != null:
cnt += 1
print("[mut] user:// 真实档已备份(在档 %d 个)——跑毕核对零触碰" % cnt)
func _restore_user_save() -> void:
var touched := 0
for p in _USER_GUARD_FILES:
var path := "user://" + String(p)
var orig = _user_backup.get(p, null)
var cur_exists := FileAccess.file_exists(path)
if orig == null:
if cur_exists:
DirAccess.remove_absolute(ProjectSettings.globalize_path(path))
touched += 1
else:
var cur := PackedByteArray()
if cur_exists:
var f := FileAccess.open(path, FileAccess.READ)
if f != null:
cur = f.get_buffer(f.getLength())
f.close()
if cur != orig:
var w := FileAccess.open(path, FileAccess.WRITE)
w.store_buffer(orig)
w.close()
touched += 1
if touched > 0:
push_error("[mut] 警告:user:// 真实档被 %d 个文件触碰,已按开工备份还原" % touched)
else:
print("[mut] user:// 真实档零触碰(跑毕与开工逐字节一致)")
# ============================ 结果落盘与台账 ============================
func _result_path(host_id: String, suite: String) -> String:
return _RESULT_DIR + "/" + host_id + "__" + suite + ".json"
func _write_result(host_id: String, suite: String, data: Dictionary) -> void:
data["generated_at"] = Time.get_datetime_string_from_system()
var f := FileAccess.open(_result_path(host_id, suite), FileAccess.WRITE)
if f == null:
push_error("[mut] 结果写盘失败:" + _result_path(host_id, suite))
return
f.store_string(JSON.stringify(data, " "))
f.close()
var nv := 0
var na := 0
for r in data["rows"]:
if r["status"] == "verified":
nv += 1
else:
na += 1
print("[mut] 结果已落盘 %s(%d 条:验红 %d / 异常 %d)" % [_result_path(host_id, suite), data["rows"].size(), nv, na])
func _exit_code_from_results() -> int:
var files := _list_results()
if files.is_empty():
return 2
var unresolved := 0
for fp in files:
var data := _read_json(String(fp))
for r in data.get("rows", []):
if r["status"] != "verified" and String(r.get("reason", "")) == "":
unresolved += 1
return 1 if unresolved > 0 else 0
func _list_results() -> Array:
var out: Array = []
var dir := DirAccess.open(_RESULT_DIR)
if dir == null:
return out
dir.list_dir_begin()
var f := dir.get_next()
while f != "":
if not dir.current_is_dir() and f.ends_with(".json"):
out.append(_RESULT_DIR + "/" + f)
f = dir.get_next()
dir.list_dir_end()
out.sort()
return out
func _read_json(path: String) -> Dictionary:
var f := FileAccess.open(path, FileAccess.READ)
if f == null:
return {}
var data = JSON.parse_string(f.get_as_text())
f.close()
return data if data is Dictionary else {}
# 台账落盘路径:缺省 = 仓库根 reports/(res:// 映射 game/ 切片,仓库根 reports/ 是其上级目录——
# 直接写 res://reports/ 会在 game/ 下造新目录,触熵增闸红区);--out 显式给绝对路径时原样用。
func _ledger_out_path() -> String:
var given := String(_args.get("out", ""))
if given != "" and not given.begins_with("res://"):
return given
if given.begins_with("res://reports/"):
var base := ProjectSettings.globalize_path("res://")
return base + given.substr("res://".length())
return ProjectSettings.globalize_path("res://") + "../reports/批B-变异统一台账.md"
func _build_ledger(out_path: String) -> int:
var files := _list_results()
if files.is_empty():
print("[mut] 台账中止:无结果 JSON(%s)" % _RESULT_DIR)
return 2
var ordered: Array = []
var ordered_keys := {}
for h in Plan.hosts():
for s in h["suites"].keys():
var p := _result_path(String(h["id"]), String(s))
if FileAccess.file_exists(p):
ordered.append(_read_json(p))
ordered_keys[p] = true
for fp in files:
if not ordered_keys.has(String(fp)):
ordered.append(_read_json(String(fp)))
var total := 0
var verified := 0
var anom := 0
var unresolved := 0
var md := ""
var cmd_disp := String(_args.get("label", "")) if _args.has("label") else " ".join(OS.get_cmdline_user_args())
md += "# 批 B 变异统一台账(机器生成)\n\n"
md += "> 生成命令:`godot --headless --path game --script res://tests/mutation/mutation_engine.gd -- %s`\n" % cmd_disp
md += "> 生成时间:%s · 引擎:游戏环境启动一次,逐条执行「内存内改坏 → 重跑该断言所在套函数 → 验 FAIL → 撤销 → 记录」,全程不改任何文件。\n" % Time.get_datetime_string_from_system()
md += "> 三武器:table=已加载 Resource 内存字段改写 · inject=生产脚本源码内存变换+reload 原地重编译(零触盘)· state=对象状态篡改(长活成员/静态与初值/autoload 树状态)。\n"
md += "> 中间结果 JSON 在 `user://mutation_work/results/`(仓库外);本文件为唯一持久产出。\n\n"
md += "## 一、逐条记录\n\n"
md += "| 宿主 | 套 | # | 标签 | 武器 | 改坏点 | 结果 | 复验轮耗时 | 连带红 |\n"
md += "|---|---|---|---|---|---|---|---|---|\n"
for data in ordered:
if data.is_empty():
continue
for r in data.get("rows", []):
total += 1
var status := String(r["status"])
if status == "verified":
verified += 1
else:
anom += 1
if String(r.get("reason", "")) == "":
unresolved += 1
var cand_disp := String(r.get("cand", ""))
var cfile := String(r.get("cand_file", ""))
if cfile != "" and status == "verified":
cand_disp = "%s:%s %s | %s" % [cfile.get_file(), String(r.get("cand_line", "")), String(r.get("cand_kind", "")), cand_disp]
md += "| %s | %s | %d | %s | %s | %s | %s | %s | %s |\n" % [
_md_cell(String(data["host"])), _md_cell(String(data["suite"]).lstrip("_")), int(r["idx"]),
_md_cell(String(r["label"])), _md_cell(String(r.get("weapon", ""))), _md_cell(cand_disp),
("验红" if status == "verified" else "异常"),
("%dms" % int(r.get("ms", 0)) ) if status == "verified" else "—",
str(int(r.get("collateral", 0))) if status == "verified" else "—"]
md += "\n## 二、异常清单(打不红的条+原因)\n\n"
if anom == 0:
md += "(无——全部条目验红)\n"
for data in ordered:
if data.is_empty():
continue
for a in data.get("anomalies", []):
var idxs := int(a.get("idx", -1))
var lbl := ""
for r in data.get("rows", []):
if int(r["idx"]) == idxs and String(r["status"]) == "anomaly":
lbl = String(r["label"])
break
md += "- %s:%s #%s「%s」——%s\n" % [data["host"], String(data["suite"]).lstrip("_"),
(str(idxs) if idxs >= 0 else "—"), _md_cell(lbl), _md_cell(String(a["reason"]))]
for data in ordered:
if not data.is_empty() and not bool(data.get("end_baseline_green", true)):
md += "- %s:%s——跑后基线未复绿(存在跨试跑状态残留)\n" % [data.get("host", "?"), String(data.get("suite", "?")).lstrip("_")]
md += "\n## 三、汇总\n\n"
for data in ordered:
if data.is_empty():
continue
var v := 0
var a2 := 0
for r in data["rows"]:
if r["status"] == "verified":
v += 1
else:
a2 += 1
md += "- %s:%s——%d 条(验红 %d / 异常 %d)· 改坏试跑 %d 轮 · 跑后基线%s\n" % [
data["host"], String(data["suite"]).lstrip("_"), data["rows"].size(), v, a2,
int(data.get("tried", 0)), "复绿" if bool(data.get("end_baseline_green", true)) else "**未复绿**"]
md += "\n合计 %d 条:验红 %d / 异常 %d。\n" % [total, verified, anom]
var f := FileAccess.open(out_path, FileAccess.WRITE)
if f == null:
push_error("[mut] 台账写盘失败:" + out_path)
return 1
f.store_string(md)
f.close()
print("[mut] 台账已生成 %s(%d 条:验红 %d / 异常 %d)" % [out_path, total, verified, anom])
if total == 0:
return 2
return 1 if unresolved > 0 else 0
func _md_cell(s: String) -> String:
var out := s.replace("|", "/").replace("\n", " ").replace("\t", " ")
if out.length() > 96:
out = out.substr(0, 96) + "…"
return out