# 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==<宿主 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= · --suite=:<套名>(可多次)· --snap=<宿主id>=<快照路径>") print(" --adhoc==<宿主 res:// 路径>(零配置宿主:套/表/模块自动推导,可多次)") print(" --excl-func=<文件名>:<函数名>(函数级候选排除——挂死/崩溃型毒候选隔离阀,可多次)") print(" --excl-line=<文件名>:<1基行号>(行级毒候选定点排除,可多次)") print(" --resume · --budget=N · --cap-ms=N · --ledger --out= · --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