diff --git a/PORTING.md b/PORTING.md index 4607b87..dd42faf 100644 --- a/PORTING.md +++ b/PORTING.md @@ -58,7 +58,7 @@ Tests covering the engine-specific part of Node-API, defined in `js_native_api.h | `test_exception` | Ported ✅ | Medium | | `test_finalizer` | Ported ✅ | Medium | | `test_function` | Ported ✅ | Medium | -| `test_general` | Not ported | Hard | +| `test_general` | Partial | Hard | | `test_handle_scope` | Ported ✅ | Easy | | `test_instance_data` | Not ported | Medium | | `test_new_target` | Ported ✅ | Easy | diff --git a/implementors/node/features.js b/implementors/node/features.js index fec2137..04bd0af 100644 --- a/implementors/node/features.js +++ b/implementors/node/features.js @@ -11,7 +11,15 @@ globalThis.experimentalFeatures = { // causing addons that reference them to fail at dlopen time. sharedArrayBuffer: major >= 25 || (major === 24 && minor >= 9), createObjectWithProperties: true, - setPrototype: true, + // node_api_set_prototype was added in Node.js v25.4.0 and v24.13.1, and not + // backported to v20.x or v22.x. Earlier versions do not export the symbol, + // so an addon referencing it fails at dlopen time. + setPrototype: + major > 25 || + (major === 25 && minor >= 4) || + (major === 24 && (minor > 13 || (minor === 13 && patch >= 1))), + // node_api_post_finalizer has been available since v20.10.0, which is at or + // below every Node.js version this harness supports. postFinalizer: true, }; diff --git a/tests/js-native-api/test_general/CMakeLists.txt b/tests/js-native-api/test_general/CMakeLists.txt new file mode 100644 index 0000000..46e41af --- /dev/null +++ b/tests/js-native-api/test_general/CMakeLists.txt @@ -0,0 +1,5 @@ +add_node_api_cts_addon(test_general test_general.c) + +# node_api_set_prototype is gated behind NAPI_EXPERIMENTAL, so it builds as a +# separate addon that only runtimes exporting it need to load. +add_node_api_cts_experimental_addon(test_general_set_prototype test_general_set_prototype.c) diff --git a/tests/js-native-api/test_general/test.js b/tests/js-native-api/test_general/test.js new file mode 100644 index 0000000..54450d1 --- /dev/null +++ b/tests/js-native-api/test_general/test.js @@ -0,0 +1,96 @@ +const test_general = loadAddon('test_general'); + +const val1 = '1'; +const val2 = 1; +const val3 = 1; + +class BaseClass { +} + +class ExtendedClass extends BaseClass { +} + +const baseObject = new BaseClass(); +const extendedObject = new ExtendedClass(); + +// napi_strict_equals +assert.ok(test_general.testStrictEquals(val1, val1)); +assert.strictEqual(test_general.testStrictEquals(val1, val2), false); +assert.ok(test_general.testStrictEquals(val2, val3)); + +// napi_get_prototype +assert.strictEqual( + test_general.testGetPrototype(baseObject), + Object.getPrototypeOf(baseObject), +); +assert.strictEqual( + test_general.testGetPrototype(extendedObject), + Object.getPrototypeOf(extendedObject), +); +// Prototypes for base and extended should be different. +assert.notStrictEqual( + test_general.testGetPrototype(baseObject), + test_general.testGetPrototype(extendedObject), +); + +// napi_get_version. Upstream pins this to Node.js's own Node-API version; +// portably, the addon must report whatever version the runtime declares. +assert.strictEqual(test_general.testGetVersion(), napiVersion); + +// napi_typeof +[ + 123, + 'test string', + function() {}, + new Object(), + true, + undefined, + Symbol(), +].forEach((val) => { + assert.strictEqual(test_general.testNapiTypeof(val), typeof val); +}); + +// typeof null is 'object' in JS, so napi_null gets its own case. +assert.strictEqual(test_general.testNapiTypeof(null), 'null'); + +// Wrapping the same object twice fails. +const x = {}; +test_general.wrap(x); +assert.throws( + () => test_general.wrap(x), + { name: 'Error', message: 'Invalid argument' }, +); +// Clean up here, otherwise derefItemWasCalled() will be polluted. +test_general.removeWrap(x); + +// Wrapping twice succeeds if a removeWrap() separates the instances. +const y = {}; +test_general.wrap(y); +test_general.removeWrap(y); +test_general.wrap(y); +// Clean up here, otherwise derefItemWasCalled() will be polluted. +test_general.removeWrap(y); + +// napi_adjust_external_memory +const adjustedValue = test_general.testAdjustExternalMemory(); +assert.strictEqual(typeof adjustedValue, 'number'); +assert.ok(adjustedValue > 0); + +// Garbage collecting a wrapped object calls the finalizer. +assert.strictEqual(test_general.derefItemWasCalled(), false); + +(() => test_general.wrap({}))(); +await gcUntil( + 'deref_item() was called upon garbage collecting a wrapped object.', + () => test_general.derefItemWasCalled(), +); + +// Removing a wrap and then garbage collecting does not call the finalizer. +let z = {}; +test_general.testFinalizeWrap(z); +test_general.removeWrap(z); +z = null; +await gcUntil( + 'finalize callback was not called upon garbage collection.', + () => !test_general.finalizeWasCalled(), +); diff --git a/tests/js-native-api/test_general/testGlobals.js b/tests/js-native-api/test_general/testGlobals.js new file mode 100644 index 0000000..ad33b54 --- /dev/null +++ b/tests/js-native-api/test_general/testGlobals.js @@ -0,0 +1,4 @@ +const test_general = loadAddon('test_general'); + +assert.strictEqual(test_general.getUndefined(), undefined); +assert.strictEqual(test_general.getNull(), null); diff --git a/tests/js-native-api/test_general/testNapiRun.js b/tests/js-native-api/test_general/testNapiRun.js new file mode 100644 index 0000000..d11ed81 --- /dev/null +++ b/tests/js-native-api/test_general/testNapiRun.js @@ -0,0 +1,7 @@ +const test_general = loadAddon('test_general'); + +assert.strictEqual(test_general.testNapiRun('(41.92 + 0.08);'), 42); +assert.throws( + () => test_general.testNapiRun({ abc: 'def' }), + /string was expected/, +); diff --git a/tests/js-native-api/test_general/testNapiStatus.js b/tests/js-native-api/test_general/testNapiStatus.js new file mode 100644 index 0000000..1a1cdf3 --- /dev/null +++ b/tests/js-native-api/test_general/testNapiStatus.js @@ -0,0 +1,10 @@ +const test_general = loadAddon('test_general'); + +// createNapiError provokes a failing call, then checks that +// napi_get_last_error_info reports that failure. The next successful call must +// reset the recorded status back to napi_ok. +test_general.createNapiError(); +assert.ok( + test_general.testNapiErrorCleanup(), + 'napi_status cleaned up for second call', +); diff --git a/tests/js-native-api/test_general/testSetPrototype.js b/tests/js-native-api/test_general/testSetPrototype.js new file mode 100644 index 0000000..e7145b2 --- /dev/null +++ b/tests/js-native-api/test_general/testSetPrototype.js @@ -0,0 +1,20 @@ +// node_api_set_prototype is gated behind NAPI_EXPERIMENTAL, so it lives in its +// own addon and the stable test_general addon stays loadable everywhere. +if (!experimentalFeatures.setPrototype) { + skipTest(); +} + +const test_general = loadAddon('test_general'); +const test_general_set_prototype = loadAddon('test_general_set_prototype'); + +const nullProtoObject = { __proto__: null }; +assert.strictEqual(Object.getPrototypeOf(nullProtoObject), null); + +test_general_set_prototype.testSetPrototype(nullProtoObject, Object.prototype); + +assert.strictEqual(Object.getPrototypeOf(nullProtoObject), Object.prototype); +// napi_get_prototype must agree with the prototype just installed. +assert.strictEqual( + test_general.testGetPrototype(nullProtoObject), + Object.prototype, +); diff --git a/tests/js-native-api/test_general/test_general.c b/tests/js-native-api/test_general/test_general.c new file mode 100644 index 0000000..03826ea --- /dev/null +++ b/tests/js-native-api/test_general/test_general.c @@ -0,0 +1,229 @@ +#include +#include +#include "../common.h" +#include "../entry_point.h" + +static napi_value testStrictEquals(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + bool bool_result; + napi_value result; + NODE_API_CALL(env, napi_strict_equals(env, args[0], args[1], &bool_result)); + NODE_API_CALL(env, napi_get_boolean(env, bool_result, &result)); + + return result; +} + +static napi_value testGetPrototype(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value result; + NODE_API_CALL(env, napi_get_prototype(env, args[0], &result)); + + return result; +} + +static napi_value testGetVersion(napi_env env, napi_callback_info info) { + uint32_t version; + napi_value result; + NODE_API_CALL(env, napi_get_version(env, &version)); + NODE_API_CALL(env, napi_create_uint32(env, version, &result)); + return result; +} + +static napi_value getNull(napi_env env, napi_callback_info info) { + napi_value result; + NODE_API_CALL(env, napi_get_null(env, &result)); + return result; +} + +static napi_value getUndefined(napi_env env, napi_callback_info info) { + napi_value result; + NODE_API_CALL(env, napi_get_undefined(env, &result)); + return result; +} + +static napi_value createNapiError(napi_env env, napi_callback_info info) { + napi_value value; + NODE_API_CALL(env, napi_create_string_utf8(env, "xyz", 3, &value)); + + double double_value; + napi_status status = napi_get_value_double(env, value, &double_value); + + NODE_API_ASSERT(env, status != napi_ok, "Failed to produce error condition"); + + const napi_extended_error_info* error_info = 0; + NODE_API_CALL(env, napi_get_last_error_info(env, &error_info)); + + NODE_API_ASSERT(env, + error_info->error_code == status, + "Last error info code should match last status"); + NODE_API_ASSERT(env, + error_info->error_message, + "Last error info message should not be null"); + + return NULL; +} + +static napi_value testNapiErrorCleanup(napi_env env, napi_callback_info info) { + const napi_extended_error_info* error_info = 0; + NODE_API_CALL(env, napi_get_last_error_info(env, &error_info)); + + napi_value result; + bool is_ok = error_info->error_code == napi_ok; + NODE_API_CALL(env, napi_get_boolean(env, is_ok, &result)); + + return result; +} + +static napi_value testNapiTypeof(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_valuetype argument_type; + NODE_API_CALL(env, napi_typeof(env, args[0], &argument_type)); + + const char* name = NULL; + switch (argument_type) { + case napi_number: name = "number"; break; + case napi_string: name = "string"; break; + case napi_function: name = "function"; break; + case napi_object: name = "object"; break; + case napi_boolean: name = "boolean"; break; + case napi_undefined: name = "undefined"; break; + case napi_symbol: name = "symbol"; break; + case napi_null: name = "null"; break; + default: return NULL; + } + + napi_value result; + NODE_API_CALL( + env, napi_create_string_utf8(env, name, NAPI_AUTO_LENGTH, &result)); + return result; +} + +static bool deref_item_called = false; + +static void deref_item(node_api_basic_env env, void* data, void* hint) { + (void)hint; + + NODE_API_BASIC_ASSERT_RETURN_VOID( + data == &deref_item_called, + "Finalize callback was called with the correct pointer"); + + deref_item_called = true; +} + +static napi_value deref_item_was_called(napi_env env, napi_callback_info info) { + napi_value it_was_called; + + NODE_API_CALL(env, napi_get_boolean(env, deref_item_called, &it_was_called)); + + return it_was_called; +} + +static napi_value wrap_first_arg(napi_env env, + napi_callback_info info, + node_api_basic_finalize finalizer, + void* data) { + size_t argc = 1; + napi_value to_wrap; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &to_wrap, NULL, NULL)); + NODE_API_CALL(env, napi_wrap(env, to_wrap, data, finalizer, NULL, NULL)); + + return to_wrap; +} + +static napi_value wrap(napi_env env, napi_callback_info info) { + deref_item_called = false; + return wrap_first_arg(env, info, deref_item, &deref_item_called); +} + +static napi_value remove_wrap(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value wrapped; + void* data; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &wrapped, NULL, NULL)); + NODE_API_CALL(env, napi_remove_wrap(env, wrapped, &data)); + + return NULL; +} + +static bool finalize_called = false; + +static void test_finalize(node_api_basic_env env, void* data, void* hint) { + (void)env; + (void)data; + (void)hint; + + finalize_called = true; +} + +static napi_value test_finalize_wrap(napi_env env, napi_callback_info info) { + return wrap_first_arg(env, info, test_finalize, NULL); +} + +static napi_value finalize_was_called(napi_env env, napi_callback_info info) { + napi_value it_was_called; + + NODE_API_CALL(env, napi_get_boolean(env, finalize_called, &it_was_called)); + + return it_was_called; +} + +static napi_value testAdjustExternalMemory(napi_env env, + napi_callback_info info) { + napi_value result; + int64_t adjustedValue; + + NODE_API_CALL(env, napi_adjust_external_memory(env, 1, &adjustedValue)); + NODE_API_CALL(env, napi_create_double(env, (double)adjustedValue, &result)); + + return result; +} + +static napi_value testNapiRun(napi_env env, napi_callback_info info) { + napi_value script, result; + size_t argc = 1; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &script, NULL, NULL)); + NODE_API_CALL(env, napi_run_script(env, script, &result)); + + return result; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("testStrictEquals", testStrictEquals), + DECLARE_NODE_API_PROPERTY("testGetPrototype", testGetPrototype), + DECLARE_NODE_API_PROPERTY("testGetVersion", testGetVersion), + DECLARE_NODE_API_PROPERTY("testNapiRun", testNapiRun), + DECLARE_NODE_API_PROPERTY("getUndefined", getUndefined), + DECLARE_NODE_API_PROPERTY("getNull", getNull), + DECLARE_NODE_API_PROPERTY("createNapiError", createNapiError), + DECLARE_NODE_API_PROPERTY("testNapiErrorCleanup", testNapiErrorCleanup), + DECLARE_NODE_API_PROPERTY("testNapiTypeof", testNapiTypeof), + DECLARE_NODE_API_PROPERTY("wrap", wrap), + DECLARE_NODE_API_PROPERTY("removeWrap", remove_wrap), + DECLARE_NODE_API_PROPERTY("testFinalizeWrap", test_finalize_wrap), + DECLARE_NODE_API_PROPERTY("finalizeWasCalled", finalize_was_called), + DECLARE_NODE_API_PROPERTY("derefItemWasCalled", deref_item_was_called), + DECLARE_NODE_API_PROPERTY("testAdjustExternalMemory", + testAdjustExternalMemory)}; + + NODE_API_CALL( + env, + napi_define_properties( + env, exports, sizeof(descriptors) / sizeof(*descriptors), descriptors)); + + return exports; +} +EXTERN_C_END diff --git a/tests/js-native-api/test_general/test_general_set_prototype.c b/tests/js-native-api/test_general/test_general_set_prototype.c new file mode 100644 index 0000000..ad1621a --- /dev/null +++ b/tests/js-native-api/test_general/test_general_set_prototype.c @@ -0,0 +1,32 @@ +// node_api_set_prototype is experimental, so this addon is built separately +// from test_general.c: a runtime that lacks the symbol can still load the +// stable test_general addon. +#include +#include "../common.h" +#include "../entry_point.h" + +static napi_value testSetPrototype(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_CALL(env, node_api_set_prototype(env, args[0], args[1])); + + return NULL; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("testSetPrototype", testSetPrototype)}; + + NODE_API_CALL(env, + napi_define_properties(env, + exports, + sizeof(descriptors) / + sizeof(*descriptors), + descriptors)); + + return exports; +} +EXTERN_C_END