Skip to content

Commit 7156085

Browse files
committed
test: duplicate async_hooks tests in esm
1 parent 6d61f75 commit 7156085

File tree

90 files changed

+5013
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

90 files changed

+5013
-0
lines changed

test/async-hooks/hook-checks.mjs

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import '../common/index.mjs';
2+
import assert, { ok, strictEqual } from 'assert';
3+
4+
/**
5+
* Checks the expected invocations against the invocations that actually
6+
* occurred.
7+
*
8+
* @name checkInvocations
9+
* @function
10+
* @param {object} activity including timestamps for each life time event,
11+
* i.e. init, before ...
12+
* @param {object} hooks the expected life time event invocations with a count
13+
* indicating how often they should have been invoked,
14+
* i.e. `{ init: 1, before: 2, after: 2 }`
15+
* @param {string} stage the name of the stage in the test at which we are
16+
* checking the invocations
17+
*/
18+
export function checkInvocations(activity, hooks, stage) {
19+
const stageInfo = `Checking invocations at stage "${stage}":\n `;
20+
21+
ok(activity != null,
22+
`${stageInfo} Trying to check invocation for an activity, ` +
23+
'but it was empty/undefined.'
24+
);
25+
26+
// Check that actual invocations for all hooks match the expected invocations
27+
[ 'init', 'before', 'after', 'destroy', 'promiseResolve' ].forEach(checkHook);
28+
29+
function checkHook(k) {
30+
const val = hooks[k];
31+
// Not expected ... all good
32+
if (val == null) return;
33+
34+
if (val === 0) {
35+
// Didn't expect any invocations, but it was actually invoked
36+
const invocations = activity[k].length;
37+
const msg = `${stageInfo} Called "${k}" ${invocations} time(s), ` +
38+
'but expected no invocations.';
39+
assert(activity[k] === null && activity[k] === undefined, msg);
40+
} else {
41+
// Expected some invocations, make sure that it was invoked at all
42+
const msg1 = `${stageInfo} Never called "${k}", ` +
43+
`but expected ${val} invocation(s).`;
44+
assert(activity[k] !== null && activity[k] !== undefined, msg1);
45+
46+
// Now make sure that the expected count and
47+
// the actual invocation count match
48+
const msg2 = `${stageInfo} Called "${k}" ${activity[k].length} ` +
49+
`time(s), but expected ${val} invocation(s).`;
50+
strictEqual(activity[k].length, val, msg2);
51+
}
52+
}
53+
}

test/async-hooks/init-hooks.mjs

