Last active
February 11, 2018 15:08
-
-
Save ssube/566cdf57af64f9f52e5efb456b891c89 to your computer and use it in GitHub Desktop.
async test harness
This file contains hidden or 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
import {AsyncHook, createHook} from 'async_hooks'; | |
import {expect} from 'chai'; | |
// this will pull Mocha internals out of the stacks | |
const {stackTraceFilter} = require('mocha/lib/utils'); | |
const filterStack = stackTraceFilter(); | |
type AsyncMochaTest = (this: Mocha.ITestCallbackContext | void, done: MochaDone) => Promise<void>; | |
type AsyncMochaSuite = (this: Mocha.ISuiteCallbackContext) => Promise<void>; | |
const UNHANDLED_REJECTION = 'unhandledRejection'; | |
export function describeAsync(description: string, cb: AsyncMochaSuite): Mocha.ISuite { | |
return describe(description, function track() { | |
const tracker = new Tracker(); | |
beforeEach(() => { | |
tracker.enable(); | |
}); | |
afterEach(() => { | |
const leaked = tracker.size; | |
tracker.dump(); | |
tracker.clear(); | |
if (leaked > 0) { | |
throw new Error('test leaked async resources'); | |
} | |
}); | |
const suite: PromiseLike<void> | undefined = cb.call(this); | |
if (!suite || !suite.then) { | |
console.error(`test suite '${description}' did not return a promise`); | |
} | |
return suite; | |
}); | |
} | |
/** | |
* Run an asynchronous test with unhandled rejection guards. | |
* | |
* This function may not have any direct test coverage. It is too simple to reasonably mock. | |
*/ | |
export function itAsync(expectation: string, cb: AsyncMochaTest): Mocha.ITest { | |
return it(expectation, function track(done) { | |
try { | |
const test: PromiseLike<void> | undefined = cb.call(this); | |
if (!test || !test.then) { | |
console.error(`test '${expectation}' did not return a promise`); | |
} else { | |
test.then((value: any) => { | |
done(); | |
}, (err: Error) => { | |
done(err); | |
}); | |
} | |
} catch (err) { | |
console.error('test leaked synchronous error', err); | |
done(err); | |
} | |
}); | |
} | |
export function delay(ms: number) { | |
return new Promise((res) => setTimeout(() => res(), ms)); | |
} | |
export interface TrackedResource { | |
source: string; | |
triggerAsyncId: number; | |
type: string; | |
}; | |
/** | |
* Async resource tracker using node's internal hooks. | |
* | |
* This probably won't work in a browser. It does not hold references to the resource, to avoid leaks. | |
* Adapted from https://gist.github.com/boneskull/7fe75b63d613fa940db7ec990a5f5843#file-async-dump-js | |
*/ | |
export class Tracker { | |
private hook: AsyncHook; | |
private resources: Map<number, TrackedResource>; | |
constructor() { | |
this.resources = new Map(); | |
this.hook = createHook({ | |
destroy: (id: number) => { | |
this.resources.delete(id); | |
}, | |
init: (id: number, type: string, triggerAsyncId: number) => { | |
const source = filterStack((new Error()).stack || 'unknown'); | |
this.resources.set(id, { | |
source, | |
triggerAsyncId, | |
type | |
}); | |
} | |
}); | |
} | |
public clear() { | |
this.resources.clear(); | |
} | |
public dump() { | |
this.hook.disable(); | |
console.error(`listing ${this.resources.size} tracked async resources`); | |
this.resources.forEach((res, id) => { | |
console.error(`id: ${id}`); | |
console.error(`type: ${res.type}`); | |
console.error(res.source); | |
console.error('\n'); | |
}); | |
} | |
public enable() { | |
this.hook.enable(); | |
console.error('enabling async resource tracker'); | |
} | |
public get size(): number { | |
return this.resources.size; | |
} | |
} |
This file contains hidden or 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
import * as chai from 'chai'; | |
import * as chaiAsPromised from 'chai-as-promised'; | |
import {ineeda} from 'ineeda'; | |
import * as sinonChai from 'sinon-chai'; | |
import * as sourceMapSupport from 'source-map-support'; | |
/** | |
* This will break the whole test run if any test leaks an unhandled rejection. | |
* | |
* To ensure only a single test breaks, make sure to wrap each test with the `handleRejection` helper. | |
*/ | |
process.on('unhandledRejection', (reason, promise) => { | |
console.error('unhandled error during tests', reason); | |
process.exit(1); | |
}); | |
chai.use(chaiAsPromised); | |
chai.use(sinonChai); | |
ineeda.intercept({ | |
then: null, | |
unsubscribe: null | |
}); | |
sourceMapSupport.install(); | |
const context = (require as any).context('.', true, /Test.*$/); | |
context.keys().forEach(context); | |
export default context; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment