mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
## Summary - `napi_typeof` was returning `napi_object` for `AsyncContextFrame` values, which are internally callable JSObjects - Native addons that check callback types (e.g. encore.dev's runtime) would fail with `expect Function, got: Object` and panic - Added a `jsDynamicCast<AsyncContextFrame*>` check before the final `napi_object` fallback to correctly report these values as `napi_function` Closes #25933 ## Test plan - [x] Verify encore.dev + supertokens reproduction from the issue no longer panics - [ ] Existing napi tests continue to pass Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude <noreply@anthropic.com>
947 lines
30 KiB
JavaScript
947 lines
30 KiB
JavaScript
const assert = require("node:assert");
|
|
const nativeTests = require("./build/Debug/napitests.node");
|
|
const secondAddon = require("./build/Debug/second_addon.node");
|
|
const asyncFinalizeAddon = require("./build/Debug/async_finalize_addon.node");
|
|
const testReferenceUnrefInFinalizer = require("./build/Debug/test_reference_unref_in_finalizer.node");
|
|
const testReferenceUnrefInFinalizerExperimental = require("./build/Debug/test_reference_unref_in_finalizer_experimental.node");
|
|
|
|
async function gcUntil(fn) {
|
|
const MAX = 100;
|
|
for (let i = 0; i < MAX; i++) {
|
|
await new Promise(resolve => {
|
|
setTimeout(resolve, 1);
|
|
});
|
|
if (typeof Bun == "object") {
|
|
Bun.gc(true);
|
|
} else {
|
|
// if this fails, you need to pass --expose-gc to node
|
|
global.gc();
|
|
}
|
|
if (fn()) {
|
|
return;
|
|
}
|
|
}
|
|
throw new Error(`Condition was not met after ${MAX} GC attempts`);
|
|
}
|
|
|
|
nativeTests.test_napi_class_constructor_handle_scope = () => {
|
|
const NapiClass = nativeTests.get_class_with_constructor();
|
|
const x = new NapiClass();
|
|
console.log("x.foo =", x.foo);
|
|
};
|
|
|
|
nativeTests.test_napi_handle_scope_finalizer = async () => {
|
|
// Create a weak reference, which will be collected eventually
|
|
// Pass false in Node.js so it does not create a handle scope
|
|
nativeTests.create_ref_with_finalizer(Boolean(process.isBun));
|
|
|
|
// Wait until it actually has been collected by ticking the event loop and forcing GC
|
|
await gcUntil(() => nativeTests.was_finalize_called());
|
|
};
|
|
|
|
nativeTests.test_napi_async_work_execute_null_check = () => {
|
|
const res = nativeTests.create_async_work_with_null_execute();
|
|
if (res) {
|
|
console.log("success!");
|
|
} else {
|
|
console.log("failure!");
|
|
}
|
|
};
|
|
|
|
nativeTests.test_napi_async_work_complete_null_check = async () => {
|
|
nativeTests.create_async_work_with_null_complete();
|
|
await gcUntil(() => true);
|
|
};
|
|
|
|
nativeTests.test_napi_async_work_cancel = () => {
|
|
// UV_THREADPOOL_SIZE is set to 2, create two blocking tasks,
|
|
// then create another and cancel it, ensuring the work is not
|
|
// scheduled before `napi_cancel_async_work` is called
|
|
const res = nativeTests.test_cancel_async_work(result => {
|
|
if (result) {
|
|
console.log("success!");
|
|
} else {
|
|
console.log("failure!");
|
|
}
|
|
});
|
|
|
|
if (!res) {
|
|
console.log("failure!");
|
|
}
|
|
};
|
|
|
|
nativeTests.test_promise_with_threadsafe_function = async () => {
|
|
await new Promise(resolve => setTimeout(resolve, 1));
|
|
// create_promise_with_threadsafe_function returns a promise that calls our function from another
|
|
// thread (via napi_threadsafe_function) and resolves with its return value
|
|
return await nativeTests.create_promise_with_threadsafe_function(() => 1234);
|
|
};
|
|
|
|
nativeTests.test_get_exception = (_, value) => {
|
|
function thrower() {
|
|
throw value;
|
|
}
|
|
try {
|
|
const result = nativeTests.call_and_get_exception(thrower);
|
|
console.log("got same exception back?", result === value);
|
|
} catch (e) {
|
|
console.log("native module threw", typeof e, e);
|
|
throw e;
|
|
}
|
|
};
|
|
|
|
nativeTests.test_get_property = () => {
|
|
const objects = [
|
|
{},
|
|
{ foo: "bar" },
|
|
{
|
|
get foo() {
|
|
throw new Error("get foo");
|
|
},
|
|
},
|
|
{
|
|
set foo(newValue) {},
|
|
},
|
|
new Proxy(
|
|
{},
|
|
{
|
|
get(_target, key) {
|
|
throw new Error(`proxy get ${key}`);
|
|
},
|
|
},
|
|
),
|
|
5,
|
|
"hello",
|
|
null,
|
|
undefined,
|
|
];
|
|
const keys = [
|
|
"foo",
|
|
{
|
|
toString() {
|
|
throw new Error("toString");
|
|
},
|
|
},
|
|
{
|
|
[Symbol.toPrimitive]() {
|
|
throw new Error("Symbol.toPrimitive");
|
|
},
|
|
},
|
|
"toString",
|
|
"slice",
|
|
];
|
|
|
|
for (const object of objects) {
|
|
for (const key of keys) {
|
|
try {
|
|
const ret = nativeTests.perform_get(object, key);
|
|
console.log("native function returned", ret);
|
|
} catch (e) {
|
|
console.log("threw", e.name);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
nativeTests.test_set_property = () => {
|
|
const objects = [
|
|
{},
|
|
{ foo: "bar" },
|
|
{
|
|
set foo(value) {
|
|
throw new Error(`set foo to ${value}`);
|
|
},
|
|
},
|
|
{
|
|
// getter but no setter
|
|
get foo() {},
|
|
},
|
|
new Proxy(
|
|
{},
|
|
{
|
|
set(_target, key, value) {
|
|
throw new Error(`proxy set ${key} to ${value}`);
|
|
},
|
|
},
|
|
),
|
|
null,
|
|
undefined,
|
|
];
|
|
const keys = [
|
|
"foo",
|
|
{
|
|
toString() {
|
|
throw new Error("toString");
|
|
},
|
|
},
|
|
{
|
|
[Symbol.toPrimitive]() {
|
|
throw new Error("Symbol.toPrimitive");
|
|
},
|
|
},
|
|
];
|
|
|
|
for (const object of objects) {
|
|
for (const key of keys) {
|
|
console.log(objects.indexOf(object) + ", " + keys.indexOf(key));
|
|
try {
|
|
const ret = nativeTests.perform_set(object, key, 42);
|
|
console.log("native function returned", ret);
|
|
if (object[key] != 42) {
|
|
throw new Error("setting property did not throw an error, but the property was not actually set");
|
|
}
|
|
} catch (e) {
|
|
console.log("threw", e.name);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
nativeTests.test_number_integer_conversions_from_js = () => {
|
|
const i32 = { min: -(2 ** 31), max: 2 ** 31 - 1 };
|
|
const u32Max = 2 ** 32 - 1;
|
|
// this is not the actual max value for i64, but rather the highest double that is below the true max value
|
|
const i64 = { min: -(2 ** 63), max: 2 ** 63 - 1024 };
|
|
|
|
const i32Cases = [
|
|
// special values
|
|
[Infinity, 0],
|
|
[-Infinity, 0],
|
|
[NaN, 0],
|
|
// normal
|
|
[0.0, 0],
|
|
[1.0, 1],
|
|
[-1.0, -1],
|
|
// truncation
|
|
[1.25, 1],
|
|
[-1.25, -1],
|
|
// limits
|
|
[i32.min, i32.min],
|
|
[i32.max, i32.max],
|
|
// wrap around
|
|
[i32.min - 1.0, i32.max],
|
|
[i32.max + 1.0, i32.min],
|
|
[i32.min - 2.0, i32.max - 1],
|
|
[i32.max + 2.0, i32.min + 1],
|
|
// type errors
|
|
["5", undefined],
|
|
[new Number(5), undefined],
|
|
];
|
|
|
|
for (const [input, expectedOutput] of i32Cases) {
|
|
const actualOutput = nativeTests.double_to_i32(input);
|
|
console.log(`${input} as i32 => ${actualOutput}`);
|
|
assert(actualOutput === expectedOutput);
|
|
}
|
|
|
|
const u32Cases = [
|
|
// special values
|
|
[Infinity, 0],
|
|
[-Infinity, 0],
|
|
[NaN, 0],
|
|
// normal
|
|
[0.0, 0],
|
|
[1.0, 1],
|
|
// truncation
|
|
[1.25, 1],
|
|
[-1.25, u32Max],
|
|
// limits
|
|
[u32Max, u32Max],
|
|
// wrap around
|
|
[-1.0, u32Max],
|
|
[u32Max + 1.0, 0],
|
|
[-2.0, u32Max - 1],
|
|
[u32Max + 2.0, 1],
|
|
// type errors
|
|
["5", undefined],
|
|
[new Number(5), undefined],
|
|
];
|
|
|
|
for (const [input, expectedOutput] of u32Cases) {
|
|
const actualOutput = nativeTests.double_to_u32(input);
|
|
console.log(`${input} as u32 => ${actualOutput}`);
|
|
assert(actualOutput === expectedOutput);
|
|
}
|
|
|
|
const i64Cases = [
|
|
// special values
|
|
[Infinity, 0],
|
|
[-Infinity, 0],
|
|
[NaN, 0],
|
|
// normal
|
|
[0.0, 0],
|
|
[1.0, 1],
|
|
[-1.0, -1],
|
|
// truncation
|
|
[1.25, 1],
|
|
[-1.25, -1],
|
|
// limits
|
|
[i64.min, i64.min],
|
|
[i64.max, i64.max],
|
|
// clamp
|
|
[i64.min - 4096.0, i64.min],
|
|
// this one clamps to the exact max value of i64 (2**63 - 1), which is then rounded
|
|
// to exactly 2**63 since that's the closest double that can be represented
|
|
[i64.max + 4096.0, 2 ** 63],
|
|
// type errors
|
|
["5", undefined],
|
|
[new Number(5), undefined],
|
|
];
|
|
|
|
for (const [input, expectedOutput] of i64Cases) {
|
|
const actualOutput = nativeTests.double_to_i64(input);
|
|
console.log(
|
|
`${typeof input == "number" ? input.toFixed(2) : input} as i64 => ${typeof actualOutput == "number" ? actualOutput.toFixed(2) : actualOutput}`,
|
|
);
|
|
assert(actualOutput === expectedOutput);
|
|
}
|
|
};
|
|
|
|
nativeTests.test_create_array_with_length = () => {
|
|
for (const size of [0, 5]) {
|
|
const array = nativeTests.make_empty_array(size);
|
|
console.log("length =", array.length);
|
|
// should be 0 as array contains empty slots
|
|
console.log("number of keys =", Object.keys(array).length);
|
|
}
|
|
};
|
|
|
|
nativeTests.test_throw_functions_exhaustive = () => {
|
|
for (const errorKind of ["error", "type_error", "range_error", "syntax_error"]) {
|
|
for (const code of [undefined, "", "error code"]) {
|
|
for (const msg of [undefined, "", "error message"]) {
|
|
try {
|
|
nativeTests.throw_error(code, msg, errorKind);
|
|
console.log(`napi_throw_${errorKind}(${code ?? "nullptr"}, ${msg ?? "nullptr"}) did not throw`);
|
|
} catch (e) {
|
|
console.log(
|
|
`napi_throw_${errorKind} threw ${e.name}: message ${JSON.stringify(e.message)}, code ${JSON.stringify(e.code)}`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
nativeTests.test_create_error_functions_exhaustive = () => {
|
|
for (const errorKind of ["error", "type_error", "range_error", "syntax_error"]) {
|
|
// null (JavaScript null) is changed to nullptr by the native function
|
|
for (const code of [undefined, null, "", 42, "error code"]) {
|
|
for (const msg of [undefined, null, "", 42, "error message"]) {
|
|
try {
|
|
nativeTests.create_and_throw_error(code, msg, errorKind);
|
|
console.log(
|
|
`napi_create_${errorKind}(${code === null ? "nullptr" : code}, ${msg === null ? "nullptr" : msg}) did not make an error`,
|
|
);
|
|
} catch (e) {
|
|
console.log(
|
|
`create_and_throw_error(${errorKind}) threw ${e.name}: message ${JSON.stringify(e.message)}, code ${JSON.stringify(e.code)}`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
nativeTests.test_type_tag = () => {
|
|
const o1 = {};
|
|
const o2 = {};
|
|
|
|
nativeTests.add_tag(o1, 1, 2);
|
|
|
|
try {
|
|
// re-tag
|
|
nativeTests.add_tag(o1, 1, 2);
|
|
} catch (e) {
|
|
console.log("tagging already-tagged object threw", e.toString());
|
|
}
|
|
|
|
console.log("tagging non-object succeeds: ", !nativeTests.try_add_tag(null, 0, 0));
|
|
|
|
nativeTests.add_tag(o2, 3, 4);
|
|
console.log("o1 matches o1:", nativeTests.check_tag(o1, 1, 2));
|
|
console.log("o1 matches o2:", nativeTests.check_tag(o1, 3, 4));
|
|
console.log("o2 matches o1:", nativeTests.check_tag(o2, 1, 2));
|
|
console.log("o2 matches o2:", nativeTests.check_tag(o2, 3, 4));
|
|
};
|
|
|
|
nativeTests.test_napi_class = () => {
|
|
const NapiClass = nativeTests.get_class_with_constructor();
|
|
const instance = new NapiClass();
|
|
console.log("static data =", NapiClass.getStaticData());
|
|
console.log("static getter =", NapiClass.getter);
|
|
console.log("foo =", instance.foo);
|
|
console.log("data =", instance.getData());
|
|
};
|
|
|
|
nativeTests.test_subclass_napi_class = () => {
|
|
const NapiClass = nativeTests.get_class_with_constructor();
|
|
class Subclass extends NapiClass {}
|
|
const instance = new Subclass();
|
|
console.log("subclass static data =", Subclass.getStaticData());
|
|
console.log("subclass static getter =", Subclass.getter);
|
|
console.log("subclass foo =", instance.foo);
|
|
console.log("subclass data =", instance.getData());
|
|
};
|
|
|
|
nativeTests.test_napi_class_non_constructor_call = () => {
|
|
const NapiClass = nativeTests.get_class_with_constructor();
|
|
console.log("non-constructor call NapiClass() =", NapiClass());
|
|
console.log("global foo set to ", typeof foo != "undefined" ? foo : undefined);
|
|
};
|
|
|
|
nativeTests.test_reflect_construct_napi_class = () => {
|
|
const NapiClass = nativeTests.get_class_with_constructor();
|
|
let instance = Reflect.construct(NapiClass, [], Object);
|
|
console.log("reflect constructed foo =", instance.foo);
|
|
console.log("reflect constructed data =", instance.getData?.());
|
|
class Foo {}
|
|
instance = Reflect.construct(NapiClass, [], Foo);
|
|
console.log("reflect constructed foo =", instance.foo);
|
|
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 = [
|
|
{},
|
|
{}, // should be able to be wrapped differently than the distinct empty object above
|
|
5,
|
|
new Number(5),
|
|
"abc",
|
|
new String("abc"),
|
|
null,
|
|
Symbol("abc"),
|
|
Symbol.for("abc"),
|
|
new (nativeTests.get_class_with_constructor())(),
|
|
new Proxy(
|
|
{},
|
|
Object.fromEntries(
|
|
[
|
|
"apply",
|
|
"construct",
|
|
"defineProperty",
|
|
"deleteProperty",
|
|
"get",
|
|
"getOwnPropertyDescriptor",
|
|
"getPrototypeOf",
|
|
"has",
|
|
"isExtensible",
|
|
"ownKeys",
|
|
"preventExtensions",
|
|
"set",
|
|
"setPrototypeOf",
|
|
].map(name => [
|
|
name,
|
|
() => {
|
|
throw new Error("oops");
|
|
},
|
|
]),
|
|
),
|
|
),
|
|
];
|
|
const wrapSuccess = Array(values.length).fill(false);
|
|
for (const [i, v] of values.entries()) {
|
|
wrapSuccess[i] = nativeTests.try_wrap(v, i + 1);
|
|
console.log(`${typeof v} did wrap: `, wrapSuccess[i]);
|
|
}
|
|
|
|
for (const [i, v] of values.entries()) {
|
|
if (wrapSuccess[i]) {
|
|
if (nativeTests.try_unwrap(v) !== i + 1) {
|
|
throw new Error("could not unwrap same value");
|
|
}
|
|
} else {
|
|
if (nativeTests.try_unwrap(v) !== undefined) {
|
|
throw new Error("value unwraps without being successfully wrapped");
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
nativeTests.test_napi_wrap_proxy = () => {
|
|
const target = {};
|
|
const proxy = new Proxy(target, {});
|
|
assert(nativeTests.try_wrap(target, 5));
|
|
assert(nativeTests.try_wrap(proxy, 6));
|
|
console.log(nativeTests.try_unwrap(target), nativeTests.try_unwrap(proxy));
|
|
};
|
|
|
|
nativeTests.test_napi_wrap_cross_addon = () => {
|
|
const wrapped = {};
|
|
console.log("wrap succeeds:", nativeTests.try_wrap(wrapped, 42));
|
|
console.log("unwrapped from other addon", secondAddon.try_unwrap(wrapped));
|
|
};
|
|
|
|
nativeTests.test_napi_wrap_prototype = () => {
|
|
class Foo {}
|
|
console.log("wrap prototype succeeds:", nativeTests.try_wrap(Foo.prototype, 42));
|
|
// wrapping should not look at prototype chain
|
|
console.log("unwrap instance:", nativeTests.try_unwrap(new Foo()));
|
|
};
|
|
|
|
nativeTests.test_napi_remove_wrap = () => {
|
|
const targets = [{}, new (nativeTests.get_class_with_constructor())()];
|
|
for (const t of targets) {
|
|
const target = {};
|
|
// fails
|
|
assert(nativeTests.try_remove_wrap(target) === undefined);
|
|
// wrap it
|
|
assert(nativeTests.try_wrap(target, 5));
|
|
// remove yields the wrapped value
|
|
assert(nativeTests.try_remove_wrap(target) === 5);
|
|
// neither remove nor unwrap work anymore
|
|
assert(nativeTests.try_unwrap(target) === undefined);
|
|
assert(nativeTests.try_remove_wrap(target) === undefined);
|
|
// can re-wrap
|
|
assert(nativeTests.try_wrap(target, 6));
|
|
assert(nativeTests.try_unwrap(target) === 6);
|
|
}
|
|
};
|
|
|
|
// parameters to create_wrap are: object, ask_for_ref, strong
|
|
const createWrapWithoutRef = o => nativeTests.create_wrap(o, false, false);
|
|
const createWrapWithWeakRef = o => nativeTests.create_wrap(o, true, false);
|
|
const createWrapWithStrongRef = o => nativeTests.create_wrap(o, true, true);
|
|
|
|
nativeTests.test_wrap_lifetime_without_ref = async () => {
|
|
let object = { foo: "bar" };
|
|
assert(createWrapWithoutRef(object) === object);
|
|
assert(nativeTests.get_wrap_data(object) === 42);
|
|
object = undefined;
|
|
await gcUntil(() => nativeTests.was_wrap_finalize_called());
|
|
};
|
|
|
|
nativeTests.test_wrap_lifetime_with_weak_ref = async () => {
|
|
// this looks the same as test_wrap_lifetime_without_ref because it is -- these cases should behave the same
|
|
let object = { foo: "bar" };
|
|
assert(createWrapWithWeakRef(object) === object);
|
|
assert(nativeTests.get_wrap_data(object) === 42);
|
|
object = undefined;
|
|
await gcUntil(() => nativeTests.was_wrap_finalize_called());
|
|
};
|
|
|
|
nativeTests.test_wrap_lifetime_with_strong_ref = async () => {
|
|
let object = { foo: "bar" };
|
|
assert(createWrapWithStrongRef(object) === object);
|
|
assert(nativeTests.get_wrap_data(object) === 42);
|
|
|
|
object = undefined;
|
|
// still referenced by native module so this should fail
|
|
try {
|
|
await gcUntil(() => nativeTests.was_wrap_finalize_called());
|
|
throw new Error("object was garbage collected while still referenced by native code");
|
|
} catch (e) {
|
|
if (!e.toString().includes("Condition was not met")) {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
// can still get the value using the ref
|
|
assert(nativeTests.get_wrap_data_from_ref() === 42);
|
|
|
|
// now we free it
|
|
nativeTests.unref_wrapped_value();
|
|
await gcUntil(() => nativeTests.was_wrap_finalize_called());
|
|
};
|
|
|
|
nativeTests.test_remove_wrap_lifetime_with_weak_ref = async () => {
|
|
let object = { foo: "bar" };
|
|
assert(createWrapWithWeakRef(object) === object);
|
|
|
|
assert(nativeTests.get_wrap_data(object) === 42);
|
|
|
|
nativeTests.remove_wrap(object);
|
|
assert(nativeTests.get_wrap_data(object) === undefined);
|
|
assert(nativeTests.get_wrap_data_from_ref() === undefined);
|
|
assert(nativeTests.get_object_from_ref() === object);
|
|
|
|
object = undefined;
|
|
|
|
// ref will stop working once the object is collected
|
|
await gcUntil(() => nativeTests.get_object_from_ref() === undefined);
|
|
|
|
// finalizer shouldn't have been called
|
|
assert(nativeTests.was_wrap_finalize_called() === false);
|
|
};
|
|
|
|
nativeTests.test_remove_wrap_lifetime_with_strong_ref = async () => {
|
|
let object = { foo: "bar" };
|
|
assert(createWrapWithStrongRef(object) === object);
|
|
|
|
assert(nativeTests.get_wrap_data(object) === 42);
|
|
|
|
nativeTests.remove_wrap(object);
|
|
assert(nativeTests.get_wrap_data(object) === undefined);
|
|
assert(nativeTests.get_wrap_data_from_ref() === undefined);
|
|
assert(nativeTests.get_object_from_ref() === object);
|
|
|
|
object = undefined;
|
|
|
|
// finalizer should not be called and object should not be freed
|
|
try {
|
|
await gcUntil(() => nativeTests.was_wrap_finalize_called() || nativeTests.get_object_from_ref() === undefined);
|
|
throw new Error("finalizer ran");
|
|
} catch (e) {
|
|
if (!e.toString().includes("Condition was not met")) {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
// native code can still get the object
|
|
assert(JSON.stringify(nativeTests.get_object_from_ref()) === `{"foo":"bar"}`);
|
|
|
|
// now it gets deleted
|
|
nativeTests.unref_wrapped_value();
|
|
await gcUntil(() => nativeTests.get_object_from_ref() === undefined);
|
|
};
|
|
|
|
nativeTests.test_ref_deleted_in_cleanup = () => {
|
|
let object = { foo: "bar" };
|
|
assert(createWrapWithWeakRef(object) === object);
|
|
assert(nativeTests.get_wrap_data(object) === 42);
|
|
};
|
|
|
|
nativeTests.test_ref_deleted_in_async_finalize = () => {
|
|
asyncFinalizeAddon.create_ref();
|
|
};
|
|
|
|
nativeTests.test_reference_unref_in_finalizer = async gc => {
|
|
// Create objects with finalizers that will call napi_reference_unref when GC'd
|
|
let objects = testReferenceUnrefInFinalizer.test_reference_unref_in_finalizer();
|
|
|
|
// Clear the reference to allow GC
|
|
objects = null;
|
|
|
|
// Force GC multiple times to ensure finalizers run
|
|
if (gc) {
|
|
gc();
|
|
gc();
|
|
}
|
|
|
|
// Allocate large ArrayBuffers to trigger GC pressure
|
|
const buffers = [];
|
|
for (let i = 0; i < 100; i++) {
|
|
buffers.push(new ArrayBuffer(10 * 1024 * 1024)); // 10MB each
|
|
if (gc && i % 10 === 0) {
|
|
gc();
|
|
}
|
|
}
|
|
|
|
// Wait for async operations
|
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
|
|
// Force final GC
|
|
if (gc) {
|
|
gc();
|
|
gc();
|
|
}
|
|
|
|
// Get stats to verify finalizers were called
|
|
const stats = testReferenceUnrefInFinalizer.get_stats();
|
|
console.log(`Finalizers called: ${stats.finalizersCalled}, Unrefs succeeded: ${stats.unrefsSucceeded}`);
|
|
|
|
if (stats.finalizersCalled === 0) {
|
|
throw new Error("No finalizers were called - test did not properly trigger GC");
|
|
}
|
|
|
|
if (stats.unrefsSucceeded === 0) {
|
|
throw new Error("No napi_reference_unref calls succeeded");
|
|
}
|
|
|
|
console.log("SUCCESS: napi_reference_unref worked in finalizers without crashing");
|
|
};
|
|
|
|
nativeTests.test_reference_unref_in_finalizer_experimental = async gc => {
|
|
// This test is expected to CRASH when the finalizer runs
|
|
// The experimental NAPI module enforces GC checks and will abort the process
|
|
console.log("WARNING: This test will crash the process - this is expected behavior!");
|
|
|
|
// Create objects with finalizers that will call napi_reference_unref when GC'd
|
|
let objects = testReferenceUnrefInFinalizerExperimental.test_reference_unref_in_finalizer_experimental();
|
|
|
|
// Clear the reference to allow GC
|
|
objects = null;
|
|
|
|
// Force GC to trigger the finalizers - this should crash the process
|
|
if (gc) {
|
|
gc();
|
|
gc();
|
|
}
|
|
|
|
// Allocate memory to ensure GC runs
|
|
for (let i = 0; i < 5; i++) {
|
|
new ArrayBuffer(10 * 1024 * 1024);
|
|
if (gc) gc();
|
|
}
|
|
|
|
// If we get here, the test has FAILED - the process should have crashed
|
|
console.log("ERROR: Process did not crash as expected!");
|
|
console.log("ERROR: The GC check for experimental modules is NOT working!");
|
|
throw new Error("Test FAILED: napi_reference_unref should have aborted for experimental module");
|
|
};
|
|
|
|
nativeTests.test_create_bigint_words = () => {
|
|
console.log(nativeTests.create_weird_bigints());
|
|
};
|
|
|
|
nativeTests.test_bigint_word_count = () => {
|
|
// Test with a 2-word BigInt
|
|
const bigint = 0x123456789abcdef0123456789abcdefn;
|
|
const result = nativeTests.test_bigint_actual_word_count(bigint);
|
|
|
|
console.log(`BigInt: ${bigint.toString(16)}`);
|
|
console.log(`Queried word count: ${result.queriedWordCount}`);
|
|
console.log(`Actual word count: ${result.actualWordCount}`);
|
|
console.log(`Sign bit: ${result.signBit}`);
|
|
|
|
// Both counts should be 2 for this BigInt
|
|
if (result.queriedWordCount === 2 && result.actualWordCount === 2) {
|
|
console.log("✅ PASS: Word count correctly returns 2");
|
|
} else {
|
|
console.log(
|
|
`❌ FAIL: Expected word count 2, got queried=${result.queriedWordCount}, actual=${result.actualWordCount}`,
|
|
);
|
|
}
|
|
};
|
|
|
|
nativeTests.test_ref_unref_underflow = () => {
|
|
// Test that napi_reference_unref properly handles refCount == 0
|
|
const obj = { test: "value" };
|
|
const result = nativeTests.test_reference_unref_underflow(obj);
|
|
|
|
console.log(`First unref count: ${result.firstUnrefCount}`);
|
|
console.log(`Second unref status: ${result.secondUnrefStatus}`);
|
|
|
|
// First unref should succeed and return count of 0
|
|
// Second unref should fail with napi_generic_failure (status = 1)
|
|
if (result.firstUnrefCount === 0 && result.secondUnrefStatus === 1) {
|
|
console.log("✅ PASS: Reference unref correctly prevents underflow");
|
|
} else {
|
|
console.log(
|
|
`❌ FAIL: Expected firstUnrefCount=0, secondUnrefStatus=1, got ${result.firstUnrefCount}, ${result.secondUnrefStatus}`,
|
|
);
|
|
}
|
|
};
|
|
|
|
nativeTests.test_get_value_string = () => {
|
|
function to16Bit(string) {
|
|
if (typeof Bun != "object") return string;
|
|
const jsc = require("bun:jsc");
|
|
const codeUnits = new DataView(new ArrayBuffer(2 * string.length));
|
|
for (let i = 0; i < string.length; i++) {
|
|
codeUnits.setUint16(2 * i, string.charCodeAt(i), true);
|
|
}
|
|
const decoder = new TextDecoder("utf-16le");
|
|
const string16Bit = decoder.decode(codeUnits);
|
|
// make sure we succeeded in making a UTF-16 string
|
|
assert(jsc.jscDescribe(string16Bit).includes("8Bit:(0)"));
|
|
return string16Bit;
|
|
}
|
|
function assert8Bit(string) {
|
|
if (typeof Bun != "object") return string;
|
|
const jsc = require("bun:jsc");
|
|
// make sure we succeeded in making a Latin-1 string
|
|
assert(jsc.jscDescribe(string).includes("8Bit:(1)"));
|
|
return string;
|
|
}
|
|
// test all of our get_value_string_XXX functions on a variety of inputs
|
|
for (const [string, description] of [
|
|
["hello", "simple latin-1"],
|
|
[to16Bit("hello"), "16-bit encoded with only BMP characters"],
|
|
[assert8Bit("café"), "8-bit with non-ascii characters"],
|
|
[to16Bit("café"), "16-bit with non-ascii but latin-1 characters"],
|
|
["你好小圆面包", "16-bit, all BMP, all outside latin-1"],
|
|
["🐱🏳️⚧️", "16-bit with many surrogate pairs"],
|
|
// TODO(@190n) handle these correctly
|
|
// ["\ud801", "unpaired high surrogate"],
|
|
// ["\udc02", "unpaired low surrogate"],
|
|
]) {
|
|
console.log(`test napi_get_value_string on ${string} (${description})`);
|
|
for (const encoding of ["latin1", "utf8", "utf16"]) {
|
|
console.log(encoding);
|
|
const fn = nativeTests[`test_get_value_string_${encoding}`];
|
|
fn(string);
|
|
}
|
|
}
|
|
};
|
|
|
|
nativeTests.test_constructor_order = () => {
|
|
require("./build/Debug/constructor_order_addon.node");
|
|
};
|
|
|
|
// Cleanup hook tests
|
|
nativeTests.test_cleanup_hook_order = () => {
|
|
const addon = require("./build/Debug/test_cleanup_hook_order.node");
|
|
addon.test();
|
|
};
|
|
|
|
nativeTests.test_cleanup_hook_remove_nonexistent = () => {
|
|
const addon = require("./build/Debug/test_cleanup_hook_remove_nonexistent.node");
|
|
addon.test();
|
|
};
|
|
|
|
nativeTests.test_async_cleanup_hook_remove_nonexistent = () => {
|
|
const addon = require("./build/Debug/test_async_cleanup_hook_remove_nonexistent.node");
|
|
addon.test();
|
|
};
|
|
|
|
nativeTests.test_cleanup_hook_duplicates = () => {
|
|
const addon = require("./build/Debug/test_cleanup_hook_duplicates.node");
|
|
addon.test();
|
|
};
|
|
|
|
nativeTests.test_cleanup_hook_mixed_order = () => {
|
|
const addon = require("./build/Debug/test_cleanup_hook_mixed_order.node");
|
|
addon.test();
|
|
};
|
|
|
|
nativeTests.test_cleanup_hook_modification_during_iteration = () => {
|
|
const addon = require("./build/Debug/test_cleanup_hook_modification_during_iteration.node");
|
|
addon.test();
|
|
};
|
|
|
|
// Test for napi_typeof with boxed primitive objects (String, Number, Boolean)
|
|
// See: https://github.com/oven-sh/bun/issues/25351
|
|
nativeTests.test_napi_typeof_boxed_primitives = () => {
|
|
// napi_valuetype enum values (from node_api_types.h):
|
|
// napi_undefined = 0, napi_null = 1, napi_boolean = 2, napi_number = 3,
|
|
// napi_string = 4, napi_symbol = 5, napi_object = 6, napi_function = 7,
|
|
// napi_external = 8, napi_bigint = 9
|
|
|
|
const napi_string = 4;
|
|
const napi_object = 6;
|
|
|
|
// Primitive string should be napi_string
|
|
const primitiveStringType = nativeTests.napi_get_typeof("hello");
|
|
assert.strictEqual(
|
|
primitiveStringType,
|
|
napi_string,
|
|
`primitive string should be napi_string (${napi_string}), got ${primitiveStringType}`,
|
|
);
|
|
console.log("PASS: primitive string returns napi_string");
|
|
|
|
// String object should be napi_object
|
|
const stringObjectType = nativeTests.napi_get_typeof(new String("hello"));
|
|
assert.strictEqual(
|
|
stringObjectType,
|
|
napi_object,
|
|
`String object should be napi_object (${napi_object}), got ${stringObjectType}`,
|
|
);
|
|
console.log("PASS: String object returns napi_object");
|
|
|
|
// Number object should be napi_object
|
|
const numberObjectType = nativeTests.napi_get_typeof(new Number(42));
|
|
assert.strictEqual(
|
|
numberObjectType,
|
|
napi_object,
|
|
`Number object should be napi_object (${napi_object}), got ${numberObjectType}`,
|
|
);
|
|
console.log("PASS: Number object returns napi_object");
|
|
|
|
// Boolean object should be napi_object
|
|
const booleanObjectType = nativeTests.napi_get_typeof(new Boolean(true));
|
|
assert.strictEqual(
|
|
booleanObjectType,
|
|
napi_object,
|
|
`Boolean object should be napi_object (${napi_object}), got ${booleanObjectType}`,
|
|
);
|
|
console.log("PASS: Boolean object returns napi_object");
|
|
|
|
console.log("All boxed primitive tests passed!");
|
|
};
|
|
|
|
// https://github.com/oven-sh/bun/issues/25933
|
|
// Test that napi_typeof returns napi_function for callbacks wrapped in
|
|
// AsyncContextFrame (which happens inside AsyncLocalStorage.run()).
|
|
nativeTests.test_napi_typeof_async_context_frame = async () => {
|
|
const { AsyncLocalStorage } = require("node:async_hooks");
|
|
const als = new AsyncLocalStorage();
|
|
|
|
await als.run({ key: "value" }, () => {
|
|
return new Promise(resolve => {
|
|
// Pass a callback to the native addon. Because we're inside
|
|
// AsyncLocalStorage.run(), Bun wraps it in AsyncContextFrame.
|
|
// The native call_js_cb will call napi_typeof on the received
|
|
// js_callback and print the result.
|
|
nativeTests.test_issue_25933(() => {});
|
|
// The threadsafe function callback fires asynchronously.
|
|
setTimeout(resolve, 50);
|
|
});
|
|
});
|
|
};
|
|
|
|
// Test that napi_make_callback works when the func is an AsyncContextFrame
|
|
// (received by a threadsafe function's call_js_cb inside AsyncLocalStorage.run()).
|
|
nativeTests.test_make_callback_with_async_context = async () => {
|
|
const { AsyncLocalStorage } = require("node:async_hooks");
|
|
const als = new AsyncLocalStorage();
|
|
|
|
await als.run({ key: "value" }, () => {
|
|
return new Promise(resolve => {
|
|
nativeTests.test_napi_make_callback_async_context_frame(() => {});
|
|
setTimeout(resolve, 50);
|
|
});
|
|
});
|
|
};
|
|
|
|
// Test that napi_create_threadsafe_function with call_js_cb=NULL accepts an
|
|
// AsyncContextFrame as the func (received from another threadsafe function's call_js_cb).
|
|
nativeTests.test_create_tsfn_with_async_context = async () => {
|
|
const { AsyncLocalStorage } = require("node:async_hooks");
|
|
const als = new AsyncLocalStorage();
|
|
|
|
await als.run({ key: "value" }, () => {
|
|
return new Promise(resolve => {
|
|
nativeTests.test_napi_create_tsfn_async_context_frame(() => {});
|
|
setTimeout(resolve, 100);
|
|
});
|
|
});
|
|
};
|
|
|
|
module.exports = nativeTests;
|