2174 lines
80 KiB
GDScript
2174 lines
80 KiB
GDScript
# 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
|