mirror of
https://github.com/oven-sh/bun
synced 2026-02-16 22:01:47 +00:00
okay it works now
This commit is contained in:
@@ -106,6 +106,15 @@ public:
|
||||
inspector.setInspectable(true);
|
||||
|
||||
globalObject->inspectorController().connectFrontend(*this, true, false); // waitingForConnection
|
||||
static bool hasConnected = false;
|
||||
|
||||
if (!hasConnected) {
|
||||
hasConnected = true;
|
||||
globalObject->inspectorController().registerAlternateAgent(
|
||||
WTF::makeUnique<Inspector::InspectorLifecycleAgent>(*globalObject));
|
||||
globalObject->inspectorController().registerAlternateAgent(
|
||||
WTF::makeUnique<Inspector::InspectorTestReporterAgent>(*globalObject));
|
||||
}
|
||||
|
||||
Inspector::JSGlobalObjectDebugger* debugger = reinterpret_cast<Inspector::JSGlobalObjectDebugger*>(globalObject->debugger());
|
||||
if (debugger) {
|
||||
@@ -495,11 +504,6 @@ extern "C" void Bun__ensureDebugger(ScriptExecutionContextIdentifier scriptId, b
|
||||
auto& inspector = globalObject->inspectorDebuggable();
|
||||
inspector.setInspectable(true);
|
||||
|
||||
globalObject->inspectorController().registerAlternateAgent(
|
||||
WTF::makeUnique<Inspector::InspectorLifecycleAgent>(*globalObject));
|
||||
globalObject->inspectorController().registerAlternateAgent(
|
||||
WTF::makeUnique<Inspector::InspectorTestReporterAgent>(*globalObject));
|
||||
|
||||
Inspector::JSGlobalObjectDebugger* debugger = reinterpret_cast<Inspector::JSGlobalObjectDebugger*>(globalObject->debugger());
|
||||
if (debugger) {
|
||||
debugger->runWhilePausedCallback = [](JSC::JSGlobalObject& globalObject, bool& isDoneProcessingEvents) -> void {
|
||||
|
||||
@@ -40,6 +40,8 @@ void Bun__LifecycleAgentStopPreventingExit(Inspector::InspectorLifecycleAgent* a
|
||||
InspectorLifecycleAgent::InspectorLifecycleAgent(JSC::JSGlobalObject& globalObject)
|
||||
: InspectorAgentBase("LifecycleReporter"_s)
|
||||
, m_globalObject(globalObject)
|
||||
, m_backendDispatcher(LifecycleReporterBackendDispatcher::create(m_globalObject.inspectorController().backendDispatcher(), this))
|
||||
, m_frontendDispatcher(makeUnique<LifecycleReporterFrontendDispatcher>(const_cast<FrontendRouter&>(m_globalObject.inspectorController().frontendRouter())))
|
||||
{
|
||||
}
|
||||
|
||||
@@ -52,7 +54,6 @@ InspectorLifecycleAgent::~InspectorLifecycleAgent()
|
||||
|
||||
void InspectorLifecycleAgent::didCreateFrontendAndBackend(FrontendRouter*, BackendDispatcher*)
|
||||
{
|
||||
this->m_frontendDispatcher = makeUnique<LifecycleReporterFrontendDispatcher>(const_cast<FrontendRouter&>(m_globalObject.inspectorController().frontendRouter()));
|
||||
}
|
||||
|
||||
void InspectorLifecycleAgent::willDestroyFrontendAndBackend(DisconnectReason)
|
||||
@@ -82,7 +83,7 @@ Protocol::ErrorStringOr<void> InspectorLifecycleAgent::disable()
|
||||
|
||||
void InspectorLifecycleAgent::reportReload()
|
||||
{
|
||||
if (!m_enabled || !m_frontendDispatcher)
|
||||
if (!m_enabled)
|
||||
return;
|
||||
|
||||
m_frontendDispatcher->reload();
|
||||
@@ -90,7 +91,7 @@ void InspectorLifecycleAgent::reportReload()
|
||||
|
||||
void InspectorLifecycleAgent::reportError(ZigException& exception)
|
||||
{
|
||||
if (!m_enabled || !m_frontendDispatcher)
|
||||
if (!m_enabled)
|
||||
return;
|
||||
|
||||
String message = exception.message.toWTFString();
|
||||
|
||||
@@ -40,6 +40,7 @@ public:
|
||||
private:
|
||||
JSC::JSGlobalObject& m_globalObject;
|
||||
std::unique_ptr<LifecycleReporterFrontendDispatcher> m_frontendDispatcher;
|
||||
Ref<LifecycleReporterBackendDispatcher> m_backendDispatcher;
|
||||
bool m_enabled { false };
|
||||
bool m_preventingExit { false };
|
||||
};
|
||||
|
||||
@@ -68,6 +68,8 @@ void Bun__TestReporterAgentReportTestEnd(Inspector::InspectorTestReporterAgent*
|
||||
InspectorTestReporterAgent::InspectorTestReporterAgent(JSC::JSGlobalObject& globalObject)
|
||||
: InspectorAgentBase("TestReporter"_s)
|
||||
, m_globalObject(globalObject)
|
||||
, m_backendDispatcher(TestReporterBackendDispatcher::create(m_globalObject.inspectorController().backendDispatcher(), this))
|
||||
, m_frontendDispatcher(makeUnique<TestReporterFrontendDispatcher>(const_cast<FrontendRouter&>(m_globalObject.inspectorController().frontendRouter())))
|
||||
{
|
||||
}
|
||||
|
||||
@@ -111,7 +113,7 @@ Protocol::ErrorStringOr<void> InspectorTestReporterAgent::disable()
|
||||
|
||||
void InspectorTestReporterAgent::reportTestFound(JSC::CallFrame* callFrame, int testId, const String& name)
|
||||
{
|
||||
if (!m_enabled || !m_frontendDispatcher)
|
||||
if (!m_enabled)
|
||||
return;
|
||||
|
||||
JSC::LineColumn lineColumn;
|
||||
|
||||
@@ -39,6 +39,7 @@ public:
|
||||
private:
|
||||
JSC::JSGlobalObject& m_globalObject;
|
||||
std::unique_ptr<TestReporterFrontendDispatcher> m_frontendDispatcher;
|
||||
Ref<TestReporterBackendDispatcher> m_backendDispatcher;
|
||||
bool m_enabled { false };
|
||||
};
|
||||
|
||||
|
||||
@@ -848,6 +848,8 @@ pub const TestCommand = struct {
|
||||
var results = try std.ArrayList(PathString).initCapacity(ctx.allocator, ctx.positionals.len);
|
||||
defer results.deinit();
|
||||
|
||||
// Start the debugger before we scan for files
|
||||
// But, don't block the main thread waiting if they used --inspect-wait.
|
||||
const test_files, const search_count = scan: {
|
||||
if (for (ctx.positionals) |arg| {
|
||||
if (std.fs.path.isAbsolute(arg) or
|
||||
|
||||
@@ -273,6 +273,7 @@ class Debugger {
|
||||
framer,
|
||||
backend,
|
||||
};
|
||||
socket.ref();
|
||||
},
|
||||
data: (socket, bytes) => {
|
||||
if (!socket.data) {
|
||||
|
||||
351
test/cli/inspect/junit-reporter.ts
Normal file
351
test/cli/inspect/junit-reporter.ts
Normal file
@@ -0,0 +1,351 @@
|
||||
// This is a test app for:
|
||||
// - TestReporter.enable
|
||||
// - TestReporter.found
|
||||
// - TestReporter.start
|
||||
// - TestReporter.end
|
||||
// - Console.messageAdded
|
||||
// - LifecycleReporter.enable
|
||||
// - LifecycleReporter.error
|
||||
|
||||
const debug = false;
|
||||
import { listen, type Socket } from "bun";
|
||||
|
||||
import { SocketFramer } from "./socket-framer.ts";
|
||||
import type { JSC } from "../../../packages/bun-inspector-protocol/src/protocol/jsc";
|
||||
|
||||
interface Message {
|
||||
id?: number;
|
||||
method?: string;
|
||||
params?: any;
|
||||
result?: any;
|
||||
}
|
||||
|
||||
class InspectorSession {
|
||||
private messageCallbacks: Map<number, (result: any) => void>;
|
||||
private eventListeners: Map<string, ((params: any) => void)[]>;
|
||||
private nextId: number;
|
||||
framer?: SocketFramer;
|
||||
socket?: Socket<{ onData: (socket: Socket<any>, data: Buffer) => void }>;
|
||||
|
||||
constructor() {
|
||||
this.messageCallbacks = new Map();
|
||||
this.eventListeners = new Map();
|
||||
this.nextId = 1;
|
||||
}
|
||||
|
||||
onMessage(data: string) {
|
||||
if (debug) console.log(data);
|
||||
const message: Message = JSON.parse(data);
|
||||
|
||||
if (message.id && this.messageCallbacks.has(message.id)) {
|
||||
const callback = this.messageCallbacks.get(message.id)!;
|
||||
callback(message.result);
|
||||
this.messageCallbacks.delete(message.id);
|
||||
} else if (message.method && this.eventListeners.has(message.method)) {
|
||||
if (debug) console.log("event", message.method, message.params);
|
||||
const listeners = this.eventListeners.get(message.method)!;
|
||||
for (const listener of listeners) {
|
||||
listener(message.params);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
send(method: string, params: any = {}) {
|
||||
if (!this.framer) throw new Error("Socket not connected");
|
||||
const id = this.nextId++;
|
||||
const message = { id, method, params };
|
||||
this.framer.send(this.socket as any, JSON.stringify(message));
|
||||
}
|
||||
|
||||
addEventListener(method: string, callback: (params: any) => void) {
|
||||
if (!this.eventListeners.has(method)) {
|
||||
this.eventListeners.set(method, []);
|
||||
}
|
||||
this.eventListeners.get(method)!.push(callback);
|
||||
}
|
||||
}
|
||||
|
||||
interface JUnitTestCase {
|
||||
name: string;
|
||||
classname: string;
|
||||
time: number;
|
||||
failure?: {
|
||||
message: string;
|
||||
type: string;
|
||||
content: string;
|
||||
};
|
||||
systemOut?: string;
|
||||
systemErr?: string;
|
||||
}
|
||||
|
||||
interface JUnitTestSuite {
|
||||
name: string;
|
||||
tests: number;
|
||||
failures: number;
|
||||
errors: number;
|
||||
skipped: number;
|
||||
time: number;
|
||||
timestamp: string;
|
||||
testCases: JUnitTestCase[];
|
||||
}
|
||||
|
||||
interface TestInfo {
|
||||
id: number;
|
||||
name: string;
|
||||
file: string;
|
||||
startTime?: number;
|
||||
stdout: string[];
|
||||
stderr: string[];
|
||||
}
|
||||
|
||||
class JUnitReporter {
|
||||
private session: InspectorSession;
|
||||
private testSuites: Map<string, JUnitTestSuite>;
|
||||
private tests: Map<number, TestInfo>;
|
||||
private currentTest: TestInfo | null = null;
|
||||
|
||||
constructor(session: InspectorSession) {
|
||||
this.session = session;
|
||||
this.testSuites = new Map();
|
||||
this.tests = new Map();
|
||||
|
||||
this.enableDomains();
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
private async enableDomains() {
|
||||
this.session.send("Inspector.enable");
|
||||
this.session.send("TestReporter.enable");
|
||||
this.session.send("LifecycleReporter.enable");
|
||||
this.session.send("Console.enable");
|
||||
this.session.send("Runtime.enable");
|
||||
}
|
||||
|
||||
private setupEventListeners() {
|
||||
this.session.addEventListener("TestReporter.found", this.handleTestFound.bind(this));
|
||||
this.session.addEventListener("TestReporter.start", this.handleTestStart.bind(this));
|
||||
this.session.addEventListener("TestReporter.end", this.handleTestEnd.bind(this));
|
||||
this.session.addEventListener("Console.messageAdded", this.handleConsoleMessage.bind(this));
|
||||
this.session.addEventListener("LifecycleReporter.error", this.handleException.bind(this));
|
||||
}
|
||||
|
||||
private getOrCreateTestSuite(file: string): JUnitTestSuite {
|
||||
if (!this.testSuites.has(file)) {
|
||||
this.testSuites.set(file, {
|
||||
name: file,
|
||||
tests: 0,
|
||||
failures: 0,
|
||||
errors: 0,
|
||||
skipped: 0,
|
||||
time: 0,
|
||||
timestamp: new Date().toISOString(),
|
||||
testCases: [],
|
||||
});
|
||||
}
|
||||
return this.testSuites.get(file)!;
|
||||
}
|
||||
|
||||
private handleTestFound(params: JSC.TestReporter.FoundEvent) {
|
||||
const file = params.url || "unknown";
|
||||
const suite = this.getOrCreateTestSuite(file);
|
||||
suite.tests++;
|
||||
|
||||
const test: TestInfo = {
|
||||
id: params.id,
|
||||
name: params.name || `Test ${params.id}`,
|
||||
file,
|
||||
stdout: [],
|
||||
stderr: [],
|
||||
};
|
||||
this.tests.set(params.id, test);
|
||||
}
|
||||
|
||||
private handleTestStart(params: JSC.TestReporter.StartEvent) {
|
||||
const test = this.tests.get(params.id);
|
||||
if (test) {
|
||||
test.startTime = Date.now();
|
||||
this.currentTest = test;
|
||||
}
|
||||
}
|
||||
|
||||
private handleTestEnd(params: JSC.TestReporter.EndEvent) {
|
||||
const test = this.tests.get(params.id);
|
||||
if (!test || !test.startTime) return;
|
||||
|
||||
const suite = this.getOrCreateTestSuite(test.file);
|
||||
const testCase: JUnitTestCase = {
|
||||
name: test.name,
|
||||
classname: test.file,
|
||||
time: (Date.now() - test.startTime) / 1000,
|
||||
};
|
||||
|
||||
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++;
|
||||
testCase.failure = {
|
||||
message: "Test failed",
|
||||
type: "AssertionError",
|
||||
content: test.stderr.join("\n") || "No error details available",
|
||||
};
|
||||
} else if (params.status === "skip" || params.status === "todo") {
|
||||
suite.skipped++;
|
||||
}
|
||||
|
||||
suite.testCases.push(testCase);
|
||||
this.currentTest = null;
|
||||
}
|
||||
|
||||
private handleConsoleMessage(params: any) {
|
||||
if (!this.currentTest) return;
|
||||
|
||||
const message = params.message;
|
||||
const text = message.text || "";
|
||||
|
||||
if (message.level === "error" || message.level === "warning") {
|
||||
this.currentTest.stderr.push(text);
|
||||
} else {
|
||||
this.currentTest.stdout.push(text);
|
||||
}
|
||||
}
|
||||
|
||||
private handleException(params: JSC.LifecycleReporter.ErrorEvent) {
|
||||
if (!this.currentTest) return;
|
||||
|
||||
const error = params;
|
||||
let stackTrace = "";
|
||||
for (let i = 0; i < error.urls.length; i++) {
|
||||
let url = error.urls[i];
|
||||
let line = Number(error.lineColumns[i * 2]);
|
||||
let column = Number(error.lineColumns[i * 2 + 1]);
|
||||
|
||||
if (column > 0 && line > 0) {
|
||||
stackTrace += ` at ${url}:${line}:${column}\n`;
|
||||
} else if (line > 0) {
|
||||
stackTrace += ` at ${url}:${line}\n`;
|
||||
} else {
|
||||
stackTrace += ` at ${url}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
this.currentTest.stderr.push(`${error.name || "Error"}: ${error.message || "Unknown error"}`, "");
|
||||
if (stackTrace) {
|
||||
this.currentTest.stderr.push(stackTrace);
|
||||
this.currentTest.stderr.push("");
|
||||
}
|
||||
}
|
||||
|
||||
generateReport(): string {
|
||||
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
|
||||
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)}" `;
|
||||
xml += `type="${escapeXml(testCase.failure.type)}">\n`;
|
||||
xml += ` ${escapeXml(testCase.failure.content)}\n`;
|
||||
xml += " </failure>\n";
|
||||
}
|
||||
|
||||
if (testCase.systemOut) {
|
||||
xml += ` <system-out>${escapeXml(testCase.systemOut)}</system-out>\n`;
|
||||
}
|
||||
|
||||
if (testCase.systemErr) {
|
||||
xml += ` <system-err>${escapeXml(testCase.systemErr)}</system-err>\n`;
|
||||
}
|
||||
|
||||
xml += " </testcase>\n";
|
||||
}
|
||||
|
||||
xml += " </testsuite>\n";
|
||||
}
|
||||
|
||||
xml += "</testsuites>";
|
||||
return xml;
|
||||
}
|
||||
}
|
||||
|
||||
function escapeXml(str: string): string {
|
||||
return str
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
async function connect(address: string): 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 }>({
|
||||
unix: address.slice("unix://".length),
|
||||
socket: {
|
||||
open: socket => {
|
||||
listener.stop();
|
||||
socket.ref();
|
||||
resolve(socket);
|
||||
},
|
||||
data(socket, data: Buffer) {
|
||||
socket.data?.onData(socket, data);
|
||||
},
|
||||
error(socket, error) {
|
||||
console.error(error);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return await promise;
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
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),
|
||||
};
|
||||
|
||||
reporter = new JUnitReporter(session);
|
||||
|
||||
// Handle process exit
|
||||
process.on("exit", () => {
|
||||
if (reporter) {
|
||||
const report = reporter.generateReport();
|
||||
console.log(report);
|
||||
}
|
||||
});
|
||||
79
test/cli/inspect/socket-framer.ts
Normal file
79
test/cli/inspect/socket-framer.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
interface Socket<T = any> {
|
||||
data: T;
|
||||
write(data: string | Buffer): void;
|
||||
}
|
||||
|
||||
const enum FramerState {
|
||||
WaitingForLength,
|
||||
WaitingForMessage,
|
||||
}
|
||||
|
||||
let socketFramerMessageLengthBuffer: Buffer;
|
||||
export 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, 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;
|
||||
}
|
||||
|
||||
const remainingBytes = 4 - this.sizeBufferIndex;
|
||||
this.bufferedData.copy(this.sizeBuffer, this.sizeBufferIndex, 0, remainingBytes);
|
||||
this.pendingLength = this.sizeBuffer.readUInt32BE(0);
|
||||
|
||||
this.state = FramerState.WaitingForMessage;
|
||||
this.sizeBufferIndex = 0;
|
||||
this.bufferedData = this.bufferedData.slice(remainingBytes);
|
||||
}
|
||||
|
||||
if (this.bufferedData.length < this.pendingLength) {
|
||||
break;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
for (const message of messagesToDeliver) {
|
||||
this.onMessage(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user