Public: anyone can read this; the crew can change it.

A red check's summary names each shard's first failure, never the (unnamed) fallout behind it #8750

Merged opened this

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

Merged

mason/forge:mason/8730-a-red-check-s-summary-names-each-shard-s → main 1 commit fork 1 thread · 1 unresolved

  • Merged. mason/8730-a-red-check-s-summary-names-each-shard-s was merged into main as 028e313.

Review

Sign in to review this change.
3 files changed +88 −12
src/checks/bun-test.ts +40 −6
@@ -13,7 +13,11 @@ import type { CheckDetail } from "../core/runs.ts";
1313
1414 // The duration is bun's to print or not: under CI it leaves it off a test
1515 // that took next to no time, so a name is a name with or without one.
16 const FAIL_LINE = /^\(fail\) (.+?)(?: \[[0-9.]+m?s\])?(?: \[shard [0-9]+\])?$/;
16 const FAIL_LINE = /^\(fail\) (.+?)(?: \[[0-9.]+m?s\])?(?: \[shard ([0-9]+)\])?$/;
17 /** The runner's header over one shard's output, live (`── shard 2/4: …`) or held (`── shard 2/4 (exit 1, 3s): …`). */
18 const SHARD_HEADER = /^── shard ([0-9]+)\/[0-9]+(?::| \(exit)/;
19 /** A name that belongs to no test: bun's `(unnamed)`, however {@link nameUnnamed} renamed it. */
20 const FALLOUT = /(?:^|> )\(unnamed\)(?::|$)/;
1721 const PASS_COUNT = /^\s*(\d+) pass$/;
1822 const FAIL_COUNT = /^\s*(\d+) fail$/;
1923
@@ -85,7 +89,32 @@ export function nameUnnamed(lines: readonly string[]): string[] {
8589 export const MAX_FAILING_NAMES = 3;
8690
8791 /**
88 * The counts and the first few failing names, in the order printed, or
92 * The first few failing names: each red shard's lead first, in shard order
93 * (the log holds shards in the order they finished, the tally in shard
94 * order), then the rest of each shard's in the order printed. A shard's
95 * named failures are its names; its `(unnamed)` hook fallout counts only
96 * when it named nothing else, so a summary never leads with what fell over
97 * behind a real failure (#8730). Output with no shards is one shard.
98 */
99 function failingNames(failures: readonly (readonly [shard: number, name: string])[]): string[] {
100 const byShard = new Map<number, string[]>();
101 for (const [shard, name] of failures) {
102 const names = byShard.get(shard) ?? [];
103 if (!names.includes(name)) names.push(name);
104 byShard.set(shard, names);
105 }
106 const groups = [...byShard.entries()]
107 .sort(([a], [b]) => a - b)
108 .map(([, names]) => {
109 const named = names.filter((name) => !FALLOUT.test(name));

Fallout is dropped when the shard has any named failure, including a real afterAll throw that nameUnnamed gave a file name. The summary then hides a genuine cause. Consider keeping renamed entries that carry a file prefix.

110 return named.length > 0 ? named : names;
111 });
112 const ordered = [...groups.map((names) => names[0]!), ...groups.flatMap((names) => names.slice(1))];
113 return [...new Set(ordered)].slice(0, MAX_FAILING_NAMES);
114 }
115
116 /**
117 * The counts and the first few failing names ({@link failingNames}), or
89118 * undefined when the lines never carried `bun test`'s own summary (no
90119 * `N pass` / `M fail`, or every `M` came to zero — a failure from something
91120 * other than a test). A green shard's `0 fail` printed after a red one's
@@ -94,14 +123,19 @@ export const MAX_FAILING_NAMES = 3;
94123 export function parseBunTestFailure(
95124 lines: readonly string[],
96125 ): CheckDetail | undefined {
97 const names: string[] = [];
126 const failures: (readonly [number, string])[] = [];
127 let shard = 0;
98128 let passed: number | undefined;
99129 let failed: number | undefined;
100130 for (const line of nameUnnamed(lines)) {
131 const header = SHARD_HEADER.exec(line);
132 if (header) {
133 shard = Number(header[1]);
134 continue;
135 }
101136 const fail = FAIL_LINE.exec(line);
102137 if (fail) {
103 if (names.length < MAX_FAILING_NAMES && !names.includes(fail[1]!))
104 names.push(fail[1]!);
138 failures.push([fail[2] === undefined ? shard : Number(fail[2]), fail[1]!]);
105139 continue;
106140 }
107141 const pass = PASS_COUNT.exec(line);
@@ -114,5 +148,5 @@ export function parseBunTestFailure(
114148 }
115149 if (passed === undefined || failed === undefined || failed === 0)
116150 return undefined;
117 return { passed, failed, names };
151 return { passed, failed, names: failingNames(failures) };
118152 }
test/checks.test.ts +45
@@ -172,6 +172,51 @@ describe("parseBunTestFailure: a convenience over one known runner, not a genera
172172 });
173173 });
174174
175 test("a shard's named failure leads; the (unnamed) fallout behind it is not named", () => {
176 const lines = [
177 "── shard 1/1 (exit 1, 3s): test/a.test.ts",
178 "test/a.test.ts:",
179 "(fail) a > writes the row [2ms]",
180 "error: database is closed",
181 "(fail) (unnamed) [1ms]",
182 " 4 pass",
183 " 2 fail",
184 ];
185 expect(parseBunTestFailure(lines)?.names).toEqual(["a > writes the row"]);
186 // Only a shard that named nothing else is named by its fallout.
187 const only = ["test/b.test.ts:", "error: boom", "(fail) (unnamed)", " 0 pass", " 1 fail"];
188 expect(parseBunTestFailure(only)?.names).toEqual(["test/b.test.ts > (unnamed): error: boom"]);
189 });
190
191 test("shards that finished out of order are named in shard order, each shard's first failure first", () => {
192 const lines = [
193 "── shard 3/3 (exit 1, 2s): test/c.test.ts",
194 "test/c.test.ts:",
195 "(fail) c one [1ms]",
196 "(fail) c two [1ms]",
197 " 1 pass",
198 " 2 fail",
199 "── shard 1/3 (exit 1, 9s): test/a.test.ts",
200 "test/a.test.ts:",
201 "error: boom",
202 "(fail) (unnamed) [1ms]",
203 " 3 pass",
204 " 1 fail",
205 "── shard 2/3 (exit 0, 5s): test/b.test.ts",
206 " 7 pass",
207 " 0 fail",
208 "2 of 3 shards failed:",
209 "(fail) test/a.test.ts > (unnamed): error: boom [1ms] [shard 1]",
210 "(fail) c one [1ms] [shard 3]",
211 "(fail) c two [1ms] [shard 3]",
212 ];
213 expect(parseBunTestFailure(lines)).toEqual({
214 passed: 11,
215 failed: 3,
216 names: ["test/a.test.ts > (unnamed): error: boom", "c one", "c two"],
217 });
218 });
219
175220 test("output that is not bun test's own shape is not guessed at", () => {
176221 expect(
177222 parseBunTestFailure([
test/shared-red.test.ts +3 −6
@@ -255,12 +255,9 @@ describe("a failure outside a named test names its file and hook (#4582)", () =>
255255
256256 test("summary names the file and the hook, not (unnamed)", () => {
257257 const failure = summariseCheckFailure(captured, "check exited with code 1");
258 expect(failure.detail?.names).toEqual([
259 "a.test.ts > beforeEach/afterEach (timeout)",
260 "c.test.ts > s > (unnamed): error: late",
261 "(unnamed)",
262 ]);
263 expect(failure.summary).toContain("a.test.ts > beforeEach/afterEach (timeout)");
258 // The (unnamed) fallout shows only when nothing else is named (#8730).
259 expect(failure.detail?.names).toEqual(["a.test.ts > beforeEach/afterEach (timeout)"]);
260 expect(failure.summary).toBe('2 of 12 tests — "a.test.ts > beforeEach/afterEach (timeout)" and 1 more');
264261 });
265262
266263 test("the recap after the summary is not given the last file's name", () => {

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs