364 lines
7.4 KiB
JavaScript
364 lines
7.4 KiB
JavaScript
|
/**
|
||
|
* Module dependencies.
|
||
|
*/
|
||
|
|
||
|
var EventEmitter = require('events').EventEmitter;
|
||
|
var Pending = require('./pending');
|
||
|
var debug = require('debug')('mocha:runnable');
|
||
|
var milliseconds = require('./ms');
|
||
|
var utils = require('./utils');
|
||
|
var inherits = utils.inherits;
|
||
|
|
||
|
/**
|
||
|
* Save timer references to avoid Sinon interfering (see GH-237).
|
||
|
*/
|
||
|
|
||
|
/* eslint-disable no-unused-vars, no-native-reassign */
|
||
|
var Date = global.Date;
|
||
|
var setTimeout = global.setTimeout;
|
||
|
var setInterval = global.setInterval;
|
||
|
var clearTimeout = global.clearTimeout;
|
||
|
var clearInterval = global.clearInterval;
|
||
|
/* eslint-enable no-unused-vars, no-native-reassign */
|
||
|
|
||
|
/**
|
||
|
* 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
|
||
|
* @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._trace = new Error('done() called multiple times');
|
||
|
this._retries = -1;
|
||
|
this._currentRetry = 0;
|
||
|
this.pending = false;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Inherit from `EventEmitter.prototype`.
|
||
|
*/
|
||
|
inherits(Runnable, EventEmitter);
|
||
|
|
||
|
/**
|
||
|
* Set & get timeout `ms`.
|
||
|
*
|
||
|
* @api private
|
||
|
* @param {number|string} ms
|
||
|
* @return {Runnable|number} ms or Runnable instance.
|
||
|
*/
|
||
|
Runnable.prototype.timeout = function(ms) {
|
||
|
if (!arguments.length) {
|
||
|
return this._timeout;
|
||
|
}
|
||
|
if (ms === 0) {
|
||
|
this._enableTimeouts = false;
|
||
|
}
|
||
|
if (typeof ms === 'string') {
|
||
|
ms = milliseconds(ms);
|
||
|
}
|
||
|
debug('timeout %d', ms);
|
||
|
this._timeout = ms;
|
||
|
if (this.timer) {
|
||
|
this.resetTimeout();
|
||
|
}
|
||
|
return this;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Set & get slow `ms`.
|
||
|
*
|
||
|
* @api private
|
||
|
* @param {number|string} ms
|
||
|
* @return {Runnable|number} ms or Runnable instance.
|
||
|
*/
|
||
|
Runnable.prototype.slow = function(ms) {
|
||
|
if (!arguments.length) {
|
||
|
return this._slow;
|
||
|
}
|
||
|
if (typeof ms === 'string') {
|
||
|
ms = milliseconds(ms);
|
||
|
}
|
||
|
debug('timeout %d', ms);
|
||
|
this._slow = ms;
|
||
|
return this;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Set and get whether timeout is `enabled`.
|
||
|
*
|
||
|
* @api 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.
|
||
|
*
|
||
|
* @api public
|
||
|
*/
|
||
|
Runnable.prototype.skip = function() {
|
||
|
throw new Pending();
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Check if this runnable or its parent suite is marked as pending.
|
||
|
*
|
||
|
* @api private
|
||
|
*/
|
||
|
Runnable.prototype.isPending = function() {
|
||
|
return this.pending || (this.parent && this.parent.isPending());
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Set number of retries.
|
||
|
*
|
||
|
* @api private
|
||
|
*/
|
||
|
Runnable.prototype.retries = function(n) {
|
||
|
if (!arguments.length) {
|
||
|
return this._retries;
|
||
|
}
|
||
|
this._retries = n;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Get current retry
|
||
|
*
|
||
|
* @api 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.
|
||
|
*
|
||
|
* @api public
|
||
|
* @return {string}
|
||
|
*/
|
||
|
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.
|
||
|
*
|
||
|
* @api 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.
|
||
|
*
|
||
|
* @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 a list of globals for this test run.
|
||
|
*
|
||
|
* @api 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
|
||
|
* @api 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;
|
||
|
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);
|
||
|
}
|
||
|
|
||
|
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();
|
||
|
|
||
|
if (this.allowUncaught) {
|
||
|
return callFnAsync(this.fn);
|
||
|
}
|
||
|
try {
|
||
|
callFnAsync(this.fn);
|
||
|
} catch (err) {
|
||
|
done(utils.getError(err));
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (this.allowUncaught) {
|
||
|
callFn(this.fn);
|
||
|
done();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// sync or promise-returning
|
||
|
try {
|
||
|
if (this.isPending()) {
|
||
|
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();
|
||
|
// 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) {
|
||
|
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));
|
||
|
}
|
||
|
done();
|
||
|
});
|
||
|
}
|
||
|
};
|