Public: anyone can read this; the crew can change it.
A shard killed without naming a test says which test was running and for how long #6367
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/6352-a-shard-killed-without-naming-a-test-say → main 1 commit fork 1 thread · 1 unresolved
4 files
changed +41
−4
scripts/shards.ts +17 −3
| @@ -363,6 +363,8 @@ export interface ShardResult { | ||
| 363 | 363 | readonly files: readonly string[]; |
| 364 | 364 | readonly code: number; |
| 365 | 365 | readonly output: string; |
| 366 | /** How long the shard ran, when the runner timed it. */ | |
| 367 | readonly seconds?: number; | |
| 366 | 368 | } |
| 367 | 369 | |
| 368 | 370 | /** What the log ends with, and the exit code of the whole run. */ |
| @@ -373,6 +375,20 @@ export interface Tally { | ||
| 373 | 375 | |
| 374 | 376 | const RAN = /^Ran (\d+) tests? across (\d+) files?\./m; |
| 375 | 377 | const FAIL = /^\(fail\) .*$/gm; |
| 378 | const FILE_STARTED = /^(\S+\.test\.[cm]?[jt]sx?):$/gm; | |
| 379 | ||
| 380 | /** The last test file a shard's output says it started, if any. */ | |
| 381 | export function lastStarted(output: string): string | undefined { | |
| 382 | return [...output.matchAll(FILE_STARTED)].at(-1)?.[1]; | |
| 383 | } | |
| 384 | ||
| 385 | /** The line for a red shard that named no test: where it was and how long it ran, when known. */ | |
| 386 | function unnamedLine(n: number, result: ShardResult): string { | |
| 387 | const running = lastStarted(result.output); | |
| 388 | if (running !== undefined && result.seconds !== undefined) | |
| 389 | return `shard ${n} killed after ${Math.round(result.seconds)} s, running ${running}`; | |
| 390 | return `shard ${n} exited ${result.code} without naming a test: ${result.files.join(" ")}`; | |
| 391 | } | |
| 376 | 392 | |
| 377 | 393 | /** |
| 378 | 394 | * Reads every shard's `bun test` output into one ending. Green: one line, |
| @@ -394,9 +410,7 @@ export function tally(results: readonly ShardResult[], seconds: number): Tally { | ||
| 394 | 410 | const named = nameUnnamed(result.output.split("\n")).join("\n").match(FAIL) ?? []; |
| 395 | 411 | for (const line of new Set(named)) failing.push(`${line} [shard ${i + 1}]`); |
| 396 | 412 | if (result.code !== 0 && named.length === 0) |
| 397 | unnamed.push( | |
| 398 | `shard ${i + 1} exited ${result.code} without naming a test: ${result.files.join(" ")}`, | |
| 399 | ); | |
| 413 | unnamed.push(unnamedLine(i + 1, result)); | |
| 400 | 414 | }); |
| 401 | 415 | const took = `${seconds.toFixed(1)}s`; |
| 402 | 416 | const shards = `${results.length} shard${results.length === 1 ? "" : "s"}`; |
src/checks/summary.ts +2 −1
| @@ -20,7 +20,8 @@ import { webvitals, type Entries } from "../release/webvitals.ts"; | ||
| 20 | 20 | export const GATE_SUMMARY_HEADING = "── gate summary"; |
| 21 | 21 | |
| 22 | 22 | /** The sharded gate's line for a red shard that named no test (`tally` in scripts/shards.ts). */ |
| 23 | const UNNAMED_SHARD = /^shard [0-9]+ exited -?[0-9]+ without naming a test: /; | |
| 23 | const UNNAMED_SHARD = | |
| 24 | /^shard [0-9]+ (?:exited -?[0-9]+ without naming a test: |killed after [0-9]+ s, running )/; | |
| 24 | 25 | |
| 25 | 26 | /** A summary is one line on a card; a name from a subprocess is bounded to fit it. */ |
| 26 | 27 | const SUMMARY_MAX = 200; |
test/checks.test.ts +9
| @@ -323,6 +323,15 @@ describe("summariseCheckFailure: a red gate is summarised by its red test, never | ||
| 323 | 323 | ); |
| 324 | 324 | }); |
| 325 | 325 | |
| 326 | test("a killed shard's line is the summary, and the old line is still recognised", () => { | |
| 327 | expect( | |
| 328 | summariseCheckFailure(["shard 258 killed after 412 s, running test/x.test.ts"], "check exited with code 1").summary, | |
| 329 | ).toBe("shard 258 killed after 412 s, running test/x.test.ts"); | |
| 330 | expect( | |
| 331 | summariseCheckFailure(["shard 258 exited 143 without naming a test: a b"], "check exited with code 1").summary, | |
| 332 | ).toBe("shard 258 exited 143 without naming a test: a b"); | |
| 333 | }); | |
| 334 | ||
| 326 | 335 | test("without names or a tally, the last line before the gate summary block, never a line of it", () => { |
| 327 | 336 | const lines = ["test-shards: something went wrong", "", ...block([{ file: "test/crew.test.ts", seconds: 52.5 }])]; |
| 328 | 337 | expect(summariseCheckFailure(lines, "check exited with code 1").summary).toBe( |
test/shards.test.ts +13
| @@ -446,6 +446,19 @@ describe("tally", () => { | ||
| 446 | 446 | }); |
| 447 | 447 | }); |
| 448 | 448 | |
| 449 | describe("tally, a shard killed mid-file", () => { | |
| 450 | test("names the last file it started and how long it ran", () => { | |
| 451 | const output = "test/a.test.ts:\n(pass) x [1ms]\n\ntest/x.test.ts:\n(pass) y [1ms]\n"; | |
| 452 | const ending = tally([{ files: ["test/a.test.ts", "test/x.test.ts"], code: 143, output, seconds: 411.6 }], 412); | |
| 453 | expect(ending.lines).toContain("shard 1 killed after 412 s, running test/x.test.ts"); | |
| 454 | }); | |
| 455 | ||
| 456 | test("without a started file or a timing, the old line stands", () => { | |
| 457 | const ending = tally([{ files: ["b"], code: 143, output: "", seconds: 5 }], 5); | |
| 458 | expect(ending.lines).toContain("shard 1 exited 143 without naming a test: b"); | |
| 459 | }); | |
| 460 | }); | |
| 461 | ||
| 449 | 462 | describe("splitArgs", () => { |
| 450 | 463 | test("after --, a bare name is a file under test/", () => { |
| 451 | 464 | expect( |
new above
offline
"killed" is asserted for every non-zero exit without a named test; a crash exiting 1 will read as a kill. Branch on code (137/143) or say "exited N after S s, running F".