Files
bun.sh/test/cli/inspect/inspect.test.ts

430 lines
10 KiB
TypeScript

import { Subprocess, spawn } from "bun";
import { afterEach, expect, test, describe } from "bun:test";
import { bunEnv, bunExe, isPosix, randomPort, tempDirWithFiles } from "harness";
import { WebSocket } from "ws";
import { join } from "node:path";
let inspectee: Subprocess;
import { SocketFramer } from "./socket-framer";
import { JUnitReporter, InspectorSession, connect } from "./junit-reporter";
import stripAnsi from "strip-ansi";
const anyPort = expect.stringMatching(/^\d+$/);
const anyPathname = expect.stringMatching(/^\/[a-z0-9]+$/);
describe("websocket", () => {
const tests = [
{
args: ["--inspect"],
url: {
protocol: "ws:",
hostname: "localhost",
port: "6499",
pathname: anyPathname,
},
},
{
args: ["--inspect=0"],
url: {
protocol: "ws:",
hostname: "localhost",
port: anyPort,
pathname: anyPathname,
},
},
{
args: [`--inspect=${randomPort()}`],
url: {
protocol: "ws:",
hostname: "localhost",
port: anyPort,
pathname: anyPathname,
},
},
{
args: ["--inspect=localhost"],
url: {
protocol: "ws:",
hostname: "localhost",
port: "6499",
pathname: anyPathname,
},
},
{
args: ["--inspect=localhost/"],
url: {
protocol: "ws:",
hostname: "localhost",
port: "6499",
pathname: "/",
},
},
{
args: ["--inspect=localhost:0"],
url: {
protocol: "ws:",
hostname: "localhost",
port: anyPort,
pathname: anyPathname,
},
},
{
args: ["--inspect=localhost:0/"],
url: {
protocol: "ws:",
hostname: "localhost",
port: anyPort,
pathname: "/",
},
},
{
args: ["--inspect=localhost/foo/bar"],
url: {
protocol: "ws:",
hostname: "localhost",
port: "6499",
pathname: "/foo/bar",
},
},
{
args: ["--inspect=127.0.0.1"],
url: {
protocol: "ws:",
hostname: "127.0.0.1",
port: "6499",
pathname: anyPathname,
},
},
{
args: ["--inspect=127.0.0.1/"],
url: {
protocol: "ws:",
hostname: "127.0.0.1",
port: "6499",
pathname: "/",
},
},
{
args: ["--inspect=127.0.0.1:0/"],
url: {
protocol: "ws:",
hostname: "127.0.0.1",
port: anyPort,
pathname: "/",
},
},
{
args: ["--inspect=[::1]"],
url: {
protocol: "ws:",
hostname: "[::1]",
port: "6499",
pathname: anyPathname,
},
},
{
args: ["--inspect=[::1]:0"],
url: {
protocol: "ws:",
hostname: "[::1]",
port: anyPort,
pathname: anyPathname,
},
},
{
args: ["--inspect=[::1]:0/"],
url: {
protocol: "ws:",
hostname: "[::1]",
port: anyPort,
pathname: "/",
},
},
{
args: ["--inspect=/"],
url: {
protocol: "ws:",
hostname: "localhost",
port: "6499",
pathname: "/",
},
},
{
args: ["--inspect=/foo"],
url: {
protocol: "ws:",
hostname: "localhost",
port: "6499",
pathname: "/foo",
},
},
{
args: ["--inspect=/foo/baz/"],
url: {
protocol: "ws:",
hostname: "localhost",
port: "6499",
pathname: "/foo/baz/",
},
},
{
args: ["--inspect=:0"],
url: {
protocol: "ws:",
hostname: "localhost",
port: anyPort,
pathname: anyPathname,
},
},
{
args: ["--inspect=:0/"],
url: {
protocol: "ws:",
hostname: "localhost",
port: anyPort,
pathname: "/",
},
},
{
args: ["--inspect=ws://localhost/"],
url: {
protocol: "ws:",
hostname: "localhost",
port: anyPort,
pathname: "/",
},
},
{
args: ["--inspect=ws://localhost:0/"],
url: {
protocol: "ws:",
hostname: "localhost",
port: anyPort,
pathname: "/",
},
},
{
args: ["--inspect=ws://localhost:6499/foo/bar"],
url: {
protocol: "ws:",
hostname: "localhost",
port: "6499",
pathname: "/foo/bar",
},
},
];
for (const { args, url: expected } of tests) {
test(`bun ${args.join(" ")}`, async () => {
inspectee = spawn({
cwd: import.meta.dir,
cmd: [bunExe(), ...args, "inspectee.js"],
env: bunEnv,
stdout: "ignore",
stderr: "pipe",
});
let url: URL | undefined;
let stderr = "";
const decoder = new TextDecoder();
for await (const chunk of inspectee.stderr as ReadableStream) {
stderr += decoder.decode(chunk);
for (const line of stderr.split("\n")) {
try {
url = new URL(line);
} catch {
// Ignore
}
if (url?.protocol.includes("ws")) {
break;
}
}
if (stderr.includes("Listening:")) {
break;
}
}
if (!url) {
process.stderr.write(stderr);
throw new Error("Unable to find listening URL");
}
const { protocol, hostname, port, pathname } = url;
expect({
protocol,
hostname,
port,
pathname,
}).toMatchObject(expected);
const webSocket = new WebSocket(url);
expect(
new Promise<void>((resolve, reject) => {
webSocket.addEventListener("open", () => resolve());
webSocket.addEventListener("error", cause => reject(new Error("WebSocket error", { cause })));
webSocket.addEventListener("close", cause => reject(new Error("WebSocket closed", { cause })));
}),
).resolves.toBeUndefined();
webSocket.send(JSON.stringify({ id: 1, method: "Runtime.evaluate", params: { expression: "1 + 1" } }));
expect(
new Promise(resolve => {
webSocket.addEventListener("message", ({ data }) => {
resolve(JSON.parse(data.toString()));
});
}),
).resolves.toMatchObject({
id: 1,
result: {
result: {
type: "number",
value: 2,
},
},
});
webSocket.close();
});
}
// FIXME: Depends on https://github.com/oven-sh/bun/pull/4649
test.todo("bun --inspect=ws+unix:///tmp/inspect.sock");
afterEach(() => {
inspectee?.kill();
});
});
describe("unix domain socket without websocket", () => {
if (isPosix) {
async function runTest(path: string, args: string[], env = bunEnv) {
let { promise, resolve, reject } = Promise.withResolvers();
const framer = new SocketFramer(message => {
resolve(JSON.parse(message));
});
let sock;
using listener = Bun.listen({
unix: path,
socket: {
open: socket => {
sock = socket;
framer.send(socket, JSON.stringify({ id: 1, method: "Runtime.evaluate", params: { expression: "1 + 1" } }));
},
data: (socket, bytes) => {
framer.onData(socket, bytes);
},
error: reject,
},
});
const inspectee = spawn({
cmd: [bunExe(), ...args, join(import.meta.dir, "inspectee.js")],
env,
stdout: "inherit",
stderr: "inherit",
stdin: "inherit",
});
const message = await promise;
expect(message).toMatchObject({
id: 1,
result: {
result: { type: "number", value: 2 },
},
});
inspectee.kill();
sock?.end?.();
}
test("bun --inspect=unix://", async () => {
const path = Math.random().toString(36).substring(2, 15) + ".sock";
const url = new URL(`unix://${path}`);
await runTest(path, ["--inspect=" + url.href]);
});
test("bun --inspect=unix:", async () => {
const path = Math.random().toString(36).substring(2, 15) + ".sock";
await runTest(path, ["--inspect=unix:" + path]);
});
test("BUN_INSPECT=' unix://' bun --inspect", async () => {
const path = Math.random().toString(36).substring(2, 15) + ".sock";
await runTest(path, [], { ...bunEnv, BUN_INSPECT: "unix://" + path });
});
test("BUN_INSPECT='unix:' bun --inspect", async () => {
const path = Math.random().toString(36).substring(2, 15) + ".sock";
await runTest(path, [], { ...bunEnv, BUN_INSPECT: "unix:" + path });
});
}
});
/// TODO: this test is flaky because the inspect may not send all messages before the process exit
/// we need to implement a way/option so we wait every message from the inspector before exiting
test.todo("junit reporter", async () => {
const path = Math.random().toString(36).substring(2, 15) + ".sock";
let reporter: JUnitReporter;
let session: InspectorSession;
const tempdir = tempDirWithFiles("junit-reporter", {
"package.json": `
{
"type": "module",
"scripts": {
"test": "bun a.test.js"
}
}
`,
"a.test.js": `
import { test, expect } from "bun:test";
test("fail", () => {
expect(1).toBe(2);
});
test("success", () => {
expect(1).toBe(1);
});
`,
});
let { resolve, reject, promise } = Promise.withResolvers();
const [socket, subprocess] = await Promise.all([
connect(`unix://${path}`, resolve),
spawn({
cmd: [bunExe(), "--inspect-wait=unix:" + path, "test", join(tempdir, "a.test.js")],
env: bunEnv,
stdout: "inherit",
stderr: "inherit",
stdin: "inherit",
}),
]);
const framer = new SocketFramer((message: string) => {
session.onMessage(message);
});
session = new InspectorSession();
session.socket = socket;
session.framer = framer;
socket.data = {
onData: framer.onData.bind(framer),
};
reporter = new JUnitReporter(session);
await Promise.all([subprocess.exited, promise]);
for (const [file, suite] of reporter.testSuites.entries()) {
suite.time = 1000 * 5;
suite.timestamp = new Date(2024, 11, 17, 15, 37, 38, 935).toISOString();
}
const report = reporter
.generateReport()
.replaceAll("\r\n", "\n")
.replaceAll("\\", "/")
.replaceAll(tempdir.replaceAll("\\", "/"), "<dir>")
.replaceAll(process.cwd().replaceAll("\\", "/"), "<cwd>")
.trim();
expect(stripAnsi(report)).toMatchSnapshot();
});