Public: anyone can read this; the crew can change it.
A check that fails "(unnamed)" names the test file and the hook that failed #4620
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/4582-a-check-that-fails-unnamed-names-the-tes → main 2 commits fork 2 threads · 2 unresolved
3 files
changed +87
−2
scripts/shards.ts +3 −1
| @@ -7,6 +7,8 @@ | ||
| 7 | 7 | * is disjoint and covering, and that one red shard makes the run red. |
| 8 | 8 | */ |
| 9 | 9 | |
| 10 | import { nameUnnamed } from "../src/checks/bun-test.ts"; | |
| 11 | ||
| 10 | 12 | /** |
| 11 | 13 | * No more than this many `bun test` processes at once, whatever the host: |
| 12 | 14 | * a ceiling for a big shared machine, not the check cell's number. The cell |
| @@ -389,7 +391,7 @@ export function tally(results: readonly ShardResult[], seconds: number): Tally { | ||
| 389 | 391 | tests += Number(ran[1]); |
| 390 | 392 | files += Number(ran[2]); |
| 391 | 393 | } |
| 392 | const named = result.output.match(FAIL) ?? []; | |
| 394 | const named = nameUnnamed(result.output.split("\n")).join("\n").match(FAIL) ?? []; | |
| 393 | 395 | for (const line of new Set(named)) failing.push(`${line} [shard ${i + 1}]`); |
| 394 | 396 | if (result.code !== 0 && named.length === 0) |
| 395 | 397 | unnamed.push( |
src/checks/bun-test.ts +34 −1
| @@ -17,6 +17,39 @@ const FAIL_LINE = /^\(fail\) (.+?)(?: \[[0-9.]+m?s\])?(?: \[shard [0-9]+\])?$/; | ||
| 17 | 17 | const PASS_COUNT = /^\s*(\d+) pass$/; |
| 18 | 18 | const FAIL_COUNT = /^\s*(\d+) fail$/; |
| 19 | 19 | |
| 20 | const HEADER = /^(\S+\.test\.[cm]?[jt]sx?):$/; | |
| 21 | const UNNAMED_FAIL = /^\(fail\) ((?:.* > )?)\(unnamed\)/; | |
| 22 | /** bun's own caret line under a hook that timed out, printed right after its `(fail)`. */ | |
| 23 | const HOOK_TIMEOUT = /^\s*\^ a (\S+) hook timed out/; | |
| 24 | /** The tail that ends a file's report; bun's recap of `(fail)` lines can follow, headerless. */ | |
| 25 | const SUMMARY = /^\s*\d+ pass$|^\d+ tests? failed:$/; | |
| 26 | ||
| 27 | /** | |
| 28 | * bun labels a failure that belongs to no test — a hook that timed out or | |
| 29 | * threw — `(unnamed)`, so every such red looked like one. Each is renamed | |
| 30 | * for the file whose header precedes it, and for the hook when bun's caret | |
| 31 | * line under it says one: `a.test.ts > beforeEach/afterEach (timeout)`. | |
| 32 | * A hook that threw keeps bun's own describe prefix: `a.test.ts > s > (unnamed)`. | |
| 33 | * Once the summary starts no file is current until the next header, so a | |
| 34 | * recap's `(fail)` lines are never given the last file's name. | |
| 35 | */ | |
| 36 | export function nameUnnamed(lines: readonly string[]): string[] { | |
| 37 | let file: string | undefined; | |
| 38 | return lines.map((line, i) => { | |
| 39 | const header = HEADER.exec(line); | |
| 40 | if (header) { | |
| 41 | file = header[1]; | |
| 42 | return line; | |
| 43 | } | |
| 44 | if (SUMMARY.test(line)) file = undefined; | |
| 45 | const unnamed = UNNAMED_FAIL.exec(line); | |
| 46 | if (!unnamed || file === undefined) return line; | |
| 47 | const timeout = HOOK_TIMEOUT.exec(lines[i + 1] ?? ""); | |
| 48 | const what = timeout ? `${timeout[1]} (timeout)` : `${unnamed[1]}(unnamed)`; | |
| 49 | return `(fail) ${file} > ${what}${line.slice(unnamed[0].length)}`; | |
| 50 | }); | |
| 51 | } | |
Next: a recap |
||
| 52 | ||
| 20 | 53 | /** How many failing names are kept — a run with forty failures still fits on one line. */ |
| 21 | 54 | export const MAX_FAILING_NAMES = 3; |
| 22 | 55 | |
| @@ -33,7 +66,7 @@ export function parseBunTestFailure( | ||
| 33 | 66 | const names: string[] = []; |
| 34 | 67 | let passed: number | undefined; |
| 35 | 68 | let failed: number | undefined; |
| 36 | for (const line of lines) { | |
| 69 | for (const line of nameUnnamed(lines)) { | |
| 37 | 70 | const fail = FAIL_LINE.exec(line); |
| 38 | 71 | if (fail) { |
| 39 | 72 | if (names.length < MAX_FAILING_NAMES && !names.includes(fail[1]!)) |
new above
offline
This wording is unverified against real bun output. Use a captured log as a test fixture, including the recap block, so the file and hook attribution is proven rather than assumed.