Files
MailUI4Agents/client/electron/test/mutants/mut.py
JianFeeeee bcd4f97e37 跨端: 变异体计数收进仓库 —— 前面报过 40/48/58 四个数,根因是"job 集合"从没定义
pi 用 `/tmp/mut/` 复算后指出:48 也不对。他是对的,而且**不是记性问题、是口径问题**——
我把 9 个 `jobs*.json` 的条目**直接相加**,没做归一:同一个变异体在跨批重锚时被键了多次
(13 组重复、15 条冗余),最典型的是「计划不搬运模糊值」同时挂在 `jobs-b3`/`jobs-blur`/`jobs-blur2`
三个文件、三个不同 `test` 键上 —— 于是"按判据文件分组求和"必然把它算三次,
而"跑在新增判据上的是多少"在交叉归类下**没有唯一答案**(41 或 35)。

更根本的是 pi 指出的第二层:**那个统计脚本根本不在 `/tmp/mut/` 里**(他为了复算是现写的),
而且 `/tmp` 会被清、不在仓库里 ⇒ 变异体数字**只活在信里**。
这一路已经立过同形状的规则(余额打在 `RESULT` 行、权威源在文件里),这条当时漏了。

## 做了什么

- `client/electron/test/mutants/`:把 `mut.py`、`run.sh`、`jobs*.json`、`baseline.sha`
  从 `/tmp` 挪进仓库(`/tmp` 会清、复核方够不着)。
- `mutants/summary.py`:**口径的唯一权威**,定义写死在代码里:
  · 不同变异体 = 按 (file, pat, repl) 去重(`retired` 不计);
  · 跑起来 = 锚点在该文件里**恰好命中 1 次**(与 mut.py 同一条件);
  · `on_new_criteria` = 该变异体的**每一个** test 键都指向本批新增的两个判据文件
    (口径 A —— 不因交叉归类虚高;另报口径 B 作参考,它只增不减,不拿来报数)。
- `run-all.mjs` 的 `RESULT` 行播报它,并**顺带自证基线**:跑不起 `summary.py` 时
  **不静默**(打印 status 与 stderr 末行)——我第一版路径写错,只看到"计数未知",
  真因(`can't open file …/test/test/mutants/summary.py`)被吞掉了。
- `mutants/test-keys.json`:`test` 键 → 判据文件的**唯一来源**(`mut.py` 与 `summary.py`
  共用)。此前两处各写一份,分叉过一次:键名从旧名换成 API 名后 `summary.py` 那份没跟上,
  于是所有锚点被判 `hits=-1`、报出"51 个变异体全部 skipped"。
- 无歧义口径下的**当前真值**:`mutants=48 ran=48 skipped=0 on_new_criteria=36 baseline=7/7✓`
  (口径 B = 41;原始条目 66,其中 `retired` 5)。
- 清掉 pi 指出的三类脏数据:
  · **过期条目**(锚点是修复前的旧写法,`hits=0`)标 `retired` 5 条 ——
    它们**不是"没跑成的变异体"**,重锚后都跑过、都红了;留着只会把 skipped 一直抬高;
  · **重复计数**(multipart 那条在两个文件里各一次)去重;
  · **真 skip** 的 multipart 锚点切片成 `name: 'file',\n contentType: mimeType` ⇒ 真的跑起来了
    (此前命中 2 次,因为 `ApiClient.ets` 有两个 multipart 构造器)。
- 修两处并发/竞争:`mut.py` 的 `tempfile.mktemp()`(Py3 起 deprecated,**TOCTOU**)→ `mkstemp`;
  `run.sh` 的固定 `/tmp/mut/bak` → 按 `$$-$RANDOM` 唯一(并行跑会互相覆盖备份)。

## 未做(如实说)

- **`AdminUsersPage.ets` 有一处不是我做的改动留在工作树里**(11:20:48,我 11:21 的提交之后):
  `Chip(text, bg, fg: string)` → `ResourceColor`。核实过是**正确的 ArkTS 修法**
  (`Theme.surfaceMuted`/`textSubtle` 是 `Resource`、`chipNeutralBg` 是 `string`,
  第 368 行的三目因此是 `Resource | string` ⇒ 旧签名**编译不过**)。
  我**没有提交也没有回退**它 —— 工作树是共享的,不该替别人提交别人的活。
  基线因此重算了(`baseline.sha` 顶部记了原因与哈希来源,重算本身是**有意动作**:
  随手重算会把"某次变异没还原"永久掩盖掉)。
- Go 侧 `debt_registry_test.go` 仍未跑(沙箱无 Go 模块缓存),只做了 `gofmt`。
2026-09-15 11:23:41 +08:00

37 lines
2.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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'])