diff options
-rw-r--r-- | src/bun.js/api/bun.zig | 6 | ||||
-rw-r--r-- | src/bun.js/bindings/JSFFIFunction.cpp | 33 | ||||
-rw-r--r-- | src/bun.js/bindings/bindings.zig | 41 | ||||
-rw-r--r-- | src/bun.js/javascript.zig | 43 | ||||
-rw-r--r-- | src/bun.js/test/jest.zig | 137 | ||||
-rw-r--r-- | src/cli/test_command.zig | 5 | ||||
-rw-r--r-- | test/bun.js/event-emitter.test.ts | 38 |
7 files changed, 284 insertions, 19 deletions
diff --git a/src/bun.js/api/bun.zig b/src/bun.js/api/bun.zig index 52b457e54..727712897 100644 --- a/src/bun.js/api/bun.zig +++ b/src/bun.js/api/bun.zig @@ -2270,7 +2270,7 @@ pub const Timer = struct { } var this = args.ptr[1].asPtr(CallbackJob); - globalThis.bunVM().runErrorHandler(args.ptr[0], null); + globalThis.bunVM().runErrorHandlerWithDedupe(args.ptr[0], null); this.deinit(); return JSValue.jsUndefined(); } @@ -2313,7 +2313,7 @@ pub const Timer = struct { } if (result.isAnyError(globalThis)) { - vm.runErrorHandler(result, null); + vm.runErrorHandlerWithDedupe(result, null); this.deinit(); return; } @@ -2322,7 +2322,7 @@ pub const Timer = struct { switch (promise.status(globalThis.vm())) { .Rejected => { this.deinit(); - vm.runErrorHandler(promise.result(globalThis.vm()), null); + vm.runErrorHandlerWithDedupe(promise.result(globalThis.vm()), null); }, .Fulfilled => { this.deinit(); diff --git a/src/bun.js/bindings/JSFFIFunction.cpp b/src/bun.js/bindings/JSFFIFunction.cpp index c0fc34fc0..a0cd83ba6 100644 --- a/src/bun.js/bindings/JSFFIFunction.cpp +++ b/src/bun.js/bindings/JSFFIFunction.cpp @@ -70,14 +70,45 @@ extern "C" FFICallbackFunctionWrapper* Bun__createFFICallbackFunction( return wrapper; } -extern "C" Zig::JSFFIFunction* Bun__CreateFFIFunction(Zig::GlobalObject* globalObject, const ZigString* symbolName, unsigned argCount, Zig::FFIFunction functionPointer, bool strong) +extern "C" Zig::JSFFIFunction* Bun__CreateFFIFunctionWithData(Zig::GlobalObject* globalObject, const ZigString* symbolName, unsigned argCount, Zig::FFIFunction functionPointer, bool strong, void* data) { JSC::VM& vm = globalObject->vm(); Zig::JSFFIFunction* function = Zig::JSFFIFunction::create(vm, globalObject, argCount, symbolName != nullptr ? Zig::toStringCopy(*symbolName) : String(), functionPointer, JSC::NoIntrinsic); if (strong) globalObject->trackFFIFunction(function); + function->dataPtr = data; return function; } + +extern "C" JSC::EncodedJSValue Bun__CreateFFIFunctionWithDataValue(Zig::GlobalObject* globalObject, const ZigString* symbolName, unsigned argCount, Zig::FFIFunction functionPointer, bool strong, void* data) +{ + return JSC::JSValue::encode(Bun__CreateFFIFunctionWithData(globalObject, symbolName, argCount, functionPointer, strong, data)); +} + +extern "C" Zig::JSFFIFunction* Bun__CreateFFIFunction(Zig::GlobalObject* globalObject, const ZigString* symbolName, unsigned argCount, Zig::FFIFunction functionPointer, bool strong) +{ + return Bun__CreateFFIFunctionWithData(globalObject, symbolName, argCount, functionPointer, strong, nullptr); +} + +extern "C" void* Bun__FFIFunction_getDataPtr(JSC::EncodedJSValue jsValue) +{ + + Zig::JSFFIFunction* function = jsDynamicCast<Zig::JSFFIFunction*>(JSC::JSValue::decode(jsValue)); + if (!function) + return nullptr; + + return function->dataPtr; +} + +extern "C" void Bun__FFIFunction_setDataPtr(JSC::EncodedJSValue jsValue, void* ptr) +{ + + Zig::JSFFIFunction* function = jsDynamicCast<Zig::JSFFIFunction*>(JSC::JSValue::decode(jsValue)); + if (!function) + return; + + function->dataPtr = ptr; +} extern "C" void Bun__untrackFFIFunction(Zig::GlobalObject* globalObject, JSC::EncodedJSValue function) { globalObject->untrackFFIFunction(JSC::jsCast<JSC::JSFunction*>(JSC::JSValue::decode(function))); diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 76bb1011a..813a222ef 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -4217,6 +4217,14 @@ pub const JSArray = struct { }; const private = struct { + pub extern fn Bun__CreateFFIFunctionWithDataValue( + *JSGlobalObject, + ?*const ZigString, + argCount: u32, + function: *const anyopaque, + strong: bool, + data: *anyopaque, + ) JSValue; pub extern fn Bun__CreateFFIFunction( globalObject: *JSGlobalObject, symbolName: ?*const ZigString, @@ -4237,7 +4245,11 @@ const private = struct { globalObject: *JSGlobalObject, function: JSValue, ) bool; + + pub extern fn Bun__FFIFunction_getDataPtr(JSValue) ?*anyopaque; + pub extern fn Bun__FFIFunction_setDataPtr(JSValue, ?*anyopaque) void; }; + pub fn NewFunctionPtr(globalObject: *JSGlobalObject, symbolName: ?*const ZigString, argCount: u32, functionPointer: anytype, strong: bool) *anyopaque { if (comptime JSC.is_bindgen) unreachable; return private.Bun__CreateFFIFunction(globalObject, symbolName, argCount, @ptrCast(*const anyopaque, functionPointer), strong); @@ -4254,6 +4266,35 @@ pub fn NewFunction( return private.Bun__CreateFFIFunctionValue(globalObject, symbolName, argCount, @ptrCast(*const anyopaque, functionPointer), strong); } +pub fn getFunctionData(function: JSValue) ?*anyopaque { + if (comptime JSC.is_bindgen) unreachable; + return private.Bun__FFIFunction_getDataPtr(function); +} + +pub fn setFunctionData(function: JSValue, value: ?*anyopaque) void { + if (comptime JSC.is_bindgen) unreachable; + return private.Bun__FFIFunction_setDataPtr(function, value); +} + +pub fn NewFunctionWithData( + globalObject: *JSGlobalObject, + symbolName: ?*const ZigString, + argCount: u32, + functionPointer: anytype, + strong: bool, + data: *anyopaque, +) JSValue { + if (comptime JSC.is_bindgen) unreachable; + return private.Bun__CreateFFIFunctionWithDataValue( + globalObject, + symbolName, + argCount, + @ptrCast(*const anyopaque, functionPointer), + strong, + data, + ); +} + pub fn untrackFunction( globalObject: *JSGlobalObject, value: JSValue, diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index bbc10291c..1fd6783be 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -282,6 +282,7 @@ comptime { _ = Bun__readOriginTimer; _ = Bun__onDidAppendPlugin; _ = Bun__readOriginTimerStart; + _ = Bun__reportUnhandledError; } } @@ -291,6 +292,12 @@ pub export fn Bun__queueTask(global: *JSGlobalObject, task: *JSC.CppTask) void { global.bunVM().eventLoop().enqueueTask(Task.init(task)); } +pub export fn Bun__reportUnhandledError(globalObject: *JSGlobalObject, value: JSValue) callconv(.C) JSValue { + var jsc_vm = globalObject.bunVM(); + jsc_vm.onUnhandledError(globalObject, value); + return JSC.JSValue.jsUndefined(); +} + /// This function is called on another thread /// The main difference: we need to allocate the task & wakeup the thread /// We can avoid that if we run it from the main thread. @@ -305,7 +312,8 @@ pub export fn Bun__queueTaskConcurrently(global: *JSGlobalObject, task: *JSC.Cpp pub export fn Bun__handleRejectedPromise(global: *JSGlobalObject, promise: *JSC.JSPromise) void { const result = promise.result(global.vm()); - global.bunVM().runErrorHandler(result, null); + var jsc_vm = global.bunVM(); + jsc_vm.onUnhandledError(global, result); } pub export fn Bun__onDidAppendPlugin(jsc_vm: *VirtualMachine, globalObject: *JSGlobalObject) void { @@ -350,6 +358,7 @@ pub const VirtualMachine = struct { plugin_runner: ?PluginRunner = null, is_main_thread: bool = false, + last_reported_error_for_dedupe: JSValue = .zero, /// Do not access this field directly /// It exists in the VirtualMachine struct so that @@ -411,10 +420,27 @@ pub const VirtualMachine = struct { auto_install_dependencies: bool = false, load_builtins_from_path: []const u8 = "", + onUnhandledRejection: fn (*VirtualMachine, globalObject: *JSC.JSGlobalObject, JSC.JSValue) void = defaultOnUnhandledRejection, + onUnhandledRejectionCtx: ?*anyopaque = null, + unhandled_error_counter: usize = 0, + modules: ModuleLoader.AsyncModule.Queue = .{}, pub threadlocal var is_main_thread_vm: bool = false; + pub fn resetUnhandledRejection(this: *VirtualMachine) void { + this.onUnhandledRejection = defaultOnUnhandledRejection; + } + + pub fn onUnhandledError(this: *JSC.VirtualMachine, globalObject: *JSC.JSGlobalObject, value: JSC.JSValue) void { + this.unhandled_error_counter += 1; + this.onUnhandledRejection(this, globalObject, value); + } + + pub fn defaultOnUnhandledRejection(this: *JSC.VirtualMachine, _: *JSC.JSGlobalObject, value: JSC.JSValue) void { + this.runErrorHandler(value, null); + } + pub inline fn packageManager(this: *VirtualMachine) *PackageManager { return this.bundler.getPackageManager(); } @@ -1173,7 +1199,17 @@ pub const VirtualMachine = struct { } } + pub fn runErrorHandlerWithDedupe(this: *VirtualMachine, result: JSValue, exception_list: ?*ExceptionList) void { + if (this.last_reported_error_for_dedupe == result and !this.last_reported_error_for_dedupe.isEmptyOrUndefinedOrNull()) + return; + + this.runErrorHandler(result, exception_list); + } + pub fn runErrorHandler(this: *VirtualMachine, result: JSValue, exception_list: ?*ExceptionList) void { + if (!result.isEmptyOrUndefinedOrNull()) + this.last_reported_error_for_dedupe = result; + if (result.isException(this.global.vm())) { var exception = @ptrCast(*Exception, result.asVoid()); @@ -1454,8 +1490,9 @@ pub const VirtualMachine = struct { } } - pub fn reportUncaughtExceptio(_: *JSGlobalObject, exception: *JSC.Exception) JSValue { - VirtualMachine.vm.runErrorHandler(exception.value(), null); + pub fn reportUncaughtException(globalObject: *JSGlobalObject, exception: *JSC.Exception) JSValue { + var jsc_vm = globalObject.bunVM(); + jsc_vm.onUnhandledError(globalObject, exception.value()); return JSC.JSValue.jsUndefined(); } diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index 7440a9512..8674e2c22 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const bun = @import("../../global.zig"); const Api = @import("../../api/schema.zig").Api; const RequestContext = @import("../../http.zig").RequestContext; const MimeType = @import("../../http.zig").MimeType; @@ -535,14 +536,36 @@ pub const TestScope = struct { const err = arguments.ptr[0]; globalThis.bunVM().runErrorHandler(err, null); var task: *TestRunnerTask = arguments.ptr[1].asPromisePtr(TestRunnerTask); - task.handleResult(.{ .fail = active_test_expectation_counter.actual }); + task.handleResult(.{ .fail = active_test_expectation_counter.actual }, .promise); return JSValue.jsUndefined(); } pub fn onResolve(_: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSValue { const arguments = callframe.arguments(2); var task: *TestRunnerTask = arguments.ptr[1].asPromisePtr(TestRunnerTask); - task.handleResult(.{ .pass = active_test_expectation_counter.actual }); + task.handleResult(.{ .pass = active_test_expectation_counter.actual }, .promise); + return JSValue.jsUndefined(); + } + + pub fn onDone( + globalThis: *JSC.JSGlobalObject, + callframe: *JSC.CallFrame, + ) callconv(.C) JSValue { + const function = callframe.callee(); + const args = callframe.arguments(1); + + if (JSC.getFunctionData(function)) |data| { + var task = bun.cast(*TestRunnerTask, data); + JSC.setFunctionData(function, null); + if (args.len > 0) { + const err = args.ptr[0]; + globalThis.bunVM().runErrorHandlerWithDedupe(err, null); + task.handleResult(.{ .fail = active_test_expectation_counter.actual }, .callback); + } else { + task.handleResult(.{ .pass = active_test_expectation_counter.actual }, .callback); + } + } + return JSValue.jsUndefined(); } @@ -558,7 +581,24 @@ pub const TestScope = struct { this.callback = null; } JSC.markBinding(@src()); - const initial_value = js.JSObjectCallAsFunctionReturnValue(vm.global, callback, null, 0, null); + + const callback_length = JSValue.fromRef(callback).getLengthOfArray(vm.global); + + var initial_value = JSValue.zero; + if (callback_length > 0) { + const callback_func = JSC.NewFunctionWithData( + vm.global, + ZigString.static("done"), + 0, + TestScope.onDone, + false, + task, + ); + task.done_callback_state = .pending; + initial_value = JSValue.fromRef(callback.?).call(vm.global, &.{callback_func}); + } else { + initial_value = js.JSObjectCallAsFunctionReturnValue(vm.global, callback, null, 0, null); + } if (initial_value.isException(vm.global.vm()) or initial_value.isError() or initial_value.isAggregateError(vm.global)) { vm.runErrorHandler(initial_value, null); @@ -579,6 +619,7 @@ pub const TestScope = struct { return .{ .fail = active_test_expectation_counter.actual }; }, .Pending => { + task.promise_state = .pending; _ = promise.asValue(vm.global).then(vm.global, task, onResolve, onReject); return .{ .pending = {} }; }, @@ -589,6 +630,10 @@ pub const TestScope = struct { } } + if (callback_length > 0) { + return .{ .pending = {} }; + } + this.callback = null; if (active_test_expectation_counter.expected > 0 and active_test_expectation_counter.expected < active_test_expectation_counter.actual) { @@ -960,7 +1005,7 @@ pub const DescribeScope = struct { var active_test_expectation_counter: TestScope.Counter = undefined; -const TestRunnerTask = struct { +pub const TestRunnerTask = struct { test_id: TestRunner.Test.ID, describe: *DescribeScope, globalThis: *JSC.JSGlobalObject, @@ -969,6 +1014,31 @@ const TestRunnerTask = struct { needs_before_each: bool = true, ref: JSC.Ref = JSC.Ref.init(), + done_callback_state: AsyncState = .none, + promise_state: AsyncState = .none, + sync_state: AsyncState = .none, + reported: bool = false, + + pub const AsyncState = enum { + none, + pending, + fulfilled, + }; + + pub fn onUnhandledRejection(jsc_vm: *VirtualMachine, _: *JSC.JSGlobalObject, rejection: JSC.JSValue) void { + if (jsc_vm.last_reported_error_for_dedupe == rejection and rejection != .zero) { + jsc_vm.last_reported_error_for_dedupe = .zero; + } else { + jsc_vm.runErrorHandlerWithDedupe(rejection, null); + } + + if (jsc_vm.onUnhandledRejectionCtx) |ctx| { + var this = bun.cast(*TestRunnerTask, ctx); + jsc_vm.onUnhandledRejectionCtx = null; + this.handleResult(.{ .fail = active_test_expectation_counter.actual }, .unhandledRejection); + } + } + pub fn run(this: *TestRunnerTask) bool { var describe = this.describe; @@ -977,7 +1047,9 @@ const TestRunnerTask = struct { DescribeScope.active = describe; active_test_expectation_counter = .{}; + describe.current_test_id = this.test_id; var globalThis = this.globalThis; + globalThis.bunVM().onUnhandledRejectionCtx = this; var test_: TestScope = this.describe.tests.items[this.test_id]; const label = this.describe.tests.items[this.test_id].label; @@ -995,19 +1067,56 @@ const TestRunnerTask = struct { } } - const result = TestScope.run(&test_, this); + this.sync_state = .pending; - if (result == .pending) { + const result = TestScope.run(&test_, this); + if (result == .pending and this.sync_state == .pending and (this.done_callback_state == .pending or this.promise_state == .pending)) { + this.sync_state = .fulfilled; this.value.set(globalThis, this.describe.value); return true; } - this.handleResult(result); + this.handleResult(result, .sync); return false; } - pub fn handleResult(this: *TestRunnerTask, result: Result) void { + pub fn handleResult(this: *TestRunnerTask, result: Result, comptime from: @Type(.EnumLiteral)) void { + switch (comptime from) { + .promise => { + std.debug.assert(this.promise_state == .pending); + this.promise_state = .fulfilled; + + if (this.done_callback_state == .pending and result == .pass) { + return; + } + }, + .callback => { + std.debug.assert(this.done_callback_state == .pending); + this.done_callback_state = .fulfilled; + + if (this.promise_state == .pending and result == .pass) { + return; + } + }, + .sync => { + std.debug.assert(this.sync_state == .pending); + this.sync_state = .fulfilled; + }, + .unhandledRejection => {}, + else => @compileError("Bad from"), + } + + defer { + if (this.reported and this.promise_state != .pending and this.sync_state != .pending and this.done_callback_state != .pending) + this.deinit(); + } + + if (this.reported) + return; + + this.reported = true; + var globalThis = this.globalThis; var test_ = this.describe.tests.items[this.test_id]; const label = this.describe.tests.items[this.test_id].label; @@ -1015,20 +1124,26 @@ const TestRunnerTask = struct { var describe = this.describe; describe.tests.items[this.test_id] = test_; - switch (result) { .pass => |count| Jest.runner.?.reportPass(test_id, this.source.path.text, label, count, describe), .fail => |count| Jest.runner.?.reportFailure(test_id, this.source.path.text, label, count, describe), .pending => @panic("Unexpected pending test"), } describe.onTestComplete(globalThis, this.test_id); - this.deinit(); + Jest.runner.?.runNextTest(); } fn deinit(this: *TestRunnerTask) void { + var vm = JSC.VirtualMachine.vm; + if (vm.onUnhandledRejectionCtx) |ctx| { + if (ctx == @ptrCast(*anyopaque, this)) { + vm.onUnhandledRejectionCtx = null; + } + } + this.value.deinit(); - this.ref.unref(JSC.VirtualMachine.vm); + this.ref.unref(vm); default_allocator.destroy(this); } }; diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig index 1927e6027..7b5c88947 100644 --- a/src/cli/test_command.zig +++ b/src/cli/test_command.zig @@ -482,10 +482,13 @@ pub const TestCommand = struct { var modules: []*Jest.DescribeScope = reporter.jest.files.items(.module_scope)[file_start..]; for (modules) |module| { + vm.onUnhandledRejectionCtx = null; + vm.onUnhandledRejection = Jest.TestRunnerTask.onUnhandledRejection; module.runTests(JSC.JSValue.zero, vm.global); vm.eventLoop().tick(); - while (vm.active_tasks > 0) { + const initial_unhandled_counter = vm.unhandled_error_counter; + while (vm.active_tasks > 0 and vm.unhandled_error_counter == initial_unhandled_counter) { if (!Jest.Jest.runner.?.has_pending_tests) Jest.Jest.runner.?.drain(); vm.eventLoop().tick(); diff --git a/test/bun.js/event-emitter.test.ts b/test/bun.js/event-emitter.test.ts index 4c8d70452..952805cf8 100644 --- a/test/bun.js/event-emitter.test.ts +++ b/test/bun.js/event-emitter.test.ts @@ -13,4 +13,42 @@ describe("EventEmitter", () => { emitter.setMaxListeners(100); expect(emitter.getMaxListeners()).toBe(100); }); + + // These are also tests for the done() function in the test runner. + test("EventEmitter emit (different tick)", (done) => { + var emitter = new EventEmitter(); + emitter.on("wow", () => done()); + queueMicrotask(() => { + emitter.emit("wow"); + }); + }); + + // Unlike Jest, bun supports async and done + test("async EventEmitter emit (microtask)", async (done) => { + await 1; + var emitter = new EventEmitter(); + emitter.on("wow", () => done()); + emitter.emit("wow"); + }); + + test("async EventEmitter emit (microtask) after", async (done) => { + var emitter = new EventEmitter(); + emitter.on("wow", () => done()); + await 1; + emitter.emit("wow"); + }); + + test("EventEmitter emit (same tick)", (done) => { + var emitter = new EventEmitter(); + + emitter.on("wow", () => done()); + + emitter.emit("wow"); + }); + + test("EventEmitter emit (setTimeout task)", (done) => { + var emitter = new EventEmitter(); + emitter.on("wow", () => done()); + setTimeout(() => emitter.emit("wow"), 1); + }); }); |