Spaces:
Paused
Paused
| ; | |
| var EventEmitter = require('events').EventEmitter; | |
| var Pending = require('./pending'); | |
| var debug = require('debug')('mocha:runnable'); | |
| var milliseconds = require('ms'); | |
| var utils = require('./utils'); | |
| const { | |
| createInvalidExceptionError, | |
| createMultipleDoneError, | |
| createTimeoutError | |
| } = require('./errors'); | |
| /** | |
| * Save timer references to avoid Sinon interfering (see GH-237). | |
| * @private | |
| */ | |
| 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._retries = -1; | |
| utils.assignNewMochaID(this); | |
| Object.defineProperty(this, 'id', { | |
| get() { | |
| return utils.getMochaID(this); | |
| } | |
| }); | |
| this.reset(); | |
| } | |
| /** | |
| * Inherit from `EventEmitter.prototype`. | |
| */ | |
| utils.inherits(Runnable, EventEmitter); | |
| /** | |
| * Resets the state initially or for a next run. | |
| */ | |
| Runnable.prototype.reset = function() { | |
| this.timedOut = false; | |
| this._currentRetry = 0; | |
| this.pending = false; | |
| delete this.state; | |
| delete this.err; | |
| }; | |
| /** | |
| * 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._timeout = 0; | |
| } else { | |
| this._timeout = ms; | |
| } | |
| debug('timeout %d', this._timeout); | |
| 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; | |
| }; | |
| /** | |
| * Halt and mark as pending. | |
| * | |
| * @memberof Mocha.Runnable | |
| * @public | |
| */ | |
| Runnable.prototype.skip = function() { | |
| this.pending = true; | |
| throw new Pending('sync skip; aborting execution'); | |
| }; | |
| /** | |
| * 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); | |
| }; | |
| /** | |
| * Reset the timeout. | |
| * | |
| * @private | |
| */ | |
| Runnable.prototype.resetTimeout = function() { | |
| var self = this; | |
| var ms = this.timeout(); | |
| if (ms === 0) { | |
| return; | |
| } | |
| this.clearTimeout(); | |
| this.timer = setTimeout(function() { | |
| if (self.timeout() === 0) { | |
| 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 errorWasHandled = false; | |
| if (this.isPending()) return fn(); | |
| // Sometimes the ctx exists, but it is not runnable | |
| if (ctx && ctx.runnable) { | |
| ctx.runnable(this); | |
| } | |
| // called multiple times | |
| function multiple(err) { | |
| if (errorWasHandled) { | |
| return; | |
| } | |
| errorWasHandled = true; | |
| self.emit('error', createMultipleDoneError(self, err)); | |
| } | |
| // 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 && ms > 0) { | |
| err = self._timeoutError(ms); | |
| } | |
| fn(err); | |
| } | |
| // for .resetTimeout() and Runner#uncaught() | |
| this.callback = done; | |
| if (this.fn && typeof this.fn.call !== 'function') { | |
| done( | |
| new TypeError( | |
| 'A runnable must be passed a function as its second argument.' | |
| ) | |
| ); | |
| return; | |
| } | |
| // explicit async with `done` argument | |
| if (this.async) { | |
| this.resetTimeout(); | |
| // allows skip() to be used in an explicit async context | |
| this.skip = function asyncSkip() { | |
| this.pending = true; | |
| done(); | |
| // halt execution, the uncaught handler will ignore the failure. | |
| throw new Pending('async skip; aborting execution'); | |
| }; | |
| try { | |
| callFnAsync(this.fn); | |
| } catch (err) { | |
| // handles async runnables which actually run synchronously | |
| errorWasHandled = true; | |
| if (err instanceof Pending) { | |
| return; // done() is already called in this.skip() | |
| } else if (this.allowUncaught) { | |
| throw err; | |
| } | |
| done(Runnable.toValueOrError(err)); | |
| } | |
| return; | |
| } | |
| // sync or promise-returning | |
| try { | |
| callFn(this.fn); | |
| } catch (err) { | |
| errorWasHandled = true; | |
| if (err instanceof Pending) { | |
| return done(); | |
| } else if (this.allowUncaught) { | |
| throw err; | |
| } | |
| 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) { | |
| let 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 createTimeoutError(msg, ms, this.file); | |
| }; | |
| 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', | |
| /** | |
| * Value of `state` prop when a `Runnable` has been skipped by user | |
| */ | |
| STATE_PENDING: 'pending' | |
| } | |
| ); | |
| /** | |
| * 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; | |