Permalink
Browse files

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 <amwebdk@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Sam Roberts <vieuxtech@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Refael Ackermann <refack@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jeremiah Senkpiel <fishrock123@rocketmail.com>
  • Loading branch information...
thlorenz authored and addaleax committed Nov 22, 2016
1 parent 4a7233c commit e3e56f1d71fd496c8a25d6b4b50e51d4a682d184
Showing with 3,322 additions and 0 deletions.
  1. +32 −0 test/async-hooks/coverage.md
  2. +54 −0 test/async-hooks/hook-checks.js
  3. +219 −0 test/async-hooks/init-hooks.js
  4. +90 −0 test/async-hooks/test-connection.ssl.js
  5. +42 −0 test/async-hooks/test-crypto-pbkdf2.js
  6. +43 −0 test/async-hooks/test-crypto-randomBytes.js
  7. +48 −0 test/async-hooks/test-embedder.api.async-event.after-on-destroyed.js
  8. +48 −0 test/async-hooks/test-embedder.api.async-event.before-on-destroyed.js
  9. +46 −0 test/async-hooks/test-embedder.api.async-event.improper-order.js
  10. +55 −0 test/async-hooks/test-embedder.api.async-event.improper-unwind.js
  11. +85 −0 test/async-hooks/test-embedder.api.async-event.js
  12. +274 −0 test/async-hooks/test-enable-disable.js
  13. +33 −0 test/async-hooks/test-fseventwrap.js
  14. +37 −0 test/async-hooks/test-fsreqwrap-access.js
  15. +48 −0 test/async-hooks/test-fsreqwrap-readFile.js
  16. +39 −0 test/async-hooks/test-getaddrinforeqwrap.js
  17. +40 −0 test/async-hooks/test-getnameinforeqwrap.js
  18. +56 −0 test/async-hooks/test-graph.connection.js
  19. +26 −0 test/async-hooks/test-graph.fsreq-readFile.js
  20. +37 −0 test/async-hooks/test-graph.intervals.js
  21. +32 −0 test/async-hooks/test-graph.pipe.js
  22. +37 −0 test/async-hooks/test-graph.pipeconnect.js
  23. +49 −0 test/async-hooks/test-graph.shutdown.js
  24. +54 −0 test/async-hooks/test-graph.signal.js
  25. +34 −0 test/async-hooks/test-graph.statwatcher.js
  26. +51 −0 test/async-hooks/test-graph.tcp.js
  27. +35 −0 test/async-hooks/test-graph.timeouts.js
  28. +79 −0 test/async-hooks/test-graph.tls-write.js
  29. +58 −0 test/async-hooks/test-httpparser.request.js
  30. +68 −0 test/async-hooks/test-httpparser.response.js
  31. +66 −0 test/async-hooks/test-immediate.js
  32. +104 −0 test/async-hooks/test-pipeconnectwrap.js
  33. +89 −0 test/async-hooks/test-pipewrap.js
  34. +40 −0 test/async-hooks/test-querywrap.js
  35. +69 −0 test/async-hooks/test-shutdownwrap.js
  36. +91 −0 test/async-hooks/test-signalwrap.js
  37. +64 −0 test/async-hooks/test-statwatcher.js
  38. +177 −0 test/async-hooks/test-tcpwrap.js
  39. +56 −0 test/async-hooks/test-timerwrap.setInterval.js
  40. +78 −0 test/async-hooks/test-timerwrap.setTimeout.js
  41. +133 −0 test/async-hooks/test-tlswrap.js
  42. +42 −0 test/async-hooks/test-ttywrap.readstream.js
  43. +62 −0 test/async-hooks/test-ttywrap.writestream.js
  44. +58 −0 test/async-hooks/test-udpsendwrap.js
  45. +38 −0 test/async-hooks/test-udpwrap.js
  46. +98 −0 test/async-hooks/test-writewrap.js
  47. +62 −0 test/async-hooks/test-zlib.zlib-binding.deflate.js
  48. +6 −0 test/async-hooks/testcfg.py
  49. +13 −0 test/async-hooks/tick.js
  50. +114 −0 test/async-hooks/verify-graph.js
  51. +12 −0 test/testpy/__init__.py
  52. +1 −0 tools/test.py
