-
Notifications
You must be signed in to change notification settings - Fork 783
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
Core: hooks invoked after a test is considered torn down can cause error #1377
Comments
The description here was a bit confusing to me, so I looked into this some more and I'm not sure the reproduction is a minimal repro. I believe the error which needs to be fixed here is the If that is the case, it appears the error occurs anytime an assertion happens after an error (or rejected promise) has occurred within a test. You can find a smaller reproduction here which is the following: QUnit.test( "assertion after errored test", function( assert ) {
setTimeout(() => assert.ok(true), 10);
return Promise.reject();
}); For some reason, when this happens QUnit is trying to invoke the test's module hooks (e.g., |
This is absolutely correct. If you look at the codepen and open the console, you can see that this error is ultimately thrown, albeit later in the execution (when the module's hooks are being run). That minimal repro looks better :) |
Also, I had a tricky time trying to devise a reasonable test to guard against this regressing. I'm open to suggestions on how to make this better. |
I've been looking into this a little and dumping here what I've got so far. Test fails normally at firstThe test case from Trent still fails more or less the same way today with qunit 2.14.1. The Test and module teardownAfter this, because it is the last test in the module, the module teardown happens as well. This clears out Report "global failure"Then, when the setTimeout is scheduled, the Now, the way If the test suite in question doesn't have any global tests, and uses a scoped module, then the "module teardown" that happened previously will have been the one specific to that scoped module. Thus we'd now be implicitly idling in the global module, and adding a test there is fine at this point since that module never existed/began until now, so it'll be begin, run this new test, and then teardown just fine. But.. if the test suite did have some global tests earlier, or if the failing test itself is the last one in the global module, then the global module too will have closed by now. In which case Re-opening a closed test suiteIn earlier versions of QUnit, it was (unofficially) allowed on in our HTML runner to keep running tests from the console and we'd just keep appending them to the screen as if nothing is wrong. Internally this wasn't great, because it meant Over time we've tried to restrict this since it's unprectable whether these end up reported or not in a CI context. You should only be able to end a run once, right? Good news – I'm not 100% sure sure, but it seems unlikely that this could cause a false positive. The kinds of situations that result in these errors seem like things we can and do already detect. It's just that if two of these things happen, we tend to want to move on and not wait for any other possible failures, so any second problem might end up not reported back to CI if it happens after the test suite has already finished. Bad news - We should not internally crash under these circumstances. And in addition to it being confusing when running QUnit in your browser directly, it can also at times show up in CI if it happens quick enough before the browser is shutdown. I think perhaps the right thing to do here is to have Some ideas
|
Nice short repro in #1592 by @smcclure15:
|
As part of fixing this, I'll have to convert the |
The next commit in this branch for qunitjs#1511, will disallow adding tests if `QUnit.done()` and `runEnd` have already happened, thus leading these hacks to fail as follows: ```` Running "qunit:all" (qunit) task Testing http://localhost:4000/test/index.html […] Testing http://localhost:4000/test/module-skip.html .... Error: Unexpected new test after the run already ended at new Test (http://localhost:4000/qunit/qunit.js:2206:13) ^C ``` In addition, due to a known issue in grunt-contrib-qunit, these would also indefinitely hack instead of actually failing. Ref gruntjs/grunt-contrib-qunit#178. Ref qunitjs#1377. Ref qunitjs#1511.
== Background == Previously, QUnit.onError and QUnit.onUnhandledRejection could report global errors by synthesizing a new test, even after a run has ended. This is problematic when an errors ocurrs after all modules (and their hooks) have finished, and the overall test run has ended. The most immediate problem is that hooks having finished already, means it is illegal for a new test to start since "after" has already run. To protect against such illegal calls, the hooks object is emptied internally, and this new test causes an internal error: ``` TypeError: Cannot read property 'length' of undefined ``` This is not underlying problem though, but rather our internal safeguard working as intended. The higher-level problem is that there is no appropiate way to report a late error as a test since the run has already ended. The `QUnit.done()` callbacks have run, and the `runEnd` event has been emitted. == Approach == Instead of trying to report (late) errors as a test, only print them to `console.warn()`, which goes to stderr in Node.js. For the CLI, also remember that uncaught errors were found and use that to make sure we don't change exitCode back to zero (e.g. in case we have an uncaught error after the last test but before our `runEnd` callback is called). == Changes == * Generalise `QUnit.onUnhandledRejection` and re-use it for `window.onerror` (browser), and uncaught exceptions (CLI). * Fix broken use of `QUnit.onError` in `process.on( "uncaughtException" )`. This was passing the wrong parameters. Use the new onUncaughtException method instead. * Clarify that `QUnit.onError` is only for `window.onerror`. For now, keep its strange non-standard signature as-is (with the custom object parameter), but document this and its return value. * Remove the unused "..args" from `QUnit.onError`. This was only ever passed from one of our unit tests to give one extra argument (a string of "actual"), which then ended up passed as "actual" parameter to `pushFailure()`. We never used this in the actual onError binding, so remove this odd variadic construct for now. * Change `ProcessingQueue#done`, which is in charge of reporting the "No tests were run" error, to no longer rely on the way that `QUnit.onError` previously queued a late test. The first part of this function may run twice (same as before, once after an empty test run, and one more time after the synthetic test has finished and the queue is empty again). Change this so that we no longer assign `finished = true` in that first part. This means we will still support queueing of this one late test. But, since the quueue is empty, we do need to call `advance()` manually as otherwise it'd never get processed. Previously, `finished = true` was assigned first, which meant that `QUnit.onError` was adding a test under that condition. But this worked anyway because `Test#queue` internally had manual advancing exactly for this use case, which is also where we now emit a deprecation warning (to become an error in QUnit 3). Note that using this for anything other than the "No tests run" error was already unreliable since generally runEnd would have been emitted already. The "No tests run" test was exactly done from the one sweet spot where it was (and remains) safe because that threw an error and thus prevented runEnd from being emitted. Fixes qunitjs#1377. Ref qunitjs#1322. Ref qunitjs#1446.
== Background == Previously, QUnit.onError and QUnit.onUnhandledRejection could report global errors by synthesizing a new test, even after a run has ended. This is problematic when an errors ocurrs after all modules (and their hooks) have finished, and the overall test run has ended. The most immediate problem is that hooks having finished already, means it is illegal for a new test to start since "after" has already run. To protect against such illegal calls, the hooks object is emptied internally, and this new test causes an internal error: ``` TypeError: Cannot read property 'length' of undefined ``` This is not underlying problem though, but rather our internal safeguard working as intended. The higher-level problem is that there is no appropiate way to report a late error as a test since the run has already ended. The `QUnit.done()` callbacks have run, and the `runEnd` event has been emitted. == Approach == Instead of trying to report (late) errors as a test, only print them to `console.warn()`, which goes to stderr in Node.js. For the CLI, also remember that uncaught errors were found and use that to make sure we don't change exitCode back to zero (e.g. in case we have an uncaught error after the last test but before our `runEnd` callback is called). == Changes == * Generalise `QUnit.onUnhandledRejection` and re-use it for `window.onerror` (browser), and uncaught exceptions (CLI). * Fix broken use of `QUnit.onError` in `process.on( "uncaughtException" )`. This was passing the wrong parameters. Use the new onUncaughtException method instead. * Clarify that `QUnit.onError` is only for `window.onerror`. For now, keep its strange non-standard signature as-is (with the custom object parameter), but document this and its return value. * Remove the unused "..args" from `QUnit.onError`. This was only ever passed from one of our unit tests to give one extra argument (a string of "actual"), which then ended up passed as "actual" parameter to `pushFailure()`. We never used this in the actual onError binding, so remove this odd variadic construct for now. * Change `ProcessingQueue#done`, which is in charge of reporting the "No tests were run" error, to no longer rely on the way that `QUnit.onError` previously queued a late test. The first part of this function may run twice (same as before, once after an empty test run, and one more time after the synthetic test has finished and the queue is empty again). Change this so that we no longer assign `finished = true` in that first part. This means we will still support queueing of this one late test. But, since the quueue is empty, we do need to call `advance()` manually as otherwise it'd never get processed. Previously, `finished = true` was assigned first, which meant that `QUnit.onError` was adding a test under that condition. But this worked anyway because `Test#queue` internally had manual advancing exactly for this use case, which is also where we now emit a deprecation warning (to become an error in QUnit 3). Note that using this for anything other than the "No tests run" error was already unreliable since generally runEnd would have been emitted already. The "No tests run" test was exactly done from the one sweet spot where it was (and remains) safe because that threw an error and thus prevented runEnd from being emitted. Fixes qunitjs#1377. Ref qunitjs#1322. Ref qunitjs#1446.
The next commit in this branch for #1511, will disallow adding tests if `QUnit.done()` and `runEnd` have already happened, thus leading these hacks to fail as follows: ```` Running "qunit:all" (qunit) task Testing http://localhost:4000/test/index.html […] Testing http://localhost:4000/test/module-skip.html .... Error: Unexpected new test after the run already ended at new Test (http://localhost:4000/qunit/qunit.js:2206:13) ^C ``` In addition, due to a known issue in grunt-contrib-qunit, these would also indefinitely hack instead of actually failing. Ref gruntjs/grunt-contrib-qunit#178. Ref #1377. Ref #1511.
== Background == Previously, QUnit.onError and QUnit.onUnhandledRejection could report global errors by synthesizing a new test, even after a run has ended. This is problematic when an errors ocurrs after all modules (and their hooks) have finished, and the overall test run has ended. The most immediate problem is that hooks having finished already, means it is illegal for a new test to start since "after" has already run. To protect against such illegal calls, the hooks object is emptied internally, and this new test causes an internal error: ``` TypeError: Cannot read property 'length' of undefined ``` This is not underlying problem though, but rather our internal safeguard working as intended. The higher-level problem is that there is no appropiate way to report a late error as a test since the run has already ended. The `QUnit.done()` callbacks have run, and the `runEnd` event has been emitted. == Approach == Instead of trying to report (late) errors as a test, only print them to `console.warn()`, which goes to stderr in Node.js. For the CLI, also remember that uncaught errors were found and use that to make sure we don't change exitCode back to zero (e.g. in case we have an uncaught error after the last test but before our `runEnd` callback is called). == Changes == * Generalise `QUnit.onUnhandledRejection` and re-use it for `window.onerror` (browser), and uncaught exceptions (CLI). * Fix broken use of `QUnit.onError` in `process.on( "uncaughtException" )`. This was passing the wrong parameters. Use the new onUncaughtException method instead. * Clarify that `QUnit.onError` is only for `window.onerror`. For now, keep its strange non-standard signature as-is (with the custom object parameter), but document this and its return value. * Remove the unused "..args" from `QUnit.onError`. This was only ever passed from one of our unit tests to give one extra argument (a string of "actual"), which then ended up passed as "actual" parameter to `pushFailure()`. We never used this in the actual onError binding, so remove this odd variadic construct for now. * Change `ProcessingQueue#done`, which is in charge of reporting the "No tests were run" error, to no longer rely on the way that `QUnit.onError` previously queued a late test. The first part of this function may run twice (same as before, once after an empty test run, and one more time after the synthetic test has finished and the queue is empty again). Change this so that we no longer assign `finished = true` in that first part. This means we will still support queueing of this one late test. But, since the quueue is empty, we do need to call `advance()` manually as otherwise it'd never get processed. Previously, `finished = true` was assigned first, which meant that `QUnit.onError` was adding a test under that condition. But this worked anyway because `Test#queue` internally had manual advancing exactly for this use case, which is also where we now emit a deprecation warning (to become an error in QUnit 3). Note that using this for anything other than the "No tests run" error was already unreliable since generally runEnd would have been emitted already. The "No tests run" test was exactly done from the one sweet spot where it was (and remains) safe because that threw an error and thus prevented runEnd from being emitted. Fixes #1377. Ref #1322. Ref #1446.
Tell us about your runtime:
qunit
Node CLI /testem
What are you trying to do?
A test whose done hooks are attempted to be invoked after the test is considered torn down can produce an error when trying to access those hooks.
Code that reproduces the problem:
Will result in:
Codepen: https://codepen.io/rwjblue/pen/KJLQEJ
If you have any relevant configuration information, please include that here:
What did you expect to happen?
Trying to access hooks on an already torn down test should not result in an error.
What actually happened?
An error was thrown when trying to access hooks on a module. At this point,
hooks
has already been torn down.The text was updated successfully, but these errors were encountered: