/** * Module dependencies. */ var EventEmitter = require('events').EventEmitter; var Pending = require('./pending'); var debug = require('debug')('mocha:runnable'); var milliseconds = require('./ms'); var utils = require('./utils'); var inherits = utils.inherits; /** * Save timer references to avoid Sinon interfering (see GH-237). */ /* eslint-disable no-unused-vars, no-native-reassign */ var Date = global.Date; var setTimeout = global.setTimeout; var setInterval = global.setInterval; var clearTimeout = global.clearTimeout; var clearInterval = global.clearInterval; /* eslint-enable no-unused-vars, no-native-reassign */ /** * Object#toString(). */ var toString = Object.prototype.toString; /** * Expose `Runnable`. */ module.exports = Runnable; /** * Initialize a new `Runnable` with the given `title` and callback `fn`. * * @param {String} title * @param {Function} fn * @api private * @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._trace = new Error('done() called multiple times'); this._retries = -1; this._currentRetry = 0; this.pending = false; } /** * Inherit from `EventEmitter.prototype`. */ inherits(Runnable, EventEmitter); /** * Set & get timeout `ms`. * * @api private * @param {number|string} ms * @return {Runnable|number} ms or Runnable instance. */ Runnable.prototype.timeout = function(ms) { if (!arguments.length) { return this._timeout; } if (ms === 0) { this._enableTimeouts = false; } if (typeof ms === 'string') { ms = milliseconds(ms); } debug('timeout %d', ms); this._timeout = ms; if (this.timer) { this.resetTimeout(); } return this; }; /** * Set & get slow `ms`. * * @api private * @param {number|string} ms * @return {Runnable|number} ms or Runnable instance. */ Runnable.prototype.slow = function(ms) { if (!arguments.length) { return this._slow; } if (typeof ms === 'string') { ms = milliseconds(ms); } debug('timeout %d', ms); this._slow = ms; return this; }; /** * Set and get whether timeout is `enabled`. * * @api 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. * * @api public */ Runnable.prototype.skip = function() { throw new Pending(); }; /** * Check if this runnable or its parent suite is marked as pending. * * @api private */ Runnable.prototype.isPending = function() { return this.pending || (this.parent && this.parent.isPending()); }; /** * Set number of retries. * * @api private */ Runnable.prototype.retries = function(n) { if (!arguments.length) { return this._retries; } this._retries = n; }; /** * Get current retry * * @api 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. * * @api public * @return {string} */ Runnable.prototype.fullTitle = function() { return this.parent.fullTitle() + ' ' + this.title; }; /** * Clear the timeout. * * @api private */ Runnable.prototype.clearTimeout = function() { clearTimeout(this.timer); }; /** * Inspect the runnable void of private properties. * * @api private * @return {string} */ Runnable.prototype.inspect = function() { return JSON.stringify(this, function(key, val) { if (key[0] === '_') { return; } if (key === 'parent') { return '#'; } if (key === 'ctx') { return '#'; } return val; }, 2); }; /** * Reset the timeout. * * @api 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(new Error('timeout of ' + ms + 'ms exceeded. Ensure the done() callback is being called in this test.')); self.timedOut = true; }, ms); }; /** * Whitelist a list of globals for this test run. * * @api 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 * @api 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; self.emit('error', err || new Error('done() called multiple times; stacktrace may be inaccurate')); } // finished function done(err) { var ms = self.timeout(); if (self.timedOut) { return; } if (finished) { return multiple(err || self._trace); } self.clearTimeout(); self.duration = new Date() - start; finished = true; if (!err && self.duration > ms && self._enableTimeouts) { err = new Error('timeout of ' + ms + 'ms exceeded. Ensure the done() callback is being called in this test.'); } fn(err); } // for .resetTimeout() this.callback = done; // explicit async with `done` argument if (this.async) { this.resetTimeout(); if (this.allowUncaught) { return callFnAsync(this.fn); } try { callFnAsync(this.fn); } catch (err) { done(utils.getError(err)); } return; } if (this.allowUncaught) { callFn(this.fn); done(); return; } // sync or promise-returning try { if (this.isPending()) { done(); } else { callFn(this.fn); } } catch (err) { done(utils.getError(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) { 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)); } done(); }); } };