-
-
Notifications
You must be signed in to change notification settings - Fork 11
port test_reference to CTS #47
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 6 commits
fc1d77a
c033445
0d6c4b7
b3ef295
7699680
ceeb53a
5a60dfd
224918d
4d58c45
e2f52af
3ab0c97
b74409b
587f083
76b4858
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,26 @@ | ||
| // Capture the engine-provided gc (Node exposes it under --expose-gc) before | ||
| // we overwrite globalThis.gc with the harness wrapper below. | ||
| const engineGc = globalThis.gc; | ||
| if (typeof engineGc !== "function") { | ||
| throw new Error( | ||
| "Node harness expects globalThis.gc to be available (run with --expose-gc)", | ||
| ); | ||
| } | ||
|
|
||
| const gc = () => { | ||
| engineGc(); | ||
| }; | ||
|
|
||
| const gcUntil = async (name, condition) => { | ||
| let count = 0; | ||
| while (!condition()) { | ||
| await new Promise((resolve) => setImmediate(resolve)); | ||
| if (++count < 10) { | ||
| globalThis.gc(); | ||
| engineGc(); | ||
| } else { | ||
| throw new Error(`GC test "${name}" failed after ${count} attempts`); | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| Object.assign(globalThis, { gcUntil }); | ||
| Object.assign(globalThis, { gc, gcUntil }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| const onUncaughtException = (cb) => { | ||
| process.on("uncaughtException", cb); | ||
| }; | ||
|
|
||
| Object.assign(globalThis, { onUncaughtException }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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, "..", ".."); | ||
|
|
@@ -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 ON_UNCAUGHT_EXCEPTION_MODULE_PATH = path.join( | ||
| ROOT_PATH, | ||
| "implementors", | ||
| "node", | ||
| "must-call.js" | ||
| "on-uncaught-exception.js", | ||
| ); | ||
|
|
||
| export function listDirectoryEntries(dir: string) { | ||
|
|
@@ -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( | ||
|
|
@@ -80,9 +81,13 @@ export function runFileInSubprocess( | |
| "file://" + GC_MODULE_PATH, | ||
| "--import", | ||
| "file://" + MUST_CALL_MODULE_PATH, | ||
| // test_finalizer needs this | ||
| "--force-node-api-uncaught-exceptions-policy", | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would we need a way to run tests with and without this enabled? 🤔
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Created #60 to track that.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This flag has been enabled by default, and it is a legacy Node.js bug. I don't think it is necessary to keep track of this flag.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. removed the flag |
||
| "--import", | ||
| "file://" + ON_UNCAUGHT_EXCEPTION_MODULE_PATH, | ||
| filePath, | ||
| ], | ||
| { cwd } | ||
| { cwd }, | ||
| ); | ||
|
|
||
| let stderrOutput = ""; | ||
|
|
@@ -111,9 +116,9 @@ export function runFileInSubprocess( | |
| new Error( | ||
| `Test file ${path.relative( | ||
| TESTS_ROOT_PATH, | ||
| filePath | ||
| )} failed (${reason})${stderrSuffix}` | ||
| ) | ||
| filePath, | ||
| )} failed (${reason})${stderrSuffix}`, | ||
| ), | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| add_node_api_cts_addon(test_reference test_reference.c) | ||
| add_node_api_cts_addon(test_finalizer test_finalizer.c) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,168 @@ | ||
| "use strict"; | ||
| // Flags: --expose-gc | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a V8 specific flag.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does it need any action item? |
||
|
|
||
| const test_reference = loadAddon("test_reference"); | ||
|
|
||
| // This test script uses external values with finalizer callbacks | ||
| // in order to track when values get garbage-collected. Each invocation | ||
| // of a finalizer callback increments the finalizeCount property. | ||
| assert.strictEqual(test_reference.finalizeCount, 0); | ||
|
|
||
| // Run each test function in sequence, | ||
| // with an async delay and GC call between each. | ||
| async function runTests() { | ||
| (() => { | ||
| const symbol = test_reference.createSymbol("testSym"); | ||
| test_reference.createReference(symbol, 0); | ||
| assert.strictEqual(test_reference.referenceValue, symbol); | ||
| })(); | ||
| test_reference.deleteReference(); | ||
|
|
||
| (() => { | ||
| const symbol = test_reference.createSymbolFor("testSymFor"); | ||
| test_reference.createReference(symbol, 0); | ||
| assert.strictEqual(test_reference.referenceValue, symbol); | ||
| })(); | ||
| test_reference.deleteReference(); | ||
|
|
||
| (() => { | ||
| const symbol = test_reference.createSymbolFor("testSymFor"); | ||
| test_reference.createReference(symbol, 1); | ||
| assert.strictEqual(test_reference.referenceValue, symbol); | ||
| assert.strictEqual(test_reference.referenceValue, Symbol.for("testSymFor")); | ||
| })(); | ||
| test_reference.deleteReference(); | ||
|
|
||
| (() => { | ||
| const symbol = test_reference.createSymbolForEmptyString(); | ||
| test_reference.createReference(symbol, 0); | ||
| assert.strictEqual(test_reference.referenceValue, Symbol.for("")); | ||
| })(); | ||
| test_reference.deleteReference(); | ||
|
|
||
| (() => { | ||
| const symbol = test_reference.createSymbolForEmptyString(); | ||
| test_reference.createReference(symbol, 1); | ||
| assert.strictEqual(test_reference.referenceValue, symbol); | ||
| assert.strictEqual(test_reference.referenceValue, Symbol.for("")); | ||
| })(); | ||
| test_reference.deleteReference(); | ||
|
|
||
| assert.throws( | ||
| () => test_reference.createSymbolForIncorrectLength(), | ||
| /Invalid argument/, | ||
| ); | ||
|
|
||
| (() => { | ||
| const value = test_reference.createExternal(); | ||
| assert.strictEqual(test_reference.finalizeCount, 0); | ||
| assert.strictEqual(typeof value, "object"); | ||
| test_reference.checkExternal(value); | ||
| })(); | ||
| await gcUntil( | ||
| "External value without a finalizer", | ||
| () => test_reference.finalizeCount === 0, | ||
| ); | ||
|
|
||
| (() => { | ||
| const value = test_reference.createExternalWithFinalize(); | ||
| assert.strictEqual(test_reference.finalizeCount, 0); | ||
| assert.strictEqual(typeof value, "object"); | ||
| test_reference.checkExternal(value); | ||
| })(); | ||
| await gcUntil( | ||
| "External value with a finalizer", | ||
| () => test_reference.finalizeCount === 1, | ||
| ); | ||
|
|
||
| (() => { | ||
| const value = test_reference.createExternalWithFinalize(); | ||
| assert.strictEqual(test_reference.finalizeCount, 0); | ||
| test_reference.createReference(value, 0); | ||
| assert.strictEqual(test_reference.referenceValue, value); | ||
| })(); | ||
| // Value should be GC'd because there is only a weak ref | ||
| await gcUntil( | ||
| "Weak reference", | ||
| () => | ||
| test_reference.referenceValue === undefined && | ||
| test_reference.finalizeCount === 1, | ||
| ); | ||
| test_reference.deleteReference(); | ||
|
|
||
| (() => { | ||
| const value = test_reference.createExternalWithFinalize(); | ||
| assert.strictEqual(test_reference.finalizeCount, 0); | ||
| test_reference.createReference(value, 1); | ||
| assert.strictEqual(test_reference.referenceValue, value); | ||
| })(); | ||
| // Value should NOT be GC'd because there is a strong ref | ||
| await gcUntil("Strong reference", () => test_reference.finalizeCount === 0); | ||
| test_reference.deleteReference(); | ||
| await gcUntil( | ||
| "Strong reference (cont.d)", | ||
| () => test_reference.finalizeCount === 1, | ||
| ); | ||
|
|
||
| (() => { | ||
| const value = test_reference.createExternalWithFinalize(); | ||
| assert.strictEqual(test_reference.finalizeCount, 0); | ||
| test_reference.createReference(value, 1); | ||
| })(); | ||
| // Value should NOT be GC'd because there is a strong ref | ||
| await gcUntil( | ||
| "Strong reference, increment then decrement to weak reference", | ||
| () => test_reference.finalizeCount === 0, | ||
| ); | ||
| assert.strictEqual(test_reference.incrementRefcount(), 2); | ||
| // Value should NOT be GC'd because there is a strong ref | ||
| await gcUntil( | ||
| "Strong reference, increment then decrement to weak reference (cont.d-1)", | ||
| () => test_reference.finalizeCount === 0, | ||
| ); | ||
| assert.strictEqual(test_reference.decrementRefcount(), 1); | ||
| // Value should NOT be GC'd because there is a strong ref | ||
| await gcUntil( | ||
| "Strong reference, increment then decrement to weak reference (cont.d-2)", | ||
| () => test_reference.finalizeCount === 0, | ||
| ); | ||
| assert.strictEqual(test_reference.decrementRefcount(), 0); | ||
| // Value should be GC'd because the ref is now weak! | ||
| await gcUntil( | ||
| "Strong reference, increment then decrement to weak reference (cont.d-3)", | ||
| () => test_reference.finalizeCount === 1, | ||
| ); | ||
| test_reference.deleteReference(); | ||
| // Value was already GC'd | ||
| await gcUntil( | ||
| "Strong reference, increment then decrement to weak reference (cont.d-4)", | ||
| () => test_reference.finalizeCount === 1, | ||
| ); | ||
| } | ||
| runTests(); | ||
|
|
||
| // This test creates a napi_ref on an object that has | ||
| // been wrapped by napi_wrap and for which the finalizer | ||
| // for the wrap calls napi_delete_ref on that napi_ref. | ||
| // | ||
| // Since both the wrap and the reference use the same | ||
| // object the finalizer for the wrap and reference | ||
| // may run in the same gc and in any order. | ||
| // | ||
| // It does that to validate that napi_delete_ref can be | ||
| // called before the finalizer has been run for the | ||
| // reference (there is a finalizer behind the scenes even | ||
| // though it cannot be passed to napi_create_reference). | ||
| // | ||
| // Since the order is not guaranteed, run the | ||
| // test a number of times maximize the chance that we | ||
| // get a run with the desired order for the test. | ||
| // | ||
| // 1000 reliably recreated the problem without the fix | ||
| // required to ensure delete could be called before | ||
| // the finalizer in manual testing. | ||
| for (let i = 0; i < 1000; i++) { | ||
| const wrapObject = new Object(); | ||
| test_reference.validateDeleteBeforeFinalize(wrapObject); | ||
| gc(); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again - unsure how we'd be implementing this in other implementors 🤔 But perhaps we'll just have to cross that bridge when we get there.