Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 96 additions & 67 deletions PORTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,87 +23,115 @@ in `js_native_api.h` and is therefore engine-agnostic.
## Difficulty Ratings

Difficulty is assessed on two axes:

- **Size/complexity** — total lines of C/C++ and JS across all source files
- **Runtime-API dependence** — pure `js_native_api.h` is cheapest; Node.js extensions and direct
libuv calls require harness work or Node-only scoping

| Rating | Meaning |
|---|---|
| Easy | Small test, pure `js_native_api.h` or trivial runtime API, straightforward 1:1 port |
| Medium | Moderate size or uses a Node.js extension API that the harness will need to abstract |
| Hard | Large test and/or deep libuv/worker/SEA dependency; may need new harness primitives or Node-only scoping |
| Rating | Meaning |
| ------ | -------------------------------------------------------------------------------------------------------- |
| Easy | Small test, pure `js_native_api.h` or trivial runtime API, straightforward 1:1 port |
| Medium | Moderate size or uses a Node.js extension API that the harness will need to abstract |
| Hard | Large test and/or deep libuv/worker/SEA dependency; may need new harness primitives or Node-only scoping |

## Engine-specific (`js-native-api`)

Tests covering the engine-specific part of Node-API, defined in `js_native_api.h`.

| Directory | Status | Difficulty |
|---|---|---|
| `2_function_arguments` | Ported | — |
| `3_callbacks` | Not ported | Easy |
| `4_object_factory` | Not ported | Easy |
| `5_function_factory` | Not ported | Easy |
| `6_object_wrap` | Not ported | Medium |
| `7_factory_wrap` | Not ported | Easy |
| `8_passing_wrapped` | Not ported | Easy |
| `test_array` | Not ported | Easy |
| `test_bigint` | Not ported | Easy |
| `test_cannot_run_js` | Not ported | Medium |
| `test_constructor` | Not ported | Medium |
| `test_conversions` | Not ported | Medium |
| `test_dataview` | Not ported | Easy |
| `test_date` | Not ported | Easy |
| `test_error` | Not ported | Medium |
| `test_exception` | Not ported | Medium |
| `test_finalizer` | Not ported | Medium |
| `test_function` | Not ported | Medium |
| `test_general` | Not ported | Hard |
| `test_handle_scope` | Not ported | Easy |
| `test_instance_data` | Not ported | Easy |
| `test_new_target` | Not ported | Easy |
| `test_number` | Not ported | Easy |
| `test_object` | Not ported | Hard |
| `test_promise` | Not ported | Easy |
| `test_properties` | Not ported | Easy |
| `test_reference` | Not ported | Medium |
| `test_reference_double_free` | Not ported | Easy |
| `test_sharedarraybuffer` | Not ported | Medium |
| `test_string` | Not ported | Medium |
| `test_symbol` | Not ported | Easy |
| `test_typedarray` | Not ported | Medium |
| Directory | Status | Difficulty |
| ---------------------------- | ---------- | ---------- |
| `2_function_arguments` | Ported | — |
| `3_callbacks` | Ported ✅ | Easy |
| `4_object_factory` | Ported ✅ | Easy |
| `5_function_factory` | Ported ✅ | Easy |
| `6_object_wrap` | Not ported | Medium |
| `7_factory_wrap` | Ported ✅ | Easy |
| `8_passing_wrapped` | Ported ✅ | Easy |
| `test_array` | Ported ✅ | Easy |
| `test_bigint` | Ported ✅ | Easy |
| `test_cannot_run_js` | Not ported | Medium |
| `test_constructor` | Not ported | Medium |
| `test_conversions` | Not ported | Medium |
| `test_dataview` | Not ported | Medium |
| `test_date` | Ported ✅ | Easy |
| `test_error` | Not ported | Medium |
| `test_exception` | Not ported | Medium |
| `test_finalizer` | Not ported | Medium |
| `test_function` | Not ported | Medium |
| `test_general` | Not ported | Hard |
| `test_handle_scope` | Ported ✅ | Easy |
| `test_instance_data` | Not ported | Medium |
| `test_new_target` | Ported ✅ | Easy |
| `test_number` | Ported ✅ | Easy |
| `test_object` | Not ported | Hard |
| `test_promise` | Ported ✅ | Easy |
| `test_properties` | Ported ✅ | Easy |
| `test_reference` | Not ported | Medium |
| `test_reference_double_free` | Ported ✅ | Easy |
| `test_sharedarraybuffer` | Not ported | Medium |
| `test_string` | Not ported | Medium |
| `test_symbol` | Ported ✅ | Easy |
| `test_typedarray` | Not ported | Medium |

## Runtime-specific (`node-api`)

