diff --git a/lib/reporters/base.js b/lib/reporters/base.js index 172d489785..f3a4e67ca3 100644 --- a/lib/reporters/base.js +++ b/lib/reporters/base.js @@ -302,7 +302,7 @@ function Base (runner) { failures.push(test); }); - runner.on('end', function () { + runner.once('end', function () { stats.end = new Date(); stats.duration = stats.end - stats.start; }); diff --git a/lib/reporters/dot.js b/lib/reporters/dot.js index 81e106edd0..3ccbce9ad3 100644 --- a/lib/reporters/dot.js +++ b/lib/reporters/dot.js @@ -56,7 +56,7 @@ function Dot (runner) { process.stdout.write(color('fail', Base.symbols.bang)); }); - runner.on('end', function () { + runner.once('end', function () { console.log(); self.epilogue(); }); diff --git a/lib/reporters/json-stream.js b/lib/reporters/json-stream.js index c0d79cf970..af3e86d259 100644 --- a/lib/reporters/json-stream.js +++ b/lib/reporters/json-stream.js @@ -39,7 +39,7 @@ function List (runner) { console.log(JSON.stringify(['fail', test])); }); - runner.on('end', function () { + runner.once('end', function () { process.stdout.write(JSON.stringify(['end', self.stats])); }); } diff --git a/lib/reporters/json.js b/lib/reporters/json.js index 259a782121..97989ccbb3 100644 --- a/lib/reporters/json.js +++ b/lib/reporters/json.js @@ -43,7 +43,7 @@ function JSONReporter (runner) { pending.push(test); }); - runner.on('end', function () { + runner.once('end', function () { var obj = { stats: self.stats, tests: tests.map(clean), diff --git a/lib/reporters/landing.js b/lib/reporters/landing.js index b8c7b5f20c..3cc036421d 100644 --- a/lib/reporters/landing.js +++ b/lib/reporters/landing.js @@ -81,7 +81,7 @@ function Landing (runner) { stream.write('\u001b[0m'); }); - runner.on('end', function () { + runner.once('end', function () { cursor.show(); console.log(); self.epilogue(); diff --git a/lib/reporters/list.js b/lib/reporters/list.js index a562cdb94c..719ec0166c 100644 --- a/lib/reporters/list.js +++ b/lib/reporters/list.js @@ -54,7 +54,7 @@ function List (runner) { console.log(color('fail', ' %d) %s'), ++n, test.fullTitle()); }); - runner.on('end', self.epilogue.bind(self)); + runner.once('end', self.epilogue.bind(self)); } /** diff --git a/lib/reporters/markdown.js b/lib/reporters/markdown.js index 9c06616ea9..2c27e1dc25 100644 --- a/lib/reporters/markdown.js +++ b/lib/reporters/markdown.js @@ -91,7 +91,7 @@ function Markdown (runner) { buf += '```\n\n'; }); - runner.on('end', function () { + runner.once('end', function () { process.stdout.write('# TOC\n'); process.stdout.write(generateTOC(runner.suite)); process.stdout.write(buf); diff --git a/lib/reporters/min.js b/lib/reporters/min.js index 0d772f9601..2573a43450 100644 --- a/lib/reporters/min.js +++ b/lib/reporters/min.js @@ -29,7 +29,7 @@ function Min (runner) { process.stdout.write('\u001b[1;3H'); }); - runner.on('end', this.epilogue.bind(this)); + runner.once('end', this.epilogue.bind(this)); } /** diff --git a/lib/reporters/nyan.js b/lib/reporters/nyan.js index 1be49a97b9..77478ebd51 100644 --- a/lib/reporters/nyan.js +++ b/lib/reporters/nyan.js @@ -52,7 +52,7 @@ function NyanCat (runner) { self.draw(); }); - runner.on('end', function () { + runner.once('end', function () { Base.cursor.show(); for (var i = 0; i < self.numberOfLines; i++) { write('\n'); diff --git a/lib/reporters/progress.js b/lib/reporters/progress.js index 9d545534bc..27b79f99e3 100644 --- a/lib/reporters/progress.js +++ b/lib/reporters/progress.js @@ -80,7 +80,7 @@ function Progress (runner, options) { // tests are complete, output some stats // and the failures if any - runner.on('end', function () { + runner.once('end', function () { cursor.show(); console.log(); self.epilogue(); diff --git a/lib/reporters/spec.js b/lib/reporters/spec.js index 993aff8dd3..d0543a5360 100644 --- a/lib/reporters/spec.js +++ b/lib/reporters/spec.js @@ -72,7 +72,7 @@ function Spec (runner) { console.log(indent() + color('fail', ' %d) %s'), ++n, test.title); }); - runner.on('end', self.epilogue.bind(self)); + runner.once('end', self.epilogue.bind(self)); } /** diff --git a/lib/reporters/tap.js b/lib/reporters/tap.js index e37ac1b16f..4e99165f8f 100644 --- a/lib/reporters/tap.js +++ b/lib/reporters/tap.js @@ -51,7 +51,7 @@ function TAP (runner) { } }); - runner.on('end', function () { + runner.once('end', function () { console.log('# tests ' + (passes + failures)); console.log('# pass ' + passes); console.log('# fail ' + failures); diff --git a/lib/reporters/xunit.js b/lib/reporters/xunit.js index 7d98815ca2..faf96adac1 100644 --- a/lib/reporters/xunit.js +++ b/lib/reporters/xunit.js @@ -78,7 +78,7 @@ function XUnit (runner, options) { tests.push(test); }); - runner.on('end', function () { + runner.once('end', function () { self.write(tag('testsuite', { name: suiteName, tests: stats.tests, diff --git a/lib/runnable.js b/lib/runnable.js index e8ca41800f..1045b0680b 100644 --- a/lib/runnable.js +++ b/lib/runnable.js @@ -142,6 +142,24 @@ Runnable.prototype.isPending = function () { return this.pending || (this.parent && this.parent.isPending()); }; +/** + * Return `true` if this Runnable has failed. + * @return {boolean} + * @private + */ +Runnable.prototype.isFailed = function () { + return !this.isPending() && this.state === 'failed'; +}; + +/** + * Return `true` if this Runnable has passed. + * @return {boolean} + * @private + */ +Runnable.prototype.isPassed = function () { + return !this.isPending() && this.state === 'passed'; +}; + /** * Set or get number of retries. * diff --git a/lib/runner.js b/lib/runner.js index 2e74c985fe..5a7e3e03e0 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -726,21 +726,25 @@ Runner.prototype.uncaught = function (err) { runnable.clearTimeout(); - // Ignore errors if complete or pending - if (runnable.state || runnable.isPending()) { + // Ignore errors if already failed or pending + // See #3226 + if (runnable.isFailed() || runnable.isPending()) { return; } + // we cannot recover gracefully if a Runnable has already passed + // then fails asynchronously + var alreadyPassed = runnable.isPassed(); + // this will change the state to "failed" regardless of the current value this.fail(runnable, err); + if (!alreadyPassed) { + // recover from test + if (runnable.type === 'test') { + this.emit('test end', runnable); + this.hookUp('afterEach', this.next); + return; + } - // recover from test - if (runnable.type === 'test') { - this.emit('test end', runnable); - this.hookUp('afterEach', this.next); - return; - } - - // recover from hooks - if (runnable.type === 'hook') { + // recover from hooks var errSuite = this.suite; // if hook failure is in afterEach block if (runnable.fullTitle().indexOf('after each') > -1) { diff --git a/test/integration/fixtures/uncaught-fatal.fixture.js b/test/integration/fixtures/uncaught-fatal.fixture.js new file mode 100644 index 0000000000..10be283816 --- /dev/null +++ b/test/integration/fixtures/uncaught-fatal.fixture.js @@ -0,0 +1,11 @@ +'use strict'; + +it('should bail if a successful test asynchronously fails', function(done) { + done(); + process.nextTick(function () { + throw new Error('global error'); + }); +}); + +it('should not actually get run', function () { +}); diff --git a/test/integration/uncaught.spec.js b/test/integration/uncaught.spec.js index 9a3dadbbbd..db495a923a 100644 --- a/test/integration/uncaught.spec.js +++ b/test/integration/uncaught.spec.js @@ -40,4 +40,24 @@ describe('uncaught exceptions', function () { done(); }); }); + + it('handles uncaught exceptions from which Mocha cannot recover', function (done) { + run('uncaught-fatal.fixture.js', args, function (err, res) { + if (err) { + done(err); + return; + } + assert.equal(res.stats.pending, 0); + assert.equal(res.stats.passes, 1); + assert.equal(res.stats.failures, 1); + + assert.equal(res.failures[0].title, + 'should bail if a successful test asynchronously fails'); + assert.equal(res.passes[0].title, + 'should bail if a successful test asynchronously fails'); + + assert.equal(res.code, 1); + done(); + }); + }); }); diff --git a/test/unit/runnable.spec.js b/test/unit/runnable.spec.js index b7f2cecb7a..06545e10a2 100644 --- a/test/unit/runnable.spec.js +++ b/test/unit/runnable.spec.js @@ -490,4 +490,37 @@ describe('Runnable(title, fn)', function () { }); }); }); + + describe('#isFailed()', function () { + it('should return `true` if test has not failed', function () { + var test = new Runnable('foo', function () { + }); + // runner sets the state + test.run(function () { + expect(test.isFailed()).not.to.be.ok(); + }); + }); + + it('should return `true` if test has failed', function () { + var test = new Runnable('foo', function () { + }); + // runner sets the state + test.state = 'failed'; + test.run(function () { + expect(test.isFailed()).to.be.ok(); + }); + }); + + it('should return `false` if test is pending', function () { + var test = new Runnable('foo', function () { + }); + // runner sets the state + test.isPending = function () { + return true; + }; + test.run(function () { + expect(test.isFailed()).not.to.be.ok(); + }); + }); + }); });