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

The loopback-only looks test runs in the forge's check and says what Chrome does there #10633

Merged opened this

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

Merged

torque/forge:torque/10631-the-loopback-only-looks-test-runs-in-the → main 1 commit fork

  • Merged. torque/10631-the-loopback-only-looks-test-runs-in-the was merged into main as 1937f23.

Review

Sign in to review this change.
2 files changed +104 −0
test/look-loopback-only.child.ts added +43
@@ -0,0 +1,43 @@
1 /**
2 * Runs inside `unshare -rn` with only `lo` up (see look-loopback-only.test.ts):
3 * serves a page on 127.0.0.1, takes a look of it with the real `takeLook`, and
4 * prints exactly one line — `loopback-only look: png <bytes>` or
5 * `loopback-only look: error <message>`.
6 */
7 import { existsSync, mkdtempSync, rmSync, statSync } from "node:fs";
8 import { tmpdir } from "node:os";
9 import { join } from "node:path";
10 import { takeLook } from "../src/looks/capture.ts";
11
12 const server = Bun.serve({
13 hostname: "127.0.0.1",
14 port: 0,
15 fetch: () =>
16 new Response("<!doctype html><title>loopback</title><h1>loopback only</h1>", {
17 headers: { "content-type": "text/html; charset=utf-8" },
18 }),
19 });
20 const dir = mkdtempSync(join(tmpdir(), "look-lo-"));
21 const out = join(dir, "look.png");
22 let line: string;
23 try {
24 const got = await takeLook({
25 url: `http://127.0.0.1:${server.port}/`,
26 out,
27 width: 800,
28 height: 600,
29 timeoutMs: 20_000,
30 });
31 line = got.ok
32 ? existsSync(out)
33 ? `png ${statSync(out).size}`
34 : "error no picture written"
35 : `error ${got.error}`;
36 } catch (e) {
37 line = `error ${e instanceof Error ? e.message : String(e)}`;
38 } finally {
39 server.stop(true);
40 rmSync(dir, { recursive: true, force: true });
41 }
42 console.log(`loopback-only look: ${line.replace(/\s+/g, " ").trim()}`);
43 process.exit(0);
test/look-loopback-only.test.ts added +61
@@ -0,0 +1,61 @@
1 /**
2 * What Chrome does when the only interface is loopback (#10631, first half of
3 * #10619). `takeLook` runs inside `unshare -rn` with just `lo` up, against a
4 * Bun server on 127.0.0.1. The test prints what happened and asserts only that
5 * one of the two outcomes came back inside the budget — it proves the failure
6 * (or its absence), it does not fix it. Skipped, by name and with the reason
7 * printed, where unshare, ip, Chromium or namespace permission is missing.
8 */
9 import { describe, expect, test } from "bun:test";
10 import { findChromium } from "../src/looks/capture.ts";
11
12 const BUDGET_MS = 30_000;
13 const CHILD = new URL("./look-loopback-only.child.ts", import.meta.url).pathname;
14
15 function missing(): string | undefined {
16 if (process.platform !== "linux") return "not linux";
17 if (Bun.which("unshare") === null) return "unshare is not installed";
18 if (Bun.which("ip") === null) return "ip is not installed";
19 if (findChromium() === undefined) return "no Chromium found";
20 const probe = Bun.spawnSync(["unshare", "-rn", "ip", "link", "set", "lo", "up"], {
21 stdout: "ignore",
22 stderr: "pipe",
23 });
24 if (probe.exitCode !== 0)
25 return `cannot make a network namespace: ${probe.stderr.toString().trim().slice(0, 200)}`;
26 return undefined;
27 }
28
29 const why = missing();
30 if (why !== undefined) console.log(`loopback-only look: skipped, ${why}`);
31
32 describe("takeLook with only loopback up", () => {
33 (why === undefined ? test : test.skip)(
34 why === undefined ? "answers with a picture or an error" : `skipped: ${why}`,
35 async () => {
36 const proc = Bun.spawn(
37 [
38 "unshare", "-rn", "sh", "-c",
39 `ip link set lo up && exec "${process.execPath}" "${CHILD}"`,
40 ],
41 { stdout: "pipe", stderr: "pipe", env: process.env },
42 );
43 let timedOut = false;
44 const timer = setTimeout(() => {
45 timedOut = true;
46 proc.kill("SIGKILL");
47 }, BUDGET_MS);
48 const [out, err] = await Promise.all([
49 new Response(proc.stdout).text(),
50 new Response(proc.stderr).text(),
51 proc.exited,
52 ]);
53 clearTimeout(timer);
54 if (timedOut) throw new Error(`loopback-only look: no answer within ${BUDGET_MS / 1000} s`);
55 const line = out.split("\n").find((l) => l.startsWith("loopback-only look: "));
56 console.log(line ?? `loopback-only look: error child printed nothing (${err.trim().slice(0, 300)})`);
57 expect(line).toMatch(/^loopback-only look: (png \d+|error .+)$/);
58 },
59 BUDGET_MS + 5_000,
60 );
61 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs