433 lines
8.3 KiB
JavaScript
433 lines
8.3 KiB
JavaScript
|
|
/**
|
|
* Module dependencies.
|
|
*/
|
|
|
|
var EventEmitter = require('events').EventEmitter
|
|
, debug = require('debug')('runner')
|
|
, Test = require('./test')
|
|
, utils = require('./utils')
|
|
, noop = function(){};
|
|
|
|
/**
|
|
* Expose `Runner`.
|
|
*/
|
|
|
|
module.exports = Runner;
|
|
|
|
/**
|
|
* Initialize a `Runner` for the given `suite`.
|
|
*
|
|
* Events:
|
|
*
|
|
* - `start` execution started
|
|
* - `end` execution complete
|
|
* - `suite` (suite) test suite execution started
|
|
* - `suite end` (suite) all tests (and sub-suites) have finished
|
|
* - `test` (test) test execution started
|
|
* - `test end` (test) test completed
|
|
* - `hook` (hook) hook execution started
|
|
* - `hook end` (hook) hook complete
|
|
* - `pass` (test) test passed
|
|
* - `fail` (test, err) test failed
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
function Runner(suite) {
|
|
var self = this;
|
|
this._globals = [];
|
|
this.suite = suite;
|
|
this.total = suite.total();
|
|
this.failures = 0;
|
|
this.on('test end', function(test){ self.checkGlobals(test); });
|
|
this.on('hook end', function(hook){ self.checkGlobals(hook); });
|
|
this.grep(/.*/);
|
|
this.globals(utils.keys(global).concat(['errno']));
|
|
}
|
|
|
|
/**
|
|
* Inherit from `EventEmitter.prototype`.
|
|
*/
|
|
|
|
Runner.prototype.__proto__ = EventEmitter.prototype;
|
|
|
|
/**
|
|
* Run tests with full titles matching `re`.
|
|
*
|
|
* @param {RegExp} re
|
|
* @return {Runner} for chaining
|
|
* @api public
|
|
*/
|
|
|
|
Runner.prototype.grep = function(re){
|
|
debug('grep %s', re);
|
|
this._grep = re;
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Allow the given `arr` of globals.
|
|
*
|
|
* @param {Array} arr
|
|
* @return {Runner} for chaining
|
|
* @api public
|
|
*/
|
|
|
|
Runner.prototype.globals = function(arr){
|
|
if (0 == arguments.length) return this._globals;
|
|
debug('globals %j', arr);
|
|
utils.forEach(arr, function(arr){
|
|
this._globals.push(arr);
|
|
}, this);
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Check for global variable leaks.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Runner.prototype.checkGlobals = function(test){
|
|
if (this.ignoreLeaks) return;
|
|
|
|
var leaks = utils.filter(utils.keys(global), function(key){
|
|
return !~utils.indexOf(this._globals, key) && (!global.navigator || 'onerror' !== key);
|
|
}, this);
|
|
|
|
this._globals = this._globals.concat(leaks);
|
|
|
|
if (leaks.length > 1) {
|
|
this.fail(test, new Error('global leaks detected: ' + leaks.join(', ') + ''));
|
|
} else if (leaks.length) {
|
|
this.fail(test, new Error('global leak detected: ' + leaks[0]));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Fail the given `test`.
|
|
*
|
|
* @param {Test} test
|
|
* @param {Error} err
|
|
* @api private
|
|
*/
|
|
|
|
Runner.prototype.fail = function(test, err){
|
|
++this.failures;
|
|
test.failed = true;
|
|
this.emit('fail', test, err);
|
|
};
|
|
|
|
/**
|
|
* Fail the given `hook` with `err`.
|
|
*
|
|
* Hook failures (currently) hard-end due
|
|
* to that fact that a failing hook will
|
|
* surely cause subsequent tests to fail,
|
|
* causing jumbled reporting.
|
|
*
|
|
* @param {Hook} hook
|
|
* @param {Error} err
|
|
* @api private
|
|
*/
|
|
|
|
Runner.prototype.failHook = function(hook, err){
|
|
this.fail(hook, err);
|
|
this.emit('end');
|
|
};
|
|
|
|
/**
|
|
* Run hook `name` callbacks and then invoke `fn()`.
|
|
*
|
|
* @param {String} name
|
|
* @param {Function} function
|
|
* @api private
|
|
*/
|
|
|
|
Runner.prototype.hook = function(name, fn){
|
|
var suite = this.suite
|
|
, hooks = suite['_' + name]
|
|
, ms = suite._timeout
|
|
, self = this
|
|
, timer;
|
|
|
|
function next(i) {
|
|
var hook = hooks[i];
|
|
if (!hook) return fn();
|
|
self.currentRunnable = hook;
|
|
hook.context = self.test;
|
|
|
|
self.emit('hook', hook);
|
|
|
|
hook.on('error', function(err){
|
|
self.failHook(hook, err);
|
|
});
|
|
|
|
hook.run(function(err){
|
|
hook.removeAllListeners('error');
|
|
if (err) return self.failHook(hook, err);
|
|
self.emit('hook end', hook);
|
|
next(++i);
|
|
});
|
|
}
|
|
|
|
process.nextTick(function(){
|
|
next(0);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Run hook `name` for the given array of `suites`
|
|
* in order, and callback `fn(err)`.
|
|
*
|
|
* @param {String} name
|
|
* @param {Array} suites
|
|
* @param {Function} fn
|
|
* @api private
|
|
*/
|
|
|
|
Runner.prototype.hooks = function(name, suites, fn){
|
|
var self = this
|
|
, orig = this.suite;
|
|
|
|
function next(suite) {
|
|
self.suite = suite;
|
|
|
|
if (!suite) {
|
|
self.suite = orig;
|
|
return fn();
|
|
}
|
|
|
|
self.hook(name, function(err){
|
|
if (err) {
|
|
self.suite = orig;
|
|
return fn(err);
|
|
}
|
|
|
|
next(suites.pop());
|
|
});
|
|
}
|
|
|
|
next(suites.pop());
|
|
};
|
|
|
|
/**
|
|
* Run hooks from the top level down.
|
|
*
|
|
* @param {String} name
|
|
* @param {Function} fn
|
|
* @api private
|
|
*/
|
|
|
|
Runner.prototype.hookUp = function(name, fn){
|
|
var suites = [this.suite].concat(this.parents()).reverse();
|
|
this.hooks(name, suites, fn);
|
|
};
|
|
|
|
/**
|
|
* Run hooks from the bottom up.
|
|
*
|
|
* @param {String} name
|
|
* @param {Function} fn
|
|
* @api private
|
|
*/
|
|
|
|
Runner.prototype.hookDown = function(name, fn){
|
|
var suites = [this.suite].concat(this.parents());
|
|
this.hooks(name, suites, fn);
|
|
};
|
|
|
|
/**
|
|
* Return an array of parent Suites from
|
|
* closest to furthest.
|
|
*
|
|
* @return {Array}
|
|
* @api private
|
|
*/
|
|
|
|
Runner.prototype.parents = function(){
|
|
var suite = this.suite
|
|
, suites = [];
|
|
while (suite = suite.parent) suites.push(suite);
|
|
return suites;
|
|
};
|
|
|
|
/**
|
|
* Run the current test and callback `fn(err)`.
|
|
*
|
|
* @param {Function} fn
|
|
* @api private
|
|
*/
|
|
|
|
Runner.prototype.runTest = function(fn){
|
|
var test = this.test
|
|
, self = this;
|
|
|
|
try {
|
|
test.on('error', function(err){
|
|
self.fail(test, err);
|
|
});
|
|
test.run(fn);
|
|
} catch (err) {
|
|
fn(err);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Run tests in the given `suite` and invoke
|
|
* the callback `fn()` when complete.
|
|
*
|
|
* @param {Suite} suite
|
|
* @param {Function} fn
|
|
* @api private
|
|
*/
|
|
|
|
Runner.prototype.runTests = function(suite, fn){
|
|
var self = this
|
|
, tests = suite.tests
|
|
, test;
|
|
|
|
function next(err) {
|
|
// if we bail after first err
|
|
if (self.failures && suite._bail) return fn();
|
|
|
|
// next test
|
|
test = tests.shift();
|
|
|
|
// all done
|
|
if (!test) return fn();
|
|
|
|
// grep
|
|
if (!self._grep.test(test.fullTitle())) return next();
|
|
|
|
// pending
|
|
if (test.pending) {
|
|
self.emit('pending', test);
|
|
self.emit('test end', test);
|
|
return next();
|
|
}
|
|
|
|
// execute test and hook(s)
|
|
self.emit('test', self.test = test);
|
|
self.hookDown('beforeEach', function(){
|
|
self.currentRunnable = self.test;
|
|
self.runTest(function(err){
|
|
test = self.test;
|
|
|
|
if (err) {
|
|
self.fail(test, err);
|
|
self.emit('test end', test);
|
|
return self.hookUp('afterEach', next);
|
|
}
|
|
|
|
test.passed = true;
|
|
self.emit('pass', test);
|
|
self.emit('test end', test);
|
|
self.hookUp('afterEach', next);
|
|
});
|
|
});
|
|
}
|
|
|
|
this.next = next;
|
|
next();
|
|
};
|
|
|
|
/**
|
|
* Run the given `suite` and invoke the
|
|
* callback `fn()` when complete.
|
|
*
|
|
* @param {Suite} suite
|
|
* @param {Function} fn
|
|
* @api private
|
|
*/
|
|
|
|
Runner.prototype.runSuite = function(suite, fn){
|
|
var self = this
|
|
, i = 0;
|
|
|
|
debug('run suite %s', suite.fullTitle());
|
|
this.emit('suite', this.suite = suite);
|
|
|
|
function next() {
|
|
var curr = suite.suites[i++];
|
|
if (!curr) return done();
|
|
self.runSuite(curr, next);
|
|
}
|
|
|
|
function done() {
|
|
self.suite = suite;
|
|
self.hook('afterAll', function(){
|
|
self.emit('suite end', suite);
|
|
fn();
|
|
});
|
|
}
|
|
|
|
this.hook('beforeAll', function(){
|
|
self.runTests(suite, next);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Handle uncaught exceptions.
|
|
*
|
|
* @param {Error} err
|
|
* @api private
|
|
*/
|
|
|
|
Runner.prototype.uncaught = function(err){
|
|
debug('uncaught exception');
|
|
var runnable = this.currentRunnable;
|
|
if (runnable.failed) return;
|
|
runnable.clearTimeout();
|
|
err.uncaught = true;
|
|
this.fail(runnable, err);
|
|
|
|
// recover from test
|
|
if ('test' == runnable.type) {
|
|
this.emit('test end', runnable);
|
|
this.hookUp('afterEach', this.next);
|
|
return;
|
|
}
|
|
|
|
// bail on hooks
|
|
this.emit('end');
|
|
};
|
|
|
|
/**
|
|
* Run the root suite and invoke `fn(failures)`
|
|
* on completion.
|
|
*
|
|
* @param {Function} fn
|
|
* @return {Runner} for chaining
|
|
* @api public
|
|
*/
|
|
|
|
Runner.prototype.run = function(fn){
|
|
var self = this
|
|
, fn = fn || function(){};
|
|
|
|
debug('start');
|
|
|
|
// callback
|
|
this.on('end', function(){
|
|
debug('end');
|
|
process.removeListener('uncaughtException', this.uncaught);
|
|
fn(self.failures);
|
|
});
|
|
|
|
// run suites
|
|
this.emit('start');
|
|
this.runSuite(this.suite, function(){
|
|
debug('finished running');
|
|
self.emit('end');
|
|
});
|
|
|
|
// uncaught exception
|
|
process.on('uncaughtException', function(err){
|
|
self.uncaught(err);
|
|
});
|
|
|
|
return this;
|
|
};
|