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

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/4582-a-check-that-fails-unnamed-names-the-tes → main 2 commits fork 2 threads · 2 unresolved

  • Merged. scout/4582-a-check-that-fails-unnamed-names-the-tes was merged into main as 2991dfd.

Review

Sign in to review this change.
3 files changed +87 −2
scripts/shards.ts +3 −1
@@ -7,6 +7,8 @@
77 * is disjoint and covering, and that one red shard makes the run red.
88 */
99
10 import { nameUnnamed } from "../src/checks/bun-test.ts";
11
1012 /**
1113 * No more than this many `bun test` processes at once, whatever the host:
1214 * 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 {
389391 tests += Number(ran[1]);
390392 files += Number(ran[2]);
391393 }
392 const named = result.output.match(FAIL) ?? [];
394 const named = nameUnnamed(result.output.split("\n")).join("\n").match(FAIL) ?? [];
393395 for (const line of new Set(named)) failing.push(`${line} [shard ${i + 1}]`);
394396 if (result.code !== 0 && named.length === 0)
395397 unnamed.push(
src/checks/bun-test.ts +34 −1
@@ -17,6 +17,39 @@ const FAIL_LINE = /^\(fail\) (.+?)(?: \[[0-9.]+m?s\])?(?: \[shard [0-9]+\])?$/;
1717 const PASS_COUNT = /^\s*(\d+) pass$/;
1818 const FAIL_COUNT = /^\s*(\d+) fail$/;
1919
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);
outdated

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.

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 (fail) (unnamed) that stays bare still surfaces as a name. Drop it when a named failure was already found, so the summary never lists both.

52
2053 /** How many failing names are kept — a run with forty failures still fits on one line. */
2154 export const MAX_FAILING_NAMES = 3;
2255
@@ -33,7 +66,7 @@ export function parseBunTestFailure(
3366 const names: string[] = [];
3467 let passed: number | undefined;
3568 let failed: number | undefined;
36 for (const line of lines) {
69 for (const line of nameUnnamed(lines)) {
3770 const fail = FAIL_LINE.exec(line);
3871 if (fail) {
3972 if (names.length < MAX_FAILING_NAMES && !names.includes(fail[1]!))
test/shared-red.test.ts +50
@@ -4,6 +4,9 @@
44 * longer ago, is the pull's own; the vitals carry one card per shared test,
55 * linked to its flake issue once the sentinel has filed one.
66 */
7 import { nameUnnamed } from "../src/checks/bun-test.ts";
8 import { summariseCheckFailure } from "../src/checks/summary.ts";
9 import { tally } from "../scripts/shards.ts";
710 import { beforeEach, describe, expect, test } from "bun:test";
811 import type { Database } from "bun:sqlite";
912 import { Glob } from "bun";
@@ -158,3 +161,50 @@ describe("a red many pulls share", () => {
158161 expect(idFloor(at)).toHaveLength(26);
159162 });
160163 });
164
165 describe("a failure outside a named test names its file and hook (#4582)", () => {
166 // Captured from bun 1.3.14 (`CI=1 bun test`): a beforeAll with a 50 ms
167 // timeout, an afterAll that throws, then the summary and a headerless
168 // recap such as bun prints when many tests fail.
169 const captured = [
170 "bun test v1.3.14 (0d9b296a)",
171 "",
172 "a.test.ts:",
173 "(fail) (unnamed) [50.82ms]",
174 " ^ a beforeEach/afterEach hook timed out for this test.",
175 "",
176 "c.test.ts:",
177 '1 | import { afterAll, describe, test } from "bun:test";',
178 "error: late",
179 " at <anonymous> (c.test.ts:2:62)",
180 "(fail) s > (unnamed) [0.13ms]",
181 "",
182 " 10 pass",
183 " 2 fail",
184 "Ran 12 tests across 2 files. [127.00ms]",
185 "",
186 "2 tests failed:",
187 "(fail) (unnamed) [50.82ms]",
188 "(fail) s > (unnamed) [0.13ms]",
189 ];
190
191 test("summary names the file and the hook, not (unnamed)", () => {
192 const failure = summariseCheckFailure(captured, "check exited with code 1");
193 expect(failure.detail?.names).toEqual([
194 "a.test.ts > beforeEach/afterEach (timeout)",
195 "c.test.ts > s > (unnamed)",
196 "(unnamed)",
197 ]);
198 expect(failure.summary).toContain("a.test.ts > beforeEach/afterEach (timeout)");
199 });
200
201 test("the recap after the summary is not given the last file's name", () => {
202 const named = nameUnnamed(captured);
203 expect(named.slice(-2)).toEqual(captured.slice(-2));
204 });
205
206 test("the sharded tally echoes the named failure", () => {
207 const { lines } = tally([{ files: ["a.test.ts"], code: 1, output: captured.join("\n") }], 1);
208 expect(lines).toContain("(fail) a.test.ts > beforeEach/afterEach (timeout) [50.82ms] [shard 1]");
209 });
210 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs