Skip to content

Commit e3e56f1

Browse files
thlorenzaddaleax
authored andcommitted
test: adding tests for initHooks API
Async wrap providers tested: - crypto.randomBytes - crypto.pbkdf2 - fs event wrap - fsreqwrap access - fsreqwrap readFile - getaddrinforeq wrap - getnameinforeq wrap - pipe connect wrap - query wrap - pipewrap - processwrap - shutdown wrap - tcpwrap - udpwrap - send wrap - detailed signal wrap - statwatcher - timerwrap via setTimeout - timerwrap via setInterval - for Immediate - http parser request - http parser response - connection via ssl server - tls wrap - write wrap - ttywrap via readstream - ttywrap via wriream - zctx via zlib binding deflate Embedder API: - async-event tests - one test looks at the happy paths - another ensures that in cases of events emitted in an order that doesn't make sense, the order is enforced by async hooks throwing a meaningful error - embedder enforcement tests are split up since async hook stack corruption now the process - therefore we launch a child and check for error output of the offending code Additional tests: - tests that show that we can enable/disable hooks inside their lifetime events - tests that verify the graph of resources triggering the creation of other resources Test Helpers: - init-hooks: - returns one collector instance - when created an async hook is created and the lifetime events are registered to call the appropriate collector functions - the collector also exposes `enable` and `disable` functions which call through to the async hook - hook checks: - checks invocations of life time hooks against the actual invocations that were collected - in some cases like `destroy` a min/max range of invocations can be supplied since in these cases the exact number is non-deterministic - verify graph: - verifies the triggerIds of specific async resources are as expected, i.e. the creation of resources was triggered by the resource we expect - includes a printGraph function to generate easily readable test input for verify graph - both functions prune TickObjects to create less brittle and easier to understand tests PR-URL: #12892 Ref: #11883 Ref: #8531 Reviewed-By: Andreas Madsen <[email protected]> Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Sam Roberts <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Refael Ackermann <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Jeremiah Senkpiel <[email protected]>
1 parent 4a7233c commit e3e56f1

Some content is hidden

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

52 files changed

+3322
-0
lines changed

test/async-hooks/coverage.md

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
## AsyncHooks Coverage Overview
2+
3+
Showing which kind of async resource is covered by which test:
4+
5+
| Resource Type | Test |
6+
|----------------------|----------------------------------------|
7+
| CONNECTION | test-connection.ssl.js |
8+
| FSEVENTWRAP | test-fseventwrap.js |
9+
| FSREQWRAP | test-fsreqwrap-{access,readFile}.js |
10+
| GETADDRINFOREQWRAP | test-getaddrinforeqwrap.js |
11+
| GETNAMEINFOREQWRAP | test-getnameinforeqwrap.js |
12+
| HTTPPARSER | test-httpparser.{request,response}.js |
13+
| Immediate | test-immediate.js |
14+
| JSSTREAM | TODO (crashes when accessing directly) |
15+
| PBKDF2REQUEST | test-crypto-pbkdf2.js |
16+
| PIPECONNECTWRAP | test-pipeconnectwrap.js |
17+
| PIPEWRAP | test-pipewrap.js |
18+
| PROCESSWRAP | test-pipewrap.js |
19+
| QUERYWRAP | test-querywrap.js |
20+
| RANDOMBYTESREQUEST | test-crypto-randomBytes.js |
21+
| SHUTDOWNWRAP | test-shutdownwrap.js |
22+
| SIGNALWRAP | test-signalwrap.js |
23+
| STATWATCHER | test-statwatcher.js |
24+
| TCPCONNECTWRAP | test-tcpwrap.js |
25+
| TCPWRAP | test-tcpwrap.js |
26+
| TIMERWRAP | test-timerwrap.set{Timeout,Interval}.js|
27+
| TLSWRAP | test-tlswrap.js |
28+
| TTYWRAP | test-ttywrap.{read,write}stream.js |
29+
| UDPSENDWRAP | test-udpsendwrap.js |
30+
| UDPWRAP | test-udpwrap.js |
31+
| WRITEWRAP | test-writewrap.js |
32+
| ZLIB | test-zlib.zlib-binding.deflate.js |

test/async-hooks/hook-checks.js

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

test/async-hooks/init-hooks.js

