-
-
Notifications
You must be signed in to change notification settings - Fork 3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
multiple async done() calls result in failure; closes #4151 #4152
Conversation
f5c142b
to
1a5269f
Compare
This PR is a good one, it helps me out with a problem I have in #4150. Following test case shows more reliable results, maybe you can fix the failing CI tests that way. describe('uncaught test', function() {
this.timeout(1500);
it('throw delayed error', (done) => {
setTimeout(() => {done(/*new Error()*/);}, 1000);
setTimeout(() => {done();}, 2000);
});
}); When you remove the first Btw, I have not understood yet why (most parts of) |
lib/runner.js
Outdated
@@ -834,7 +857,7 @@ Runner.prototype.uncaught = function(err) { | |||
runnable = new Runnable('Uncaught error outside test suite'); | |||
runnable.parent = this.suite; | |||
|
|||
if (this.started) { | |||
if (this.state === constants.STATE_RUNNING) { | |||
this.fail(runnable, err); | |||
} else { | |||
// Can't recover from this failure |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This part is probably wrong as well. Why don't we just abort here, or throw? When the runner has stopped, we throw anyway. When it hasn't even started, then there is nothing to recover or fail or to report, so we should just throw.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if it's wrong or not. If we know which Runnable
is throwing the uncaught error, then I think it makes sense to report it as a failure instead of just bailing. But I could be convinced otherwise. Either way, I think we can address it in another PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree, separate PR.
When a reporter errors (except |
@juergba If a reporter errors, that's an internal error, and shouldn't we just bail? Don't think that's related to this, though.
|
I'll add a case for this and see if I can get to the bottom of it and maybe make it more consistent, but no guarantees right now. when an async test times out, Mocha calls |
I know that in We should leave it as is, not aiming for perfection in dealing with uncaught exceptions. Most important is that Mocha should never leave with exitCode=0 when there was an uncaght exception thrown. |
@@ -928,7 +951,7 @@ Runner.prototype.run = function(fn) { | |||
if (rootSuite.hasOnly()) { | |||
rootSuite.filterOnly(); | |||
} | |||
self.started = true; | |||
self.state = constants.STATE_RUNNING; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is setting runner's state not too early, in case of delay
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure. will see.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we have a Runner#state
property now, we don't need to unregister Runner#uncaught
and register Runner#uncaughtEnd
when runner ends. We just need one uncaught handler, where we rethrow when runner.state === constants.STATE_STOPPED
;
Btw, I have not understood yet why (most parts of) Runner#uncaught() is registered on process.on('uncaughtException', ...) instead of runner.on('error', ...).
Do you know why?
Ooh, that's good. Yes.
If we catch an error in For example, a simple assertion error would be catchable, and could be emitted as it('should fail', function() {
throw new Error()
}); likewise it('should fail', function(done) {
setTimeout(() => {
done(new Error())
});
}); or it('should fail', async function() {
return Promise.reject()
}); as far as I recall, this very common pattern is handled by it('should fail', function(done) {
setTimeout(() => {
throw new Error();
});
}); last time I checked, Mocha doesn't even do anything with this: it('should fail', function() {
Promise.reject();
}); The only way to handle this--like the previous case--is a listener on We should probably add an |
9f0ecd5
to
e69edbe
Compare
- added a method in `errors` module to create a "multiple done" err - modernize `multiple-done.spec.js` - refactor errors into constants in `errors` module - remove `Runner#started` prop; replace with `Runner#state` prop + constants - add a catchall `createFatalError()` function to `errors` module; this is called when a test fails twice by other means (unsure what those means are yet) - force color in Travis CI b/c my eyes
move logic to `Runner#uncaught`, since we can now rely on the value of `Runner#state`. Signed-off-by: Christopher Hiller <boneskull@boneskull.com>
Signed-off-by: Christopher Hiller <boneskull@boneskull.com>
e69edbe
to
55283e3
Compare
Signed-off-by: Christopher Hiller <boneskull@boneskull.com>
@nicojs Some of this touches stuff you changed recently. I found an issue, but I think it has existed for a long time. In particular, in var self = this;
function uncaught(err) {
self.uncaught(err);
}
self._removeEventListener(process, 'uncaughtException', uncaught);
self._addEventListener(process, 'uncaughtException', uncaught); Do you see the bug? 😄 We use this
This is unlikely to be a problem during the normal course of operation, I think, which is why it wouldn't have been noticed. The fix isn't awesome. I renamed this.uncaught = this._uncaught.bind(this); Now, we change the aforementioned code to: // "self" isn't strictly necessary here
var self = this;
self._removeEventListener(process, 'uncaughtException', self.uncaught);
self._addEventListener(process, 'uncaughtException', self.uncaught); Even having access to lambda functions, we'd still need the listener to call with the correct context. So even then, the this.uncaught = err => this._uncaught(err); This could also probably be solved with a |
- added a method in `errors` module to create a "multiple done" err - modernize `multiple-done.spec.js` - refactor errors into constants in `errors` module - remove `Runner#started` prop; replace with `Runner#state` prop + constants - add a catchall `createFatalError()` function to `errors` module; this is called when a test fails twice by other means (unsure what those means are yet) - force color in Travis CI b/c my eyes - remove `Runner#uncaughtEnd`; move logic to `Runner#uncaught`, since we can now rely on the value of `Runner#state`. - upgrade `unexpected-eventemitter` - fix potential listener leak in `Runner#run`
This fixes bug #4151.
errors
module to create a "multiple done" errmultiple-done.spec.js
errors
moduleRunner#started
prop; replace withRunner#state
prop + constantscreateFatalError()
function toerrors
module; this is calledwhen a test fails twice by other means (unsure what those means are yet)
Please note that because of how the fixture for this is written (using
setTimeout
), the behavior of the associated test is non-deterministic. We could increase the delay, but we're just hacking around and will likely have to increase or decrease it again (and write another test!). One of two things will happen:Runner
completes, orRunner
completesIn the first case, it's handled by the
Runner
's uncaught exception listener, (assuming--allow-uncaught
is not set)and will exit with code 1. In the second case, it's caught by the, uh, other uncaught exception listener,uncaughtEnd()
, (@juergba will know more about this)and exits with code 7.I've tried to give the user enough context to track down the problem. The test covers whichever of these two happens to occur via a dynamic call to
skip()
.Update: Can't rely on the exit codes for anything