#!/usr/bin/env python3 """按"行范围替换"做变异(避开多行锚点的空白问题),跑判据,然后**逐字节还原**。""" import subprocess, sys, hashlib, shutil, tempfile, os, re # test 键 → 判据文件:**唯一来源**是同目录的 test-keys.json # (此前这里与 summary.py 各有一份,分叉过一次 —— 见那个文件的 `_` 说明) import json as _json, os as _os _KEYS = _json.load(open(_os.path.join(_os.path.dirname(_os.path.abspath(__file__)), 'test-keys.json'), encoding='utf-8')) TESTS = {k: v for k, v in _KEYS.items() if k != '_'} def run(path, pat, repl, testname, why): # ★ 不用 tempfile.mktemp():它自 Py3 起 deprecated(**TOCTOU 竞争**:名字先给你、 # 文件后创建,中间别人能占位)。而且它直接接受一个"还没创建的文件名", # 正好踩在那个竞争上。改成**先原子创建**再往里拷。 # (pi 2026-09-15 指出;同一族的另一个坑在 run.sh:那份用的是**固定**的 # /tmp/mut/bak,两个变异并行跑会互相覆盖 —— 已改成按 pid/随机后缀。) _fd, bak = tempfile.mkstemp(prefix='mut-bak-', suffix='.orig') os.close(_fd) shutil.copy2(path, bak) before = hashlib.sha256(open(path,'rb').read()).hexdigest() src = open(path, encoding='utf-8').read() hits = list(re.finditer(pat, src)) if len(hits) != 1: print(f" [SKIP] 匹配 {len(hits)} 次(要 1 次): {why}") os.unlink(bak); return open(path,'w',encoding='utf-8').write(src[:hits[0].start()] + repl + src[hits[0].end():]) r = subprocess.run(['node','--test',TESTS[testname]], capture_output=True, text=True) shutil.copy2(bak, path); os.unlink(bak) after = hashlib.sha256(open(path,'rb').read()).hexdigest() assert before == after, f"!! 还原失败 {path} —— 必须立刻处理" print(f" [{'红不了·缺陷' if r.returncode==0 else '红✓'}] {why}") if __name__ == '__main__': import json for job in json.load(open(sys.argv[1], encoding='utf-8')): run(job['file'], job['pat'], job['repl'], job['test'], job['why'])