UNPKG

bmocha

Version:

Alternative implementation of mocha

2,506 lines (1,957 loc) 69.4 kB
/*! * bmocha.js - alternative mocha implementation * Copyright (c) 2018-2019, Christopher Jeffrey (MIT License). * https://github.com/bcoin-org/bmocha * * Parts of this software are based on mochajs/mocha: * Copyright (c) 2011-2018, JS Foundation and contributors * https://github.com/mochajs/mocha */ /* eslint no-control-regex: "off" */ /* eslint no-ex-assign: "off" */ 'use strict'; const util = require('./util'); const { assert, get, nextTick, isPromise, inject, restore, stackError, noop, castArray, indent, singlify, escape, clean, toError, toMessage, toStack, errorify, stackify, jsonify, isShowable, show, inspect, why } = util; /* * Globals */ const { Array, Boolean, clearTimeout, Date, Error, JSON, Math, Object, Promise, RegExp, setTimeout, String, TypeError } = global; /* * Constants */ const style = { __proto__: null, title: 'bmocha', font: 'monospace', fg: '#000000', // #eeeeee bg: '#ffffff', // #111111 colors: [ '#2e3436', // black '#cc0000', // red '#4e9a06', // green '#c4a000', // yellow '#3465a4', // blue '#75507b', // magenta '#06989a', // cyan '#d3d7cf', // white '#555753', // bright black '#ef2929', // bright red '#8ae234', // bright green '#fce94f', // bright yellow '#729fcf', // bright blue '#ad7fa8', // bright magenta '#34e2e2', // bright cyan '#eeeeec' // bright white ] }; const colors = { __proto__: null, suite: 0, title: 0, plane: 0, fail: 31, crash: 31, slow: 31, message: 31, checkmark: 32, green: 32, medium: 33, pending: 36, light: 90, fast: 90, stack: 90, pass: 90, runway: 90, progress: 90, warning: 93 }; const symbolsUnix = { __proto__: null, ok: '\u2713', err: '\u2716', dot: '\u2024', dash: '-', comma: ',', bang: '!', plane: '\u2708', runway: '\u22c5', open: '[', complete: '\u25ac', incomplete: '\u2024', close: ']' }; const symbolsWindows = { __proto__: null, ok: '\u221a', err: '\u00d7', dot: '.', dash: '-', comma: ',', bang: '!', plane: '\u2708', runway: '\u22c5', open: '[', complete: '\u25ac', incomplete: '.', close: ']' }; const PENDING = new Error('pending'); /** * Runnable */ class Runnable { constructor() { this.mocha = null; this.suite = null; this.parent = null; this.name = ''; this.title = ''; this.matching = false; this.depth = 0; this.slow = 0; this.timeout = 0; this.timeouts = false; this.retries = 0; this.skippable = false; this.skip = false; this.only = false; this.running = false; this.stats = null; this.context = null; } get duration() { return this.stats.duration; } get elapsed() { return this.stats.elapsed; } get speed() { if (this.duration > this.slow) return 'slow'; if (this.duration > (this.slow >>> 1)) return 'medium'; return 'fast'; } titlePath() { const path = [this.title]; let parent = this.parent; while (parent) { if (parent.title) path.push(parent.title); parent = parent.parent; } return path.reverse(); } fullTitle() { return this.titlePath().join(' '); } } /** * Mocha */ class Mocha extends Runnable { constructor(options) { super(); // Runnable Properties this.mocha = this; this.suite = null; this.parent = null; this.name = ''; this.title = ''; this.matching = false; this.depth = -1; this.slow = 75; this.timeout = 2000; this.timeouts = true; this.retries = 0; this.skippable = false; this.skip = false; this.only = false; this.running = false; this.stats = new Stats(); this.context = new MochaContext(this); // Mocha Options this.asyncOnly = false; this.bail = false; this.catcher = this._catcher.bind(this); this.checkLeaks = false; this.colors = false; this.delay = false; this.diff = true; this.dryRun = false; this.exit = this._exit.bind(this); this.failZero = false; this.fgrep = ''; this.forbidOnly = false; this.forbidPending = false; this.fullTrace = false; this.global = true; this.globals = []; this.grep = null; this.invert = false; this.notify = null; this.reporter = 'spec'; this.reporterOptions = Object.create(null); this.retries; this.slow; this.stream = new Stream(); this.swallow = true; this.timeout; this.timeouts; this.why = false; this.windows = false; // Plugins. this.globalSetup = []; this.globalTeardown = []; this.rootHooks = { beforeAll: [], beforeEach: [], afterAll: [], afterEach: [] }; // State this.report = null; this.current = null; this.exclusive = false; this.beforeEaches = []; this.afterEaches = []; this.results = []; this.errors = []; this.test = null; this.uncatcher = null; this.aborters = []; this.runCalled = false; this.runResolve = null; // API this.before = this._before.bind(this); this.after = this._after.bind(this); this.beforeEach = this._beforeEach.bind(this); this.afterEach = this._afterEach.bind(this); this.describe = this._describe.bind(this); this.it = this._it.bind(this); this.doRun = this._doRun.bind(this); this.init(); this.set(options); } init() { for (const method of [this.describe, this.it]) { method.only = function only(title, func) { return method(title, func, 'only', only); }; method.skip = function skip(title, func) { return method(title, func, 'skip', skip); }; } } set(options) { if (options == null) return this; if (typeof options === 'function' || typeof options === 'string') { options = { reporter: options }; } assert(typeof options === 'object'); if (typeof options.write === 'function') options = { stream: options }; if (options.asyncOnly != null) this.asyncOnly = Boolean(options.asyncOnly); if (options.bail != null) this.bail = Boolean(options.bail); if (typeof options.catcher === 'function') this.catcher = options.catcher; if (options.checkLeaks != null) this.checkLeaks = Boolean(options.checkLeaks); if (options.colors != null) this.colors = Boolean(options.colors); if (options.delay != null) this.delay = Boolean(options.delay); if (options.diff != null) this.diff = Boolean(options.diff); if (options.dryRun != null) this.dryRun = Boolean(options.dryRun); if (typeof options.exit === 'function') this.exit = options.exit; if (options.failZero != null) this.failZero = Boolean(options.failZero); if (options.fgrep != null) this.fgrep = String(options.fgrep); if (options.forbidOnly != null) this.forbidOnly = Boolean(options.forbidOnly); if (options.forbidPending != null) this.forbidPending = Boolean(options.forbidPending); if (options.fullTrace != null) this.fullTrace = Boolean(options.fullTrace); if (options.global != null) this.global = Boolean(options.global); if (Array.isArray(options.globals)) this.globals = options.globals.slice(); if (options.grep != null) this.grep = RegExp(options.grep); if (options.invert != null) this.invert = Boolean(options.invert); if (typeof options.notify === 'function') this.notify = options.notify; if (options.reporter != null) this.reporter = Base.get(options.reporter).id; if (options.reporterOptions != null) { assert(options.reporterOptions); assert(typeof options.reporterOptions === 'object'); this.reporterOptions = options.reporterOptions; } if (options.retries != null) this.retries = options.retries >>> 0; if (options.slow != null) this.slow = options.slow >>> 0; if (options.stream != null) { assert(typeof options.stream.write === 'function'); this.stream = options.stream; if (options.colors == null) this.colors = Boolean(options.stream.isTTY); } if (options.swallow != null) this.swallow = Boolean(options.swallow); if (options.timeout != null) this.timeout = options.timeout >>> 0; if (options.timeouts != null) this.timeouts = Boolean(options.timeouts); if (options.why != null) this.why = Boolean(options.why); if (options.windows != null) this.windows = Boolean(options.windows); if (options.rootHooks != null) this._pushHooks(options.rootHooks); if (options.globalSetup != null) { for (const func of castArray(options.globalSetup)) this.globalSetup.push(func); } if (options.globalTeardown != null) { for (const func of castArray(options.globalTeardown)) this.globalTeardown.push(func); } return this; } _suite() { if (!this.current) throw new Error('No suite is currently initializing.'); return this.current; } _before(desc, func) { return this._suite().before(desc, func, this._before); } _after(desc, func) { return this._suite().after(desc, func, this._after); } _beforeEach(desc, func) { return this._suite().beforeEach(desc, func, this._beforeEach); } _afterEach(desc, func) { return this._suite().afterEach(desc, func, this._afterEach); } _describe(title, func, action) { return this._suite().describe(title, func, action, this._describe); } _it(title, func, action) { return this._suite().it(title, func, action, this._it); } globalize() { if (!this.global) return null; return inject(global, { // API before: this.before, after: this.after, beforeEach: this.beforeEach, afterEach: this.afterEach, describe: this.describe, it: this.it, // Aliases xdescribe: this.describe.skip, xit: this.it.skip, specify: this.it, // Runner run: this.delay ? this.doRun : undefined }); } unglobalize(snapshot) { if (this.global) restore(global, snapshot); return this; } _catcher(reject) { return null; } _exit(code) { throw new Error(`Test suite failed: ${code >>> 0}.`); } catch() { if (this.global && !this.uncatcher) { const reject = this.reject.bind(this); this.uncatcher = this.catcher(reject); } return this; } uncatch() { const uncatcher = this.uncatcher; if (uncatcher) { this.uncatcher = null; uncatcher(); } return this; } reject(error) { if (this.test) { // If we have a running test, reject it. // This usually means an uncaught exception // or an unhandled rejection. if (this.test.job && !this.test.job.done) { this.test.job.reject(error); return; } // Otherwise, inject into our last test. // This is usually triggered by the "multiple // resolves" event. if (!this.test.fail) { this.test.setError(toError(error)); return; } } // Fallback to "global" error system. // Something happening in the background // that we can't get a hold on? this.error(error); } error(error) { const err = toError(error); // If the suite is still running, push // onto an array for the reporter to // display. if (this.running) { this.errors.push(err); this.stats.failures += 1; return; } // Last resort: print the error to // stdout and exit. this.report.exception(err); this.exit(1); } matches(title) { assert(typeof title === 'string'); let ret = !this.invert; if (this.grep) ret = this.grep.test(title); else if (this.fgrep) ret = title.includes(this.fgrep); if (this.invert) ret = !ret; return ret; } _doRun() { if (this.runCalled) { // Note: mocha doesn't throw at all here. throw stackError(this._doRun, 'run() called twice!'); } this.runCalled = true; if (this.runResolve) this.runResolve(); } async waitForRun() { if (this.runCalled) return undefined; return new Promise((resolve) => { this.current = this.suite; this.runResolve = () => { this.current = null; this.runResolve = null; resolve(); }; }); } async run(funcs) { const Reporter = Base.get(this.reporter); this.stats = new Stats(); this.results = []; this.errors = []; this.exclusive = false; this.runResolve = null; this.runCalled = false; this.report = new Reporter(this.stream, this.reporterOptions); this.report.stats = this.stats; this.report.colors = this.colors; this.report.diff = this.diff; this.report.fullTrace = this.fullTrace; this.report.windows = this.windows; this.suite = new Suite(this); // Track async hooks. let calls = null; if (this.why) calls = why(); // Inject globals. const save = this.globalize(); try { await this.suite.initAsync(funcs); } catch (e) { this.unglobalize(save); const err = toError(e); err.uncaught = true; err.exception = true; this.report.exception(err); return 1; } // Start catching all errors. this.catch(); // Wait for run() call if // we're delaying things. if (this.delay) await this.waitForRun(); // Revert globals. this.unglobalize(save); try { await this._globalSetup(); } catch (e) { this.report.exception(e); return 1; } this.running = true; this.stats.mark(); this.stats.total = this.suite.total(); if (this.stats.total > 0) this.report.start(this); await this.suite.run(); this.stats.mark(); // Maybe send notification. if (this.notify) { try { await this.notify(this.stats); } catch (e) { this.error(e); } } // Unbinding to the error events in // node can cause some errors to get // missed. Only unbind if we're not // "global". if (!this.global) this.uncatch(); this.running = false; if (this.stats.total > 0) this.report.end(this); try { await this._globalTeardown(); } catch (e) { // Note: mocha swallows this error. this.report.exception(e); return 1; } if (calls) { await nextTick(); this.report.why(calls()); } if (this.isAborting) this.doAbort(); if (this.failZero && this.stats.total === 0) return 1; return Math.min(this.stats.failures, 255); } async abort() { return new Promise((resolve, reject) => { if (!this.running) { resolve(); return; } this.aborters.push(resolve); }); } get isAborting() { return this.aborters.length > 0; } doAbort() { const aborters = this.aborters; this.aborters = []; for (const abort of aborters) abort(); } async plugin(mod, file) { if (mod == null) return; if (mod.mochaHooks != null) { let hooks = mod.mochaHooks; if (typeof hooks === 'function') hooks = await hooks(); this._pushHooks(hooks, file); } if (mod.mochaGlobalSetup != null) { for (const func of castArray(mod.mochaGlobalSetup, file)) this.globalSetup.push(func); } if (mod.mochaGlobalTeardown != null) { for (const func of castArray(mod.mochaGlobalTeardown, file)) this.globalTeardown.push(func); } } _pushHooks(hooks, file = null) { if (Array.isArray(hooks) || hooks === null || typeof hooks !== 'object') { let msg = 'Root hooks must be an object'; if (file != null) msg += ` (${file})`; throw new TypeError(msg); } for (const [dst, src] of [[this.rootHooks.beforeAll, hooks.beforeAll], [this.rootHooks.beforeEach, hooks.beforeEach], [this.rootHooks.afterAll, hooks.afterAll], [this.rootHooks.afterEach, hooks.afterEach]]) { for (const func of castArray(src, file)) dst.push(func); } } async _globalSetup() { const ctx = this.context; for (const func of this.globalSetup) await func.call(ctx, ctx); } async _globalTeardown() { const ctx = this.context; for (const func of this.globalTeardown) await func.call(ctx, ctx); } _rootHooks() { for (const func of this.rootHooks.beforeAll) this.suite.before(null, func, this._rootHooks); for (const func of this.rootHooks.afterAll) this.suite.after(null, func, this._rootHooks); for (const func of this.rootHooks.beforeEach) this.suite.beforeEach(null, func, this._rootHooks); for (const func of this.rootHooks.afterEach) this.suite.afterEach(null, func, this._rootHooks); } } /** * Suite */ class Suite extends Runnable { constructor(parent, title = '', start) { assert((parent instanceof Mocha) || (parent instanceof Suite)); if (typeof title !== 'string') throw stackError(start, 'Must provide a title for suite.'); if (/[\x00-\x1f\x7f]/.test(title)) throw stackError(start, 'Invalid suite title.'); super(); this.mocha = parent.mocha; this.suite = this; this.parent = parent; this.name = ''; this.title = title; this.matching = parent.matching || parent.mocha.matches(title); this.depth = parent.depth + 1; this.slow = parent.slow; this.timeout = parent.timeout; this.timeouts = parent.timeouts; this.retries = parent.retries; this.skippable = parent.skippable; this.only = parent.only; this.running = false; this.stats = new Stats(); this.context = new Context(this); this.root = this.depth === 0; this.befores = []; this.afters = []; this.beforeEaches = parent.beforeEaches.slice(); this.afterEaches = parent.afterEaches.slice(); this.tests = []; this.suites = []; } before(desc, func, start) { if (typeof desc === 'function') [desc, func] = [func, desc]; const hook = new Hook(this, 'before all', desc, func, start); this.befores.push(hook); } after(desc, func, start) { if (typeof desc === 'function') [desc, func] = [func, desc]; const hook = new Hook(this, 'after all', desc, func, start); this.afters.push(hook); } beforeEach(desc, func, start) { if (typeof desc === 'function') [desc, func] = [func, desc]; const hook = new Hook(this, 'before each', desc, func, start); this.beforeEaches.push(hook); } afterEach(desc, func, start) { if (typeof desc === 'function') [desc, func] = [func, desc]; const hook = new Hook(this, 'after each', desc, func, start); this.afterEaches.push(hook); } describe(title, func, action, start) { const suite = new Suite(this, title, start); if (action === 'only') suite.context.only(); else if (action === 'skip') suite.context.skip(); else if (action != null) throw stackError(start, `Invalid action: ${action}`); if (typeof func !== 'function') { throw stackError(start, `Suite ${suite.fullTitle()} was ` + 'defined but no callback was ' + 'supplied. Supply a callback ' + 'or explicitly skip the suite.'); } suite.init(func, start); this.suites.push(suite); return suite.context; } it(title, func, action, start) { if (func == null) { func = noop; action = 'skip'; } const test = new Test(this, title, func, start); if (action === 'only') test.context.only(); else if (action === 'skip') test.context.skip(); else if (action != null) throw stackError(start, `Invalid action: ${action}`); this.tests.push(test); return test.context; } init(funcs, start) { if (typeof funcs === 'function') funcs = [funcs]; assert(Array.isArray(funcs)); for (const func of funcs) assert(typeof func === 'function'); const ctx = this.mocha.context; const current = this.mocha.current; this.mocha.current = this; try { for (const func of funcs) { const result = func.call(ctx, ctx); if (isPromise(result)) throw stackError(start, 'Cannot resolve asynchronous test suites.'); } } finally { this.mocha.current = current; } return this; } async initAsync(funcs) { if (typeof funcs === 'function') funcs = [funcs]; assert(Array.isArray(funcs)); for (const func of funcs) assert(typeof func === 'function'); const ctx = this.mocha.context; const current = this.mocha.current; this.mocha.current = this; if (this.root) this.mocha._rootHooks(); try { for (const func of funcs) await func.call(ctx, ctx); } finally { this.mocha.current = current; } return this; } total() { let count = 0; for (const test of this.tests) { if (this.mocha.exclusive && !test.only) continue; if (!test.matching) continue; count += 1; } for (const suite of this.suites) count += suite.total(); return count; } succeed(test) { assert(test instanceof Executable); if (test.skip) this.mocha.stats.pending += 1; else this.mocha.stats.passes += 1; this.mocha.stats.tests += 1; this.mocha.results.push(test); this.mocha.report.testEnd(test); } fail(test) { assert(test instanceof Executable); if (this.mocha.isAborting) return false; this.mocha.stats.failures += 1; this.mocha.stats.tests += 1; this.mocha.results.push(test); this.mocha.report.testEnd(test); return !this.mocha.bail; } async run() { if (this.total() === 0) return true; this.stats = new Stats(); this.running = true; this.stats.mark(); if (!this.root) this.mocha.stats.suites += 1; this.mocha.report.suiteStart(this); const ok = await this.exec(); this.stats.mark(); this.running = false; this.mocha.report.suiteEnd(this); return ok; } async exec() { for (const hook of this.befores) { if (!await hook.run()) return this.fail(hook); } for (const test of this.tests) { if (this.mocha.exclusive && !test.only) continue; if (!test.matching) continue; this.mocha.report.testStart(test); let success = false; for (let retry = 0; retry < test.retries + 1; retry++) { for (const hook of this.beforeEaches) { if (!await hook.run(test)) return this.fail(hook); } success = await test.run(retry); for (const hook of this.afterEaches) { if (!await hook.run(test)) return this.fail(hook); } if (success) break; } if (success) { this.succeed(test); continue; } if (!this.fail(test)) return false; } for (const suite of this.suites) { if (!await suite.run()) return false; } for (const hook of this.afters) { if (!await hook.run()) return this.fail(hook); } return true; } } /** * Executable */ class Executable extends Runnable { constructor(parent, name, title, body) { assert(parent instanceof Suite); assert(typeof name === 'string'); assert(typeof title === 'string'); assert(typeof body === 'function'); super(); this.mocha = parent.mocha; this.suite = parent; this.parent = parent; this.name = name; this.title = title; this.matching = parent.matching || parent.mocha.matches(title); this.depth = parent.depth; this.slow = parent.slow; this.timeout = parent.timeout; this.timeouts = parent.timeouts; this.retries = parent.retries; this.skippable = parent.skippable; this.only = parent.only; this.running = false; this.stats = new Stats(); this.context = new Context(this); this.body = body; this.job = null; this.retry = 0; this.fail = false; this.error = null; this.swallowed = null; } setError(err) { assert(err instanceof Error); this.skip = false; this.fail = true; this.error = err; this.swallowed = null; } getGlobals() { if (!this.mocha.checkLeaks) return null; return Object.keys(global); } checkGlobals(snapshot) { if (!this.mocha.checkLeaks) return; const globals = Object.keys(global); const leaks = []; for (const name of globals) { if (snapshot.includes(name)) continue; if (this.mocha.globals.includes(name)) continue; leaks.push(name); } if (leaks.length === 0) return; this.setError(new Error(`global leaks detected: ${leaks.join(', ')}`)); } async exec() { return new Promise((resolve, reject) => { if (this.mocha.isAborting) { resolve(); return; } if (this.mocha.forbidOnly && this.only) { reject(new Error('`.only` forbidden')); return; } if (this.skippable) { this.skip = true; resolve(); return; } if (this.mocha.dryRun) { resolve(); return; } const ctx = this.context; const job = new Job(this, resolve, reject); if (this.body.length > 0) { const done = job.callback(); let result; try { result = this.body.call(ctx, done); } catch (e) { if (this.mocha.swallow) { // No idea why, but mocha behaves // this way for some reason and // _swallows the error_ if the callback // has already been called synchronously. // I repeat: mocha SWALLOWS THE ERROR. // Update: it seems that the mocha devs // are aware of this. See: // https://github.com/mochajs/mocha/issues/3226 if (job.called && e !== PENDING) { this.swallowed = toError(e); return; } } job.reject(e); return; } if (isPromise(result)) { job.reject(new Error('' + 'Resolution method is overspecified. ' + 'Specify a callback *or* return a ' + 'Promise; not both.')); return; } } else { let result; try { result = this.body.call(ctx); } catch (e) { job.reject(e); return; } if (!isPromise(result)) { if (this.mocha.asyncOnly) { job.reject(new Error('' + '--async-only option in use ' + 'without declaring `done()` ' + 'or returning a promise')); return; } job.resolve(); return; } const onResolve = () => { job.resolve(); return null; }; const onReject = (err) => { if (!err) err = new Error('Promise rejected with no or falsy reason'); job.reject(err); }; try { result.then(onResolve, onReject); } catch (e) { job.reject(e); return; } } job.start(); }); } async run() { this.stats = new Stats(); this.skip = false; this.fail = false; this.error = null; this.swallowed = null; this.running = true; this.stats.mark(); this.mocha.test = this; const snapshot = this.getGlobals(); try { await this.exec(); } catch (e) { if (e !== PENDING) this.setError(toError(e)); } if (this.mocha.forbidPending && this.skip) this.setError(new Error('Pending test forbidden')); this.checkGlobals(snapshot); await nextTick(); this.mocha.test = null; this.stats.mark(); this.running = false; if (this.mocha.isAborting) return false; return !this.fail; } toJSON(minimal = false) { assert(typeof minimal === 'boolean'); let err, stack; if (this.fail) { const json = jsonify(this.error, this.mocha.fullTrace); if (minimal) { err = json.message; stack = json.stack; } else { err = json; } } else { if (!minimal) err = {}; } return { title: this.title, fullTitle: this.fullTitle(), duration: this.stats.duration, currentRetry: this.retry, speed: this.speed, err, stack }; } } /** * Hook */ class Hook extends Executable { constructor(parent, type, desc, body, start) { if (desc == null) desc = ''; assert(typeof type === 'string'); if (typeof desc !== 'string') throw stackError(start, 'Must provide a description for hook.'); if (typeof body !== 'function') throw stackError(start, 'Must provide a callback for hook.'); if (/[\x00-\x1f\x7f]/.test(desc)) throw stackError(start, 'Invalid hook description.'); let name = `"${type}" hook`; if (!desc && body.name) desc = body.name; if (desc) name += `: ${desc}`; super(parent, name, '', body); } async run(test) { assert(test == null || (test instanceof Test)); if (test) { this.context = test.context; this.title = `${this.name} for "${test.title}"`; } else { this.context = this.suite.context; this.title = this.name; } return super.run(); } } /** * Test */ class Test extends Executable { constructor(parent, title, body, start) { if (typeof title !== 'string') throw stackError(start, 'Must provide a title for test.'); if (typeof body !== 'function') throw stackError(start, 'Must provide a callback for test.'); // Note: // Temporary hack to get // bcoin tests passing. title = singlify(title); if (/[\x00-\x1f\x7f]/.test(title)) throw stackError(start, 'Invalid test title.'); super(parent, '', title, body); } async run(retry = 0) { assert((retry >>> 0) === retry); this.retry = retry; return super.run(); } } /** * AbstractContext */ class AbstractContext { constructor() {} bail(enabled) { if (arguments.length === 0) return this.mocha.bail; this.mocha.bail = Boolean(enabled); return this; } enableTimeouts(enabled) { if (arguments.length === 0) return this.runnable.timeouts; this.runnable.timeouts = Boolean(enabled); return this; } only() { this.runnable.only = true; this.mocha.exclusive = true; return this; } retries(n) { if (arguments.length === 0) return this.runnable.retries; this.runnable.retries = n >>> 0; return this; } skip() { if (this.runnable.running) { this.runnable.skip = true; throw PENDING; } this.runnable.skippable = true; return this; } slow(ms) { if (arguments.length === 0) return this.runnable.slow; this.runnable.slow = ms >>> 0; return this; } timeout(ms) { if (arguments.length === 0) return this.runnable.timeout; this.runnable.timeout = ms >>> 0; return this; } } /** * MochaContext */ class MochaContext extends AbstractContext { constructor(mocha) { assert(mocha instanceof Mocha); super(); this.mocha = mocha; } get runnable() { const mocha = this.mocha; const runnable = mocha.current || mocha.test; if (!runnable) throw new Error('No context currently running!'); return runnable; } } /** * Context */ class Context extends AbstractContext { constructor(runnable) { assert(runnable instanceof Runnable); super(); this.runnable = runnable; } get mocha() { return this.runnable.mocha; } } /** * Job */ class Job { constructor(test, resolve, reject) { assert(test instanceof Executable); assert(typeof resolve === 'function'); assert(typeof reject === 'function'); this.test = test; this.timer = null; this.done = false; this.called = false; this._resolve = resolve; this._reject = reject; this.init(); } init() { this.test.job = this; return this; } resolve() { if (this.done) return null; this.done = true; this.clear(); this._resolve(); return null; } reject(err) { if (this.done) return null; this.done = true; this.clear(); this._reject(err); return null; } _callback(err) { if (this.called) { const msg = 'done() called multiple times'; const message = err ? get(err, 'message') : null; if (typeof message === 'string') { try { err.message += ` (and Mocha's ${msg})`; } catch (e) { ; } } else { err = new Error(msg); } this.reject(err); return this; } this.called = true; setImmediate(() => { if (err) this.reject(err); else this.resolve(); }); return this; } callback() { const self = this; return function done(err) { self._callback(err); }; } start() { const {timeout, timeouts} = this.test; if (this.done) return this; assert(this.timer == null); if (!timeouts || timeout === 0) { // We still want something on the event // loop in the case that we're calling // out to a native library which does // not poll anything directly. this.timer = setTimeout(() => {}, 1 << 29); return this; } this.timer = setTimeout(() => { this.reject(new Error('' + `Timeout of ${timeout}ms exceeded. ` + 'For async tests and hooks, ensure ' + '"done()" is called; if returning a ' + 'Promise, ensure it resolves.')); }, timeout); return this; } clear() { if (this.timer != null) { clearTimeout(this.timer); this.timer = null; } this.test.job = null; return this; } } /* * Stats */ class Stats { constructor() { this.start = 0; this.end = 0; this.duration = 0; this.suites = 0; this.passes = 0; this.pending = 0; this.failures = 0; this.tests = 0; this.total = 0; } get elapsed() { if (this.end === 0) return Math.max(0, Date.now() - this.start); return this.duration; } mark() { if (this.start === 0) { this.start = Date.now(); this.end = 0; this.duration = 0; } else { this.end = Date.now(); this.duration = Math.max(0, this.end - this.start); } return this; } toJSON() { return { suites: this.suites, tests: this.tests, passes: this.passes, pending: this.pending, failures: this.failures, start: new Date(this.start).toISOString(), end: new Date(this.end).toISOString(), duration: this.duration }; } } /** * Base */ class Base { constructor(stream, options) { if (options == null) options = Object.create(null); assert(stream && typeof stream.write === 'function'); assert(options && typeof options === 'object'); this.stats = new Stats(); this.stream = stream; this.options = options; this.colors = false; this.diff = true; this.fullTrace = false; this.windows = false; this.color = this._color.bind(this); } get id() { return this.constructor.id; } get isTTY() { return this.stream.isTTY && typeof this.stream.columns === 'number'; } get columns() { if (typeof this.stream.columns === 'number') return this.stream.columns; return 75; } get width() { return (Math.min(100, this.columns) * 0.75) >>> 0; } get symbols() { return this.windows ? symbolsWindows : symbolsUnix; } _color(col, str) { if (!this.colors) return str; if (typeof col === 'string') col = colors[col]; return `\x1b[${col >>> 0}m${str}\x1b[0m`; } write(str) { try { return this.stream.write(String(str)); } catch (e) { if (e && e.code === 'ERR_STREAM_DESTROYED') return false; throw e; } } hide() { if (this.isTTY) this.write('\x1b[?25l'); } show() { if (this.isTTY) this.write('\x1b[?25h'); } deleteLine() { if (this.isTTY) this.write('\x1b[2K'); } beginningOfLine() { if (this.isTTY) this.write('\x1b[0G'); } carriage() { if (this.isTTY) { this.deleteLine(); this.beginningOfLine(); } } cursorUp(n) { if (this.isTTY) this.write(`\x1b[${n >>> 0}A`); } cursorDown(n) { if (this.isTTY) this.write(`\x1b[${n >>> 0}B`); } start(mocha) { assert(mocha instanceof Mocha); } suiteStart(suite) { assert(suite instanceof Suite); } testStart(test) { assert(test instanceof Executable); } testEnd(test) { assert(test instanceof Executable); } suiteEnd(suite) { assert(suite instanceof Suite); } end(mocha) { assert(mocha instanceof Mocha); } exception(error) { this.write('\n'); this.write(' An error occurred outside of the test suite:\n'); this.error(toError(error), 2); } error(error, depth) { assert(error && typeof error === 'object'); assert((depth >>> 0) === depth); const {color} = this; const message = toMessage(error); const stack = toStack(error, this.fullTrace); this.write('\n'); this.write(' '.repeat(depth) + color('message', message) + '\n'); if (this.diff && isShowable(error)) { const text = show(error, this.colors, this.fullTrace); if (text.length > 0) { this.write('\n'); this.write(indent(text, depth) + '\n'); } } if (stack.length > 0) { const text = color('stack', stack); this.write('\n'); this.write(indent(text, depth) + '\n'); } this.write('\n'); } epilogue(mocha) { assert(mocha instanceof Mocha); const {color, stats} = this; const duration = stats.duration >= 1000 ? Math.ceil(stats.duration / 1000) + 's' : stats.duration + 'ms'; this.write('\n'); this.write(' ' + color('green', ` ${stats.passes} passing`) + color('light', ` (${duration})`) + '\n'); if (stats.pending > 0) { this.write(' ' + color('pending', `${stats.pending} pending`) + '\n'); } if (stats.failures > 0) { this.write(' ' + color('fail', `${stats.failures} failing`) + '\n'); } this.write('\n'); let total = 0; for (let i = 0; i < mocha.results.length; i++) { const test = mocha.results[i]; if (!test.fail) continue; const {error} = test; const id = (total + 1).toString(10); const path = test.titlePath(); for (let j = 0; j < path.length; j++) { let title = path[j]; if (j === path.length - 1) title += ':'; const padding = ' '.repeat(j + 1); const col = get(error, 'uncaught') === true ? 'warning' : 'title'; if (j === 0) { this.write(padding + color(col, `${id})`) + ' ' + color('title', title) + '\n'); } else { this.write(padding + ' '.repeat(id.length) + ' ' + color('title', title) + '\n'); } } this.error(error, 3); total += 1; } for (let i = 0; i < mocha.errors.length; i++) { const id = total + 1; const error = mocha.errors[i]; this.write(' ' + color('warning', `${id})`) + ' ' + color('title', 'Uncaught Error') + '\n'); this.error(error, 3); total += 1; } } why(active) { const {color} = this; assert(Array.isArray(active)); this.write(color('warning', ' ' + `There are ${active.length} handle(s) ` + 'potentially keeping the process running.\n')); this.write('\n'); // Based on: // https://github.com/mafintosh/why-is-node-running for (const [type, calls] of active) { const stacks = []; let max = 0; this.write(' ' + color('title', type) + '\n'); if (calls.length === 0) { this.write(' '); this.write(color('stack', '(unknown stack trace)\n')); } for (const call of calls) { const prefix = ' at ' + `${call.filename}:` + `${call.line + 1}:` + `${call.column + 1}`; if (prefix.length > max) max = prefix.length; stacks.push([prefix, call.code]); } for (const [prefix, code] of stacks) { const pad = ' '.repeat(max - prefix.length); const pre = color('stack', prefix); if (code) this.write(`${pre}:${pad} ${code}\n`); else this.write(pre); } this.write('\n'); } } static get(reporter) { if (reporter == null) return SpecReporter; if (typeof reporter === 'string') { switch (reporter) { case 'doc': reporter = DocReporter; break; case 'dot': reporter = DotReporter; break; case 'json': reporter = JSONReporter; break; case 'json-stream': reporter = JSONStreamReporter; break; case 'landing': reporter = LandingReporter; break; case 'list': reporter = ListReporter; break; case 'markdown': reporter = MarkdownReporter; break; case 'min': reporter = MinReporter; break; case 'nyan': reporter = NyanReporter; break; case 'progress': reporter = ProgressReporter; break; case 'spec': reporter = SpecReporter; break; case 'tap': reporter = TapReporter; break; case 'xunit': reporter = XUnitReporter; break; default: throw new Error(`Unknown reporter: ${reporter}.`); } } assert(typeof reporter === 'function'); assert(typeof reporter.id === 'string'); return reporter; } } Base.id = ''; /** * DocReporter */ class DocReporter extends Base { constructor(stream, options) { super(stream, options); } suiteStart(suite) { const indent = ' '.repeat(suite.depth); this.write(indent + '<section class="suite">\n'); if (!suite.root) this.write(indent + ` <h1>${escape(suite.title)}</h1>\n`); this.write(indent + ' <dl>\n'); } suiteEnd(suite) { const indent = ' '.repeat(suite.depth); this.write(indent + ' </dl>\n'); this.write(indent + '</section>\n'); } testEnd(test) { const indent = ' '.repeat(test.depth + 2); const code = escape(clean(test.body)); if (test.fail) { const message = escape(toMessage(test.error)); const stack = escape(toStack(test.error, this.fullTrace)); this.write(indent + `<dt class="error">${escape(test.title)}</dt>` + '\n'); this.write(indent + `<dd class="error"><pre><code>${code}</code></pre></dd>` + '\n'); this.write(indent + `<dd class="error">${message}\n\n${stack}</dd>` + '\n'); return; } this.write(indent + `<dt>${escape(test.title)}</dt>` + '\n'); this.write(indent + `<dd><pre><code>${code}</code></pre></dd>` + '\n'); } } DocReporter.id = 'doc'; /** * DotReporter */ class DotReporter extends Base { constructor(stream, options) { super(stream, options); this.n = -1; } start(mocha) { this.n = -1; this.write('\n'); } testEnd(test) { const {color} = this; const {comma, bang, dot} = this.symbols; if (++this.n % this.width === 0) this.write('\n '); if (test.skip) this.write(color('pending', comma)); else if (test.fail) this.write(color('fail', bang)); else this.write(color(test.speed, dot)); } end(mocha) { this.write('\n'); this.epilogue(mocha); } } DotReporter.id = 'dot'; /** * JSONReporter */ class JSONReporter extends Base { constructor(stream, options) { super(stream, options); this.pending = []; this.failures = []; this.passes = []; this.tests = []; } json(json) { this.write(JSON.stringify(json, null, 2) + '\n'); } start(mocha) { this.pending = []; this.failures = []; this.passes = []; this.tests = []; } testEnd(test) { const json = test.toJSON(); if (test.skip) this.pending.push(json); else if (test.fail) this.failures.push(json); else this.passes.push(json); this.tests.push(json); } end(mocha) { this.json({ stats: this.stats.toJSON(), tests: this.tests, pending: this.pending, failures: this.failures, passes: this.passes }); } } JSONReporter.id = 'json'; /** * JSONStreamReporter */ class JSONStreamReporter extends Base { constructor(stream, options) { super(stream, options); } json(json) { this.write(JSON.stringify(json) + '\n'); } start(mocha) { this.json(['start', { total: this.stats.total }]); } testEnd(test) { this.json([ test.fail ? 'fail' : 'pass', test.toJSON(true) ]); } end(mocha) { this.json(['end', this.stats.toJSON()]); } } JSONStreamReporter.id = 'json-stream'; /** * LandingReporter */ class LandingReporter extends Base { constructor(stream, options) { super(stream, options); this.crashed = -1; this.n = 0; } runway() { const {color, symbols} = this; const width = Math.max(0, this.width - 1); const line = symbols.dash.repeat(width); this.write(' ' + color('runway', line)); } start(mocha) { this.crashed = -1; this.n = 0; this.write('\n\n\n '); this.hide(); } testEnd(test) { const {color, symbols} = this; const {plane, runway} = symbols; const col = this.crashed === -1 ? (this.width * ++this.n / this.stats.total) >>> 0 : this.crashed; let icon = color('plane', plane); if (test.fail) { icon = color('crash', plane); this.crashed = col; } if (this.isTTY) this.write(`\x1b[${this.width + 1}D\x1b[2A`); else this.write('\n'); const x = Math.max(0, col - 1); const y = Math.max(0, this.width - col - 1); this.runway(); this.write('\n'); this.write(' ' + color('runway', runway.repeat(x)) + icon + color('runway', runway.repeat(y)) + '\n'); this.runway(); this.write('\x1b[0m'); } end(mocha) { this.show(); this.write('\n'); this.epilogue(mocha); } } LandingReporter.id = 'landing'; /** * ListReporter */ class ListReporter extends Base { constructor(stream, options) { super(stream, options); this.n = 0; } start(mocha) { this.write('\n'); this.n = 0; } testStart(test) { const {color} = this; if (this.isTTY) { this.write(' ' + color('pass', `${test.fullTitle()}:`) + ' '); } } testEnd(test) { const {color, symbols} = this; if (test.skip) { this.carriage(); this.write(' ' + color('checkmark', symbols.dash) + ' ' + color('pending', `${test.fullTitle()}`) + '\n'); return; } if (test.fail) { this.carriage(); this.n += 1; this.write(' ' + color('fail', `${this.n}) ${test.fullTitle()}`) + '\n'); return; } this.carriage(); this.write(' ' + color('checkmark', symbols.ok) + ' ' + color('pass', `${test.fullTitle()}:`) + ' ' + color(test.speed, `${test.duration}ms`) + '\n'); } end(mocha) { this.epilogue(mocha); } } ListReporter.id = 'list'; /** * MarkdownReporter */ class MarkdownReporter extends Base { constructor(stream, options) { super(stream, options); this.buffer = ''; } title(suite) { return '#'.repeat(suite.depth) + ' ' + suite.title; } slug(str) { assert(typeof str === 'string'); return str .toLowerCase() .replace(/ +/g, '-') .replace(/[^-\w]/g, ''); } mapTOC(suite, obj) { const key = '$' + suite.title; if (!obj[key]) obj[key] = { suite }; for (const child of suite.suites) this.mapTOC(child, obj[key]); return obj; } stringifyTOC(obj, level) { level += 1; let buffer = ''; let link; for (const key of Object.keys(obj)) { if (key === 'suite') continue; if (key !== '$') { link = `- [${key.substring(1)}]`; link += `(#${this.slug(obj[key].suite.fullTitle())})\n`; buffer += ' '.repeat(level - 2) + link; } buffer += this.stringifyTOC(obj[key], level); } return buffer; } generateTOC(suite) { const obj = this.mapTOC(suite, {}); return this.stringifyTOC(obj, 0); } start(mocha) { this.buffer = ''; } suiteStart(suite) { if (suite.root) return; const slug = this.slug(suite.fullTitle()); this.buffer += `<