From fc1d77abc801079584634ddc658db95a9a6e54f0 Mon Sep 17 00:00:00 2001 From: Balakrishna Avulapati Date: Wed, 1 Apr 2026 14:59:29 +0530 Subject: [PATCH 1/3] port test_reference to CTS ports [test_reference](https://github.com/nodejs/node/tree/main/test/js-native-api/test_reference) from the Node.js test suite to the CTS. Signed-off-by: Balakrishna Avulapati --- eslint.config.js | 25 +- implementors/node/on-uncaught-exception.js | 5 + implementors/node/tests.ts | 31 ++- .../test_reference/CMakeLists.txt | 2 + tests/js-native-api/test_reference/test.js | 169 ++++++++++++ .../test_reference/test_finalizer.c | 71 +++++ .../test_reference/test_finalizer.js | 24 ++ .../test_reference/test_reference.c | 252 ++++++++++++++++++ 8 files changed, 560 insertions(+), 19 deletions(-) create mode 100644 implementors/node/on-uncaught-exception.js create mode 100644 tests/js-native-api/test_reference/CMakeLists.txt create mode 100644 tests/js-native-api/test_reference/test.js create mode 100644 tests/js-native-api/test_reference/test_finalizer.c create mode 100644 tests/js-native-api/test_reference/test_finalizer.js create mode 100644 tests/js-native-api/test_reference/test_reference.c diff --git a/eslint.config.js b/eslint.config.js index fff73c6..9258b3c 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -17,16 +17,29 @@ export default defineConfig([ mustNotCall: "readonly", gcUntil: "readonly", experimentalFeatures: "readonly", + onUncaughtException: "readonly", }, }, rules: { "no-undef": "error", - "no-restricted-imports": ["error", { - patterns: ["*"], - }], - "no-restricted-syntax": ["error", - { selector: "MemberExpression[object.name='globalThis']", message: "Avoid globalThis access in test files — use CTS harness globals instead" }, - { selector: "MemberExpression[object.name='global']", message: "Avoid global access in test files — use CTS harness globals instead" } + "no-restricted-imports": [ + "error", + { + patterns: ["*"], + }, + ], + "no-restricted-syntax": [ + "error", + { + selector: "MemberExpression[object.name='globalThis']", + message: + "Avoid globalThis access in test files — use CTS harness globals instead", + }, + { + selector: "MemberExpression[object.name='global']", + message: + "Avoid global access in test files — use CTS harness globals instead", + }, ], }, }, diff --git a/implementors/node/on-uncaught-exception.js b/implementors/node/on-uncaught-exception.js new file mode 100644 index 0000000..f25c157 --- /dev/null +++ b/implementors/node/on-uncaught-exception.js @@ -0,0 +1,5 @@ +const onUncaughtException = (cb) => { + process.on("uncaughtException", cb); +}; + +Object.assign(globalThis, { onUncaughtException }); diff --git a/implementors/node/tests.ts b/implementors/node/tests.ts index d4f8b2c..40066b9 100644 --- a/implementors/node/tests.ts +++ b/implementors/node/tests.ts @@ -5,7 +5,7 @@ import path from "node:path"; assert( typeof import.meta.dirname === "string", - "Expecting a recent Node.js runtime API version" + "Expecting a recent Node.js runtime API version", ); const ROOT_PATH = path.resolve(import.meta.dirname, "..", ".."); @@ -14,31 +14,32 @@ const FEATURES_MODULE_PATH = path.join( ROOT_PATH, "implementors", "node", - "features.js" + "features.js", ); const ASSERT_MODULE_PATH = path.join( ROOT_PATH, "implementors", "node", - "assert.js" + "assert.js", ); const LOAD_ADDON_MODULE_PATH = path.join( ROOT_PATH, "implementors", "node", - "load-addon.js" + "load-addon.js", ); -const GC_MODULE_PATH = path.join( +const GC_MODULE_PATH = path.join(ROOT_PATH, "implementors", "node", "gc.js"); +const MUST_CALL_MODULE_PATH = path.join( ROOT_PATH, "implementors", "node", - "gc.js" + "must-call.js", ); -const MUST_CALL_MODULE_PATH = path.join( +const ON_UNCAUGHT_EXCEPTION_MODULE_PATH = path.join( ROOT_PATH, "implementors", "node", - "must-call.js" + "on-uncaught-exception.js", ); export function listDirectoryEntries(dir: string) { @@ -62,7 +63,7 @@ export function listDirectoryEntries(dir: string) { export function runFileInSubprocess( cwd: string, - filePath: string + filePath: string, ): Promise { return new Promise((resolve, reject) => { const child = spawn( @@ -80,9 +81,13 @@ export function runFileInSubprocess( "file://" + GC_MODULE_PATH, "--import", "file://" + MUST_CALL_MODULE_PATH, + // test_finalizer needs this + "--force-node-api-uncaught-exceptions-policy", + "--import", + "file://" + ON_UNCAUGHT_EXCEPTION_MODULE_PATH, filePath, ], - { cwd } + { cwd }, ); let stderrOutput = ""; @@ -111,9 +116,9 @@ export function runFileInSubprocess( new Error( `Test file ${path.relative( TESTS_ROOT_PATH, - filePath - )} failed (${reason})${stderrSuffix}` - ) + filePath, + )} failed (${reason})${stderrSuffix}`, + ), ); }); }); diff --git a/tests/js-native-api/test_reference/CMakeLists.txt b/tests/js-native-api/test_reference/CMakeLists.txt new file mode 100644 index 0000000..5e66738 --- /dev/null +++ b/tests/js-native-api/test_reference/CMakeLists.txt @@ -0,0 +1,2 @@ +add_node_api_cts_addon(test_reference test_reference.c) +add_node_api_cts_addon(test_finalizer test_finalizer.c) diff --git a/tests/js-native-api/test_reference/test.js b/tests/js-native-api/test_reference/test.js new file mode 100644 index 0000000..2caaca6 --- /dev/null +++ b/tests/js-native-api/test_reference/test.js @@ -0,0 +1,169 @@ +"use strict"; +// Flags: --expose-gc + +const test_reference = loadAddon("test_reference"); + +// This test script uses external values with finalizer callbacks +// in order to track when values get garbage-collected. Each invocation +// of a finalizer callback increments the finalizeCount property. +assert.strictEqual(test_reference.finalizeCount, 0); + +// Run each test function in sequence, +// with an async delay and GC call between each. +async function runTests() { + (() => { + const symbol = test_reference.createSymbol("testSym"); + test_reference.createReference(symbol, 0); + assert.strictEqual(test_reference.referenceValue, symbol); + })(); + test_reference.deleteReference(); + + (() => { + const symbol = test_reference.createSymbolFor("testSymFor"); + test_reference.createReference(symbol, 0); + assert.strictEqual(test_reference.referenceValue, symbol); + })(); + test_reference.deleteReference(); + + (() => { + const symbol = test_reference.createSymbolFor("testSymFor"); + test_reference.createReference(symbol, 1); + assert.strictEqual(test_reference.referenceValue, symbol); + assert.strictEqual(test_reference.referenceValue, Symbol.for("testSymFor")); + })(); + test_reference.deleteReference(); + + (() => { + const symbol = test_reference.createSymbolForEmptyString(); + test_reference.createReference(symbol, 0); + assert.strictEqual(test_reference.referenceValue, Symbol.for("")); + })(); + test_reference.deleteReference(); + + (() => { + const symbol = test_reference.createSymbolForEmptyString(); + test_reference.createReference(symbol, 1); + assert.strictEqual(test_reference.referenceValue, symbol); + assert.strictEqual(test_reference.referenceValue, Symbol.for("")); + })(); + test_reference.deleteReference(); + + assert.throws( + () => test_reference.createSymbolForIncorrectLength(), + /Invalid argument/, + ); + + (() => { + const value = test_reference.createExternal(); + assert.strictEqual(test_reference.finalizeCount, 0); + assert.strictEqual(typeof value, "object"); + test_reference.checkExternal(value); + })(); + await gcUntil( + "External value without a finalizer", + () => test_reference.finalizeCount === 0, + ); + + (() => { + const value = test_reference.createExternalWithFinalize(); + assert.strictEqual(test_reference.finalizeCount, 0); + assert.strictEqual(typeof value, "object"); + test_reference.checkExternal(value); + })(); + await gcUntil( + "External value with a finalizer", + () => test_reference.finalizeCount === 1, + ); + + (() => { + const value = test_reference.createExternalWithFinalize(); + assert.strictEqual(test_reference.finalizeCount, 0); + test_reference.createReference(value, 0); + assert.strictEqual(test_reference.referenceValue, value); + })(); + // Value should be GC'd because there is only a weak ref + await gcUntil( + "Weak reference", + () => + test_reference.referenceValue === undefined && + test_reference.finalizeCount === 1, + ); + test_reference.deleteReference(); + + (() => { + const value = test_reference.createExternalWithFinalize(); + assert.strictEqual(test_reference.finalizeCount, 0); + test_reference.createReference(value, 1); + assert.strictEqual(test_reference.referenceValue, value); + })(); + // Value should NOT be GC'd because there is a strong ref + await gcUntil("Strong reference", () => test_reference.finalizeCount === 0); + test_reference.deleteReference(); + await gcUntil( + "Strong reference (cont.d)", + () => test_reference.finalizeCount === 1, + ); + + (() => { + const value = test_reference.createExternalWithFinalize(); + assert.strictEqual(test_reference.finalizeCount, 0); + test_reference.createReference(value, 1); + })(); + // Value should NOT be GC'd because there is a strong ref + await gcUntil( + "Strong reference, increment then decrement to weak reference", + () => test_reference.finalizeCount === 0, + ); + assert.strictEqual(test_reference.incrementRefcount(), 2); + // Value should NOT be GC'd because there is a strong ref + await gcUntil( + "Strong reference, increment then decrement to weak reference (cont.d-1)", + () => test_reference.finalizeCount === 0, + ); + assert.strictEqual(test_reference.decrementRefcount(), 1); + // Value should NOT be GC'd because there is a strong ref + await gcUntil( + "Strong reference, increment then decrement to weak reference (cont.d-2)", + () => test_reference.finalizeCount === 0, + ); + assert.strictEqual(test_reference.decrementRefcount(), 0); + // Value should be GC'd because the ref is now weak! + await gcUntil( + "Strong reference, increment then decrement to weak reference (cont.d-3)", + () => test_reference.finalizeCount === 1, + ); + test_reference.deleteReference(); + // Value was already GC'd + await gcUntil( + "Strong reference, increment then decrement to weak reference (cont.d-4)", + () => test_reference.finalizeCount === 1, + ); +} +runTests(); + +// This test creates a napi_ref on an object that has +// been wrapped by napi_wrap and for which the finalizer +// for the wrap calls napi_delete_ref on that napi_ref. +// +// Since both the wrap and the reference use the same +// object the finalizer for the wrap and reference +// may run in the same gc and in any order. +// +// It does that to validate that napi_delete_ref can be +// called before the finalizer has been run for the +// reference (there is a finalizer behind the scenes even +// though it cannot be passed to napi_create_reference). +// +// Since the order is not guaranteed, run the +// test a number of times maximize the chance that we +// get a run with the desired order for the test. +// +// 1000 reliably recreated the problem without the fix +// required to ensure delete could be called before +// the finalizer in manual testing. +for (let i = 0; i < 1000; i++) { + const wrapObject = new Object(); + test_reference.validateDeleteBeforeFinalize(wrapObject); + let gcCount = 1; + gcUntil("test", () => gcCount-- > 0); +} diff --git a/tests/js-native-api/test_reference/test_finalizer.c b/tests/js-native-api/test_reference/test_finalizer.c new file mode 100644 index 0000000..51492d9 --- /dev/null +++ b/tests/js-native-api/test_reference/test_finalizer.c @@ -0,0 +1,71 @@ +#include +#include +#include +#include "../common.h" +#include "../entry_point.h" + +static int test_value = 1; +static int finalize_count = 0; + +static void FinalizeExternalCallJs(napi_env env, void* data, void* hint) { + int* actual_value = data; + NODE_API_ASSERT_RETURN_VOID( + env, + actual_value == &test_value, + "The correct pointer was passed to the finalizer"); + + napi_ref finalizer_ref = (napi_ref)hint; + napi_value js_finalizer; + napi_value recv; + NODE_API_CALL_RETURN_VOID( + env, napi_get_reference_value(env, finalizer_ref, &js_finalizer)); + NODE_API_CALL_RETURN_VOID(env, napi_get_global(env, &recv)); + NODE_API_CALL_RETURN_VOID( + env, napi_call_function(env, recv, js_finalizer, 0, NULL, NULL)); + NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, finalizer_ref)); +} + +static napi_value CreateExternalWithJsFinalize(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)); + NODE_API_ASSERT(env, argc == 1, "Wrong number of arguments"); + napi_value finalizer = args[0]; + napi_valuetype finalizer_valuetype; + NODE_API_CALL(env, napi_typeof(env, finalizer, &finalizer_valuetype)); + NODE_API_ASSERT(env, + finalizer_valuetype == napi_function, + "Wrong type of first argument"); + napi_ref finalizer_ref; + NODE_API_CALL(env, napi_create_reference(env, finalizer, 1, &finalizer_ref)); + + napi_value result; + NODE_API_CALL(env, + napi_create_external(env, + &test_value, + FinalizeExternalCallJs, + finalizer_ref, /* finalize_hint */ + &result)); + + finalize_count = 0; + return result; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("createExternalWithJsFinalize", + CreateExternalWithJsFinalize), + }; + + 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_reference/test_finalizer.js b/tests/js-native-api/test_reference/test_finalizer.js new file mode 100644 index 0000000..b742c28 --- /dev/null +++ b/tests/js-native-api/test_reference/test_finalizer.js @@ -0,0 +1,24 @@ +"use strict"; +// Flags: --expose-gc --force-node-api-uncaught-exceptions-policy + +const binding = loadAddon("test_finalizer"); + +onUncaughtException( + mustCall((err) => { + assert.throws(() => { + throw err; + }, /finalizer error/); + }), +); + +(async function () { + { + binding.createExternalWithJsFinalize( + mustCall(() => { + throw new Error("finalizer error"); + }), + ); + } + let gcCount = 1; + await gcUntil("test", () => gcCount-- > 0); +})().then(mustCall()); diff --git a/tests/js-native-api/test_reference/test_reference.c b/tests/js-native-api/test_reference/test_reference.c new file mode 100644 index 0000000..058be07 --- /dev/null +++ b/tests/js-native-api/test_reference/test_reference.c @@ -0,0 +1,252 @@ +#define NAPI_VERSION 9 +#include +#include +#include +#include "../common.h" +#include "../entry_point.h" + +static int test_value = 1; +static int finalize_count = 0; +static napi_ref test_reference = NULL; + +static napi_value GetFinalizeCount(napi_env env, napi_callback_info info) { + napi_value result; + NODE_API_CALL(env, napi_create_int32(env, finalize_count, &result)); + return result; +} + +static void FinalizeExternal(napi_env env, void* data, void* hint) { + int *actual_value = data; + NODE_API_ASSERT_RETURN_VOID(env, actual_value == &test_value, + "The correct pointer was passed to the finalizer"); + finalize_count++; +} + +static napi_value CreateExternal(napi_env env, napi_callback_info info) { + int* data = &test_value; + + napi_value result; + NODE_API_CALL(env, + napi_create_external(env, + data, + NULL, /* finalize_cb */ + NULL, /* finalize_hint */ + &result)); + + finalize_count = 0; + return result; +} + +static napi_value CreateSymbol(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)); + NODE_API_ASSERT( + env, argc == 1, "Expect one argument only (symbol description)"); + + napi_value result_symbol; + + NODE_API_CALL(env, napi_create_symbol(env, args[0], &result_symbol)); + return result_symbol; +} + +static napi_value CreateSymbolFor(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + + char description[256]; + size_t description_length; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + NODE_API_ASSERT( + env, argc == 1, "Expect one argument only (symbol description)"); + + NODE_API_CALL( + env, + napi_get_value_string_utf8( + env, args[0], description, sizeof(description), &description_length)); + NODE_API_ASSERT(env, + description_length <= 255, + "Cannot accommodate descriptions longer than 255 bytes"); + + napi_value result_symbol; + + NODE_API_CALL(env, + node_api_symbol_for( + env, description, description_length, &result_symbol)); + return result_symbol; +} + +static napi_value CreateSymbolForEmptyString(napi_env env, napi_callback_info info) { + napi_value result_symbol; + NODE_API_CALL(env, node_api_symbol_for(env, NULL, 0, &result_symbol)); + return result_symbol; +} + +static napi_value CreateSymbolForIncorrectLength(napi_env env, napi_callback_info info) { + napi_value result_symbol; + NODE_API_CALL(env, node_api_symbol_for(env, NULL, 5, &result_symbol)); + return result_symbol; +} + +static napi_value +CreateExternalWithFinalize(napi_env env, napi_callback_info info) { + napi_value result; + NODE_API_CALL(env, + napi_create_external(env, + &test_value, + FinalizeExternal, + NULL, /* finalize_hint */ + &result)); + + finalize_count = 0; + return result; +} + +static napi_value CheckExternal(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value arg; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &arg, NULL, NULL)); + + NODE_API_ASSERT(env, argc == 1, "Expected one argument."); + + napi_valuetype argtype; + NODE_API_CALL(env, napi_typeof(env, arg, &argtype)); + + NODE_API_ASSERT(env, argtype == napi_external, "Expected an external value."); + + void* data; + NODE_API_CALL(env, napi_get_value_external(env, arg, &data)); + + NODE_API_ASSERT(env, data != NULL && *(int*)data == test_value, + "An external data value of 1 was expected."); + + return NULL; +} + +static napi_value CreateReference(napi_env env, napi_callback_info info) { + NODE_API_ASSERT(env, test_reference == NULL, + "The test allows only one reference at a time."); + + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + NODE_API_ASSERT(env, argc == 2, "Expected two arguments."); + + uint32_t initial_refcount; + NODE_API_CALL(env, napi_get_value_uint32(env, args[1], &initial_refcount)); + + NODE_API_CALL(env, + napi_create_reference(env, args[0], initial_refcount, &test_reference)); + + NODE_API_ASSERT(env, test_reference != NULL, + "A reference should have been created."); + + return NULL; +} + +static napi_value DeleteReference(napi_env env, napi_callback_info info) { + NODE_API_ASSERT(env, test_reference != NULL, + "A reference must have been created."); + + NODE_API_CALL(env, napi_delete_reference(env, test_reference)); + test_reference = NULL; + return NULL; +} + +static napi_value IncrementRefcount(napi_env env, napi_callback_info info) { + NODE_API_ASSERT(env, test_reference != NULL, + "A reference must have been created."); + + uint32_t refcount; + NODE_API_CALL(env, napi_reference_ref(env, test_reference, &refcount)); + + napi_value result; + NODE_API_CALL(env, napi_create_uint32(env, refcount, &result)); + return result; +} + +static napi_value DecrementRefcount(napi_env env, napi_callback_info info) { + NODE_API_ASSERT(env, test_reference != NULL, + "A reference must have been created."); + + uint32_t refcount; + NODE_API_CALL(env, napi_reference_unref(env, test_reference, &refcount)); + + napi_value result; + NODE_API_CALL(env, napi_create_uint32(env, refcount, &result)); + return result; +} + +static napi_value GetReferenceValue(napi_env env, napi_callback_info info) { + NODE_API_ASSERT(env, test_reference != NULL, + "A reference must have been created."); + + napi_value result; + NODE_API_CALL(env, napi_get_reference_value(env, test_reference, &result)); + return result; +} + +static void DeleteBeforeFinalizeFinalizer( + napi_env env, void* finalize_data, void* finalize_hint) { + napi_ref* ref = (napi_ref*)finalize_data; + napi_value value; + assert(napi_get_reference_value(env, *ref, &value) == napi_ok); + assert(value == NULL); + napi_delete_reference(env, *ref); + free(ref); +} + +static napi_value ValidateDeleteBeforeFinalize(napi_env env, napi_callback_info info) { + napi_value wrapObject; + size_t argc = 1; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &wrapObject, NULL, NULL)); + + napi_ref* ref_t = malloc(sizeof(napi_ref)); + NODE_API_CALL(env, + napi_wrap( + env, wrapObject, ref_t, DeleteBeforeFinalizeFinalizer, NULL, NULL)); + + // Create a reference that will be eligible for collection at the same + // time as the wrapped object by passing in the same wrapObject. + // This means that the FinalizeOrderValidation callback may be run + // before the finalizer for the newly created reference (there is a finalizer + // behind the scenes even though it cannot be passed to napi_create_reference) + // The Finalizer for the wrap (which is different than the finalizer + // for the reference) calls napi_delete_reference validating that + // napi_delete_reference can be called before the finalizer for the + // reference runs. + NODE_API_CALL(env, napi_create_reference(env, wrapObject, 0, ref_t)); + return wrapObject; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_GETTER("finalizeCount", GetFinalizeCount), + DECLARE_NODE_API_PROPERTY("createExternal", CreateExternal), + DECLARE_NODE_API_PROPERTY("createExternalWithFinalize", + CreateExternalWithFinalize), + DECLARE_NODE_API_PROPERTY("checkExternal", CheckExternal), + DECLARE_NODE_API_PROPERTY("createReference", CreateReference), + DECLARE_NODE_API_PROPERTY("createSymbol", CreateSymbol), + DECLARE_NODE_API_PROPERTY("createSymbolFor", CreateSymbolFor), + DECLARE_NODE_API_PROPERTY("createSymbolForEmptyString", + CreateSymbolForEmptyString), + DECLARE_NODE_API_PROPERTY("createSymbolForIncorrectLength", + CreateSymbolForIncorrectLength), + DECLARE_NODE_API_PROPERTY("deleteReference", DeleteReference), + DECLARE_NODE_API_PROPERTY("incrementRefcount", IncrementRefcount), + DECLARE_NODE_API_PROPERTY("decrementRefcount", DecrementRefcount), + DECLARE_NODE_API_GETTER("referenceValue", GetReferenceValue), + DECLARE_NODE_API_PROPERTY("validateDeleteBeforeFinalize", + ValidateDeleteBeforeFinalize), + }; + + NODE_API_CALL(env, napi_define_properties( + env, exports, sizeof(descriptors) / sizeof(*descriptors), descriptors)); + + return exports; +} +EXTERN_C_END From c0334454f8a7aad999e33bffb0e85cd87d2f9f9c Mon Sep 17 00:00:00 2001 From: Balakrishna Avulapati Date: Sat, 4 Apr 2026 12:44:23 +0530 Subject: [PATCH 2/3] flip the gcUntil condition Signed-off-by: Balakrishna Avulapati --- tests/js-native-api/test_reference/test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/js-native-api/test_reference/test.js b/tests/js-native-api/test_reference/test.js index 2caaca6..ebbf02b 100644 --- a/tests/js-native-api/test_reference/test.js +++ b/tests/js-native-api/test_reference/test.js @@ -165,5 +165,5 @@ for (let i = 0; i < 1000; i++) { const wrapObject = new Object(); test_reference.validateDeleteBeforeFinalize(wrapObject); let gcCount = 1; - gcUntil("test", () => gcCount-- > 0); + gcUntil("test", () => gcCount-- <= 0); } From 0d6c4b7e9b12f21f62847fb4668d1a87348fb876 Mon Sep 17 00:00:00 2001 From: Balakrishna Avulapati Date: Tue, 7 Apr 2026 11:45:08 +0530 Subject: [PATCH 3/3] expose a harness gc() wrapper Signed-off-by: Balakrishna Avulapati --- eslint.config.js | 1 + implementors/node/gc.js | 17 +++++++++++++++-- tests/harness/gc.js | 7 +++++++ tests/js-native-api/test_reference/test.js | 3 +-- .../test_reference/test_finalizer.js | 3 +-- 5 files changed, 25 insertions(+), 6 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 9258b3c..5bafb36 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -15,6 +15,7 @@ export default defineConfig([ loadAddon: "readonly", mustCall: "readonly", mustNotCall: "readonly", + gc: "readonly", gcUntil: "readonly", experimentalFeatures: "readonly", onUncaughtException: "readonly", diff --git a/implementors/node/gc.js b/implementors/node/gc.js index 791cb73..21c4a53 100644 --- a/implementors/node/gc.js +++ b/implementors/node/gc.js @@ -1,13 +1,26 @@ +// Capture the engine-provided gc (Node exposes it under --expose-gc) before +// we overwrite globalThis.gc with the harness wrapper below. +const engineGc = globalThis.gc; +if (typeof engineGc !== "function") { + throw new Error( + "Node harness expects globalThis.gc to be available (run with --expose-gc)", + ); +} + +const gc = () => { + engineGc(); +}; + const gcUntil = async (name, condition) => { let count = 0; while (!condition()) { await new Promise((resolve) => setImmediate(resolve)); if (++count < 10) { - globalThis.gc(); + engineGc(); } else { throw new Error(`GC test "${name}" failed after ${count} attempts`); } } }; -Object.assign(globalThis, { gcUntil }); +Object.assign(globalThis, { gc, gcUntil }); diff --git a/tests/harness/gc.js b/tests/harness/gc.js index de7d570..4466236 100644 --- a/tests/harness/gc.js +++ b/tests/harness/gc.js @@ -1,7 +1,14 @@ +if (typeof gc !== 'function') { + throw new Error('Expected a global gc function'); +} + if (typeof gcUntil !== 'function') { throw new Error('Expected a global gcUntil function'); } +// gc should run synchronously without throwing +gc(); + // gcUntil should resolve once the condition becomes true let count = 0; await gcUntil('test-passes', () => { diff --git a/tests/js-native-api/test_reference/test.js b/tests/js-native-api/test_reference/test.js index ebbf02b..58d5fb8 100644 --- a/tests/js-native-api/test_reference/test.js +++ b/tests/js-native-api/test_reference/test.js @@ -164,6 +164,5 @@ runTests(); for (let i = 0; i < 1000; i++) { const wrapObject = new Object(); test_reference.validateDeleteBeforeFinalize(wrapObject); - let gcCount = 1; - gcUntil("test", () => gcCount-- <= 0); + gc(); } diff --git a/tests/js-native-api/test_reference/test_finalizer.js b/tests/js-native-api/test_reference/test_finalizer.js index b742c28..4bc9975 100644 --- a/tests/js-native-api/test_reference/test_finalizer.js +++ b/tests/js-native-api/test_reference/test_finalizer.js @@ -19,6 +19,5 @@ onUncaughtException( }), ); } - let gcCount = 1; - await gcUntil("test", () => gcCount-- > 0); + gc(); })().then(mustCall());