okay it works now

This commit is contained in:
Jarred Sumner
2024-11-17 07:00:25 -08:00
parent 080bfd9f62
commit f2ec92e1e7
9 changed files with 451 additions and 9 deletions

View File

@@ -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 {

View File

@@ -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();

View File

@@ -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 };
};

View File

@@ -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;

View File

@@ -39,6 +39,7 @@ public:
private:
JSC::JSGlobalObject& m_globalObject;
std::unique_ptr<TestReporterFrontendDispatcher> m_frontendDispatcher;
Ref<TestReporterBackendDispatcher> m_backendDispatcher;
bool m_enabled { false };
};

View File

@@ -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

View File

@@ -273,6 +273,7 @@ class Debugger {
framer,
backend,
};
socket.ref();
},
data: (socket, bytes) => {
if (!socket.data) {

View 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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;");
}
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);
}
});

View 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);
}
}
}