| '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; |