+247
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
// Flags: --expose-gc
2+
3+
import { isMainThread } from '../common/index.mjs';
4+
import { fail } from 'assert';
5+
import { createHook } from 'async_hooks';
6+
import process, { _rawDebug as print } from 'process';
7+
import { inspect as utilInspect } from 'util';
8+
9+
if (typeof globalThis.gc === 'function') {
10+
(function exity(cntr) {
11+
process.once('beforeExit', () => {
12+
globalThis.gc();
13+
if (cntr < 4) setImmediate(() => exity(cntr + 1));
14+
});
15+
})(0);
16+
}
17+
18+
function noop() {}
19+
20+
class ActivityCollector {
21+
constructor(start, {
22+
allowNoInit = false,
23+
oninit,
24+
onbefore,
25+
onafter,
26+
ondestroy,
27+
onpromiseResolve,
28+
logid = null,
29+
logtype = null
30+
} = {}) {
31+
this._start = start;
32+
this._allowNoInit = allowNoInit;
33+
this._activities = new Map();
34+
this._logid = logid;
35+
this._logtype = logtype;
36+
37+
// Register event handlers if provided
38+
this.oninit = typeof oninit === 'function' ? oninit : noop;
39+
this.onbefore = typeof onbefore === 'function' ? onbefore : noop;
40+
this.onafter = typeof onafter === 'function' ? onafter : noop;
41+
this.ondestroy = typeof ondestroy === 'function' ? ondestroy : noop;
42+
this.onpromiseResolve = typeof onpromiseResolve === 'function' ?
43+
onpromiseResolve : noop;
44+
45+
// Create the hook with which we'll collect activity data
46+
this._asyncHook = createHook({
47+
init: this._init.bind(this),
48+
before: this._before.bind(this),
49+
after: this._after.bind(this),
50+
destroy: this._destroy.bind(this),
51+
promiseResolve: this._promiseResolve.bind(this)
52+
});
53+
}
54+
55+
enable() {
56+
this._asyncHook.enable();
57+
}
58+
59+
disable() {
60+
this._asyncHook.disable();
61+
}
62+
63+
sanityCheck(types) {
64+
if (types != null && !Array.isArray(types)) types = [ types ];
65+
66+
function activityString(a) {
67+
return utilInspect(a, false, 5, true);
68+
}
69+
70+
const violations = [];
71+
let tempActivityString;
72+
73+
function v(msg) { violations.push(msg); }
74+
for (const a of this._activities.values()) {
75+
tempActivityString = activityString(a);
76+
if (types != null && !types.includes(a.type)) continue;
77+
78+
if (a.init && a.init.length > 1) {
79+
v(`Activity inited twice\n${tempActivityString}` +
80+
'\nExpected "init" to be called at most once');
81+
}
82+
if (a.destroy && a.destroy.length > 1) {
83+
v(`Activity destroyed twice\n${tempActivityString}` +
84+
'\nExpected "destroy" to be called at most once');
85+
}
86+
if (a.before && a.after) {
87+
if (a.before.length < a.after.length) {
88+
v('Activity called "after" without calling "before"\n' +
89+
`${tempActivityString}` +
90+
'\nExpected no "after" call without a "before"');
91+
}
92+
if (a.before.some((x, idx) => x > a.after[idx])) {
93+
v('Activity had an instance where "after" ' +
94+
'was invoked before "before"\n' +
95+
`${tempActivityString}` +
96+
'\nExpected "after" to be called after "before"');
97+
}
98+
}
99+
if (a.before && a.destroy) {
100+
if (a.before.some((x, idx) => x > a.destroy[idx])) {
101+
v('Activity had an instance where "destroy" ' +
102+
'was invoked before "before"\n' +
103+
`${tempActivityString}` +
104+
'\nExpected "destroy" to be called after "before"');
105+
}
106+
}
107+
if (a.after && a.destroy) {
108+
if (a.after.some((x, idx) => x > a.destroy[idx])) {
109+
v('Activity had an instance where "destroy" ' +
110+
'was invoked before "after"\n' +
111+
`${tempActivityString}` +
112+
'\nExpected "destroy" to be called after "after"');
113+
}
114+
}
115+
if (!a.handleIsObject) {
116+
v(`No resource object\n${tempActivityString}` +
117+
'\nExpected "init" to be called with a resource object');
118+
}
119+
}
120+
if (violations.length) {
121+
console.error(violations.join('\n\n') + '\n');
122+
fail(`${violations.length} failed sanity checks`);
123+
}
124+
}
125+
126+
inspect(opts = {}) {
127+
if (typeof opts === 'string') opts = { types: opts };
128+
const { types = null, depth = 5, stage = null } = opts;
129+
const activities = types == null ?
130+
Array.from(this._activities.values()) :
131+
this.activitiesOfTypes(types);
132+
133+
if (stage != null) console.log(`\n${stage}`);
134+
console.log(utilInspect(activities, false, depth, true));
135+
}
136+
137+
activitiesOfTypes(types) {
138+
if (!Array.isArray(types)) types = [ types ];
139+
return this.activities.filter((x) => types.includes(x.type));
140+
}
141+
142+
get activities() {
143+
return Array.from(this._activities.values());
144+
}
145+
146+
_stamp(h, hook) {
147+
if (h == null) return;
148+
if (h[hook] == null) h[hook] = [];
149+
const time = process.hrtime(this._start);
150+
h[hook].push((time[0] * 1e9) + time[1]);
151+
}
152+
153+
_getActivity(uid, hook) {
154+
const h = this._activities.get(uid);
155+
if (!h) {
156+
// If we allowed handles without init we ignore any further life time
157+
// events this makes sense for a few tests in which we enable some hooks
158+
// later
159+
if (this._allowNoInit) {
160+
const stub = { uid, type: 'Unknown', handleIsObject: true, handle: {} };
161+
this._activities.set(uid, stub);
162+
return stub;
163+
} else if (!isMainThread) {
164+
// Worker threads start main script execution inside of an AsyncWrap
165+
// callback, so we don't yield errors for these.
166+
return null;
167+
}
168+
const err = new Error(`Found a handle whose ${hook}` +
169+
' hook was invoked but not its init hook');
170+
throw err;
171+
}
172+
return h;
173+
}
174+
175+
_init(uid, type, triggerAsyncId, handle) {
176+
const activity = {
177+
uid,
178+
type,
179+
triggerAsyncId,
180+
// In some cases (e.g. Timeout) the handle is a function, thus the usual
181+
// `typeof handle === 'object' && handle !== null` check can't be used.
182+
handleIsObject: handle instanceof Object,
183+
handle
184+
};
185+
this._stamp(activity, 'init');
186+
this._activities.set(uid, activity);
187+
this._maybeLog(uid, type, 'init');
188+
this.oninit(uid, type, triggerAsyncId, handle);
189+
}
190+
191+
_before(uid) {
192+
const h = this._getActivity(uid, 'before');
193+
this._stamp(h, 'before');
194+
this._maybeLog(uid, h && h.type, 'before');
195+
this.onbefore(uid);
196+
}
197+
198+
_after(uid) {
199+
const h = this._getActivity(uid, 'after');
200+
this._stamp(h, 'after');
201+
this._maybeLog(uid, h && h.type, 'after');
202+
this.onafter(uid);
203+
}
204+
205+
_destroy(uid) {
206+
const h = this._getActivity(uid, 'destroy');
207+
this._stamp(h, 'destroy');
208+
this._maybeLog(uid, h && h.type, 'destroy');
209+
this.ondestroy(uid);
210+
}
211+
212+
_promiseResolve(uid) {
213+
const h = this._getActivity(uid, 'promiseResolve');
214+
this._stamp(h, 'promiseResolve');
215+
this._maybeLog(uid, h && h.type, 'promiseResolve');
216+
this.onpromiseResolve(uid);
217+
}
218+
219+
_maybeLog(uid, type, name) {
220+
if (this._logid &&
221+
(type == null || this._logtype == null || this._logtype === type)) {
222+
print(`${this._logid}.${name}.uid-${uid}`);
223+
}
224+
}
225+
}
226+
227+
export default function initHooks({
228+
oninit,
229+
onbefore,
230+
onafter,
231+
ondestroy,
232+
onpromiseResolve,
233+
allowNoInit,
234+
logid,
235+
logtype
236+
} = {}) {
237+
return new ActivityCollector(process.hrtime(), {
238+
oninit,
239+
onbefore,
240+
onafter,
241+
ondestroy,
242+
onpromiseResolve,
243+
allowNoInit,
244+
logid,
245+
logtype
246+
});
247+
};

