Compare commits

...

2 Commits

Author SHA1 Message Date
Claude Bot
a68864ca60 fix: Improve TestReporter.error message extraction
- Safely extract actual error messages from exceptions
- Try message property first, then fall back to error toString
- Avoid complex string manipulations that could crash
- Keep implementation minimal and focused on the core issue

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-23 08:29:30 +00:00
Claude Bot
254c8a7e01 feat: Add TestReporter.error event with test_id for better error tracking
This adds a new TestReporter.error event that includes the test_id, making it
possible for external tools (like VS Code extensions) to correctly associate
errors with the specific test that generated them, especially important for
concurrent test execution.

Changes:
- Added TestReporter.error event to protocol definition
- Implemented reportTestError in InspectorTestReporterAgent (C++)
- Added Zig bindings for reportTestError
- Integrated error reporting with test_id in onUncaughtException

This solves the issue where socket errors and other exceptions couldn't be
reliably associated with their originating test in concurrent execution.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-22 16:47:23 +00:00
5 changed files with 121 additions and 0 deletions

View File

@@ -3107,6 +3107,35 @@
"description": "Elapsed time in milliseconds since the test started."
}
]
},
{
"name": "error",
"description": "Reports an error that occurred within the context of a test.",
"parameters": [
{
"name": "id",
"type": "integer",
"description": "Unique identifier of the test where the error occurred.",
"optional": true
},
{
"name": "message",
"type": "string",
"description": "Error message."
},
{
"name": "name",
"type": "string",
"description": "Error name/type.",
"optional": true
},
{
"name": "stack",
"type": "string",
"description": "Error stack trace.",
"optional": true
}
]
}
]
}

View File

