diff --git a/src/bun.js/bindings/NapiClass.cpp b/src/bun.js/bindings/NapiClass.cpp index 4eab77fb52..eacf7fb6db 100644 --- a/src/bun.js/bindings/NapiClass.cpp +++ b/src/bun.js/bindings/NapiClass.cpp @@ -34,8 +34,10 @@ JSC_HOST_CALL_ATTRIBUTES JSC::EncodedJSValue NapiClass_ConstructorFunction(JSC:: JSValue newTarget; if constexpr (ConstructCall) { - NapiPrototype* prototype = JSC::jsDynamicCast(napi->getIfPropertyExists(globalObject, vm.propertyNames->prototype)); + // Use ::get instead of ::getIfPropertyExists here so that DontEnum is ignored. + auto prototypeValue = napi->get(globalObject, vm.propertyNames->prototype); RETURN_IF_EXCEPTION(scope, {}); + NapiPrototype* prototype = JSC::jsDynamicCast(prototypeValue); if (!prototype) { JSC::throwVMError(globalObject, scope, JSC::createTypeError(globalObject, "NapiClass constructor is missing the prototype"_s)); @@ -43,9 +45,21 @@ JSC_HOST_CALL_ATTRIBUTES JSC::EncodedJSValue NapiClass_ConstructorFunction(JSC:: } newTarget = callFrame->newTarget(); - auto* subclass = prototype->subclass(globalObject, asObject(newTarget)); + JSObject* thisValue; + // Match the behavior from + // https://github.com/oven-sh/WebKit/blob/397dafc9721b8f8046f9448abb6dbc14efe096d3/Source/JavaScriptCore/runtime/ObjectConstructor.cpp#L118-L145 + if (newTarget && newTarget != napi) { + JSGlobalObject* functionGlobalObject = getFunctionRealm(globalObject, asObject(newTarget)); + RETURN_IF_EXCEPTION(scope, {}); + Structure* baseStructure = functionGlobalObject->objectStructureForObjectConstructor(); + Structure* objectStructure = InternalFunction::createSubclassStructure(globalObject, asObject(newTarget), baseStructure); + RETURN_IF_EXCEPTION(scope, {}); + thisValue = constructEmptyObject(vm, objectStructure); + } else { + thisValue = prototype->subclass(globalObject, asObject(newTarget)); + } RETURN_IF_EXCEPTION(scope, {}); - callFrame->setThisValue(subclass); + callFrame->setThisValue(thisValue); } NAPICallFrame frame(globalObject, callFrame, napi->dataPtr(), newTarget); diff --git a/src/bun.js/bindings/napi.cpp b/src/bun.js/bindings/napi.cpp index 26e41ac9cd..5840ad360a 100644 --- a/src/bun.js/bindings/napi.cpp +++ b/src/bun.js/bindings/napi.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include "BufferEncodingType.h" #include #include @@ -2331,9 +2332,10 @@ extern "C" napi_status napi_get_value_bigint_int64(napi_env env, napi_value valu *result = jsValue.toBigInt64(toJS(env)); JSBigInt* bigint = jsValue.asHeapBigInt(); - uint64_t digit = bigint->length() > 0 ? bigint->digit(0) : 0; + auto length = bigint->length(); + uint64_t digit = length > 0 ? bigint->digit(0) : 0; - if (bigint->length() > 1) { + if (length > 1) { *lossless = false; } else if (bigint->sign()) { // negative @@ -2389,21 +2391,16 @@ extern "C" napi_status napi_get_value_bigint_words(napi_env env, JSC::JSBigInt* bigInt = jsValue.asHeapBigInt(); - size_t available_words = *word_count; - *word_count = bigInt->length(); - // Return ok in this case if (sign_bit == nullptr && words == nullptr) { + *word_count = bigInt->length(); NAPI_RETURN_SUCCESS(env); } - *sign_bit = (int)bigInt->sign(); - - size_t len = *word_count; - for (size_t i = 0; i < available_words && i < len; i++) { - words[i] = bigInt->digit(i); - } - + std::span writable_words(words, *word_count); + *sign_bit = static_cast(bigInt->sign()); + *word_count = bigInt->toWordsArray(writable_words); + ensureStillAliveHere(bigInt); NAPI_RETURN_SUCCESS(env); } @@ -2478,6 +2475,30 @@ extern "C" napi_status napi_set_instance_data(napi_env env, NAPI_RETURN_SUCCESS(env); } +extern "C" napi_status napi_create_bigint_uint64(napi_env env, uint64_t value, napi_value* result) +{ + NAPI_PREAMBLE(env); + NAPI_CHECK_ARG(env, result); + auto* globalObject = toJS(env); + auto* bigint = JSBigInt::createFrom(globalObject, value); + NAPI_RETURN_IF_EXCEPTION(env); + *result = toNapi(bigint, globalObject); + ensureStillAliveHere(bigint); + NAPI_RETURN_SUCCESS(env); +} + +extern "C" napi_status napi_create_bigint_int64(napi_env env, int64_t value, napi_value* result) +{ + NAPI_PREAMBLE(env); + NAPI_CHECK_ARG(env, result); + auto* globalObject = toJS(env); + auto* bigint = JSBigInt::createFrom(globalObject, value); + NAPI_RETURN_IF_EXCEPTION(env); + *result = toNapi(bigint, globalObject); + ensureStillAliveHere(bigint); + NAPI_RETURN_SUCCESS(env); +} + extern "C" napi_status napi_create_bigint_words(napi_env env, int sign_bit, size_t word_count, @@ -2504,34 +2525,16 @@ extern "C" napi_status napi_create_bigint_words(napi_env env, RETURN_IF_EXCEPTION(scope, napi_set_last_error(env, napi_pending_exception)); } - // JSBigInt requires there are no leading zeroes in the words array, but native modules may have - // passed an array containing leading zeroes. so we have to cut those off. - while (word_count > 0 && words[word_count - 1] == 0) { - word_count--; - } - - if (word_count == 0) { - auto* bigint = JSBigInt::createZero(globalObject); - RETURN_IF_EXCEPTION(scope, napi_set_last_error(env, napi_pending_exception)); - *result = toNapi(bigint, globalObject); - return napi_set_last_error(env, napi_ok); - } + std::span words_span(words, word_count); // throws RangeError if size is larger than JSC's limit - auto* bigint = JSBigInt::createWithLength(globalObject, word_count); + auto* bigint = JSBigInt::createFromWords(globalObject, words_span, sign_bit != 0); RETURN_IF_EXCEPTION(scope, napi_set_last_error(env, napi_pending_exception)); ASSERT(bigint); - bigint->setSign(sign_bit != 0); - - const uint64_t* current_word = words; - // TODO: add fast path that uses memcpy here instead of setDigit - // we need to add this to JSC. V8 has this optimization - for (size_t i = 0; i < word_count; i++) { - bigint->setDigit(i, *current_word++); - } - *result = toNapi(bigint, globalObject); + + ensureStillAliveHere(bigint); return napi_set_last_error(env, napi_ok); } @@ -2560,7 +2563,9 @@ extern "C" napi_status napi_create_symbol(napi_env env, napi_value description, // TODO handle empty string? } - *result = toNapi(JSC::Symbol::create(vm), globalObject); + auto* symbol = JSC::Symbol::create(vm); + *result = toNapi(symbol, globalObject); + ensureStillAliveHere(symbol); NAPI_RETURN_SUCCESS(env); } @@ -2671,8 +2676,9 @@ extern "C" napi_status napi_type_tag_object(napi_env env, napi_value value, cons Zig::GlobalObject* globalObject = toJS(env); JSObject* js_object = toJS(value).getObject(); NAPI_RETURN_EARLY_IF_FALSE(env, js_object, napi_object_expected); + JSValue napiTypeTagValue = globalObject->napiTypeTags()->get(js_object); - auto* existing_tag = jsDynamicCast(globalObject->napiTypeTags()->get(js_object)); + auto* existing_tag = jsDynamicCast(napiTypeTagValue); // cannot tag an object that is already tagged NAPI_RETURN_EARLY_IF_FALSE(env, existing_tag == nullptr, napi_invalid_arg); diff --git a/src/bun.js/bindings/webcore/Worker.cpp b/src/bun.js/bindings/webcore/Worker.cpp index 13d4748d51..ae0ad8654b 100644 --- a/src/bun.js/bindings/webcore/Worker.cpp +++ b/src/bun.js/bindings/webcore/Worker.cpp @@ -569,8 +569,9 @@ JSValue createNodeWorkerThreadsBinding(Zig::GlobalObject* globalObject) ASSERT(pair->canGetIndexQuickly(1u)); workerData = pair->getIndexQuickly(0); RETURN_IF_EXCEPTION(scope, {}); + auto environmentDataValue = pair->getIndexQuickly(1); // it might not be a Map if the parent had not set up environmentData yet - environmentData = jsDynamicCast(pair->getIndexQuickly(1)); + environmentData = environmentDataValue ? jsDynamicCast(environmentDataValue) : nullptr; RETURN_IF_EXCEPTION(scope, {}); // Main thread starts at 1 diff --git a/src/napi/napi.zig b/src/napi/napi.zig index 7b001a6c19..f8c8f1f7c3 100644 --- a/src/napi/napi.zig +++ b/src/napi/napi.zig @@ -997,30 +997,8 @@ pub export fn napi_is_date(env_: napi_env, value_: napi_value, is_date_: ?*bool) } pub extern fn napi_get_date_value(env: napi_env, value: napi_value, result: *f64) napi_status; pub extern fn napi_add_finalizer(env: napi_env, js_object: napi_value, native_object: ?*anyopaque, finalize_cb: napi_finalize, finalize_hint: ?*anyopaque, result: napi_ref) napi_status; -pub export fn napi_create_bigint_int64(env_: napi_env, value: i64, result_: ?*napi_value) napi_status { - log("napi_create_bigint_int64", .{}); - const env = env_ orelse { - return envIsNull(); - }; - env.checkGC(); - const result = result_ orelse { - return env.invalidArg(); - }; - result.set(env, JSC.JSValue.fromInt64NoTruncate(env.toJS(), value)); - return env.ok(); -} -pub export fn napi_create_bigint_uint64(env_: napi_env, value: u64, result_: ?*napi_value) napi_status { - log("napi_create_bigint_uint64", .{}); - const env = env_ orelse { - return envIsNull(); - }; - env.checkGC(); - const result = result_ orelse { - return env.invalidArg(); - }; - result.set(env, JSC.JSValue.fromUInt64NoTruncate(env.toJS(), value)); - return env.ok(); -} +pub extern fn napi_create_bigint_int64(env: napi_env, value: i64, result_: ?*napi_value) napi_status; +pub extern fn napi_create_bigint_uint64(env: napi_env, value: u64, result_: ?*napi_value) napi_status; pub extern fn napi_create_bigint_words(env: napi_env, sign_bit: c_int, word_count: usize, words: [*c]const u64, result: *napi_value) napi_status; pub extern fn napi_get_value_bigint_int64(_: napi_env, value_: napi_value, result_: ?*i64, _: *bool) napi_status; pub extern fn napi_get_value_bigint_uint64(_: napi_env, value_: napi_value, result_: ?*u64, _: *bool) napi_status; diff --git a/test/napi/napi-app/class_test.cpp b/test/napi/napi-app/class_test.cpp index 57dd62ea89..deffdda2a4 100644 --- a/test/napi/napi-app/class_test.cpp +++ b/test/napi/napi-app/class_test.cpp @@ -164,8 +164,53 @@ static napi_value get_class_with_constructor(const Napi::CallbackInfo &info) { return napi_class; } +static napi_value test_constructor_with_no_prototype(const Napi::CallbackInfo &info) { + // This test verifies that Reflect.construct with a newTarget that has no prototype + // property doesn't crash. This was a bug where jsDynamicCast was called on a JSValue + // of 0 when the prototype property didn't exist. + + napi_env env = info.Env(); + + // Get the NapiClass constructor + napi_value napi_class = get_class_with_constructor(info); + + // Create a newTarget object with no prototype property + napi_value new_target; + NODE_API_CALL(env, napi_create_object(env, &new_target)); + + // Call Reflect.construct(NapiClass, [], newTarget) + napi_value global; + NODE_API_CALL(env, napi_get_global(env, &global)); + + napi_value reflect; + NODE_API_CALL(env, napi_get_named_property(env, global, "Reflect", &reflect)); + + napi_value construct_fn; + NODE_API_CALL(env, napi_get_named_property(env, reflect, "construct", &construct_fn)); + + napi_value empty_array; + NODE_API_CALL(env, napi_create_array_with_length(env, 0, &empty_array)); + + napi_value args[3] = { napi_class, empty_array, new_target }; + napi_value result; + + // This should not crash - previously it would crash when trying to access + // the prototype property of newTarget + napi_status status = napi_call_function(env, reflect, construct_fn, 3, args, &result); + + if (status == napi_ok) { + return Napi::String::New(env, "success - no crash"); + } else { + // If there was an error, return it + const napi_extended_error_info* error_info; + napi_get_last_error_info(env, &error_info); + return Napi::String::New(env, error_info->error_message ? error_info->error_message : "error"); + } +} + void register_class_test(Napi::Env env, Napi::Object exports) { REGISTER_FUNCTION(env, exports, get_class_with_constructor); + REGISTER_FUNCTION(env, exports, test_constructor_with_no_prototype); } } // namespace napitests diff --git a/test/napi/napi-app/get_string_tests.cpp b/test/napi/napi-app/get_string_tests.cpp index 6fb7d8e96c..5020191612 100644 --- a/test/napi/napi-app/get_string_tests.cpp +++ b/test/napi/napi-app/get_string_tests.cpp @@ -19,6 +19,10 @@ test_get_value_string_any_encoding(const Napi::CallbackInfo &info) { std::array buf; napi_value string = info[0]; +#ifndef _WIN32 + BlockingStdoutScope stdout_scope; +#endif + size_t full_length; NODE_API_CALL(env, get_value_string_fn(env, string, nullptr, 0, &full_length)); diff --git a/test/napi/napi-app/module.js b/test/napi/napi-app/module.js index 1488404b70..9b6002ab02 100644 --- a/test/napi/napi-app/module.js +++ b/test/napi/napi-app/module.js @@ -399,6 +399,51 @@ nativeTests.test_reflect_construct_napi_class = () => { console.log("reflect constructed data =", instance.getData?.()); }; +nativeTests.test_reflect_construct_no_prototype_crash = () => { + // This test verifies the fix for jsDynamicCast being called on JSValue(0) + // when a NAPI class constructor is called via Reflect.construct with a + // newTarget that has no prototype property. + + const NapiClass = nativeTests.get_class_with_constructor(); + + // Test 1: Constructor function with deleted prototype property + // This case should work without crashing + function ConstructorWithoutPrototype() {} + delete ConstructorWithoutPrototype.prototype; + + try { + const instance1 = Reflect.construct(NapiClass, [], ConstructorWithoutPrototype); + console.log("constructor without prototype: success - no crash"); + } catch (e) { + console.log("constructor without prototype error:", e.message); + } + + // Test 2: Regular constructor (control test) + // This should always work + function NormalConstructor() {} + + try { + const instance2 = Reflect.construct(NapiClass, [], NormalConstructor); + console.log("normal constructor: success - no crash"); + } catch (e) { + console.log("normal constructor error:", e.message); + } + + // Test 3: Reflect.construct with Proxy newTarget (prototype returns undefined) + function ProxyObject() {} + + const proxyTarget = new Proxy(ProxyObject, { + get(target, prop) { + if (prop === "prototype") { + return undefined; + } + return target[prop]; + }, + }); + const instance3 = Reflect.construct(NapiClass, [], proxyTarget); + console.log("✓ Success - no crash!"); +}; + nativeTests.test_napi_wrap = () => { const values = [ {}, diff --git a/test/napi/napi-app/standalone_tests.cpp b/test/napi/napi-app/standalone_tests.cpp index 8795218b9b..5984927787 100644 --- a/test/napi/napi-app/standalone_tests.cpp +++ b/test/napi/napi-app/standalone_tests.cpp @@ -109,6 +109,9 @@ test_napi_get_value_string_utf8_with_buffer(const Napi::CallbackInfo &info) { NODE_API_CALL(env, napi_get_value_string_utf8(env, string_js, buf, len, &copied)); +#ifndef _WIN32 + BlockingStdoutScope stdout_scope; +#endif std::cout << "Chars to copy: " << len << std::endl; std::cout << "Copied chars: " << copied << std::endl; @@ -118,6 +121,7 @@ test_napi_get_value_string_utf8_with_buffer(const Napi::CallbackInfo &info) { } std::cout << std::endl; std::cout << "Value str: " << buf << std::endl; + return ok(env); } @@ -163,15 +167,23 @@ test_napi_handle_scope_bigint(const Napi::CallbackInfo &info) { auto *small_ints = new napi_value[num_small_ints]; - for (size_t i = 0; i < num_small_ints; i++) { - std::array words; - words.fill(i + 1); - NODE_API_CALL(env, napi_create_bigint_words(env, 0, small_int_size, - words.data(), &small_ints[i])); + for (size_t i = 0, small_int_index = 1; i < num_small_ints; + i++, small_int_index++) { + uint64_t words[small_int_size]; + for (size_t j = 0; j < small_int_size; j++) { + words[j] = small_int_index; + } + + NODE_API_CALL(env, napi_create_bigint_words(env, 0, small_int_size, words, + &small_ints[i])); } run_gc(info); +#ifndef _WIN32 + BlockingStdoutScope stdout_scope; +#endif + for (size_t j = 0; j < num_small_ints; j++) { std::array words; int sign; @@ -370,7 +382,8 @@ static napi_value test_napi_throw_with_nullptr(const Napi::CallbackInfo &info) { bool is_exception_pending; NODE_API_CALL(env, napi_is_exception_pending(env, &is_exception_pending)); - printf("napi_is_exception_pending -> %s\n", is_exception_pending ? "true" : "false"); + printf("napi_is_exception_pending -> %s\n", + is_exception_pending ? "true" : "false"); return ok(env); } @@ -382,6 +395,10 @@ static napi_value test_extended_error_messages(const Napi::CallbackInfo &info) { napi_env env = info.Env(); const napi_extended_error_info *error; +#ifndef _WIN32 + BlockingStdoutScope stdout_scope; +#endif + // this function is implemented in C++ // error because the result pointer is null printf("erroneous napi_create_double returned code %d\n", @@ -432,6 +449,11 @@ static napi_value test_extended_error_messages(const Napi::CallbackInfo &info) { static napi_value bigint_to_i64(const Napi::CallbackInfo &info) { napi_env env = info.Env(); + +#ifndef _WIN32 + BlockingStdoutScope stdout_scope; +#endif + // start at 1 is intentional, since argument 0 is the callback to run GC // passed to every function // perform test on all arguments @@ -460,6 +482,10 @@ static napi_value bigint_to_i64(const Napi::CallbackInfo &info) { static napi_value bigint_to_u64(const Napi::CallbackInfo &info) { napi_env env = info.Env(); +#ifndef _WIN32 + BlockingStdoutScope stdout_scope; +#endif + // start at 1 is intentional, since argument 0 is the callback to run GC // passed to every function // perform test on all arguments @@ -489,6 +515,10 @@ static napi_value bigint_to_u64(const Napi::CallbackInfo &info) { static napi_value bigint_to_64_null(const Napi::CallbackInfo &info) { napi_env env = info.Env(); +#ifndef _WIN32 + BlockingStdoutScope stdout_scope; +#endif + napi_value bigint; NODE_API_CALL(env, napi_create_bigint_int64(env, 5, &bigint)); diff --git a/test/napi/napi-app/utils.h b/test/napi/napi-app/utils.h index 92e158e6b7..0711999a32 100644 --- a/test/napi/napi-app/utils.h +++ b/test/napi/napi-app/utils.h @@ -2,6 +2,33 @@ #include "napi_with_version.h" #include +#ifndef _WIN32 +#include +#include + +// Node.js makes stdout non-blocking +// This messes up printf when you spam it quickly enough. +class BlockingStdoutScope { +public: + BlockingStdoutScope() { + original = fcntl(1, F_GETFL); + fcntl(1, F_SETFL, original & ~O_NONBLOCK); + setvbuf(stdout, nullptr, _IOFBF, 8192); + fflush(stdout); + } + + ~BlockingStdoutScope() { + fflush(stdout); + fcntl(1, F_SETFL, original); + setvbuf(stdout, nullptr, _IOLBF, 0); + } + +private: + int original; +}; + +#endif + // e.g NODE_API_CALL(env, napi_create_int32(env, 5, &my_napi_integer)) #define NODE_API_CALL(env, call) NODE_API_CALL_CUSTOM_RETURN(env, NULL, call) diff --git a/test/napi/napi.test.ts b/test/napi/napi.test.ts index 4143cf0d7d..69cb6cad5a 100644 --- a/test/napi/napi.test.ts +++ b/test/napi/napi.test.ts @@ -1,4 +1,4 @@ -import { spawnSync } from "bun"; +import { spawn, spawnSync } from "bun"; import { beforeAll, describe, expect, it } from "bun:test"; import { readdirSync } from "fs"; import { bunEnv, bunExe, tempDirWithFiles } from "harness"; @@ -150,88 +150,88 @@ describe("napi", () => { }); describe("issue_7685", () => { - it("works", () => { + it("works", async () => { const args = [...Array(20).keys()]; - checkSameOutput("test_issue_7685", args); + await checkSameOutput("test_issue_7685", args); }); }); describe("issue_11949", () => { - it("napi_call_threadsafe_function should accept null", () => { - const result = checkSameOutput("test_issue_11949", []); + it("napi_call_threadsafe_function should accept null", async () => { + const result = await checkSameOutput("test_issue_11949", []); expect(result).toStartWith("data = 1234, context = 42"); }); }); describe("napi_get_value_string_utf8 with buffer", () => { // see https://github.com/oven-sh/bun/issues/6949 - it("copies one char", () => { - const result = checkSameOutput("test_napi_get_value_string_utf8_with_buffer", ["abcdef", 2]); + it("copies one char", async () => { + const result = await checkSameOutput("test_napi_get_value_string_utf8_with_buffer", ["abcdef", 2]); expect(result).toEndWith("str: a"); }); - it("copies null terminator", () => { - const result = checkSameOutput("test_napi_get_value_string_utf8_with_buffer", ["abcdef", 1]); + it("copies null terminator", async () => { + const result = await checkSameOutput("test_napi_get_value_string_utf8_with_buffer", ["abcdef", 1]); expect(result).toEndWith("str:"); }); - it("copies zero char", () => { - const result = checkSameOutput("test_napi_get_value_string_utf8_with_buffer", ["abcdef", 0]); + it("copies zero char", async () => { + const result = await checkSameOutput("test_napi_get_value_string_utf8_with_buffer", ["abcdef", 0]); expect(result).toEndWith("str: *****************************"); }); - it("copies more than given len", () => { - const result = checkSameOutput("test_napi_get_value_string_utf8_with_buffer", ["abcdef", 25]); + it("copies more than given len", async () => { + const result = await checkSameOutput("test_napi_get_value_string_utf8_with_buffer", ["abcdef", 25]); expect(result).toEndWith("str: abcdef"); }); - it("copies auto len", () => { - const result = checkSameOutput("test_napi_get_value_string_utf8_with_buffer", ["abcdef", 424242]); + it("copies auto len", async () => { + const result = await checkSameOutput("test_napi_get_value_string_utf8_with_buffer", ["abcdef", 424242]); expect(result).toEndWith("str:"); }); }); describe("napi_get_value_string_*", () => { - it("behaves like node on edge cases", () => { - checkSameOutput("test_get_value_string", []); + it("behaves like node on edge cases", async () => { + await checkSameOutput("test_get_value_string", []); }); }); it("#1288", async () => { - const result = checkSameOutput("self", []); + const result = await checkSameOutput("self", []); expect(result).toBe("hello world!"); }); describe("handle_scope", () => { - it("keeps strings alive", () => { - checkSameOutput("test_napi_handle_scope_string", []); + it("keeps strings alive", async () => { + await checkSameOutput("test_napi_handle_scope_string", []); }); - it("keeps bigints alive", () => { - checkSameOutput("test_napi_handle_scope_bigint", []); + it("keeps bigints alive", async () => { + await checkSameOutput("test_napi_handle_scope_bigint", []); }, 10000); - it("keeps the parent handle scope alive", () => { - checkSameOutput("test_napi_handle_scope_nesting", []); + it("keeps the parent handle scope alive", async () => { + await checkSameOutput("test_napi_handle_scope_nesting", []); }); - it("exists when calling a napi constructor", () => { - checkSameOutput("test_napi_class_constructor_handle_scope", []); + it("exists when calling a napi constructor", async () => { + await checkSameOutput("test_napi_class_constructor_handle_scope", []); }); - it("exists while calling a napi_async_complete_callback", () => { - checkSameOutput("create_promise", [false]); + it("exists while calling a napi_async_complete_callback", async () => { + await checkSameOutput("create_promise", [false]); }); - it("keeps arguments moved off the stack alive", () => { - checkSameOutput("test_napi_handle_scope_many_args", ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]); + it("keeps arguments moved off the stack alive", async () => { + await checkSameOutput("test_napi_handle_scope_many_args", ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]); }); }); describe("escapable_handle_scope", () => { - it("keeps the escaped value alive in the outer scope", () => { - checkSameOutput("test_napi_escapable_handle_scope", []); + it("keeps the escaped value alive in the outer scope", async () => { + await checkSameOutput("test_napi_escapable_handle_scope", []); }); }); describe("napi_delete_property", () => { - it("returns a valid boolean", () => { - checkSameOutput( + it("returns a valid boolean", async () => { + await checkSameOutput( "test_napi_delete_property", // generate a string representing an array around an IIFE which main.js will eval // we do this as the napi_delete_property test needs an object with an own non-configurable @@ -253,48 +253,48 @@ describe("napi", () => { }); describe("napi_ref", () => { - it("can recover the value from a weak ref", () => { - checkSameOutput("test_napi_ref", []); + it("can recover the value from a weak ref", async () => { + await checkSameOutput("test_napi_ref", []); }); - it("allows creating a handle scope in the finalizer", () => { - checkSameOutput("test_napi_handle_scope_finalizer", []); + it("allows creating a handle scope in the finalizer", async () => { + await checkSameOutput("test_napi_handle_scope_finalizer", []); }); }); describe("napi_async_work", () => { - it("null checks execute callbacks", () => { - const output = checkSameOutput("test_napi_async_work_execute_null_check", []); + it("null checks execute callbacks", async () => { + const output = await checkSameOutput("test_napi_async_work_execute_null_check", []); expect(output).toContain("success!"); expect(output).not.toContain("failure!"); }); - it("null checks complete callbacks after scheduling", () => { - checkSameOutput("test_napi_async_work_complete_null_check", []); + it("null checks complete callbacks after scheduling", async () => { + await checkSameOutput("test_napi_async_work_complete_null_check", []); }); - it("works with cancelation", () => { - const output = checkSameOutput("test_napi_async_work_cancel", [], { "UV_THREADPOOL_SIZE": "2" }); + it("works with cancelation", async () => { + const output = await checkSameOutput("test_napi_async_work_cancel", [], { "UV_THREADPOOL_SIZE": "2" }); expect(output).toContain("success!"); expect(output).not.toContain("failure!"); }); }); describe("napi_threadsafe_function", () => { - it("keeps the event loop alive without async_work", () => { - const result = checkSameOutput("test_promise_with_threadsafe_function", []); + it("keeps the event loop alive without async_work", async () => { + const result = await checkSameOutput("test_promise_with_threadsafe_function", []); expect(result).toContain("tsfn_callback"); expect(result).toContain("resolved to 1234"); expect(result).toContain("tsfn_finalize_callback"); }); - it("does not hang on finalize", () => { - const result = checkSameOutput("test_napi_threadsafe_function_does_not_hang_after_finalize", []); + it("does not hang on finalize", async () => { + const result = await checkSameOutput("test_napi_threadsafe_function_does_not_hang_after_finalize", []); expect(result).toBe("success!"); }); }); describe("exception handling", () => { - it("can check for a pending error and catch the right value", () => { - checkSameOutput("test_get_exception", [5]); - checkSameOutput("test_get_exception", [{ foo: "bar" }]); + it("can check for a pending error and catch the right value", async () => { + await checkSameOutput("test_get_exception", [5]); + await checkSameOutput("test_get_exception", [{ foo: "bar" }]); }); it("can throw an exception from an async_complete_callback", async () => { const count = 10; @@ -303,19 +303,19 @@ describe("napi", () => { }); describe("napi_run_script", () => { - it("evaluates a basic expression", () => { - checkSameOutput("test_napi_run_script", ["5 * (1 + 2)"]); + it("evaluates a basic expression", async () => { + await checkSameOutput("test_napi_run_script", ["5 * (1 + 2)"]); }); - it("provides the right this value", () => { - checkSameOutput("test_napi_run_script", ["this === global"]); + it("provides the right this value", async () => { + await checkSameOutput("test_napi_run_script", ["this === global"]); }); - it("propagates exceptions", () => { - checkSameOutput("test_napi_run_script", ["(()=>{ throw new TypeError('oops'); })()"]); + it("propagates exceptions", async () => { + await checkSameOutput("test_napi_run_script", ["(()=>{ throw new TypeError('oops'); })()"]); }); - it("cannot see locals from around its invocation", () => { + it("cannot see locals from around its invocation", async () => { // variable should_not_exist is declared on main.js:18, but it should not be in scope for the eval'd code - // this doesn't use checkSameOutput because V8 and JSC use different error messages for a missing variable - let bunResult = runOn(bunExe(), "test_napi_run_script", ["shouldNotExist"]); + // this doesn't use await checkSameOutput because V8 and JSC use different error messages for a missing variable + let bunResult = await runOn(bunExe(), "test_napi_run_script", ["shouldNotExist"]); // remove all debug logs bunResult = bunResult.replaceAll(/^\[\w+\].+$/gm, "").trim(); expect(bunResult).toBe( @@ -325,100 +325,104 @@ describe("napi", () => { }); describe("napi_get_named_property", () => { - it("handles edge cases", () => { - checkSameOutput("test_get_property", []); + it("handles edge cases", async () => { + await checkSameOutput("test_get_property", []); }); }); describe("napi_set_named_property", () => { - it("handles edge cases", () => { - checkSameOutput("test_set_property", []); + it("handles edge cases", async () => { + await checkSameOutput("test_set_property", []); }); }); describe("napi_value <=> integer conversion", () => { - it("works", () => { - checkSameOutput("test_number_integer_conversions_from_js", []); - checkSameOutput("test_number_integer_conversions", []); + it("works", async () => { + await checkSameOutput("test_number_integer_conversions_from_js", []); + await checkSameOutput("test_number_integer_conversions", []); }); }); describe("arrays", () => { describe("napi_create_array_with_length", () => { - it("creates an array with empty slots", () => { - checkSameOutput("test_create_array_with_length", []); + it("creates an array with empty slots", async () => { + await checkSameOutput("test_create_array_with_length", []); }); }); }); describe("napi_throw functions", () => { - it("has the right code and message", () => { - checkSameOutput("test_throw_functions_exhaustive", []); + it("has the right code and message", async () => { + await checkSameOutput("test_throw_functions_exhaustive", []); }); - it("does not throw with nullptr", () => { - checkSameOutput("test_napi_throw_with_nullptr", []); + it("does not throw with nullptr", async () => { + await checkSameOutput("test_napi_throw_with_nullptr", []); }); }); describe("napi_create_error functions", () => { - it("has the right code and message", () => { - checkSameOutput("test_create_error_functions_exhaustive", []); + it("has the right code and message", async () => { + await checkSameOutput("test_create_error_functions_exhaustive", []); }); }); describe("napi_type_tag_object", () => { - it("works", () => { - checkSameOutput("test_type_tag", []); + it("works", async () => { + await checkSameOutput("test_type_tag", []); }); }); // TODO(@190n) test allocating in a finalizer from a napi module with the right version describe("napi_wrap", () => { - it("accepts the right kinds of values", () => { - checkSameOutput("test_napi_wrap", []); + it("accepts the right kinds of values", async () => { + await checkSameOutput("test_napi_wrap", []); }); - it("is shared between addons", () => { - checkSameOutput("test_napi_wrap_cross_addon", []); + it("is shared between addons", async () => { + await checkSameOutput("test_napi_wrap_cross_addon", []); }); - it("does not follow prototypes", () => { - checkSameOutput("test_napi_wrap_prototype", []); + it("does not follow prototypes", async () => { + await checkSameOutput("test_napi_wrap_prototype", []); }); - it("does not consider proxies", () => { - checkSameOutput("test_napi_wrap_proxy", []); + it("does not consider proxies", async () => { + await checkSameOutput("test_napi_wrap_proxy", []); }); - it("can remove a wrap", () => { - checkSameOutput("test_napi_remove_wrap", []); + it("can remove a wrap", async () => { + await checkSameOutput("test_napi_remove_wrap", []); }); - it("has the right lifetime", () => { - checkSameOutput("test_wrap_lifetime_without_ref", []); - checkSameOutput("test_wrap_lifetime_with_weak_ref", []); - checkSameOutput("test_wrap_lifetime_with_strong_ref", []); - checkSameOutput("test_remove_wrap_lifetime_with_weak_ref", []); - checkSameOutput("test_remove_wrap_lifetime_with_strong_ref", []); + it("has the right lifetime", async () => { + await checkSameOutput("test_wrap_lifetime_without_ref", []); + await checkSameOutput("test_wrap_lifetime_with_weak_ref", []); + await checkSameOutput("test_wrap_lifetime_with_strong_ref", []); + await checkSameOutput("test_remove_wrap_lifetime_with_weak_ref", []); + await checkSameOutput("test_remove_wrap_lifetime_with_strong_ref", []); // check that napi finalizers also run at VM exit, even if they didn't get run by GC - checkSameOutput("test_ref_deleted_in_cleanup", []); + await checkSameOutput("test_ref_deleted_in_cleanup", []); // check that calling napi_delete_ref in the ref's finalizer is not use-after-free - checkSameOutput("test_ref_deleted_in_async_finalize", []); + await checkSameOutput("test_ref_deleted_in_async_finalize", []); }); }); describe("napi_define_class", () => { - it("handles edge cases in the constructor", () => { - checkSameOutput("test_napi_class", []); - checkSameOutput("test_subclass_napi_class", []); - checkSameOutput("test_napi_class_non_constructor_call", []); - checkSameOutput("test_reflect_construct_napi_class", []); + it("handles edge cases in the constructor", async () => { + await checkSameOutput("test_napi_class", []); + await checkSameOutput("test_subclass_napi_class", []); + await checkSameOutput("test_napi_class_non_constructor_call", []); + await checkSameOutput("test_reflect_construct_napi_class", []); + }); + + it("does not crash with Reflect.construct when newTarget has no prototype", async () => { + await checkSameOutput("test_reflect_construct_no_prototype_crash", []); }); }); describe("bigint conversion to int64/uint64", () => { - it("works", () => { + it("works", async () => { const tests = [-1n, 0n, 1n]; for (const power of [63, 64, 65]) { for (const sign of [-1, 1]) { @@ -428,26 +432,26 @@ describe("napi", () => { } const testsString = "[" + tests.map(bigint => bigint.toString() + "n").join(",") + "]"; - checkSameOutput("bigint_to_i64", testsString); - checkSameOutput("bigint_to_u64", testsString); + await checkSameOutput("bigint_to_i64", testsString); + await checkSameOutput("bigint_to_u64", testsString); }); - it("returns the right error code", () => { + it("returns the right error code", async () => { const badTypes = '[null, undefined, 5, "123", "abc"]'; - checkSameOutput("bigint_to_i64", badTypes); - checkSameOutput("bigint_to_u64", badTypes); - checkSameOutput("bigint_to_64_null", []); + await checkSameOutput("bigint_to_i64", badTypes); + await checkSameOutput("bigint_to_u64", badTypes); + await checkSameOutput("bigint_to_64_null", []); }); }); describe("create_bigint_words", () => { - it("works", () => { - checkSameOutput("test_create_bigint_words", []); + it("works", async () => { + await checkSameOutput("test_create_bigint_words", []); }); }); describe("napi_get_last_error_info", () => { - it("returns information from the most recent call", () => { - checkSameOutput("test_extended_error_messages", []); + it("returns information from the most recent call", async () => { + await checkSameOutput("test_extended_error_messages", []); }); }); @@ -463,10 +467,10 @@ describe("napi", () => { ["[1, 2, 3]", false], ["'hello'", false], ]; - it("returns consistent values with node.js", () => { + it("returns consistent values with node.js", async () => { for (const [value, expected] of tests) { // main.js does eval then spread so to pass a single value we need to wrap in an array - const output = checkSameOutput(`test_is_${kind}`, "[" + value + "]"); + const output = await checkSameOutput(`test_is_${kind}`, "[" + value + "]"); expect(output).toBe(`napi_is_${kind} -> ${expected.toString()}`); } }); @@ -479,26 +483,33 @@ describe("napi", () => { ])("works when the module register function returns %s", (returnKind, expected) => { expect(require(`./napi-app/build/Debug/${returnKind}_addon.node`)).toEqual(expected); }); - it("works when the module register function throws", () => { + it("works when the module register function throws", async () => { expect(() => require("./napi-app/build/Debug/throw_addon.node")).toThrow(new Error("oops!")); }); }); -function checkSameOutput(test: string, args: any[] | string, envArgs: Record = {}) { - const nodeResult = runOn("node", test, args, envArgs).trim(); - let bunResult = runOn(bunExe(), test, args, envArgs); +async function checkSameOutput(test: string, args: any[] | string, envArgs: Record = {}) { + let [nodeResult, bunResult] = await Promise.all([ + runOn("node", test, args, envArgs), + runOn(bunExe(), test, args, envArgs), + ]); + nodeResult = nodeResult.trim(); // remove all debug logs - bunResult = bunResult.replaceAll(/^\[\w+\].+$/gm, "").trim(); + bunResult = bunResult + .replaceAll(/^\[\w+\].+$/gm, "") + // TODO: we don't seem to print ProxyObject in this case. + .replaceAll("function ProxyObject()", "function ()") + .trim(); expect(bunResult).toEqual(nodeResult); return nodeResult; } -function runOn(executable: string, test: string, args: any[] | string, envArgs: Record = {}) { +async function runOn(executable: string, test: string, args: any[] | string, envArgs: Record = {}) { // when the inspector runs (can be due to VSCode extension), there is // a bug that in debug modes the console logs extra stuff const { BUN_INSPECT_CONNECT_TO: _, ...rest } = bunEnv; const env = { ...rest, ...envArgs }; - const exec = spawnSync({ + const exec = spawn({ cmd: [ executable, "--expose-gc", @@ -507,11 +518,19 @@ function runOn(executable: string, test: string, args: any[] | string, envArgs: typeof args == "string" ? args : JSON.stringify(args), ], env, + stdout: "pipe", + stderr: "pipe", + stdin: "inherit", }); - const errs = exec.stderr.toString(); + const [stdout, stderr, result] = await Promise.all([ + new Response(exec.stdout).text(), + new Response(exec.stderr).text(), + exec.exited, + ]); + const errs = stderr.toString(); if (errs !== "") { throw new Error(errs); } - expect(exec.success).toBeTrue(); - return exec.stdout.toString(); + expect(result).toBe(0); + return stdout; }