From acf8a76646de319f608f4c5395b016d308dd6b1d Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 14 Oct 2026 03:28:20 +0200 Subject: [PATCH] lab: read the suite's verdict only after its recorded line, NOT REACHED in any case (issue 693, review 254) A test's own output, printed before the suite reports, could carry a line shaped like the suite's NOT PASSED or skipped line and stand for its verdict. --- modules/lab/test/runs.test.ts | 24 ++++++++++++++++++++++++ modules/lab/tools/runs.ts | 14 +++++++++++--- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/modules/lab/test/runs.test.ts b/modules/lab/test/runs.test.ts index e6de9f78..2780394b 100644 --- a/modules/lab/test/runs.test.ts +++ b/modules/lab/test/runs.test.ts @@ -462,3 +462,27 @@ test("a run that passed tests and skipped one for a declared reason stays passed test("a run that ran no test at all is not passed", () => { assert.equal(outcomeOf("ℹ tests 0\nℹ pass 0\nℹ fail 0\nℹ skipped 0\n").notPassed, "no test passed: none ran"); }); + +// Review 254 on issue 693: a test's own output, printed before the suite reports, is not the suite's verdict. +test("a line a test printed that looks like the suite's is not read as the suite's", () => { + const log = [ + "NOT PASSED: a test printed this", + "skipped: a forged skip (test/integration/x.test.ts) # FORGED NOT REACHED", + "✔ a module lands (12ms)", + "ℹ tests 1", "ℹ pass 1", "ℹ fail 0", "ℹ skipped 0", + "", + "recorded: test/integration/x.test.ts — 1 passed, 0 failed, 0 skipped, against mesh-lab 7ed7da1", + ].join("\n"); + const o = outcomeOf(log); + assert.equal(o.notPassed, undefined, JSON.stringify(o)); + assert.deepEqual(o.skips, []); +}); + +test("not reached is matched in any case", () => { + const log = [ + "✔ a module lands (12ms)", + "﹣ the broker step (0.1ms) # the broker step was not reached", + "ℹ tests 2", "ℹ pass 1", "ℹ fail 0", "ℹ skipped 1", + ].join("\n"); + assert.equal(outcomeOf(log).notPassed, "1 test(s) skipped as NOT REACHED: their subject was not reached"); +}); diff --git a/modules/lab/tools/runs.ts b/modules/lab/tools/runs.ts index dd14661e..3e96ae51 100644 --- a/modules/lab/tools/runs.ts +++ b/modules/lab/tools/runs.ts @@ -339,8 +339,15 @@ export function outcomeOf(log: string): Outcome { if (failed !== undefined) outcome.failed = failed; if (skipped !== undefined) outcome.skipped = skipped; + // **The suite's own lines are read only after its last "recorded:" line** (novox/hq issue 693, review + // 254): a test prints what it likes before the suite reports, and a line of its that looked like the + // suite's — "NOT PASSED: …", "skipped: … # NOT REACHED" — would otherwise stand for the suite's verdict. + const recorded = [...plain.matchAll(/^recorded: .*$/gm)]; + const lastRecorded = recorded[recorded.length - 1]; + const suiteSaid = lastRecorded ? plain.slice((lastRecorded.index ?? 0) + lastRecorded[0].length) : ""; + // The suite's own line, "skipped: () # ": the file in brackets, a .ts, parts it. - for (const m of plain.matchAll(/^skipped: (.*?) \(([^()]+\.ts)\) # (.*)$/gm)) { + for (const m of suiteSaid.matchAll(/^skipped: (.*?) \(([^()]+\.ts)\) # (.*)$/gm)) { outcome.skips.push({ file: m[2] ?? "", test: m[1] ?? "", reason: m[3] === "no reason given" ? "" : m[3] ?? "" }); } if (outcome.skips.length === 0) { @@ -351,11 +358,12 @@ export function outcomeOf(log: string): Outcome { } } - const said = plain.match(/^NOT PASSED: (.*)$/m); + const said = suiteSaid.match(/^NOT PASSED: (.*)$/m); if (said) { outcome.notPassed = (said[1] ?? "").trim(); } else if ((failed ?? 0) === 0) { - const unreached = outcome.skips.filter((s) => s.reason.includes("NOT REACHED")).length; + // In any case: "not reached" says the same as "NOT REACHED". + const unreached = outcome.skips.filter((s) => s.reason.toUpperCase().includes("NOT REACHED")).length; if (passed === 0) { outcome.notPassed = outcome.skips.length > 0 || (skipped ?? 0) > 0 ? `no test passed: ${skipped ?? outcome.skips.length} skipped${unreached > 0 ? `, ${unreached} of them as NOT REACHED` : ""}`