'use strict';
var EventEmitter = require('events').EventEmitter; var Pending = require('./pending'); var debug = require('debug')('mocha:runnable'); var milliseconds = require('ms'); var utils = require('./utils'); var createInvalidExceptionError = require('./errors')
.createInvalidExceptionError;
/**
* Save timer references to avoid Sinon interfering (see GH-237). */
var Date = global.Date; var setTimeout = global.setTimeout; var clearTimeout = global.clearTimeout; var toString = Object.prototype.toString;
module.exports = Runnable;
/**
* Initialize a new `Runnable` with the given `title` and callback `fn`. * * @class * @extends external:EventEmitter * @public * @param {String} title * @param {Function} fn */
function Runnable(title, fn) {
this.title = title; this.fn = fn; this.body = (fn || '').toString(); this.async = fn && fn.length; this.sync = !this.async; this._timeout = 2000; this._slow = 75; this._enableTimeouts = true; this.timedOut = false; this._retries = -1; this._currentRetry = 0; this.pending = false;
}
/**
* Inherit from `EventEmitter.prototype`. */
utils.inherits(Runnable, EventEmitter);
/**
* Get current timeout value in msecs. * * @private * @returns {number} current timeout threshold value */
/**
* @summary * Set timeout threshold value (msecs). * * @description * A string argument can use shorthand (e.g., "2s") and will be converted. * The value will be clamped to range [<code>0</code>, <code>2^<sup>31</sup>-1</code>]. * If clamped value matches either range endpoint, timeouts will be disabled. * * @private * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout#Maximum_delay_value} * @param {number|string} ms - Timeout threshold value. * @returns {Runnable} this * @chainable */
Runnable.prototype.timeout = function(ms) {
if (!arguments.length) { return this._timeout; } if (typeof ms === 'string') { ms = milliseconds(ms); } // Clamp to range var INT_MAX = Math.pow(2, 31) - 1; var range = [0, INT_MAX]; ms = utils.clamp(ms, range); // see #1652 for reasoning if (ms === range[0] || ms === range[1]) { this._enableTimeouts = false; } debug('timeout %d', ms); this._timeout = ms; if (this.timer) { this.resetTimeout(); } return this;
};
/**
* Set or get slow `ms`. * * @private * @param {number|string} ms * @return {Runnable|number} ms or Runnable instance. */
Runnable.prototype.slow = function(ms) {
if (!arguments.length || typeof ms === 'undefined') { return this._slow; } if (typeof ms === 'string') { ms = milliseconds(ms); } debug('slow %d', ms); this._slow = ms; return this;
};
/**
* Set and get whether timeout is `enabled`. * * @private * @param {boolean} enabled * @return {Runnable|boolean} enabled or Runnable instance. */
Runnable.prototype.enableTimeouts = function(enabled) {
if (!arguments.length) { return this._enableTimeouts; } debug('enableTimeouts %s', enabled); this._enableTimeouts = enabled; return this;
};
/**
* Halt and mark as pending. * * @memberof Mocha.Runnable * @public */
Runnable.prototype.skip = function() {
throw new Pending('sync skip');
};
/**
* Check if this runnable or its parent suite is marked as pending. * * @private */
Runnable.prototype.isPending = function() {
return this.pending || (this.parent && this.parent.isPending());
};
/**
* Return `true` if this Runnable has failed. * @return {boolean} * @private */
Runnable.prototype.isFailed = function() {
return !this.isPending() && this.state === constants.STATE_FAILED;
};
/**
* Return `true` if this Runnable has passed. * @return {boolean} * @private */
Runnable.prototype.isPassed = function() {
return !this.isPending() && this.state === constants.STATE_PASSED;
};
/**
* Set or get number of retries. * * @private */
Runnable.prototype.retries = function(n) {
if (!arguments.length) { return this._retries; } this._retries = n;
};
/**
* Set or get current retry * * @private */
Runnable.prototype.currentRetry = function(n) {
if (!arguments.length) { return this._currentRetry; } this._currentRetry = n;
};
/**
* Return the full title generated by recursively concatenating the parent's * full title. * * @memberof Mocha.Runnable * @public * @return {string} */
Runnable.prototype.fullTitle = function() {
return this.titlePath().join(' ');
};
/**
* Return the title path generated by concatenating the parent's title path with the title. * * @memberof Mocha.Runnable * @public * @return {string} */
Runnable.prototype.titlePath = function() {
return this.parent.titlePath().concat([this.title]);
};
/**
* Clear the timeout. * * @private */
Runnable.prototype.clearTimeout = function() {
clearTimeout(this.timer);
};
/**
* Inspect the runnable void of private properties. * * @private * @return {string} */
Runnable.prototype.inspect = function() {
return JSON.stringify( this, function(key, val) { if (key[0] === '_') { return; } if (key === 'parent') { return '#<Suite>'; } if (key === 'ctx') { return '#<Context>'; } return val; }, 2 );
};
/**
* Reset the timeout. * * @private */
Runnable.prototype.resetTimeout = function() {
var self = this; var ms = this.timeout() || 1e9; if (!this._enableTimeouts) { return; } this.clearTimeout(); this.timer = setTimeout(function() { if (!self._enableTimeouts) { return; } self.callback(self._timeoutError(ms)); self.timedOut = true; }, ms);
};
/**
* Set or get a list of whitelisted globals for this test run. * * @private * @param {string[]} globals */
Runnable.prototype.globals = function(globals) {
if (!arguments.length) { return this._allowedGlobals; } this._allowedGlobals = globals;
};
/**
* Run the test and invoke `fn(err)`. * * @param {Function} fn * @private */
Runnable.prototype.run = function(fn) {
var self = this; var start = new Date(); var ctx = this.ctx; var finished; var emitted; // Sometimes the ctx exists, but it is not runnable if (ctx && ctx.runnable) { ctx.runnable(this); } // called multiple times function multiple(err) { if (emitted) { return; } emitted = true; var msg = 'done() called multiple times'; if (err && err.message) { err.message += " (and Mocha's " + msg + ')'; self.emit('error', err); } else { self.emit('error', new Error(msg)); } } // finished function done(err) { var ms = self.timeout(); if (self.timedOut) { return; } if (finished) { return multiple(err); } self.clearTimeout(); self.duration = new Date() - start; finished = true; if (!err && self.duration > ms && self._enableTimeouts) { err = self._timeoutError(ms); } fn(err); } // for .resetTimeout() this.callback = done; // explicit async with `done` argument if (this.async) { this.resetTimeout(); // allows skip() to be used in an explicit async context this.skip = function asyncSkip() { done(new Pending('async skip call')); // halt execution. the Runnable will be marked pending // by the previous call, and the uncaught handler will ignore // the failure. throw new Pending('async skip; aborting execution'); }; if (this.allowUncaught) { return callFnAsync(this.fn); } try { callFnAsync(this.fn); } catch (err) { emitted = true; done(Runnable.toValueOrError(err)); } return; } if (this.allowUncaught) { if (this.isPending()) { done(); } else { callFn(this.fn); } return; } // sync or promise-returning try { if (this.isPending()) { done(); } else { callFn(this.fn); } } catch (err) { emitted = true; done(Runnable.toValueOrError(err)); } function callFn(fn) { var result = fn.call(ctx); if (result && typeof result.then === 'function') { self.resetTimeout(); result.then( function() { done(); // Return null so libraries like bluebird do not warn about // subsequently constructed Promises. return null; }, function(reason) { done(reason || new Error('Promise rejected with no or falsy reason')); } ); } else { if (self.asyncOnly) { return done( new Error( '--async-only option in use without declaring `done()` or returning a promise' ) ); } done(); } } function callFnAsync(fn) { var result = fn.call(ctx, function(err) { if (err instanceof Error || toString.call(err) === '[object Error]') { return done(err); } if (err) { if (Object.prototype.toString.call(err) === '[object Object]') { return done( new Error('done() invoked with non-Error: ' + JSON.stringify(err)) ); } return done(new Error('done() invoked with non-Error: ' + err)); } if (result && utils.isPromise(result)) { return done( new Error( 'Resolution method is overspecified. Specify a callback *or* return a Promise; not both.' ) ); } done(); }); }
};
/**
* Instantiates a "timeout" error * * @param {number} ms - Timeout (in milliseconds) * @returns {Error} a "timeout" error * @private */
Runnable.prototype._timeoutError = function(ms) {
var msg = 'Timeout of ' + ms + 'ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves.'; if (this.file) { msg += ' (' + this.file + ')'; } return new Error(msg);
};
var constants = utils.defineConstants(
/** * {@link Runnable}-related constants. * @public * @memberof Runnable * @readonly * @static * @alias constants * @enum {string} */ { /** * Value of `state` prop when a `Runnable` has failed */ STATE_FAILED: 'failed', /** * Value of `state` prop when a `Runnable` has passed */ STATE_PASSED: 'passed' }
);
/**
* Given `value`, return identity if truthy, otherwise create an "invalid exception" error and return that. * @param {*} [value] - Value to return, if present * @returns {*|Error} `value`, otherwise an `Error` * @private */
Runnable.toValueOrError = function(value) {
return ( value || createInvalidExceptionError( 'Runnable failed with falsy or undefined exception. Please throw an Error instead.', value ) );
};
Runnable.constants = constants;