Skip to content
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

Assert: Introduce timeout to set per-test timeout durations #1165

Merged
merged 4 commits into from
Jul 7, 2017

Conversation

trentmwillis
Copy link
Member

As discussed in #1132.

This makes it easy to set per-test timeouts, instead of the globally configured QUnit.config.testTimeout.


### Description

`assert.timeout()`sets the length of time, in milliseconds, to wait for async operations in the current test. This is equivalent to setting `config.testTimeout` on a per-test basis. The timeout length only applies when performing async operations.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing space between "timeout()" and "sets".

src/assert.js Outdated
@@ -14,6 +14,14 @@ class Assert {

// Assert helpers

timeout( duration ) {
if ( objectType( duration ) !== "number" ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need objectType here. typeof should suffices, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For 99% of cases it's probably fine, but if someone were to pass a new Number(1000), we'd miss it, even though it is a number.

Copy link
Member

@Krinkle Krinkle Apr 20, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should support that implicitly. I'd rather be strict and throw an exception from a well-defined and stable interface.

src/test.js Outdated
@@ -36,6 +36,7 @@ export default function Test( settings ) {
this.module = config.currentModule;
this.stack = sourceFromStacktrace( 3 );
this.steps = [];
this.timeout = 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

config.testTimeout is undefined by default. Having this one be 0 feels inconsistent. On the other hand, I don't like undefined as default for config.testTimeout. false is probably a better fit here. 0 could also work although my first-impression expectation based on other software would be that 0 either means infinite waiting or that no waiting is allowed (e.g. any async will be considered an error). Whereas in our case it means no waiting. Although I suppose infinite waiting and no waiting are very similar in the case of qunit - except that issue #1132 exposes a difference, albeit through a bug we should fix separately.

If we go with 0 we should document that for this and for config.testTimeout and which ever value we pick, we should file an issue to also change config.testTimeout's default in the next major release.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that 0 is a poor default (it should describe the lowest meaningful timeout value), but I don't mind undefined for "no test-specific timeout".

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't put much thought into this when I was implementing it originally. undefined seems reasonable and is consistent with the current testTimeout, so I will go that route.

@platinumazure
Copy link
Contributor

@trentmwillis I'm going to wait to see what shakes out of Timo's review before reviewing this myself. Feel free to ping me when you've committed any changes.

var done = assert.async();
var input = $( "#test-input" ).focus();
setTimeout(function() {
assert.equal( document.activeElement, input[0], "Input was focused" );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we have a non-DOM example?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree with @gibson042 here. Note: Since the above comment was added, a non-DOM example was also added below, which is great.

I don't particularly mind having an example involving DOM in someway, but as unimportant as it might seem, the specific example we have above seems like an anti-pattern I'd rather not promote in the documentation.

I'd be okay with having just the below example, although in that case I'd rather have the main example showcase assert.async() which is easier to understand when learning QUnit. Having two examples, where one shows compat with Promise still would be nice too, though.

src/test.js Outdated
@@ -36,6 +36,7 @@ export default function Test( settings ) {
this.module = config.currentModule;
this.stack = sourceFromStacktrace( 3 );
this.steps = [];
this.timeout = 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that 0 is a poor default (it should describe the lowest meaningful timeout value), but I don't mind undefined for "no test-specific timeout".

src/test.js Outdated
test.semaphore += 1;
config.blocking = true;

// Set a recovery timeout, if so configured.
if ( config.testTimeout && defined.setTimeout ) {
const timeoutDuration = test.timeout || config.testTimeout;
if ( timeoutDuration && defined.setTimeout ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per the above, this should be more explicit. There's also a question of whether the global value is a fallback or a maximum. I'm personally inclined towards the latter, e.g.

const specificTimeout = typeof test.timeout === "number" ? test.timeout : Infinity;
const globalTimeout = typeof config.testTimeout === "number" ? config.testTimeout : Infinity;
const resolvedTimeout = Math.min( explicitTimeout, globalTimeout );
if ( resolvedTimeout < Infinity && defined.setTimeout ) {
	
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer the fallback approach. It is conceivable to want the majority of your tests to use a short timeout and then override that for a specific, slow test. If we go with the global maximum approach, we force the default for all tests to be the longer value.

@trentmwillis
Copy link
Member Author

@Krinkle @gibson042 I've updated and believe I have addressed all comments. Another review would be appreciated 😄

@trentmwillis
Copy link
Member Author

@qunitjs/qunit-team can I get someone to review this? I'd like to land this before releasing 2.4. Thanks!

QUnit.test( "does not push a failure if test is synchronous", function( assert ) {
assert.timeout( 1 );

var wait = Date.now() + 50;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use 10 here as with the other one. Will make the test run a bit faster, too.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rebased + updated.


`assert.timeout()` sets the length of time, in milliseconds, to wait for async operations in the current test. This is equivalent to setting `config.testTimeout` on a per-test basis. The timeout length only applies when performing async operations.

If `0` is passed, then the test will be assumed to be completely synchronous. If a non-numeric value is passed as an argument, the function will throw an error.
Copy link
Member

@Krinkle Krinkle Jul 4, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we currently handle this. I haven't manually confirmed, but from looking at the code and the test, this isn't ensured as far as I can see. The current test verifies the error message, not the behaviour.

"Completely synchronous" is quite a bold statement, one I'd love to have in our documentation, but it does set high expectations. We wouldn't want to have this with low-confidence of it actually being so. It should mean that, by the end of the test() callback, the test is over.

setTimeout(, 0) tends to be rounded up to at least ~4ms on most platforms. And even without that aspect, "the next (available) tick" can still have many (small) things in-between. Especially the micro-task queue in JavaScript (containing Promise callbacks, event handlers, and various other things), and things like requestAnimationFrame, setImmediate, and other features that offer a shorter "timeout" than setTimeout.

Apart from that, even we'd have a way to enqueue before the micro-task queue, that means the user's code could as well, and it tends to be FIFO, so our handler might still be after the user's handler that calls done() which still violates the fine "Completely synchronous" guarantee.

I'd like to see the following fail (with timeout=0):

  • returning Promise.resolve().
  • Manually, via Promise.resolve().then( assert.async() )`.
  • Manually, via setTimeout( assert.sync(), 0 );.

As for how to make these fail, I think we may have to special-case 0 and actually validate it right after calling test(), which means a more tight integration, but I think it's worth for this feature.

Copy link
Member Author

@trentmwillis trentmwillis Jul 4, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To summarize (to ensure I'm understanding everything):

  • setTimeout(, 0) doesn't guarantee synchronicity (agreed)
  • However, we do want to guarantee synchronicity when using a timeout of 0
  • Therefore, we should special case assert.timeout(0)
  • And, add tests handling edge cases that likely fail with the current implementation

Does that seem correct?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, that's right.

Copy link
Contributor

@platinumazure platinumazure left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, aside from concerns already raised in other reviews.

@trentmwillis
Copy link
Member Author

I have updated and addressed @Krinkle's concerns.

@platinumazure I've disabled the max-len ESLint rule for the test directory since it is not uncommon for test descriptions to be longer than it and I feel it adds negative value to split them on multiple lines.

stubPushFailure( assert );

var done = assert.async();
setTimeout( () => done(), 0 );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Causes CI failure) Too new syntax for the web browser we use for our Travis tests, and presumably for some of our other browsers as well.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bah, a side-effect of too much Babel usage...

@trentmwillis
Copy link
Member Author

CI passing again.


// If the test has a "lock" on it, but the timeout is 0, then we push a
// failure as the test should be synchronous.
if ( test.timeout === 0 && test.semaphore !== 0 ) {
Copy link
Member

@Krinkle Krinkle Jul 6, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I keep staring at test.semaphore !== 0 here. I can't quite put my finger on it... We need this because... Because, semaphore controls whether or not we'll wait before starting the next test, and is incremented whenever a Promise is returned and/or async was used. Got it.

@platinumazure
Copy link
Contributor

@trentmwillis re: max-len Sounds good to me.

@trentmwillis trentmwillis dismissed gibson042’s stale review July 6, 2017 15:28

Prior comments have been addressed

@trentmwillis trentmwillis merged commit 6fc5541 into qunitjs:master Jul 7, 2017
@trentmwillis trentmwillis deleted the assert-timeout branch July 7, 2017 03:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

4 participants