修复: 第三例落在两层之间 —— unlisted/ghosts 退 **0** ⇒ 已算出的警告被下一层丢掉;并把**上游退出码**也钉进自检(我变异时发现下游自检守不住它)
pi 2026-09-18 报的第三例。我复现了它,修完后又**自己变异出一个更值得记的问题**:修完之后 `--mutants-line-selftest` 仍守不住上游。 ## 一、第三例:复现(pi 报的那一例,端到端) 加一个未列入清单的 job 文件,改前实测: ``` 磁盘上 job 文件数 13 summary.py 自己说"未列入清单" 1 次 summary.py 退出码 0 ← 就是这里 套件报的 mutants mutants=48 ran=47 skipped=1 on_new_criteria=35 套件输出里 grep "未列入清单" 0 次 ← 一个字都没到读者眼前 ``` 根因两层:①`summary.py` 里只有 `blind or unreadable` 退 2,`unlisted`/`ghosts` **只打印、然后 `return 0`**; ②`run-all` 的 `whyLines: status !== 0 ? whyLines : []` 把**已经算出来**的警告又丢掉。 ⇒ 数字按清单算是**对的**,错的是**读的人不知道它不是全集**。 ★ 这**正是** `590a72a` 标题那句承诺("清单外即报,与 SUITE 同形状")和那边注释 ("与 run-all.mjs 自检 2 同形状:**清单外即红**")说的东西:自检 2 是**真的红**, 而这边只打印、退 0、打印被下一层丢掉 —— **"同形状"当时只同了前一半**。 ## 二、修法:退出码按**修法不同**分两类(照 pi 的提醒) | 情形 | 退出码 | 含义 | 读者该做什么 | |---|---|---|---| | `blind` / `unreadable` | **2** | 环境 | 去修权限 | | **`unlisted` / `ghosts`** | **1** | 清单/数据 | 去改 `jobs.manifest.json` | 按 `env-defaults.sh:25` 那条"别让环境问题冒充代码缺陷"的**反方向**:**也别让"清单没跟上"冒充环境**。 两类都退非零 ⇒ `run-all` 那边**既有的** `status !== 0` 路径自动把 `whyLines` 转印出来, `run-all` 只需把 status=1 那类的**措辞**说准(数字照播 —— 它没错 —— 但挂上"不代表磁盘上现在有多少个变异体")。 **端到端复验(跑出来的)**: | 场景 | summary.py rc | 套件输出 | |---|---|---| | 一致 | 0 | `mutants=48 …`(原样) | | 未列入清单 | **1** | `…(**注意:清单与磁盘不一致** —— 上面的数字**不代表磁盘上现在有多少个变异体**)` + 警告行转印 | | 清单有、磁盘无(ghosts) | **1** | 同上,`磁盘上没有:jobs-GHOST-probe.json` 转印 | ## 三★ 我修完后自己变异,发现**下游自检守不住上游** 把 `summary.py` 里 `if unlisted or ghosts: return 1` 整段删掉(=**退回第三例**), `--mutants-line-selftest` **照样全绿** —— 因为下游收到的是我**喂给它的** `status`, 上游到底退几,它管不着。**同一个缝换了个位置**:结论到达套件的那条通道,上游没有判据守着。 ⇒ 补 `--exitcode-selftest`:把**仓库里那份 `summary.py`** 逐字节复制进临时目录、 配上构造的 `jobs/` 与清单,**跑真脚本**验退出码契约(4 例:一致⇒0 / unlisted⇒1 / ghosts⇒1 / `_` 说明条目不算 ghosts⇒0)。重做 M17(删掉那段)⇒ **自检红、exit 1**,缝在两层都封住。 ★ 这个自检我第一版**只拷了一半依赖**(`summary.py` + `jobs/`,漏了 `test-keys.json` 与 `baseline.sha`)⇒ 每次都 `FileNotFoundError` 退 1。危险之处在于**四个案例里有两个期望 本来就是 1**,于是"没跑起来"**长得像**那两个通过。只有期望 0 的两条把它揭出来。 ⇒ 现在先判 stderr 里有没有 `Traceback`,有就单独报"**脚本没跑起来**,别把它当成退出码不对"。 ## 四、自检自身的两处错(照实记) 1. **声明值与现算值两份实现**:我既在案例里写 `want.why`,又用一条正则从 stdout **现算**一遍 期望条数 ⇒ `want.why` **从没被读**,且现算那条一旦与 `whyLines` 的过滤器不同步, 自检会**自证自恰**地绿。改成只读声明值 —— 立刻暴露出我两个声明值都写错了 (`partial` 真值 2 我写 1、`unlisted` 真值 3 我写 2)。**这正是本仓反复消的"同一事实多份实现"**。 2. **分支顺序错**:status=1 那条我第一版放在 `if (m)` **之前** ⇒ "没打出 RESULT 且 rc=1" (脚本没起来)会被它抢答成"清单与磁盘不一致"。自检⑤当场红,已收进 `if (m)` 内。 ## 五、验证与状态 · 四个自检全绿:`--mutants-line-selftest` **8/8**、`--exitcode-selftest` **4/4**、 `--skip-selftest` 5/5、`--probe-selftest` 3/3,都 exit 0。 · 全套件 `checks=459 pass=455 fail=4 skip=0 red=9 broken=0 unreported=0`(与改动前**同样 9 条**)、 `mutants=48 ran=47 skipped=1 on_new_criteria=35`。 · 变异:M17(删上游 `return 1`)⇒ 退出码自检红、exit 1;已还原(`sha256` 比对)。 · 实验残留全还原:`jobs/` 12 个、`jobs.manifest.json` 17 条且无 GHOST、 `/tmp` 隔离副本已删、`git status` 只剩本笔两个文件。
This commit is contained in:
@ -265,16 +265,32 @@ def main():
|
||||
print(f' hits={h} {key[0]} 「{group[0].get("why", "")}」 ({len(group)} 条条目:{srcs})')
|
||||
print(' ⚠️ hits=0 通常是**过期条目**(锚点是旧写法)—— 请标 retired 或删除,')
|
||||
print(' 否则它会把 skipped 一直抬高(方向与"让欠账显形"相反)。')
|
||||
# ★ 读不到 ⇒ **非零退出**(pi 2026-09-18 的洞 2 的关键:原来 rc=0)。
|
||||
# 读不到就是"没读数",而没读数**不是成功** —— 与本仓"失败要说清是环境问题、
|
||||
# 不要让它冒充代码缺陷"是同一套:这里更该退非零,因为它连"是环境还是代码"都判不了。
|
||||
# 退出码按本仓约定用 **2 = 环境问题**(见 `env-defaults.sh:25`):
|
||||
# 目录不可进入 / 有 job 文件读不到,都是环境,不是"变异体少了"。
|
||||
# (`run-all.mjs` 只 grep `RESULT mutants=` 片段、不看退出码,所以那边也会看到
|
||||
# `mutants=0` —— 但这一行现在自己带 ✗✗ 说明,且 `原始条目 0` 与 `清单 12` 并排,
|
||||
# 不再可能被读成"集合为空且一切正常"。)
|
||||
# ★ 退出码要说实话,而且**要说清是哪一类**(pi 2026-09-18 的第三例)。
|
||||
#
|
||||
# ⚠️ 这一格原来只有 `blind or unreadable` ⇒ 非零;`unlisted`/`ghosts` **只打印、
|
||||
# 然后 `return 0`**。于是套件那边 `whyLines: status !== 0 ? whyLines : []`
|
||||
# 把已经**算出来**的警告又丢掉了 ⇒ 端到端实测(真加一个未列入清单的 job 文件):
|
||||
# 磁盘 13 个 job 文件 · summary.py 自己说"未列入清单" 1 次 · 退出码 **0**
|
||||
# 套件报 `mutants=48 ran=47 skipped=1 on_new_criteria=35`
|
||||
# 套件输出里 grep "未列入清单" = **0 次**
|
||||
# ⇒ 数字按清单算是**对的**,错的是**读的人不知道它不是全集**。
|
||||
#
|
||||
# ★ 这**正是** `f632de4` 标题里那句承诺("清单外即报,与 SUITE 同形状")与
|
||||
# 上面 `245` 那行注释("与 run-all.mjs 自检 2 同形状:清单外即红")说的东西:
|
||||
# 自检 2 是**真的红**,而这边只打印、退 0、打印又被下一层丢掉 ——
|
||||
# **"同形状"当时只同了前一半**(有清单、有检查),"即红"那一半没落地。
|
||||
#
|
||||
# 退出码按**修法不同**分两类(照 pi 的提醒,也照 `env-defaults.sh:25` 那条
|
||||
# "别让环境问题冒充代码缺陷"的**反方向**:也别让"清单没跟上"冒充环境):
|
||||
# · 2 = **环境**:目录不可进入 / 有 job 文件读不到 ⇒ 去修权限。
|
||||
# · 1 = **该改的是清单/数据**:`unlisted`(新加的 job 文件没登记)/
|
||||
# `ghosts`(清单里有、磁盘上没有)⇒ 去改 `jobs.manifest.json`。
|
||||
# 两类都退非零 ⇒ `run-all` 那边既有的 `status !== 0` 路径会自动把 `whyLines`
|
||||
# 转印出来,**不需要动 run-all**(那一层早就能承接,只是上游没把状态传上来)。
|
||||
if blind or unreadable:
|
||||
return 2
|
||||
if unlisted or ghosts:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user