Tests covering the runtime-specific part of Node-API, defined in `node_api.h`.

| Directory | Status | Difficulty |
|---|---|---|
| `1_hello_world` | Not ported | Easy |
| `test_async` | Not ported | Hard |
| `test_async_cleanup_hook` | Not ported | Hard |
| `test_async_context` | Not ported | Hard |
| `test_buffer` | Not ported | Medium |
| `test_callback_scope` | Not ported | Hard |
| `test_cleanup_hook` | Not ported | Medium |
| `test_env_teardown_gc` | Not ported | Easy |
| `test_exception` | Not ported | Easy |
| `test_fatal` | Not ported | Hard |
| `test_fatal_exception` | Not ported | Easy |
| `test_general` | Not ported | Medium |
| `test_init_order` | Not ported | Medium |
| `test_instance_data` | Not ported | Hard |
| `test_make_callback` | Not ported | Hard |
| `test_make_callback_recurse` | Not ported | Hard |
| `test_null_init` | Not ported | Medium |
| `test_reference_by_node_api_version` | Not ported | Medium |
| `test_sea_addon` | Not ported | Hard |
| `test_threadsafe_function` | Not ported | Hard |
| `test_threadsafe_function_shutdown` | Not ported | Hard |
| `test_uv_loop` | Not ported | Hard |
| `test_uv_threadpool_size` | Not ported | Hard |
| `test_worker_buffer_callback` | Not ported | Hard |
| `test_worker_terminate` | Not ported | Hard |
| `test_worker_terminate_finalization` | Not ported | Hard |
| Directory | Status | Difficulty |
| ------------------------------------ | ---------- | ---------- |
| `1_hello_world` | Not ported | Easy |
| `test_async` | Not ported | Hard |
| `test_async_cleanup_hook` | Not ported | Hard |
| `test_async_context` | Not ported | Hard |
| `test_buffer` | Not ported | Medium |
| `test_callback_scope` | Not ported | Hard |
| `test_cleanup_hook` | Not ported | Medium |
| `test_env_teardown_gc` | Not ported | Easy |
| `test_exception` | Not ported | Easy |
| `test_fatal` | Not ported | Hard |
| `test_fatal_exception` | Not ported | Easy |
| `test_general` | Not ported | Medium |
| `test_init_order` | Not ported | Medium |
| `test_instance_data` | Not ported | Hard |
| `test_make_callback` | Not ported | Hard |
| `test_make_callback_recurse` | Not ported | Hard |
| `test_null_init` | Not ported | Medium |
| `test_reference_by_node_api_version` | Not ported | Medium |
| `test_sea_addon` | Not ported | Hard |
| `test_threadsafe_function` | Not ported | Hard |
| `test_threadsafe_function_shutdown` | Not ported | Hard |
| `test_uv_loop` | Not ported | Hard |
| `test_uv_threadpool_size` | Not ported | Hard |
| `test_worker_buffer_callback` | Not ported | Hard |
| `test_worker_terminate` | Not ported | Hard |
| `test_worker_terminate_finalization` | Not ported | Hard |

## Experimental Node-API Features

Several tests in the upstream Node.js repository use experimental APIs that are guarded behind
`#ifdef NAPI_EXPERIMENTAL` in Node.js's `js_native_api.h`. The `node-api-headers` package (which
the CTS uses for compilation) does not currently include any of these experimental API declarations.

When `NAPI_EXPERIMENTAL` is defined in Node.js, `NAPI_VERSION` is set to
`NAPI_VERSION_EXPERIMENTAL (2147483647)`. The `NAPI_MODULE` macro exports this version, and the
runtime uses it to decide whether to enable experimental behavior for that addon.

| Feature macro | APIs | Used by |
| --------------------------------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------- |
| `NODE_API_EXPERIMENTAL_HAS_SHAREDARRAYBUFFER` | `node_api_is_sharedarraybuffer`, `node_api_create_sharedarraybuffer` | `test_dataview`, `test_sharedarraybuffer` |
| `NODE_API_EXPERIMENTAL_HAS_CREATE_OBJECT_WITH_PROPERTIES` | `node_api_create_object_with_properties` | `test_object` |
| `NODE_API_EXPERIMENTAL_HAS_SET_PROTOTYPE` | `node_api_set_prototype` | `test_general` |
| `NODE_API_EXPERIMENTAL_HAS_POST_FINALIZER` | `node_api_post_finalizer` | `test_general`, `test_finalizer`, `6_object_wrap` |

Tests that depend on these APIs are currently ported without the experimental test cases (marked
as "Partial" in the status column) or not ported at all. To fully support them, the CTS will need:

