Skip to content

Commit

Permalink
Add inspecto test
Browse files Browse the repository at this point in the history
  • Loading branch information
Jarred-Sumner committed Nov 17, 2024
1 parent 74457e3 commit f46e3f6
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 106 deletions.
19 changes: 19 additions & 0 deletions test/cli/inspect/__snapshots__/inspect.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Bun Snapshot v1, https://goo.gl/fbAQLP

exports[`junit reporter 1`] = `
"<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
<testsuite name="<dir>/a.test.js" tests="2" failures="1" errors="0" skipped="0" timestamp="2024-12-17T15:37:38.935Z">
<testcase classname="<dir>/a.test.js" name="fail" <failure message="Test failed" type="AssertionError">
Error: expect(received).toBe(expected)
Expected: 2
Received: 1
</failure>
</testcase>
<testcase classname="<dir>/a.test.js" name="success" </testcase>
</testsuite>
</testsuites>"
`;
132 changes: 63 additions & 69 deletions test/cli/inspect/inspect.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Subprocess, spawn } from "bun";
import { afterEach, expect, test, describe } from "bun:test";
import { bunEnv, bunExe, isPosix, randomPort } from "harness";
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]+$/);

Expand Down Expand Up @@ -357,77 +359,69 @@ describe("unix domain socket without websocket", () => {
}
});

const enum FramerState {
WaitingForLength,
WaitingForMessage,
}

let socketFramerMessageLengthBuffer: Buffer;
class SocketFramer {
private state: FramerState = FramerState.WaitingForLength;
private pendingLength: number = 0;
private sizeBuffer: Buffer = Buffer.alloc(0);
private sizeBufferIndex: number = 0;
private bufferedData: Buffer = Buffer.alloc(0);

constructor(private onMessage: (message: string) => void) {
if (!socketFramerMessageLengthBuffer) {
socketFramerMessageLengthBuffer = Buffer.alloc(4);
}
this.reset();
}

reset(): void {
this.state = FramerState.WaitingForLength;
this.bufferedData = Buffer.alloc(0);
this.sizeBufferIndex = 0;
this.sizeBuffer = Buffer.alloc(4);
}

send(socket: Socket, data: string): void {
socketFramerMessageLengthBuffer.writeUInt32BE(data.length, 0);
socket.write(socketFramerMessageLengthBuffer);
socket.write(data);
}

onData(socket: Socket<{ framer: SocketFramer; backend: Writer }>, data: Buffer): void {
this.bufferedData = this.bufferedData.length > 0 ? Buffer.concat([this.bufferedData, data]) : data;

let messagesToDeliver: string[] = [];

while (this.bufferedData.length > 0) {
if (this.state === FramerState.WaitingForLength) {
if (this.sizeBufferIndex + this.bufferedData.length < 4) {
const remainingBytes = Math.min(4 - this.sizeBufferIndex, this.bufferedData.length);
this.bufferedData.copy(this.sizeBuffer, this.sizeBufferIndex, 0, remainingBytes);
this.sizeBufferIndex += remainingBytes;
this.bufferedData = this.bufferedData.slice(remainingBytes);
break;
test("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);
});
const remainingBytes = 4 - this.sizeBufferIndex;
this.bufferedData.copy(this.sizeBuffer, this.sizeBufferIndex, 0, remainingBytes);
this.pendingLength = this.sizeBuffer.readUInt32BE(0);
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);
});

this.state = FramerState.WaitingForMessage;
this.sizeBufferIndex = 0;
this.bufferedData = this.bufferedData.slice(remainingBytes);
}
session = new InspectorSession();
session.socket = socket;
session.framer = framer;
socket.data = {
onData: framer.onData.bind(framer),
};

if (this.bufferedData.length < this.pendingLength) {
break;
}
reporter = new JUnitReporter(session);

const message = this.bufferedData.toString("utf-8", 0, this.pendingLength);
this.bufferedData = this.bufferedData.slice(this.pendingLength);
this.state = FramerState.WaitingForLength;
this.pendingLength = 0;
this.sizeBufferIndex = 0;
messagesToDeliver.push(message);
}
await Promise.all([subprocess.exited, promise]);

for (const message of messagesToDeliver) {
this.onMessage(message);
}
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();
});
82 changes: 45 additions & 37 deletions test/cli/inspect/junit-reporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ interface Message {
result?: any;
}

