/** * Module dependencies. */ var EventEmitter = require('events').EventEmitter , debug = require('debug')('mocha:runnable') , Pending = require('./pending') , milliseconds = require('./ms') , utils = require('./utils'); /** * Save timer references to avoid Sinon interfering (see GH-237). */ var Date = global.Date , setTimeout = global.setTimeout , setInterval = global.setInterval , clearTimeout = global.clearTimeout , clearInterval = global.clearInterval; /** * 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 */ function Runnable(title, fn) { this.title = title; this.fn = fn; 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') } /** * Inherit from `EventEmitter.prototype`. */ Runnable.prototype.__proto__ = EventEmitter.prototype; /** * Set & get timeout `ms`. * * @param {Number|String} ms * @return {Runnable|Number} ms or self * @api private */ Runnable.prototype.timeout = function(ms){ if (0 == arguments.length) return this._timeout; if (ms === 0) this._enableTimeouts = false; if ('string' == typeof ms) ms = milliseconds(ms); debug('timeout %d', ms); this._timeout = ms; if (this.timer) this.resetTimeout(); return this; }; /** * Set & get slow `ms`. * * @param {Number|String} ms * @return {Runnable|Number} ms or self * @api private */ Runnable.prototype.slow = function(ms){ if (0 === arguments.length) return this._slow; if ('string' == typeof ms) ms = milliseconds(ms); debug('timeout %d', ms); this._slow = ms; return this; }; /** * Set and & get timeout `enabled`. * * @param {Boolean} enabled * @return {Runnable|Boolean} enabled or self * @api private */ Runnable.prototype.enableTimeouts = function(enabled){ if (arguments.length === 0) return this._enableTimeouts; debug('enableTimeouts %s', enabled); this._enableTimeouts = enabled; return this; }; /** * Halt and mark as pending. * * @api private */ Runnable.prototype.skip = function(){ throw new Pending(); }; /** * Return the full title generated by recursively * concatenating the parent's full title. * * @return {String} * @api public */ 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. * * @return {String} * @api private */ Runnable.prototype.inspect = function(){ return JSON.stringify(this, function(key, val){ if ('_' == key[0]) return; if ('parent' == key) return '#'; if ('ctx' == key) 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 these globals for this test run * * @api private */ Runnable.prototype.globals = function(arr){ var self = this; this._allowedGlobals = arr; }; /** * Run the test and invoke `fn(err)`. * * @param {Function} fn * @api private */ Runnable.prototype.run = function(fn){ var self = this , start = new Date , ctx = this.ctx , finished , emitted; // Some times 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); // Discard the resolution if this test has already failed asynchronously if (self.state) return; 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(); try { this.fn.call(ctx, function(err){ if (err instanceof Error || toString.call(err) === "[object Error]") return done(err); if (null != err) { if (Object.prototype.toString.call(err) === '[object Object]') { return done(new Error('done() invoked with non-Error: ' + JSON.stringify(err))); } else { return done(new Error('done() invoked with non-Error: ' + err)); } } done(); }); } catch (err) { done(utils.getError(err)); } return; } if (this.asyncOnly) { return done(new Error('--async-only option in use without declaring `done()`')); } // sync or promise-returning try { if (this.pending) { 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() }, function(reason) { done(reason || new Error('Promise rejected with no or falsy reason')) }); } else { done(); } } };