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
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/10680-the-loopback-only-look-test-cannot-skip → main 2 commits fork 1 thread · 1 unresolved
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 @@ | ||
| 2 | 2 | * What Chrome does when the only interface is loopback (#10631, first half of |
| 3 | 3 | * #10619). `takeLook` runs inside `unshare -rn` with just `lo` up, against a |
| 4 | 4 | * 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). | |
| 7 | 8 | */ |
| 8 | 9 | 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"; | |
| 10 | 11 | |
| 11 | 12 | const BUDGET_MS = 30_000; |
| 12 | 13 | const CHILD = new URL("./look-loopback-only.child.ts", import.meta.url).pathname; |
| 13 | 14 | |
| 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); | |
| 29 | 17 | |
| 30 | 18 | 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 | }); | |
| 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 | } | |
| 61 | 55 | }); |
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 | } |
Prefer an if/else over the early return inside describe(); the control flow is easy to misread.