-
Notifications
You must be signed in to change notification settings - Fork 784
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: Fix bug calling hooks with skipped tests #1156
Conversation
Nice, sleuth work! |
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.
Thanks for digging into this! This approach works, but I'd prefer to have it such that before
runs before the first test that actually executes. In other words, if all tests in a module were skipped, then before
/after
will never run. This is more inline with how beforeEach
/afterEach
work and I think is more intuitive.
test/main/modules.js
Outdated
|
||
QUnit.test( "has run by the time second (unskipped) test runs", function( assert ) { | ||
assert.expect( 1 ); | ||
assert.equal( this.beforeCount, 1, "beforeCount should be one" ); |
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.
if we remove the this.assert = new Assert( this );
for skipped tests line will this fail?
test/main/modules.js
Outdated
QUnit.test( "after hook ran after last (skipped) test", function( assert ) { | ||
assert.expect( 1 ); | ||
assert.ok( ranAfterHook, "after hook ran" ); | ||
} ); |
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.
can we nest the after (skip)
and after (skip) verifier
modules in a single module? mostly a
QUnit.module( "after (skip)", function() {
var ranAfterHook = 0;
QUnit.module( "main", function(hooks) {
hooks.after = function( assert ) {
ranAfterHook += 1;
};
QUnit.test( "does not run after initial tests", ... );
QUnit.skip( "skipped, but should still run after hook", ... );
} );
QUnit.module( "verifier", function() {
QUnit.test( "after hook ran after last (skipped) test", ... );
} );
} );
test/main/modules.js
Outdated
} ); | ||
|
||
QUnit.skip( "skipped, but should still run after hook", function( assert ) { | ||
assert.expect( 1 ); |
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 should not be necessary. Some throwing expression here, like throw 'foo'
here would help better
test/main/modules.js
Outdated
|
||
QUnit.skip( "skipped, but should still run before hook", function( assert ) { | ||
assert.expect( 1 ); | ||
assert.ok( true ); |
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 should not be necessary. Some throwing expression here, like throw 'foo' here would help better, or even assert.ok( false );
+1 |
When the first/last test in a module was skipped, it would cause the module's before()/after() hook to not be called. This ensures those hooks will always be called.
42352e0
to
4d8bf05
Compare
@trentmwillis @leobalter how does this look? These changes didn't really have much in common with the previous version, so I squashed the commits. |
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 changes in the src code are a bit confusing, but I'm +1 for this PR after following the tests.
I found a couple nit-picks in the comments below.
src/test.js
Outdated
const modules = [ ...module.childModules ]; | ||
|
||
// Do a breadth-first traversal of the child modules | ||
while ( modules.length ) { | ||
const nextModule = modules.shift(); | ||
count += nextModule.tests.length; | ||
tests = tests.concat( nextModule[ key ] ); |
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.
tests.push.apply( tests, nextModule[ keys ] );
it avoids creating a new array and discarding the previous one.
test/main/modules.js
Outdated
|
||
QUnit.module( "verifier", function() { | ||
QUnit.test( "hooks did not run", function( assert ) { | ||
assert.expect( 2 ); |
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 not necessary here.
assert.expect( 1 ); | ||
} ); | ||
|
||
QUnit.skip( "last test in module is skipped" ); |
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 nice!
@leobalter I addresses your feedback in new commit so you can easily see the diff, but I'd like to squash it with the previous before merging, so let me know if the latest commit is 👍 by you, and if so I'll squash them and wait for @trentmwillis to provide feedback or merge. |
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.
Thanks for updating; some suggestions on the exact implementation, but the tests look good!
src/core.js
Outdated
moduleId: generateHash( moduleName ), | ||
testsRun: 0, | ||
unskippedTestsRun: 0, |
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.
testsRun
isn't used for anything other than determining whether to invoke module hooks, so we can just use that instead of introducing a new variable. Basically, we should count a test that actually ran should be an unskipped test. Make sense?
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.
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.
Ah good point, carry on.
src/test.js
Outdated
@@ -64,6 +64,10 @@ export default function Test( settings ) { | |||
this.async = false; | |||
this.expected = 0; | |||
} else { | |||
this.module.unskippedTests.push( { |
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.
We should create a single object up on line 55 and push it into both the tests
and unskippedTests
array, since we want that info to be in sync.
Also, I'd appreciate a flag skipped
to be added to the object, for more info.
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 actually considered keeping just one array of tests (module.tests
) and adding a skip
flag to its entries. That way we wouldn't need anything extra because we can always find out if the current test is the first/last unskipped test by looking through module.tests
(and child modules) and comparing testId
to this.testId
and looking at skip
flags.
I didn't go in that direction because module.tests
is passed into various callbacks as the context, and I didn't want to make any public-facing changes. If you prefer that route (then we don't need any new counts or arrays or anything, just a skip
flag on the module.tests
entries), I can certainly do that.
If not, can you elaborate on exactly where you want the skipped
flag? From your comment I'm not entirely sure.
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 think adding the flag is fine, it's an additive change so it should be considered backwards compatible.
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.
Blerg. Turns out we need to keep an unskipped count. I was hoping to just look at the list of tests and when we run the first/last unskipped test, run the before/after hook, but when looking through the module
/childModule
/tests
tree, we can't tell the difference between this:
QUnit.module("outer", function() {
test("outer test", function() { ... });
QUnit.module("inner", function() {
test("inner test", function() { ... });
});
});
and this:
QUnit.module("outer", function() {
QUnit.module("inner", function() {
test("inner test", function() { ... });
});
test("outer test", function() { ... });
});
So we don't know whether to run outer
's before
hook when we run outer
or inner
. Without some knowledge of how a module's tests are ordered with respect to its child modules, this approach will not work.
So I guess I'll fall back on still adding the skip
flag to module.tests
(so we can dynamically count the number of unskipped tests in a module and don't have to keep a separate list/count of them), but also keeping an unskippedTestsRun
on the module.
src/test.js
Outdated
const modules = [ ...module.childModules ]; | ||
|
||
// Do a breadth-first traversal of the child modules | ||
while ( modules.length ) { | ||
const nextModule = modules.shift(); | ||
count += nextModule.tests.length; | ||
tests.push.apply( tests, nextModule[ key ] ); |
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'd prefer to keep this as a counter instead of an array as we don't need the flattened array for anything other than length.
Instead of storing an array of unskipped tests, we can just set a skip flag on each test, and compute the number of unskipped tests on the fly.
@trentmwillis I've addressed your feedback, but left the commits un-squashed so it's easier to see the changes. Let me know if you'd like me to squash this PR down to a single commit (or if you just want to do it as part of the merge). |
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.
Okay, sorry for the runaround, one final set of changes and this should be good to merge. Thanks again for working on it!
src/test.js
Outdated
module.testsRun++; | ||
while ( ( module = module.parentModule ) ) { | ||
module.testsRun++; | ||
} | ||
|
||
if ( !skipped ) { |
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.
We should perform this check inside the while
loop above so that we're only iterating once.
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 cringe at checking a loop invariant inside the loop, although this time I guess I went the direction of incurring a performance cost in the common case rather that the uncommon one 🤦♂️
How about this:
let notify;
if ( skipped ) {
notify = function(module) {
module.testsRun++;
};
} else {
notify = function(module) {
module.testsRun++;
module.unskippedTestsRun++;
};
}
notify(module);
while ( ( module = module.parentModule ) ) {
notify(module);
}
Not a big deal if you don't like it and prefer just checking the invariant inside the loop -- let me know.
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'd prefer just checking the invariant. It's a fast check and less code to reason about.
test/main/modules.js
Outdated
|
||
QUnit.skip( "last test in module is skipped" ); | ||
|
||
QUnit.module( "before/after with all tests skipped (wrapper)", function() { |
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.
We can make this test much simpler:
QUnit.module( "before/after with all tests skipped", {
before: function( assert ) {
assert.ok( false, "should not occur" );
},
after: function( assert ) {
assert.ok( false, "should not occur" );
}
} );
QUnit.skip( "verifier" );
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.
👍 -- let me know your decision on the other two comments, and I'll roll all three into one commit.
test/main/modules.js
Outdated
|
||
QUnit.test( "runs before first unskipped test", function( assert ) { | ||
assert.expect( 2 ); | ||
assert.equal( this.beforeCount, 1, "beforeCount should be one" ); |
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.
We can ditch the beforeCount
stuff. Since we assert.expect()
we know whether or not the hook ran.
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 was following the convention in a few other tests in this file like this one. I'm happy either way, but want to double-check that you prefer terseness over consistency here -- let me know which way you prefer.
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'd prefer the terse version. It's likely that the other ones should be changed as well, but that doesn't need to be done here.
Okay @trentmwillis comments addressed. Let me know if you see anything else or want me to squash commits, otherwise merge away. |
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.
Thanks! @leobalter I'm dismissing your review since your comments have been addressed.
Thanks again @bendemboski! |
When the first/last test in a module was skipped, it would cause the module's
before()/after() hook to not be called. This ensures those hooks will always be
called.