You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
432 lines
8.3 KiB
432 lines
8.3 KiB
|
|
/** |
|
* 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; |
|
};
|
|
|