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

The loopback-only look test cannot skip in the forge's check: a skip there is a failure that says why #10682

Merged opened this

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

Merged

mason/forge:mason/10680-the-loopback-only-look-test-cannot-skip → main 2 commits fork 1 thread · 1 unresolved

  • Merged. mason/10680-the-loopback-only-look-test-cannot-skip was merged into main as 0085a34.

Review

Sign in to review this change.
3 files changed +108 −48
test/look-loopback-only-verdict.test.ts added +30
@@ -0,0 +1,30 @@
1 /**
2 * The decision behind test/look-loopback-only.test.ts (#10680), without
3 * spawning Chrome: in the forge's check a missing prerequisite fails and
4 * names itself; outside it the same gap skips with the reason.
5 */
6 import { describe, expect, test } from "bun:test";
7 import { verdict } from "./look-loopback-only.verdict.ts";
8
9 describe("loopback-only look verdict", () => {
10 test("in the check, a missing prerequisite fails and names it", () => {
11 expect(verdict("no Chromium found", { FORGE_CHECK: "01abc" })).toEqual({
12 kind: "fail",
13 line: "loopback-only look: failed in the check, no Chromium found",
14 });
15 expect(verdict("unshare is not installed", { FORGE_CHECK: "01abc" }).kind).toBe("fail");
16 });
17
18 test("outside the check, a missing prerequisite skips with the reason", () => {
19 expect(verdict("no Chromium found", {})).toEqual({
20 kind: "skip",
21 line: "loopback-only look: skipped, no Chromium found",
22 });
23 expect(verdict("cannot make a network namespace: denied", { FORGE_CHECK: "" }).kind).toBe("skip");
24 });
25
26 test("nothing missing runs, in the check or not", () => {
27 expect(verdict(undefined, { FORGE_CHECK: "01abc" })).toEqual({ kind: "run" });
28 expect(verdict(undefined, {})).toEqual({ kind: "run" });
29 });
30 });
test/look-loopback-only.test.ts +42 −48
@@ -2,60 +2,54 @@
22 * What Chrome does when the only interface is loopback (#10631, first half of
33 * #10619). `takeLook` runs inside `unshare -rn` with just `lo` up, against a
44 * Bun server on 127.0.0.1. The test prints what happened and requires a PNG
5 * inside the budget (#10632). Skipped, by name and with the reason
6 * printed, where unshare, Chromium or namespace permission is missing.
5 * inside the budget (#10632). Where unshare, Chromium or namespace
6 * permission is missing it is skipped, by name and with the reason printed;
7 * inside the forge's check that gap fails instead, naming it (#10680).
78 */
89 import { describe, expect, test } from "bun:test";
9 import { findChromium } from "../src/looks/capture.ts";
10 import { missingPrerequisite, verdict } from "./look-loopback-only.verdict.ts";
1011
1112 const BUDGET_MS = 30_000;
1213 const CHILD = new URL("./look-loopback-only.child.ts", import.meta.url).pathname;
1314
14 function missing(): string | undefined {
15 if (process.platform !== "linux") return "not linux";
16 if (Bun.which("unshare") === null) return "unshare is not installed";
17 if (findChromium() === undefined) return "no Chromium found";
18 const probe = Bun.spawnSync(["unshare", "-rn", "true"], {
19 stdout: "ignore",
20 stderr: "pipe",
21 });
22 if (probe.exitCode !== 0)
23 return `cannot make a network namespace: ${probe.stderr.toString().trim().slice(0, 200)}`;
24 return undefined;
25 }
26
27 const why = missing();
28 if (why !== undefined) console.log(`loopback-only look: skipped, ${why}`);
15 const decided = verdict(missingPrerequisite(), process.env);
16 if (decided.kind !== "run") console.log(decided.line);
2917
3018 describe("takeLook with only loopback up", () => {
31 (why === undefined ? test : test.skip)(
32 why === undefined ? "answers with a picture" : `skipped: ${why}`,
33 async () => {
34 const proc = Bun.spawn(
35 [
36 "unshare", "-rn", process.execPath, CHILD,
37 ],
38 { stdout: "pipe", stderr: "pipe", env: process.env },
39 );
40 let timedOut = false;
41 const timer = setTimeout(() => {
42 timedOut = true;
43 proc.kill("SIGKILL");
44 }, BUDGET_MS);
45 const [out, err] = await Promise.all([
46 new Response(proc.stdout).text(),
47 new Response(proc.stderr).text(),
48 proc.exited,
49 ]);
50 clearTimeout(timer);
51 if (timedOut) throw new Error(`loopback-only look: no answer within ${BUDGET_MS / 1000} s`);
52 const line = out.split("\n").find((l) => l.startsWith("loopback-only look: "));
53 console.log(line ?? `loopback-only look: error child printed nothing (${err.trim().slice(0, 300)})`);
54 expect(line).toMatch(/^loopback-only look: png \d+$/);
55 const away = out.split("\n").find((l) => l.startsWith("loopback-only away: "));
56 console.log(away);
57 expect(away).toMatch(/^loopback-only away: refused /);
58 },
59 BUDGET_MS + 5_000,
60 );
19 if (decided.kind === "fail") {
20 test("answers with a picture: its prerequisites are missing in the check", () => {
21 throw new Error(decided.line);
22 });

Prefer an if/else over the early return inside describe(); the control flow is easy to misread.

23 } else {
24 (decided.kind === "run" ? test : test.skip)(
25 decided.kind === "skip" ? `skipped: ${decided.line}` : "answers with a picture",
26 async () => {
27 const proc = Bun.spawn(
28 [
29 "unshare", "-rn", process.execPath, CHILD,
30 ],
31 { stdout: "pipe", stderr: "pipe", env: process.env },
32 );
33 let timedOut = false;
34 const timer = setTimeout(() => {
35 timedOut = true;
36 proc.kill("SIGKILL");
37 }, BUDGET_MS);
38 const [out, err] = await Promise.all([
39 new Response(proc.stdout).text(),
40 new Response(proc.stderr).text(),
41 proc.exited,
42 ]);
43 clearTimeout(timer);
44 if (timedOut) throw new Error(`loopback-only look: no answer within ${BUDGET_MS / 1000} s`);
45 const line = out.split("\n").find((l) => l.startsWith("loopback-only look: "));
46 console.log(line ?? `loopback-only look: error child printed nothing (${err.trim().slice(0, 300)})`);
47 expect(line).toMatch(/^loopback-only look: png \d+$/);
48 const away = out.split("\n").find((l) => l.startsWith("loopback-only away: "));
49 console.log(away);
50 expect(away).toMatch(/^loopback-only away: refused /);
51 },
52 BUDGET_MS + 5_000,
53 );
54 }
6155 });
test/look-loopback-only.verdict.ts added +36
@@ -0,0 +1,36 @@
1 /**
2 * Whether the loopback-only look test runs, skips or fails (#10680). Outside
3 * the forge's check a missing prerequisite (Chromium, `unshare`, a network
4 * namespace) is a skip that says why; inside it — the check sets
5 * `FORGE_CHECK` to its run id — the same gap is a failure that names it, so a
6 * green check always means a picture was taken.
7 */
8 import { findChromium } from "../src/looks/capture.ts";
9
10 export type Verdict =
11 | { kind: "run" }
12 | { kind: "skip"; line: string }
13 | { kind: "fail"; line: string };
14
15 /** Plain data in, plain data out: what is missing, if anything, and where we are. */
16 export function verdict(missing: string | undefined, env: Record<string, string | undefined>): Verdict {
17 if (missing === undefined) return { kind: "run" };
18 const inCheck = (env["FORGE_CHECK"] ?? "") !== "";
19 return inCheck
20 ? { kind: "fail", line: `loopback-only look: failed in the check, ${missing}` }
21 : { kind: "skip", line: `loopback-only look: skipped, ${missing}` };
22 }
23
24 /** What this machine lacks to take the look, or undefined when it has it all. */
25 export function missingPrerequisite(): string | undefined {
26 if (process.platform !== "linux") return "not linux";
27 if (Bun.which("unshare") === null) return "unshare is not installed";
28 if (findChromium() === undefined) return "no Chromium found";
29 const probe = Bun.spawnSync(["unshare", "-rn", "true"], {
30 stdout: "ignore",
31 stderr: "pipe",
32 });
33 if (probe.exitCode !== 0)
34 return `cannot make a network namespace: ${probe.stderr.toString().trim().slice(0, 200)}`;
35 return undefined;
36 }

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs