修复: "该红的红"没到达 verdict —— summary.py 的 status 只喂了文案,没喂决定颜色的那一格
pi 2026-09-18 本封。它读代码 + 端到端对照得出的结论**成立**,我独立复核了:
`verdict` 的四条输入(`reds`/`brokens`/`dueFailed`/`selfCheckFailed`)**没有一条**被
`sp.status` 喂过 —— 全部消费者只有两处、都是文本(`mutantsLine` 一行、`whyLines` 几行打印)。
⇒ 退 1 或 2 **只改措辞、不改颜色**;把别的红清掉之后就会出现
`verdict=green` 与 `diag=baseline-residue`("优先按变异残留查")**并排**。
(★ 比我上一轮修的那几处**好一档**:读者**看得见**了 —— 前几例是连读者都看不到。
但"变异可能没还原"坐在 green 旁边,仍是不能不管的形状。)
## 一、修法:诊断码表同时决定"红不红",接线**只此一处**
1. `DIAG` 表改成 `{ note, blocksGreen }` —— **一个码一行,两个字段一次定完**。
分开写就是"同一事实两份实现"(本仓已反复消:`blurStyleFor`、`stripComments`、
`want.why`、`.tmp` 台账路径)。放一起的结构性好处:**加一个新码就必须同时决定它红不红**。
该红:`counts-unusable` / `manifest-mismatch` / `baseline-residue` /
`baseline-unrunnable` / `baseline-unknown`(= **退非零的全部**);
不红:`baseline-stale` / `baseline-absent` / `none`(stale 不是缺陷,退非零会让正常提交天天假红)。
2. `summarizeMutants` 多返回一个 `red`(人类可读的一句)。**不认识的码也红** ——
上一版这里只改了**措辞**(打"请同步 DIAG")却仍然可以绿,**同一条缝的又一跳**。
3. `reds.push(r.red)` 走 pi 建议的那条路径(运行器自己推的红);
顺手补上**连 `summary.py` 都起不来**那一支(原来只改文案 ⇒ 整个读数器没跑起来也照样能绿)。
## 二★★ `verdict` 判定抽成纯函数 ⇒ **pi 说"代价不值"的那个实验变得可构造**
pi 明说它没做"清掉全部红后 `verdict` 真变绿"的完整实验,因为要动 SUITE 登记数、会撞接线自检。
⇒ 把判定抽成 `verdictOf({reds,brokens,dueFailed,selfCheckFailed})`,那个实验**只要传空数组**:
```
情形 verdict reds 该不该绿
正常 green 0 绿 ✓
残留 red 1 非绿 ✓ ← 修复前这里是 **green**(缺陷本体)
过期 green 0 绿 ✓
读数器没跑起来 red 1 非绿 ✓
```
★ **修复前的反面对照**(把接线退回"只改措辞"):残留 = `green`、读数器没跑起来 = `green`
⇒ 正是 pi 描述的那个"并存",**现在能跑出来了**。
## 三★★ 加自检时我自己的两处错(都当场红,照实记)
1. **自检验的是副本**:第一版自检自己写 `reds = r.red ? [r.red] : []`。
⇒ 我把生产那行 `if (r.red) reds.push(r.red);` **整行删掉**,自检**照样全绿**(实测 M24)——
因为它验的是**自己复刻的接线**。本仓老形状(`parseSkips` 抄份、`want.why` 现算一遍)第 N 次。
**修法不是再加断言,而是让接线住进被测函数**:抽出 `applyMutants(sp, reds)`,
生产与自检调**同一个它** ⇒ M24 重做后**红 7 条**。
2. **结构锚点假红 + 报文自相矛盾**:
· `indexOf('let mutantsLine')` 先命中**注释里提到这串字**的地方 ⇒ 扫的区间多包一个自检块
⇒ 报"1 处直接调"假红。改用 `lastIndexOf`(与自检 4 当年同一个坑,结论照抄)。
· 结构锚点**无条件**打 `ok` ⇒ M27 的输出长成 `RED … RED … ok 结构锚点:…`,
一个 `ok` 跟在自己的红后面。改成"没红才打 ok"。
## 四、变异验证(都已还原)
| 变异 | 期望 | 结果 |
|---|---|---|
| **M24** 删掉 `applyMutants` 里的 push(上一轮逃掉的那个) | 红 | **红 7 条**(修法①之后) |
| **M25** `baseline-residue` 的 `blocksGreen` 改 false | 红 | **红**,`残留` 报 green |
| **M26** `verdictOf` 恒返回 green | 红 | **红 7 条** |
| **M27** 生产绕过 `applyMutants`(直接调 `summarizeMutants`) | 红 | **红 2 条**(结构锚点抓住) |
| **M28** 端到端:让 `summary.py` 不存在 / `PATH` 无 `python3` | 红 | **红**(`red=10`、`verdict=red`) |
★ **M28 顺带查清一件事并写进注释**:`catch` 那一支**本机覆盖不到** ——
`summary.py` 不存在 ⇒ `spawnSync` 退 **2**(python 自己报错);`PATH` 里没有 `python3`
⇒ **`spawnSync` 不抛**,返回 `status=null, error.code='ENOENT'` —— 两条都走 `!m` 那一支。
`spawnSync` 只在参数非法时才同步抛。⇒ 照 pi 那句"前提构造不出来就写明":
**catch 那条红是兜底、不是实测通路**,注释里说清哪条是实测的(`!m`,两条都端到端验过)。
## 五、口径固定(回 pi §五)
`stale ⇒ 提示(rc=0,不进 verdict)` / `residue ⇒ 红(rc=1,进 verdict)` ——
pi 说这条口径要等 §四 修完才能真正固定,**现在固定了**:`--verdict-selftest` 里
`baseline-stale` 判"**可以**绿"、`baseline-residue` 判"**不许**绿",两条同时绿才算数。
## 六、验证与状态
· **五个自检全 exit 0**(新增 `--verdict-selftest` **12 例**:9 个情形 + 码表字段完整性 + 口径一致性 + 结构锚点)。
· 全套件 `checks=459 pass=455 fail=4 skip=0 red=9 broken=0 unreported=0 verdict=red`、
`mutants=48 ran=47 skipped=1 on_new_criteria=35 diag=none baseline=7/7✓` —— 与 pi 读数一致。
· 端到端对照:残留态 `red=9 → red=10`(多出的那条点名 `(summary.py)baseline-residue`);正常态仍 `red=9`。
· 残留全清;`git status` 仅本文件;`HEAD=93dabb4`。
This commit is contained in:
@ -1498,19 +1498,62 @@ try {
|
||||
* 那是**假话**:数字照常有效,**只有 baseline 那一格没读数**。
|
||||
* ⇒ 措辞一律由 `diag` 决定,`status` 只作兜底(且**先判 `diag`**)。
|
||||
*/
|
||||
const DIAG_NOTE = {
|
||||
'none': null,
|
||||
'counts-unusable': '**环境:没读数** —— 上面的 mutants 数字**不可信**(读不到 jobs/ 目录)',
|
||||
'manifest-mismatch': '**注意:清单与磁盘不一致** —— 上面的数字**不代表磁盘上现在有多少个变异体**',
|
||||
'baseline-unrunnable': '**环境:baseline 那一格没读数**(跑不了 `sha256sum`)—— '
|
||||
+ 'mutants 数字**仍然有效**,但"底本对不对"**没答案**',
|
||||
'baseline-residue': '**注意:有文件既不在底本、也与 HEAD 不同 ⇒ 优先按"变异残留"查**'
|
||||
+ '(数字按清单算是有效的,但**可能有变异没还原**)',
|
||||
'baseline-stale': '底本过期(文件与 HEAD 逐字节相同 ⇒ 是正常提交改过,不是残留)',
|
||||
'baseline-unknown': '**环境:judge 不了 baseline 那一格**(git 答不了 ⇒ 判不出是残留还是过期)—— '
|
||||
+ '这一格**没读数**,**不是**"有残留"',
|
||||
/*
|
||||
* ★★ 诊断码表:**一个码一行,两个字段一次定完**。
|
||||
*
|
||||
* `blocksGreen` 是本轮(pi 2026-09-18)补的那一半:**"该红的红"必须到达 `verdict`**。
|
||||
* 上一轮我只做到"到达读者" —— `diag` 与 `baseline=` 都印出来了,但 `sp.status`
|
||||
* 只喂了 `mutantsLine`(一行文本)和 `whyLines`(几行打印),
|
||||
* 而 `reds`/`brokens`/`dueFailed`/`selfCheckFailed` **没有一处**被它喂过
|
||||
* ⇒ `summary.py` 退 1 或 2 **只改措辞、不改颜色**。pi 的端到端对照:残留态与正常态
|
||||
* `RESULT` 尾部**逐字相同**(都是 `red=9 … verdict=red`);把别的红清掉之后就会出现
|
||||
* `verdict=green` 与 `diag=baseline-residue`("优先按变异残留查")**并排**。
|
||||
*
|
||||
* ★ 为什么把"提示"与"要不要红"放进**同一张表**:分开写就是"同一事实两份实现",
|
||||
* 而本仓那族缺陷(`blurStyleFor`、`stripComments`、`want.why`、`.tmp` 台账路径)
|
||||
* 已经反复消过。放一起的结构性好处:**加一个新码就必须同时决定它红不红** ——
|
||||
* 少填一个字段,下面那条"每个码都得有 blocksGreen"的自检会立刻响。
|
||||
*
|
||||
* ★ 三个口径(与 `summary.py` 的退出码分类一一对应,别让它们漂移):
|
||||
* · **该红的**(blocksGreen=true):`manifest-mismatch`(1)、`baseline-residue`(1)、
|
||||
* `counts-unusable`(2)、`baseline-unrunnable`(2)、`baseline-unknown`(2)。
|
||||
* —— 退非零的**全部**在这里,即"上游说我不该被当成一切正常"**真的有票**。
|
||||
* · **不该红的**:`baseline-stale`(0)、`baseline-absent`(0)、`none`(0)。
|
||||
* stale 是"底本没重算"而非缺陷(pi 与我 §七 同意:退非零会让正常提交天天假红),
|
||||
* 它靠 `diag` 通道被**看见**但不改颜色。
|
||||
* · **不认识的码**:红(报警),见下面那条。
|
||||
*/
|
||||
const DIAG = {
|
||||
'none': { note: null, blocksGreen: false },
|
||||
'counts-unusable': {
|
||||
note: '**环境:没读数** —— 上面的 mutants 数字**不可信**(读不到 jobs/ 目录)',
|
||||
blocksGreen: true,
|
||||
},
|
||||
'manifest-mismatch': {
|
||||
note: '**注意:清单与磁盘不一致** —— 上面的数字**不代表磁盘上现在有多少个变异体**',
|
||||
blocksGreen: true,
|
||||
},
|
||||
'baseline-unrunnable': {
|
||||
note: '**环境:baseline 那一格没读数**(跑不了 `sha256sum`)—— '
|
||||
+ 'mutants 数字**仍然有效**,但"底本对不对"**没答案**',
|
||||
blocksGreen: true,
|
||||
},
|
||||
'baseline-residue': {
|
||||
note: '**注意:有文件既不在底本、也与 HEAD 不同 ⇒ 优先按"变异残留"查**'
|
||||
+ '(数字按清单算是有效的,但**可能有变异没还原**)',
|
||||
blocksGreen: true,
|
||||
},
|
||||
'baseline-stale': {
|
||||
note: '底本过期(文件与 HEAD 逐字节相同 ⇒ 是正常提交改过,不是残留)',
|
||||
blocksGreen: false, // 不是缺陷;退非零会让"正常提交"天天假红(pi 与我 §七 同意)
|
||||
},
|
||||
'baseline-unknown': {
|
||||
note: '**环境:判不了 baseline 那一格**(git 答不了 ⇒ 判不出是残留还是过期)—— '
|
||||
+ '这一格**没读数**,**不是**"有残留"',
|
||||
blocksGreen: true,
|
||||
},
|
||||
// 没装 baseline.sha 是可接受的配置,不必刷屏 —— 但**仍算"知道这个状态"**(与"不认识的码"分开)
|
||||
'baseline-absent': null,
|
||||
'baseline-absent': { note: null, blocksGreen: false },
|
||||
};
|
||||
|
||||
function summarizeMutants(stdout, status, stderr) {
|
||||
@ -1527,23 +1570,30 @@ function summarizeMutants(stdout, status, stderr) {
|
||||
*
|
||||
* ★ **排除 `RESULT` 行本身**:`baseline=` 那一段就长在 `RESULT` 行里,若不排除,
|
||||
* 它会被当成"原因行"再转印一遍(同一句话印两次)。而 `RESULT` 行的内容
|
||||
* 由 `diag` + `DIAG_NOTE` 负责传达 —— 两条通道**各管一段**,不重叠。
|
||||
* 由 `diag` + `DIAG` 的说明负责传达 —— 两条通道**各管一段**,不重叠。
|
||||
*/
|
||||
const whyLines = (stdout || '').split('\n')
|
||||
.filter(l => !/^RESULT /.test(l.trim()))
|
||||
.filter(l => /✗✗|⚠️|★ 清单与磁盘不一致|在但读不到|未列入清单|清单里有、磁盘上没有/.test(l))
|
||||
.map(l => l.trim());
|
||||
// 上游加了新码而这里不认识 ⇒ **报警**(不是静静当成"没问题")
|
||||
if (diag && !(diag in DIAG_NOTE)) {
|
||||
/*
|
||||
* 上游加了新码而这里不认识 ⇒ **报警,并且红**(不是静静当成"没问题")。
|
||||
* ★ 上一轮这里只改了**措辞**(报警文案)却仍然 `blocksGreen` 无从谈起 ⇒
|
||||
* 一个不认识的码会打出"请同步 DIAG",而 `verdict` 照样可以绿。**同一条缝的又一跳。**
|
||||
*/
|
||||
if (diag && !(diag in DIAG)) {
|
||||
return {
|
||||
line: ` mutants=(**套件不认识诊断码 \`${diag}\`** —— summary.py 加了新状态,`
|
||||
+ '请同步 run-all.mjs 的 DIAG_NOTE,不要让它静静变成"没问题")',
|
||||
+ '请同步 run-all.mjs 的 DIAG,不要让它静静变成"没问题")',
|
||||
whyLines,
|
||||
red: `(summary.py)不认识的诊断码 \`${diag}\` —— 上游加了状态而套件不认识它,`
|
||||
+ '这一格**没读数**(不是"没问题")',
|
||||
};
|
||||
}
|
||||
const note = diag ? DIAG_NOTE[diag] : null;
|
||||
const entry = diag ? DIAG[diag] : null;
|
||||
const note = entry ? entry.note : null;
|
||||
/*
|
||||
* ★ 把 `baseline=` 的**读数本身**也带上(pi 本封的 ① 的另一半)。
|
||||
* ★ 把 `baseline=` 的**读数本身**也带上(pi 2026-09-18 上一封的 ① 的另一半)。
|
||||
*
|
||||
* 原来 `baseline=…` 只活在 `summary.py` 的 stdout 里:捕获组是前缀、过滤器又不含它
|
||||
* ⇒ 套件输出里 `grep -c baseline=` = **0**(我三态各测过)。诊断码解决了"**结论**到不了",
|
||||
@ -1552,11 +1602,29 @@ function summarizeMutants(stdout, status, stderr) {
|
||||
*
|
||||
* 取值用**字符类排除**(`(`/`*`/`⚠` 都是 `baseline=` 后面那句话的一部分),
|
||||
* 只留数字型的读数(`7/7✓`、`6/7`);"跑不了"那种没有数字 ⇒ 空,就不重复说了
|
||||
* (`DIAG_NOTE` 已经讲过)。**不扩捕获组**是有意的:扩了就得连整句一起搬,
|
||||
* (`DIAG` 的说明已经讲过)。**不扩捕获组**是有意的:扩了就得连整句一起搬,
|
||||
* 那才是"按字面量裁射程"。
|
||||
*/
|
||||
const blv = /baseline=([^\s(*⚠]*)/.exec(stdout || '');
|
||||
const blPart = blv && blv[1] ? ` baseline=${blv[1]}` : '';
|
||||
/*
|
||||
* ★★ `red`:**这一格要不要把套件判红**(pi 2026-09-18 本封的要害)。
|
||||
* 判据来自 `DIAG` 的 `blocksGreen`,**不在这里另写一套**(同一事实一份实现)。
|
||||
* 没打出 RESULT(状态非 0 且没有码)⇒ 也算红:那是"**读数器根本没跑出来**",
|
||||
* 与 `brokens` 的语义同族,但它没有文件记录可挂,所以走 `reds`(运行器自己推的红)。
|
||||
*/
|
||||
let red = null;
|
||||
if (m) {
|
||||
if (entry && entry.blocksGreen) {
|
||||
red = `(summary.py)${diag} —— ${entry.note}`;
|
||||
} else if (!diag && status !== 0) {
|
||||
// 有 RESULT 行但没诊断码、退出码却非 0 ⇒ 契约被破坏(新码没带 `diag=`),也要红
|
||||
red = `(summary.py)退出码 ${status} 但那一格没带 \`diag=\`(诊断码是判据的一部分,`
|
||||
+ '不许漏)—— 这一格**没读数**';
|
||||
}
|
||||
} else {
|
||||
red = `(summary.py)没打出 RESULT(status=${status})—— 变异体这一族**根本没被读数**`;
|
||||
}
|
||||
if (m) {
|
||||
/*
|
||||
* ⚠️ `m[1]` 是**前缀**(不含 `diag=`/`baseline=`)—— 这正是原来那一格"到不了"的成因。
|
||||
@ -1568,21 +1636,232 @@ function summarizeMutants(stdout, status, stderr) {
|
||||
line: ` ${head}` +
|
||||
(note ? `(${note})` : (status !== 0 ? `(注意:summary.py status=${status})` : '')),
|
||||
whyLines: (note || status !== 0) ? whyLines : [],
|
||||
red,
|
||||
};
|
||||
}
|
||||
if (status === 2) {
|
||||
return {
|
||||
line: ' mutants=(**环境:没读数**,summary.py status=2 —— 上面的 mutants 数字**不可信**)',
|
||||
whyLines,
|
||||
red,
|
||||
};
|
||||
}
|
||||
return {
|
||||
line: ` mutants=(summary.py 没打出 RESULT:status=${status} ` +
|
||||
`${(stderr || '').trim().split('\n').slice(-1)[0] || ''})`,
|
||||
whyLines,
|
||||
red,
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* ★★ `verdict` 的**判定本身**抽成纯函数(pi 2026-09-18 本封)。
|
||||
*
|
||||
* 为什么必须抽出来:pi 说它没有把 §四 做成"清掉全部红后 `verdict` 真变绿"的完整实验,
|
||||
* 因为那要动 SUITE 登记数、会撞接线自检 —— **代价不值,所以只有静态证据**。
|
||||
* 抽成纯函数之后,那个实验**不需要动任何登记**:把 `reds` 传成空数组就行。
|
||||
* ⇒ "结论到不了决定颜色的那一格"这类缺陷,从此**有判据守着**,
|
||||
* 而不是靠下一轮的人再读一遍源码。
|
||||
*/
|
||||
function verdictOf({ reds, brokens, dueFailed, selfCheckFailed }) {
|
||||
return (reds.length === 0 && brokens.length === 0 && !dueFailed && !selfCheckFailed)
|
||||
? 'green' : 'red';
|
||||
}
|
||||
|
||||
/*
|
||||
* ★★ **唯一的接线点**:把 `summary.py` 的读数接进汇总。
|
||||
*
|
||||
* 为什么要有这个函数(我自己变异出来的):上一版的自检**自己**做了接线 ——
|
||||
* 它调 `summarizeMutants` 拿 `red`,然后**自己**写 `const reds = r.red ? [r.red] : []`。
|
||||
* 于是我把生产那行 `if (r.red) reds.push(r.red);` **整行删掉**,自检**照样全绿**(实测 M24)——
|
||||
* 因为自检验的是**它自己复刻的那份接线**,不是生产那份。
|
||||
* 这正是本仓反复消的老形状(自检验副本:`parseSkips` 抄份、`want.why` 现算一遍)。
|
||||
*
|
||||
* ⇒ 修法不是"再加一条断言",而是**让接线本身住进被判的那个函数**:
|
||||
* `applyMutants` 收 `sp` 与 `reds`(**不捕外部变量**),生产与自检**调同一个它**。
|
||||
* 这样"忘了 push"就落在被测函数体内 ⇒ M24 会被抓住。
|
||||
* (剩下"生产到底有没有调它"这一跳,由下面 `--verdict-selftest` 的结构锚点守。)
|
||||
*/
|
||||
function applyMutants(sp, reds) {
|
||||
const r = summarizeMutants(sp.stdout, sp.status, sp.stderr);
|
||||
for (const l of r.whyLines) console.log(` ${l}`);
|
||||
/*
|
||||
* ★★ **"该红的红"到达 `verdict`**(pi 2026-09-18 本封)。
|
||||
*
|
||||
* 上一轮我只做到"到达读者":`diag`/`baseline=` 都印得出来,但 `sp.status` 只喂了
|
||||
* `mutantsLine`(一行文本)与 `whyLines`(几行打印)—— `reds`/`brokens`/`dueFailed`/
|
||||
* `selfCheckFailed` **没有一处**被它喂过 ⇒ 退 1 或 2 **只改措辞、不改颜色**。
|
||||
* pi 的端到端对照:残留态与正常态的 `RESULT` 尾部**逐字相同**;把别的红清掉之后,
|
||||
* 会出现 `verdict=green` 与 `diag=baseline-residue`("优先按变异残留查")**并排**。
|
||||
*
|
||||
* ⇒ 沿用 pi 建议的那条路径(`reds.push` 本来就是"运行器自己推的红",
|
||||
* 注释里已写明这类红**进不了 `fail`**,所以两个格都必须打)。
|
||||
*/
|
||||
if (r.red) reds.push(r.red);
|
||||
return r.line;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* ★★ 汇总判定自检(`--verdict-selftest`):**pi 2026-09-18 本封要的那个实验**。
|
||||
*
|
||||
* pi 说它没有把"清掉全部红之后 `verdict` 会不会真变绿"做成完整实验 ——
|
||||
* 那要动 SUITE 登记数、会撞接线自检,"代价不值",所以只给了两条静态证据
|
||||
* (`verdict` 的条件表达式、`sp.status` 只有两个消费者)。
|
||||
* **它是对的**,而且这正是"判据在,但走不到"的又一例:结论本身没错,但**没有判据守着它**。
|
||||
*
|
||||
* ⇒ 把判定抽成纯函数 `verdictOf`(已做),于是那个实验**不需要动任何登记**:
|
||||
* 把 `reds` 传成空数组即可。这一格就钉住"**`summary.py` 说该红时,`verdict` 真的会红**"。
|
||||
*
|
||||
* 判的是**端到端的那一跳**,不是 `DIAG` 表本身的取值:
|
||||
* ① 先调**真** `summarizeMutants` 拿 `red`(走真正则、真码表);
|
||||
* ② 再把那个 `red` 放进 `reds` 交给**真** `verdictOf`;
|
||||
* ③ 要求在"其它红全清掉"的情形下 `verdict !== 'green'`。
|
||||
* —— 这比"断言 `DIAG[diag].blocksGreen === true`"强:它证明的是**接线真的通了**,
|
||||
* 而不是"表里写了个 true"(后者在 `r.red` 忘了 push 时照样绿 —— 那正是上一轮的缺陷)。
|
||||
*/
|
||||
if (process.argv.includes('--verdict-selftest')) {
|
||||
const clean = { brokens: [], dueFailed: false, selfCheckFailed: false };
|
||||
const cases = [
|
||||
/*
|
||||
* ① 正常 ⇒ 绿(反面对照:证明这条判据**不是永远报红**)
|
||||
*/
|
||||
['正常(diag=none)⇒ 清掉别的红之后应为 green',
|
||||
'RESULT mutants=48 ran=47 skipped=1 on_new_criteria=35 diag=none baseline=7/7✓\n', 0, true],
|
||||
/*
|
||||
* ②★ 要害:**残留 ⇒ 即使别的红全清了,也不许绿**
|
||||
*/
|
||||
['baseline-residue(status 1)⇒ 别的红全清也**不许绿**',
|
||||
'RESULT mutants=48 ran=47 skipped=1 on_new_criteria=35 diag=baseline-residue baseline=6/7✗\n',
|
||||
1, false],
|
||||
['counts-unusable(status 2)⇒ 不许绿',
|
||||
'RESULT mutants=NO-READING ran=NO-READING skipped=NO-READING on_new_criteria=NO-READING '
|
||||
+ 'diag=counts-unusable(**没读数**)\n', 2, false],
|
||||
['manifest-mismatch(status 1)⇒ 不许绿',
|
||||
'RESULT mutants=48 ran=47 skipped=1 on_new_criteria=35 diag=manifest-mismatch\n'
|
||||
+ ' 未列入清单:jobs-x.json\n', 1, false],
|
||||
['baseline-unrunnable(status 2)⇒ 不许绿',
|
||||
'RESULT mutants=1 ran=0 skipped=1 on_new_criteria=0 diag=baseline-unrunnable baseline='
|
||||
+ '(**跑不了 sha256sum 校验**)\n', 2, false],
|
||||
['baseline-unknown(status 2)⇒ 不许绿',
|
||||
'RESULT mutants=1 ran=0 skipped=1 on_new_criteria=0 diag=baseline-unknown baseline=0/7\n',
|
||||
2, false],
|
||||
/*
|
||||
* ③ `baseline-stale`(status **0**)⇒ **可以为绿**。
|
||||
* pi 与我 §七 口径一致:stale 不是缺陷,退非零会让"正常提交"天天假红。
|
||||
* 这一条同时是②的反面对照:证明**不是"有 diag 就红"**(否则 stale 会被一起误红)。
|
||||
*/
|
||||
['baseline-stale(status 0)⇒ **可以**绿(不是缺陷)',
|
||||
'RESULT mutants=48 ran=47 skipped=1 on_new_criteria=35 diag=baseline-stale baseline=6/7⚠\n',
|
||||
0, true],
|
||||
['不认识的诊断码 ⇒ 不许绿(报警)',
|
||||
'RESULT mutants=1 ran=0 skipped=1 on_new_criteria=0 diag=brand-new-state\n', 0, false],
|
||||
['没打出 RESULT(status 1)⇒ 不许绿',
|
||||
'', 1, false],
|
||||
];
|
||||
let bad = 0;
|
||||
for (const [what, stdout, status, mayBeGreen] of cases) {
|
||||
/*
|
||||
* ★★ 走**生产那一个** `applyMutants`(不是我在这里复刻一份接线)。
|
||||
* 我第一版自检自己写 `const reds = r.red ? [r.red] : []` ⇒ 把生产那行
|
||||
* `if (r.red) reds.push(r.red);` 整行删掉,自检**照样全绿**(实测 M24)——
|
||||
* 因为它验的是**自己复刻的那份**。现在接线只在 `applyMutants` 里,
|
||||
* 生产与自检调同一个它 ⇒ "忘了 push"必然被抓。
|
||||
* (`whyLines` 会打到 stdout,这里吞掉,免得自检输出被几百行原因淹没。)
|
||||
*/
|
||||
const reds = [];
|
||||
const swallow = console.log;
|
||||
console.log = () => {};
|
||||
try { applyMutants({ stdout, status, stderr: 'boom' }, reds); } finally { console.log = swallow; }
|
||||
// ★ 别的红**全清掉** —— 这就是 pi 说"代价不值"的那个实验,现在只要传空数组
|
||||
const v = verdictOf({ reds, ...clean });
|
||||
const ok = mayBeGreen ? v === 'green' : v !== 'green';
|
||||
console.log(`${ok ? 'ok ' : 'RED '} ${what}:verdict=${v}` +
|
||||
`${ok ? '' : `(期望 ${mayBeGreen ? 'green' : '非 green'};reds=${reds.length})`}`);
|
||||
if (!ok) bad++;
|
||||
}
|
||||
/*
|
||||
* ④ 结构性对照:**表里每个码都得显式决定"红不红"**。
|
||||
* 缺字段 ⇒ 立刻红 —— 这是"加码必须同时决定颜色"的机械保证
|
||||
* (与"不认识的码要报警"互补:那管**下游不认识**,这管**上游新加时漏填**)。
|
||||
*/
|
||||
for (const [code, e] of Object.entries(DIAG)) {
|
||||
if (typeof e.blocksGreen !== 'boolean') {
|
||||
console.log(`RED 诊断码 \`${code}\` 没显式声明 blocksGreen ⇒ **加码时必须同时决定它红不红**`);
|
||||
bad++;
|
||||
}
|
||||
}
|
||||
console.log(`ok 码表每个码都有 blocksGreen(${Object.keys(DIAG).length} 个码)`);
|
||||
/*
|
||||
* ⑤ 口径一致性:`blocksGreen === true` 的码,其**上游退出码必须非零**。
|
||||
* 两边漂移(上游退 0、下游却红)会让人去查一个"其实没被报错"的东西。
|
||||
* 这条把 `summary.py` 的退出码分类与 `run-all` 的表**绑在一起**验(不在一边写死另一边的值)。
|
||||
*/
|
||||
for (const [code, e] of Object.entries(DIAG)) {
|
||||
if (code === 'none' || code === 'baseline-absent' || code === 'baseline-stale') continue;
|
||||
if (!e.blocksGreen) {
|
||||
console.log(`RED \`${code}\` 上游退非零,下游却不红 ⇒ 两侧口径漂移`);
|
||||
bad++;
|
||||
}
|
||||
}
|
||||
/*
|
||||
* ⑥★ **结构锚点:生产路径必须真的经过 `applyMutants`**。
|
||||
*
|
||||
* 上面①–③判的是 `applyMutants` **内部**的行为("接线忘了 push"已被 M24 证明抓得住)。
|
||||
* 但还剩一跳:**生产到底有没有调它** —— 若哪天有人绕过它、
|
||||
* 自己写 `summarizeMutants(...)` 而忘了推进 `reds`,本自检照样全绿。
|
||||
* ⇒ 扫**自己**的源码(去掉注释与字符串,免得咬到本文档里那些字面量),
|
||||
* 要求:① `mutantsLine = applyMutants(` 出现**恰好一次**;
|
||||
* ② 除了函数定义那一行,**没有**别处直接调 `summarizeMutants(`。
|
||||
* 位置用 `prose()`/`stripComments()`(本仓既有的读取器),不自己搓正则。
|
||||
*/
|
||||
/*
|
||||
* ⚠️ 第一版我写的是"全文件 `summarizeMutants(` 只许出现 1 处" ⇒ 实测**报 3 处**而红。
|
||||
* 三处都合法:**函数定义**、`applyMutants` 内部、以及 `--mutants-line-selftest`
|
||||
* (它拿合成 stdout 测那个纯函数)。**我的期望值写错了**,不是代码错了。
|
||||
* —— 硬编码"3"又是脆的(加一个自检就红),且那个数字**不表达我想守的性质**。
|
||||
*
|
||||
* ⇒ 改扫**生产区段**(从 `let mutantsLine` 到文件末尾 —— 本文件里自检都在中段,
|
||||
* 生产在最后)。要守的性质只有一句:**生产不许绕过 `applyMutants`**。
|
||||
*/
|
||||
const src = stripComments(prose(fileURLToPath(import.meta.url)));
|
||||
/*
|
||||
* ⚠️ 必须用 `lastIndexOf`(我第一版用 `indexOf` ⇒ **假红**):
|
||||
* `indexOf('let mutantsLine')` 先命中的是**上面那段自检注释里提到这串字的地方**,
|
||||
* 不是真正的声明(1955 行)⇒ 扫的区间多包了一个自检块 ⇒ 报"1 处直接调"。
|
||||
* —— 与自检 4 当年踩的是**同一个坑**(锚点自匹配),那里的结论照抄:
|
||||
* **生产在最后,自检在中段**,所以取 `lastIndexOf`;再加一条"区间不许为空"的兜底。
|
||||
*/
|
||||
const prodFrom = src.lastIndexOf('let mutantsLine');
|
||||
const prod = prodFrom >= 0 ? src.slice(prodFrom) : '';
|
||||
if (!prod) {
|
||||
console.log('RED 找不到生产区段(`let mutantsLine`)⇒ 这条结构锚点**自己失效**,必须红');
|
||||
bad++;
|
||||
} else {
|
||||
const calls = (prod.match(/applyMutants\(/g) || []).length;
|
||||
const direct = (prod.match(/summarizeMutants\(/g) || []).length;
|
||||
if (calls !== 1) {
|
||||
console.log(`RED 生产区段对 \`applyMutants\` 的调用有 ${calls} 处(应为**恰好 1**)`
|
||||
+ ' ⇒ 接线可能被绕过或重复');
|
||||
bad++;
|
||||
}
|
||||
if (direct !== 0) {
|
||||
console.log(`RED 生产区段有 ${direct} 处**直接**调 \`summarizeMutants\``
|
||||
+ ' ⇒ 那条路绕过了接线,红的票会丢(这正是 M24 的形状)');
|
||||
bad++;
|
||||
}
|
||||
/*
|
||||
* ⚠️ 只在**这一格没报红**时打 ok —— 我第一版无条件打,于是 M27 的输出长成
|
||||
* `RED …` / `RED …` / `ok 结构锚点:生产区段经由 applyMutants 接线`,
|
||||
* 一个 `ok` 跟在自己的红后面 ⇒ 读者会以为"没红"(**同一格的报文自相矛盾**)。
|
||||
*/
|
||||
if (calls === 1 && direct === 0) {
|
||||
console.log('ok 结构锚点:生产区段经由 `applyMutants` 接线'
|
||||
+ `(applyMutants ${calls} 处、直接调 summarizeMutants ${direct} 处)`);
|
||||
}
|
||||
}
|
||||
process.exit(bad ? 1 : 0);
|
||||
}
|
||||
|
||||
/*
|
||||
* 变异体播报自检(`--mutants-line-selftest`):钉住"**先看退出码,再看正则**"。
|
||||
@ -1718,11 +1997,29 @@ try {
|
||||
* 却打出一行**看着完全正常**的 `mutants=47 ran=0 skipped=47 …`,**它匹配正则**。
|
||||
* ⇒ 只靠修法① 会被它骗过,只有"先判 status"能兜住。
|
||||
*/
|
||||
const r = summarizeMutants(sp.stdout, sp.status, sp.stderr);
|
||||
mutantsLine = r.line;
|
||||
for (const l of r.whyLines) console.log(` ${l}`);
|
||||
mutantsLine = applyMutants(sp, reds);
|
||||
} catch (e) {
|
||||
mutantsLine = ` mutants=(跑不了 summary.py:${e.code || e.message})`;
|
||||
/*
|
||||
* ★ **连 `summary.py` 都起不来时也要红**(我上一版漏了):原来这里只改文案 ⇒
|
||||
* "整个读数器没跑起来"会让 `verdict` 照样可以绿。与上面 `r.red` 是同一族。
|
||||
*
|
||||
* ⚠️⚠️ **这一条分支本机覆盖不到,只能靠代码审查** —— 照 pi 那句
|
||||
* "哪条前提构造不出来,就写明'这条分支本机自检覆盖不到,只能人手验',
|
||||
* 别让它默认看起来是被守着的"。我实测过两条最像的前提,**都不走这里**:
|
||||
* · `summary.py` 不存在 ⇒ `spawnSync` 返回 `status=2`(python 自己报错)⇒ 走 `!m` 那一支;
|
||||
* · `PATH` 里没有 `python3` ⇒ **`spawnSync` 不抛**,返回 `status=null, error.code='ENOENT'`
|
||||
* ⇒ 也不走这里,同样走 `!m` 那一支。
|
||||
* `spawnSync` 只在**参数非法**(options 类型不对之类)时才同步抛 —— 那不是运行期会遇到的输入。
|
||||
*
|
||||
* ★ 所以这条红**是兜底**,不是我实测过的通路。真正的通路是 `!m`:
|
||||
* 上面那两种情形端到端都验过 —— 两条都得到 `red=10 verdict=red`,
|
||||
* 那一格打 `mutants=(summary.py 没打出 RESULT:status=null …)`。
|
||||
* ⇒ 两个都要保留(兜底 + 实测通路),但**注释必须说清哪条是实测的** ——
|
||||
* 否则下一个人会以为 catch 这条路有判据守着。
|
||||
*/
|
||||
reds.push(`(summary.py)**整个读数器没跑起来**(${e.code || e.message})—— `
|
||||
+ '变异体这一族**根本没被读数**,不是"没有问题"');
|
||||
}
|
||||
|
||||
console.log(`RESULT phase=${PHASE} static=${STATIC_ONLY.length}${debtsLine} probe=${probeSummary}` +
|
||||
@ -1770,7 +2067,13 @@ const totalsLine = (verdict) => `RESULT files=${SUITE.length} ran=${ranFilesCoun
|
||||
`checks=${totalTests} pass=${totalTests - totalFail - totalSkip} fail=${totalFail} ` +
|
||||
`skip=${totalSkip} ` +
|
||||
`red=${reds.length} broken=${brokens.length} unreported=${unreportedFiles} verdict=${verdict}`;
|
||||
if (reds.length === 0 && brokens.length === 0 && !dueFailed && !selfCheckFailed) {
|
||||
/*
|
||||
* ★ 判定走**纯函数** `verdictOf`(定义在 `summarizeMutants` 那边)——
|
||||
* 这样"清掉全部红之后会不会绿"是**可构造**的实验(见 `--verdict-selftest`),
|
||||
* 不必动 SUITE 登记数。原来这条条件表达式是**内联**的,
|
||||
* 于是"结论到不了这一格"只能靠人读源码发现(pi 本封就是读出来的)。
|
||||
*/
|
||||
if (verdictOf({ reds, brokens, dueFailed, selfCheckFailed }) === 'green') {
|
||||
// 报**本相位实际跑了**几条,不报清单长度 —— 上面刚写完"不能声称自己做过的事",
|
||||
// 这里如果打 SUITE.length 就是在同一行里犯同一个错(2 条构建相位判据被跳过了)。
|
||||
const ranFiles = SUITE.map(([f]) => f).filter(f => !skippedByPhase.includes(f));
|
||||
|
||||
Reference in New Issue
Block a user