@@ -0,0 +1,32 @@
## AsyncHooks Coverage Overview
Showing which kind of async resource is covered by which test:
| Resource Type | Test |
|----------------------|----------------------------------------|
| CONNECTION | test-connection.ssl.js |
| FSEVENTWRAP | test-fseventwrap.js |
| FSREQWRAP | test-fsreqwrap-{access,readFile}.js |
| GETADDRINFOREQWRAP | test-getaddrinforeqwrap.js |
| GETNAMEINFOREQWRAP | test-getnameinforeqwrap.js |
| HTTPPARSER | test-httpparser.{request,response}.js |
| Immediate | test-immediate.js |
| JSSTREAM | TODO (crashes when accessing directly) |
| PBKDF2REQUEST | test-crypto-pbkdf2.js |
| PIPECONNECTWRAP | test-pipeconnectwrap.js |
| PIPEWRAP | test-pipewrap.js |
| PROCESSWRAP | test-pipewrap.js |
| QUERYWRAP | test-querywrap.js |
| RANDOMBYTESREQUEST | test-crypto-randomBytes.js |
| SHUTDOWNWRAP | test-shutdownwrap.js |
| SIGNALWRAP | test-signalwrap.js |
| STATWATCHER | test-statwatcher.js |
| TCPCONNECTWRAP | test-tcpwrap.js |
| TCPWRAP | test-tcpwrap.js |
| TIMERWRAP | test-timerwrap.set{Timeout,Interval}.js|
| TLSWRAP | test-tlswrap.js |
| TTYWRAP | test-ttywrap.{read,write}stream.js |
| UDPSENDWRAP | test-udpsendwrap.js |
| UDPWRAP | test-udpwrap.js |
| WRITEWRAP | test-writewrap.js |
| ZLIB | test-zlib.zlib-binding.deflate.js |
@@ -0,0 +1,54 @@
'use strict';
const assert = require('assert');
require('../common');
/**
* 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 oftn 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
*/
exports.checkInvocations = function checkInvocations(activity, hooks, stage) {
const stageInfo = `Checking invocations at stage "${stage}":\n `;
assert.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' ].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).`;
assert.strictEqual(activity[k].length, val, msg2);
}
}
};
@@ -0,0 +1,219 @@
'use strict';
// Flags: --expose-gc
const assert = require('assert');
const async_hooks = require('async_hooks');
const util = require('util');
const print = process._rawDebug;
require('../common');
if (typeof global.gc === 'function') {
(function exity(cntr) {
process.once('beforeExit', () => {
global.gc();
if (cntr < 4) setImmediate(() => exity(cntr + 1));
});
})(0);
}
function noop() {}
class ActivityCollector {
constructor(start, {
allowNoInit = false,
oninit,
onbefore,
onafter,
ondestroy,
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;
// 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)
});
}
enable() {
this._asyncHook.enable();
}
disable() {
this._asyncHook.disable();
}
sanityCheck(types) {
if (types != null && !Array.isArray(types)) types = [ types ];
function activityString(a) {
return util.inspect(a, false, 5, true);
}
const violations = [];
function v(msg) { violations.push(msg); }
for (const a of this._activities.values()) {
if (types != null && types.indexOf(a.type) < 0) continue;
if (a.init && a.init.length > 1) {
v('Activity inited twice\n' + activityString(a) +
'\nExpected "init" to be called at most once');
}
if (a.destroy && a.destroy.length > 1) {
v('Activity destroyed twice\n' + activityString(a) +
'\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' +
activityString(a) +
'\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' +
activityString(a) +
'\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' +
activityString(a) +
'\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' +
activityString(a) +
'\nExpected "destroy" to be called after "after"');
}
}
}
if (violations.length) {
console.error(violations.join('\n'));
assert.fail(violations.length, 0, 'Failed sanity check');
}
}
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%s', stage);
console.log(util.inspect(activities, false, depth, true));
}
activitiesOfTypes(types) {
if (!Array.isArray(types)) types = [ types ];
return this.activities.filter((x) => types.indexOf(x.type) >= 0);
}
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' };
this._activities.set(uid, stub);
return stub;
} else {
const err = new Error('Found a handle who\'s ' + hook +
' hook was invoked but not it\'s init hook');
// Don't throw if we see invocations due to an assertion in a test
// failing since we want to list the assertion failure instead
if (/process\._fatalException/.test(err.stack)) return null;
throw err;
}
}
return h;
}
_init(uid, type, triggerId, handle) {
const activity = { uid, type, triggerId };
this._stamp(activity, 'init');
this._activities.set(uid, activity);
this._maybeLog(uid, type, 'init');
this.oninit(uid, type, triggerId, 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);
}
_maybeLog(uid, type, name) {
if (this._logid &&
(type == null || this._logtype == null || this._logtype === type)) {
print(this._logid + '.' + name + '.uid-' + uid);
}
}
}
exports = module.exports = function initHooks({
oninit,
onbefore,
onafter,
ondestroy,
allowNoInit,
logid,
logtype } = {}) {
return new ActivityCollector(process.hrtime(), {
oninit,
onbefore,
onafter,
ondestroy,
allowNoInit,
logid,
logtype
});
};
@@ -0,0 +1,90 @@
'use strict';
const initHooks = require('./init-hooks');
const tick = require('./tick');
const common = require('../common');
const assert = require('assert');
const { checkInvocations } = require('./hook-checks');
if (!common.hasCrypto) {
common.skip('missing crypto');
return;
}
const tls = require('tls');
const Connection = process.binding('crypto').Connection;
const hooks = initHooks();
hooks.enable();
function createServerConnection(
onhandshakestart,
certificate = null,
isServer = true,
servername = 'some server',
rejectUnauthorized
) {
if (certificate == null) certificate = tls.createSecureContext();
const ssl = new Connection(
certificate.context, isServer, servername, rejectUnauthorized
);
if (isServer) {
ssl.onhandshakestart = onhandshakestart;
ssl.lastHandshakeTime = 0;
}
return ssl;
}
// creating first server connection
const sc1 = createServerConnection(common.mustCall(onfirstHandShake));
let as = hooks.activitiesOfTypes('CONNECTION');
assert.strictEqual(as.length, 1,
'one CONNECTION after first connection created');
const f1 = as[0];
assert.strictEqual(f1.type, 'CONNECTION', 'connection');
assert.strictEqual(typeof f1.uid, 'number', 'uid is a number');
assert.strictEqual(typeof f1.triggerId, 'number', 'triggerId is a number');
checkInvocations(f1, { init: 1 }, 'first connection, when first created');
// creating second server connection
const sc2 = createServerConnection(common.mustCall(onsecondHandShake));
as = hooks.activitiesOfTypes('CONNECTION');
assert.strictEqual(as.length, 2,
'two CONNECTIONs after second connection created');
const f2 = as[1];
assert.strictEqual(f2.type, 'CONNECTION', 'connection');
assert.strictEqual(typeof f2.uid, 'number', 'uid is a number');
assert.strictEqual(typeof f2.triggerId, 'number', 'triggerId is a number');
checkInvocations(f1, { init: 1 }, 'first connection, when second created');
checkInvocations(f2, { init: 1 }, 'second connection, when second created');
// starting the connections which results in handshake starts
sc1.start();
sc2.start();
function onfirstHandShake() {
checkInvocations(f1, { init: 1, before: 1 },
'first connection, when first handshake');
checkInvocations(f2, { init: 1 }, 'second connection, when first handshake');
}
function onsecondHandShake() {
checkInvocations(f1, { init: 1, before: 1, after: 1 },
'first connection, when second handshake');
checkInvocations(f2, { init: 1, before: 1 },
'second connection, when second handshake');
tick(1E4);
}
process.on('exit', onexit);
function onexit() {
hooks.disable();
hooks.sanityCheck('CONNECTION');
checkInvocations(f1, { init: 1, before: 1, after: 1 },
'first connection, when process exits');
checkInvocations(f2, { init: 1, before: 1, after: 1 },
'second connection, when process exits');
}
Oops, something went wrong.

0 comments on commit e3e56f1

Please sign in to comment.