test/async-hooks/test-async-await.mjs

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { platformTimeout, mustCall } from '../common/index.mjs';
2+
3+
// This test ensures async hooks are being properly called
4+
// when using async-await mechanics. This involves:
5+
// 1. Checking that all initialized promises are being resolved
6+
// 2. Checking that for each 'before' corresponding hook 'after' hook is called
7+
8+
import assert, { strictEqual } from 'assert';
9+
import process from 'process';
10+
import initHooks from './init-hooks.mjs';
11+
12+
import { promisify } from 'util';
13+
14+
const sleep = promisify(setTimeout);
15+
// Either 'inited' or 'resolved'
16+
const promisesInitState = new Map();
17+
// Either 'before' or 'after' AND asyncId must be present in the other map
18+
const promisesExecutionState = new Map();
19+
20+
const hooks = initHooks({
21+
oninit,
22+
onbefore,
23+
onafter,
24+
ondestroy: null, // Intentionally not tested, since it will be removed soon
25+
onpromiseResolve
26+
});
27+
hooks.enable();
28+
29+
function oninit(asyncId, type) {
30+
if (type === 'PROMISE') {
31+
promisesInitState.set(asyncId, 'inited');
32+
}
33+
}
34+
35+
function onbefore(asyncId) {
36+
if (!promisesInitState.has(asyncId)) {
37+
return;
38+
}
39+
promisesExecutionState.set(asyncId, 'before');
40+
}
41+
42+
function onafter(asyncId) {
43+
if (!promisesInitState.has(asyncId)) {
44+
return;
45+
}
46+
47+
strictEqual(promisesExecutionState.get(asyncId), 'before',
48+
'after hook called for promise without prior call' +
49+
'to before hook');
50+
strictEqual(promisesInitState.get(asyncId), 'resolved',
51+
'after hook called for promise without prior call' +
52+
'to resolve hook');
53+
promisesExecutionState.set(asyncId, 'after');
54+
}
55+
56+
function onpromiseResolve(asyncId) {
57+
assert(promisesInitState.has(asyncId),
58+
'resolve hook called for promise without prior call to init hook');
59+
60+
promisesInitState.set(asyncId, 'resolved');
61+
}
62+
63+
const timeout = platformTimeout(10);
64+
65+
function checkPromisesInitState() {
66+
for (const initState of promisesInitState.values()) {
67+
// Promise should not be initialized without being resolved.
68+
strictEqual(initState, 'resolved');
69+
}
70+
}
71+
72+
function checkPromisesExecutionState() {
73+
for (const executionState of promisesExecutionState.values()) {
74+
// Check for mismatch between before and after hook calls.
75+
strictEqual(executionState, 'after');
76+
}
77+
}
78+
79+
process.on('beforeExit', mustCall(() => {
80+
hooks.disable();
81+
hooks.sanityCheck('PROMISE');
82+
83+
checkPromisesInitState();
84+
checkPromisesExecutionState();
85+
}));
86+
87+
async function asyncFunc() {
88+
await sleep(timeout);
89+
}
90+
91+
asyncFunc();

0 commit comments

Comments
 (0)