1265 lines
33 KiB
JavaScript
1265 lines
33 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* Module dependencies.
|
|
* @private
|
|
*/
|
|
var util = require('util');
|
|
var EventEmitter = require('events').EventEmitter;
|
|
var Pending = require('./pending');
|
|
var utils = require('./utils');
|
|
var debug = require('debug')('mocha:runner');
|
|
var Runnable = require('./runnable');
|
|
var Suite = require('./suite');
|
|
var HOOK_TYPE_BEFORE_EACH = Suite.constants.HOOK_TYPE_BEFORE_EACH;
|
|
var HOOK_TYPE_AFTER_EACH = Suite.constants.HOOK_TYPE_AFTER_EACH;
|
|
var HOOK_TYPE_AFTER_ALL = Suite.constants.HOOK_TYPE_AFTER_ALL;
|
|
var HOOK_TYPE_BEFORE_ALL = Suite.constants.HOOK_TYPE_BEFORE_ALL;
|
|
var EVENT_ROOT_SUITE_RUN = Suite.constants.EVENT_ROOT_SUITE_RUN;
|
|
var STATE_FAILED = Runnable.constants.STATE_FAILED;
|
|
var STATE_PASSED = Runnable.constants.STATE_PASSED;
|
|
var STATE_PENDING = Runnable.constants.STATE_PENDING;
|
|
var dQuote = utils.dQuote;
|
|
var sQuote = utils.sQuote;
|
|
var stackFilter = utils.stackTraceFilter();
|
|
var stringify = utils.stringify;
|
|
|
|
const {
|
|
createInvalidExceptionError,
|
|
createUnsupportedError,
|
|
createFatalError,
|
|
isMochaError,
|
|
constants: errorConstants
|
|
} = require('./errors');
|
|
|
|
/**
|
|
* Non-enumerable globals.
|
|
* @private
|
|
* @readonly
|
|
*/
|
|
var globals = [
|
|
'setTimeout',
|
|
'clearTimeout',
|
|
'setInterval',
|
|
'clearInterval',
|
|
'XMLHttpRequest',
|
|
'Date',
|
|
'setImmediate',
|
|
'clearImmediate'
|
|
];
|
|
|
|
var constants = utils.defineConstants(
|
|
/**
|
|
* {@link Runner}-related constants.
|
|
* @public
|
|
* @memberof Runner
|
|
* @readonly
|
|
* @alias constants
|
|
* @static
|
|
* @enum {string}
|
|
*/
|
|
{
|
|
/**
|
|
* Emitted when {@link Hook} execution begins
|
|
*/
|
|
EVENT_HOOK_BEGIN: 'hook',
|
|
/**
|
|
* Emitted when {@link Hook} execution ends
|
|
*/
|
|
EVENT_HOOK_END: 'hook end',
|
|
/**
|
|
* Emitted when Root {@link Suite} execution begins (all files have been parsed and hooks/tests are ready for execution)
|
|
*/
|
|
EVENT_RUN_BEGIN: 'start',
|
|
/**
|
|
* Emitted when Root {@link Suite} execution has been delayed via `delay` option
|
|
*/
|
|
EVENT_DELAY_BEGIN: 'waiting',
|
|
/**
|
|
* Emitted when delayed Root {@link Suite} execution is triggered by user via `global.run()`
|
|
*/
|
|
EVENT_DELAY_END: 'ready',
|
|
/**
|
|
* Emitted when Root {@link Suite} execution ends
|
|
*/
|
|
EVENT_RUN_END: 'end',
|
|
/**
|
|
* Emitted when {@link Suite} execution begins
|
|
*/
|
|
EVENT_SUITE_BEGIN: 'suite',
|
|
/**
|
|
* Emitted when {@link Suite} execution ends
|
|
*/
|
|
EVENT_SUITE_END: 'suite end',
|
|
/**
|
|
* Emitted when {@link Test} execution begins
|
|
*/
|
|
EVENT_TEST_BEGIN: 'test',
|
|
/**
|
|
* Emitted when {@link Test} execution ends
|
|
*/
|
|
EVENT_TEST_END: 'test end',
|
|
/**
|
|
* Emitted when {@link Test} execution fails
|
|
*/
|
|
EVENT_TEST_FAIL: 'fail',
|
|
/**
|
|
* Emitted when {@link Test} execution succeeds
|
|
*/
|
|
EVENT_TEST_PASS: 'pass',
|
|
/**
|
|
* Emitted when {@link Test} becomes pending
|
|
*/
|
|
EVENT_TEST_PENDING: 'pending',
|
|
/**
|
|
* Emitted when {@link Test} execution has failed, but will retry
|
|
*/
|
|
EVENT_TEST_RETRY: 'retry',
|
|
/**
|
|
* Initial state of Runner
|
|
*/
|
|
STATE_IDLE: 'idle',
|
|
/**
|
|
* State set to this value when the Runner has started running
|
|
*/
|
|
STATE_RUNNING: 'running',
|
|
/**
|
|
* State set to this value when the Runner has stopped
|
|
*/
|
|
STATE_STOPPED: 'stopped'
|
|
}
|
|
);
|
|
|
|
class Runner extends EventEmitter {
|
|
/**
|
|
* Initialize a `Runner` at the Root {@link Suite}, which represents a hierarchy of {@link Suite|Suites} and {@link Test|Tests}.
|
|
*
|
|
* @extends external:EventEmitter
|
|
* @public
|
|
* @class
|
|
* @param {Suite} suite - Root suite
|
|
* @param {Object|boolean} [opts] - Options. If `boolean`, whether or not to delay execution of root suite until ready (for backwards compatibility).
|
|
* @param {boolean} [opts.delay] - Whether to delay execution of root suite until ready.
|
|
* @param {boolean} [opts.cleanReferencesAfterRun] - Whether to clean references to test fns and hooks when a suite is done.
|
|
*/
|
|
constructor(suite, opts) {
|
|
super();
|
|
if (opts === undefined) {
|
|
opts = {};
|
|
}
|
|
if (typeof opts === 'boolean') {
|
|
// TODO: deprecate this
|
|
this._delay = opts;
|
|
opts = {};
|
|
} else {
|
|
this._delay = opts.delay;
|
|
}
|
|
var self = this;
|
|
this._globals = [];
|
|
this._abort = false;
|
|
this.suite = suite;
|
|
this._opts = opts;
|
|
this.state = constants.STATE_IDLE;
|
|
this.total = suite.total();
|
|
this.failures = 0;
|
|
/**
|
|
* @type {Map<EventEmitter,Map<string,Set<EventListener>>>}
|
|
*/
|
|
this._eventListeners = new Map();
|
|
this.on(constants.EVENT_TEST_END, function(test) {
|
|
if (test.type === 'test' && test.retriedTest() && test.parent) {
|
|
var idx =
|
|
test.parent.tests && test.parent.tests.indexOf(test.retriedTest());
|
|
if (idx > -1) test.parent.tests[idx] = test;
|
|
}
|
|
self.checkGlobals(test);
|
|
});
|
|
this.on(constants.EVENT_HOOK_END, function(hook) {
|
|
self.checkGlobals(hook);
|
|
});
|
|
this._defaultGrep = /.*/;
|
|
this.grep(this._defaultGrep);
|
|
this.globals(this.globalProps());
|
|
|
|
this.uncaught = this._uncaught.bind(this);
|
|
this.unhandled = (reason, promise) => {
|
|
if (isMochaError(reason)) {
|
|
debug(
|
|
'trapped unhandled rejection coming out of Mocha; forwarding to uncaught handler:',
|
|
reason
|
|
);
|
|
this.uncaught(reason);
|
|
} else {
|
|
debug(
|
|
'trapped unhandled rejection from (probably) user code; re-emitting on process'
|
|
);
|
|
this._removeEventListener(
|
|
process,
|
|
'unhandledRejection',
|
|
this.unhandled
|
|
);
|
|
try {
|
|
process.emit('unhandledRejection', reason, promise);
|
|
} finally {
|
|
this._addEventListener(process, 'unhandledRejection', this.unhandled);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wrapper for setImmediate, process.nextTick, or browser polyfill.
|
|
*
|
|
* @param {Function} fn
|
|
* @private
|
|
*/
|
|
Runner.immediately = global.setImmediate || process.nextTick;
|
|
|
|
/**
|
|
* Replacement for `target.on(eventName, listener)` that does bookkeeping to remove them when this runner instance is disposed.
|
|
* @param {EventEmitter} target - The `EventEmitter`
|
|
* @param {string} eventName - The event name
|
|
* @param {string} fn - Listener function
|
|
* @private
|
|
*/
|
|
Runner.prototype._addEventListener = function(target, eventName, listener) {
|
|
debug(
|
|
'_addEventListener(): adding for event %s; %d current listeners',
|
|
eventName,
|
|
target.listenerCount(eventName)
|
|
);
|
|
/* istanbul ignore next */
|
|
if (
|
|
this._eventListeners.has(target) &&
|
|
this._eventListeners.get(target).has(eventName) &&
|
|
this._eventListeners
|
|
.get(target)
|
|
.get(eventName)
|
|
.has(listener)
|
|
) {
|
|
debug(
|
|
'warning: tried to attach duplicate event listener for %s',
|
|
eventName
|
|
);
|
|
return;
|
|
}
|
|
target.on(eventName, listener);
|
|
const targetListeners = this._eventListeners.has(target)
|
|
? this._eventListeners.get(target)
|
|
: new Map();
|
|
const targetEventListeners = targetListeners.has(eventName)
|
|
? targetListeners.get(eventName)
|
|
: new Set();
|
|
targetEventListeners.add(listener);
|
|
targetListeners.set(eventName, targetEventListeners);
|
|
this._eventListeners.set(target, targetListeners);
|
|
};
|
|
|
|
/**
|
|
* Replacement for `target.removeListener(eventName, listener)` that also updates the bookkeeping.
|
|
* @param {EventEmitter} target - The `EventEmitter`
|
|
* @param {string} eventName - The event name
|
|
* @param {function} listener - Listener function
|
|
* @private
|
|
*/
|
|
Runner.prototype._removeEventListener = function(target, eventName, listener) {
|
|
target.removeListener(eventName, listener);
|
|
|
|
if (this._eventListeners.has(target)) {
|
|
const targetListeners = this._eventListeners.get(target);
|
|
if (targetListeners.has(eventName)) {
|
|
const targetEventListeners = targetListeners.get(eventName);
|
|
targetEventListeners.delete(listener);
|
|
if (!targetEventListeners.size) {
|
|
targetListeners.delete(eventName);
|
|
}
|
|
}
|
|
if (!targetListeners.size) {
|
|
this._eventListeners.delete(target);
|
|
}
|
|
} else {
|
|
debug('trying to remove listener for untracked object %s', target);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Removes all event handlers set during a run on this instance.
|
|
* Remark: this does _not_ clean/dispose the tests or suites themselves.
|
|
*/
|
|
Runner.prototype.dispose = function() {
|
|
this.removeAllListeners();
|
|
this._eventListeners.forEach((targetListeners, target) => {
|
|
targetListeners.forEach((targetEventListeners, eventName) => {
|
|
targetEventListeners.forEach(listener => {
|
|
target.removeListener(eventName, listener);
|
|
});
|
|
});
|
|
});
|
|
this._eventListeners.clear();
|
|
};
|
|
|
|
/**
|
|
* Run tests with full titles matching `re`. Updates runner.total
|
|
* with number of tests matched.
|
|
*
|
|
* @public
|
|
* @memberof Runner
|
|
* @param {RegExp} re
|
|
* @param {boolean} invert
|
|
* @return {Runner} Runner instance.
|
|
*/
|
|
Runner.prototype.grep = function(re, invert) {
|
|
debug('grep(): setting to %s', re);
|
|
this._grep = re;
|
|
this._invert = invert;
|
|
this.total = this.grepTotal(this.suite);
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Returns the number of tests matching the grep search for the
|
|
* given suite.
|
|
*
|
|
* @memberof Runner
|
|
* @public
|
|
* @param {Suite} suite
|
|
* @return {number}
|
|
*/
|
|
Runner.prototype.grepTotal = function(suite) {
|
|
var self = this;
|
|
var total = 0;
|
|
|
|
suite.eachTest(function(test) {
|
|
var match = self._grep.test(test.fullTitle());
|
|
if (self._invert) {
|
|
match = !match;
|
|
}
|
|
if (match) {
|
|
total++;
|
|
}
|
|
});
|
|
|
|
return total;
|
|
};
|
|
|
|
/**
|
|
* Return a list of global properties.
|
|
*
|
|
* @return {Array}
|
|
* @private
|
|
*/
|
|
Runner.prototype.globalProps = function() {
|
|
var props = Object.keys(global);
|
|
|
|
// non-enumerables
|
|
for (var i = 0; i < globals.length; ++i) {
|
|
if (~props.indexOf(globals[i])) {
|
|
continue;
|
|
}
|
|
props.push(globals[i]);
|
|
}
|
|
|
|
return props;
|
|
};
|
|
|
|
/**
|
|
* Allow the given `arr` of globals.
|
|
*
|
|
* @public
|
|
* @memberof Runner
|
|
* @param {Array} arr
|
|
* @return {Runner} Runner instance.
|
|
*/
|
|
Runner.prototype.globals = function(arr) {
|
|
if (!arguments.length) {
|
|
return this._globals;
|
|
}
|
|
debug('globals(): setting to %O', arr);
|
|
this._globals = this._globals.concat(arr);
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Check for global variable leaks.
|
|
*
|
|
* @private
|
|
*/
|
|
Runner.prototype.checkGlobals = function(test) {
|
|
if (!this.checkLeaks) {
|
|
return;
|
|
}
|
|
var ok = this._globals;
|
|
|
|
var globals = this.globalProps();
|
|
var leaks;
|
|
|
|
if (test) {
|
|
ok = ok.concat(test._allowedGlobals || []);
|
|
}
|
|
|
|
if (this.prevGlobalsLength === globals.length) {
|
|
return;
|
|
}
|
|
this.prevGlobalsLength = globals.length;
|
|
|
|
leaks = filterLeaks(ok, globals);
|
|
this._globals = this._globals.concat(leaks);
|
|
|
|
if (leaks.length) {
|
|
var msg = 'global leak(s) detected: %s';
|
|
var error = new Error(util.format(msg, leaks.map(sQuote).join(', ')));
|
|
this.fail(test, error);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Fail the given `test`.
|
|
*
|
|
* If `test` is a hook, failures work in the following pattern:
|
|
* - If bail, run corresponding `after each` and `after` hooks,
|
|
* then exit
|
|
* - Failed `before` hook skips all tests in a suite and subsuites,
|
|
* but jumps to corresponding `after` hook
|
|
* - Failed `before each` hook skips remaining tests in a
|
|
* suite and jumps to corresponding `after each` hook,
|
|
* which is run only once
|
|
* - Failed `after` hook does not alter execution order
|
|
* - Failed `after each` hook skips remaining tests in a
|
|
* suite and subsuites, but executes other `after each`
|
|
* hooks
|
|
*
|
|
* @private
|
|
* @param {Runnable} test
|
|
* @param {Error} err
|
|
* @param {boolean} [force=false] - Whether to fail a pending test.
|
|
*/
|
|
Runner.prototype.fail = function(test, err, force) {
|
|
force = force === true;
|
|
if (test.isPending() && !force) {
|
|
return;
|
|
}
|
|
if (this.state === constants.STATE_STOPPED) {
|
|
if (err.code === errorConstants.MULTIPLE_DONE) {
|
|
throw err;
|
|
}
|
|
throw createFatalError(
|
|
'Test failed after root suite execution completed!',
|
|
err
|
|
);
|
|
}
|
|
|
|
++this.failures;
|
|
debug('total number of failures: %d', this.failures);
|
|
test.state = STATE_FAILED;
|
|
|
|
if (!isError(err)) {
|
|
err = thrown2Error(err);
|
|
}
|
|
|
|
try {
|
|
err.stack =
|
|
this.fullStackTrace || !err.stack ? err.stack : stackFilter(err.stack);
|
|
} catch (ignore) {
|
|
// some environments do not take kindly to monkeying with the stack
|
|
}
|
|
|
|
this.emit(constants.EVENT_TEST_FAIL, test, err);
|
|
};
|
|
|
|
/**
|
|
* Run hook `name` callbacks and then invoke `fn()`.
|
|
*
|
|
* @private
|
|
* @param {string} name
|
|
* @param {Function} fn
|
|
*/
|
|
|
|
Runner.prototype.hook = function(name, fn) {
|
|
var suite = this.suite;
|
|
var hooks = suite.getHooks(name);
|
|
var self = this;
|
|
|
|
function next(i) {
|
|
var hook = hooks[i];
|
|
if (!hook) {
|
|
return fn();
|
|
}
|
|
self.currentRunnable = hook;
|
|
|
|
if (name === HOOK_TYPE_BEFORE_ALL) {
|
|
hook.ctx.currentTest = hook.parent.tests[0];
|
|
} else if (name === HOOK_TYPE_AFTER_ALL) {
|
|
hook.ctx.currentTest = hook.parent.tests[hook.parent.tests.length - 1];
|
|
} else {
|
|
hook.ctx.currentTest = self.test;
|
|
}
|
|
|
|
setHookTitle(hook);
|
|
|
|
hook.allowUncaught = self.allowUncaught;
|
|
|
|
self.emit(constants.EVENT_HOOK_BEGIN, hook);
|
|
|
|
if (!hook.listeners('error').length) {
|
|
self._addEventListener(hook, 'error', function(err) {
|
|
self.fail(hook, err);
|
|
});
|
|
}
|
|
|
|
hook.run(function cbHookRun(err) {
|
|
var testError = hook.error();
|
|
if (testError) {
|
|
self.fail(self.test, testError);
|
|
}
|
|
// conditional skip
|
|
if (hook.pending) {
|
|
if (name === HOOK_TYPE_AFTER_EACH) {
|
|
// TODO define and implement use case
|
|
if (self.test) {
|
|
self.test.pending = true;
|
|
}
|
|
} else if (name === HOOK_TYPE_BEFORE_EACH) {
|
|
if (self.test) {
|
|
self.test.pending = true;
|
|
}
|
|
self.emit(constants.EVENT_HOOK_END, hook);
|
|
hook.pending = false; // activates hook for next test
|
|
return fn(new Error('abort hookDown'));
|
|
} else if (name === HOOK_TYPE_BEFORE_ALL) {
|
|
suite.tests.forEach(function(test) {
|
|
test.pending = true;
|
|
});
|
|
suite.suites.forEach(function(suite) {
|
|
suite.pending = true;
|
|
});
|
|
hooks = [];
|
|
} else {
|
|
hook.pending = false;
|
|
var errForbid = createUnsupportedError('`this.skip` forbidden');
|
|
self.fail(hook, errForbid);
|
|
return fn(errForbid);
|
|
}
|
|
} else if (err) {
|
|
self.fail(hook, err);
|
|
// stop executing hooks, notify callee of hook err
|
|
return fn(err);
|
|
}
|
|
self.emit(constants.EVENT_HOOK_END, hook);
|
|
delete hook.ctx.currentTest;
|
|
setHookTitle(hook);
|
|
next(++i);
|
|
});
|
|
|
|
function setHookTitle(hook) {
|
|
hook.originalTitle = hook.originalTitle || hook.title;
|
|
if (hook.ctx && hook.ctx.currentTest) {
|
|
hook.title =
|
|
hook.originalTitle + ' for ' + dQuote(hook.ctx.currentTest.title);
|
|
} else {
|
|
var parentTitle;
|
|
if (hook.parent.title) {
|
|
parentTitle = hook.parent.title;
|
|
} else {
|
|
parentTitle = hook.parent.root ? '{root}' : '';
|
|
}
|
|
hook.title = hook.originalTitle + ' in ' + dQuote(parentTitle);
|
|
}
|
|
}
|
|
}
|
|
|
|
Runner.immediately(function() {
|
|
next(0);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Run hook `name` for the given array of `suites`
|
|
* in order, and callback `fn(err, errSuite)`.
|
|
*
|
|
* @private
|
|
* @param {string} name
|
|
* @param {Array} suites
|
|
* @param {Function} fn
|
|
*/
|
|
Runner.prototype.hooks = function(name, suites, fn) {
|
|
var self = this;
|
|
var orig = this.suite;
|
|
|
|
function next(suite) {
|
|
self.suite = suite;
|
|
|
|
if (!suite) {
|
|
self.suite = orig;
|
|
return fn();
|
|
}
|
|
|
|
self.hook(name, function(err) {
|
|
if (err) {
|
|
var errSuite = self.suite;
|
|
self.suite = orig;
|
|
return fn(err, errSuite);
|
|
}
|
|
|
|
next(suites.pop());
|
|
});
|
|
}
|
|
|
|
next(suites.pop());
|
|
};
|
|
|
|
/**
|
|
* Run hooks from the top level down.
|
|
*
|
|
* @param {String} name
|
|
* @param {Function} fn
|
|
* @private
|
|
*/
|
|
Runner.prototype.hookUp = function(name, fn) {
|
|
var suites = [this.suite].concat(this.parents()).reverse();
|
|
this.hooks(name, suites, fn);
|
|
};
|
|
|
|
/**
|
|
* Run hooks from the bottom up.
|
|
*
|
|
* @param {String} name
|
|
* @param {Function} fn
|
|
* @private
|
|
*/
|
|
Runner.prototype.hookDown = function(name, fn) {
|
|
var suites = [this.suite].concat(this.parents());
|
|
this.hooks(name, suites, fn);
|
|
};
|
|
|
|
/**
|
|
* Return an array of parent Suites from
|
|
* closest to furthest.
|
|
*
|
|
* @return {Array}
|
|
* @private
|
|
*/
|
|
Runner.prototype.parents = function() {
|
|
var suite = this.suite;
|
|
var suites = [];
|
|
while (suite.parent) {
|
|
suite = suite.parent;
|
|
suites.push(suite);
|
|
}
|
|
return suites;
|
|
};
|
|
|
|
/**
|
|
* Run the current test and callback `fn(err)`.
|
|
*
|
|
* @param {Function} fn
|
|
* @private
|
|
*/
|
|
Runner.prototype.runTest = function(fn) {
|
|
var self = this;
|
|
var test = this.test;
|
|
|
|
if (!test) {
|
|
return;
|
|
}
|
|
|
|
if (this.asyncOnly) {
|
|
test.asyncOnly = true;
|
|
}
|
|
this._addEventListener(test, 'error', function(err) {
|
|
self.fail(test, err);
|
|
});
|
|
if (this.allowUncaught) {
|
|
test.allowUncaught = true;
|
|
return test.run(fn);
|
|
}
|
|
try {
|
|
test.run(fn);
|
|
} catch (err) {
|
|
fn(err);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Run tests in the given `suite` and invoke the callback `fn()` when complete.
|
|
*
|
|
* @private
|
|
* @param {Suite} suite
|
|
* @param {Function} fn
|
|
*/
|
|
Runner.prototype.runTests = function(suite, fn) {
|
|
var self = this;
|
|
var tests = suite.tests.slice();
|
|
var test;
|
|
|
|
function hookErr(_, errSuite, after) {
|
|
// before/after Each hook for errSuite failed:
|
|
var orig = self.suite;
|
|
|
|
// for failed 'after each' hook start from errSuite parent,
|
|
// otherwise start from errSuite itself
|
|
self.suite = after ? errSuite.parent : errSuite;
|
|
|
|
if (self.suite) {
|
|
// call hookUp afterEach
|
|
self.hookUp(HOOK_TYPE_AFTER_EACH, function(err2, errSuite2) {
|
|
self.suite = orig;
|
|
// some hooks may fail even now
|
|
if (err2) {
|
|
return hookErr(err2, errSuite2, true);
|
|
}
|
|
// report error suite
|
|
fn(errSuite);
|
|
});
|
|
} else {
|
|
// there is no need calling other 'after each' hooks
|
|
self.suite = orig;
|
|
fn(errSuite);
|
|
}
|
|
}
|
|
|
|
function next(err, errSuite) {
|
|
// if we bail after first err
|
|
if (self.failures && suite._bail) {
|
|
tests = [];
|
|
}
|
|
|
|
if (self._abort) {
|
|
return fn();
|
|
}
|
|
|
|
if (err) {
|
|
return hookErr(err, errSuite, true);
|
|
}
|
|
|
|
// next test
|
|
test = tests.shift();
|
|
|
|
// all done
|
|
if (!test) {
|
|
return fn();
|
|
}
|
|
|
|
// grep
|
|
var match = self._grep.test(test.fullTitle());
|
|
if (self._invert) {
|
|
match = !match;
|
|
}
|
|
if (!match) {
|
|
// Run immediately only if we have defined a grep. When we
|
|
// define a grep — It can cause maximum callstack error if
|
|
// the grep is doing a large recursive loop by neglecting
|
|
// all tests. The run immediately function also comes with
|
|
// a performance cost. So we don't want to run immediately
|
|
// if we run the whole test suite, because running the whole
|
|
// test suite don't do any immediate recursive loops. Thus,
|
|
// allowing a JS runtime to breathe.
|
|
if (self._grep !== self._defaultGrep) {
|
|
Runner.immediately(next);
|
|
} else {
|
|
next();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// static skip, no hooks are executed
|
|
if (test.isPending()) {
|
|
if (self.forbidPending) {
|
|
self.fail(test, new Error('Pending test forbidden'), true);
|
|
} else {
|
|
test.state = STATE_PENDING;
|
|
self.emit(constants.EVENT_TEST_PENDING, test);
|
|
}
|
|
self.emit(constants.EVENT_TEST_END, test);
|
|
return next();
|
|
}
|
|
|
|
// execute test and hook(s)
|
|
self.emit(constants.EVENT_TEST_BEGIN, (self.test = test));
|
|
self.hookDown(HOOK_TYPE_BEFORE_EACH, function(err, errSuite) {
|
|
// conditional skip within beforeEach
|
|
if (test.isPending()) {
|
|
if (self.forbidPending) {
|
|
self.fail(test, new Error('Pending test forbidden'), true);
|
|
} else {
|
|
test.state = STATE_PENDING;
|
|
self.emit(constants.EVENT_TEST_PENDING, test);
|
|
}
|
|
self.emit(constants.EVENT_TEST_END, test);
|
|
// skip inner afterEach hooks below errSuite level
|
|
var origSuite = self.suite;
|
|
self.suite = errSuite || self.suite;
|
|
return self.hookUp(HOOK_TYPE_AFTER_EACH, function(e, eSuite) {
|
|
self.suite = origSuite;
|
|
next(e, eSuite);
|
|
});
|
|
}
|
|
if (err) {
|
|
return hookErr(err, errSuite, false);
|
|
}
|
|
self.currentRunnable = self.test;
|
|
self.runTest(function(err) {
|
|
test = self.test;
|
|
// conditional skip within it
|
|
if (test.pending) {
|
|
if (self.forbidPending) {
|
|
self.fail(test, new Error('Pending test forbidden'), true);
|
|
} else {
|
|
test.state = STATE_PENDING;
|
|
self.emit(constants.EVENT_TEST_PENDING, test);
|
|
}
|
|
self.emit(constants.EVENT_TEST_END, test);
|
|
return self.hookUp(HOOK_TYPE_AFTER_EACH, next);
|
|
} else if (err) {
|
|
var retry = test.currentRetry();
|
|
if (retry < test.retries()) {
|
|
var clonedTest = test.clone();
|
|
clonedTest.currentRetry(retry + 1);
|
|
tests.unshift(clonedTest);
|
|
|
|
self.emit(constants.EVENT_TEST_RETRY, test, err);
|
|
|
|
// Early return + hook trigger so that it doesn't
|
|
// increment the count wrong
|
|
return self.hookUp(HOOK_TYPE_AFTER_EACH, next);
|
|
} else {
|
|
self.fail(test, err);
|
|
}
|
|
self.emit(constants.EVENT_TEST_END, test);
|
|
return self.hookUp(HOOK_TYPE_AFTER_EACH, next);
|
|
}
|
|
|
|
test.state = STATE_PASSED;
|
|
self.emit(constants.EVENT_TEST_PASS, test);
|
|
self.emit(constants.EVENT_TEST_END, test);
|
|
self.hookUp(HOOK_TYPE_AFTER_EACH, next);
|
|
});
|
|
});
|
|
}
|
|
|
|
this.next = next;
|
|
this.hookErr = hookErr;
|
|
next();
|
|
};
|
|
|
|
/**
|
|
* Run the given `suite` and invoke the callback `fn()` when complete.
|
|
*
|
|
* @private
|
|
* @param {Suite} suite
|
|
* @param {Function} fn
|
|
*/
|
|
Runner.prototype.runSuite = function(suite, fn) {
|
|
var i = 0;
|
|
var self = this;
|
|
var total = this.grepTotal(suite);
|
|
|
|
debug('runSuite(): running %s', suite.fullTitle());
|
|
|
|
if (!total || (self.failures && suite._bail)) {
|
|
debug('runSuite(): bailing');
|
|
return fn();
|
|
}
|
|
|
|
this.emit(constants.EVENT_SUITE_BEGIN, (this.suite = suite));
|
|
|
|
function next(errSuite) {
|
|
if (errSuite) {
|
|
// current suite failed on a hook from errSuite
|
|
if (errSuite === suite) {
|
|
// if errSuite is current suite
|
|
// continue to the next sibling suite
|
|
return done();
|
|
}
|
|
// errSuite is among the parents of current suite
|
|
// stop execution of errSuite and all sub-suites
|
|
return done(errSuite);
|
|
}
|
|
|
|
if (self._abort) {
|
|
return done();
|
|
}
|
|
|
|
var curr = suite.suites[i++];
|
|
if (!curr) {
|
|
return done();
|
|
}
|
|
|
|
// Avoid grep neglecting large number of tests causing a
|
|
// huge recursive loop and thus a maximum call stack error.
|
|
// See comment in `this.runTests()` for more information.
|
|
if (self._grep !== self._defaultGrep) {
|
|
Runner.immediately(function() {
|
|
self.runSuite(curr, next);
|
|
});
|
|
} else {
|
|
self.runSuite(curr, next);
|
|
}
|
|
}
|
|
|
|
function done(errSuite) {
|
|
self.suite = suite;
|
|
self.nextSuite = next;
|
|
|
|
// remove reference to test
|
|
delete self.test;
|
|
|
|
self.hook(HOOK_TYPE_AFTER_ALL, function() {
|
|
self.emit(constants.EVENT_SUITE_END, suite);
|
|
fn(errSuite);
|
|
});
|
|
}
|
|
|
|
this.nextSuite = next;
|
|
|
|
this.hook(HOOK_TYPE_BEFORE_ALL, function(err) {
|
|
if (err) {
|
|
return done();
|
|
}
|
|
self.runTests(suite, next);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Handle uncaught exceptions within runner.
|
|
*
|
|
* This function is bound to the instance as `Runner#uncaught` at instantiation
|
|
* time. It's intended to be listening on the `Process.uncaughtException` event.
|
|
* In order to not leak EE listeners, we need to ensure no more than a single
|
|
* `uncaughtException` listener exists per `Runner`. The only way to do
|
|
* this--because this function needs the context (and we don't have lambdas)--is
|
|
* to use `Function.prototype.bind`. We need strict equality to unregister and
|
|
* _only_ unregister the _one_ listener we set from the
|
|
* `Process.uncaughtException` event; would be poor form to just remove
|
|
* everything. See {@link Runner#run} for where the event listener is registered
|
|
* and unregistered.
|
|
* @param {Error} err - Some uncaught error
|
|
* @private
|
|
*/
|
|
Runner.prototype._uncaught = function(err) {
|
|
// this is defensive to prevent future developers from mis-calling this function.
|
|
// it's more likely that it'd be called with the incorrect context--say, the global
|
|
// `process` object--than it would to be called with a context that is not a "subclass"
|
|
// of `Runner`.
|
|
if (!(this instanceof Runner)) {
|
|
throw createFatalError(
|
|
'Runner#uncaught() called with invalid context',
|
|
this
|
|
);
|
|
}
|
|
if (err instanceof Pending) {
|
|
debug('uncaught(): caught a Pending');
|
|
return;
|
|
}
|
|
// browser does not exit script when throwing in global.onerror()
|
|
if (this.allowUncaught && !utils.isBrowser()) {
|
|
debug('uncaught(): bubbling exception due to --allow-uncaught');
|
|
throw err;
|
|
}
|
|
|
|
if (this.state === constants.STATE_STOPPED) {
|
|
debug('uncaught(): throwing after run has completed!');
|
|
throw err;
|
|
}
|
|
|
|
if (err) {
|
|
debug('uncaught(): got truthy exception %O', err);
|
|
} else {
|
|
debug('uncaught(): undefined/falsy exception');
|
|
err = createInvalidExceptionError(
|
|
'Caught falsy/undefined exception which would otherwise be uncaught. No stack trace found; try a debugger',
|
|
err
|
|
);
|
|
}
|
|
|
|
if (!isError(err)) {
|
|
err = thrown2Error(err);
|
|
debug('uncaught(): converted "error" %o to Error', err);
|
|
}
|
|
err.uncaught = true;
|
|
|
|
var runnable = this.currentRunnable;
|
|
|
|
if (!runnable) {
|
|
runnable = new Runnable('Uncaught error outside test suite');
|
|
debug('uncaught(): no current Runnable; created a phony one');
|
|
runnable.parent = this.suite;
|
|
|
|
if (this.state === constants.STATE_RUNNING) {
|
|
debug('uncaught(): failing gracefully');
|
|
this.fail(runnable, err);
|
|
} else {
|
|
// Can't recover from this failure
|
|
debug('uncaught(): test run has not yet started; unrecoverable');
|
|
this.emit(constants.EVENT_RUN_BEGIN);
|
|
this.fail(runnable, err);
|
|
this.emit(constants.EVENT_RUN_END);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
runnable.clearTimeout();
|
|
|
|
if (runnable.isFailed()) {
|
|
debug('uncaught(): Runnable has already failed');
|
|
// Ignore error if already failed
|
|
return;
|
|
} else if (runnable.isPending()) {
|
|
debug('uncaught(): pending Runnable wound up failing!');
|
|
// report 'pending test' retrospectively as failed
|
|
this.fail(runnable, err, true);
|
|
return;
|
|
}
|
|
|
|
// we cannot recover gracefully if a Runnable has already passed
|
|
// then fails asynchronously
|
|
if (runnable.isPassed()) {
|
|
debug('uncaught(): Runnable has already passed; bailing gracefully');
|
|
this.fail(runnable, err);
|
|
this.abort();
|
|
} else {
|
|
debug('uncaught(): forcing Runnable to complete with Error');
|
|
return runnable.callback(err);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Run the root suite and invoke `fn(failures)`
|
|
* on completion.
|
|
*
|
|
* @public
|
|
* @memberof Runner
|
|
* @param {Function} fn - Callback when finished
|
|
* @param {{files: string[], options: Options}} [opts] - For subclasses
|
|
* @returns {Runner} Runner instance.
|
|
*/
|
|
Runner.prototype.run = function(fn, opts = {}) {
|
|
var rootSuite = this.suite;
|
|
var options = opts.options || {};
|
|
|
|
debug('run(): got options: %O', options);
|
|
fn = fn || function() {};
|
|
|
|
const end = () => {
|
|
debug('run(): root suite completed; emitting %s', constants.EVENT_RUN_END);
|
|
this.emit(constants.EVENT_RUN_END);
|
|
};
|
|
|
|
const begin = () => {
|
|
debug('run(): emitting %s', constants.EVENT_RUN_BEGIN);
|
|
this.emit(constants.EVENT_RUN_BEGIN);
|
|
debug('run(): emitted %s', constants.EVENT_RUN_BEGIN);
|
|
|
|
this.runSuite(rootSuite, end);
|
|
};
|
|
|
|
const prepare = () => {
|
|
debug('run(): starting');
|
|
// If there is an `only` filter
|
|
if (rootSuite.hasOnly()) {
|
|
rootSuite.filterOnly();
|
|
debug('run(): filtered exclusive Runnables');
|
|
}
|
|
this.state = constants.STATE_RUNNING;
|
|
if (this._delay) {
|
|
this.emit(constants.EVENT_DELAY_END);
|
|
debug('run(): "delay" ended');
|
|
}
|
|
|
|
return begin();
|
|
};
|
|
|
|
// references cleanup to avoid memory leaks
|
|
if (this._opts.cleanReferencesAfterRun) {
|
|
this.on(constants.EVENT_SUITE_END, suite => {
|
|
suite.cleanReferences();
|
|
});
|
|
}
|
|
|
|
// callback
|
|
this.on(constants.EVENT_RUN_END, function() {
|
|
this.state = constants.STATE_STOPPED;
|
|
debug('run(): emitted %s', constants.EVENT_RUN_END);
|
|
fn(this.failures);
|
|
});
|
|
|
|
this._removeEventListener(process, 'uncaughtException', this.uncaught);
|
|
this._removeEventListener(process, 'unhandledRejection', this.unhandled);
|
|
this._addEventListener(process, 'uncaughtException', this.uncaught);
|
|
this._addEventListener(process, 'unhandledRejection', this.unhandled);
|
|
|
|
if (this._delay) {
|
|
// for reporters, I guess.
|
|
// might be nice to debounce some dots while we wait.
|
|
this.emit(constants.EVENT_DELAY_BEGIN, rootSuite);
|
|
rootSuite.once(EVENT_ROOT_SUITE_RUN, prepare);
|
|
debug('run(): waiting for green light due to --delay');
|
|
} else {
|
|
Runner.immediately(prepare);
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Toggle partial object linking behavior; used for building object references from
|
|
* unique ID's. Does nothing in serial mode, because the object references already exist.
|
|
* Subclasses can implement this (e.g., `ParallelBufferedRunner`)
|
|
* @abstract
|
|
* @param {boolean} [value] - If `true`, enable partial object linking, otherwise disable
|
|
* @returns {Runner}
|
|
* @chainable
|
|
* @public
|
|
* @example
|
|
* // this reporter needs proper object references when run in parallel mode
|
|
* class MyReporter() {
|
|
* constructor(runner) {
|
|
* this.runner.linkPartialObjects(true)
|
|
* .on(EVENT_SUITE_BEGIN, suite => {
|
|
// this Suite may be the same object...
|
|
* })
|
|
* .on(EVENT_TEST_BEGIN, test => {
|
|
* // ...as the `test.parent` property
|
|
* });
|
|
* }
|
|
* }
|
|
*/
|
|
Runner.prototype.linkPartialObjects = function(value) {
|
|
return this;
|
|
};
|
|
|
|
/*
|
|
* Like {@link Runner#run}, but does not accept a callback and returns a `Promise` instead of a `Runner`.
|
|
* This function cannot reject; an `unhandledRejection` event will bubble up to the `process` object instead.
|
|
* @public
|
|
* @memberof Runner
|
|
* @param {Object} [opts] - Options for {@link Runner#run}
|
|
* @returns {Promise<number>} Failure count
|
|
*/
|
|
Runner.prototype.runAsync = async function runAsync(opts = {}) {
|
|
return new Promise(resolve => {
|
|
this.run(resolve, opts);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Cleanly abort execution.
|
|
*
|
|
* @memberof Runner
|
|
* @public
|
|
* @return {Runner} Runner instance.
|
|
*/
|
|
Runner.prototype.abort = function() {
|
|
debug('abort(): aborting');
|
|
this._abort = true;
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Returns `true` if Mocha is running in parallel mode. For reporters.
|
|
*
|
|
* Subclasses should return an appropriate value.
|
|
* @public
|
|
* @returns {false}
|
|
*/
|
|
Runner.prototype.isParallelMode = function isParallelMode() {
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* Configures an alternate reporter for worker processes to use. Subclasses
|
|
* using worker processes should implement this.
|
|
* @public
|
|
* @param {string} path - Absolute path to alternate reporter for worker processes to use
|
|
* @returns {Runner}
|
|
* @throws When in serial mode
|
|
* @chainable
|
|
* @abstract
|
|
*/
|
|
Runner.prototype.workerReporter = function() {
|
|
throw createUnsupportedError('workerReporter() not supported in serial mode');
|
|
};
|
|
|
|
/**
|
|
* Filter leaks with the given globals flagged as `ok`.
|
|
*
|
|
* @private
|
|
* @param {Array} ok
|
|
* @param {Array} globals
|
|
* @return {Array}
|
|
*/
|
|
function filterLeaks(ok, globals) {
|
|
return globals.filter(function(key) {
|
|
// Firefox and Chrome exposes iframes as index inside the window object
|
|
if (/^\d+/.test(key)) {
|
|
return false;
|
|
}
|
|
|
|
// in firefox
|
|
// if runner runs in an iframe, this iframe's window.getInterface method
|
|
// not init at first it is assigned in some seconds
|
|
if (global.navigator && /^getInterface/.test(key)) {
|
|
return false;
|
|
}
|
|
|
|
// an iframe could be approached by window[iframeIndex]
|
|
// in ie6,7,8 and opera, iframeIndex is enumerable, this could cause leak
|
|
if (global.navigator && /^\d+/.test(key)) {
|
|
return false;
|
|
}
|
|
|
|
// Opera and IE expose global variables for HTML element IDs (issue #243)
|
|
if (/^mocha-/.test(key)) {
|
|
return false;
|
|
}
|
|
|
|
var matched = ok.filter(function(ok) {
|
|
if (~ok.indexOf('*')) {
|
|
return key.indexOf(ok.split('*')[0]) === 0;
|
|
}
|
|
return key === ok;
|
|
});
|
|
return !matched.length && (!global.navigator || key !== 'onerror');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Check if argument is an instance of Error object or a duck-typed equivalent.
|
|
*
|
|
* @private
|
|
* @param {Object} err - object to check
|
|
* @param {string} err.message - error message
|
|
* @returns {boolean}
|
|
*/
|
|
function isError(err) {
|
|
return err instanceof Error || (err && typeof err.message === 'string');
|
|
}
|
|
|
|
/**
|
|
*
|
|
* Converts thrown non-extensible type into proper Error.
|
|
*
|
|
* @private
|
|
* @param {*} thrown - Non-extensible type thrown by code
|
|
* @return {Error}
|
|
*/
|
|
function thrown2Error(err) {
|
|
return new Error(
|
|
`the ${utils.canonicalType(err)} ${stringify(
|
|
err
|
|
)} was thrown, throw an Error :)`
|
|
);
|
|
}
|
|
|
|
Runner.constants = constants;
|
|
|
|
/**
|
|
* Node.js' `EventEmitter`
|
|
* @external EventEmitter
|
|
* @see {@link https://nodejs.org/api/events.html#events_class_eventemitter}
|
|
*/
|
|
|
|
module.exports = Runner;
|