From 5ba88c69fa80678d46a7f2c7dfcfe540bfac3daa Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 14 Oct 2026 03:28:14 +0200 Subject: [PATCH] Read the runner's last summary and NOT REACHED in any case (issue 693, review 253) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A test that printed a summary-shaped line before it skipped was read as the runner's totals: "ℹ pass 5" made a run that proved nothing pass. --- src/outcomes.ts | 3 ++- src/suite.ts | 14 +++++++++----- test/outcomes.test.ts | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 6 deletions(-) diff --git a/src/outcomes.ts b/src/outcomes.ts index 6b8cb13..0ec89a9 100644 --- a/src/outcomes.ts +++ b/src/outcomes.ts @@ -54,7 +54,8 @@ export function judgeRun( skips: Skip[], ): { passed: boolean; why: string } { if (counts.failed !== null && counts.failed > 0) return { passed: false, why: `${counts.failed} test(s) failed` }; - const unreached = skips.filter((s) => s.reason.includes(NOT_REACHED)); + // In any case: "not reached" says the same as "NOT REACHED". + const unreached = skips.filter((s) => s.reason.toUpperCase().includes(NOT_REACHED)); if (counts.passed === 0) { return { passed: false, diff --git a/src/suite.ts b/src/suite.ts index 141ba73..42fbd5f 100644 --- a/src/suite.ts +++ b/src/suite.ts @@ -28,14 +28,18 @@ export function counted(output: string): { passed: number | null; failed: number // running this against the real runner — the fixture it was first written against was output I // had imagined, which is a test that agrees with the mistake it was written beside. const plain = output.replace(/\u001b\[[0-9;]*m/g, ""); + // **The last such line, never the first** (novox/hq issue 693): the runner prints its summary after + // everything a test printed, so a test that prints "ℹ pass 5" before it skips would otherwise be read + // as five passes, and a run that proved nothing as passed. const total = (what: RegExp) => { - const found = plain.match(what); - return found ? Number(found[1]) : null; + const found = [...plain.matchAll(what)]; + const last = found[found.length - 1]; + return last ? Number(last[1]) : null; }; return { - passed: total(/^\s*(?:\u2139|#)\s*pass\s+(\d+)\s*$/m), - failed: total(/^\s*(?:\u2139|#)\s*fail\s+(\d+)\s*$/m), - skipped: total(/^\s*(?:\u2139|#)\s*skipped\s+(\d+)\s*$/m), + passed: total(/^\s*(?:\u2139|#)\s*pass\s+(\d+)\s*$/gm), + failed: total(/^\s*(?:\u2139|#)\s*fail\s+(\d+)\s*$/gm), + skipped: total(/^\s*(?:\u2139|#)\s*skipped\s+(\d+)\s*$/gm), }; } diff --git a/test/outcomes.test.ts b/test/outcomes.test.ts index 025a7ad..b74ee06 100644 --- a/test/outcomes.test.ts +++ b/test/outcomes.test.ts @@ -73,6 +73,20 @@ test("the recorded line says what skipped and why, and NOT PASSED", () => { }); }); +// A test that prints a summary-shaped line before it skips must not stand for the runner's summary. +test("the runner's last summary is read, not a line a test printed before it", () => { + const output = "\u2139 pass 5\n\u2139 fail 0\n\u2139 skipped 0\n\ufe63 genesis adopts its bus (1ms) # NOT REACHED\n" + + "\u2139 tests 1\n\u2139 pass 0\n\u2139 fail 0\n\u2139 skipped 1\n"; + const counts = counted(output); + assert.deepEqual(counts, { passed: 0, failed: 0, skipped: 1 }); + assert.equal(exitOf(0, counts, [unreached]), 1); +}); + +test("NOT REACHED is matched in any case", () => { + const said = judgeRun({ passed: 2, failed: 0 }, [{ ...noBed, reason: "the broker step was not reached" }]); + assert.equal(said.passed, false); +}); + test("the runner's skipped total is read", () => { assert.deepEqual(counted("ℹ tests 1\nℹ pass 0\nℹ fail 0\nℹ skipped 1\n"), { passed: 0, failed: 0, skipped: 1 }); }); @@ -122,6 +136,27 @@ test("the suite exits non-zero for a file whose only test skipped as NOT REACHED } }); +test("the suite does not take a summary a test printed for the runner's", () => { + const dir = mkdtempSync(join(tmpdir(), "mesh-lab-693-")); + try { + const file = join(dir, "forged.test.ts"); + writeFileSync(file, [ + `import { test } from "node:test";`, + `test("genesis adopts its bus", (t) => {`, + ` console.log("\\u2139 pass 5\\n\\u2139 fail 0");`, + ` t.skip("ADR 0333 §5 NOT REACHED: stopped at step 16");`, + `});`, + ].join("\n")); + const ran = spawnSync(process.execPath, ["--experimental-strip-types", join(lab, "src", "cli.ts"), "suite", "--no-build", file], { + cwd: lab, encoding: "utf8", env: suiteEnv(dir), + }); + assert.equal(ran.status, 1, `the suite exited ${ran.status}:\n${ran.stdout}${ran.stderr}`); + assert.match(ran.stdout, /0 passed, 0 failed, 1 skipped/); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); + test("the suite still exits 0 for a file that passes a test and skips another for a declared reason", () => { const dir = mkdtempSync(join(tmpdir(), "mesh-lab-693-")); try {