+219
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
'use strict';
2+
// Flags: --expose-gc
3+
4+
const assert = require('assert');
5+
const async_hooks = require('async_hooks');
6+
const util = require('util');
7+
const print = process._rawDebug;
8+
require('../common');
9+
10+
if (typeof global.gc === 'function') {
11+
(function exity(cntr) {
12+
process.once('beforeExit', () => {
13+
global.gc();
14+
if (cntr < 4) setImmediate(() => exity(cntr + 1));
15+
});
16+
})(0);
17+
}
18+
19+
function noop() {}
20+
21+
class ActivityCollector {
22+
constructor(start, {
23+
allowNoInit = false,
24+
oninit,
25+
onbefore,
26+
onafter,
27+
ondestroy,
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+
43+
// create the hook with which we'll collect activity data
44+
this._asyncHook = async_hooks.createHook({
45+
init: this._init.bind(this),
46+
before: this._before.bind(this),
47+
after: this._after.bind(this),
48+
destroy: this._destroy.bind(this)
49+
});
50+
}
51+
52+
enable() {
53+
this._asyncHook.enable();
54+
}
55+
56+
disable() {
57+
this._asyncHook.disable();
58+
}
59+
60+
sanityCheck(types) {
61+
if (types != null && !Array.isArray(types)) types = [ types ];
62+
63+
function activityString(a) {
64+
return util.inspect(a, false, 5, true);
65+
}
66+
67+
const violations = [];
68+
function v(msg) { violations.push(msg); }
69+
for (const a of this._activities.values()) {
70+
if (types != null && types.indexOf(a.type) < 0) continue;
71+
72+
if (a.init && a.init.length > 1) {
73+
v('Activity inited twice\n' + activityString(a) +
74+
'\nExpected "init" to be called at most once');
75+
}
76+
if (a.destroy && a.destroy.length > 1) {
77+
v('Activity destroyed twice\n' + activityString(a) +
78+
'\nExpected "destroy" to be called at most once');
79+
}
80+
if (a.before && a.after) {
81+
if (a.before.length < a.after.length) {
82+
v('Activity called "after" without calling "before"\n' +
83+
activityString(a) +
84+
'\nExpected no "after" call without a "before"');
85+
}
86+
if (a.before.some((x, idx) => x > a.after[idx])) {
87+
v('Activity had an instance where "after" ' +
88+
'was invoked before "before"\n' +
89+
activityString(a) +
90+
'\nExpected "after" to be called after "before"');
91+
}
92+
}
93+
if (a.before && a.destroy) {
94+
if (a.before.some((x, idx) => x > a.destroy[idx])) {
95+
v('Activity had an instance where "destroy" ' +
96+
'was invoked before "before"\n' +
97+
activityString(a) +
98+
'\nExpected "destroy" to be called after "before"');
99+
}
100+
}
101+
if (a.after && a.destroy) {
102+
if (a.after.some((x, idx) => x > a.destroy[idx])) {
103+
v('Activity had an instance where "destroy" ' +
104+
'was invoked before "after"\n' +
105+
activityString(a) +
106+
'\nExpected "destroy" to be called after "after"');
107+
}
108+
}
109+
}
110+
if (violations.length) {
111+
console.error(violations.join('\n'));
112+
assert.fail(violations.length, 0, 'Failed sanity check');
113+
}
114+
}
115+
116+
inspect(opts = {}) {
117+
if (typeof opts === 'string') opts = { types: opts };
118+
const { types = null, depth = 5, stage = null } = opts;
119+
const activities = types == null ?
120+
Array.from(this._activities.values()) :
121+
this.activitiesOfTypes(types);
122+
123+
if (stage != null) console.log('\n%s', stage);
124+
console.log(util.inspect(activities, false, depth, true));
125+
}
126+
127+
activitiesOfTypes(types) {
128+
if (!Array.isArray(types)) types = [ types ];
129+
return this.activities.filter((x) => types.indexOf(x.type) >= 0);
130+
}
131+
132+
get activities() {
133+
return Array.from(this._activities.values());
134+
}
135+
136+
_stamp(h, hook) {
137+
if (h == null) return;
138+
if (h[hook] == null) h[hook] = [];
139+
const time = process.hrtime(this._start);
140+
h[hook].push((time[0] * 1e9) + time[1]);
141+
}
142+
143+
_getActivity(uid, hook) {
144+
const h = this._activities.get(uid);
145+
if (!h) {
146+
// if we allowed handles without init we ignore any further life time
147+
// events this makes sense for a few tests in which we enable some hooks
148+
// later
149+
if (this._allowNoInit) {
150+
const stub = { uid, type: 'Unknown' };
151+
this._activities.set(uid, stub);
152+
return stub;
153+
} else {
154+
const err = new Error('Found a handle who\'s ' + hook +
155+
' hook was invoked but not it\'s init hook');
156+
// Don't throw if we see invocations due to an assertion in a test
157+
// failing since we want to list the assertion failure instead
158+
if (/process\._fatalException/.test(err.stack)) return null;
159+
throw err;
160+
}
161+
}
162+
return h;
163+
}
164+
165+
_init(uid, type, triggerId, handle) {
166+
const activity = { uid, type, triggerId };
167+
this._stamp(activity, 'init');
168+
this._activities.set(uid, activity);
169+
this._maybeLog(uid, type, 'init');
170+
this.oninit(uid, type, triggerId, handle);
171+
}
172+
173+
_before(uid) {
174+
const h = this._getActivity(uid, 'before');
175+
this._stamp(h, 'before');
176+
this._maybeLog(uid, h && h.type, 'before');
177+
this.onbefore(uid);
178+
}
179+
180+
_after(uid) {
181+
const h = this._getActivity(uid, 'after');
182+
this._stamp(h, 'after');
183+
this._maybeLog(uid, h && h.type, 'after');
184+
this.onafter(uid);
185+
}
186+
187+
_destroy(uid) {
188+
const h = this._getActivity(uid, 'destroy');
189+
this._stamp(h, 'destroy');
190+
this._maybeLog(uid, h && h.type, 'destroy');
191+
this.ondestroy(uid);
192+
}
193+
194+
_maybeLog(uid, type, name) {
195+
if (this._logid &&
196+
(type == null || this._logtype == null || this._logtype === type)) {
197+
print(this._logid + '.' + name + '.uid-' + uid);
198+
}
199+
}
200+
}
201+
202+
exports = module.exports = function initHooks({
203+
oninit,
204+
onbefore,
205+
onafter,
206+
ondestroy,
207+
allowNoInit,
208+
logid,
209+
logtype } = {}) {
210+
return new ActivityCollector(process.hrtime(), {
211+
oninit,
212+
onbefore,
213+
onafter,
214+
ondestroy,
215+
allowNoInit,
216+
logid,
217+
logtype
218+
});
219+
};
+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
'use strict';
2+
3+
const initHooks = require('./init-hooks');
4+
const tick = require('./tick');
5+
const common = require('../common');
6+
const assert = require('assert');
7+
const { checkInvocations } = require('./hook-checks');
8+
9+
if (!common.hasCrypto) {
10+
common.skip('missing crypto');
11+
return;
12+
}
13+
14+
const tls = require('tls');
15+
const Connection = process.binding('crypto').Connection;
16+
const hooks = initHooks();
17+
hooks.enable();
18+
19+
function createServerConnection(
20+
onhandshakestart,
21+
certificate = null,
22+
isServer = true,
23+
servername = 'some server',
24+
rejectUnauthorized
25+
) {
26+
if (certificate == null) certificate = tls.createSecureContext();
27+
const ssl = new Connection(
28+
certificate.context, isServer, servername, rejectUnauthorized
29+
);
30+
if (isServer) {
31+
ssl.onhandshakestart = onhandshakestart;
32+
ssl.lastHandshakeTime = 0;
33+
}
34+
return ssl;
35+
}
36+
37+
// creating first server connection
38+
const sc1 = createServerConnection(common.mustCall(onfirstHandShake));
39+
40+
let as = hooks.activitiesOfTypes('CONNECTION');
41+
assert.strictEqual(as.length, 1,
42+
'one CONNECTION after first connection created');
43+
const f1 = as[0];
44+
assert.strictEqual(f1.type, 'CONNECTION', 'connection');
45+
assert.strictEqual(typeof f1.uid, 'number', 'uid is a number');
46+
assert.strictEqual(typeof f1.triggerId, 'number', 'triggerId is a number');
47+
checkInvocations(f1, { init: 1 }, 'first connection, when first created');
48+
49+
// creating second server connection
50+
const sc2 = createServerConnection(common.mustCall(onsecondHandShake));
51+
52+
as = hooks.activitiesOfTypes('CONNECTION');
53+
assert.strictEqual(as.length, 2,
54+
'two CONNECTIONs after second connection created');
55+
const f2 = as[1];
56+
assert.strictEqual(f2.type, 'CONNECTION', 'connection');
57+
assert.strictEqual(typeof f2.uid, 'number', 'uid is a number');
58+
assert.strictEqual(typeof f2.triggerId, 'number', 'triggerId is a number');
59+
checkInvocations(f1, { init: 1 }, 'first connection, when second created');
60+
checkInvocations(f2, { init: 1 }, 'second connection, when second created');
61+
62+
// starting the connections which results in handshake starts
63+
sc1.start();
64+
sc2.start();
65+
66+
function onfirstHandShake() {
67+
checkInvocations(f1, { init: 1, before: 1 },
68+
'first connection, when first handshake');
69+
checkInvocations(f2, { init: 1 }, 'second connection, when first handshake');
70+
}
71+
72+
function onsecondHandShake() {
73+
checkInvocations(f1, { init: 1, before: 1, after: 1 },
74+
'first connection, when second handshake');
75+
checkInvocations(f2, { init: 1, before: 1 },
76+
'second connection, when second handshake');
77+
tick(1E4);
78+
}
79+
80+
process.on('exit', onexit);
81+
82+
function onexit() {
83+
hooks.disable();
84+
hooks.sanityCheck('CONNECTION');
85+
86+
checkInvocations(f1, { init: 1, before: 1, after: 1 },
87+
'first connection, when process exits');
88+
checkInvocations(f2, { init: 1, before: 1, after: 1 },
89+
'second connection, when process exits');
90+
}

0 commit comments

Comments
 (0)