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
2 changes: 1 addition & 1 deletion PORTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ Tests covering the engine-specific part of Node-API, defined in `js_native_api.h
| `test_dataview` | Not ported | Medium |
| `test_date` | Ported ✅ | Easy |
| `test_error` | Ported ✅ | Medium |
| `test_exception` | Not ported | Medium |
| `test_exception` | Ported ✅ | Medium |
| `test_finalizer` | Not ported | Medium |
| `test_function` | Not ported | Medium |
| `test_general` | Not ported | Hard |
Expand Down
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export default defineConfig([
mustCall: "readonly",
mustNotCall: "readonly",
gcUntil: "readonly",
spawnTest: "readonly",
experimentalFeatures: "readonly",
},
},
Expand Down
26 changes: 12 additions & 14 deletions implementors/node/assert.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@

import {
ok,
strictEqual,
notStrictEqual,
deepStrictEqual,
throws,
match,
} from "node:assert/strict";

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),
},
);
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),
match: (string, regex, message) => match(string, regex, message),
});

Object.assign(globalThis, { assert });
66 changes: 66 additions & 0 deletions implementors/node/child_process.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { spawnSync } from "node:child_process";
import path from "node:path";

const ROOT_PATH = path.resolve(import.meta.dirname, "..", "..");
const FEATURES_MODULE_PATH = path.join(
ROOT_PATH,
"implementors",
"node",
"features.js",
);
const ASSERT_MODULE_PATH = path.join(
ROOT_PATH,
"implementors",
"node",
"assert.js",
);
const LOAD_ADDON_MODULE_PATH = path.join(
ROOT_PATH,
"implementors",
"node",
"load-addon.js",
);
const GC_MODULE_PATH = path.join(ROOT_PATH, "implementors", "node", "gc.js");
const MUST_CALL_MODULE_PATH = path.join(
ROOT_PATH,
"implementors",
"node",
"must-call.js",
);
const CHILD_PROCESS_MODULE_PATH = path.join(
ROOT_PATH,
"implementors",
"node",
"child_process.js",
);

const spawnTest = (filePath, options = {}) => {
const result = spawnSync(
process.execPath,
[
"--expose-gc",
"--import",
"file://" + FEATURES_MODULE_PATH,
"--import",
"file://" + ASSERT_MODULE_PATH,
"--import",
"file://" + LOAD_ADDON_MODULE_PATH,
"--import",
"file://" + GC_MODULE_PATH,
"--import",
"file://" + MUST_CALL_MODULE_PATH,
"--import",
"file://" + CHILD_PROCESS_MODULE_PATH,
filePath,
],
{ cwd: options.cwd || process.cwd() },
);
return {
status: result.status,
signal: result.signal,
stderr: result.stderr?.toString() ?? "",
stdout: result.stdout?.toString() ?? "",
};
};

Object.assign(globalThis, { spawnTest });
29 changes: 16 additions & 13 deletions implementors/node/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, "..", "..");
Expand All @@ -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 CHILD_PROCESS_MODULE_PATH = path.join(
ROOT_PATH,
"implementors",
"node",
"must-call.js"
"child_process.js",
);

export function listDirectoryEntries(dir: string) {
Expand All @@ -62,7 +63,7 @@ export function listDirectoryEntries(dir: string) {

export function runFileInSubprocess(
cwd: string,
filePath: string
filePath: string,
): Promise<void> {
return new Promise((resolve, reject) => {
const child = spawn(
Expand All @@ -80,9 +81,11 @@ export function runFileInSubprocess(
"file://" + GC_MODULE_PATH,
"--import",
"file://" + MUST_CALL_MODULE_PATH,
"--import",
"file://" + CHILD_PROCESS_MODULE_PATH,
filePath,
],
{ cwd }
{ cwd },
);

let stderrOutput = "";
Expand Down Expand Up @@ -111,9 +114,9 @@ export function runFileInSubprocess(
new Error(
`Test file ${path.relative(
TESTS_ROOT_PATH,
filePath
)} failed (${reason})${stderrSuffix}`
)
filePath,
)} failed (${reason})${stderrSuffix}`,
),
);
});
});
Expand Down
1 change: 1 addition & 0 deletions tests/js-native-api/test_exception/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
add_node_api_cts_addon(test_exception test_exception.c)
146 changes: 146 additions & 0 deletions tests/js-native-api/test_exception/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
"use strict";
// Flags: --expose-gc