class InspectorSession {
export class InspectorSession {
private messageCallbacks: Map<number, (result: any) => void>;
private eventListeners: Map<string, ((params: any) => void)[]>;
private nextId: number;
Expand Down Expand Up @@ -98,9 +98,9 @@ interface TestInfo {
stderr: string[];
}

class JUnitReporter {
export class JUnitReporter {
private session: InspectorSession;
private testSuites: Map<string, JUnitTestSuite>;
testSuites: Map<string, JUnitTestSuite>;
private tests: Map<number, TestInfo>;
private currentTest: TestInfo | null = null;

Expand Down Expand Up @@ -182,9 +182,6 @@ class JUnitReporter {
if (test.stdout.length > 0) {
testCase.systemOut = test.stdout.join("\n");
}
if (test.stderr.length > 0) {
testCase.systemErr = test.stderr.join("\n");
}

if (params.status === "fail") {
suite.failures++;
Expand All @@ -193,10 +190,15 @@ class JUnitReporter {
type: "AssertionError",
content: test.stderr.join("\n") || "No error details available",
};
test.stderr = [];
} else if (params.status === "skip" || params.status === "todo") {
suite.skipped++;
}

if (test.stderr.length > 0) {
testCase.systemErr = test.stderr.join("\n");
}

suite.testCases.push(testCase);
this.currentTest = null;
}
Expand Down Expand Up @@ -245,21 +247,17 @@ class JUnitReporter {
xml += "<testsuites>\n";

for (const suite of this.testSuites.values()) {
const totalTime = suite.testCases.reduce((sum, test) => sum + test.time, 0);
suite.time = totalTime;

xml += ` <testsuite name="${escapeXml(suite.name)}" `;
xml += `tests="${suite.tests}" `;
xml += `failures="${suite.failures}" `;
xml += `errors="${suite.errors}" `;
xml += `skipped="${suite.skipped}" `;
xml += `time="${suite.time}" `;

xml += `timestamp="${suite.timestamp}">\n`;

for (const testCase of suite.testCases) {
xml += ` <testcase classname="${escapeXml(testCase.classname)}" `;
xml += `name="${escapeXml(testCase.name)}" `;
xml += `time="${testCase.time}">\n`;

if (testCase.failure) {
xml += ` <failure message="${escapeXml(testCase.failure.message)}" `;
Expand Down Expand Up @@ -296,7 +294,10 @@ function escapeXml(str: string): string {
.replace(/'/g, "&apos;");
}

async function connect(address: string): Promise<Socket<{ onData: (socket: Socket<any>, data: Buffer) => void }>> {
export async function connect(
address: string,
onClose?: () => void,
): Promise<Socket<{ onData: (socket: Socket<any>, data: Buffer) => void }>> {
const { promise, resolve } = Promise.withResolvers<Socket<{ onData: (socket: Socket<any>, data: Buffer) => void }>>();

var listener = listen<{ onData: (socket: Socket<any>, data: Buffer) => void }>({
Expand All @@ -313,39 +314,46 @@ async function connect(address: string): Promise<Socket<{ onData: (socket: Socke
error(socket, error) {
console.error(error);
},
close(socket) {
if (onClose) {
onClose();
}
},
},
});

return await promise;
}

// Main execution
const address = process.argv[2];
if (!address) {
throw new Error("Please provide the inspector address as an argument");
}
if (import.meta.main) {
// Main execution
const address = process.argv[2];
if (!address) {
throw new Error("Please provide the inspector address as an argument");
}

let reporter: JUnitReporter;
let session: InspectorSession;
let reporter: JUnitReporter;
let session: InspectorSession;

const socket = await connect(address);
const framer = new SocketFramer((message: string) => {
session.onMessage(message);
});
const socket = await connect(address);
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),
};
session = new InspectorSession();
session.socket = socket;
session.framer = framer;
socket.data = {
onData: framer.onData.bind(framer),
};

reporter = new JUnitReporter(session);
reporter = new JUnitReporter(session);

// Handle process exit
process.on("exit", () => {
if (reporter) {
const report = reporter.generateReport();
console.log(report);
}
});
// Handle process exit
process.on("exit", () => {
if (reporter) {
const report = reporter.generateReport();
console.log(report);
}
});
}

0 comments on commit f46e3f6

Please sign in to comment.