Skip to content

Commit 48e4370

Browse files
committed
feat: port test_exception to CTS
ports [test_exception](https://github.com/nodejs/node/tree/main/test/js-native-api/test_exception) from Node.js test suite to the CTS. Signed-off-by: Balakrishna Avulapati <ba@bavulapati.com>
1 parent 57dba59 commit 48e4370

9 files changed

Lines changed: 377 additions & 27 deletions

File tree

eslint.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export default defineConfig([
1616
mustCall: "readonly",
1717
mustNotCall: "readonly",
1818
gcUntil: "readonly",
19+
spawnTest: "readonly",
1920
experimentalFeatures: "readonly",
2021
},
2122
},

implementors/node/assert.js

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,22 @@
1-
21
import {
32
ok,
43
strictEqual,
54
notStrictEqual,
65
deepStrictEqual,
76
throws,
7+
match,
88
} from "node:assert/strict";
99

10-
const assert = Object.assign(
11-
(value, message) => ok(value, message),
12-
{
13-
ok: (value, message) => ok(value, message),
14-
strictEqual: (actual, expected, message) =>
15-
strictEqual(actual, expected, message),
16-
notStrictEqual: (actual, expected, message) =>
17-
notStrictEqual(actual, expected, message),
18-
deepStrictEqual: (actual, expected, message) =>
19-
deepStrictEqual(actual, expected, message),
20-
throws: (fn, error, message) => throws(fn, error, message),
21-
},
22-
);
10+
const assert = Object.assign((value, message) => ok(value, message), {
11+
ok: (value, message) => ok(value, message),
12+
strictEqual: (actual, expected, message) =>
13+
strictEqual(actual, expected, message),
14+
notStrictEqual: (actual, expected, message) =>
15+
notStrictEqual(actual, expected, message),
16+
deepStrictEqual: (actual, expected, message) =>
17+
deepStrictEqual(actual, expected, message),
18+
throws: (fn, error, message) => throws(fn, error, message),
19+
match: (string, regex, message) => match(string, regex, message),
20+
});
2321

2422
Object.assign(globalThis, { assert });

implementors/node/child_process.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { spawnSync } from "node:child_process";
2+
import path from "node:path";
3+
4+
const ROOT_PATH = path.resolve(import.meta.dirname, "..", "..");
5+
const FEATURES_MODULE_PATH = path.join(
6+
ROOT_PATH,
7+
"implementors",
8+
"node",
9+
"features.js",
10+
);
11+
const ASSERT_MODULE_PATH = path.join(
12+
ROOT_PATH,
13+
"implementors",
14+
"node",
15+
"assert.js",
16+
);
17+
const LOAD_ADDON_MODULE_PATH = path.join(
18+
ROOT_PATH,
19+
"implementors",
20+
"node",
21+
"load-addon.js",
22+
);
23+
const GC_MODULE_PATH = path.join(ROOT_PATH, "implementors", "node", "gc.js");
24+
const MUST_CALL_MODULE_PATH = path.join(
25+
ROOT_PATH,
26+
"implementors",
27+
"node",
28+
"must-call.js",
29+
);
30+
const CHILD_PROCESS_MODULE_PATH = path.join(
31+
ROOT_PATH,
32+
"implementors",
33+
"node",
34+
"child_process.js",
35+
);
36+
37+
const spawnTest = (filePath, options = {}) => {
38+
const result = spawnSync(
39+
process.execPath,
40+
[
41+
"--expose-gc",
42+
"--import",
43+
"file://" + FEATURES_MODULE_PATH,
44+
"--import",
45+
"file://" + ASSERT_MODULE_PATH,
46+
"--import",
47+
"file://" + LOAD_ADDON_MODULE_PATH,
48+
"--import",
49+
"file://" + GC_MODULE_PATH,
50+
"--import",
51+
"file://" + MUST_CALL_MODULE_PATH,
52+
"--import",
53+
"file://" + CHILD_PROCESS_MODULE_PATH,
54+
filePath,
55+
],
56+
{ cwd: options.cwd || process.cwd() },
57+
);
58+
return {
59+
status: result.status,
60+
signal: result.signal,
61+
stderr: result.stderr?.toString() ?? "",
62+
stdout: result.stdout?.toString() ?? "",
63+
};
64+
};
65+
66+
Object.assign(globalThis, { spawnTest });

implementors/node/tests.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import path from "node:path";
55

66
assert(
77
typeof import.meta.dirname === "string",
8-
"Expecting a recent Node.js runtime API version"
8+
"Expecting a recent Node.js runtime API version",
99
);
1010

1111
const ROOT_PATH = path.resolve(import.meta.dirname, "..", "..");
@@ -14,31 +14,32 @@ const FEATURES_MODULE_PATH = path.join(
1414
ROOT_PATH,
1515
"implementors",
1616
"node",
17-
"features.js"
17+
"features.js",
1818
);
1919
const ASSERT_MODULE_PATH = path.join(
2020
ROOT_PATH,
2121
"implementors",
2222
"node",
23-
"assert.js"
23+
"assert.js",
2424
);
2525
const LOAD_ADDON_MODULE_PATH = path.join(
2626
ROOT_PATH,
2727
"implementors",
2828
"node",
29-
"load-addon.js"
29+
"load-addon.js",
3030
);
31-
const GC_MODULE_PATH = path.join(
31+
const GC_MODULE_PATH = path.join(ROOT_PATH, "implementors", "node", "gc.js");
32+
const MUST_CALL_MODULE_PATH = path.join(
3233
ROOT_PATH,
3334
"implementors",
3435
"node",
35-
"gc.js"
36+
"must-call.js",
3637
);
37-
const MUST_CALL_MODULE_PATH = path.join(
38+
const CHILD_PROCESS_MODULE_PATH = path.join(
3839
ROOT_PATH,
3940
"implementors",
4041
"node",
41-
"must-call.js"
42+
"child_process.js",
4243
);
4344

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

6364
export function runFileInSubprocess(
6465
cwd: string,
65-
filePath: string
66+
filePath: string,
6667
): Promise<void> {
6768
return new Promise((resolve, reject) => {
6869
const child = spawn(
@@ -80,9 +81,11 @@ export function runFileInSubprocess(
8081
"file://" + GC_MODULE_PATH,
8182
"--import",
8283
"file://" + MUST_CALL_MODULE_PATH,
84+
"--import",
85+
"file://" + CHILD_PROCESS_MODULE_PATH,
8386
filePath,
8487
],
85-
{ cwd }
88+
{ cwd },
8689
);
8790

8891
let stderrOutput = "";
@@ -111,9 +114,9 @@ export function runFileInSubprocess(
111114
new Error(
112115
`Test file ${path.relative(
113116
TESTS_ROOT_PATH,
114-
filePath
115-
)} failed (${reason})${stderrSuffix}`
116-
)
117+
filePath,
118+
)} failed (${reason})${stderrSuffix}`,
119+
),
117120
);
118121
});
119122
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
add_node_api_cts_addon(test_exception test_exception.c)
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
"use strict";
2+
// Flags: --expose-gc
3+
4+
const theError = new Error("Some error");
5+
6+
// The test module throws an error during Init, but in order for its exports to
7+
// not be lost, it attaches them to the error's "bindings" property. This way,
8+
// we can make sure that exceptions thrown during the module initialization
9+
// phase are propagated through require() into JavaScript.
10+
// https://github.com/nodejs/node/issues/19437
11+
const test_exception = (function () {
12+
let resultingException;
13+
try {
14+
loadAddon("test_exception");
15+
} catch (anException) {
16+
resultingException = anException;
17+
}
18+
assert.strictEqual(resultingException.message, "Error during Init");
19+
return resultingException.binding;
20+
})();
21+
22+
{
23+
const throwTheError = () => {
24+
throw theError;
25+
};
26+
27+
// Test that the native side successfully captures the exception
28+
let returnedError = test_exception.returnException(throwTheError);
29+
assert.strictEqual(returnedError, theError);
30+
31+
// Test that the native side passes the exception through
32+
assert.throws(
33+
() => {
34+
test_exception.allowException(throwTheError);
35+
},
36+
(err) => err === theError,
37+
);
38+
39+
// Test that the exception thrown above was marked as pending
40+
// before it was handled on the JS side
41+
const exception_pending = test_exception.wasPending();
42+
assert.strictEqual(
43+
exception_pending,
44+
true,
45+
"Exception not pending as expected," +
46+
` .wasPending() returned ${exception_pending}`,
47+
);
48+
49+
// Test that the native side does not capture a non-existing exception
50+
returnedError = test_exception.returnException(mustCall());
51+
assert.strictEqual(
52+
returnedError,
53+
undefined,
54+
"Returned error should be undefined when no exception is" +
55+
` thrown, but ${returnedError} was passed`,
56+
);
57+
}
58+
59+
{
60+
const throwTheError = class {
61+
constructor() {
62+
throw theError;
63+
}
64+
};
65+
66+
// Test that the native side successfully captures the exception
67+
let returnedError = test_exception.constructReturnException(throwTheError);
68+
assert.strictEqual(returnedError, theError);
69+
70+
// Test that the native side passes the exception through
71+
assert.throws(
72+
() => {
73+
test_exception.constructAllowException(throwTheError);
74+
},
75+
(err) => err === theError,
76+
);
77+
78+
// Test that the exception thrown above was marked as pending
79+
// before it was handled on the JS side
80+
const exception_pending = test_exception.wasPending();
81+
assert.strictEqual(
82+
exception_pending,
83+
true,
84+
"Exception not pending as expected," +
85+
` .wasPending() returned ${exception_pending}`,
86+
);
87+
88+
// Test that the native side does not capture a non-existing exception
89+
returnedError = test_exception.constructReturnException(mustCall());
90+
assert.strictEqual(
91+
returnedError,
92+
undefined,
93+
"Returned error should be undefined when no exception is" +
94+
` thrown, but ${returnedError} was passed`,
95+
);
96+
}
97+
98+
{
99+
// Test that no exception appears that was not thrown by us
100+
let caughtError;
101+
try {
102+
test_exception.allowException(mustCall());
103+
} catch (anError) {
104+
caughtError = anError;
105+
}
106+
assert.strictEqual(
107+
caughtError,
108+
undefined,
109+
"No exception originated on the native side, but" +
110+
` ${caughtError} was passed`,
111+
);
112+
113+
// Test that the exception state remains clear when no exception is thrown
114+
const exception_pending = test_exception.wasPending();
115+
assert.strictEqual(
116+
exception_pending,
117+
false,
118+
"Exception state did not remain clear as expected," +
119+
` .wasPending() returned ${exception_pending}`,
120+
);
121+
}
122+
123+
{
124+
// Test that no exception appears that was not thrown by us
125+
let caughtError;
126+
try {
127+
test_exception.constructAllowException(mustCall());
128+
} catch (anError) {
129+
caughtError = anError;
130+
}
131+
assert.strictEqual(
132+
caughtError,
133+
undefined,
134+
"No exception originated on the native side, but" +
135+
` ${caughtError} was passed`,
136+
);
137+
138+
// Test that the exception state remains clear when no exception is thrown
139+
const exception_pending = test_exception.wasPending();
140+
assert.strictEqual(
141+
exception_pending,
142+
false,
143+
"Exception state did not remain clear as expected," +
144+
` .wasPending() returned ${exception_pending}`,
145+
);
146+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// This test verifies that exceptions thrown from C finalizers during GC
2+
// are propagated as uncaught exceptions (printed to stderr).
3+
// It spawns a child process that triggers the finalizer and checks its stderr.
4+
const result = spawnTest("testFinalizerException_child.mjs");
5+
assert.match(result.stderr, /Error during Finalize/);
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// This file is spawned as a child process by testFinalizerException.js.
2+
// It loads the addon, creates an external with a C finalizer that throws,
3+
// then runs GC until the finalizer fires and crashes the process.
4+
try {
5+
loadAddon("test_exception");
6+
} catch (anException) {
7+
anException.binding.createExternal();
8+
}
9+
10+
// Collect garbage 10 times. At least one of those should throw the exception
11+
// and cause the whole process to bail with it, its text printed to stderr and
12+
// asserted by the parent process to match expectations.
13+
let gcCount = 10;
14+
await gcUntil("test", () => --gcCount <= 0);

0 commit comments

Comments
 (0)