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

A check's summary, test row and error line are cut with `clip`, so a failing test named with an emoji reads whole #6764

Merged opened this

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

Merged

mason/forge:mason/6629-a-check-s-summary-test-row-and-error-lin → main 1 commit fork

  • Merged. mason/6629-a-check-s-summary-test-row-and-error-lin was merged into main as 257f1dc.

Review

Sign in to review this change.
5 files changed +52 −10
src/checks/summary.ts +2 −5
@@ -10,6 +10,7 @@
1010 import type { CheckDetail, Run } from "../core/runs.ts";
1111 import { parseBunTestFailure } from "./bun-test.ts";
1212 import { oneLine } from "./words.ts";
13 import { clip } from "../text/clip.ts";
1314 import { webvitals, type Entries } from "../release/webvitals.ts";
1415
1516 /**
@@ -26,11 +27,7 @@ const UNNAMED_SHARD =
2627 /** A summary is one line on a card; a name from a subprocess is bounded to fit it. */
2728 const SUMMARY_MAX = 200;
2829
29 function truncate(text: string): string {
30 return text.length > SUMMARY_MAX
31 ? `${text.slice(0, SUMMARY_MAX - 1)}…`
32 : text;
33 }
30 const truncate = (text: string): string => clip(text, SUMMARY_MAX);
3431
3532 export interface CheckFailure {
3633 /** The one line a card shows, already bounded. */
src/checks/tests-row.ts +2 −2
@@ -8,6 +8,7 @@
88 */
99
1010 import { parseShardProgress, type CheckProgress } from "./progress.ts";
11 import { clip } from "../text/clip.ts";
1112
1213 export type PinnedFailure = {
1314 readonly name: string;
@@ -35,8 +36,7 @@ export const PIN_LINES = 3;
3536 const LINE_MAX = 200;
3637
3738 const names = (text: string): string[] => text.split(" ").filter((n) => n !== "");
38 const bound = (text: string): string =>
39 text.length > LINE_MAX ? `${text.slice(0, LINE_MAX - 1)}…` : text;
39 const bound = (text: string): string => clip(text, LINE_MAX);
4040
4141 export function parseTestsRow(lines: readonly string[]): TestsRow {
4242 const base = parseShardProgress(lines);
src/checks/words.ts +3 −2
@@ -1,8 +1,9 @@
1 import { clip } from "../text/clip.ts";
2
13 /** One line of a failure's summary on a narrow screen; the rest is in the run. */
24 const LINE_MAX = 80;
35
46 /** `text` cut to one line of at most `LINE_MAX` characters, with an ellipsis. */
57 export function oneLine(text: string): string {
6 const flat = text.replace(/\s+/g, " ").trim();
7 return flat.length > LINE_MAX ? `${flat.slice(0, LINE_MAX - 1)}…` : flat;
8 return clip(text, LINE_MAX);
89 }
src/runs/error_line.ts +3 −1
@@ -6,6 +6,8 @@
66 * opens a log.
77 */
88
9 import { clip } from "../text/clip.ts";
10
911 /** bun's `(fail)`, `error:`, `Error`, `failed`, `failure` — as words, any case. */
1012 const MARKER = /\(fail\)|\b(?:error|fail(?:ed|ure|s)?)\b/i;
1113
@@ -22,7 +24,7 @@ export function firstErrorLine(lines: readonly string[]): ErrorLine | undefined
2224 for (let i = 0; i < lines.length; i++) {
2325 const text = lines[i]!.trim();
2426 if (text !== "" && MARKER.test(text))
25 return { text: text.length > ERROR_LINE_MAX ? `${text.slice(0, ERROR_LINE_MAX - 1)}…` : text, at: i + 1 };
27 return { text: clip(text, ERROR_LINE_MAX), at: i + 1 };
2628 }
2729 return undefined;
2830 }
test/text-clip-checks.test.ts added +42
@@ -0,0 +1,42 @@
1 import { describe, expect, test } from "bun:test";
2 import { ERROR_LINE_MAX, firstErrorLine } from "../src/runs/error_line.ts";
3 import { oneLine } from "../src/checks/words.ts";
4 import { parseTestsRow } from "../src/checks/tests-row.ts";
5 import { summariseCheckFailure } from "../src/checks/summary.ts";
6
7 const LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
8
9 describe("the lines a red check is explained by are cut at a code point", () => {
10 test("error_line cuts a 300-character line with an emoji straddling its limit and leaves no lone surrogate", () => {
11 // The emoji's high surrogate sits at index ERROR_LINE_MAX - 2, so a
12 // UTF-16 cut at ERROR_LINE_MAX - 1 would split it.
13 const head = "error: " + "x".repeat(ERROR_LINE_MAX - 2 - 7);
14 const line = head + "🔥" + "y".repeat(300 - head.length - 2);
15 expect(line.length).toBe(300);
16 const got = firstErrorLine([line])!;
17 expect(got.at).toBe(1);
18 expect(LONE_SURROGATE.test(got.text)).toBe(false);
19 expect(got.text.endsWith("…")).toBe(true);
20 expect(Array.from(got.text).length).toBe(ERROR_LINE_MAX);
21 });
22
23 test("a failure's one line keeps an emoji whole at its limit", () => {
24 const text = "x".repeat(78) + "🔥" + "y".repeat(40);
25 const got = oneLine(text);
26 expect(LONE_SURROGATE.test(got)).toBe(false);
27 expect(Array.from(got).length).toBe(80);
28 });
29
30 test("a pinned failing test named with an emoji reads whole", () => {
31 const name = "z".repeat(198) + "🔥 tail";
32 const row = parseTestsRow([`(fail) ${name}`]);
33 expect(LONE_SURROGATE.test(row.failures[0]!.name)).toBe(false);
34 });
35
36 test("a summary cut through an emoji leaves no lone surrogate", () => {
37 const line = "w".repeat(198) + "🔥" + "v".repeat(20);
38 const got = summariseCheckFailure([line], "check exited with code 1");
39 expect(LONE_SURROGATE.test(got.summary)).toBe(false);
40 expect(Array.from(got.summary).length).toBe(200);
41 });
42 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs