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