diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000000..c846ddf2e9 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,15 @@ +{ + "asi": true + "laxcomma": true, + "white": false, + "onevar": false, + "curly": false, + "plusplus": false, + "bitwise": false, + "eqeqeq": false, + "latedef": false, + "proto": true, + "boss": true, + "eqnull": false, + "scripturl": true +} diff --git a/Makefile b/Makefile index d9efed170d..df42eb7b91 100644 --- a/Makefile +++ b/Makefile @@ -92,7 +92,7 @@ test-bail: @./bin/mocha \ --reporter $(REPORTER) \ --bail \ - test/acceptance/misc/bail + test/acceptance/misc/bail* test-async-only: @./bin/mocha \ diff --git a/bin/_mocha b/bin/_mocha index c16ea7b0e6..28309e7879 100755 --- a/bin/_mocha +++ b/bin/_mocha @@ -70,6 +70,7 @@ program .option('-d, --debug', "enable node's debugger, synonym for node --debug") .option('-b, --bail', "bail after first test failure") .option('-A, --async-only', "force all tests to take a callback (async)") + .option('-p, --parallel', "run all root-level suites in parallel") .option('--recursive', 'include sub directories') .option('--debug-brk', "enable node's debugger breaking on the first line") .option('--globals ', 'allow the given comma-delimited global [names]', list, []) @@ -237,6 +238,10 @@ if (program.growl) mocha.growl(); if (program.asyncOnly) mocha.asyncOnly(); +// --parallel + +if (program.parallel) mocha.parallel(); + // --globals mocha.globals(globals); diff --git a/lib/mocha.js b/lib/mocha.js index cd3d303268..8f3f57a9ba 100644 --- a/lib/mocha.js +++ b/lib/mocha.js @@ -293,6 +293,18 @@ Mocha.prototype.asyncOnly = function(){ return this; }; +/** + * Makes root-level suites run in parallel + * + * @return {Mocha} + * @api public + */ + +Mocha.prototype.parallel = function(){ + this.options.parallel = true; + return this; +}; + /** * Run tests and invoke `fn()` when complete. * @@ -309,6 +321,7 @@ Mocha.prototype.run = function(fn){ var reporter = new this._reporter(runner); runner.ignoreLeaks = false !== options.ignoreLeaks; runner.asyncOnly = options.asyncOnly; + runner.suite.parallel(options.parallel); if (options.grep) runner.grep(options.grep, options.invert); if (options.globals) runner.globals(options.globals); if (options.growl) this._growl(runner, reporter); diff --git a/lib/runner.js b/lib/runner.js index 436980899e..058282a94b 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -7,6 +7,7 @@ var EventEmitter = require('events').EventEmitter , debug = require('debug')('mocha:runner') , Test = require('./test') , utils = require('./utils') + , isNode = typeof window === 'undefined' , filter = utils.filter , keys = utils.keys; @@ -23,6 +24,21 @@ var globals = [ 'Date' ]; + +var _events = [ + 'start', + 'end', + 'suite', + 'suite end', + 'pending', + 'test', + 'test end', + 'hook', + 'hook end', + 'pass', + 'fail' +]; + /** * Expose `Runner`. */ @@ -38,6 +54,7 @@ module.exports = Runner; * - `end` execution complete * - `suite` (suite) test suite execution started * - `suite end` (suite) all tests (and sub-suites) have finished + * - `pending` (test) pending test encountered * - `test` (test) test execution started * - `test end` (test) test completed * - `hook` (hook) hook execution started @@ -125,13 +142,7 @@ Runner.prototype.grepTotal = function(suite) { Runner.prototype.globalProps = function() { var props = utils.keys(global); - // non-enumerables - for (var i = 0; i < globals.length; ++i) { - if (~utils.indexOf(props, globals[i])) continue; - props.push(globals[i]); - } - - return props; + return utils.uniq(props.concat(globals)); }; /** @@ -143,11 +154,12 @@ Runner.prototype.globalProps = function() { */ Runner.prototype.globals = function(arr){ - if (0 == arguments.length) return this._globals; + if (0 === arguments.length) return this._globals; debug('globals %j', arr); utils.forEach(arr, function(arr){ this._globals.push(arr); }, this); + this._globals = utils.uniq(this._globals); return this; }; @@ -159,16 +171,17 @@ Runner.prototype.globals = function(arr){ Runner.prototype.checkGlobals = function(test){ if (this.ignoreLeaks) return; - var ok = this._globals; - var globals = this.globalProps(); - var isNode = process.kill; - var leaks; + test = test || this.test; + var ok = this._globals + , currGlobals = this.globalProps() + , isNode = process.kill + , leaks; // check length - 2 ('errno' and 'location' globals) - if (isNode && 1 == ok.length - globals.length) return - else if (2 == ok.length - globals.length) return; + if (isNode && 1 == ok.length - currGlobals.length) return; + else if (2 == ok.length - currGlobals.length) return; - leaks = filterLeaks(ok, globals); + leaks = filterLeaks(ok, currGlobals); this._globals = this._globals.concat(leaks); if (leaks.length > 1) { @@ -229,6 +242,9 @@ Runner.prototype.hook = function(name, fn){ , self = this , timer; + // if we're bailing, only run hooks if this suite caused it, otherwise, skip + if (suite.failures && suite.bail() && !suite.failSource) return fn(); + function next(i) { var hook = hooks[i]; if (!hook) return fn(); @@ -265,29 +281,18 @@ Runner.prototype.hook = function(name, 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(); - } +Runner.prototype.hooks = function(name, runners, fn){ - self.hook(name, function(err){ - if (err) { - self.suite = orig; - return fn(err); - } + function next(runner) { + if (!runner) return fn(); - next(suites.pop()); + runner.hook(name, function(err){ + if (err) return fn(err); + next(runners.pop()); }); } - next(suites.pop()); + next(runners.pop()); }; /** @@ -299,8 +304,8 @@ Runner.prototype.hooks = function(name, suites, fn){ */ Runner.prototype.hookUp = function(name, fn){ - var suites = [this.suite].concat(this.parents()).reverse(); - this.hooks(name, suites, fn); + var runners = [this].concat(this.parents()).reverse(); + this.hooks(name, runners, fn); }; /** @@ -312,8 +317,8 @@ Runner.prototype.hookUp = function(name, fn){ */ Runner.prototype.hookDown = function(name, fn){ - var suites = [this.suite].concat(this.parents()); - this.hooks(name, suites, fn); + var runners = [this].concat(this.parents()); + this.hooks(name, runners, fn); }; /** @@ -325,10 +330,10 @@ Runner.prototype.hookDown = function(name, fn){ */ Runner.prototype.parents = function(){ - var suite = this.suite - , suites = []; - while (suite = suite.parent) suites.push(suite); - return suites; + var runner = this + , runners = []; + while (runner = runner.parent) runners.push(runner); + return runners; }; /** @@ -370,7 +375,7 @@ Runner.prototype.runTests = function(suite, fn){ function next(err) { // if we bail after first err - if (self.failures && suite._bail) return fn(); + if (suite.failures && suite._bail) return fn(); // next test test = tests.shift(); @@ -426,32 +431,99 @@ Runner.prototype.runTests = function(suite, fn){ Runner.prototype.runSuite = function(suite, fn){ var total = this.grepTotal(suite) - , self = this - , i = 0; + , self = this, + runners = []; debug('run suite %s', suite.fullTitle()); if (!total) return fn(); - this.emit('suite', this.suite = suite); + this.emit('suite', suite); - function next() { - var curr = suite.suites[i++]; - if (!curr) return done(); - self.runSuite(curr, next); + function suiteItr(suite, cb) { + var runner = self.createRunner(suite); + runners.push(runner); + runner.run(function (failures) { + self.failures += failures; + cb(failures); + }); } + function done() { - self.suite = suite; + debug('suite done %s', suite.fullTitle()); + self.hook('afterAll', function(){ + debug('suite end %s', suite.fullTitle()); + if (suite.parallel()) { + self._rebroadcastAll(runners); + } self.emit('suite end', suite); fn(); }); } + function next() { + if (suite.parallel() && isNode) { // cant run in parallel in the browser + utils.eachParallel(suite.suites, suiteItr, done); + } else { + utils.eachSeries(suite.suites, suiteItr, done); + } + } + + this.on('fail', function(){ + suite.failSource = true; + suite.fail(); + }); + this.hook('beforeAll', function(){ self.runTests(suite, next); }); + +}; + +Runner.prototype.createRunner = function(suite) { + var runner = new Runner(suite); + runner.parent = this; + runner.ignoreLeaks = this.ignoreLeaks; + runner.asyncOnly = this.asyncOnly; + runner.grep(this._grep, this._invert); + runner.globals(this.globals()); + this._wrapEvents(runner); + + return runner; +}; + +/** + * Wrap a child runner's events and rebroadcast. Cache them and batch + * rebroadcast at the end if we are running in parallel. + */ +Runner.prototype._wrapEvents = function(runner) { + var self = this; + runner.eventCache = []; + _events.forEach(function (event) { + runner.on(event, function (data, err) { + if (self.suite.parallel()) { + runner.eventCache.push({event: event, data: data, err: err}); + } else { + self.emit(event, data, err); + } + }); + }); +}; + +/** + * Replay the cached events from child runners as if this runner emitted them + * @param {Array[Runner]} runners + * @api private + */ +Runner.prototype._rebroadcastAll = function (runners) { + var self = this; + runners.forEach(function (runner) { + runner.eventCache.forEach(function (cache) { + self.emit(cache.event, cache.data, cache.err); + }); + }) }; /** @@ -491,30 +563,51 @@ Runner.prototype.uncaught = function(err){ Runner.prototype.run = function(fn){ var self = this - , fn = fn || function(){}; + , domain; + fn = fn || function(){}; function uncaught(err){ self.uncaught(err); } - debug('start'); - - // callback - this.on('end', function(){ + function end() { debug('end'); - process.removeListener('uncaughtException', uncaught); + if (!domain) { + process.removeListener('uncaughtException', uncaught); + } fn(self.failures); - }); + } - // run suites - this.emit('start'); - this.runSuite(this.suite, function(){ - debug('finished running'); - self.emit('end'); - }); + function start() { + debug('start'); + + // callback + self.on('end', end); + + // run suites + if (!self.suite.parent) self.emit('start'); + self.runSuite(self.suite, function(){ + debug('finished running'); + if (!self.suite.parent) + self.emit('end'); + else { + end(); + } + }); + } + + if (this.suite.parent && this.suite.parent.parallel() && isNode) { + // if this suite is to be run in parallel, then we have to create a domain + // to sandbox errors + domain = require('domain').create(); + domain.on('error', uncaught) + domain.run(start); + } else { + // uncaught exception, works when all suites are serial + process.on('uncaughtException', uncaught); + start(); + } - // uncaught exception - process.on('uncaughtException', uncaught); return this; }; @@ -531,11 +624,11 @@ Runner.prototype.run = function(fn){ function filterLeaks(ok, globals) { return filter(globals, function(key){ var matched = filter(ok, function(ok){ - if (~ok.indexOf('*')) return 0 == key.indexOf(ok.split('*')[0]); + if (~ok.indexOf('*')) return !key.indexOf(ok.split('*')[0]); // Opera and IE expose global variables for HTML element IDs (issue #243) if (/^mocha-/.test(key)) return true; return key == ok; }); - return matched.length == 0 && (!global.navigator || 'onerror' !== key); + return !matched.length && (!global.navigator || 'onerror' !== key); }); } diff --git a/lib/suite.js b/lib/suite.js index 869bb88d52..860b52335d 100644 --- a/lib/suite.js +++ b/lib/suite.js @@ -52,6 +52,7 @@ function Suite(title, ctx) { this.suites = []; this.tests = []; this.pending = false; + this.failures = false; this._beforeEach = []; this._beforeAll = []; this._afterEach = []; @@ -120,7 +121,7 @@ Suite.prototype.slow = function(ms){ /** * Sets whether to bail after first error. * - * @parma {Boolean} bail + * @param {Boolean} bail * @return {Suite|Number} for chaining * @api private */ @@ -132,6 +133,40 @@ Suite.prototype.bail = function(bail){ return this; }; +/** + * Sets whether to run child suites in parallel + * + * @param {Boolean} parallel + * @return {Suite|Number} for chaining + * @api private + */ + +Suite.prototype.parallel = function(parallel){ + if (0 == arguments.length) return this._parallel; + debug('parallel %s', parallel); + this._parallel = parallel; + return this; +}; + +/** + * Mark this suite as having failures, as well as any child suites. + * This is bookkeeping for fine-grained bailing. + * + * @return {Suite|Number} for chaining + * @api private + */ + +Suite.prototype.fail = function(){ + if (this.bail()) { + debug('setting suite failure'); + this.failures = true; + utils.forEach(this.suites, function(suite) { + suite.fail(); + }); + } + return this; +} + /** * Run `fn(test[, done])` before running tests. * diff --git a/lib/utils.js b/lib/utils.js index 2bf0978aa2..880186a2b1 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -13,6 +13,7 @@ var fs = require('fs') */ var ignore = ['node_modules', '.git']; +var nextTick = global.setImmediate || process.nextTick; /** * Escape special characters in the given string of html. @@ -99,6 +100,21 @@ exports.filter = function(arr, fn){ return ret; }; +/** + * uniq -- takes an array of strings and removes duplicated elements + * @param {Array} arr + * @return {Array} + */ +exports.uniq = function(arr) { + var cache = {} + , ret = []; + exports.forEach(arr, function (val) { + if (!cache[val]) ret.push(val); + cache[val] = true; + }); + return ret; +}; + /** * Object.keys (<=IE8) * @@ -109,7 +125,7 @@ exports.filter = function(arr, fn){ exports.keys = Object.keys || function(obj) { var keys = [] - , has = Object.prototype.hasOwnProperty // for `window` on <=IE8 + , has = Object.prototype.hasOwnProperty; // for `window` on <=IE8 for (var key in obj) { if (has.call(obj, key)) { @@ -120,6 +136,60 @@ exports.keys = Object.keys || function(obj) { return keys; }; +/** + * Run an async function on an array of items, callback when done. + * Similar to async.eachSeries + * @param {Array} arr Array of items + * @param {Function} iterator Async Function + * @param {Function} callback + */ + +exports.eachSeries = function(arr, iterator, callback) { + var self = this + , i = 0; + + function next() { + if (i === arr.length) { + return callback(); + } + + var item = arr[i++]; + + iterator.call(self, item, function () { + nextTick(next); + }); + } + + next(); +}; + + +/** + * Run an async function on an array of items in parallel. Callback when all + * are done. Similar to async.eachParallel + * @param {Array} arr The array of items + * @param {Function} iterator Async iterator + * @param {Function} callback + */ + +exports.eachParallel = function(arr, iterator, callback) { + var self = this + , completed = 0; + + function done(err) { + completed++; + if (completed === arr.length) callback(); + } + + if (arr.length === 0) return callback(); + + //kick off the + exports.forEach(arr, function (item) { + iterator.call(self, item, done); + }); + +} + /** * Watch the given `files` for changes * and invoke `fn(file)` on modification. diff --git a/test/acceptance/context.js b/test/acceptance/context.js index e2af9d5eae..d52e72fc38 100644 --- a/test/acceptance/context.js +++ b/test/acceptance/context.js @@ -23,4 +23,4 @@ describe('Context', function(){ after(function(){ this.calls.should.eql(['before', 'before two', 'test', 'after two']); }) -}) \ No newline at end of file +}) diff --git a/test/acceptance/misc/bail.js b/test/acceptance/misc/bail.js index 25b0c10f15..7c46c7439f 100644 --- a/test/acceptance/misc/bail.js +++ b/test/acceptance/misc/bail.js @@ -10,6 +10,11 @@ describe('bail', function(){ }) describe('bail-2', function(){ + + before(function (done) { + throw new Error('this hook should not be displayed'); + }) + it('should not display this error', function(done){ throw new Error('this should not be displayed'); }) diff --git a/test/acceptance/misc/bail2.js b/test/acceptance/misc/bail2.js new file mode 100644 index 0000000000..2c9aa95629 --- /dev/null +++ b/test/acceptance/misc/bail2.js @@ -0,0 +1,100 @@ +describe("Granular bail tests: ", function () { + + var runs = 0; + + this.bail(false); // override any previous bail setting + + describe("bailing suite: ", function () { + this.bail(true); + it("should fail", function () { + throw new Error("this is ok"); + }); + + it("should not get here", function () { + throw new Error("we should have bailed"); + }); + }); + + describe("second bailing suite: ", function () { + this.bail(true); + + it("should have run no successful tests by now", function () { + runs += 1; + runs.should.eql(1); + }); + + it("should fail", function () { + throw new Error("this is ok"); + }); + + it("should not get here", function () { + throw new Error("we should have bailed"); + }); + + }); + + describe("non-bailing suite: ", function () { + var bail = this.bail(); + it("should run this test, the second successful test", function () { + runs += 1; + runs.should.eql(2); + }); + + it("this suite should not bail", function () { + bail.should.equal(false); + }); + + it("should run this and fail", function () { + throw new Error("this is ok"); + }); + + it("should run this test, the third test", function () { + runs += 1; + runs.should.eql(3); + }); + + }); + + describe("third bailing suite: ", function () { + this.bail(true); + + after(function () { + runs += 1; + runs.should.eql(6); + }); + + before(function () { + runs += 1; + }); + + it("should have run 4 successful tests by now", function () { + runs += 1; + runs.should.eql(5); + }); + + it("should fail", function () { + throw new Error("this is ok"); + }); + + it("should not get here", function () { + throw new Error("we should have bailed"); + }); + + describe("child suite: ", function () { + + after(function () { + throw new Error("we should have bailed"); + }); + + before(function () { + throw new Error("we should have bailed"); + }); + + it("should fail", function () { + throw new Error("we should have bailed"); + }); + }); + + }); + +}); diff --git a/test/acceptance/misc/parallelFail.js b/test/acceptance/misc/parallelFail.js new file mode 100644 index 0000000000..b7ec743d71 --- /dev/null +++ b/test/acceptance/misc/parallelFail.js @@ -0,0 +1,69 @@ +describe('parallel failing', function () { + + this.parallel(true); + var runs = 0; + + after(function (done) { + runs.should.eql(4); + done(); + }) + + describe('bailing suite 1', function () { + + this.bail(true); + + it('test 1-1', function (done) { + runs++; + setTimeout(done, 80); + }) + + it('should fail', function (done) { + throw new Error('this is ok (1-2)'); + }); + + it('should not run this', function (done) { + throw new Error('we should have bailed'); + }); + }) + + describe('suite 2', function () { + it('test 2-1', function (done) { + runs++; + setTimeout(done, 10); + }) + + it('should fail', function (done) { + setTimeout(function () { + throw new Error('this is ok (2-2)'); + }, 10); + }); + + it('should run this if not bailing', function () { + runs++; + }) + }) + + describe('suite 3', function () { + it('test 3-1', function (done) { + setTimeout(done, 80); + runs++; + }) + + it('pending test'); + + it('should fail by passing Error to done()', function (done) { + setTimeout(function () { + done(new Error('this is ok (3-2)')); + }, 10) + }); + }) +}) + + +describe('parallel no children', function () { + + this.parallel(true); + + it("should report after this", function () {}); + +}); diff --git a/test/acceptance/parallel.js b/test/acceptance/parallel.js new file mode 100644 index 0000000000..8e06c312b3 --- /dev/null +++ b/test/acceptance/parallel.js @@ -0,0 +1,50 @@ +describe('parallel suite', function () { + var start; + + this.parallel(true); + + before(function (done) { + start = new Date(); + done(); + }); + + after(function (done) { + var end = new Date(); + (end.getTime() - start.getTime()).should.be.below(200); + done(); + }) + + describe('parallel suite 1', function () { + it('test 1-1', function (done) { + setTimeout(done, 0); + }); + + it('async test 1-2', function (done) { + setTimeout(done, 100); + }); + + it('pending test'); + + after(function (done) { + var end = new Date(); + (end.getTime() - start.getTime()).should.be.below(115); + done(); + }) + }) + + describe('parallel suite 2', function () { + it('test 2-1', function (done) { + setTimeout(done, 60); + }) + + it('test 2-2', function (done) { + setTimeout(done, 60); + }) + + after(function (done) { + var end = new Date(); + (end.getTime() - start.getTime()).should.be.below(135); + done(); + }) + }); +})