@@ -318,6 +318,7 @@ pub const TestReporterAgent = struct {
extern "c" fn Bun__TestReporterAgentReportTestFound(agent: *Handle, callFrame: *jsc.CallFrame, testId: c_int, name: *bun.String, item_type: TestType, parentId: c_int) 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;
extern "c" fn Bun__TestReporterAgentReportTestError(agent: *Handle, testId: c_int, message: ?*bun.String, name: ?*bun.String, stack: ?*bun.String) void;
pub fn reportTestFound(this: *Handle, callFrame: *jsc.CallFrame, testId: i32, name: *bun.String, item_type: TestType, parentId: i32) void {
Bun__TestReporterAgentReportTestFound(this, callFrame, testId, name, item_type, parentId);
@@ -330,6 +331,10 @@ pub const TestReporterAgent = struct {
pub fn reportTestEnd(this: *Handle, testId: c_int, bunTestStatus: TestStatus, elapsed: f64) void {
Bun__TestReporterAgentReportTestEnd(this, testId, bunTestStatus, elapsed);
}
pub fn reportTestError(this: *Handle, testId: c_int, message: ?*bun.String, name: ?*bun.String, stack: ?*bun.String) void {
Bun__TestReporterAgentReportTestError(this, testId, message, name, stack);
}
};
pub export fn Bun__TestReporterAgentEnable(agent: *Handle) void {
if (VirtualMachine.get().debugger) |*debugger| {
@@ -365,6 +370,12 @@ pub const TestReporterAgent = struct {
this.handle.?.reportTestEnd(test_id, bunTestStatus, elapsed);
}
/// Caller must ensure that it is enabled first.
pub fn reportTestError(this: TestReporterAgent, test_id: i32, message: ?*bun.String, name: ?*bun.String, stack: ?*bun.String) void {
debug("reportTestError", .{});
this.handle.?.reportTestError(test_id, message, name, stack);
}
pub fn isEnabled(this: TestReporterAgent) bool {
return this.handle != null;
}

View File

@@ -7,6 +7,7 @@
#include <JavaScriptCore/ScriptCallStack.h>
#include <JavaScriptCore/JSGlobalObjectDebuggable.h>
#include <JavaScriptCore/JSGlobalObjectInspectorController.h>
#include <wtf/JSONValues.h>
#include "ErrorStackTrace.h"
#include "ZigGlobalObject.h"
@@ -93,6 +94,15 @@ void Bun__TestReporterAgentReportTestEnd(Inspector::InspectorTestReporterAgent*
agent->reportTestEnd(testId, status, elapsed);
}
void Bun__TestReporterAgentReportTestError(Inspector::InspectorTestReporterAgent* agent, int testId, BunString* message, BunString* name, BunString* stack)
{
auto messageStr = message ? message->toWTFString(BunString::ZeroCopy) : String();
auto nameStr = name ? name->toWTFString(BunString::ZeroCopy) : String();
auto stackStr = stack ? stack->toWTFString(BunString::ZeroCopy) : String();
agent->reportTestError(testId, messageStr, nameStr, stackStr);
}
}
InspectorTestReporterAgent::InspectorTestReporterAgent(JSC::JSGlobalObject& globalObject)
@@ -227,4 +237,27 @@ void InspectorTestReporterAgent::reportTestEnd(int testId, Protocol::TestReporte
m_frontendDispatcher->end(testId, status, elapsed);
}
void InspectorTestReporterAgent::reportTestError(int testId, const String& message, const String& name, const String& stack)
{
if (!m_enabled || !m_frontendDispatcher)
return;
// The error event isn't part of the official protocol yet, so we emit it as a custom event
// This will be replaced once the protocol is regenerated
auto errorObject = JSON::Object::create();
if (testId > 0)
errorObject->setInteger("id"_s, testId);
errorObject->setString("message"_s, message);
if (!name.isEmpty())
errorObject->setString("name"_s, name);
if (!stack.isEmpty())
errorObject->setString("stack"_s, stack);
auto paramsObject = JSON::Object::create();
paramsObject->setObject("params"_s, WTFMove(errorObject));
paramsObject->setString("method"_s, "TestReporter.error"_s);
m_globalObject.inspectorController().frontendRouter().sendEvent(paramsObject->toJSONString());
}
} // namespace Inspector

View File

@@ -36,6 +36,7 @@ public:
void reportTestFound(JSC::CallFrame*, int testId, const String& name, Protocol::TestReporter::TestType type = Protocol::TestReporter::TestType::Test, int parentId = -1);
void reportTestStart(int testId);
void reportTestEnd(int testId, Protocol::TestReporter::TestStatus status, double elapsed);
void reportTestError(int testId, const String& message, const String& name = String(), const String& stack = String());
private:
JSC::JSGlobalObject& m_globalObject;

View File

@@ -620,6 +620,53 @@ pub const BunTest = struct {
if (handle_status == .hide_error) return; // do not print error, it was already consumed
if (exception == null) return; // the exception should not be visible (eg m_terminationException)
// Report error to TestReporter with test ID
if (jsc.VirtualMachine.get().debugger) |*debugger| {
if (debugger.test_reporter_agent.isEnabled()) {
var test_id: i32 = 0;
// Try to get the test ID from the current execution context
if (this.phase == .execution) {
if (user_data.sequence(this)) |sequence| {
if (sequence.test_entry) |test_entry| {
test_id = test_entry.base.test_id_for_debugger;
}
}
}
// Extract error message safely
const error_value = exception.?;
// Try to get the message property first
var message_str = bun.String.empty;
defer message_str.deref();
if (error_value.getTruthy(globalThis, "message") catch null) |msg_val| {
var msg_zig = jsc.ZigString.init("");
msg_val.toZigString(&msg_zig, globalThis) catch {};
if (msg_zig.len > 0) {
message_str = bun.String.init(msg_zig);
}
}
// If no message property, convert the whole error to string
if (message_str.isEmpty()) {
var error_zig = jsc.ZigString.init("");
error_value.toZigString(&error_zig, globalThis) catch {};
if (error_zig.len > 0) {
message_str = bun.String.init(error_zig);
}
}
debugger.test_reporter_agent.reportTestError(
test_id,
if (message_str.isEmpty()) null else &message_str,
null,
null
);
}
}
if (handle_status == .show_unhandled_error_between_tests or handle_status == .show_unhandled_error_in_describe) {
this.reporter.?.jest.unhandled_errors_between_tests += 1;
bun.Output.prettyErrorln(