Skip to content

Commit fc1d77a

Browse files
committed
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 <ba@bavulapati.com>
1 parent 86d68d3 commit fc1d77a

File tree

8 files changed

+560
-19
lines changed

8 files changed

+560
-19
lines changed

eslint.config.js

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,29 @@ export default defineConfig([
1717
mustNotCall: "readonly",
1818
gcUntil: "readonly",
1919
experimentalFeatures: "readonly",
20+
onUncaughtException: "readonly",
2021
},
2122
},
2223
rules: {
2324
"no-undef": "error",
24-
"no-restricted-imports": ["error", {
25-
patterns: ["*"],
26-
}],
27-
"no-restricted-syntax": ["error",
28-
{ selector: "MemberExpression[object.name='globalThis']", message: "Avoid globalThis access in test files — use CTS harness globals instead" },
29-
{ selector: "MemberExpression[object.name='global']", message: "Avoid global access in test files — use CTS harness globals instead" }
25+
"no-restricted-imports": [
26+
"error",
27+
{
28+
patterns: ["*"],
29+
},
30+
],
31+
"no-restricted-syntax": [
32+
"error",
33+
{
34+
selector: "MemberExpression[object.name='globalThis']",
35+
message:
36+
"Avoid globalThis access in test files — use CTS harness globals instead",
37+
},
38+
{
39+
selector: "MemberExpression[object.name='global']",
40+
message:
41+
"Avoid global access in test files — use CTS harness globals instead",
42+
},
3043
],
3144
},
3245
},
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const onUncaughtException = (cb) => {
2+
process.on("uncaughtException", cb);
3+
};
4+
5+
Object.assign(globalThis, { onUncaughtException });

implementors/node/tests.ts

Lines changed: 18 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 ON_UNCAUGHT_EXCEPTION_MODULE_PATH = path.join(
3839
ROOT_PATH,
3940
"implementors",
4041
"node",
41-
"must-call.js"
42+
"on-uncaught-exception.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,13 @@ export function runFileInSubprocess(
8081
"file://" + GC_MODULE_PATH,
8182
"--import",
8283
"file://" + MUST_CALL_MODULE_PATH,
84+
// test_finalizer needs this
85+
"--force-node-api-uncaught-exceptions-policy",
86+
"--import",
87+
"file://" + ON_UNCAUGHT_EXCEPTION_MODULE_PATH,
8388
filePath,
8489
],
85-
{ cwd }
90+
{ cwd },
8691
);
8792

