Files
bun.sh/test/js/node/inspector/inspector-profiler.test.ts
2026-01-16 10:12:28 -08:00

403 lines
13 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import inspector from "node:inspector";
import inspectorPromises from "node:inspector/promises";
describe("node:inspector", () => {
describe("Session", () => {
let session: inspector.Session;
beforeEach(() => {
session = new inspector.Session();
});
afterEach(() => {
try {
session.disconnect();
} catch {
// Ignore if already disconnected
}
});
test("Session is a constructor", () => {
expect(inspector.Session).toBeInstanceOf(Function);
expect(session).toBeInstanceOf(inspector.Session);
});
test("Session extends EventEmitter", () => {
expect(typeof session.on).toBe("function");
expect(typeof session.emit).toBe("function");
expect(typeof session.removeListener).toBe("function");
});
test("connect() establishes connection", () => {
expect(() => session.connect()).not.toThrow();
});
test("connect() throws if already connected", () => {
session.connect();
expect(() => session.connect()).toThrow("already connected");
});
test("connectToMainThread() works like connect()", () => {
expect(() => session.connectToMainThread()).not.toThrow();
});
test("disconnect() closes connection cleanly", () => {
session.connect();
expect(() => session.disconnect()).not.toThrow();
});
test("disconnect() is a no-op if not connected", () => {
expect(() => session.disconnect()).not.toThrow();
});
test("post() throws if not connected", () => {
expect(() => session.post("Profiler.enable")).toThrow("not connected");
});
test("post() with callback calls callback with error if not connected", async () => {
const { promise, resolve, reject } = Promise.withResolvers<Error>();
session.post("Profiler.enable", err => {
if (err) resolve(err);
else reject(new Error("Expected error"));
});
const error = await promise;
expect(error.message).toContain("not connected");
});
});
describe("Profiler", () => {
let session: inspector.Session;
beforeEach(() => {
session = new inspector.Session();
session.connect();
});
afterEach(() => {
try {
session.disconnect();
} catch {
// Ignore
}
});
test("Profiler.enable succeeds", () => {
const result = session.post("Profiler.enable");
expect(result).toEqual({});
});
test("Profiler.disable succeeds", () => {
session.post("Profiler.enable");
const result = session.post("Profiler.disable");
expect(result).toEqual({});
});
test("Profiler.start without enable throws", () => {
expect(() => session.post("Profiler.start")).toThrow("not enabled");
});
test("Profiler.start after enable succeeds", () => {
session.post("Profiler.enable");
const result = session.post("Profiler.start");
expect(result).toEqual({});
});
test("Profiler.stop without start throws", () => {
session.post("Profiler.enable");
expect(() => session.post("Profiler.stop")).toThrow("not started");
});
test("Profiler.stop returns valid profile", () => {
session.post("Profiler.enable");
session.post("Profiler.start");
// Do some work to generate profile data
let sum = 0;
for (let i = 0; i < 10000; i++) {
sum += Math.sqrt(i);
}
const result = session.post("Profiler.stop");
expect(result).toHaveProperty("profile");
const profile = result.profile;
// Validate profile structure
expect(profile).toHaveProperty("nodes");
expect(profile).toHaveProperty("startTime");
expect(profile).toHaveProperty("endTime");
expect(profile).toHaveProperty("samples");
expect(profile).toHaveProperty("timeDeltas");
expect(profile.nodes).toBeArray();
expect(profile.nodes.length).toBeGreaterThanOrEqual(1);
// First node should be (root)
const rootNode = profile.nodes[0];
expect(rootNode).toHaveProperty("id", 1);
expect(rootNode).toHaveProperty("callFrame");
expect(rootNode.callFrame).toHaveProperty("functionName", "(root)");
expect(rootNode.callFrame).toHaveProperty("scriptId", "0");
expect(rootNode.callFrame).toHaveProperty("url", "");
expect(rootNode.callFrame).toHaveProperty("lineNumber", -1);
expect(rootNode.callFrame).toHaveProperty("columnNumber", -1);
});
test("complete enable->start->stop workflow", () => {
// Enable profiler
const enableResult = session.post("Profiler.enable");
expect(enableResult).toEqual({});
// Start profiling
const startResult = session.post("Profiler.start");
expect(startResult).toEqual({});
// Do some work
function fibonacci(n: number): number {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
fibonacci(20);
// Stop profiling
const stopResult = session.post("Profiler.stop");
expect(stopResult).toHaveProperty("profile");
// Disable profiler
const disableResult = session.post("Profiler.disable");
expect(disableResult).toEqual({});
});
test("samples and timeDeltas have same length", () => {
session.post("Profiler.enable");
session.post("Profiler.start");
// Do some work
let sum = 0;
for (let i = 0; i < 5000; i++) {
sum += Math.sqrt(i);
}
const result = session.post("Profiler.stop");
const profile = result.profile;
expect(profile.samples.length).toBe(profile.timeDeltas.length);
});
test("samples reference valid node IDs", () => {
session.post("Profiler.enable");
session.post("Profiler.start");
// Do some work
let sum = 0;
for (let i = 0; i < 5000; i++) {
sum += Math.sqrt(i);
}
const result = session.post("Profiler.stop");
const profile = result.profile;
const nodeIds = new Set(profile.nodes.map((n: any) => n.id));
for (const sample of profile.samples) {
expect(nodeIds.has(sample)).toBe(true);
}
});
test("Profiler.setSamplingInterval works", () => {
session.post("Profiler.enable");
const result = session.post("Profiler.setSamplingInterval", { interval: 500 });
expect(result).toEqual({});
});
test("Profiler.setSamplingInterval throws if profiler is running", () => {
session.post("Profiler.enable");
session.post("Profiler.start");
expect(() => session.post("Profiler.setSamplingInterval", { interval: 500 })).toThrow(
"Cannot change sampling interval while profiler is running",
);
session.post("Profiler.stop");
});
test("Profiler.setSamplingInterval requires positive interval", () => {
session.post("Profiler.enable");
expect(() => session.post("Profiler.setSamplingInterval", { interval: 0 })).toThrow();
expect(() => session.post("Profiler.setSamplingInterval", { interval: -1 })).toThrow();
});
test("double Profiler.start is a no-op", () => {
session.post("Profiler.enable");
session.post("Profiler.start");
const result = session.post("Profiler.start");
expect(result).toEqual({});
session.post("Profiler.stop");
});
test("profiler can be restarted after stop", () => {
// First run
session.post("Profiler.enable");
session.post("Profiler.start");
let sum = 0;
for (let i = 0; i < 1000; i++) sum += i;
const result1 = session.post("Profiler.stop");
expect(result1).toHaveProperty("profile");
// Second run
session.post("Profiler.start");
for (let i = 0; i < 1000; i++) sum += i;
const result2 = session.post("Profiler.stop");
expect(result2).toHaveProperty("profile");
// Both profiles should be valid
expect(result1.profile.nodes.length).toBeGreaterThanOrEqual(1);
expect(result2.profile.nodes.length).toBeGreaterThanOrEqual(1);
});
test("disconnect() stops running profiler", () => {
session.post("Profiler.enable");
session.post("Profiler.start");
session.disconnect();
// Create new session and verify profiler was stopped
const session2 = new inspector.Session();
session2.connect();
session2.post("Profiler.enable");
// This should work without error (profiler is not running)
const result = session2.post("Profiler.setSamplingInterval", { interval: 500 });
expect(result).toEqual({});
session2.disconnect();
});
});
describe("callback API", () => {
test("post() with callback receives result", async () => {
const session = new inspector.Session();
session.connect();
const { promise, resolve } = Promise.withResolvers<any>();
session.post("Profiler.enable", (err, result) => {
resolve({ err, result });
});
const { err, result } = await promise;
expect(err).toBeNull();
expect(result).toEqual({});
session.disconnect();
});
test("post() with callback receives error", async () => {
const session = new inspector.Session();
session.connect();
const { promise, resolve } = Promise.withResolvers<any>();
session.post("Profiler.start", (err, result) => {
resolve({ err, result });
});
const { err, result } = await promise;
expect(err).toBeInstanceOf(Error);
expect(err.message).toContain("not enabled");
session.disconnect();
});
});
describe("unsupported methods", () => {
test("unsupported method throws", () => {
const session = new inspector.Session();
session.connect();
expect(() => session.post("Runtime.evaluate")).toThrow("not supported");
session.disconnect();
});
test("coverage APIs throw not supported", () => {
const session = new inspector.Session();
session.connect();
session.post("Profiler.enable");
expect(() => session.post("Profiler.getBestEffortCoverage")).toThrow("not supported");
expect(() => session.post("Profiler.startPreciseCoverage")).toThrow("not supported");
expect(() => session.post("Profiler.stopPreciseCoverage")).toThrow("not supported");
expect(() => session.post("Profiler.takePreciseCoverage")).toThrow("not supported");
session.disconnect();
});
});
describe("exports", () => {
test("url() returns undefined", () => {
expect(inspector.url()).toBeUndefined();
});
test("console is exported", () => {
expect(inspector.console).toBeObject();
expect(inspector.console.log).toBe(globalThis.console.log);
});
test("open() throws not implemented", () => {
expect(() => inspector.open()).toThrow();
});
test("close() throws not implemented", () => {
expect(() => inspector.close()).toThrow();
});
test("waitForDebugger() throws not implemented", () => {
expect(() => inspector.waitForDebugger()).toThrow();
});
});
});
describe("node:inspector/promises", () => {
test("Session is exported", () => {
expect(inspectorPromises.Session).toBeInstanceOf(Function);
});
test("post() returns a Promise", async () => {
const session = new inspectorPromises.Session();
session.connect();
const result = session.post("Profiler.enable");
expect(result).toBeInstanceOf(Promise);
await expect(result).resolves.toEqual({});
session.disconnect();
});
test("post() rejects on error", async () => {
const session = new inspectorPromises.Session();
session.connect();
await expect(session.post("Profiler.start")).rejects.toThrow("not enabled");
session.disconnect();
});
test("complete profiling workflow with promises", async () => {
const session = new inspectorPromises.Session();
session.connect();
await session.post("Profiler.enable");
await session.post("Profiler.start");
// Do some work
function work(n: number): number {
if (n <= 1) return n;
return work(n - 1) + work(n - 2);
}
work(15);
const result = await session.post("Profiler.stop");
expect(result).toHaveProperty("profile");
expect(result.profile.nodes).toBeArray();
await session.post("Profiler.disable");
session.disconnect();
});
test("other exports are the same as node:inspector", () => {
expect(inspectorPromises.url).toBe(inspector.url);
expect(inspectorPromises.console).toBe(inspector.console);
expect(inspectorPromises.open).toBe(inspector.open);
expect(inspectorPromises.close).toBe(inspector.close);
expect(inspectorPromises.waitForDebugger).toBe(inspector.waitForDebugger);
});
});