const theError = new Error("Some error");

// The test module throws an error during Init, but in order for its exports to
// not be lost, it attaches them to the error's "bindings" property. This way,
// we can make sure that exceptions thrown during the module initialization
// phase are propagated through require() into JavaScript.
// https://github.com/nodejs/node/issues/19437
const test_exception = (function () {
let resultingException;
try {
loadAddon("test_exception");
} catch (anException) {
resultingException = anException;
}
assert.strictEqual(resultingException.message, "Error during Init");
return resultingException.binding;
})();

{
const throwTheError = () => {
throw theError;
};

// Test that the native side successfully captures the exception
let returnedError = test_exception.returnException(throwTheError);
assert.strictEqual(returnedError, theError);

// Test that the native side passes the exception through
assert.throws(
() => {
test_exception.allowException(throwTheError);
},
(err) => err === theError,
);

// Test that the exception thrown above was marked as pending
// before it was handled on the JS side
const exception_pending = test_exception.wasPending();
assert.strictEqual(
exception_pending,
true,
"Exception not pending as expected," +
` .wasPending() returned ${exception_pending}`,
);

// Test that the native side does not capture a non-existing exception
returnedError = test_exception.returnException(mustCall());
assert.strictEqual(
returnedError,
undefined,
"Returned error should be undefined when no exception is" +
` thrown, but ${returnedError} was passed`,
);
}

{
const throwTheError = class {
constructor() {
throw theError;
}
};

// Test that the native side successfully captures the exception
let returnedError = test_exception.constructReturnException(throwTheError);
assert.strictEqual(returnedError, theError);

// Test that the native side passes the exception through
assert.throws(
() => {
test_exception.constructAllowException(throwTheError);
},
(err) => err === theError,
);

// Test that the exception thrown above was marked as pending
// before it was handled on the JS side
const exception_pending = test_exception.wasPending();
assert.strictEqual(
exception_pending,
true,
"Exception not pending as expected," +
` .wasPending() returned ${exception_pending}`,
);

// Test that the native side does not capture a non-existing exception
returnedError = test_exception.constructReturnException(mustCall());
assert.strictEqual(
returnedError,
undefined,
"Returned error should be undefined when no exception is" +
` thrown, but ${returnedError} was passed`,
);
}

{
// Test that no exception appears that was not thrown by us
let caughtError;
try {
test_exception.allowException(mustCall());
} catch (anError) {
caughtError = anError;
}
assert.strictEqual(
caughtError,
undefined,
"No exception originated on the native side, but" +
` ${caughtError} was passed`,
);

// Test that the exception state remains clear when no exception is thrown
const exception_pending = test_exception.wasPending();
assert.strictEqual(
exception_pending,
false,
"Exception state did not remain clear as expected," +
` .wasPending() returned ${exception_pending}`,
);
}

{
// Test that no exception appears that was not thrown by us
let caughtError;
try {
test_exception.constructAllowException(mustCall());
} catch (anError) {
caughtError = anError;
}
assert.strictEqual(
caughtError,
undefined,
"No exception originated on the native side, but" +
` ${caughtError} was passed`,
);

// Test that the exception state remains clear when no exception is thrown
const exception_pending = test_exception.wasPending();
assert.strictEqual(
exception_pending,
false,
"Exception state did not remain clear as expected," +
` .wasPending() returned ${exception_pending}`,
);
}
5 changes: 5 additions & 0 deletions tests/js-native-api/test_exception/testFinalizerException.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// This test verifies that exceptions thrown from C finalizers during GC
// are propagated as uncaught exceptions (printed to stderr).
// It spawns a child process that triggers the finalizer and checks its stderr.
const result = spawnTest("testFinalizerException_child.mjs");
assert.match(result.stderr, /Error during Finalize/);
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// This file is spawned as a child process by testFinalizerException.js.
// It loads the addon, creates an external with a C finalizer that throws,
// then runs GC until the finalizer fires and crashes the process.
try {
loadAddon("test_exception");
} catch (anException) {
anException.binding.createExternal();
}

// Collect garbage 10 times. At least one of those should throw the exception
// and cause the whole process to bail with it, its text printed to stderr and
// asserted by the parent process to match expectations.
let gcCount = 10;
await gcUntil("test", () => --gcCount <= 0);
Loading
Loading