From 2b4c2df087e0739e82f6ce07e4a8cf78d206f288 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sat, 28 Feb 2026 13:45:06 +0100 Subject: [PATCH] feat: extend harness with assert methods, gcUntil, and --expose-gc Adds the following to the Node.js implementor and harness in preparation for porting the easy js-native-api tests: - assert.js: expose assert.ok, .strictEqual, .notStrictEqual, .deepStrictEqual, and .throws as methods on the global assert object - gc.js: new module providing a global gcUntil(name, condition) helper that drives GC until a condition is met (needed for finalizer tests) - tests.ts: inject --expose-gc and gc.js into every test subprocess - CMakeLists.txt: broaden add_node_api_cts_addon() to accept multiple source files via ARGN (needed for multi-file addons) - tests/harness/assert.js: exercise all new assert methods - tests/harness/gc.js: exercise gcUntil pass and failure paths Co-Authored-By: Claude Sonnet 4.6 --- CMakeLists.txt | 4 +-- implementors/node/assert.js | 24 +++++++++++++++--- implementors/node/gc.js | 13 ++++++++++ implementors/node/tests.ts | 9 +++++++ tests/harness/assert.js | 50 +++++++++++++++++++++++++++++++++++++ tests/harness/gc.js | 27 ++++++++++++++++++++ 6 files changed, 121 insertions(+), 6 deletions(-) create mode 100644 implementors/node/gc.js create mode 100644 tests/harness/gc.js diff --git a/CMakeLists.txt b/CMakeLists.txt index a5a33c7..9b812d1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,8 +13,8 @@ if(MSVC) execute_process(COMMAND ${CMAKE_AR} /def:${NODE_API_DEF} /out:${NODE_API_LIB} ${CMAKE_STATIC_LINKER_FLAGS}) endif() -function(add_node_api_cts_addon ADDON_NAME SRC) - add_library(${ADDON_NAME} SHARED ${SRC}) +function(add_node_api_cts_addon ADDON_NAME) + add_library(${ADDON_NAME} SHARED ${ARGN}) set_target_properties(${ADDON_NAME} PROPERTIES PREFIX "" SUFFIX ".node" diff --git a/implementors/node/assert.js b/implementors/node/assert.js index 8a5039f..de9934c 100644 --- a/implementors/node/assert.js +++ b/implementors/node/assert.js @@ -1,8 +1,24 @@ -import { ok } from "node:assert/strict"; +import { + ok, + strictEqual, + notStrictEqual, + deepStrictEqual, + throws, +} from "node:assert/strict"; -const assert = (value, message) => { - ok(value, message); -}; +const assert = Object.assign( + (value, message) => ok(value, message), + { + ok: (value, message) => ok(value, message), + strictEqual: (actual, expected, message) => + strictEqual(actual, expected, message), + notStrictEqual: (actual, expected, message) => + notStrictEqual(actual, expected, message), + deepStrictEqual: (actual, expected, message) => + deepStrictEqual(actual, expected, message), + throws: (fn, error, message) => throws(fn, error, message), + }, +); Object.assign(globalThis, { assert }); diff --git a/implementors/node/gc.js b/implementors/node/gc.js new file mode 100644 index 0000000..791cb73 --- /dev/null +++ b/implementors/node/gc.js @@ -0,0 +1,13 @@ +const gcUntil = async (name, condition) => { + let count = 0; + while (!condition()) { + await new Promise((resolve) => setImmediate(resolve)); + if (++count < 10) { + globalThis.gc(); + } else { + throw new Error(`GC test "${name}" failed after ${count} attempts`); + } + } +}; + +Object.assign(globalThis, { gcUntil }); diff --git a/implementors/node/tests.ts b/implementors/node/tests.ts index 28dfa54..d39c64b 100644 --- a/implementors/node/tests.ts +++ b/implementors/node/tests.ts @@ -22,6 +22,12 @@ const LOAD_ADDON_MODULE_PATH = path.join( "node", "load-addon.js" ); +const GC_MODULE_PATH = path.join( + ROOT_PATH, + "implementors", + "node", + "gc.js" +); export function listDirectoryEntries(dir: string) { const entries = fs.readdirSync(dir, { withFileTypes: true }); @@ -51,10 +57,13 @@ export function runFileInSubprocess( process.execPath, [ // Using file scheme prefix when to enable imports on Windows + "--expose-gc", "--import", "file://" + ASSERT_MODULE_PATH, "--import", "file://" + LOAD_ADDON_MODULE_PATH, + "--import", + "file://" + GC_MODULE_PATH, filePath, ], { cwd } diff --git a/tests/harness/assert.js b/tests/harness/assert.js index 47b89f3..9abf943 100644 --- a/tests/harness/assert.js +++ b/tests/harness/assert.js @@ -31,3 +31,53 @@ try { if (!threw) { throw new Error('Global assert(false, message) must throw'); } + +// assert.ok +if (typeof assert.ok !== 'function') { + throw new Error('Expected assert.ok to be a function'); +} +assert.ok(true); +threw = false; +try { assert.ok(false); } catch { threw = true; } +if (!threw) throw new Error('assert.ok(false) must throw'); + +// assert.strictEqual +if (typeof assert.strictEqual !== 'function') { + throw new Error('Expected assert.strictEqual to be a function'); +} +assert.strictEqual(1, 1); +assert.strictEqual('a', 'a'); +assert.strictEqual(NaN, NaN); // uses Object.is semantics +threw = false; +try { assert.strictEqual(1, 2); } catch { threw = true; } +if (!threw) throw new Error('assert.strictEqual(1, 2) must throw'); + +// assert.notStrictEqual +if (typeof assert.notStrictEqual !== 'function') { + throw new Error('Expected assert.notStrictEqual to be a function'); +} +assert.notStrictEqual(1, 2); +assert.notStrictEqual('a', 'b'); +threw = false; +try { assert.notStrictEqual(1, 1); } catch { threw = true; } +if (!threw) throw new Error('assert.notStrictEqual(1, 1) must throw'); + +// assert.deepStrictEqual +if (typeof assert.deepStrictEqual !== 'function') { + throw new Error('Expected assert.deepStrictEqual to be a function'); +} +assert.deepStrictEqual({ a: 1 }, { a: 1 }); +assert.deepStrictEqual([1, 2, 3], [1, 2, 3]); +threw = false; +try { assert.deepStrictEqual({ a: 1 }, { a: 2 }); } catch { threw = true; } +if (!threw) throw new Error('assert.deepStrictEqual({ a: 1 }, { a: 2 }) must throw'); + +// assert.throws +if (typeof assert.throws !== 'function') { + throw new Error('Expected assert.throws to be a function'); +} +assert.throws(() => { throw new Error('oops'); }, /oops/); +assert.throws(() => { throw new TypeError('bad'); }, TypeError); +threw = false; +try { assert.throws(() => { /* does not throw */ }); } catch { threw = true; } +if (!threw) throw new Error('assert.throws must throw when fn does not throw'); diff --git a/tests/harness/gc.js b/tests/harness/gc.js new file mode 100644 index 0000000..de7d570 --- /dev/null +++ b/tests/harness/gc.js @@ -0,0 +1,27 @@ +if (typeof gcUntil !== 'function') { + throw new Error('Expected a global gcUntil function'); +} + +// gcUntil should resolve once the condition becomes true +let count = 0; +await gcUntil('test-passes', () => { + count++; + return count >= 2; +}); +if (count < 2) { + throw new Error(`Expected condition to be checked at least twice, got ${count}`); +} + +// gcUntil should throw after exhausting retries when condition never becomes true +let threw = false; +try { + await gcUntil('test-fails', () => false); +} catch (error) { + threw = true; + if (!error.message.includes('test-fails')) { + throw new Error(`Expected error message to include 'test-fails' but got: ${error.message}`); + } +} +if (!threw) { + throw new Error('gcUntil must throw when the condition never becomes true'); +}