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