diff --git a/test/async-hooks/hook-checks.js b/test/async-hooks/hook-checks.js index 2abed61555a158..44970378131c4a 100644 --- a/test/async-hooks/hook-checks.js +++ b/test/async-hooks/hook-checks.js @@ -25,7 +25,7 @@ exports.checkInvocations = function checkInvocations(activity, hooks, stage) { ); // Check that actual invocations for all hooks match the expected invocations - [ 'init', 'before', 'after', 'destroy' ].forEach(checkHook); + [ 'init', 'before', 'after', 'destroy', 'promiseResolve' ].forEach(checkHook); function checkHook(k) { const val = hooks[k]; diff --git a/test/async-hooks/init-hooks.js b/test/async-hooks/init-hooks.js index e43761df623dc4..509f443b29a671 100644 --- a/test/async-hooks/init-hooks.js +++ b/test/async-hooks/init-hooks.js @@ -25,6 +25,7 @@ class ActivityCollector { onbefore, onafter, ondestroy, + onpromiseResolve, logid = null, logtype = null } = {}) { @@ -39,13 +40,16 @@ class ActivityCollector { this.onbefore = typeof onbefore === 'function' ? onbefore : noop; this.onafter = typeof onafter === 'function' ? onafter : noop; this.ondestroy = typeof ondestroy === 'function' ? ondestroy : noop; + this.onpromiseResolve = typeof onpromiseResolve === 'function' ? + onpromiseResolve : noop; // Create the hook with which we'll collect activity data this._asyncHook = async_hooks.createHook({ init: this._init.bind(this), before: this._before.bind(this), after: this._after.bind(this), - destroy: this._destroy.bind(this) + destroy: this._destroy.bind(this), + promiseResolve: this._promiseResolve.bind(this) }); } @@ -206,6 +210,13 @@ class ActivityCollector { this.ondestroy(uid); } + _promiseResolve(uid) { + const h = this._getActivity(uid, 'promiseResolve'); + this._stamp(h, 'promiseResolve'); + this._maybeLog(uid, h && h.type, 'promiseResolve'); + this.onpromiseResolve(uid); + } + _maybeLog(uid, type, name) { if (this._logid && (type == null || this._logtype == null || this._logtype === type)) { @@ -219,6 +230,7 @@ exports = module.exports = function initHooks({ onbefore, onafter, ondestroy, + onpromiseResolve, allowNoInit, logid, logtype @@ -228,6 +240,7 @@ exports = module.exports = function initHooks({ onbefore, onafter, ondestroy, + onpromiseResolve, allowNoInit, logid, logtype diff --git a/test/async-hooks/test-async-await.js b/test/async-hooks/test-async-await.js new file mode 100644 index 00000000000000..7f88cd9b18176f --- /dev/null +++ b/test/async-hooks/test-async-await.js @@ -0,0 +1,91 @@ +'use strict'; +const common = require('../common'); + +// This test ensures async hooks are being properly called +// when using async-await mechanics. This involves: +// 1. Checking that all initialized promises are being resolved +// 2. Checking that for each 'before' corresponding hook 'after' hook is called + +const assert = require('assert'); +const initHooks = require('./init-hooks'); + +const util = require('util'); + +const sleep = util.promisify(setTimeout); +// either 'inited' or 'resolved' +const promisesInitState = new Map(); +// either 'before' or 'after' AND asyncId must be present in the other map +const promisesExecutionState = new Map(); + +const hooks = initHooks({ + oninit, + onbefore, + onafter, + ondestroy: null, // Intentionally not tested, since it will be removed soon + onpromiseResolve +}); +hooks.enable(); + +function oninit(asyncId, type, triggerAsyncId, resource) { + if (type === 'PROMISE') { + promisesInitState.set(asyncId, 'inited'); + } +} + +function onbefore(asyncId) { + if (!promisesInitState.has(asyncId)) { + return; + } + promisesExecutionState.set(asyncId, 'before'); +} + +function onafter(asyncId) { + if (!promisesInitState.has(asyncId)) { + return; + } + + assert.strictEqual(promisesExecutionState.get(asyncId), 'before', + 'after hook called for promise without prior call' + + 'to before hook'); + assert.strictEqual(promisesInitState.get(asyncId), 'resolved', + 'after hook called for promise without prior call' + + 'to resolve hook'); + promisesExecutionState.set(asyncId, 'after'); +} + +function onpromiseResolve(asyncId) { + assert(promisesInitState.has(asyncId), + 'resolve hook called for promise without prior call to init hook'); + + promisesInitState.set(asyncId, 'resolved'); +} + +const timeout = common.platformTimeout(10); + +function checkPromisesInitState() { + for (const initState of promisesInitState.values()) { + assert.strictEqual(initState, 'resolved', + 'promise initialized without being resolved'); + } +} + +function checkPromisesExecutionState() { + for (const executionState of promisesExecutionState.values()) { + assert.strictEqual(executionState, 'after', + 'mismatch between before and after hook calls'); + } +} + +process.on('beforeExit', common.mustCall(() => { + hooks.disable(); + hooks.sanityCheck('PROMISE'); + + checkPromisesInitState(); + checkPromisesExecutionState(); +})); + +async function asyncFunc() { + await sleep(timeout); +} + +asyncFunc();