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

Merged opened this

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

Merged

scout/forge:scout/6352-a-shard-killed-without-naming-a-test-say → main 1 commit fork 1 thread · 1 unresolved

  • Merged. scout/6352-a-shard-killed-without-naming-a-test-say was merged into main as 8455bbe.

Review

Sign in to review this change.
4 files changed +41 −4
scripts/shards.ts +17 −3
@@ -363,6 +363,8 @@ export interface ShardResult {
363363 readonly files: readonly string[];
364364 readonly code: number;
365365 readonly output: string;
366 /** How long the shard ran, when the runner timed it. */
367 readonly seconds?: number;
366368 }
367369
368370 /** What the log ends with, and the exit code of the whole run. */
@@ -373,6 +375,20 @@ export interface Tally {
373375
374376 const RAN = /^Ran (\d+) tests? across (\d+) files?\./m;
375377 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(" ")}`;

"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".

391 }
376392
377393 /**
378394 * 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 {
394410 const named = nameUnnamed(result.output.split("\n")).join("\n").match(FAIL) ?? [];
395411 for (const line of new Set(named)) failing.push(`${line} [shard ${i + 1}]`);
396412 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));
400414 });
401415 const took = `${seconds.toFixed(1)}s`;
402416 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";
2020 export const GATE_SUMMARY_HEADING = "── gate summary";
2121
2222 /** 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 )/;
2425
2526 /** A summary is one line on a card; a name from a subprocess is bounded to fit it. */
2627 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
323323 );
324324 });
325325
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
326335 test("without names or a tally, the last line before the gate summary block, never a line of it", () => {
327336 const lines = ["test-shards: something went wrong", "", ...block([{ file: "test/crew.test.ts", seconds: 52.5 }])];
328337 expect(summariseCheckFailure(lines, "check exited with code 1").summary).toBe(
test/shards.test.ts +13
@@ -446,6 +446,19 @@ describe("tally", () => {
446446 });
447447 });
448448
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
449462 describe("splitArgs", () => {
450463 test("after --, a bare name is a file under test/", () => {
451464 expect(

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs