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

A red check's summary names the red test, never a warning line #2272

Merged opened this in answer to

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

Merged

torque/forge:torque/2271-a-red-check-s-summary-names-the-red-test → main 1 commit fork

  • Merged. torque/2271-a-red-check-s-summary-names-the-red-test was merged into main as 155d26b.

Review

Sign in to review this change.
4 files changed +103 −10
scripts/gate-summary.ts +3 −1
@@ -9,6 +9,8 @@
99 * code: a slow file is a warning, not a red.
1010 */
1111
12 import { GATE_SUMMARY_HEADING } from "../src/checks/summary.ts";
13
1214 /** A file over this many seconds is named on its own warning line. */
1315 export const SLOW_SECONDS = 45;
1416 /** How many of the slowest files the block lists. */
@@ -107,7 +109,7 @@ export function summaryLines(s: GateSummary): string[] {
107109 // Fail-fast leaves files unstarted: the list is of the files that ran.
108110 const skipped = s.files - s.times.length;
109111 return [
110 "── gate summary",
112 GATE_SUMMARY_HEADING,
111113 `test-shards: shards: ${why(s)}`,
112114 `test-shards: wall: ${s.wallSeconds.toFixed(1)}s`,
113115 `test-shards: peak memory: ${peak}`,
src/checks/bun-test.ts +14 −7
@@ -3,13 +3,17 @@
33 * over one known runner, not a general test protocol. Anything that is not
44 * exactly this shape (another runner, a build failure, `tsc`'s own errors)
55 * returns undefined, and the caller keeps saying what it always said.
6 *
7 * The sharded gate (`scripts/test-shards.ts`) prints one tail per shard and
8 * then echoes every `(fail)` line with ` [shard N]` after it: the counts are
9 * the sum of every tail, and an echoed name is the name it echoes (#2271).
610 */
711
812 import type { CheckDetail } from "../core/runs.ts";
913
1014 // The duration is bun's to print or not: under CI it leaves it off a test
1115 // that took next to no time, so a name is a name with or without one.
12 const FAIL_LINE = /^\(fail\) (.+?)(?: \[[0-9.]+m?s\])?$/;
16 const FAIL_LINE = /^\(fail\) (.+?)(?: \[[0-9.]+m?s\])?(?: \[shard [0-9]+\])?$/;
1317 const PASS_COUNT = /^\s*(\d+) pass$/;
1418 const FAIL_COUNT = /^\s*(\d+) fail$/;
1519
@@ -17,9 +21,11 @@ const FAIL_COUNT = /^\s*(\d+) fail$/;
1721 export const MAX_FAILING_NAMES = 3;
1822
1923 /**
20 * The counts and the first few failing names, or undefined when the lines
21 * never carried `bun test`'s own summary (no `N pass` / `M fail`, or `M`
22 * came to zero — a failure from something other than a test).
24 * The counts and the first few failing names, in the order printed, or
25 * undefined when the lines never carried `bun test`'s own summary (no
26 * `N pass` / `M fail`, or every `M` came to zero — a failure from something
27 * other than a test). A green shard's `0 fail` printed after a red one's
28 * adds nothing; it never hides the red one.
2329 */
2430 export function parseBunTestFailure(
2531 lines: readonly string[],
@@ -30,16 +36,17 @@ export function parseBunTestFailure(
3036 for (const line of lines) {
3137 const fail = FAIL_LINE.exec(line);
3238 if (fail) {
33 if (names.length < MAX_FAILING_NAMES) names.push(fail[1]!);
39 if (names.length < MAX_FAILING_NAMES && !names.includes(fail[1]!))
40 names.push(fail[1]!);
3441 continue;
3542 }
3643 const pass = PASS_COUNT.exec(line);
3744 if (pass) {
38 passed = Number(pass[1]);
45 passed = (passed ?? 0) + Number(pass[1]);
3946 continue;
4047 }
4148 const failCount = FAIL_COUNT.exec(line);
42 if (failCount) failed = Number(failCount[1]);
49 if (failCount) failed = (failed ?? 0) + Number(failCount[1]);
4350 }
4451 if (passed === undefined || failed === undefined || failed === 0)
4552 return undefined;
src/checks/summary.ts +18 −2
@@ -10,6 +10,16 @@
1010 import type { CheckDetail } from "../core/runs.ts";
1111 import { parseBunTestFailure } from "./bun-test.ts";
1212
13 /**
14 * The first line of the block a timed full gate ends its log with
15 * (`scripts/gate-summary.ts`): what follows it — slow-file warnings among
16 * it — is how the cell ran, never why the check is red (#2271).
17 */
18 export const GATE_SUMMARY_HEADING = "── gate summary";
19
20 /** The sharded gate's line for a red shard that named no test (`tally` in scripts/shards.ts). */
21 const UNNAMED_SHARD = /^shard [0-9]+ exited -?[0-9]+ without naming a test: /;
22
1323 /** A summary is one line on a card; a name from a subprocess is bounded to fit it. */
1424 const SUMMARY_MAX = 200;
1525
@@ -32,7 +42,9 @@ export interface CheckFailure {
3242 * `runStep` says when a step exits non-zero — "check exited with code 1",
3343 * sometimes with a reason it found on its own — used only when this
3444 * output named nothing better, so a truthful "no output at all" is never
35 * dressed up as more than it is.
45 * dressed up as more than it is. Without test names the summary is the
46 * sharded gate's first red shard that named none, else the last line
47 * before the gate summary block — never a line of the block itself.
3648 */
3749 export function summariseCheckFailure(
3850 lines: readonly string[],
@@ -47,7 +59,11 @@ export function summariseCheckFailure(
4759 detail,
4860 };
4961 }
50 for (let i = lines.length - 1; i >= 0; i--) {
62 const unnamed = lines.find((line) => UNNAMED_SHARD.test(line.trim()));
63 if (unnamed !== undefined)
64 return { summary: truncate(unnamed.trim()), detail: undefined };
65 const block = lines.findLastIndex((line) => line.trim() === GATE_SUMMARY_HEADING);
66 for (let i = (block === -1 ? lines.length : block) - 1; i >= 0; i--) {
5167 const line = lines[i]!.trim();
5268 if (line !== "") return { summary: truncate(line), detail: undefined };
5369 }
test/checks.test.ts +68
@@ -15,6 +15,8 @@ import { setIssueState } from "../src/actions/issues.ts";
1515 import { commitToBranch, createRepo, forkRepo } from "../src/actions/repos.ts";
1616 import { parseBunTestFailure } from "../src/checks/bun-test.ts";
1717 import { summariseCheckFailure } from "../src/checks/summary.ts";
18 import { SLOW_SECONDS, summaryLines } from "../scripts/gate-summary.ts";
19 import { tally } from "../scripts/shards.ts";
1820 import {
1921 pickNext,
2022 requeueAbandonedChecks,
@@ -237,6 +239,72 @@ describe("summariseCheckFailure: what a failed check's summary says first", () =
237239 });
238240 });
239241
242 describe("summariseCheckFailure: a red gate is summarised by its red test, never a warning line (#2271)", () => {
243 // The sharded gate's log, as `scripts/test-shards.ts` prints it: each
244 // shard's own bun test output whole, the red one first, then the tally,
245 // then the gate summary block with its slow-file warning.
246 const red =
247 "bun test v1.3.14\n\ntest/standards.test.ts:\n(pass) the README names every script [1.00ms]\n" +
248 "(fail) releases.json is the shipped releases > every entry is a release [3.00ms]\n\n" +
249 " 11 pass\n 1 fail\n 40 expect() calls\nRan 12 tests across 1 file. [80.00ms]\n";
250 const slow =
251 "bun test v1.3.14\n\ntest/crew.test.ts:\n(pass) a crew is listed [2.00ms]\n\n" +
252 " 30 pass\n 0 fail\n 90 expect() calls\nRan 30 tests across 1 file. [52.40s]\n";
253 const results = [
254 { files: ["test/standards.test.ts"], code: 1, output: red, seconds: 1.2 },
255 { files: ["test/crew.test.ts"], code: 0, output: slow, seconds: 52.5 },
256 ];
257 const block = (times: { file: string; seconds: number }[]): string[] =>
258 summaryLines({
259 shards: 2,
260 cpus: 16,
261 shardsMax: 16,
262 files: 2,
263 override: undefined,
264 memoryBytes: 8 * 1024 ** 3,
265 workerBytes: 1024 ** 3,
266 memory: { limit: undefined, peak: undefined },
267 wallSeconds: 52.6,
268 times,
269 });
270 const log = (shards: typeof results): string[] => [
271 ...shards.flatMap((r, i) => [`── shard ${i + 1}/2 (exit ${r.code}, ${r.seconds}s): ${r.files[0]}`, ...r.output.split("\n")]),
272 ...tally(shards, 52.6).lines,
273 "",
274 ...block(shards.map((r) => ({ file: r.files[0]!, seconds: r.seconds }))),
275 ];
276
277 test("one red file and one slow file: the summary is the red file's test", () => {
278 const lines = log(results);
279 expect(lines.at(-1)).toBe(`test-shards: over ${SLOW_SECONDS} s: test/crew.test.ts (52.5s)`);
280 expect(summariseCheckFailure(lines, "check exited with code 1")).toEqual({
281 summary: '1 of 42 tests — "releases.json is the shipped releases > every entry is a release"',
282 detail: {
283 passed: 41,
284 failed: 1,
285 names: ["releases.json is the shipped releases > every entry is a release"],
286 },
287 });
288 });
289
290 test("a red shard that named no test is named by its files, not by the gate summary block", () => {
291 const crashed = [
292 { files: ["test/standards.test.ts"], code: 137, output: "bun test v1.3.14\n", seconds: 1.2 },
293 results[1]!,
294 ];
295 expect(summariseCheckFailure(log(crashed), "check exited with code 1").summary).toBe(
296 "shard 1 exited 137 without naming a test: test/standards.test.ts",
297 );
298 });
299
300 test("without names or a tally, the last line before the gate summary block, never a line of it", () => {
301 const lines = ["test-shards: something went wrong", "", ...block([{ file: "test/crew.test.ts", seconds: 52.5 }])];
302 expect(summariseCheckFailure(lines, "check exited with code 1").summary).toBe(
303 "test-shards: something went wrong",
304 );
305 });
306 });
307
240308 describe("checks", () => {
241309 let dataDir: string;
242310 let app: App;

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs