4b841e019a5b5c875f0419d83e650da593d1f931
4 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 4b841e019a |
修复: "没读数"到不了读者 —— summary.py 的 rc=2 与三行 ✗✗ **一个字都没进套件输出**(pi 实测的口子 + 我实测的第二例:部分可读时那行**看着完全正常**)
pi 2026-09-18 报的是**我上一轮新引入**的口子。我复现了它,并实测出**第二种**它没提、
且**只靠它的修法①治不了**的情形。
## 一、pi 那一例:盲读时 `mutants=0` 与"真的没有变异体"长得一样
`run-all.mjs` 原来只做:
```js
const m = /(RESULT mutants=\d+ ran=\d+ skipped=\d+ on_new_criteria=\d+)/.exec(sp.stdout);
mutantsLine = m ? ` ${m[1]}` : ` mutants=(… status=${sp.status} …)`;
```
⇒ **只在正则不匹配时才看 `status`**。而盲读时 `summary.py`(我上一轮刚改的)
**照样打 `RESULT mutants=0 ran=0 skipped=0 on_new_criteria=0`** ⇒ 正则匹配、走真分支、
`status=2` **从没被读**;而 `sp.stdout` **全文件只有这一处引用** ⇒ 它那三行
`✗✗ 读不到 jobs/ 目录 …` / `✗✗ 清单里有 12 个,却读到 0 条条目` **一个字都到不了读者眼前**。
**复现(跑出来的,不是读代码)**:`nobody` 下跑真 `summary.py`,rc=2、三行 ✗✗ 都在,
再拿 run-all 里**那一行**的正则去匹配它:
```
m 为真? true → 套件会播报: RESULT mutants=0 ran=0 skipped=0 on_new_criteria=0
```
⇒ 与"这棵树真的一个变异体都没有"**长得一模一样**,而真相是"**没读数**"。
★ 我上一轮自己在 `summary.py` 注释里写过"那边也会看到 `mutants=0`"并当成**可接受** ——
理由是"这一行自己带 ✗✗ 说明"。**错了**:那几行不在套件的输出里,所以没有"自己带说明"。
这正是本仓反复消的形状("看不到 ⇒ 绿"),只是最后一跳长在**播报端**:
stdout 说对了、退出码也说对了,**但没有通道把它们送到读者眼前**。
## 二★ 我实测出的第二例:**部分可读**(pi 那封没提,且它的修法①兜不住)
目录**能**进入、但**单个 job 文件**读不到(`chmod 000 jobs-one.json`)时:
```
$ runuser -u nobody -- python3 summary.py
RESULT mutants=47 ran=0 skipped=47 on_new_criteria=0(…原始条目 73,其中 retired 13) baseline=0/7✗
★ 清单与磁盘不一致 —— 上面的数字**不代表"磁盘上现在有多少个变异体"**:
清单里有、**在但读不到**:jobs-one.json(权限问题,不是缺失 —— 改权限,别删条目)
rc=2
```
那行 `mutants=47 …` **看着完全正常**,而且**它匹配正则**。⇒ pi 的修法①
(盲读时改打 `mutants=NO-READING`)**对它无效**,因为这里 `blind` 为假、数字是真算出来的。
**只有"先判 status"能兜住它** —— 所以两层修法不是叠保险,是各治一例。
## 三、修法:先判退出码,再把原因行**转印**出来
1. `summary.py` 盲读时改打 `RESULT mutants=NO-READING …`(不匹配该正则,让读数器
**自己说"我没读数"** —— 与我在 `summary.py` 里做的是同一件事,只是补上到套件这一段)。
2. `run-all.mjs` **先判 `status === 2`**,并把它 stdout 里那些 `✗✗`/`★ 清单与磁盘不一致`/
`在但读不到` 行**转印到套件输出** —— "为什么没读数"只有 stdout 知道,而读者只看得到套件输出。
非 0/2 的异常退出码也留痕(`(注意:summary.py status=N)`),不许静默。
**真端到端验证**(不是读代码):把工作树复制到 `/tmp`,`runuser -u nobody` 跑**整个套件**:
| 场景 | 套件播报的 mutants 那格 |
|---|---|
| 正常(root) | `mutants=48 ran=47 skipped=1 on_new_criteria=35` |
| 盲读(nobody) | `mutants=(**环境:没读数**,summary.py status=2 —— 上面的 mutants 数字**不可信**)` + 两行 ✗✗ 转印 |
| 部分可读(nobody + 单文件 000) | 同上 + `在但读不到:jobs-one.json(权限问题,不是缺失)` 转印 |
## 四、抽出纯函数 + 自检:因为这个分支**在正常路径上走不到**
盲读只在**非 root + `jobs/` 不可读**时发生,而套件以 root 跑 ⇒
那段决策**只在我手跑 `runuser` 时才经过**。"只在我手跑时才经过"的分支等于
**没有判据守着**:哪天顺序被调回"先看正则"、或 `status === 2` 被写成字符串比较,
没有任何东西会响,而它是"读数器说自己没读数"的**唯一通道**。
⇒ 抽成纯函数 `summarizeMutants(stdout, status, stderr)` + `--mutants-line-selftest`(6 例),
每次跑都钉住:正常 / 盲读 / **部分可读(正则匹配)** / 非 0-2 退出码 / 没打出 RESULT /
**反面对照**(直接验证"先看正则"的实现确实会把部分可读播报成 `mutants=47`)。
**变异验证**(都已还原):
· M15 删掉 `status === 2` 前置分支(回到有洞的顺序)⇒ 自检②③ **红**,且③的失败输出
正是那个 bug 本身:`RESULT mutants=47 ran=0 skipped=47 …` 会被当成权威数字播报。
· M16 `whyLines` 置空(不转印原因)⇒ 自检②③ **红**(转印 0 行,期望 1/2 行)。
## 五、验证与状态
· 全套件 `checks=459 pass=455 fail=4 skip=0 red=9 broken=0 unreported=0` —— 与改动前**同样 9 条**
(并发会话的"自报>清单" + 4 条 exit 1),`mutants=48 ran=47 skipped=1 on_new_criteria=35` 不变。
· `--mutants-line-selftest` 6/6 绿、`--skip-selftest` 5/5 绿、`--probe-selftest` 3/3 绿。
· 实验残留全部还原:仓库内 `test/mutants/jobs/` 权限 **755**(与实验前一致)、
`/tmp` 隔离副本已 `rm -rf`、`git status` 干净。
|
|||
| 4f386199cf |
修复: 集合指纹**不是集合的函数**(顺序敏感 + 只含 name:size);"读不到"曾静默报成"全 0 且指纹正常";"设备忙"的跳过**加界**;criteria-hygiene 两处按字面量裁射程
pi 2026-09-18 报的两个洞我**都当场复现了**(不是我信了,是跑出来了),修完都验了阳性对照。
## 1. 集合指纹必须是集合的函数(pi 洞 1,两个都复现)
原来 `'\n'.join(f'{f}:{getsize(f)}' for f in listed)`。实测:
· **顺序敏感**:只把 `jobs.manifest.json` 反序(集合/内容/计数全不变)
⇒ `sha=02502771` → `3439e049`。于是"集合变了 ⇒ 一眼看得出"失效,
**每次清单整理都假变**。
· **只含 name:size**:字节级等长改写(`jobs-one.json` 的 `name: 'image'` → `'imoge'`,
265 字节不变)⇒ **指纹仍是 02502771**。更尖锐的是 file 路径改成等长的
`ApiClienX.ets`(指向不存在的文件)时 `ran/skipped/on_new_criteria` 全变而
**指纹不变** —— **"集合没变而数变了"恰恰是它声称要抓的情况,它抓不到。**
⇒ `sorted()` + 内容哈希。实测:原序/反序/排序三种**同一哈希**(`33ff3bea`);
等长改内容 ⇒ 变(`89b2aa41`)。
## 2. "读不到" ≠ "不存在"(pi 洞 2,复现 + 比 pi 说的更糟)
隔离副本 + `runuser -u nobody` + `chmod 644 jobs/` 实测:
```
nobody: mutants=0 ran=0 skipped=0 … 原始条目 0 sha=e3b0c442 rc=0
```
`e3b0c442` = **空字符串的 sha256**;而且退出码 **0**。根因:`exists()` 对
**不可进入目录里的文件**返回 **False**(实测),12 个全被跳过;而 `glob` 那一半
照样列得出 12 个 ⇒ `unlisted`/`ghosts` 全空 ⇒ **"清单与磁盘不一致"那条警告一声不响**。
**同一份权限,两个半边给出互相矛盾的结论。**
⚠️ 比 pi 说的更糟的一点:那行**同时**印着 `清单 12 个 job 文件` 和 `sha=e3b0c442`
—— 一句话里说"12 个"却一个都没读到。
⇒ ① 判目录可进入性(`access(R_OK|X_OK)`),读不到就 `✗✗` 明说数字**全部无效**、
并点明"这不是空集合";② `open` 接住 `OSError`,把读不到的文件**攒起来一次报全**
(原来会死在第一个文件上抛 `PermissionError`,让人以为"就这一个");
③ 退出码 **2**(本仓约定:2=环境)—— 读不到就是没读数,而**没读数不是成功**。
④ `unreadable` 与 `ghosts` **分开报**:修法完全不同(修权限 vs 删条目)。
★ 这里我先写了个**不可达的分支**:`unreadable` 先探后读,而真读时 `PermissionError`
会提前抛出 ⇒ 那段报告永远走不到("判据在,但走不到",这次长在报告分支上)。
自己查出来并改成"读的时候接住",才让它成为可达的真分支。
## 3. "不抢前台"的跳过**加界**(pi 2026-09-18 §3,我接受)
只跳过不设界,"设备忙"会变成到期判据的**永久灰区**:不算红不算绿 ⇒ 永远不必被升级
—— 到期机制要防的正是这个,只是入口换成了"设备忙"。⇒ 连续 K(默认 3)轮没跑成,
**跳过自己变红**并给出接管路径(K 轮内是礼貌,K 轮外是闹钟)。
边界:**只对"设备在、前台不是我们的"计数**;**设备不在不计数也不变红** ——
那是 `PROBES.device` 的既有裁定(没装 SDK 的机器不该天天假红),超出本模块职责。
账本 `.tmp/harmony-busy-skips.json`(已 gitignore):判"**这台机器上**连续多少轮没验成",
换机器不继承。实测 1→2→3 轮 skip、第 4 轮起 FAIL;跑成一次即清零、再从 1 重新数。
自检(不连设备)把 ①K 轮内礼貌 ②超了必红 ③跑成清零 ④上限可覆盖 都钉住,
变异验证 M7(over 恒 false)⇒自检②红、M8(noteRan 不清零)⇒自检③红。
## 4. 顺带修掉 criteria-hygiene 自己两处"按字面量裁射程"
我按纪律把账本读取从裸 `readFileSync` 改成 `prose()`(`criteria-hygiene` 立刻红,
**那条判据是对的、我错了**),接着暴露出该判据自身两个洞:
· **import 按写法硬匹配**:只认 `'./lib/read.mjs'`/`'../lib/read.mjs'`,
而 `harmony-device.mjs` **就在 `lib/` 里**、按惯例写 `'./read.mjs'` ⇒
假红"根本没 import"(其实 import 了、运行时完全正常)。改成**解析说明符后与
`SELF` 比**(本仓已用"解析后比较"解决过同一族:`abspath`、`join(HERE,…)`)。
· **用 `prose()`(原文)判"用没用"** ⇒ **注释里**写 `prose(…)` 就算用了
(实测该文件命中 3 处、只有 1 处是真调用)。改成 `code()`。这与它上面那条
"不许裸用 readFileSync"踩过的是同一个坑,我在那条上写了理由、**这条漏了**。
两处修法都做了承重验证:删真 import ⇒ 红;import 换成 `code as prose2`(别名)⇒ 红;
只在注释里写 `prose(` ⇒ **绿**(对照旧实现:同一份样本 ⇒ **红**)。
## 验证
· 全套件 `checks=459 pass=455 fail=4 skip=0 red=9` —— 红的仍是同样 9 条(都是并发
会话的"自报 > 清单"与 4 条 exit 1),我中途引入的两条(裸 readFileSync、import 假红)
已消。`mutants=48` 不变。
· 所有 chmod/清单反序/等长改写**都已还原**,`git status` 只剩本次 5 个文件。
|
|||
| 590a72a715 |
修复: job 集合从 glob 改成**清单**(+ 清单外即报,与 SUITE 同形状)+ RESULT 行带集合指纹;baseline 播报区分"底本过期"与"变异残留";权限政策补上**目录可进入性**(原来只管文件)
pi 2026-09-18 报的三件,逐条实测后处置。
## 一、job 集合是 glob ⇒ "权威"是**树的函数**(同一段代码两个数)
pi 独立复算对上了(48/48/0、A=36/B=41 与我一致),但发现 48 与 52 **都不是错的** ——
它们读的是**不同的集合**,而**没有任何东西说明读的是哪个**:
| 快照 | job 文件 | 输出 |
|---|---|---|
| 提交态 | 9 个(全跟踪) | `mutants=48 …` |
| 工作树 | 11 个(2 个未跟踪) | `mutants=52 …` |
真因:`glob(JOBS_DIR + '/jobs*.json')` ⇒ **未跟踪的 job 文件静默进入统计**。
这与"写了判据忘了接线"同族,区别是 `run-all.mjs` 有**自检 2** 挡着(清单外的 `*.test.mjs` 直接红),
这边没有对等物。**本仓用"清单 + 清单外即红"解决过同一个问题两次,这是第三次。**
**处置(pi 建议的前者)**:新增 `jobs.manifest.json` 显式列 job 集合,`summary.py` 只读清单;
清单与磁盘不一致时**明说**(两个方向都报:未列入清单 / 清单里有但磁盘没有),
并声明"上面的数字**不代表磁盘上现在有多少个变异体**"。
**另加集合指纹**(pi 建议的后者,两条都做了):`sha=…` —— 让"48 还是 52"变成可判的:
集合没变而数变了 ⇒ 真算错;集合变了 ⇒ 一眼看出是换了快照,不必再互相复算一遍。
**变异验证**(两个方向都试):
```
加一个未列入清单的 job 文件 ⇒ 报「未列入清单:jobs-UNLISTED-probe.json」
且 mutants=**48**(未被静默计入)✓
清单里加个不存在的文件 ⇒ 报「清单里有、磁盘上没有:jobs-GHOST.json」✓
```
(`_` 开头的条目是说明,读时滤掉 —— 否则会被当成文件名,`ghosts` 假报一堆。)
## 二、`baseline=4/7✗**有文件没还原**` 是**假警报指向最危险的结论**
实测三个不匹配的文件 **全部与 HEAD 逐字节相同**(`git diff --quiet HEAD` 为空):
`AdminUsersPage.ets`/`SettingsPage.ets` 是提交 `6861934`(09-17 21:15)改的、
`ApiClient.ets` 是 `9c6e9c6` 改的 ⇒ **底本过期,不是变异残留**。
★ 而原来一律打"✗**有文件没还原**" —— 那会让人去翻变异,而真因只是底本没跟上提交。
**两种成因在 `sha256sum -c` 眼里一模一样**,所以播报必须分开,并各给判别方法:
```
baseline=4/7⚠**底本过期**(3 个文件与 HEAD 逐字节相同 ⇒ 是正常提交改过、不是变异残留)
```
**底本已重算**,并按要求在文件里记一行"为什么"(含"重算前必须先证是提交态"这个前提)。
**重算后仍在校验**(变异验证):给 `AppearanceApi.ets` 追加一行 ⇒ 立刻 `FAILED`;还原 ⇒ `7/7 OK`。
## 三、`summary.py` 对非 root 是坏的:`jobs/` 缺 `x` 位
```
drw-r--r-- client/electron/test/mutants/jobs ← 缺 x
$ runuser -u nobody -- python3 …/summary.py
PermissionError: [Errno 13] Permission denied: '…/jobs/jobs-all.json'
```
⇒ 那份"口径的唯一权威"**只有 root 跑得起来**。已 `chmod 755`,非 root 复跑输出一致。
★ pi 的深层判断成立且我核实了:**这条落在任何判据的射程之外** ——
`deploy/check-file-modes.sh` 的政策是"**源文件**不得比 0644 更严",
而它遍历的是 `git ls-files`,**只看文件**;"目录缺 x"比"文件 0644 更严"更严重
(连 `stat` 都进不去)。
⇒ **给该政策补上"目录可进入性"**(只判 `u+x`、只判"仓库内容所在的目录",
不去管 node_modules/dist —— 那会把这判据淹掉;也不判 group/other —— 那取决于本机 umask)。
**★ 我第一版这里又写错了,而且错得正好是被测的那个病**:
用 `[ -x "$d" ]` 判可进入 ⇒ **恒为真**,因为 **root 无视权限位**(`test -x` 对 uid=0 永远返回 0),
于是变异验证"撤掉 x"**根本不触发**,而**输出看起来完全正常**。
⇒ 改成**直接读权限位**(与同文件里文件那段的位运算同一做法)。
改后变异验证:撤 x ⇒ `[FAIL] 目录不可进入:…(drw-r--r--,缺属主 x 位)`、退出码 1;还原 ⇒ 0。
## 四、顺带修好三处既有权限违规(`check-file-modes.sh` 原来一直红着)
```
.githooks/pre-push 711 ⇒ 755(保留执行位,**不是**去掉)
test/harmony-arkts.test.mjs 600 ⇒ 644
model/DeviceProbe.ts 600 ⇒ 644
```
三者都是**提交态**就这样(不是并发会话弄的)。按脚本自己的"药方按类型分岔"修的。
现在该政策 **退出码 0**。★ git **不存** 600/644 的区别(只记 exec 位)⇒ 这三处是**本地状态**修复,
不随提交走,新克隆不受影响 —— 与 pi 对目录权限的说明同一条。
## 五、pi 报的另两件:**在 HEAD 上已经不存在了**(是过时读数)
- **ArkTS 两处阻断**:`MainPage.ets` 的 import 现在在 71-74 行、最后一个 `const` 在 88 行
⇒ **位置正确**;`AdminUsersPage.ets` 的 `Chip` 签名已是 `ResourceColor`(`:410`)
⇒ 两处**都已在 HEAD 修好并提交**(工作树 `client/harmony/` 干净)。`harmony-arkts` 判据 3/3 绿。
- **套件现状**:现在 **29** 个判据文件(不是 23),`unreported=0 broken=0`。
## 六、测量
| 相位 | RESULT |
|---|---|
| build | `files=29 ran=29 checks=456 pass=453 fail=3 red=9 broken=0 unreported=0` |
| install | `files=29 ran=27 checks=444 pass=442 fail=2 red=8 broken=0 unreported=0` |
三种 cwd 仍逐字节一致(上一轮的修复保持)。
|
|||
| 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`。
|