mirror of
https://github.com/oven-sh/bun
synced 2026-02-17 06:12:08 +00:00
Compare commits
9 Commits
riskymh/re
...
jarred/tes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f46e3f6841 | ||
|
|
74457e311c | ||
|
|
189bda01ed | ||
|
|
f28b262b5c | ||
|
|
f2ec92e1e7 | ||
|
|
080bfd9f62 | ||
|
|
8d7593271b | ||
|
|
8285650928 | ||
|
|
7ca95b1ed8 |
20
.gitignore
vendored
20
.gitignore
vendored
@@ -26,6 +26,7 @@
|
||||
*.db
|
||||
*.dmg
|
||||
*.dSYM
|
||||
*.generated.ts
|
||||
*.jsb
|
||||
*.lib
|
||||
*.log
|
||||
@@ -53,8 +54,8 @@
|
||||
/test-report.md
|
||||
/test.js
|
||||
/test.ts
|
||||
/testdir
|
||||
/test.zig
|
||||
/testdir
|
||||
build
|
||||
build.ninja
|
||||
bun-binary
|
||||
@@ -111,8 +112,10 @@ pnpm-lock.yaml
|
||||
profile.json
|
||||
README.md.template
|
||||
release/
|
||||
scripts/env.local
|
||||
sign.*.json
|
||||
sign.json
|
||||
src/bake/generated.ts
|
||||
src/bun.js/bindings-obj
|
||||
src/bun.js/bindings/GeneratedJS2Native.zig
|
||||
src/bun.js/debug-bindings-obj
|
||||
@@ -131,17 +134,13 @@ src/runtime.version
|
||||
src/tests.zig
|
||||
test.txt
|
||||
test/js/bun/glob/fixtures
|
||||
test/node.js/upstream
|
||||
tsconfig.tsbuildinfo
|
||||
txt.js
|
||||
x64
|
||||
yarn.lock
|
||||
zig-cache
|
||||
zig-out
|
||||
test/node.js/upstream
|
||||
.zig-cache
|
||||
scripts/env.local
|
||||
*.generated.ts
|
||||
src/bake/generated.ts
|
||||
|
||||
# Dependencies
|
||||
/vendor
|
||||
@@ -149,22 +148,23 @@ src/bake/generated.ts
|
||||
# Dependencies (before CMake)
|
||||
# These can be removed in the far future
|
||||
/src/bun.js/WebKit
|
||||
/src/deps/WebKit
|
||||
/src/deps/boringssl
|
||||
/src/deps/brotli
|
||||
/src/deps/c*ares
|
||||
/src/deps/lol*html
|
||||
/src/deps/libarchive
|
||||
/src/deps/libdeflate
|
||||
/src/deps/libuv
|
||||
/src/deps/lol*html
|
||||
/src/deps/ls*hpack
|
||||
/src/deps/mimalloc
|
||||
/src/deps/picohttpparser
|
||||
/src/deps/tinycc
|
||||
/src/deps/zstd
|
||||
/src/deps/zlib
|
||||
/src/deps/WebKit
|
||||
/src/deps/zig
|
||||
/src/deps/zlib
|
||||
/src/deps/zstd
|
||||
|
||||
# Generated files
|
||||
|
||||
.buildkite/ci.yml
|
||||
*.sock
|
||||
0
bench/snippets/INTERNAL_DEBUGGER
Normal file
0
bench/snippets/INTERNAL_DEBUGGER
Normal file
@@ -2,7 +2,7 @@ option(WEBKIT_VERSION "The version of WebKit to use")
|
||||
option(WEBKIT_LOCAL "If a local version of WebKit should be used instead of downloading")
|
||||
|
||||
if(NOT WEBKIT_VERSION)
|
||||
set(WEBKIT_VERSION 3bc4abf2d5875baf500b4687ef869987f6d19e00)
|
||||
set(WEBKIT_VERSION 8f9ae4f01a047c666ef548864294e01df731d4ea)
|
||||
endif()
|
||||
|
||||
if(WEBKIT_LOCAL)
|
||||
|
||||
@@ -1,26 +1,7 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { readFileSync, writeFileSync, realpathSync } from "node:fs";
|
||||
import type { Domain, Property, Protocol } from "../src/protocol/schema";
|
||||
|
||||
run().catch(console.error);
|
||||
|
||||
async function run() {
|
||||
const cwd = new URL("../src/protocol/", import.meta.url);
|
||||
const runner = "Bun" in globalThis ? "bunx" : "npx";
|
||||
const write = (name: string, data: string) => {
|
||||
const path = new URL(name, cwd);
|
||||
writeFileSync(path, data);
|
||||
spawnSync(runner, ["prettier", "--write", path.pathname], { cwd, stdio: "ignore" });
|
||||
};
|
||||
const base = readFileSync(new URL("protocol.d.ts", cwd), "utf-8");
|
||||
const baseNoComments = base.replace(/\/\/.*/g, "");
|
||||
const jsc = await downloadJsc();
|
||||
write("jsc/protocol.json", JSON.stringify(jsc));
|
||||
write("jsc/index.d.ts", "// GENERATED - DO NOT EDIT\n" + formatProtocol(jsc, baseNoComments));
|
||||
const v8 = await downloadV8();
|
||||
write("v8/protocol.json", JSON.stringify(v8));
|
||||
write("v8/index.d.ts", "// GENERATED - DO NOT EDIT\n" + formatProtocol(v8, baseNoComments));
|
||||
}
|
||||
import path from "node:path";
|
||||
|
||||
function formatProtocol(protocol: Protocol, extraTs?: string): string {
|
||||
const { name, domains } = protocol;
|
||||
@@ -29,6 +10,7 @@ function formatProtocol(protocol: Protocol, extraTs?: string): string {
|
||||
let body = `export namespace ${name} {`;
|
||||
for (const { domain, types = [], events = [], commands = [] } of domains) {
|
||||
body += `export namespace ${domain} {`;
|
||||
|
||||
for (const type of types) {
|
||||
body += formatProperty(type);
|
||||
}
|
||||
@@ -153,32 +135,12 @@ async function downloadV8(): Promise<Protocol> {
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* @link https://github.com/WebKit/WebKit/tree/main/Source/JavaScriptCore/inspector/protocol
|
||||
*/
|
||||
async function downloadJsc(): Promise<Protocol> {
|
||||
const baseUrl = "https://raw.githubusercontent.com/WebKit/WebKit/main/Source/JavaScriptCore/inspector/protocol";
|
||||
const domains = [
|
||||
"Runtime",
|
||||
"Console",
|
||||
"Debugger",
|
||||
"Heap",
|
||||
"ScriptProfiler",
|
||||
"CPUProfiler",
|
||||
"GenericTypes",
|
||||
"Network",
|
||||
"Inspector",
|
||||
];
|
||||
return {
|
||||
name: "JSC",
|
||||
version: {
|
||||
major: 1,
|
||||
minor: 3,
|
||||
},
|
||||
domains: await Promise.all(domains.map(domain => download<Domain>(`${baseUrl}/${domain}.json`))).then(domains =>
|
||||
domains.sort((a, b) => a.domain.localeCompare(b.domain)),
|
||||
),
|
||||
};
|
||||
async function getJSC(): Promise<Protocol> {
|
||||
let bunExecutable = Bun.which("bun-debug") || process.execPath;
|
||||
if (!bunExecutable) {
|
||||
throw new Error("bun-debug not found");
|
||||
}
|
||||
bunExecutable = realpathSync(bunExecutable);
|
||||
}
|
||||
|
||||
async function download<V>(url: string): Promise<V> {
|
||||
@@ -200,3 +162,39 @@ function toComment(description?: string): string {
|
||||
const lines = ["/**", ...description.split("\n").map(line => ` * ${line.trim()}`), "*/"];
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
const cwd = new URL("../src/protocol/", import.meta.url);
|
||||
const runner = "Bun" in globalThis ? "bunx" : "npx";
|
||||
const write = (name: string, data: string) => {
|
||||
const filePath = path.resolve(__dirname, "..", "src", "protocol", name);
|
||||
writeFileSync(filePath, data);
|
||||
spawnSync(runner, ["prettier", "--write", filePath], { cwd, stdio: "ignore" });
|
||||
};
|
||||
const base = readFileSync(new URL("protocol.d.ts", cwd), "utf-8");
|
||||
const baseNoComments = base.replace(/\/\/.*/g, "");
|
||||
|
||||
const jscJsonFile = path.resolve(__dirname, process.argv.at(-1) ?? "");
|
||||
let jscJSONFile;
|
||||
try {
|
||||
jscJSONFile = await Bun.file(jscJsonFile).json();
|
||||
} catch (error) {
|
||||
console.warn("Failed to read CombinedDomains.json from WebKit build. Is this a WebKit build from Bun?");
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const jsc = {
|
||||
name: "JSC",
|
||||
version: {
|
||||
major: 1,
|
||||
minor: 4,
|
||||
},
|
||||
domains: jscJSONFile.domains
|
||||
.filter(a => a.debuggableTypes?.includes?.("javascript"))
|
||||
.sort((a, b) => a.domain.localeCompare(b.domain)),
|
||||
};
|
||||
write("jsc/protocol.json", JSON.stringify(jsc, null, 2));
|
||||
write("jsc/index.d.ts", "// GENERATED - DO NOT EDIT\n" + formatProtocol(jsc, baseNoComments));
|
||||
const v8 = await downloadV8();
|
||||
write("v8/protocol.json", JSON.stringify(v8));
|
||||
write("v8/index.d.ts", "// GENERATED - DO NOT EDIT\n" + formatProtocol(v8, baseNoComments));
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
5
packages/bun-types/bun.d.ts
vendored
5
packages/bun-types/bun.d.ts
vendored
@@ -4534,6 +4534,11 @@ declare module "bun" {
|
||||
unix: string;
|
||||
}
|
||||
|
||||
interface FdSocketOptions<Data = undefined> extends SocketOptions<Data> {
|
||||
tls?: TLSOptions;
|
||||
fd: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a TCP client that connects to a server
|
||||
*
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
#include "BunInjectedScriptHost.h"
|
||||
#include <JavaScriptCore/JSGlobalObjectInspectorController.h>
|
||||
|
||||
#include "InspectorLifecycleAgent.h"
|
||||
#include "InspectorTestReporterAgent.h"
|
||||
|
||||
extern "C" void Bun__tickWhilePaused(bool*);
|
||||
extern "C" void Bun__eventLoop__incrementRefConcurrently(void* bunVM, int delta);
|
||||
|
||||
@@ -51,10 +54,6 @@ public:
|
||||
}
|
||||
void unpauseForInitializedInspector() override
|
||||
{
|
||||
if (waitingForConnection) {
|
||||
waitingForConnection = false;
|
||||
Debugger__didConnect();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -100,6 +99,17 @@ public:
|
||||
globalObject->setInspectable(true);
|
||||
auto& inspector = globalObject->inspectorDebuggable();
|
||||
inspector.setInspectable(true);
|
||||
|
||||
static bool hasConnected = false;
|
||||
|
||||
if (!hasConnected) {
|
||||
hasConnected = true;
|
||||
globalObject->inspectorController().registerAlternateAgent(
|
||||
WTF::makeUnique<Inspector::InspectorLifecycleAgent>(*globalObject));
|
||||
globalObject->inspectorController().registerAlternateAgent(
|
||||
WTF::makeUnique<Inspector::InspectorTestReporterAgent>(*globalObject));
|
||||
}
|
||||
|
||||
globalObject->inspectorController().connectFrontend(*this, true, false); // waitingForConnection
|
||||
|
||||
Inspector::JSGlobalObjectDebugger* debugger = reinterpret_cast<Inspector::JSGlobalObjectDebugger*>(globalObject->debugger());
|
||||
@@ -109,6 +119,11 @@ public:
|
||||
};
|
||||
}
|
||||
|
||||
if (waitingForConnection) {
|
||||
waitingForConnection = false;
|
||||
Debugger__didConnect();
|
||||
}
|
||||
|
||||
this->receiveMessagesOnInspectorThread(context, reinterpret_cast<Zig::GlobalObject*>(globalObject), false);
|
||||
}
|
||||
|
||||
@@ -313,6 +328,22 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
void sendMessageToInspectorFromDebuggerThread(Vector<WTF::String, 12>&& inputMessages)
|
||||
{
|
||||
{
|
||||
Locker<Lock> locker(jsThreadMessagesLock);
|
||||
jsThreadMessages.appendVector(inputMessages);
|
||||
}
|
||||
|
||||
if (this->jsWaitForMessageFromInspectorLock.isLocked()) {
|
||||
this->jsWaitForMessageFromInspectorLock.unlock();
|
||||
} else if (this->jsThreadMessageScheduledCount++ == 0) {
|
||||
ScriptExecutionContext::postTaskTo(scriptExecutionContextIdentifier, [connection = this](ScriptExecutionContext& context) {
|
||||
connection->receiveMessagesOnInspectorThread(context, reinterpret_cast<Zig::GlobalObject*>(context.jsGlobalObject()), true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void sendMessageToInspectorFromDebuggerThread(const WTF::String& inputMessage)
|
||||
{
|
||||
{
|
||||
@@ -345,6 +376,8 @@ public:
|
||||
std::atomic<ConnectionStatus> status = ConnectionStatus::Pending;
|
||||
|
||||
bool unrefOnDisconnect = false;
|
||||
|
||||
bool hasEverConnected = false;
|
||||
};
|
||||
|
||||
JSC_DECLARE_HOST_FUNCTION(jsFunctionSend);
|
||||
@@ -404,12 +437,22 @@ private:
|
||||
JSC_DEFINE_HOST_FUNCTION(jsFunctionSend, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
|
||||
{
|
||||
auto* jsConnection = jsDynamicCast<JSBunInspectorConnection*>(callFrame->thisValue());
|
||||
auto message = callFrame->uncheckedArgument(0).toWTFString(globalObject).isolatedCopy();
|
||||
auto message = callFrame->uncheckedArgument(0);
|
||||
|
||||
if (!jsConnection)
|
||||
return JSValue::encode(jsUndefined());
|
||||
|
||||
jsConnection->connection()->sendMessageToInspectorFromDebuggerThread(message);
|
||||
if (message.isString()) {
|
||||
jsConnection->connection()->sendMessageToInspectorFromDebuggerThread(message.toWTFString(globalObject).isolatedCopy());
|
||||
} else if (message.isCell()) {
|
||||
auto* array = jsCast<JSArray*>(message.asCell());
|
||||
Vector<WTF::String, 12> messages;
|
||||
JSC::forEachInArrayLike(globalObject, array, [&](JSC::JSValue value) -> bool {
|
||||
messages.append(value.toWTFString(globalObject).isolatedCopy());
|
||||
return true;
|
||||
});
|
||||
jsConnection->connection()->sendMessageToInspectorFromDebuggerThread(WTFMove(messages));
|
||||
}
|
||||
|
||||
return JSValue::encode(jsUndefined());
|
||||
}
|
||||
@@ -519,12 +562,15 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionCreateConnection, (JSGlobalObject * globalObj
|
||||
return JSValue::encode(JSBunInspectorConnection::create(vm, JSBunInspectorConnection::createStructure(vm, globalObject, globalObject->objectPrototype()), connection));
|
||||
}
|
||||
|
||||
extern "C" void Bun__startJSDebuggerThread(Zig::GlobalObject* debuggerGlobalObject, ScriptExecutionContextIdentifier scriptId, BunString* portOrPathString)
|
||||
extern "C" void Bun__startJSDebuggerThread(Zig::GlobalObject* debuggerGlobalObject, ScriptExecutionContextIdentifier scriptId, BunString* portOrPathString, int isAutomatic)
|
||||
{
|
||||
if (!debuggerScriptExecutionContext)
|
||||
debuggerScriptExecutionContext = debuggerGlobalObject->scriptExecutionContext();
|
||||
|
||||
JSC::VM& vm = debuggerGlobalObject->vm();
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
JSValue defaultValue = debuggerGlobalObject->internalModuleRegistry()->requireId(debuggerGlobalObject, vm, InternalModuleRegistry::Field::InternalDebugger);
|
||||
scope.assertNoException();
|
||||
JSFunction* debuggerDefaultFn = jsCast<JSFunction*>(defaultValue.asCell());
|
||||
|
||||
MarkedArgumentBuffer arguments;
|
||||
@@ -534,8 +580,9 @@ extern "C" void Bun__startJSDebuggerThread(Zig::GlobalObject* debuggerGlobalObje
|
||||
arguments.append(JSFunction::create(vm, debuggerGlobalObject, 3, String(), jsFunctionCreateConnection, ImplementationVisibility::Public));
|
||||
arguments.append(JSFunction::create(vm, debuggerGlobalObject, 1, String("send"_s), jsFunctionSend, ImplementationVisibility::Public));
|
||||
arguments.append(JSFunction::create(vm, debuggerGlobalObject, 0, String("disconnect"_s), jsFunctionDisconnect, ImplementationVisibility::Public));
|
||||
|
||||
arguments.append(jsBoolean(isAutomatic));
|
||||
JSC::call(debuggerGlobalObject, debuggerDefaultFn, arguments, "Bun__initJSDebuggerThread - debuggerDefaultFn"_s);
|
||||
scope.assertNoException();
|
||||
}
|
||||
|
||||
enum class AsyncCallTypeUint8 : uint8_t {
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
#include <wtf/Vector.h>
|
||||
#include <wtf/text/WTFString.h>
|
||||
|
||||
#include <JavaScriptCore/InspectorConsoleAgent.h>
|
||||
|
||||
namespace Inspector {
|
||||
class InspectorConsoleAgent;
|
||||
class InspectorDebuggerAgent;
|
||||
@@ -31,6 +33,7 @@ public:
|
||||
static bool logToSystemConsole();
|
||||
static void setLogToSystemConsole(bool);
|
||||
|
||||
Inspector::InspectorConsoleAgent* consoleAgent() { return m_consoleAgent; }
|
||||
void setDebuggerAgent(InspectorDebuggerAgent* agent) { m_debuggerAgent = agent; }
|
||||
void setPersistentScriptProfilerAgent(InspectorScriptProfilerAgent* agent)
|
||||
{
|
||||
|
||||
@@ -36,7 +36,7 @@ static ImplementationVisibility getImplementationVisibility(JSC::CodeBlock* code
|
||||
return ImplementationVisibility::Public;
|
||||
}
|
||||
|
||||
static bool isImplementationVisibilityPrivate(JSC::StackVisitor& visitor)
|
||||
bool isImplementationVisibilityPrivate(JSC::StackVisitor& visitor)
|
||||
{
|
||||
ImplementationVisibility implementationVisibility = [&]() -> ImplementationVisibility {
|
||||
if (visitor->callee().isCell()) {
|
||||
@@ -63,7 +63,7 @@ static bool isImplementationVisibilityPrivate(JSC::StackVisitor& visitor)
|
||||
return implementationVisibility != ImplementationVisibility::Public;
|
||||
}
|
||||
|
||||
static bool isImplementationVisibilityPrivate(const JSC::StackFrame& frame)
|
||||
bool isImplementationVisibilityPrivate(const JSC::StackFrame& frame)
|
||||
{
|
||||
ImplementationVisibility implementationVisibility = [&]() -> ImplementationVisibility {
|
||||
|
||||
|
||||
@@ -211,4 +211,6 @@ private:
|
||||
}
|
||||
};
|
||||
|
||||
bool isImplementationVisibilityPrivate(JSC::StackVisitor& visitor);
|
||||
bool isImplementationVisibilityPrivate(const JSC::StackFrame& frame);
|
||||
}
|
||||
|
||||
130
src/bun.js/bindings/InspectorLifecycleAgent.cpp
Normal file
130
src/bun.js/bindings/InspectorLifecycleAgent.cpp
Normal file
@@ -0,0 +1,130 @@
|
||||
#include "InspectorLifecycleAgent.h"
|
||||
|
||||
#include <JavaScriptCore/InspectorFrontendRouter.h>
|
||||
#include <JavaScriptCore/InspectorBackendDispatcher.h>
|
||||
#include <JavaScriptCore/JSGlobalObject.h>
|
||||
#include <wtf/text/WTFString.h>
|
||||
#include <JavaScriptCore/ScriptCallStackFactory.h>
|
||||
#include <JavaScriptCore/ScriptArguments.h>
|
||||
#include <JavaScriptCore/ConsoleMessage.h>
|
||||
#include <JavaScriptCore/InspectorConsoleAgent.h>
|
||||
#include <JavaScriptCore/JSGlobalObjectDebuggable.h>
|
||||
#include <JavaScriptCore/JSGlobalObjectInspectorController.h>
|
||||
#include "ConsoleObject.h"
|
||||
|
||||
namespace Inspector {
|
||||
|
||||
// Zig bindings implementation
|
||||
extern "C" {
|
||||
|
||||
void Bun__LifecycleAgentEnable(Inspector::InspectorLifecycleAgent* agent);
|
||||
void Bun__LifecycleAgentDisable(Inspector::InspectorLifecycleAgent* agent);
|
||||
|
||||
void Bun__LifecycleAgentReportReload(Inspector::InspectorLifecycleAgent* agent)
|
||||
{
|
||||
agent->reportReload();
|
||||
}
|
||||
|
||||
void Bun__LifecycleAgentReportError(Inspector::InspectorLifecycleAgent* agent, ZigException* exception)
|
||||
{
|
||||
ASSERT(exception);
|
||||
ASSERT(agent);
|
||||
|
||||
agent->reportError(*exception);
|
||||
}
|
||||
|
||||
void Bun__LifecycleAgentPreventExit(Inspector::InspectorLifecycleAgent* agent);
|
||||
void Bun__LifecycleAgentStopPreventingExit(Inspector::InspectorLifecycleAgent* agent);
|
||||
}
|
||||
|
||||
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())))
|
||||
{
|
||||
}
|
||||
|
||||
InspectorLifecycleAgent::~InspectorLifecycleAgent()
|
||||
{
|
||||
if (m_enabled) {
|
||||
Bun__LifecycleAgentDisable(this);
|
||||
}
|
||||
}
|
||||
|
||||
void InspectorLifecycleAgent::didCreateFrontendAndBackend(FrontendRouter*, BackendDispatcher*)
|
||||
{
|
||||
}
|
||||
|
||||
void InspectorLifecycleAgent::willDestroyFrontendAndBackend(DisconnectReason)
|
||||
{
|
||||
disable();
|
||||
}
|
||||
|
||||
Protocol::ErrorStringOr<void> InspectorLifecycleAgent::enable()
|
||||
{
|
||||
if (m_enabled)
|
||||
return {};
|
||||
|
||||
m_enabled = true;
|
||||
Bun__LifecycleAgentEnable(this);
|
||||
return {};
|
||||
}
|
||||
|
||||
Protocol::ErrorStringOr<void> InspectorLifecycleAgent::disable()
|
||||
{
|
||||
if (!m_enabled)
|
||||
return {};
|
||||
|
||||
m_enabled = false;
|
||||
Bun__LifecycleAgentDisable(this);
|
||||
return {};
|
||||
}
|
||||
|
||||
void InspectorLifecycleAgent::reportReload()
|
||||
{
|
||||
if (!m_enabled)
|
||||
return;
|
||||
|
||||
m_frontendDispatcher->reload();
|
||||
}
|
||||
|
||||
void InspectorLifecycleAgent::reportError(ZigException& exception)
|
||||
{
|
||||
if (!m_enabled)
|
||||
return;
|
||||
|
||||
String message = exception.message.toWTFString();
|
||||
String name = exception.name.toWTFString();
|
||||
|
||||
Ref<JSON::ArrayOf<String>> urls = JSON::ArrayOf<String>::create();
|
||||
Ref<JSON::ArrayOf<int>> lineColumns = JSON::ArrayOf<int>::create();
|
||||
Ref<JSON::ArrayOf<String>> sourceLines = JSON::ArrayOf<String>::create();
|
||||
|
||||
for (size_t i = 0; i < exception.stack.source_lines_len; i++) {
|
||||
sourceLines->addItem(exception.stack.source_lines_ptr[i].toWTFString());
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < exception.stack.frames_len; i++) {
|
||||
ZigStackFrame* frame = &exception.stack.frames_ptr[i];
|
||||
lineColumns->addItem(frame->position.line_zero_based + 1);
|
||||
lineColumns->addItem(frame->position.column_zero_based + 1);
|
||||
}
|
||||
|
||||
// error(const String& message, const String& name, Ref<JSON::ArrayOf<String>>&& urls, Ref<JSON::ArrayOf<int>>&& lineColumns, Ref<JSON::ArrayOf<String>>&& sourceLines);
|
||||
m_frontendDispatcher->error(WTFMove(message), WTFMove(name), WTFMove(urls), WTFMove(lineColumns), WTFMove(sourceLines));
|
||||
}
|
||||
|
||||
Protocol::ErrorStringOr<void> InspectorLifecycleAgent::preventExit()
|
||||
{
|
||||
m_preventingExit = true;
|
||||
return {};
|
||||
}
|
||||
|
||||
Protocol::ErrorStringOr<void> InspectorLifecycleAgent::stopPreventingExit()
|
||||
{
|
||||
m_preventingExit = false;
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace Inspector
|
||||
48
src/bun.js/bindings/InspectorLifecycleAgent.h
Normal file
48
src/bun.js/bindings/InspectorLifecycleAgent.h
Normal file
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include "root.h"
|
||||
#include <JavaScriptCore/AlternateDispatchableAgent.h>
|
||||
#include <JavaScriptCore/InspectorAgentBase.h>
|
||||
#include <JavaScriptCore/InspectorBackendDispatchers.h>
|
||||
#include <JavaScriptCore/InspectorFrontendDispatchers.h>
|
||||
#include <JavaScriptCore/JSGlobalObject.h>
|
||||
#include <wtf/Forward.h>
|
||||
#include <wtf/Noncopyable.h>
|
||||
#include "headers-handwritten.h"
|
||||
namespace Inspector {
|
||||
|
||||
class FrontendRouter;
|
||||
class BackendDispatcher;
|
||||
class LifecycleReporterFrontendDispatcher;
|
||||
enum class DisconnectReason;
|
||||
|
||||
class InspectorLifecycleAgent final : public InspectorAgentBase, public Inspector::LifecycleReporterBackendDispatcherHandler {
|
||||
WTF_MAKE_NONCOPYABLE(InspectorLifecycleAgent);
|
||||
|
||||
public:
|
||||
InspectorLifecycleAgent(JSC::JSGlobalObject&);
|
||||
virtual ~InspectorLifecycleAgent();
|
||||
|
||||
// InspectorAgentBase
|
||||
virtual void didCreateFrontendAndBackend(FrontendRouter*, BackendDispatcher*) final;
|
||||
virtual void willDestroyFrontendAndBackend(DisconnectReason) final;
|
||||
|
||||
// LifecycleReporterBackendDispatcherHandler
|
||||
virtual Protocol::ErrorStringOr<void> enable() final;
|
||||
virtual Protocol::ErrorStringOr<void> disable() final;
|
||||
|
||||
// Public API
|
||||
void reportReload();
|
||||
void reportError(ZigException&);
|
||||
Protocol::ErrorStringOr<void> preventExit();
|
||||
Protocol::ErrorStringOr<void> stopPreventingExit();
|
||||
|
||||
private:
|
||||
JSC::JSGlobalObject& m_globalObject;
|
||||
std::unique_ptr<LifecycleReporterFrontendDispatcher> m_frontendDispatcher;
|
||||
Ref<LifecycleReporterBackendDispatcher> m_backendDispatcher;
|
||||
bool m_enabled { false };
|
||||
bool m_preventingExit { false };
|
||||
};
|
||||
|
||||
} // namespace Inspector
|
||||
210
src/bun.js/bindings/InspectorTestReporterAgent.cpp
Normal file
210
src/bun.js/bindings/InspectorTestReporterAgent.cpp
Normal file
@@ -0,0 +1,210 @@
|
||||
#include "InspectorTestReporterAgent.h"
|
||||
|
||||
#include <JavaScriptCore/InspectorFrontendRouter.h>
|
||||
#include <JavaScriptCore/InspectorBackendDispatcher.h>
|
||||
#include <JavaScriptCore/JSGlobalObject.h>
|
||||
#include <wtf/text/WTFString.h>
|
||||
#include <JavaScriptCore/ScriptCallStack.h>
|
||||
#include <JavaScriptCore/JSGlobalObjectDebuggable.h>
|
||||
#include <JavaScriptCore/JSGlobalObjectInspectorController.h>
|
||||
#include "ErrorStackTrace.h"
|
||||
#include "ZigGlobalObject.h"
|
||||
|
||||
#include "ModuleLoader.h"
|
||||
|
||||
namespace Inspector {
|
||||
|
||||
// Zig bindings implementation
|
||||
extern "C" {
|
||||
|
||||
void Bun__TestReporterAgentEnable(Inspector::InspectorTestReporterAgent* agent);
|
||||
void Bun__TestReporterAgentDisable(Inspector::InspectorTestReporterAgent* agent);
|
||||
|
||||
void Bun__TestReporterAgentReportTestFound(Inspector::InspectorTestReporterAgent* agent, JSC::CallFrame* callFrame, int testId, BunString* name)
|
||||
{
|
||||
auto str = name->toWTFString(BunString::ZeroCopy);
|
||||
agent->reportTestFound(callFrame, testId, str);
|
||||
}
|
||||
|
||||
void Bun__TestReporterAgentReportTestStart(Inspector::InspectorTestReporterAgent* agent, int testId)
|
||||
{
|
||||
agent->reportTestStart(testId);
|
||||
}
|
||||
|
||||
enum class BunTestStatus : uint8_t {
|
||||
Pass,
|
||||
Fail,
|
||||
Timeout,
|
||||
Skip,
|
||||
Todo,
|
||||
};
|
||||
|
||||
void Bun__TestReporterAgentReportTestEnd(Inspector::InspectorTestReporterAgent* agent, int testId, BunTestStatus bunTestStatus, double elapsed)
|
||||
{
|
||||
Protocol::TestReporter::TestStatus status;
|
||||
switch (bunTestStatus) {
|
||||
case BunTestStatus::Pass:
|
||||
status = Protocol::TestReporter::TestStatus::Pass;
|
||||
break;
|
||||
case BunTestStatus::Fail:
|
||||
status = Protocol::TestReporter::TestStatus::Fail;
|
||||
break;
|
||||
case BunTestStatus::Timeout:
|
||||
status = Protocol::TestReporter::TestStatus::Timeout;
|
||||
break;
|
||||
case BunTestStatus::Skip:
|
||||
status = Protocol::TestReporter::TestStatus::Skip;
|
||||
break;
|
||||
case BunTestStatus::Todo:
|
||||
status = Protocol::TestReporter::TestStatus::Todo;
|
||||
break;
|
||||
default:
|
||||
ASSERT_NOT_REACHED();
|
||||
}
|
||||
agent->reportTestEnd(testId, status, elapsed);
|
||||
}
|
||||
}
|
||||
|
||||
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())))
|
||||
{
|
||||
}
|
||||
|
||||
InspectorTestReporterAgent::~InspectorTestReporterAgent()
|
||||
{
|
||||
if (m_enabled) {
|
||||
Bun__TestReporterAgentDisable(this);
|
||||
}
|
||||
}
|
||||
|
||||
void InspectorTestReporterAgent::didCreateFrontendAndBackend(FrontendRouter* frontendRouter, BackendDispatcher* backendDispatcher)
|
||||
{
|
||||
this->m_frontendDispatcher = makeUnique<TestReporterFrontendDispatcher>(const_cast<FrontendRouter&>(m_globalObject.inspectorController().frontendRouter()));
|
||||
}
|
||||
|
||||
void InspectorTestReporterAgent::willDestroyFrontendAndBackend(DisconnectReason)
|
||||
{
|
||||
disable();
|
||||
m_frontendDispatcher = nullptr;
|
||||
}
|
||||
|
||||
Protocol::ErrorStringOr<void> InspectorTestReporterAgent::enable()
|
||||
{
|
||||
if (m_enabled)
|
||||
return {};
|
||||
|
||||
m_enabled = true;
|
||||
Bun__TestReporterAgentEnable(this);
|
||||
return {};
|
||||
}
|
||||
|
||||
Protocol::ErrorStringOr<void> InspectorTestReporterAgent::disable()
|
||||
{
|
||||
if (!m_enabled)
|
||||
return {};
|
||||
|
||||
m_enabled = false;
|
||||
Bun__TestReporterAgentDisable(this);
|
||||
return {};
|
||||
}
|
||||
|
||||
void InspectorTestReporterAgent::reportTestFound(JSC::CallFrame* callFrame, int testId, const String& name)
|
||||
{
|
||||
if (!m_enabled)
|
||||
return;
|
||||
|
||||
JSC::LineColumn lineColumn;
|
||||
JSC::SourceID sourceID = 0;
|
||||
String sourceURL;
|
||||
|
||||
ZigStackFrame remappedFrame = {};
|
||||
|
||||
auto* globalObject = &m_globalObject;
|
||||
auto& vm = globalObject->vm();
|
||||
|
||||
JSC::StackVisitor::visit(callFrame, vm, [&](JSC::StackVisitor& visitor) -> WTF::IterationStatus {
|
||||
if (Zig::isImplementationVisibilityPrivate(visitor))
|
||||
return WTF::IterationStatus::Continue;
|
||||
|
||||
if (visitor->hasLineAndColumnInfo()) {
|
||||
lineColumn = visitor->computeLineAndColumn();
|
||||
|
||||
String sourceURLForFrame = visitor->sourceURL();
|
||||
|
||||
// Sometimes, the sourceURL is empty.
|
||||
// For example, pages in Next.js.
|
||||
if (sourceURLForFrame.isEmpty()) {
|
||||
|
||||
// hasLineAndColumnInfo() checks codeBlock(), so this is safe to access here.
|
||||
const auto& source = visitor->codeBlock()->source();
|
||||
|
||||
// source.isNull() is true when the SourceProvider is a null pointer.
|
||||
if (!source.isNull()) {
|
||||
auto* provider = source.provider();
|
||||
// I'm not 100% sure we should show sourceURLDirective here.
|
||||
if (!provider->sourceURLDirective().isEmpty()) {
|
||||
sourceURLForFrame = provider->sourceURLDirective();
|
||||
} else if (!provider->sourceURL().isEmpty()) {
|
||||
sourceURLForFrame = provider->sourceURL();
|
||||
} else {
|
||||
const auto& origin = provider->sourceOrigin();
|
||||
if (!origin.isNull()) {
|
||||
sourceURLForFrame = origin.string();
|
||||
}
|
||||
}
|
||||
|
||||
sourceID = provider->asID();
|
||||
}
|
||||
}
|
||||
|
||||
sourceURL = sourceURLForFrame;
|
||||
|
||||
return WTF::IterationStatus::Done;
|
||||
}
|
||||
|
||||
return WTF::IterationStatus::Continue;
|
||||
});
|
||||
|
||||
if (!sourceURL.isEmpty() and lineColumn.line > 0) {
|
||||
OrdinalNumber originalLine = OrdinalNumber::fromOneBasedInt(lineColumn.line);
|
||||
OrdinalNumber originalColumn = OrdinalNumber::fromOneBasedInt(lineColumn.column);
|
||||
|
||||
remappedFrame.position.line_zero_based = originalLine.zeroBasedInt();
|
||||
remappedFrame.position.column_zero_based = originalColumn.zeroBasedInt();
|
||||
remappedFrame.source_url = Bun::toStringRef(sourceURL);
|
||||
|
||||
Bun__remapStackFramePositions(globalObject, &remappedFrame, 1);
|
||||
|
||||
sourceURL = remappedFrame.source_url.toWTFString();
|
||||
lineColumn.line = OrdinalNumber::fromZeroBasedInt(remappedFrame.position.line_zero_based).oneBasedInt();
|
||||
lineColumn.column = OrdinalNumber::fromZeroBasedInt(remappedFrame.position.column_zero_based).oneBasedInt();
|
||||
}
|
||||
|
||||
m_frontendDispatcher->found(
|
||||
testId,
|
||||
sourceID > 0 ? String::number(sourceID) : String(),
|
||||
sourceURL,
|
||||
lineColumn.line,
|
||||
name);
|
||||
}
|
||||
|
||||
void InspectorTestReporterAgent::reportTestStart(int testId)
|
||||
{
|
||||
if (!m_enabled || !m_frontendDispatcher)
|
||||
return;
|
||||
|
||||
m_frontendDispatcher->start(testId);
|
||||
}
|
||||
|
||||
void InspectorTestReporterAgent::reportTestEnd(int testId, Protocol::TestReporter::TestStatus status, double elapsed)
|
||||
{
|
||||
if (!m_enabled || !m_frontendDispatcher)
|
||||
return;
|
||||
|
||||
m_frontendDispatcher->end(testId, status, elapsed);
|
||||
}
|
||||
|
||||
} // namespace Inspector
|
||||
46
src/bun.js/bindings/InspectorTestReporterAgent.h
Normal file
46
src/bun.js/bindings/InspectorTestReporterAgent.h
Normal file
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include "root.h"
|
||||
#include <JavaScriptCore/AlternateDispatchableAgent.h>
|
||||
#include <JavaScriptCore/InspectorAgentBase.h>
|
||||
#include <JavaScriptCore/InspectorBackendDispatchers.h>
|
||||
#include <JavaScriptCore/InspectorFrontendDispatchers.h>
|
||||
#include <JavaScriptCore/JSGlobalObject.h>
|
||||
#include <wtf/Forward.h>
|
||||
#include <wtf/Noncopyable.h>
|
||||
|
||||
namespace Inspector {
|
||||
|
||||
class FrontendRouter;
|
||||
class BackendDispatcher;
|
||||
class TestReporterFrontendDispatcher;
|
||||
enum class DisconnectReason;
|
||||
|
||||
class InspectorTestReporterAgent final : public InspectorAgentBase, public Inspector::TestReporterBackendDispatcherHandler {
|
||||
WTF_MAKE_NONCOPYABLE(InspectorTestReporterAgent);
|
||||
|
||||
public:
|
||||
InspectorTestReporterAgent(JSC::JSGlobalObject&);
|
||||
virtual ~InspectorTestReporterAgent();
|
||||
|
||||
// InspectorAgentBase
|
||||
virtual void didCreateFrontendAndBackend(FrontendRouter*, BackendDispatcher*) final;
|
||||
virtual void willDestroyFrontendAndBackend(DisconnectReason) final;
|
||||
|
||||
// TestReporterBackendDispatcherHandler
|
||||
virtual Protocol::ErrorStringOr<void> enable() final;
|
||||
virtual Protocol::ErrorStringOr<void> disable() final;
|
||||
|
||||
// Public API for reporting test events
|
||||
void reportTestFound(JSC::CallFrame*, int testId, const String& name);
|
||||
void reportTestStart(int testId);
|
||||
void reportTestEnd(int testId, Protocol::TestReporter::TestStatus status, double elapsed);
|
||||
|
||||
private:
|
||||
JSC::JSGlobalObject& m_globalObject;
|
||||
std::unique_ptr<TestReporterFrontendDispatcher> m_frontendDispatcher;
|
||||
Ref<TestReporterBackendDispatcher> m_backendDispatcher;
|
||||
bool m_enabled { false };
|
||||
};
|
||||
|
||||
} // namespace Inspector
|
||||
@@ -455,8 +455,6 @@ extern "C" void Bun__onFulfillAsyncModule(
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" bool isBunTest;
|
||||
|
||||
JSValue fetchCommonJSModule(
|
||||
Zig::GlobalObject* globalObject,
|
||||
JSCommonJSModule* target,
|
||||
|
||||
@@ -47,6 +47,8 @@ struct OnLoadResult {
|
||||
bool wasMock;
|
||||
};
|
||||
|
||||
extern "C" bool isBunTest;
|
||||
|
||||
class PendingVirtualModuleResult : public JSC::JSInternalFieldObjectImpl<3> {
|
||||
public:
|
||||
using Base = JSC::JSInternalFieldObjectImpl<3>;
|
||||
|
||||
@@ -1418,47 +1418,167 @@ pub const VirtualMachine = struct {
|
||||
|
||||
pub var has_created_debugger: bool = false;
|
||||
|
||||
pub const TestReporterAgent = struct {
|
||||
handle: ?*Handle = null,
|
||||
const debug = Output.scoped(.TestReporterAgent, false);
|
||||
pub const TestStatus = enum(u8) {
|
||||
pass,
|
||||
fail,
|
||||
timeout,
|
||||
skip,
|
||||
todo,
|
||||
};
|
||||
pub const Handle = opaque {
|
||||
extern "c" fn Bun__TestReporterAgentReportTestFound(agent: *Handle, callFrame: *JSC.CallFrame, testId: c_int, name: *String) void;
|
||||
extern "c" fn Bun__TestReporterAgentReportTestStart(agent: *Handle, testId: c_int) void;
|
||||
extern "c" fn Bun__TestReporterAgentReportTestEnd(agent: *Handle, testId: c_int, bunTestStatus: TestStatus, elapsed: f64) void;
|
||||
|
||||
pub fn reportTestFound(this: *Handle, callFrame: *JSC.CallFrame, testId: i32, name: *String) void {
|
||||
Bun__TestReporterAgentReportTestFound(this, callFrame, testId, name);
|
||||
}
|
||||
|
||||
pub fn reportTestStart(this: *Handle, testId: c_int) void {
|
||||
Bun__TestReporterAgentReportTestStart(this, testId);
|
||||
}
|
||||
|
||||
pub fn reportTestEnd(this: *Handle, testId: c_int, bunTestStatus: TestStatus, elapsed: f64) void {
|
||||
Bun__TestReporterAgentReportTestEnd(this, testId, bunTestStatus, elapsed);
|
||||
}
|
||||
};
|
||||
pub export fn Bun__TestReporterAgentEnable(agent: *Handle) void {
|
||||
if (JSC.VirtualMachine.get().debugger) |*debugger| {
|
||||
debug("enable", .{});
|
||||
debugger.test_reporter_agent.handle = agent;
|
||||
}
|
||||
}
|
||||
pub export fn Bun__TestReporterAgentDisable(agent: *Handle) void {
|
||||
_ = agent; // autofix
|
||||
if (JSC.VirtualMachine.get().debugger) |*debugger| {
|
||||
debug("disable", .{});
|
||||
debugger.test_reporter_agent.handle = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Caller must ensure that it is enabled first.
|
||||
///
|
||||
/// Since we may have to call .deinit on the name string.
|
||||
pub fn reportTestFound(this: TestReporterAgent, callFrame: *JSC.CallFrame, test_id: i32, name: *bun.String) void {
|
||||
debug("reportTestFound", .{});
|
||||
|
||||
this.handle.?.reportTestFound(callFrame, test_id, name);
|
||||
}
|
||||
|
||||
/// Caller must ensure that it is enabled first.
|
||||
pub fn reportTestStart(this: TestReporterAgent, test_id: i32) void {
|
||||
debug("reportTestStart", .{});
|
||||
this.handle.?.reportTestStart(test_id);
|
||||
}
|
||||
|
||||
/// Caller must ensure that it is enabled first.
|
||||
pub fn reportTestEnd(this: TestReporterAgent, test_id: i32, bunTestStatus: TestStatus, elapsed: f64) void {
|
||||
debug("reportTestEnd", .{});
|
||||
this.handle.?.reportTestEnd(test_id, bunTestStatus, elapsed);
|
||||
}
|
||||
|
||||
pub fn isEnabled(this: TestReporterAgent) bool {
|
||||
return this.handle != null;
|
||||
}
|
||||
};
|
||||
|
||||
pub const LifecycleAgent = struct {
|
||||
handle: ?*Handle = null,
|
||||
const debug = Output.scoped(.LifecycleAgent, false);
|
||||
|
||||
pub const Handle = opaque {
|
||||
extern "c" fn Bun__LifecycleAgentReportReload(agent: *Handle) void;
|
||||
extern "c" fn Bun__LifecycleAgentReportError(agent: *Handle, exception: *JSC.ZigException) void;
|
||||
extern "c" fn Bun__LifecycleAgentPreventExit(agent: *Handle) void;
|
||||
extern "c" fn Bun__LifecycleAgentStopPreventingExit(agent: *Handle) void;
|
||||
|
||||
pub fn preventExit(this: *Handle) void {
|
||||
Bun__LifecycleAgentPreventExit(this);
|
||||
}
|
||||
|
||||
pub fn stopPreventingExit(this: *Handle) void {
|
||||
Bun__LifecycleAgentStopPreventingExit(this);
|
||||
}
|
||||
|
||||
pub fn reportReload(this: *Handle) void {
|
||||
debug("reportReload", .{});
|
||||
Bun__LifecycleAgentReportReload(this);
|
||||
}
|
||||
|
||||
pub fn reportError(this: *Handle, exception: *JSC.ZigException) void {
|
||||
debug("reportError", .{});
|
||||
Bun__LifecycleAgentReportError(this, exception);
|
||||
}
|
||||
};
|
||||
|
||||
pub export fn Bun__LifecycleAgentEnable(agent: *Handle) void {
|
||||
if (JSC.VirtualMachine.get().debugger) |*debugger| {
|
||||
debug("enable", .{});
|
||||
debugger.lifecycle_reporter_agent.handle = agent;
|
||||
}
|
||||
}
|
||||
|
||||
pub export fn Bun__LifecycleAgentDisable(agent: *Handle) void {
|
||||
_ = agent; // autofix
|
||||
if (JSC.VirtualMachine.get().debugger) |*debugger| {
|
||||
debug("disable", .{});
|
||||
debugger.lifecycle_reporter_agent.handle = null;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reportReload(this: *LifecycleAgent) void {
|
||||
if (this.handle) |handle| {
|
||||
handle.reportReload();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reportError(this: *LifecycleAgent, exception: *JSC.ZigException) void {
|
||||
if (this.handle) |handle| {
|
||||
handle.reportError(exception);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn isEnabled(this: *const LifecycleAgent) bool {
|
||||
return this.handle != null;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Debugger = struct {
|
||||
path_or_port: ?[]const u8 = null,
|
||||
unix: []const u8 = "",
|
||||
from_environment_variable: []const u8 = "",
|
||||
script_execution_context_id: u32 = 0,
|
||||
next_debugger_id: u64 = 1,
|
||||
poll_ref: Async.KeepAlive = .{},
|
||||
wait_for_connection: bool = false,
|
||||
set_breakpoint_on_first_line: bool = false,
|
||||
|
||||
const debug = Output.scoped(.DEBUGGER, false);
|
||||
test_reporter_agent: TestReporterAgent = .{},
|
||||
lifecycle_reporter_agent: LifecycleAgent = .{},
|
||||
must_block_until_connected: bool = false,
|
||||
|
||||
pub const log = Output.scoped(.debugger, false);
|
||||
|
||||
extern "C" fn Bun__createJSDebugger(*JSC.JSGlobalObject) u32;
|
||||
extern "C" fn Bun__ensureDebugger(u32, bool) void;
|
||||
extern "C" fn Bun__startJSDebuggerThread(*JSC.JSGlobalObject, u32, *bun.String) void;
|
||||
extern "C" fn Bun__startJSDebuggerThread(*JSC.JSGlobalObject, u32, *bun.String, i32) void;
|
||||
var futex_atomic: std.atomic.Value(u32) = undefined;
|
||||
|
||||
pub fn create(this: *VirtualMachine, globalObject: *JSGlobalObject) !void {
|
||||
debug("create", .{});
|
||||
JSC.markBinding(@src());
|
||||
if (has_created_debugger) return;
|
||||
has_created_debugger = true;
|
||||
var debugger = &this.debugger.?;
|
||||
debugger.script_execution_context_id = Bun__createJSDebugger(globalObject);
|
||||
if (!this.has_started_debugger) {
|
||||
this.has_started_debugger = true;
|
||||
futex_atomic = std.atomic.Value(u32).init(0);
|
||||
var thread = try std.Thread.spawn(.{}, startJSDebuggerThread, .{this});
|
||||
thread.detach();
|
||||
pub fn waitForDebuggerIfNecessary(this: *VirtualMachine) void {
|
||||
const debugger = &(this.debugger orelse return);
|
||||
if (!debugger.must_block_until_connected) {
|
||||
return;
|
||||
}
|
||||
this.eventLoop().ensureWaker();
|
||||
defer debugger.must_block_until_connected = false;
|
||||
|
||||
if (debugger.wait_for_connection) {
|
||||
debugger.poll_ref.ref(this);
|
||||
}
|
||||
|
||||
debug("spin", .{});
|
||||
Debugger.log("spin", .{});
|
||||
while (futex_atomic.load(.monotonic) > 0) {
|
||||
std.Thread.Futex.wait(&futex_atomic, 1);
|
||||
}
|
||||
if (comptime Environment.allow_assert)
|
||||
debug("waitForDebugger: {}", .{Output.ElapsedFormatter{
|
||||
if (comptime Environment.enable_logs)
|
||||
Debugger.log("waitForDebugger: {}", .{Output.ElapsedFormatter{
|
||||
.colors = Output.enable_ansi_colors_stderr,
|
||||
.duration_ns = @truncate(@as(u128, @intCast(std.time.nanoTimestamp() - bun.CLI.start_time))),
|
||||
}});
|
||||
@@ -1471,10 +1591,36 @@ pub const VirtualMachine = struct {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create(this: *VirtualMachine, globalObject: *JSGlobalObject) !void {
|
||||
log("create", .{});
|
||||
JSC.markBinding(@src());
|
||||
if (!has_created_debugger) {
|
||||
has_created_debugger = true;
|
||||
std.mem.doNotOptimizeAway(&TestReporterAgent.Bun__TestReporterAgentDisable);
|
||||
std.mem.doNotOptimizeAway(&LifecycleAgent.Bun__LifecycleAgentDisable);
|
||||
std.mem.doNotOptimizeAway(&TestReporterAgent.Bun__TestReporterAgentEnable);
|
||||
std.mem.doNotOptimizeAway(&LifecycleAgent.Bun__LifecycleAgentEnable);
|
||||
var debugger = &this.debugger.?;
|
||||
debugger.script_execution_context_id = Bun__createJSDebugger(globalObject);
|
||||
if (!this.has_started_debugger) {
|
||||
this.has_started_debugger = true;
|
||||
futex_atomic = std.atomic.Value(u32).init(0);
|
||||
var thread = try std.Thread.spawn(.{}, startJSDebuggerThread, .{this});
|
||||
thread.detach();
|
||||
}
|
||||
this.eventLoop().ensureWaker();
|
||||
|
||||
if (debugger.wait_for_connection) {
|
||||
debugger.poll_ref.ref(this);
|
||||
debugger.must_block_until_connected = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn startJSDebuggerThread(other_vm: *VirtualMachine) void {
|
||||
var arena = bun.MimallocArena.init() catch unreachable;
|
||||
Output.Source.configureNamedThread("Debugger");
|
||||
debug("startJSDebuggerThread", .{});
|
||||
log("startJSDebuggerThread", .{});
|
||||
JSC.markBinding(@src());
|
||||
|
||||
var vm = JSC.VirtualMachine.init(.{
|
||||
@@ -1494,9 +1640,10 @@ pub const VirtualMachine = struct {
|
||||
|
||||
pub export fn Debugger__didConnect() void {
|
||||
var this = VirtualMachine.get();
|
||||
bun.assert(this.debugger.?.wait_for_connection);
|
||||
this.debugger.?.wait_for_connection = false;
|
||||
this.debugger.?.poll_ref.unref(this);
|
||||
if (this.debugger.?.wait_for_connection) {
|
||||
this.debugger.?.wait_for_connection = false;
|
||||
this.debugger.?.poll_ref.unref(this);
|
||||
}
|
||||
}
|
||||
|
||||
fn start(other_vm: *VirtualMachine) void {
|
||||
@@ -1505,14 +1652,14 @@ pub const VirtualMachine = struct {
|
||||
var this = VirtualMachine.get();
|
||||
const debugger = other_vm.debugger.?;
|
||||
|
||||
if (debugger.unix.len > 0) {
|
||||
var url = bun.String.createUTF8(debugger.unix);
|
||||
Bun__startJSDebuggerThread(this.global, debugger.script_execution_context_id, &url);
|
||||
if (debugger.from_environment_variable.len > 0) {
|
||||
var url = bun.String.createUTF8(debugger.from_environment_variable);
|
||||
Bun__startJSDebuggerThread(this.global, debugger.script_execution_context_id, &url, 1);
|
||||
}
|
||||
|
||||
if (debugger.path_or_port) |path_or_port| {
|
||||
var url = bun.String.createUTF8(path_or_port);
|
||||
Bun__startJSDebuggerThread(this.global, debugger.script_execution_context_id, &url);
|
||||
Bun__startJSDebuggerThread(this.global, debugger.script_execution_context_id, &url, 0);
|
||||
}
|
||||
|
||||
this.global.handleRejectedPromises();
|
||||
@@ -1523,7 +1670,7 @@ pub const VirtualMachine = struct {
|
||||
Output.flush();
|
||||
}
|
||||
|
||||
debug("wake", .{});
|
||||
log("wake", .{});
|
||||
futex_atomic.store(0, .monotonic);
|
||||
std.Thread.Futex.wake(&futex_atomic, 1);
|
||||
|
||||
@@ -1842,7 +1989,7 @@ pub const VirtualMachine = struct {
|
||||
if (unix.len > 0) {
|
||||
this.debugger = Debugger{
|
||||
.path_or_port = null,
|
||||
.unix = unix,
|
||||
.from_environment_variable = unix,
|
||||
.wait_for_connection = wait_for_connection,
|
||||
.set_breakpoint_on_first_line = set_breakpoint_on_first_line,
|
||||
};
|
||||
@@ -1851,7 +1998,7 @@ pub const VirtualMachine = struct {
|
||||
.enable => {
|
||||
this.debugger = Debugger{
|
||||
.path_or_port = debugger.enable.path_or_port,
|
||||
.unix = unix,
|
||||
.from_environment_variable = unix,
|
||||
.wait_for_connection = wait_for_connection or debugger.enable.wait_for_connection,
|
||||
.set_breakpoint_on_first_line = set_breakpoint_on_first_line or debugger.enable.set_breakpoint_on_first_line,
|
||||
};
|
||||
@@ -2796,11 +2943,23 @@ pub const VirtualMachine = struct {
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn ensureDebugger(this: *VirtualMachine, block_until_connected: bool) !void {
|
||||
if (this.debugger != null) {
|
||||
try Debugger.create(this, this.global);
|
||||
|
||||
if (block_until_connected) {
|
||||
Debugger.waitForDebuggerIfNecessary(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reloadEntryPoint(this: *VirtualMachine, entry_path: []const u8) !*JSInternalPromise {
|
||||
this.has_loaded = false;
|
||||
this.main = entry_path;
|
||||
this.main_hash = GenericWatcher.getHash(entry_path);
|
||||
|
||||
try this.ensureDebugger(true);
|
||||
|
||||
try this.entry_point.generate(
|
||||
this.allocator,
|
||||
this.bun_watcher != .none,
|
||||
@@ -2809,10 +2968,6 @@ pub const VirtualMachine = struct {
|
||||
);
|
||||
this.eventLoop().ensureWaker();
|
||||
|
||||
if (this.debugger != null) {
|
||||
try Debugger.create(this, this.global);
|
||||
}
|
||||
|
||||
if (!this.bundler.options.disable_transpilation) {
|
||||
if (try this.loadPreloads()) |promise| {
|
||||
JSC.JSValue.fromCell(promise).ensureStillAlive();
|
||||
@@ -2841,9 +2996,7 @@ pub const VirtualMachine = struct {
|
||||
|
||||
this.eventLoop().ensureWaker();
|
||||
|
||||
if (this.debugger != null) {
|
||||
try Debugger.create(this, this.global);
|
||||
}
|
||||
try this.ensureDebugger(true);
|
||||
|
||||
if (!this.bundler.options.disable_transpilation) {
|
||||
if (try this.loadPreloads()) |promise| {
|
||||
@@ -3504,10 +3657,15 @@ pub const VirtualMachine = struct {
|
||||
this.had_errors = true;
|
||||
defer this.had_errors = prev_had_errors;
|
||||
|
||||
if (allow_side_effects and Output.is_github_action) {
|
||||
defer printGithubAnnotation(exception);
|
||||
if (allow_side_effects) {
|
||||
if (this.debugger) |*debugger| {
|
||||
debugger.lifecycle_reporter_agent.reportError(exception);
|
||||
}
|
||||
}
|
||||
|
||||
defer if (allow_side_effects and Output.is_github_action)
|
||||
printGithubAnnotation(exception);
|
||||
|
||||
// This is a longer number than necessary because we don't handle this case very well
|
||||
// At the very least, we shouldn't dump 100 KB of minified code into your terminal.
|
||||
const max_line_length_with_divot = 512;
|
||||
|
||||
@@ -57,6 +57,7 @@ pub const Tag = enum(u3) {
|
||||
todo,
|
||||
};
|
||||
const debug = Output.scoped(.jest, false);
|
||||
var max_test_id_for_debugger: u32 = 0;
|
||||
pub const TestRunner = struct {
|
||||
tests: TestRunner.Test.List = .{},
|
||||
log: *logger.Log,
|
||||
@@ -586,7 +587,7 @@ pub const TestScope = struct {
|
||||
func_arg: []JSValue,
|
||||
func_has_callback: bool = false,
|
||||
|
||||
id: TestRunner.Test.ID = 0,
|
||||
test_id_for_debugger: TestRunner.Test.ID = 0,
|
||||
promise: ?*JSInternalPromise = null,
|
||||
ran: bool = false,
|
||||
task: ?*TestRunnerTask = null,
|
||||
@@ -725,6 +726,14 @@ pub const TestScope = struct {
|
||||
task.test_id,
|
||||
);
|
||||
|
||||
if (task.test_id_for_debugger > 0) {
|
||||
if (vm.debugger) |*debugger| {
|
||||
if (debugger.test_reporter_agent.isEnabled()) {
|
||||
debugger.test_reporter_agent.reportTestStart(@intCast(task.test_id_for_debugger));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.func_has_callback) {
|
||||
const callback_func = JSC.NewFunctionWithData(
|
||||
vm.global,
|
||||
@@ -1177,6 +1186,7 @@ pub const DescribeScope = struct {
|
||||
.describe = this,
|
||||
.globalThis = globalObject,
|
||||
.source_file_path = source.path.text,
|
||||
.test_id_for_debugger = 0,
|
||||
};
|
||||
runner.ref.ref(globalObject.bunVM());
|
||||
|
||||
@@ -1185,6 +1195,8 @@ pub const DescribeScope = struct {
|
||||
}
|
||||
}
|
||||
|
||||
const maybe_report_debugger = max_test_id_for_debugger > 0;
|
||||
|
||||
while (i < end) : (i += 1) {
|
||||
var runner = allocator.create(TestRunnerTask) catch unreachable;
|
||||
runner.* = .{
|
||||
@@ -1192,6 +1204,7 @@ pub const DescribeScope = struct {
|
||||
.describe = this,
|
||||
.globalThis = globalObject,
|
||||
.source_file_path = source.path.text,
|
||||
.test_id_for_debugger = if (maybe_report_debugger) tests[i].test_id_for_debugger else 0,
|
||||
};
|
||||
runner.ref.ref(globalObject.bunVM());
|
||||
|
||||
@@ -1272,6 +1285,7 @@ pub const WrappedDescribeScope = struct {
|
||||
|
||||
pub const TestRunnerTask = struct {
|
||||
test_id: TestRunner.Test.ID,
|
||||
test_id_for_debugger: TestRunner.Test.ID,
|
||||
describe: *DescribeScope,
|
||||
globalThis: *JSGlobalObject,
|
||||
source_file_path: string = "",
|
||||
@@ -1356,7 +1370,6 @@ pub const TestRunnerTask = struct {
|
||||
jsc_vm.last_reported_error_for_dedupe = .zero;
|
||||
|
||||
const test_id = this.test_id;
|
||||
|
||||
if (test_id == std.math.maxInt(TestRunner.Test.ID)) {
|
||||
describe.onTestComplete(globalThis, test_id, true);
|
||||
Jest.runner.?.runNextTest();
|
||||
@@ -1366,15 +1379,17 @@ pub const TestRunnerTask = struct {
|
||||
|
||||
var test_: TestScope = this.describe.tests.items[test_id];
|
||||
describe.current_test_id = test_id;
|
||||
const test_id_for_debugger = test_.test_id_for_debugger;
|
||||
this.test_id_for_debugger = test_id_for_debugger;
|
||||
|
||||
if (test_.func == .zero or !describe.shouldEvaluateScope() or (test_.tag != .only and Jest.runner.?.only)) {
|
||||
const tag = if (!describe.shouldEvaluateScope()) describe.tag else test_.tag;
|
||||
switch (tag) {
|
||||
.todo => {
|
||||
this.processTestResult(globalThis, .{ .todo = {} }, test_, test_id, describe);
|
||||
this.processTestResult(globalThis, .{ .todo = {} }, test_, test_id, test_id_for_debugger, describe);
|
||||
},
|
||||
.skip => {
|
||||
this.processTestResult(globalThis, .{ .skip = {} }, test_, test_id, describe);
|
||||
this.processTestResult(globalThis, .{ .skip = {} }, test_, test_id, test_id_for_debugger, describe);
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
@@ -1556,17 +1571,18 @@ pub const TestRunnerTask = struct {
|
||||
}
|
||||
|
||||
checkAssertionsCounter(result);
|
||||
processTestResult(this, this.globalThis, result.*, test_, test_id, describe);
|
||||
processTestResult(this, this.globalThis, result.*, test_, test_id, this.test_id_for_debugger, describe);
|
||||
}
|
||||
|
||||
fn processTestResult(this: *TestRunnerTask, globalThis: *JSGlobalObject, result: Result, test_: TestScope, test_id: u32, describe: *DescribeScope) void {
|
||||
fn processTestResult(this: *TestRunnerTask, globalThis: *JSGlobalObject, result: Result, test_: TestScope, test_id: u32, test_id_for_debugger: u32, describe: *DescribeScope) void {
|
||||
const elapsed = this.started_at.sinceNow();
|
||||
switch (result.forceTODO(test_.tag == .todo)) {
|
||||
.pass => |count| Jest.runner.?.reportPass(
|
||||
test_id,
|
||||
this.source_file_path,
|
||||
test_.label,
|
||||
count,
|
||||
this.started_at.sinceNow(),
|
||||
elapsed,
|
||||
describe,
|
||||
),
|
||||
.fail => |count| Jest.runner.?.reportFailure(
|
||||
@@ -1574,7 +1590,7 @@ pub const TestRunnerTask = struct {
|
||||
this.source_file_path,
|
||||
test_.label,
|
||||
count,
|
||||
this.started_at.sinceNow(),
|
||||
elapsed,
|
||||
describe,
|
||||
),
|
||||
.fail_because_expected_has_assertions => {
|
||||
@@ -1585,7 +1601,7 @@ pub const TestRunnerTask = struct {
|
||||
this.source_file_path,
|
||||
test_.label,
|
||||
0,
|
||||
this.started_at.sinceNow(),
|
||||
elapsed,
|
||||
describe,
|
||||
);
|
||||
},
|
||||
@@ -1600,7 +1616,7 @@ pub const TestRunnerTask = struct {
|
||||
this.source_file_path,
|
||||
test_.label,
|
||||
counter.actual,
|
||||
this.started_at.sinceNow(),
|
||||
elapsed,
|
||||
describe,
|
||||
);
|
||||
},
|
||||
@@ -1613,12 +1629,26 @@ pub const TestRunnerTask = struct {
|
||||
this.source_file_path,
|
||||
test_.label,
|
||||
count,
|
||||
this.started_at.sinceNow(),
|
||||
elapsed,
|
||||
describe,
|
||||
);
|
||||
},
|
||||
.pending => @panic("Unexpected pending test"),
|
||||
}
|
||||
|
||||
if (test_id_for_debugger > 0) {
|
||||
if (globalThis.bunVM().debugger) |*debugger| {
|
||||
if (debugger.test_reporter_agent.isEnabled()) {
|
||||
debugger.test_reporter_agent.reportTestEnd(@intCast(test_id_for_debugger), switch (result) {
|
||||
.pass => .pass,
|
||||
.skip => .skip,
|
||||
.todo => .todo,
|
||||
else => .fail,
|
||||
}, @floatFromInt(elapsed));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe.onTestComplete(globalThis, test_id, result == .skip or (!Jest.runner.?.test_options.run_todo and result == .todo));
|
||||
|
||||
Jest.runner.?.runNextTest();
|
||||
@@ -1803,6 +1833,21 @@ inline fn createScope(
|
||||
.func_arg = function_args,
|
||||
.func_has_callback = has_callback,
|
||||
.timeout_millis = timeout_ms,
|
||||
.test_id_for_debugger = brk: {
|
||||
if (!is_skip) {
|
||||
const vm = globalThis.bunVM();
|
||||
if (vm.debugger) |*debugger| {
|
||||
if (debugger.test_reporter_agent.isEnabled()) {
|
||||
max_test_id_for_debugger += 1;
|
||||
var name = bun.String.init(label);
|
||||
debugger.test_reporter_agent.reportTestFound(callframe, @intCast(max_test_id_for_debugger), &name);
|
||||
break :brk max_test_id_for_debugger;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break :brk 0;
|
||||
},
|
||||
}) catch unreachable;
|
||||
} else {
|
||||
var scope = allocator.create(DescribeScope) catch unreachable;
|
||||
|
||||
@@ -610,11 +610,13 @@ const Scanner = struct {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (jest.Jest.runner.?.test_options.coverage.skip_test_files) {
|
||||
const name_without_extension = slice[0 .. slice.len - ext.len];
|
||||
inline for (test_name_suffixes) |suffix| {
|
||||
if (strings.endsWithComptime(name_without_extension, suffix)) {
|
||||
return false;
|
||||
if (jest.Jest.runner) |runner| {
|
||||
if (runner.test_options.coverage.skip_test_files) {
|
||||
const name_without_extension = slice[0 .. slice.len - ext.len];
|
||||
inline for (test_name_suffixes) |suffix| {
|
||||
if (strings.endsWithComptime(name_without_extension, suffix)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -846,6 +848,11 @@ 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.
|
||||
//
|
||||
try vm.ensureDebugger(false);
|
||||
|
||||
const test_files, const search_count = scan: {
|
||||
if (for (ctx.positionals) |arg| {
|
||||
if (std.fs.path.isAbsolute(arg) or
|
||||
|
||||
@@ -1,4 +1,93 @@
|
||||
import type { ServerWebSocket, Socket, SocketHandler, WebSocketHandler, Server as WebSocketServer } from "bun";
|
||||
const enum FramerState {
|
||||
WaitingForLength,
|
||||
WaitingForMessage,
|
||||
}
|
||||
|
||||
let socketFramerMessageLengthBuffer: Buffer;
|
||||
class SocketFramer {
|
||||
state: FramerState = FramerState.WaitingForLength;
|
||||
pendingLength: number = 0;
|
||||
sizeBuffer: Buffer = Buffer.alloc(4);
|
||||
sizeBufferIndex: number = 0;
|
||||
bufferedData: Buffer = Buffer.alloc(0);
|
||||
|
||||
constructor(private onMessage: (message: string | 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<{ framer: SocketFramer; backend: Backend }>, data: string): void {
|
||||
if (!!$debug) {
|
||||
$debug("local:", data);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if (!!$debug) {
|
||||
$debug("remote:", messagesToDeliver);
|
||||
}
|
||||
|
||||
if (messagesToDeliver.length === 1) {
|
||||
this.onMessage(messagesToDeliver[0]);
|
||||
} else if (messagesToDeliver.length > 1) {
|
||||
this.onMessage(messagesToDeliver);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface Backend {
|
||||
write: (message: string | string[]) => boolean;
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
export default function (
|
||||
executionContextId: string,
|
||||
@@ -7,9 +96,10 @@ export default function (
|
||||
executionContextId: string,
|
||||
refEventLoop: boolean,
|
||||
receive: (...messages: string[]) => void,
|
||||
) => unknown,
|
||||
send: (message: string) => void,
|
||||
) => Backend,
|
||||
send: (message: string | string[]) => void,
|
||||
close: () => void,
|
||||
isAutomatic: boolean,
|
||||
): void {
|
||||
let debug: Debugger | undefined;
|
||||
try {
|
||||
@@ -18,18 +108,31 @@ export default function (
|
||||
exit("Failed to start inspector:\n", error);
|
||||
}
|
||||
|
||||
const { protocol, href, host, pathname } = debug.url;
|
||||
if (!protocol.includes("unix")) {
|
||||
Bun.write(Bun.stderr, dim("--------------------- Bun Inspector ---------------------") + reset() + "\n");
|
||||
Bun.write(Bun.stderr, `Listening:\n ${dim(href)}\n`);
|
||||
if (protocol.includes("ws")) {
|
||||
Bun.write(Bun.stderr, `Inspect in browser:\n ${link(`https://debug.bun.sh/#${host}${pathname}`)}\n`);
|
||||
// If the user types --inspect, we print the URL to the console.
|
||||
// If the user is using an editor extension, don't print anything.
|
||||
if (!isAutomatic) {
|
||||
if (debug.url) {
|
||||
const { protocol, href, host, pathname } = debug.url;
|
||||
if (!protocol.includes("unix")) {
|
||||
Bun.write(Bun.stderr, dim("--------------------- Bun Inspector ---------------------") + reset() + "\n");
|
||||
Bun.write(Bun.stderr, `Listening:\n ${dim(href)}\n`);
|
||||
if (protocol.includes("ws")) {
|
||||
Bun.write(Bun.stderr, `Inspect in browser:\n ${link(`https://debug.bun.sh/#${host}${pathname}`)}\n`);
|
||||
}
|
||||
Bun.write(Bun.stderr, dim("--------------------- Bun Inspector ---------------------") + reset() + "\n");
|
||||
}
|
||||
} else {
|
||||
Bun.write(Bun.stderr, dim("--------------------- Bun Inspector ---------------------") + reset() + "\n");
|
||||
Bun.write(Bun.stderr, `Listening on ${dim(url)}\n`);
|
||||
Bun.write(Bun.stderr, dim("--------------------- Bun Inspector ---------------------") + reset() + "\n");
|
||||
}
|
||||
Bun.write(Bun.stderr, dim("--------------------- Bun Inspector ---------------------") + reset() + "\n");
|
||||
}
|
||||
|
||||
const notifyUrl = process.env["BUN_INSPECT_NOTIFY"] || "";
|
||||
if (notifyUrl) {
|
||||
// Only send this once.
|
||||
process.env["BUN_INSPECT_NOTIFY"] = "";
|
||||
|
||||
if (notifyUrl.startsWith("unix://")) {
|
||||
const path = require("node:path");
|
||||
notify({
|
||||
@@ -46,9 +149,17 @@ export default function (
|
||||
}
|
||||
}
|
||||
|
||||
function unescapeUnixSocketUrl(href) {
|
||||
if (href.startsWith("unix://%2F")) {
|
||||
return decodeURIComponent(href.substring("unix://".length));
|
||||
}
|
||||
|
||||
return href;
|
||||
}
|
||||
|
||||
class Debugger {
|
||||
#url: URL;
|
||||
#createBackend: (refEventLoop: boolean, receive: (...messages: string[]) => void) => Writer;
|
||||
#url?: URL;
|
||||
#createBackend: (refEventLoop: boolean, receive: (...messages: string[]) => void) => Backend;
|
||||
|
||||
constructor(
|
||||
executionContextId: string,
|
||||
@@ -57,30 +168,65 @@ class Debugger {
|
||||
executionContextId: string,
|
||||
refEventLoop: boolean,
|
||||
receive: (...messages: string[]) => void,
|
||||
) => unknown,
|
||||
send: (message: string) => void,
|
||||
) => Backend,
|
||||
send: (message: string | string[]) => void,
|
||||
close: () => void,
|
||||
) {
|
||||
this.#url = parseUrl(url);
|
||||
this.#createBackend = (refEventLoop, receive) => {
|
||||
const backend = createBackend(executionContextId, refEventLoop, receive);
|
||||
return {
|
||||
write: message => {
|
||||
send.$call(backend, message);
|
||||
return true;
|
||||
},
|
||||
close: () => close.$call(backend),
|
||||
try {
|
||||
this.#createBackend = (refEventLoop, receive) => {
|
||||
const backend = createBackend(executionContextId, refEventLoop, receive);
|
||||
return {
|
||||
write: (message: string | string[]) => {
|
||||
send.$call(backend, message);
|
||||
return true;
|
||||
},
|
||||
close: () => close.$call(backend),
|
||||
};
|
||||
};
|
||||
};
|
||||
this.#listen();
|
||||
|
||||
if (url.startsWith("unix://")) {
|
||||
this.#connectOverSocket({
|
||||
unix: unescapeUnixSocketUrl(url),
|
||||
});
|
||||
return;
|
||||
} else if (url.startsWith("fd://")) {
|
||||
this.#connectOverSocket({
|
||||
fd: Number(url.substring("fd://".length)),
|
||||
});
|
||||
return;
|
||||
} else if (url.startsWith("fd:")) {
|
||||
this.#connectOverSocket({
|
||||
fd: Number(url.substring("fd:".length)),
|
||||
});
|
||||
return;
|
||||
} else if (url.startsWith("unix:")) {
|
||||
this.#connectOverSocket({
|
||||
unix: url.substring("unix:".length),
|
||||
});
|
||||
return;
|
||||
} else if (url.startsWith("tcp://")) {
|
||||
const { hostname, port } = new URL(url);
|
||||
this.#connectOverSocket({
|
||||
hostname,
|
||||
port: port && port !== "0" ? Number(port) : undefined,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.#url = parseUrl(url);
|
||||
this.#listen();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
get url(): URL {
|
||||
get url(): URL | undefined {
|
||||
return this.#url;
|
||||
}
|
||||
|
||||
#listen(): void {
|
||||
const { protocol, hostname, port, pathname } = this.#url;
|
||||
const { protocol, hostname, port, pathname } = this.#url!;
|
||||
|
||||
if (protocol === "ws:" || protocol === "wss:" || protocol === "ws+tcp:") {
|
||||
const server = Bun.serve({
|
||||
@@ -89,8 +235,8 @@ class Debugger {
|
||||
fetch: this.#fetch.bind(this),
|
||||
websocket: this.#websocket,
|
||||
});
|
||||
this.#url.hostname = server.hostname;
|
||||
this.#url.port = `${server.port}`;
|
||||
this.#url!.hostname = server.hostname;
|
||||
this.#url!.port = `${server.port}`;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -106,6 +252,48 @@ class Debugger {
|
||||
throw new TypeError(`Unsupported protocol: '${protocol}' (expected 'ws:' or 'ws+unix:')`);
|
||||
}
|
||||
|
||||
#connectOverSocket(networkOptions) {
|
||||
return Bun.connect<{ framer: SocketFramer; backend: Backend }>({
|
||||
...networkOptions,
|
||||
socket: {
|
||||
open: socket => {
|
||||
let backend: Backend;
|
||||
let framer: SocketFramer;
|
||||
const callback = (...messages: string[]) => {
|
||||
for (const message of messages) {
|
||||
framer.send(socket, message);
|
||||
}
|
||||
};
|
||||
|
||||
framer = new SocketFramer((message: string | string[]) => {
|
||||
backend.write(message);
|
||||
});
|
||||
backend = this.#createBackend(false, callback);
|
||||
socket.data = {
|
||||
framer,
|
||||
backend,
|
||||
};
|
||||
socket.ref();
|
||||
},
|
||||
data: (socket, bytes) => {
|
||||
if (!socket.data) {
|
||||
socket.terminate();
|
||||
return;
|
||||
}
|
||||
socket.data.framer.onData(socket, bytes);
|
||||
},
|
||||
drain: socket => {},
|
||||
close: socket => {
|
||||
if (socket.data) {
|
||||
const { backend, framer } = socket.data;
|
||||
backend.close();
|
||||
framer.reset();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
get #websocket(): WebSocketHandler<Connection> {
|
||||
return {
|
||||
idleTimeout: 0,
|
||||
@@ -141,7 +329,7 @@ class Debugger {
|
||||
// TODO?
|
||||
}
|
||||
|
||||
if (!this.#url.protocol.includes("unix") && this.#url.pathname !== pathname) {
|
||||
if (!this.#url!.protocol.includes("unix") && this.#url!.pathname !== pathname) {
|
||||
return new Response(null, {
|
||||
status: 404, // Not Found
|
||||
});
|
||||
@@ -161,17 +349,6 @@ class Debugger {
|
||||
}
|
||||
}
|
||||
|
||||
get #socket(): SocketHandler<Connection> {
|
||||
return {
|
||||
open: socket => this.#open(socket, socketWriter(socket)),
|
||||
data: (socket, message) => this.#message(socket, message.toString()),
|
||||
drain: socket => this.#drain(socket),
|
||||
close: socket => this.#close(socket),
|
||||
error: (socket, error) => this.#error(socket, error),
|
||||
connectError: (_, error) => exit("Failed to start inspector:\n", error),
|
||||
};
|
||||
}
|
||||
|
||||
#open(connection: ConnectionOwner, writer: Writer): void {
|
||||
const { data } = connection;
|
||||
const { refEventLoop } = data;
|
||||
@@ -190,6 +367,7 @@ class Debugger {
|
||||
#message(connection: ConnectionOwner, message: string): void {
|
||||
const { data } = connection;
|
||||
const { backend } = data;
|
||||
$debug("remote:", message);
|
||||
backend?.write(message);
|
||||
}
|
||||
|
||||
@@ -231,13 +409,6 @@ function webSocketWriter(ws: ServerWebSocket<unknown>): Writer {
|
||||
};
|
||||
}
|
||||
|
||||
function socketWriter(socket: Socket<unknown>): Writer {
|
||||
return {
|
||||
write: message => !!socket.write(message),
|
||||
close: () => socket.end(),
|
||||
};
|
||||
}
|
||||
|
||||
function bufferedWriter(writer: Writer): Writer {
|
||||
let draining = false;
|
||||
let pendingMessages: string[] = [];
|
||||
@@ -273,7 +444,7 @@ const defaultHostname = "localhost";
|
||||
const defaultPort = 6499;
|
||||
|
||||
function parseUrl(input: string): URL {
|
||||
if (input.startsWith("ws://") || input.startsWith("ws+unix://") || input.startsWith("unix://")) {
|
||||
if (input.startsWith("ws://") || input.startsWith("ws+unix://")) {
|
||||
return new URL(input);
|
||||
}
|
||||
const url = new URL(`ws://${defaultHostname}:${defaultPort}/${randomId()}`);
|
||||
@@ -356,7 +527,7 @@ type ConnectionOwner = {
|
||||
type Connection = {
|
||||
refEventLoop: boolean;
|
||||
client?: Writer;
|
||||
backend?: Writer;
|
||||
backend?: Backend;
|
||||
};
|
||||
|
||||
type Writer = {
|
||||
|
||||
19
test/cli/inspect/__snapshots__/inspect.test.ts.snap
Normal file
19
test/cli/inspect/__snapshots__/inspect.test.ts.snap
Normal file
@@ -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>"
|
||||
`;
|
||||
@@ -1,289 +1,427 @@
|
||||
import { Subprocess, spawn } from "bun";
|
||||
import { afterEach, expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe, randomPort } from "harness";
|
||||
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]+$/);
|
||||
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",
|
||||
});
|
||||
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",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
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
|
||||
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 (url?.protocol.includes("ws")) {
|
||||
if (stderr.includes("Listening:")) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (stderr.includes("Listening:")) {
|
||||
break;
|
||||
|
||||
if (!url) {
|
||||
process.stderr.write(stderr);
|
||||
throw new Error("Unable to find listening URL");
|
||||
}
|
||||
}
|
||||
|
||||
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 { 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();
|
||||
|
||||
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: {
|
||||
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: {
|
||||
type: "number",
|
||||
value: 2,
|
||||
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]);
|
||||
});
|
||||
|
||||
webSocket.close();
|
||||
});
|
||||
}
|
||||
test("bun --inspect=unix:", async () => {
|
||||
const path = Math.random().toString(36).substring(2, 15) + ".sock";
|
||||
await runTest(path, ["--inspect=unix:" + path]);
|
||||
});
|
||||
|
||||
// FIXME: Depends on https://github.com/oven-sh/bun/pull/4649
|
||||
test.todo("bun --inspect=ws+unix:///tmp/inspect.sock");
|
||||
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 });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
inspectee?.kill();
|
||||
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("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();
|
||||
});
|
||||
|
||||
359
test/cli/inspect/junit-reporter.ts
Normal file
359
test/cli/inspect/junit-reporter.ts
Normal file
@@ -0,0 +1,359 @@
|
||||
// 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;
|
||||
}
|
||||
|
||||
export 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[];
|
||||
}
|
||||
|
||||
export class JUnitReporter {
|
||||
private session: InspectorSession;
|
||||
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 (params.status === "fail") {
|
||||
suite.failures++;
|
||||
testCase.failure = {
|
||||
message: "Test failed",
|
||||
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;
|
||||
}
|
||||
|
||||
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()) {
|
||||
xml += ` <testsuite name="${escapeXml(suite.name)}" `;
|
||||
xml += `tests="${suite.tests}" `;
|
||||
xml += `failures="${suite.failures}" `;
|
||||
xml += `errors="${suite.errors}" `;
|
||||
xml += `skipped="${suite.skipped}" `;
|
||||
|
||||
xml += `timestamp="${suite.timestamp}">\n`;
|
||||
|
||||
for (const testCase of suite.testCases) {
|
||||
xml += ` <testcase classname="${escapeXml(testCase.classname)}" `;
|
||||
xml += `name="${escapeXml(testCase.name)}" `;
|
||||
|
||||
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, "'");
|
||||
}
|
||||
|
||||
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 }>({
|
||||
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);
|
||||
},
|
||||
close(socket) {
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return await promise;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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