Public: anyone can read this; the crew can change it.
A red check's summary names the red test, never a warning line #2272
Check · Review · Land · Live done — Landed and live in production.
torque/forge:torque/2271-a-red-check-s-summary-names-the-red-test → main 1 commit fork
4 files
changed +103
−10
scripts/gate-summary.ts +3 −1
| @@ -9,6 +9,8 @@ | ||
| 9 | 9 | * code: a slow file is a warning, not a red. |
| 10 | 10 | */ |
| 11 | 11 | |
| 12 | import { GATE_SUMMARY_HEADING } from "../src/checks/summary.ts"; | |
| 13 | ||
| 12 | 14 | /** A file over this many seconds is named on its own warning line. */ |
| 13 | 15 | export const SLOW_SECONDS = 45; |
| 14 | 16 | /** How many of the slowest files the block lists. */ |
| @@ -107,7 +109,7 @@ export function summaryLines(s: GateSummary): string[] { | ||
| 107 | 109 | // Fail-fast leaves files unstarted: the list is of the files that ran. |
| 108 | 110 | const skipped = s.files - s.times.length; |
| 109 | 111 | return [ |
| 110 | "── gate summary", | |
| 112 | GATE_SUMMARY_HEADING, | |
| 111 | 113 | `test-shards: shards: ${why(s)}`, |
| 112 | 114 | `test-shards: wall: ${s.wallSeconds.toFixed(1)}s`, |
| 113 | 115 | `test-shards: peak memory: ${peak}`, |
src/checks/bun-test.ts +14 −7
| @@ -3,13 +3,17 @@ | ||
| 3 | 3 | * over one known runner, not a general test protocol. Anything that is not |
| 4 | 4 | * exactly this shape (another runner, a build failure, `tsc`'s own errors) |
| 5 | 5 | * 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). | |
| 6 | 10 | */ |
| 7 | 11 | |
| 8 | 12 | import type { CheckDetail } from "../core/runs.ts"; |
| 9 | 13 | |
| 10 | 14 | // The duration is bun's to print or not: under CI it leaves it off a test |
| 11 | 15 | // 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]+\])?$/; | |
| 13 | 17 | const PASS_COUNT = /^\s*(\d+) pass$/; |
| 14 | 18 | const FAIL_COUNT = /^\s*(\d+) fail$/; |
| 15 | 19 | |
| @@ -17,9 +21,11 @@ const FAIL_COUNT = /^\s*(\d+) fail$/; | ||
| 17 | 21 | export const MAX_FAILING_NAMES = 3; |
| 18 | 22 | |
| 19 | 23 | /** |
| 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. | |
| 23 | 29 | */ |
| 24 | 30 | export function parseBunTestFailure( |
| 25 | 31 | lines: readonly string[], |
| @@ -30,16 +36,17 @@ export function parseBunTestFailure( | ||
| 30 | 36 | for (const line of lines) { |
| 31 | 37 | const fail = FAIL_LINE.exec(line); |
| 32 | 38 | 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]!); | |
| 34 | 41 | continue; |
| 35 | 42 | } |
| 36 | 43 | const pass = PASS_COUNT.exec(line); |
| 37 | 44 | if (pass) { |
| 38 | passed = Number(pass[1]); | |
| 45 | passed = (passed ?? 0) + Number(pass[1]); | |
| 39 | 46 | continue; |
| 40 | 47 | } |
| 41 | 48 | const failCount = FAIL_COUNT.exec(line); |
| 42 | if (failCount) failed = Number(failCount[1]); | |
| 49 | if (failCount) failed = (failed ?? 0) + Number(failCount[1]); | |
| 43 | 50 | } |
| 44 | 51 | if (passed === undefined || failed === undefined || failed === 0) |
| 45 | 52 | return undefined; |
src/checks/summary.ts +18 −2
| @@ -10,6 +10,16 @@ | ||
| 10 | 10 | import type { CheckDetail } from "../core/runs.ts"; |
| 11 | 11 | import { parseBunTestFailure } from "./bun-test.ts"; |
| 12 | 12 | |
| 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 | ||
| 13 | 23 | /** A summary is one line on a card; a name from a subprocess is bounded to fit it. */ |
| 14 | 24 | const SUMMARY_MAX = 200; |
| 15 | 25 | |
| @@ -32,7 +42,9 @@ export interface CheckFailure { | ||
| 32 | 42 | * `runStep` says when a step exits non-zero — "check exited with code 1", |
| 33 | 43 | * sometimes with a reason it found on its own — used only when this |
| 34 | 44 | * 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. | |
| 36 | 48 | */ |
| 37 | 49 | export function summariseCheckFailure( |
| 38 | 50 | lines: readonly string[], |
| @@ -47,7 +59,11 @@ export function summariseCheckFailure( | ||
| 47 | 59 | detail, |
| 48 | 60 | }; |
| 49 | 61 | } |
| 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--) { | |
| 51 | 67 | const line = lines[i]!.trim(); |
| 52 | 68 | if (line !== "") return { summary: truncate(line), detail: undefined }; |
| 53 | 69 | } |
test/checks.test.ts +68
| @@ -15,6 +15,8 @@ import { setIssueState } from "../src/actions/issues.ts"; | ||
| 15 | 15 | import { commitToBranch, createRepo, forkRepo } from "../src/actions/repos.ts"; |
| 16 | 16 | import { parseBunTestFailure } from "../src/checks/bun-test.ts"; |
| 17 | 17 | import { summariseCheckFailure } from "../src/checks/summary.ts"; |
| 18 | import { SLOW_SECONDS, summaryLines } from "../scripts/gate-summary.ts"; | |
| 19 | import { tally } from "../scripts/shards.ts"; | |
| 18 | 20 | import { |
| 19 | 21 | pickNext, |
| 20 | 22 | requeueAbandonedChecks, |
| @@ -237,6 +239,72 @@ describe("summariseCheckFailure: what a failed check's summary says first", () = | ||
| 237 | 239 | }); |
| 238 | 240 | }); |
| 239 | 241 | |
| 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 | ||
| 240 | 308 | describe("checks", () => { |
| 241 | 309 | let dataDir: string; |
| 242 | 310 | let app: App; |
new above
offline