1. A mechanism for compiling addons with experimental API access (either from updated
`node-api-headers`, CTS-provided forward declarations or copies of headers from the Node.js main repository)
2. A way for implementors to declare which experimental features their runtime supports
3. Conditional test execution that skips experimental assertions on runtimes that don't support them

See [#26](https://github.com/nodejs/node-api-cts/issues/26) for the full design discussion.

## Special Considerations

Expand Down Expand Up @@ -144,6 +172,7 @@ The following tests call into libuv directly — `napi_get_uv_event_loop`, `uv_t
- `test_uv_loop`, `test_uv_threadpool_size`

Porting options:

1. **Node-only scope** — mark these tests as Node.js-only and skip on other runtimes.
2. **Harness abstraction** — introduce a minimal platform-agnostic threading/async API in the
harness (e.g., `cts_thread_create`, `cts_async_schedule`) that implementors back with their
Expand Down
46 changes: 46 additions & 0 deletions implementors/node/must-call.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const pendingCalls = [];

/**
* Wraps a function and asserts it is called exactly `exact` times before the
* process exits. If `fn` is omitted, a no-op function is used.
*
* Usage:
* promise.then(mustCall((result) => {
* assert.strictEqual(result, 42);
* }));
*/
const mustCall = (fn, exact = 1) => {
const entry = {
exact,
actual: 0,
name: fn?.name || "<anonymous>",
error: new Error(), // capture call-site stack
};
pendingCalls.push(entry);
return function(...args) {
entry.actual++;
if (fn) return fn.apply(this, args);
};
};

/**
* Returns a function that throws immediately if called.
*/
const mustNotCall = (msg) => {
return () => {
throw new Error(msg || "mustNotCall function was called");
};
};

process.on("exit", () => {
for (const entry of pendingCalls) {
if (entry.actual !== entry.exact) {
entry.error.message =
`mustCall "${entry.name}" expected ${entry.exact} call(s) ` +
`but got ${entry.actual}`;
throw entry.error;
}
}
});

Object.assign(globalThis, { mustCall, mustNotCall });
8 changes: 8 additions & 0 deletions implementors/node/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ const GC_MODULE_PATH = path.join(
"node",
"gc.js"
);
const MUST_CALL_MODULE_PATH = path.join(
ROOT_PATH,
"implementors",
"node",
"must-call.js"
);

export function listDirectoryEntries(dir: string) {
const entries = fs.readdirSync(dir, { withFileTypes: true });
Expand Down Expand Up @@ -64,6 +70,8 @@ export function runFileInSubprocess(
"file://" + LOAD_ADDON_MODULE_PATH,
"--import",
"file://" + GC_MODULE_PATH,
"--import",
"file://" + MUST_CALL_MODULE_PATH,
filePath,
],
{ cwd }
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"devDependencies": {
"@types/node": "^24.10.1",
"eslint": "^9.39.1",
"node-api-headers": "^1.7.0"
"node-api-headers": "^1.8.0"
},
"dependencies": {
"amaro": "^1.1.5"
Expand Down
70 changes: 70 additions & 0 deletions tests/harness/must-call.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// mustCall is a function
if (typeof mustCall !== 'function') {
throw new Error('Expected a global mustCall function');
}

// mustCall returns a wrapper function (not a tuple)
{
const wrapper = mustCall();
if (typeof wrapper !== 'function') {
throw new Error('mustCall() must return a function');
}
wrapper();
}

// mustCall forwards arguments and return value
{
const wrapper = mustCall((a, b) => a + b);
const result = wrapper(2, 3);
assert.strictEqual(result, 5);
}

// mustCall without fn argument works as a no-op wrapper
{
const wrapper = mustCall();
const result = wrapper('ignored');
assert.strictEqual(result, undefined);
}

// mustNotCall is a function
if (typeof mustNotCall !== 'function') {
throw new Error('Expected a global mustNotCall function');
}

// mustNotCall returns a function
{
const fn = mustNotCall();
if (typeof fn !== 'function') {
throw new Error('mustNotCall() must return a function');
}
}

// mustNotCall() throws when called
{
const fn = mustNotCall();
let threw = false;
try {
fn();
} catch {
threw = true;
}
if (!threw) throw new Error('mustNotCall() must throw when called');
}

// mustNotCall(msg) includes the message
{
const fn = mustNotCall('custom message');
let threw = false;
try {
fn();
} catch (error) {
threw = true;
if (!(error instanceof Error)) {
throw new Error('mustNotCall must throw an Error instance');
}
if (!error.message.includes('custom message')) {
throw new Error(`mustNotCall error must include custom message, got: "${error.message}"`);
}
}
if (!threw) throw new Error('mustNotCall(msg) must throw when called');
}
Loading