-
-
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
Ensure all after hooks are executed, even if one of them fail #3281
Conversation
@johanblumenberg Thanks. Can you please sign the CLA? |
Done |
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.
Hi, thanks. Mostly I have some questions. Was hoping you could take a look.
@@ -318,14 +318,15 @@ Runnable.prototype.run = function (fn) { | |||
self.clearTimeout(); | |||
self.duration = new Date() - start; | |||
finished = true; | |||
delete self.callback; |
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.
what's this for?
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 is to ensure the done()
function is only invoked once for a test
this.hookUp('afterEach', this.next); | ||
return; | ||
} | ||
if (runnable.isPassed() || !runnable.callback) { |
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 unclear what we're doing with the callback
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.
The callback is what is to be executed if the test fails. It's the done()
function of the current test.
If there is a current test, then that test should be cancelled and all after hooks should be run on any uncaught exceptions.
If there is no current test, then uncaught exceptions simply mark the current suite as failed.
if (name === 'beforeAll' || name === 'beforeEach') { | ||
// stop executing hooks, notify callee of hook err | ||
return fn(err); | ||
} else { |
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 else
is not necessary
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.
ok, it can be removed
@@ -16,7 +16,7 @@ describe('uncaught exceptions', function () { | |||
assert.equal(res.stats.failures, 1); | |||
|
|||
assert.equal(res.failures[0].fullTitle, | |||
'uncaught "before each" hook'); | |||
'uncaught "before each" hook for "test"'); |
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.
where is this coming from?
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.
It's just a bonus improvement, that came for free when improving the handling of uncaught exceptions, since it now invokes the current test callback.
It's better to have the error message contain which test that failed.
} | ||
} | ||
self.emit('hook end', hook); | ||
delete hook.ctx.currentTest; | ||
next(++i); | ||
next(++i, prevErr); |
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.
so I understand: we're trapping the first error we find, then passing it along to the rest of the after
and afterEach
hooks? then, if there's no hooks remaining, we finally bail?
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.
yes
The prevErr is just to keep track of the last error that happened. The first after hook might throw an error, and the next ones succeed. Then the error has to be remembered so it can be used when calling the callback fn.
var alreadyPassed = runnable.isPassed(); | ||
// this will change the state to "failed" regardless of the current value | ||
this.fail(runnable, err); | ||
if (!alreadyPassed) { |
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.
why was all of this removed?
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.
All this just another implementation of what is already in the tests done()
function. So it's duplicate code.
It's trying to figure out which hook is being executed, and take certain actions depending on that. All that is already done, in a better way, in the tests done()
function.
FWIW: I'm unsure if we want to merge this. To some of your points:
The user can log via it('fails twice', function (done) {
process.nextTick(() => {
done(new Error('once'));
process.nextTick(() => {
throw new Error('twice');
});
});
}); See #3223 for further information.
So, to confirm, you're saying: describe('parent', function () {
afterEach(function () {
// this gets run even after 'accck' is thrown?
});
describe('child', function () {
afterEach(function () {
throw new Error('accck!');
});
it('does stuff', function () {
});
});
}); |
4547268
to
7613521
Compare
This would seem to change the symmetry expectations of Mocha hooks. You only want to do this for Seems like you could just wrap the portions of your own code that might throw in a try/catch, and this problem goes away without changes to Mocha. const _ = require('lodash');
const errors = [];
function justKeepSwimming(fn) {
let maybeError = _.attempt(fn);
if (_.isError(maybeError)) {
errors.push(maybeError);
}
}
function logErrorsAndReset() {
if (errors.length > 0) {
errors.forEach((err) => console.error(err));
errors.length = 0;
}
}
describe('suite A', function () {
afterEach(function () {
justKeepSwimming(cleanupItemX);
justKeepSwimming(cleanupItemY);
});
afterEach(function () {
logErrorsAndReset();
});
// tests...
}); |
I'm calling this inactive. please rebase/reopen if interested |
yes
In the example you provided, the test is completed when the |
Yes It cannot be done for the before hooks, for the same reason that tests cannot be run if a before hook fails. There is no point in continuing execution after an exception in the before hooks. But you need to make sure that you do cleanup of whatever you were setting up.
Problems with this code:
|
I would like this ticket to be reopened, however I cannot find the button to do so |
I'm not sure why this has to be a major semver update. The only change is that all after hooks are run on test failures. |
Reopen button is also disabled for me. You should open it as new pull request if you want. |
in that case, it doesn't necessarily need to be a semver-major, but I prefer to err on the side of caution with anything that fundamentally changes test execution. |
function logErrorsAndFail() {
if (errors.length > 0) {
var err;
if (errors.length === 1) {
err = errors.shift();
} else {
errors.forEach((err) => console.error(err));
err = new Error('multiple errors occurred during teardown... see console');
}
this.test.error(err);
}
} |
@plroebuck I don't see how asynchronous errors would be handled in the code you provided. You would need to provide a This would be possible, of course, but it is a lot of code to write. It also puts the responsibility of making this work properly on the test writer, not on the test framework. Also, all this code already exists in mocha. Why not leverage the existing code? Why require the user of the library to write the same code? Write it once, and it will work for everyone. |
@plroebuck I don't see how test reporters would be aware of the exceptions thrown. All they would receive would be the exception |
There is also the inconsistency that if I put after hooks in different describe contexts, they are always executed, as shown in the second example here: #3281 (comment) |
Description of the Change
If you have tests that require some cleanup, for example to close open files, network connections, processes, or other things, you usually have an
after()
orafterEach()
hook to do that.Now, if there is an exception from the first cleanup method, the second one will not be run.
This PR enables you to write cleanup code like this:
This will make sure that both hooks are always executed, and errors are reported properly if any of them fail.
Before this PR, the second after each hook was not executed if the first one throws an exception. But the fact that hooks in the parent suite are executed leads me to believe that this was simply overlooked, maybe because several after hooks in the same suite is not very common.
Alternate Designs
It would be possible to fix this by adding a bunch of try/finally blocks. However, there are some problems with that.
Why should this be in core?
Because there is an inconsistency in how after hooks work. Hooks in the same suite are not run if the first one fails, but hooks in parent suites are run. To be consistent, it should work the same for all hooks.
Benefits
Consistency in how hooks work, and possibility to write less try/finally clauses in the after hooks.
Possible Drawbacks
Cannot see any drawbacks
Applicable issues
I say this is a patch release, because it only affects what is executed when something fails. Also you should not be surprised that your after hook is executed when something fails, because that is the purpose of having it.