/** * 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.state = 'failed'; 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; 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.state = 'passed'; 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 ('failed' == runnable.state) 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; };