diff --git a/test/async-hooks/test-async-local-storage-args-frame.js b/test/async-hooks/test-async-local-storage-args-frame.js new file mode 100644 index 00000000000000..c961a64340cb2b --- /dev/null +++ b/test/async-hooks/test-async-local-storage-args-frame.js @@ -0,0 +1,14 @@ +// Flags: --experimental-async-context-frame +'use strict'; +require('../common'); +const assert = require('assert'); +const { AsyncLocalStorage } = require('async_hooks'); + +const asyncLocalStorage = new AsyncLocalStorage(); + +asyncLocalStorage.run({}, (runArg) => { + assert.strictEqual(runArg, 'foo'); + asyncLocalStorage.exit((exitArg) => { + assert.strictEqual(exitArg, 'bar'); + }, 'bar'); +}, 'foo'); diff --git a/test/async-hooks/test-async-local-storage-async-await-frame.js b/test/async-hooks/test-async-local-storage-async-await-frame.js new file mode 100644 index 00000000000000..501da95abad3dd --- /dev/null +++ b/test/async-hooks/test-async-local-storage-async-await-frame.js @@ -0,0 +1,20 @@ +// Flags: --experimental-async-context-frame +'use strict'; +require('../common'); +const assert = require('assert'); +const { AsyncLocalStorage } = require('async_hooks'); + +const asyncLocalStorage = new AsyncLocalStorage(); + +async function test() { + asyncLocalStorage.getStore().set('foo', 'bar'); + await Promise.resolve(); + assert.strictEqual(asyncLocalStorage.getStore().get('foo'), 'bar'); +} + +async function main() { + await asyncLocalStorage.run(new Map(), test); + assert.strictEqual(asyncLocalStorage.getStore(), undefined); +} + +main(); diff --git a/test/async-hooks/test-async-local-storage-async-functions-frame.js b/test/async-hooks/test-async-local-storage-async-functions-frame.js new file mode 100644 index 00000000000000..d6ee0e6c9c7504 --- /dev/null +++ b/test/async-hooks/test-async-local-storage-async-functions-frame.js @@ -0,0 +1,28 @@ +// Flags: --experimental-async-context-frame +'use strict'; +require('../common'); +const assert = require('assert'); +const { AsyncLocalStorage } = require('async_hooks'); + +async function foo() {} + +const asyncLocalStorage = new AsyncLocalStorage(); + +async function testOut() { + await foo(); + assert.strictEqual(asyncLocalStorage.getStore(), undefined); +} + +async function testAwait() { + await foo(); + assert.notStrictEqual(asyncLocalStorage.getStore(), undefined); + assert.strictEqual(asyncLocalStorage.getStore().get('key'), 'value'); + await asyncLocalStorage.exit(testOut); +} + +asyncLocalStorage.run(new Map(), () => { + const store = asyncLocalStorage.getStore(); + store.set('key', 'value'); + testAwait(); // should not reject +}); +assert.strictEqual(asyncLocalStorage.getStore(), undefined); diff --git a/test/async-hooks/test-async-local-storage-dgram-frame.js b/test/async-hooks/test-async-local-storage-dgram-frame.js new file mode 100644 index 00000000000000..eadc197264f4b9 --- /dev/null +++ b/test/async-hooks/test-async-local-storage-dgram-frame.js @@ -0,0 +1,27 @@ +// Flags: --experimental-async-context-frame +'use strict'; + +require('../common'); + +// Regression tests for https://github.com/nodejs/node/issues/40693 + +const assert = require('assert'); +const dgram = require('dgram'); +const { AsyncLocalStorage } = require('async_hooks'); + +dgram.createSocket('udp4') + .on('message', function(msg, rinfo) { this.send(msg, rinfo.port); }) + .on('listening', function() { + const asyncLocalStorage = new AsyncLocalStorage(); + const store = { val: 'abcd' }; + asyncLocalStorage.run(store, () => { + const client = dgram.createSocket('udp4'); + client.on('message', (msg, rinfo) => { + assert.deepStrictEqual(asyncLocalStorage.getStore(), store); + client.close(); + this.close(); + }); + client.send('Hello, world!', this.address().port); + }); + }) + .bind(0); diff --git a/test/async-hooks/test-async-local-storage-enable-disable-frame.js b/test/async-hooks/test-async-local-storage-enable-disable-frame.js new file mode 100644 index 00000000000000..cf0a413195e67f --- /dev/null +++ b/test/async-hooks/test-async-local-storage-enable-disable-frame.js @@ -0,0 +1,33 @@ +// Flags: --experimental-async-context-frame +'use strict'; +require('../common'); +const assert = require('assert'); +const { AsyncLocalStorage } = require('async_hooks'); + +const asyncLocalStorage = new AsyncLocalStorage(); + +asyncLocalStorage.run(new Map(), () => { + asyncLocalStorage.getStore().set('foo', 'bar'); + process.nextTick(() => { + assert.strictEqual(asyncLocalStorage.getStore().get('foo'), 'bar'); + process.nextTick(() => { + assert.strictEqual(asyncLocalStorage.getStore(), undefined); + }); + + asyncLocalStorage.disable(); + assert.strictEqual(asyncLocalStorage.getStore(), undefined); + + // Calls to exit() should not mess with enabled status + asyncLocalStorage.exit(() => { + assert.strictEqual(asyncLocalStorage.getStore(), undefined); + }); + assert.strictEqual(asyncLocalStorage.getStore(), undefined); + + process.nextTick(() => { + assert.strictEqual(asyncLocalStorage.getStore(), undefined); + asyncLocalStorage.run(new Map().set('bar', 'foo'), () => { + assert.strictEqual(asyncLocalStorage.getStore().get('bar'), 'foo'); + }); + }); + }); +}); diff --git a/test/async-hooks/test-async-local-storage-enter-with-frame.js b/test/async-hooks/test-async-local-storage-enter-with-frame.js new file mode 100644 index 00000000000000..106c882c8e6799 --- /dev/null +++ b/test/async-hooks/test-async-local-storage-enter-with-frame.js @@ -0,0 +1,21 @@ +// Flags: --experimental-async-context-frame +'use strict'; +require('../common'); +const assert = require('assert'); +const { AsyncLocalStorage } = require('async_hooks'); + +const asyncLocalStorage = new AsyncLocalStorage(); + +setImmediate(() => { + const store = { foo: 'bar' }; + asyncLocalStorage.enterWith(store); + + assert.strictEqual(asyncLocalStorage.getStore(), store); + setTimeout(() => { + assert.strictEqual(asyncLocalStorage.getStore(), store); + }, 10); +}); + +setTimeout(() => { + assert.strictEqual(asyncLocalStorage.getStore(), undefined); +}, 10); diff --git a/test/async-hooks/test-async-local-storage-errors-frame.js b/test/async-hooks/test-async-local-storage-errors-frame.js new file mode 100644 index 00000000000000..ab9041f9d2731e --- /dev/null +++ b/test/async-hooks/test-async-local-storage-errors-frame.js @@ -0,0 +1,119 @@ +// Flags: --experimental-async-context-frame +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { AsyncLocalStorage } = require('async_hooks'); +const vm = require('vm'); + +// err1 is emitted sync as a control - no events +// err2 is emitted after a timeout - uncaughtExceptionMonitor +// + uncaughtException +// err3 is emitted after some awaits - unhandledRejection +// err4 is emitted during handling err3 - uncaughtExceptionMonitor +// err5 is emitted after err4 from a VM lacking hooks - unhandledRejection +// + uncaughtException + +const asyncLocalStorage = new AsyncLocalStorage(); +const callbackToken = { callbackToken: true }; +const awaitToken = { awaitToken: true }; + +let i = 0; + +// Redefining the uncaughtExceptionHandler is a bit odd, so we just do this +// so we can track total invocations +let underlyingExceptionHandler; +const exceptionHandler = common.mustCall(function(...args) { + return underlyingExceptionHandler.call(this, ...args); +}, 2); +process.setUncaughtExceptionCaptureCallback(exceptionHandler); + +const exceptionMonitor = common.mustCall((err, origin) => { + if (err.message === 'err2') { + assert.strictEqual(origin, 'uncaughtException'); + assert.strictEqual(asyncLocalStorage.getStore(), callbackToken); + } else if (err.message === 'err4') { + assert.strictEqual(origin, 'unhandledRejection'); + assert.strictEqual(asyncLocalStorage.getStore(), awaitToken); + } else { + assert.fail('unknown error ' + err); + } +}, 2); +process.on('uncaughtExceptionMonitor', exceptionMonitor); + +function fireErr1() { + underlyingExceptionHandler = common.mustCall(function(err) { + ++i; + assert.strictEqual(err.message, 'err2'); + assert.strictEqual(asyncLocalStorage.getStore(), callbackToken); + }, 1); + try { + asyncLocalStorage.run(callbackToken, () => { + setTimeout(fireErr2, 0); + throw new Error('err1'); + }); + } catch (e) { + assert.strictEqual(e.message, 'err1'); + assert.strictEqual(asyncLocalStorage.getStore(), undefined); + } +} + +function fireErr2() { + process.nextTick(() => { + assert.strictEqual(i, 1); + fireErr3(); + }); + throw new Error('err2'); +} + +function fireErr3() { + assert.strictEqual(asyncLocalStorage.getStore(), callbackToken); + const rejectionHandler3 = common.mustCall((err) => { + assert.strictEqual(err.message, 'err3'); + assert.strictEqual(asyncLocalStorage.getStore(), awaitToken); + process.off('unhandledRejection', rejectionHandler3); + + fireErr4(); + }, 1); + process.on('unhandledRejection', rejectionHandler3); + async function awaitTest() { + await null; + throw new Error('err3'); + } + asyncLocalStorage.run(awaitToken, awaitTest); +} + +const uncaughtExceptionHandler4 = common.mustCall( + function(err) { + assert.strictEqual(err.message, 'err4'); + assert.strictEqual(asyncLocalStorage.getStore(), awaitToken); + fireErr5(); + }, 1); +function fireErr4() { + assert.strictEqual(asyncLocalStorage.getStore(), awaitToken); + underlyingExceptionHandler = uncaughtExceptionHandler4; + // re-entrant check + Promise.reject(new Error('err4')); +} + +function fireErr5() { + assert.strictEqual(asyncLocalStorage.getStore(), awaitToken); + underlyingExceptionHandler = () => {}; + const rejectionHandler5 = common.mustCall((err) => { + assert.strictEqual(err.message, 'err5'); + assert.strictEqual(asyncLocalStorage.getStore(), awaitToken); + process.off('unhandledRejection', rejectionHandler5); + }, 1); + process.on('unhandledRejection', rejectionHandler5); + const makeOrphan = vm.compileFunction(`(${String(() => { + async function main() { + await null; + Promise.resolve().then(() => { + throw new Error('err5'); + }); + } + main(); + })})()`); + makeOrphan(); +} + +fireErr1(); diff --git a/test/async-hooks/test-async-local-storage-gcable-frame.js b/test/async-hooks/test-async-local-storage-gcable-frame.js new file mode 100644 index 00000000000000..1c8d95e58ffcb0 --- /dev/null +++ b/test/async-hooks/test-async-local-storage-gcable-frame.js @@ -0,0 +1,20 @@ +// Flags: --experimental-async-context-frame --expose_gc +'use strict'; + +// This test ensures that AsyncLocalStorage gets gced once it was disabled +// and no strong references remain in userland. + +const common = require('../common'); +const { AsyncLocalStorage } = require('async_hooks'); +const { onGC } = require('../common/gc'); + +let asyncLocalStorage = new AsyncLocalStorage(); + +asyncLocalStorage.run({}, () => { + asyncLocalStorage.disable(); + + onGC(asyncLocalStorage, { ongc: common.mustCall() }); +}); + +asyncLocalStorage = null; +global.gc(); diff --git a/test/async-hooks/test-async-local-storage-http-agent-frame.js b/test/async-hooks/test-async-local-storage-http-agent-frame.js new file mode 100644 index 00000000000000..29c4bb0a064afc --- /dev/null +++ b/test/async-hooks/test-async-local-storage-http-agent-frame.js @@ -0,0 +1,36 @@ +// Flags: --experimental-async-context-frame +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { AsyncLocalStorage } = require('async_hooks'); +const http = require('http'); + +const asyncLocalStorage = new AsyncLocalStorage(); + +const agent = new http.Agent({ + maxSockets: 1, +}); + +const N = 3; +let responses = 0; + +const server = http.createServer(common.mustCall((req, res) => { + res.end('ok'); +}, N)); + +server.listen(0, common.mustCall(() => { + const port = server.address().port; + + for (let i = 0; i < N; i++) { + asyncLocalStorage.run(i, () => { + http.get({ agent, port }, common.mustCall((res) => { + assert.strictEqual(asyncLocalStorage.getStore(), i); + if (++responses === N) { + server.close(); + agent.destroy(); + } + res.resume(); + })); + }); + } +})); diff --git a/test/async-hooks/test-async-local-storage-http-frame.js b/test/async-hooks/test-async-local-storage-http-frame.js new file mode 100644 index 00000000000000..bf4dc7e61d53f2 --- /dev/null +++ b/test/async-hooks/test-async-local-storage-http-frame.js @@ -0,0 +1,22 @@ +// Flags: --experimental-async-context-frame +'use strict'; +require('../common'); +const assert = require('assert'); +const { AsyncLocalStorage } = require('async_hooks'); +const http = require('http'); + +const asyncLocalStorage = new AsyncLocalStorage(); +const server = http.createServer((req, res) => { + res.end('ok'); +}); + +server.listen(0, () => { + asyncLocalStorage.run(new Map(), () => { + const store = asyncLocalStorage.getStore(); + store.set('hello', 'world'); + http.get({ host: 'localhost', port: server.address().port }, () => { + assert.strictEqual(asyncLocalStorage.getStore().get('hello'), 'world'); + server.close(); + }); + }); +}); diff --git a/test/async-hooks/test-async-local-storage-misc-stores-frame.js b/test/async-hooks/test-async-local-storage-misc-stores-frame.js new file mode 100644 index 00000000000000..0ca6953c61694b --- /dev/null +++ b/test/async-hooks/test-async-local-storage-misc-stores-frame.js @@ -0,0 +1,16 @@ +// Flags: --experimental-async-context-frame +'use strict'; +require('../common'); +const assert = require('assert'); +const { AsyncLocalStorage } = require('async_hooks'); + +const asyncLocalStorage = new AsyncLocalStorage(); + +asyncLocalStorage.run('hello node', () => { + assert.strictEqual(asyncLocalStorage.getStore(), 'hello node'); +}); + +const runStore = { hello: 'node' }; +asyncLocalStorage.run(runStore, () => { + assert.strictEqual(asyncLocalStorage.getStore(), runStore); +}); diff --git a/test/async-hooks/test-async-local-storage-nested-frame.js b/test/async-hooks/test-async-local-storage-nested-frame.js new file mode 100644 index 00000000000000..95d0490b367213 --- /dev/null +++ b/test/async-hooks/test-async-local-storage-nested-frame.js @@ -0,0 +1,26 @@ +// Flags: --experimental-async-context-frame +'use strict'; +require('../common'); +const assert = require('assert'); +const { AsyncLocalStorage } = require('async_hooks'); + +const asyncLocalStorage = new AsyncLocalStorage(); +const outer = {}; +const inner = {}; + +function testInner() { + assert.strictEqual(asyncLocalStorage.getStore(), outer); + + asyncLocalStorage.run(inner, () => { + assert.strictEqual(asyncLocalStorage.getStore(), inner); + }); + assert.strictEqual(asyncLocalStorage.getStore(), outer); + + asyncLocalStorage.exit(() => { + assert.strictEqual(asyncLocalStorage.getStore(), undefined); + }); + assert.strictEqual(asyncLocalStorage.getStore(), outer); +} + +asyncLocalStorage.run(outer, testInner); +assert.strictEqual(asyncLocalStorage.getStore(), undefined); diff --git a/test/async-hooks/test-async-local-storage-no-mix-contexts-frame.js b/test/async-hooks/test-async-local-storage-no-mix-contexts-frame.js new file mode 100644 index 00000000000000..7394257970170d --- /dev/null +++ b/test/async-hooks/test-async-local-storage-no-mix-contexts-frame.js @@ -0,0 +1,39 @@ +// Flags: --experimental-async-context-frame +'use strict'; +require('../common'); +const assert = require('assert'); +const { AsyncLocalStorage } = require('async_hooks'); + +const asyncLocalStorage = new AsyncLocalStorage(); +const asyncLocalStorage2 = new AsyncLocalStorage(); + +setTimeout(() => { + asyncLocalStorage.run(new Map(), () => { + asyncLocalStorage2.run(new Map(), () => { + const store = asyncLocalStorage.getStore(); + const store2 = asyncLocalStorage2.getStore(); + store.set('hello', 'world'); + store2.set('hello', 'foo'); + setTimeout(() => { + assert.strictEqual(asyncLocalStorage.getStore().get('hello'), 'world'); + assert.strictEqual(asyncLocalStorage2.getStore().get('hello'), 'foo'); + asyncLocalStorage.exit(() => { + assert.strictEqual(asyncLocalStorage.getStore(), undefined); + assert.strictEqual(asyncLocalStorage2.getStore().get('hello'), 'foo'); + }); + assert.strictEqual(asyncLocalStorage.getStore().get('hello'), 'world'); + assert.strictEqual(asyncLocalStorage2.getStore().get('hello'), 'foo'); + }, 200); + }); + }); +}, 100); + +setTimeout(() => { + asyncLocalStorage.run(new Map(), () => { + const store = asyncLocalStorage.getStore(); + store.set('hello', 'earth'); + setTimeout(() => { + assert.strictEqual(asyncLocalStorage.getStore().get('hello'), 'earth'); + }, 100); + }); +}, 100); diff --git a/test/async-hooks/test-async-local-storage-promises-frame.js b/test/async-hooks/test-async-local-storage-promises-frame.js new file mode 100644 index 00000000000000..85103e26c4a8f5 --- /dev/null +++ b/test/async-hooks/test-async-local-storage-promises-frame.js @@ -0,0 +1,29 @@ +// Flags: --experimental-async-context-frame +'use strict'; +require('../common'); +const assert = require('assert'); +const { AsyncLocalStorage } = require('async_hooks'); + +async function main() { + const asyncLocalStorage = new AsyncLocalStorage(); + const err = new Error(); + const next = () => Promise.resolve() + .then(() => { + assert.strictEqual(asyncLocalStorage.getStore().get('a'), 1); + throw err; + }); + await new Promise((resolve, reject) => { + asyncLocalStorage.run(new Map(), () => { + const store = asyncLocalStorage.getStore(); + store.set('a', 1); + next().then(resolve, reject); + }); + }) + .catch((e) => { + assert.strictEqual(asyncLocalStorage.getStore(), undefined); + assert.strictEqual(e, err); + }); + assert.strictEqual(asyncLocalStorage.getStore(), undefined); +} + +main(); diff --git a/test/async-hooks/test-async-local-storage-socket-frame.js b/test/async-hooks/test-async-local-storage-socket-frame.js new file mode 100644 index 00000000000000..8491fc93aeaa2c --- /dev/null +++ b/test/async-hooks/test-async-local-storage-socket-frame.js @@ -0,0 +1,28 @@ +// Flags: --experimental-async-context-frame +'use strict'; + +require('../common'); + +// Regression tests for https://github.com/nodejs/node/issues/40693 + +const assert = require('assert'); +const net = require('net'); +const { AsyncLocalStorage } = require('async_hooks'); + +net + .createServer((socket) => { + socket.write('Hello, world!'); + socket.pipe(socket); + }) + .listen(0, function() { + const asyncLocalStorage = new AsyncLocalStorage(); + const store = { val: 'abcd' }; + asyncLocalStorage.run(store, () => { + const client = net.connect({ port: this.address().port }); + client.on('data', () => { + assert.deepStrictEqual(asyncLocalStorage.getStore(), store); + client.end(); + this.close(); + }); + }); + }); diff --git a/test/async-hooks/test-async-local-storage-thenable-frame.js b/test/async-hooks/test-async-local-storage-thenable-frame.js new file mode 100644 index 00000000000000..fb0220f280d673 --- /dev/null +++ b/test/async-hooks/test-async-local-storage-thenable-frame.js @@ -0,0 +1,54 @@ +// Flags: --experimental-async-context-frame +'use strict'; + +const common = require('../common'); + +const assert = require('assert'); +const { AsyncLocalStorage } = require('async_hooks'); + +// This test verifies that async local storage works with thenables + +const store = new AsyncLocalStorage(); +const data = Symbol('verifier'); + +const then = common.mustCall((cb) => { + assert.strictEqual(store.getStore(), data); + setImmediate(cb); +}, 4); + +function thenable() { + return { + then, + }; +} + +// Await a thenable +store.run(data, async () => { + assert.strictEqual(store.getStore(), data); + await thenable(); + assert.strictEqual(store.getStore(), data); +}); + +// Returning a thenable in an async function +store.run(data, async () => { + try { + assert.strictEqual(store.getStore(), data); + return thenable(); + } finally { + assert.strictEqual(store.getStore(), data); + } +}); + +// Resolving a thenable +store.run(data, () => { + assert.strictEqual(store.getStore(), data); + Promise.resolve(thenable()); + assert.strictEqual(store.getStore(), data); +}); + +// Returning a thenable in a then handler +store.run(data, () => { + assert.strictEqual(store.getStore(), data); + Promise.resolve().then(() => thenable()); + assert.strictEqual(store.getStore(), data); +}); diff --git a/test/async-hooks/test-async-local-storage-tlssocket-frame.js b/test/async-hooks/test-async-local-storage-tlssocket-frame.js new file mode 100644 index 00000000000000..b36573df5a8052 --- /dev/null +++ b/test/async-hooks/test-async-local-storage-tlssocket-frame.js @@ -0,0 +1,37 @@ +// Flags: --experimental-async-context-frame +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +// Regression tests for https://github.com/nodejs/node/issues/40693 + +const assert = require('assert'); +const fixtures = require('../common/fixtures'); +const tls = require('tls'); +const { AsyncLocalStorage } = require('async_hooks'); + +const options = { + cert: fixtures.readKey('rsa_cert.crt'), + key: fixtures.readKey('rsa_private.pem'), + rejectUnauthorized: false, +}; + +tls + .createServer(options, (socket) => { + socket.write('Hello, world!'); + socket.pipe(socket); + }) + .listen(0, function() { + const asyncLocalStorage = new AsyncLocalStorage(); + const store = { val: 'abcd' }; + asyncLocalStorage.run(store, () => { + const client = tls.connect({ port: this.address().port, ...options }); + client.on('data', () => { + assert.deepStrictEqual(asyncLocalStorage.getStore(), store); + client.end(); + this.close(); + }); + }); + }); diff --git a/test/parallel/test-async-context-frame.mjs b/test/parallel/test-async-context-frame.mjs deleted file mode 100644 index cad5d07bbd17cd..00000000000000 --- a/test/parallel/test-async-context-frame.mjs +++ /dev/null @@ -1,61 +0,0 @@ -import { isWindows } from '../common/index.mjs'; -import { spawn } from 'node:child_process'; -import { once } from 'node:events'; -import { opendir } from 'node:fs/promises'; -import { fileURLToPath } from 'node:url'; -import { describe, it } from 'node:test'; -import { sep } from 'node:path'; -import { strictEqual } from 'node:assert'; - -const python = process.env.PYTHON || (isWindows ? 'python' : 'python3'); - -const testRunner = fileURLToPath( - new URL('../../tools/test.py', import.meta.url) -); - -const setNames = ['async-hooks', 'parallel']; - -// Get all test names for each set -const testSets = await Promise.all(setNames.map(async (name) => { - const path = fileURLToPath(new URL(`../${name}`, import.meta.url)); - const dir = await opendir(path); - - const tests = []; - for await (const entry of dir) { - if (entry.name.startsWith('test-async-local-storage-')) { - tests.push(entry.name); - } - } - - return { - name, - tests - }; -})); - -// Merge test sets with set name prefix -const tests = testSets.reduce((m, v) => { - for (const test of v.tests) { - m.push(`${v.name}${sep}${test}`); - } - return m; -}, []); - -describe('AsyncContextFrame', { - concurrency: tests.length -}, () => { - for (const test of tests) { - it(test, async () => { - const proc = spawn(python, [ - testRunner, - '--node-args=--experimental-async-context-frame', - test, - ], { - stdio: ['ignore', 'ignore', 'inherit'], - }); - - const [code] = await once(proc, 'exit'); - strictEqual(code, 0, `Test ${test} failed with exit code ${code}`); - }); - } -}); diff --git a/test/parallel/test-async-local-storage-bind-frame.js b/test/parallel/test-async-local-storage-bind-frame.js new file mode 100644 index 00000000000000..84c288fb562c73 --- /dev/null +++ b/test/parallel/test-async-local-storage-bind-frame.js @@ -0,0 +1,18 @@ +// Flags: --experimental-async-context-frame +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { AsyncLocalStorage } = require('async_hooks'); + +[1, false, '', {}, []].forEach((i) => { + assert.throws(() => AsyncLocalStorage.bind(i), { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +const fn = common.mustCall(AsyncLocalStorage.bind(() => 123)); +assert.strictEqual(fn(), 123); + +const fn2 = AsyncLocalStorage.bind(common.mustCall((arg) => assert.strictEqual(arg, 'test'))); +fn2('test'); diff --git a/test/parallel/test-async-local-storage-contexts-frame.js b/test/parallel/test-async-local-storage-contexts-frame.js new file mode 100644 index 00000000000000..c4815016d5cb87 --- /dev/null +++ b/test/parallel/test-async-local-storage-contexts-frame.js @@ -0,0 +1,36 @@ +// Flags: --experimental-async-context-frame +'use strict'; + +require('../common'); +const assert = require('assert'); +const vm = require('vm'); +const { AsyncLocalStorage } = require('async_hooks'); + +// Regression test for https://github.com/nodejs/node/issues/38781 + +const context = vm.createContext({ + AsyncLocalStorage, + assert +}); + +vm.runInContext(` + const storage = new AsyncLocalStorage() + async function test() { + return storage.run({ test: 'vm' }, async () => { + assert.strictEqual(storage.getStore().test, 'vm'); + await 42; + assert.strictEqual(storage.getStore().test, 'vm'); + }); + } + test() +`, context); + +const storage = new AsyncLocalStorage(); +async function test() { + return storage.run({ test: 'main context' }, async () => { + assert.strictEqual(storage.getStore().test, 'main context'); + await 42; + assert.strictEqual(storage.getStore().test, 'main context'); + }); +} +test(); diff --git a/test/parallel/test-async-local-storage-deep-stack-frame.js b/test/parallel/test-async-local-storage-deep-stack-frame.js new file mode 100644 index 00000000000000..dff608fa4e2064 --- /dev/null +++ b/test/parallel/test-async-local-storage-deep-stack-frame.js @@ -0,0 +1,16 @@ +// Flags: --experimental-async-context-frame +'use strict'; +const common = require('../common'); +const { AsyncLocalStorage } = require('async_hooks'); + +// Regression test for: https://github.com/nodejs/node/issues/34556 + +const als = new AsyncLocalStorage(); + +const done = common.mustCall(); + +function run(count) { + if (count !== 0) return als.run({}, run, --count); + done(); +} +run(1000); diff --git a/test/parallel/test-async-local-storage-exit-does-not-leak-frame.js b/test/parallel/test-async-local-storage-exit-does-not-leak-frame.js new file mode 100644 index 00000000000000..8fb4d795f669e6 --- /dev/null +++ b/test/parallel/test-async-local-storage-exit-does-not-leak-frame.js @@ -0,0 +1,29 @@ +// Flags: --experimental-async-context-frame +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { AsyncLocalStorage } = require('async_hooks'); + +const als = new AsyncLocalStorage(); + +// The _propagate function only exists on the old JavaScript implementation. +if (typeof als._propagate === 'function') { + // The als instance should be getting removed from the storageList in + // lib/async_hooks.js when exit(...) is called, therefore when the nested runs + // are called there should be no copy of the als in the storageList to run the + // _propagate method on. + als._propagate = common.mustNotCall('_propagate() should not be called'); +} + +const done = common.mustCall(); + +const data = true; + +function run(count) { + if (count === 0) return done(); + assert.notStrictEqual(als.getStore(), data); + als.run(data, () => { + als.exit(run, --count); + }); +} +run(100); diff --git a/test/parallel/test-async-local-storage-http-multiclients-frame.js b/test/parallel/test-async-local-storage-http-multiclients-frame.js new file mode 100644 index 00000000000000..2a27654b463e37 --- /dev/null +++ b/test/parallel/test-async-local-storage-http-multiclients-frame.js @@ -0,0 +1,66 @@ +// Flags: --experimental-async-context-frame +'use strict'; +const common = require('../common'); +const Countdown = require('../common/countdown'); +const assert = require('assert'); +const { AsyncLocalStorage } = require('async_hooks'); +const http = require('http'); +const cls = new AsyncLocalStorage(); +const NUM_CLIENTS = 10; + +// Run multiple clients that receive data from a server +// in multiple chunks, in a single non-closure function. +// Use the AsyncLocalStorage (ALS) APIs to maintain the context +// and data download. Make sure that individual clients +// receive their respective data, with no conflicts. + +// Set up a server that sends large buffers of data, filled +// with cardinal numbers, increasing per request +let index = 0; +const server = http.createServer((q, r) => { + // Send a large chunk as response, otherwise the data + // may be sent in a single chunk, and the callback in the + // client may be called only once, defeating the purpose of test + r.end((index++ % 10).toString().repeat(1024 * 1024)); +}); + +const countdown = new Countdown(NUM_CLIENTS, () => { + server.close(); +}); + +server.listen(0, common.mustCall(() => { + for (let i = 0; i < NUM_CLIENTS; i++) { + cls.run(new Map(), common.mustCall(() => { + const options = { port: server.address().port }; + const req = http.get(options, common.mustCall((res) => { + const store = cls.getStore(); + store.set('data', ''); + + // Make ondata and onend non-closure + // functions and fully dependent on ALS + res.setEncoding('utf8'); + res.on('data', ondata); + res.on('end', common.mustCall(onend)); + })); + req.end(); + })); + } +})); + +// Accumulate the current data chunk with the store data +function ondata(d) { + const store = cls.getStore(); + assert.notStrictEqual(store, undefined); + let chunk = store.get('data'); + chunk += d; + store.set('data', chunk); +} + +// Retrieve the store data, and test for homogeneity +function onend() { + const store = cls.getStore(); + assert.notStrictEqual(store, undefined); + const data = store.get('data'); + assert.strictEqual(data, data[0].repeat(data.length)); + countdown.dec(); +} diff --git a/test/parallel/test-async-local-storage-snapshot-frame.js b/test/parallel/test-async-local-storage-snapshot-frame.js new file mode 100644 index 00000000000000..e9479a7acc9698 --- /dev/null +++ b/test/parallel/test-async-local-storage-snapshot-frame.js @@ -0,0 +1,17 @@ +// Flags: --experimental-async-context-frame +'use strict'; + +const common = require('../common'); +const { strictEqual } = require('assert'); +const { AsyncLocalStorage } = require('async_hooks'); + +const asyncLocalStorage = new AsyncLocalStorage(); +const runInAsyncScope = + asyncLocalStorage.run(123, common.mustCall(() => AsyncLocalStorage.snapshot())); +const result = + asyncLocalStorage.run(321, common.mustCall(() => { + return runInAsyncScope(() => { + return asyncLocalStorage.getStore(); + }); + })); +strictEqual(result, 123);