-
Notifications
You must be signed in to change notification settings - Fork 29.8k
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
[DO NOT MERGE] test: duplicate async_hooks tests in esm #44323
Draft
GeoffreyBooth
wants to merge
1
commit into
nodejs:main
Choose a base branch
from
GeoffreyBooth:async-hooks-tests-in-esm
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import '../common/index.mjs'; | ||
import assert, { ok, strictEqual } from 'assert'; | ||
|
||
/** | ||
* Checks the expected invocations against the invocations that actually | ||
* occurred. | ||
* | ||
* @name checkInvocations | ||
* @function | ||
* @param {object} activity including timestamps for each life time event, | ||
* i.e. init, before ... | ||
* @param {object} hooks the expected life time event invocations with a count | ||
* indicating how often they should have been invoked, | ||
* i.e. `{ init: 1, before: 2, after: 2 }` | ||
* @param {string} stage the name of the stage in the test at which we are | ||
* checking the invocations | ||
*/ | ||
export function checkInvocations(activity, hooks, stage) { | ||
const stageInfo = `Checking invocations at stage "${stage}":\n `; | ||
|
||
ok(activity != null, | ||
`${stageInfo} Trying to check invocation for an activity, ` + | ||
'but it was empty/undefined.' | ||
); | ||
|
||
// Check that actual invocations for all hooks match the expected invocations | ||
[ 'init', 'before', 'after', 'destroy', 'promiseResolve' ].forEach(checkHook); | ||
|
||
function checkHook(k) { | ||
const val = hooks[k]; | ||
// Not expected ... all good | ||
if (val == null) return; | ||
|
||
if (val === 0) { | ||
// Didn't expect any invocations, but it was actually invoked | ||
const invocations = activity[k].length; | ||
const msg = `${stageInfo} Called "${k}" ${invocations} time(s), ` + | ||
'but expected no invocations.'; | ||
assert(activity[k] === null && activity[k] === undefined, msg); | ||
} else { | ||
// Expected some invocations, make sure that it was invoked at all | ||
const msg1 = `${stageInfo} Never called "${k}", ` + | ||
`but expected ${val} invocation(s).`; | ||
assert(activity[k] !== null && activity[k] !== undefined, msg1); | ||
|
||
// Now make sure that the expected count and | ||
// the actual invocation count match | ||
const msg2 = `${stageInfo} Called "${k}" ${activity[k].length} ` + | ||
`time(s), but expected ${val} invocation(s).`; | ||
strictEqual(activity[k].length, val, msg2); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,247 @@ | ||
// Flags: --expose-gc | ||
|
||
import { isMainThread } from '../common/index.mjs'; | ||
import { fail } from 'assert'; | ||
import { createHook } from 'async_hooks'; | ||
import process, { _rawDebug as print } from 'process'; | ||
import { inspect as utilInspect } from 'util'; | ||
|
||
if (typeof globalThis.gc === 'function') { | ||
(function exity(cntr) { | ||
process.once('beforeExit', () => { | ||
globalThis.gc(); | ||
if (cntr < 4) setImmediate(() => exity(cntr + 1)); | ||
}); | ||
})(0); | ||
} | ||
|
||
function noop() {} | ||
|
||
class ActivityCollector { | ||
constructor(start, { | ||
allowNoInit = false, | ||
oninit, | ||
onbefore, | ||
onafter, | ||
ondestroy, | ||
onpromiseResolve, | ||
logid = null, | ||
logtype = null | ||
} = {}) { | ||
this._start = start; | ||
this._allowNoInit = allowNoInit; | ||
this._activities = new Map(); | ||
this._logid = logid; | ||
this._logtype = logtype; | ||
|
||
// Register event handlers if provided | ||
this.oninit = typeof oninit === 'function' ? oninit : noop; | ||
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 = createHook({ | ||
init: this._init.bind(this), | ||
before: this._before.bind(this), | ||
after: this._after.bind(this), | ||
destroy: this._destroy.bind(this), | ||
promiseResolve: this._promiseResolve.bind(this) | ||
}); | ||
} | ||
|
||
enable() { | ||
this._asyncHook.enable(); | ||
} | ||
|
||
disable() { | ||
this._asyncHook.disable(); | ||
} | ||
|
||
sanityCheck(types) { | ||
if (types != null && !Array.isArray(types)) types = [ types ]; | ||
|
||
function activityString(a) { | ||
return utilInspect(a, false, 5, true); | ||
} | ||
|
||
const violations = []; | ||
let tempActivityString; | ||
|
||
function v(msg) { violations.push(msg); } | ||
for (const a of this._activities.values()) { | ||
tempActivityString = activityString(a); | ||
if (types != null && !types.includes(a.type)) continue; | ||
|
||
if (a.init && a.init.length > 1) { | ||
v(`Activity inited twice\n${tempActivityString}` + | ||
'\nExpected "init" to be called at most once'); | ||
} | ||
if (a.destroy && a.destroy.length > 1) { | ||
v(`Activity destroyed twice\n${tempActivityString}` + | ||
'\nExpected "destroy" to be called at most once'); | ||
} | ||
if (a.before && a.after) { | ||
if (a.before.length < a.after.length) { | ||
v('Activity called "after" without calling "before"\n' + | ||
`${tempActivityString}` + | ||
'\nExpected no "after" call without a "before"'); | ||
} | ||
if (a.before.some((x, idx) => x > a.after[idx])) { | ||
v('Activity had an instance where "after" ' + | ||
'was invoked before "before"\n' + | ||
`${tempActivityString}` + | ||
'\nExpected "after" to be called after "before"'); | ||
} | ||
} | ||
if (a.before && a.destroy) { | ||
if (a.before.some((x, idx) => x > a.destroy[idx])) { | ||
v('Activity had an instance where "destroy" ' + | ||
'was invoked before "before"\n' + | ||
`${tempActivityString}` + | ||
'\nExpected "destroy" to be called after "before"'); | ||
} | ||
} | ||
if (a.after && a.destroy) { | ||
if (a.after.some((x, idx) => x > a.destroy[idx])) { | ||
v('Activity had an instance where "destroy" ' + | ||
'was invoked before "after"\n' + | ||
`${tempActivityString}` + | ||
'\nExpected "destroy" to be called after "after"'); | ||
} | ||
} | ||
if (!a.handleIsObject) { | ||
v(`No resource object\n${tempActivityString}` + | ||
'\nExpected "init" to be called with a resource object'); | ||
} | ||
} | ||
if (violations.length) { | ||
console.error(violations.join('\n\n') + '\n'); | ||
fail(`${violations.length} failed sanity checks`); | ||
} | ||
} | ||
|
||
inspect(opts = {}) { | ||
if (typeof opts === 'string') opts = { types: opts }; | ||
const { types = null, depth = 5, stage = null } = opts; | ||
const activities = types == null ? | ||
Array.from(this._activities.values()) : | ||
this.activitiesOfTypes(types); | ||
|
||
if (stage != null) console.log(`\n${stage}`); | ||
console.log(utilInspect(activities, false, depth, true)); | ||
} | ||
|
||
activitiesOfTypes(types) { | ||
if (!Array.isArray(types)) types = [ types ]; | ||
return this.activities.filter((x) => types.includes(x.type)); | ||
} | ||
|
||
get activities() { | ||
return Array.from(this._activities.values()); | ||
} | ||
|
||
_stamp(h, hook) { | ||
if (h == null) return; | ||
if (h[hook] == null) h[hook] = []; | ||
const time = process.hrtime(this._start); | ||
h[hook].push((time[0] * 1e9) + time[1]); | ||
} | ||
|
||
_getActivity(uid, hook) { | ||
const h = this._activities.get(uid); | ||
if (!h) { | ||
// If we allowed handles without init we ignore any further life time | ||
// events this makes sense for a few tests in which we enable some hooks | ||
// later | ||
if (this._allowNoInit) { | ||
const stub = { uid, type: 'Unknown', handleIsObject: true, handle: {} }; | ||
this._activities.set(uid, stub); | ||
return stub; | ||
} else if (!isMainThread) { | ||
// Worker threads start main script execution inside of an AsyncWrap | ||
// callback, so we don't yield errors for these. | ||
return null; | ||
} | ||
const err = new Error(`Found a handle whose ${hook}` + | ||
' hook was invoked but not its init hook'); | ||
throw err; | ||
} | ||
return h; | ||
} | ||
|
||
_init(uid, type, triggerAsyncId, handle) { | ||
const activity = { | ||
uid, | ||
type, | ||
triggerAsyncId, | ||
// In some cases (e.g. Timeout) the handle is a function, thus the usual | ||
// `typeof handle === 'object' && handle !== null` check can't be used. | ||
handleIsObject: handle instanceof Object, | ||
handle | ||
}; | ||
this._stamp(activity, 'init'); | ||
this._activities.set(uid, activity); | ||
this._maybeLog(uid, type, 'init'); | ||
this.oninit(uid, type, triggerAsyncId, handle); | ||
} | ||
|
||
_before(uid) { | ||
const h = this._getActivity(uid, 'before'); | ||
this._stamp(h, 'before'); | ||
this._maybeLog(uid, h && h.type, 'before'); | ||
this.onbefore(uid); | ||
} | ||
|
||
_after(uid) { | ||
const h = this._getActivity(uid, 'after'); | ||
this._stamp(h, 'after'); | ||
this._maybeLog(uid, h && h.type, 'after'); | ||
this.onafter(uid); | ||
} | ||
|
||
_destroy(uid) { | ||
const h = this._getActivity(uid, 'destroy'); | ||
this._stamp(h, 'destroy'); | ||
this._maybeLog(uid, h && h.type, 'destroy'); | ||
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)) { | ||
print(`${this._logid}.${name}.uid-${uid}`); | ||
} | ||
} | ||
} | ||
|
||
export default function initHooks({ | ||
oninit, | ||
onbefore, | ||
onafter, | ||
ondestroy, | ||
onpromiseResolve, | ||
allowNoInit, | ||
logid, | ||
logtype | ||
} = {}) { | ||
return new ActivityCollector(process.hrtime(), { | ||
oninit, | ||
onbefore, | ||
onafter, | ||
ondestroy, | ||
onpromiseResolve, | ||
allowNoInit, | ||
logid, | ||
logtype | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
import { platformTimeout, mustCall } from '../common/index.mjs'; | ||
|
||
// 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 | ||
|
||
import assert, { strictEqual } from 'assert'; | ||
import process from 'process'; | ||
import initHooks from './init-hooks.mjs'; | ||
|
||
import { promisify } from 'util'; | ||
|
||
const sleep = 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) { | ||
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; | ||
} | ||
|
||
strictEqual(promisesExecutionState.get(asyncId), 'before', | ||
'after hook called for promise without prior call' + | ||
'to before hook'); | ||
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 = platformTimeout(10); | ||
|
||
function checkPromisesInitState() { | ||
for (const initState of promisesInitState.values()) { | ||
// Promise should not be initialized without being resolved. | ||
strictEqual(initState, 'resolved'); | ||
} | ||
} | ||
|
||
function checkPromisesExecutionState() { | ||
for (const executionState of promisesExecutionState.values()) { | ||
// Check for mismatch between before and after hook calls. | ||
strictEqual(executionState, 'after'); | ||
} | ||
} | ||
|
||
process.on('beforeExit', mustCall(() => { | ||
hooks.disable(); | ||
hooks.sanityCheck('PROMISE'); | ||
|
||
checkPromisesInitState(); | ||
checkPromisesExecutionState(); | ||
})); | ||
|
||
async function asyncFunc() { | ||
await sleep(timeout); | ||
} | ||
|
||
asyncFunc(); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
nit: since we are rewriting the test, should we use
node:
prefix for node modules? Go with whichever form you prefer.