From ea315e83d8267bc267ad41a4c934efba8a4fe6b2 Mon Sep 17 00:00:00 2001 From: Balakrishna Avulapati Date: Mon, 10 Aug 2026 13:11:16 +0530 Subject: [PATCH 1/2] feat: port test_general core to CTS --- PORTING.md | 2 +- .../js-native-api/test_general/CMakeLists.txt | 1 + tests/js-native-api/test_general/test.js | 96 ++++++++ .../js-native-api/test_general/testGlobals.js | 4 + .../js-native-api/test_general/testNapiRun.js | 7 + .../test_general/testNapiStatus.js | 10 + .../js-native-api/test_general/test_general.c | 229 ++++++++++++++++++ 7 files changed, 348 insertions(+), 1 deletion(-) create mode 100644 tests/js-native-api/test_general/CMakeLists.txt create mode 100644 tests/js-native-api/test_general/test.js create mode 100644 tests/js-native-api/test_general/testGlobals.js create mode 100644 tests/js-native-api/test_general/testNapiRun.js create mode 100644 tests/js-native-api/test_general/testNapiStatus.js create mode 100644 tests/js-native-api/test_general/test_general.c 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/tests/js-native-api/test_general/CMakeLists.txt b/tests/js-native-api/test_general/CMakeLists.txt new file mode 100644 index 0000000..1d36e70 --- /dev/null +++ b/tests/js-native-api/test_general/CMakeLists.txt @@ -0,0 +1 @@ +add_node_api_cts_addon(test_general test_general.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/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 From 3bcecfa77e46dfacae83493ca05a8f74f9d37c5f Mon Sep 17 00:00:00 2001 From: Balakrishna Avulapati Date: Mon, 10 Aug 2026 13:17:54 +0530 Subject: [PATCH 2/2] feat: port test_general testEnvCleanup to CTS --- .../test_general/testEnvCleanup.js | 24 +++++++++++ .../test_general/testEnvCleanup_child.mjs | 41 +++++++++++++++++++ .../js-native-api/test_general/test_general.c | 29 +++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 tests/js-native-api/test_general/testEnvCleanup.js create mode 100644 tests/js-native-api/test_general/testEnvCleanup_child.mjs diff --git a/tests/js-native-api/test_general/testEnvCleanup.js b/tests/js-native-api/test_general/testEnvCleanup.js new file mode 100644 index 0000000..0789bcd --- /dev/null +++ b/tests/js-native-api/test_general/testEnvCleanup.js @@ -0,0 +1,24 @@ +// Wrap finalizers that survive until the environment is torn down only run as +// the environment goes away, so this needs a child process to observe. +if (!runtimeFeatures.spawn) { + skipTest(); +} + +const result = await spawnTest('testEnvCleanup_child.mjs'); + +assert.strictEqual( + result.status, + 0, + `child exited with status ${result.status}; stderr:\n${result.stderr}`, +); + +// The child wraps three objects and keeps them alive to teardown. Only two +// finalizers should fire: the plain wrap, and the second of the re-wrapped +// pair. The removed wrap must not report, and neither must the first wrap that +// was replaced. Order between the two is unspecified, so compare as a set. +const reported = result.stdout.split(/\r\n|\r|\n/).filter(Boolean).sort(); + +assert.deepStrictEqual(reported, [ + 'finalize at env cleanup for second wrap', + 'finalize at env cleanup for simple wrap', +]); diff --git a/tests/js-native-api/test_general/testEnvCleanup_child.mjs b/tests/js-native-api/test_general/testEnvCleanup_child.mjs new file mode 100644 index 0000000..a3a6e07 --- /dev/null +++ b/tests/js-native-api/test_general/testEnvCleanup_child.mjs @@ -0,0 +1,41 @@ +// Spawned by testEnvCleanup.js. Wraps objects, keeps them reachable until the +// process exits, and lets environment teardown run their finalizers. Each +// finalizer prints a line naming its case; the parent checks which lines +// appear. +const test_general = loadAddon('test_general'); + +// The second argument to envCleanupWrap() indexes a static string array on the +// native side. Reproduced here as a reverse mapping for clarity. +const finalizerMessages = { + 'simple wrap': 0, + 'wrap, removeWrap': 1, + 'first wrap': 2, + 'second wrap': 3, +}; + +// Held in module scope so nothing is collected before the process exits. +const kept = {}; + +// A plain wrap: its finalizer runs at teardown. +kept['simple wrap'] = test_general.envCleanupWrap( + {}, + finalizerMessages['simple wrap'], +); + +// A removed wrap: its finalizer must not run. +kept['wrap, removeWrap'] = test_general.envCleanupWrap( + {}, + finalizerMessages['wrap, removeWrap'], +); +test_general.removeWrap(kept['wrap, removeWrap']); + +// Re-wrapped: only the latest attached finalizer runs. +kept['first wrap'] = test_general.envCleanupWrap( + {}, + finalizerMessages['first wrap'], +); +test_general.removeWrap(kept['first wrap']); +test_general.envCleanupWrap( + kept['first wrap'], + finalizerMessages['second wrap'], +); diff --git a/tests/js-native-api/test_general/test_general.c b/tests/js-native-api/test_general/test_general.c index 03826ea..e415858 100644 --- a/tests/js-native-api/test_general/test_general.c +++ b/tests/js-native-api/test_general/test_general.c @@ -1,5 +1,6 @@ #include #include +#include #include "../common.h" #include "../entry_point.h" @@ -178,6 +179,33 @@ static napi_value finalize_was_called(napi_env env, napi_callback_info info) { return it_was_called; } +// Indexed by the second argument to envCleanupWrap(); testEnvCleanup_child.mjs +// keeps a matching reverse mapping. +static const char* env_cleanup_finalizer_messages[] = { + "simple wrap", "wrap, removeWrap", "first wrap", "second wrap"}; + +static void cleanup_env_finalizer(node_api_basic_env env, + void* data, + void* hint) { + (void)env; + (void)hint; + + printf("finalize at env cleanup for %s\n", + env_cleanup_finalizer_messages[(uintptr_t)data]); +} + +static napi_value env_cleanup_wrap(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value argv[2]; + uint32_t value; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL)); + NODE_API_CALL(env, napi_get_value_uint32(env, argv[1], &value)); + + return wrap_first_arg( + env, info, cleanup_env_finalizer, (void*)(uintptr_t)value); +} + static napi_value testAdjustExternalMemory(napi_env env, napi_callback_info info) { napi_value result; @@ -213,6 +241,7 @@ napi_value Init(napi_env env, napi_value exports) { DECLARE_NODE_API_PROPERTY("testNapiTypeof", testNapiTypeof), DECLARE_NODE_API_PROPERTY("wrap", wrap), DECLARE_NODE_API_PROPERTY("removeWrap", remove_wrap), + DECLARE_NODE_API_PROPERTY("envCleanupWrap", env_cleanup_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),