#!/usr/bin/env node /** * 跑套件,并从**同一次运行的 TAP 输出**里数结果行 —— 任何用例名出现两次就红。 * * # 为什么需要它:这是一条**因果无关**的判据 * * `test/env-guard.test.mjs` 里那条"测试文件之间不许互相 import"只能发现**已知的 * 那个成因**。同族的另一种成因它看不见,比如**两个文件里恰好同名**的用例 —— * `node --test` 按文件注册,跨文件重名**不会被拦**。2026-09-14 实测确认: * * 两个文件各写一个 `test('同名用例')` ⇒ `# tests 2 / # pass 2 / # fail 0` * (两条 `ok`,没有任何警告) * * 本脚本不挑成因:**只数结果行**。它同时也能抓住"二次注册"那类问题, * 所以和那条静态判据是**互补**的(静态那条更早、能指认是哪个文件引了哪个; * 这条更晚、但因果无关)。 * * # 判据锚在哪一行(这条是被人纠正出来的) * * 锚 `^(ok|not ok) - ` —— **结果行**。pi 先前给的一条验证命令是 * `grep -c '<用例名>'`,那个数的是**字符串出现次数**:TAP 里同一个用例名既出现在 * `# Subtest:` 头、又出现在结果行 ⇒ 2 倍效应 + 2 倍噪声,**两个因子恰好同值**, * 于是"4"看起来还能解释。若行种类是 3,就会把"两次注册"读成"三次"。 * 规矩:**判据必须锚在与结论一一对应的那一行上**,而不是"出现过"。 * * # 退出码(与 `env-preflight.mjs`、`redeploy-plugin.sh` 同一套) * * 0 = 套件全绿 **且**没有重名 * 1 = 断言失败(套件红了)**或**发现重名(这是真问题,要看代码) * 2 = 环境/参数问题(临时目录不足等) * * `--self-check`:不跑套件,只用两段合成的 TAP 样本验本脚本自己 * (干净样本必须放行、重复样本必须点名)—— 这就是"判据自检",与 * `check-deploy-drift.mjs --self-check` 同一形状。 */ import { spawnSync } from 'node:child_process'; import process from 'node:process'; /** TAP 结果行:`ok 12 - 名字` / `not ok 12 - 名字`(名字里可能有 `#` 之类,照收)。 */ const RESULT_RE = /^(not ok|ok) (\d+) - (.*)$/; /** 只认"用例"行:跳过整份文件的汇总行(形如 `ok 1 - test/xxx.test.mjs` 也算结果行, * 所以不能只靠这一条区分)。这里用"出现次数"来做判断,不区分文件级与用例级 —— */ export function findDuplicates(tapText) { const counts = new Map(); for (const line of tapText.split('\n')) { const m = RESULT_RE.exec(line); if (!m) continue; const name = m[3].trim(); counts.set(name, (counts.get(name) || 0) + 1); } return [...counts.entries()] .filter(([, n]) => n > 1) .map(([name, n]) => ({ name, count: n })) .sort((a, b) => b.count - a.count || a.name.localeCompare(b.name)); } /** 从 TAP 里取汇总:`# tests N` / `# pass N` / `# fail N`。取不到就是没跑完。 */ export function summaryOf(tapText) { const grab = (k) => { const m = new RegExp(`^# ${k} (\\d+)$`, 'm').exec(tapText); return m ? Number(m[1]) : null; }; return { tests: grab('tests'), pass: grab('pass'), fail: grab('fail') }; } const SELF_CHECK_SAMPLES = [ { what: '干净样本:每个名字只出现一次 → 必须放行', tap: [ '# Subtest: 甲', 'ok 1 - 甲', '# Subtest: 乙', 'ok 2 - 乙', '1..2', '# tests 2', '# pass 2', '# fail 0', ].join('\n'), expect: [], }, { what: '★重复样本:同名两次(正是跨文件重名的形状)→ 必须点名', tap: [ '# Subtest: 同名用例', 'ok 1 - 同名用例', '# Subtest: 同名用例', 'ok 2 - 同名用例', '1..2', '# tests 2', '# pass 2', '# fail 0', ].join('\n'), expect: ['同名用例'], }, { what: '★噪声样本:名字只出现在 `# Subtest:` 头里(没有结果行)→ 不算重复', tap: ['# Subtest: 只在头里', '1..0', '# tests 0', '# pass 0', '# fail 0'].join('\n'), expect: [], }, { what: '★名字里带 `#` 也要能取对(别把名字截断成前缀)', tap: ['ok 1 - ★0 字节不是「不知道」', 'ok 2 - ★0 字节不是「不知道」', '1..2'].join('\n'), expect: ['★0 字节不是「不知道」'], }, ]; function selfCheck() { let bad = 0; for (const s of SELF_CHECK_SAMPLES) { const got = findDuplicates(s.tap).map((d) => d.name); const ok = JSON.stringify(got) === JSON.stringify(s.expect); console.log(` ${ok ? '通过' : '失败'} ${s.what}`); if (!ok) { bad += 1; console.log(` 期望 ${JSON.stringify(s.expect)},实际 ${JSON.stringify(got)}`); } } console.log(bad === 0 ? '\n判据自检:全部通过。' : `\n判据自检:${bad} 条失败。`); process.exit(bad === 0 ? 0 : 1); } if (process.argv.includes('--self-check')) selfCheck(); // ── 跑套件 ──────────────────────────────────────────────────────── // `--test-reporter=tap` 让报告格式稳定(`--test` 在 TTY 下会自动换格式, // 那样就锚不住结果行)。输出走管道,本进程负责原样转写到 stderr。 const args = ['--test', '--test-reporter=tap', 'test/*.test.mjs']; const r = spawnSync(process.execPath, args, { encoding: 'utf8', env: process.env, maxBuffer: 256 * 1024 * 1024, }); if (r.error) { console.error(`[tap-dup] 起不了测试进程:${r.error.message}`); process.exit(2); } const tap = r.stdout || ''; process.stderr.write(tap); // 环境不足(前置脚本那套约定的 2)原样透传 —— 这一层不替环境问题下结论。 if (r.status === 2) { console.error('[tap-dup] 套件以 2 退出(环境问题),不据此判重名。'); process.exit(2); } const s = summaryOf(tap); if (s.tests === null) { console.error('[tap-dup] 拿不到 `# tests` 汇总 —— 套件没跑完(可能崩了)。'); process.exit(1); } const dups = findDuplicates(tap); const failed = (s.fail ?? 0) > 0 || r.status !== 0; if (dups.length > 0) { console.error('\n[tap-dup] 有用例名在同一次运行里出现多次:'); for (const d of dups) console.error(` ${d.count}× ${d.name}`); console.error(' 跨文件重名不会被 runner 拦下(按文件注册)⇒ 这多半是两个文件里有同名用例,'); console.error(' 也可能是某个测试文件被二次注册(例如别的测试文件 import 了它)。'); } console.log(`\n# 结果行重复检查:${s.tests} 条结果,${dups.length} 个名字重复`); if (failed) { console.error(`[tap-dup] 套件红:pass ${s.pass} / fail ${s.fail}`); process.exit(1); } if (dups.length > 0) process.exit(1); console.log('[tap-dup] 套件全绿,且没有重名。');