8893
let stderrOutput = "";
@@ -111,9 +116,9 @@ export function runFileInSubprocess(
111116
new Error(
112117
`Test file ${path.relative(
113118
TESTS_ROOT_PATH,
114-
filePath
115-
)} failed (${reason})${stderrSuffix}`
116-
)
119+
filePath,
120+
)} failed (${reason})${stderrSuffix}`,
121+
),
117122
);
118123
});
119124
});
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
add_node_api_cts_addon(test_reference test_reference.c)
2+
add_node_api_cts_addon(test_finalizer test_finalizer.c)
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
"use strict";
2+
// Flags: --expose-gc
3+
4+
const test_reference = loadAddon("test_reference");
5+
6+
// This test script uses external values with finalizer callbacks
7+
// in order to track when values get garbage-collected. Each invocation
8+
// of a finalizer callback increments the finalizeCount property.
9+
assert.strictEqual(test_reference.finalizeCount, 0);
10+
11+
// Run each test function in sequence,
12+
// with an async delay and GC call between each.
13+
async function runTests() {
14+
(() => {
15+
const symbol = test_reference.createSymbol("testSym");
16+
test_reference.createReference(symbol, 0);
17+
assert.strictEqual(test_reference.referenceValue, symbol);
18+
})();
19+
test_reference.deleteReference();
20+
21+
(() => {
22+
const symbol = test_reference.createSymbolFor("testSymFor");
23+
test_reference.createReference(symbol, 0);
24+
assert.strictEqual(test_reference.referenceValue, symbol);
25+
})();
26+
test_reference.deleteReference();
27+
28+
(() => {
29+
const symbol = test_reference.createSymbolFor("testSymFor");
30+
test_reference.createReference(symbol, 1);
31+
assert.strictEqual(test_reference.referenceValue, symbol);
32+
assert.strictEqual(test_reference.referenceValue, Symbol.for("testSymFor"));
33+
})();
34+
test_reference.deleteReference();
35+
36+
(() => {
37+
const symbol = test_reference.createSymbolForEmptyString();
38+
test_reference.createReference(symbol, 0);
39+
assert.strictEqual(test_reference.referenceValue, Symbol.for(""));
40+
})();
41+
test_reference.deleteReference();
42+
43+
(() => {
44+
const symbol = test_reference.createSymbolForEmptyString();
45+
test_reference.createReference(symbol, 1);
46+
assert.strictEqual(test_reference.referenceValue, symbol);
47+
assert.strictEqual(test_reference.referenceValue, Symbol.for(""));
48+
})();
49+
test_reference.deleteReference();
50+
51+
assert.throws(
52+
() => test_reference.createSymbolForIncorrectLength(),
53+
/Invalid argument/,
54+
);
55+
56+
(() => {
57+
const value = test_reference.createExternal();
58+
assert.strictEqual(test_reference.finalizeCount, 0);
59+
assert.strictEqual(typeof value, "object");
60+
test_reference.checkExternal(value);
61+
})();
62+
await gcUntil(
63+
"External value without a finalizer",
64+
() => test_reference.finalizeCount === 0,
65+
);
66+
67+
(() => {
68+
const value = test_reference.createExternalWithFinalize();
69+
assert.strictEqual(test_reference.finalizeCount, 0);
70+
assert.strictEqual(typeof value, "object");
71+
test_reference.checkExternal(value);
72+
})();
73+
await gcUntil(
74+
"External value with a finalizer",
75+
() => test_reference.finalizeCount === 1,
76+
);
77+
78+
(() => {
79+
const value = test_reference.createExternalWithFinalize();
80+
assert.strictEqual(test_reference.finalizeCount, 0);
81+
test_reference.createReference(value, 0);
82+
assert.strictEqual(test_reference.referenceValue, value);
83+
})();
84+
// Value should be GC'd because there is only a weak ref
85+
await gcUntil(
86+
"Weak reference",
87+
() =>
88+
test_reference.referenceValue === undefined &&
89+
test_reference.finalizeCount === 1,
90+
);
91+
test_reference.deleteReference();
92+
93+
(() => {
94+
const value = test_reference.createExternalWithFinalize();
95+
assert.strictEqual(test_reference.finalizeCount, 0);
96+
test_reference.createReference(value, 1);
97+
assert.strictEqual(test_reference.referenceValue, value);
98+
})();
99+
// Value should NOT be GC'd because there is a strong ref
100+
await gcUntil("Strong reference", () => test_reference.finalizeCount === 0);
101+
test_reference.deleteReference();
102+
await gcUntil(
103+
"Strong reference (cont.d)",
104+
() => test_reference.finalizeCount === 1,
105+
);
106+
107+
(() => {
108+
const value = test_reference.createExternalWithFinalize();
109+
assert.strictEqual(test_reference.finalizeCount, 0);
110+
test_reference.createReference(value, 1);
111+
})();
112+
// Value should NOT be GC'd because there is a strong ref
113+
await gcUntil(
114+
"Strong reference, increment then decrement to weak reference",
115+
() => test_reference.finalizeCount === 0,
116+
);
117+
assert.strictEqual(test_reference.incrementRefcount(), 2);
118+
// Value should NOT be GC'd because there is a strong ref
119+
await gcUntil(
120+
"Strong reference, increment then decrement to weak reference (cont.d-1)",
121+
() => test_reference.finalizeCount === 0,
122+
);
123+
assert.strictEqual(test_reference.decrementRefcount(), 1);
124+
// Value should NOT be GC'd because there is a strong ref
125+
await gcUntil(
126+
"Strong reference, increment then decrement to weak reference (cont.d-2)",
127+
() => test_reference.finalizeCount === 0,
128+
);
129+
assert.strictEqual(test_reference.decrementRefcount(), 0);
130+
// Value should be GC'd because the ref is now weak!
131+
await gcUntil(
132+
"Strong reference, increment then decrement to weak reference (cont.d-3)",
133+
() => test_reference.finalizeCount === 1,
134+
);
135+
test_reference.deleteReference();
136+
// Value was already GC'd
137+
await gcUntil(
138+
"Strong reference, increment then decrement to weak reference (cont.d-4)",
139+
() => test_reference.finalizeCount === 1,
140+
);
141+
}
142+
runTests();
143+
144+
// This test creates a napi_ref on an object that has
145+
// been wrapped by napi_wrap and for which the finalizer
146+
// for the wrap calls napi_delete_ref on that napi_ref.
147+
//
148+
// Since both the wrap and the reference use the same
149+
// object the finalizer for the wrap and reference
150+
// may run in the same gc and in any order.
151+
//
152+
// It does that to validate that napi_delete_ref can be
153+
// called before the finalizer has been run for the
154+
// reference (there is a finalizer behind the scenes even
155+
// though it cannot be passed to napi_create_reference).
156+
//
157+
// Since the order is not guaranteed, run the
158+
// test a number of times maximize the chance that we
159+
// get a run with the desired order for the test.
160+
//
161+
// 1000 reliably recreated the problem without the fix
162+
// required to ensure delete could be called before
163+
// the finalizer in manual testing.
164+
for (let i = 0; i < 1000; i++) {
165+
const wrapObject = new Object();
166+
test_reference.validateDeleteBeforeFinalize(wrapObject);
167+
let gcCount = 1;
168+
gcUntil("test", () => gcCount-- > 0);
169+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#include <assert.h>
2+
#include <js_native_api.h>
3+
#include <stdlib.h>
4+
#include "../common.h"
5+
#include "../entry_point.h"
6+
7+
static int test_value = 1;
8+
static int finalize_count = 0;
9+
10+
static void FinalizeExternalCallJs(napi_env env, void* data, void* hint) {
11+
int* actual_value = data;
12+
NODE_API_ASSERT_RETURN_VOID(
13+
env,
14+
actual_value == &test_value,
15+
"The correct pointer was passed to the finalizer");
16+
17+
napi_ref finalizer_ref = (napi_ref)hint;
18+
napi_value js_finalizer;
19+
napi_value recv;
20+
NODE_API_CALL_RETURN_VOID(
21+
env, napi_get_reference_value(env, finalizer_ref, &js_finalizer));
22+
NODE_API_CALL_RETURN_VOID(env, napi_get_global(env, &recv));
23+
NODE_API_CALL_RETURN_VOID(
24+
env, napi_call_function(env, recv, js_finalizer, 0, NULL, NULL));
25+
NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, finalizer_ref));
26+
}
27+
28+
static napi_value CreateExternalWithJsFinalize(napi_env env,
29+
napi_callback_info info) {
30+
size_t argc = 1;
31+
napi_value args[1];
32+
NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL));
33+
NODE_API_ASSERT(env, argc == 1, "Wrong number of arguments");
34+
napi_value finalizer = args[0];
35+
napi_valuetype finalizer_valuetype;
36+
NODE_API_CALL(env, napi_typeof(env, finalizer, &finalizer_valuetype));
37+
NODE_API_ASSERT(env,
38+
finalizer_valuetype == napi_function,
39+
"Wrong type of first argument");
40+
napi_ref finalizer_ref;
41+
NODE_API_CALL(env, napi_create_reference(env, finalizer, 1, &finalizer_ref));
42+
43+
napi_value result;
44+
NODE_API_CALL(env,
45+
napi_create_external(env,
46+
&test_value,
47+
FinalizeExternalCallJs,
48+
finalizer_ref, /* finalize_hint */
49+
&result));
50+
51+
finalize_count = 0;
52+
return result;
53+
}
54+
55+
EXTERN_C_START
56+
napi_value Init(napi_env env, napi_value exports) {
57+
napi_property_descriptor descriptors[] = {
58+
DECLARE_NODE_API_PROPERTY("createExternalWithJsFinalize",
59+
CreateExternalWithJsFinalize),
60+
};
61+
62+
NODE_API_CALL(
63+
env,
64+
napi_define_properties(env,
65+
exports,
66+
sizeof(descriptors) / sizeof(*descriptors),
67+
descriptors));
68+
69+
return exports;
70+
}
71+
EXTERN_C_END
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"use strict";
2+
// Flags: --expose-gc --force-node-api-uncaught-exceptions-policy
3+
4+
const binding = loadAddon("test_finalizer");
5+
6+
onUncaughtException(
7+
mustCall((err) => {
8+
assert.throws(() => {
9+
throw err;
10+
}, /finalizer error/);
11+
}),
12+
);
13+
14+
(async function () {
15+
{
16+
binding.createExternalWithJsFinalize(
17+
mustCall(() => {
18+
throw new Error("finalizer error");
19+
}),
20+
);
21+
}
22+
let gcCount = 1;
23+
await gcUntil("test", () => gcCount-- > 0);
24+
})().then(mustCall());

0 commit comments

Comments
 (0)