-
Notifications
You must be signed in to change notification settings - Fork 107
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
Upgrade lolex with async versions of all timer-executing calls #105
Conversation
Added tickAsync(), nextAsync(), runAllAsync(), runToLastAsync()
Wow, quite a bit of work. There is also the recent PR #102 that deals with asynchronicity. Would you have a chance to see if the two overlap? |
Sure thing. The 2 PRs can both solve certain specific use-cases, but their main goals are different. This PR is aimed at being a companion to native promises. #102 ( Use case where they could overlapThe var result: = null;
setTimeout(async function () {
result = await Promise.resolve(50);
}, 1000);
setTimeout(function () {
assert(result === 50);
}, 2000);
clock.tick(2000);
Case for #102
|
In terms of naming, I think ideally lolex would use Also, I've thought of 1 more caveat/consideration that I'll add above. Right now you can surround |
Related: sinonjs/sinon#738, sinonjs/sinon#1120, sinonjs/sinon#1397 |
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 a really nice pull request with a good amount of work put into it, so big thanks for giving it to the greater community! 🎂
Other than my comment on polyfilling this also requires a bit of documentation so that people can get how to use this. I am not sure if the current README format suits long-form all that well (which I think some pieces might require), but you can give it a go?
Anyway, do a rebase before the next commit (or remove the commit from the previous PR) to make the diff smaller.
@@ -423,6 +423,8 @@ var keys = Object.keys || function (obj) { | |||
|
|||
exports.timers = timers; | |||
|
|||
var globalSetImmediate = global.setImmediate; |
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 won't fly without a polyfill library for most browsers. setImmediate
is not an Ecmascript standard and is only found in Node and Microsoft's javascript engines. See this for recommended workarounds.
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.
For the purposes of this PR... would falling back to setTimeout
be good enough?
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.
@dominykas yes, I think so. if the performance implications bother people, they can supply a proper polyfill (there are lots of them) that uses fast versions (such as postMessage
, ModificationObserver, etc)
I have a different approach (keeping it all synchronous) - what do you think about #114 @kylefleming ? |
I can guarantee we will keep this order in NodeJS - but as someone who worked on promise integration in platforms (a little bit) not all browsers do this - and not all promise libraries. For example the most popular promise libraries (Bluebird and Q) do not do this but instead schedule promise resolution as a macrotick and not a microtick. |
@fatso83 I've been digging around trying to find the best solution that will work with the most platforms and user supplied Promise libraries. I have a few questions for you:
|
Thanks for the detailed list, @kylefleming. This is a biggie best left for the big boys. @mroderick and @mantoni, want to chime in? 😊 I'll try to address this to the best of my knowledge, but some bits are in murky waters.
My two cents. |
Unless you explicitly
If bluebird doesn't work with lolex (in terms of scheduling, it's supposed to) please let me know and I'll fix it on the bluebird side. We support swapping the scheduler anyway. @kylefleming still, wouldn't it be better to make everything synchronous instead and not have promises defer to the nextTick queue in tests (so we can flush the queue before the end of the test and run assertions?). Now that we have a And run things one after the other synchronously since we have all the hooks. Of course, this means we have to write |
That's good to hear.
For controlled setups, yes definitely. I can imagine lolex having a
Would this be something that bluebird and other promise libraries would implement as a sort of unofficial standard where lolex would look for the existence of this method and call if it exists as a way of making Independent of implementation, I do agree that for synchronous resolution of user code with promises, the current situation isn't ideal (setting Native promises seem a bit trickier though. Setting a custom scheduler works for third party libraries that support it, but short of changing the native promise implementation in some way (either swapping for a library, or monkey patching the Promise prototype), I don't think there's any way lolex can resolve them synchronously. You could make an argument that since If we do go that route, I'm not actually sure you could do it without going all the way and swapping the whole promise implementation with another one (unless you know of a way?). Given the discussion so far, giving lolex a promise microtask queue and hooking that into promise libraries' schedulers definitely makes the most sense for people using those libraries. For native promises, I'm not sure if it's better to stub the global promise prototype or to let the event system handle it by abusing the system task queues (ala this PR). I think swapping out promise implementations is a fairly common thing to do, so it's not likely to be an issue. It would allow promises to be resolved synchronously with the rest of the timers. It does make the user's test code 1 more step removed from their production code since it would not only replace when the promise is resolved but would also replace the implementation of how it's resolved. On the other hand, the async method would keep it closer to the production code and would be able to handle promise implementations that don't allow custom schedulers, but would be slower (quite a bit slower on some systems) and require users testing code that uses promises (even internally) to write their test code using promises. Either approach would be suitable for my current needs (the only thing I don't want to do is swap in another promise library across my whole application). I'll hold off on finishing this PR though and let one of the core maintainers chime in first. |
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. |
I think this needs further consideration |
Cool stuff @kylefleming I can confirm that it works :) Would be cool to get it merged! @mroderick |
The implementation details may be arguable, but the overall behavior is exactly what I've been looking for! Excellent work @kylefleming ! |
I can confirm that this patch
|
@benjamingr I have no strong feelings about this. If you can push this forward, and see that the work @kylefleming has put into it somehow ends up in the main branch, I think it would make a lot of people happy (judging from issues in the main Sinon repo). |
What do we need to do to get this merged? |
Would love this feature. What do we need to get this PR merged? |
Honestly? As a "promises person" and maintainer of a bunch of promise libraries I don't think this is really the best path forward - we should consider working on #114 (comment) - we can also (for node) expose running microticks which would let us write the above entirely synchronously. That said, if we want to - then all runAsync would require is to schedule for ourselves in the nextTick since order of microtasks is guaranteed for native promises (and people using Bluebird use setScheduler and don't have this problem to begin with). The problem is sequencing things - for two reasons:
That said, this approach is likely the most compatible one, it will work in browsers and Node and this issue is something people run into. If you'd like I can commit to working on this after I'm back from Holiday (around mid-October) - I can rebase work on top of this commit (I want to make sure the changes are attributed to @kylefleming ) - if they want to keep working on it that's also cool. |
Should we look into making this completely non-blocking, i.e., not restricting it to the scope of micro-task queue and also include macro tasks? Reason: on ticking the clock, if many events are generated (in-lieu of scheduling at intermediate times), the consumption of those events should not be blocked by the generation of subsequent events during |
Microtasks will always execute in front of the event loop in the future and mostly already do now. |
Merged as #237. Thanks @kylefleming! And thanks @dominykas for keeping the lights on :) |
Summary
This pull request adds
tickAsync()
,nextAsync()
,runAllAsync()
,runToLastAsync()
to parallel the existing synchronous calls. These new calls all return promises that resolve with what the synchronous call would have resolved with or reject with whatever error the synchronous call would have thrown.This pull request obviously requires feedback from the core lolex team, so I'm happy to make any changes that might come up.
Goal
The goal of these functions is to provide those writing tests with tools to test code that uses promises without having to work around the fact that lolex executes all timers synchronously in a row.
Example
Consider these timers:
Under normal circumstances, the promise callback would be executed immediately once the first timer is done executing, giving the second timer access to
result
. The order would be:However when using lolex to simulate time with:
causes the callbacks to be called in this order:
With these new async functions, you would simulate time with:
which would cause the callbacks to be called in this order:
This
a) properly simulates the js engine's event loop when promises are involved, and
b) allows assertions to be checked after the promise returned by
clock.*Async()
has settled.Implementation details:
These calls rely on the native
setImmediate()
and semi-rely on the behavior of the js engine to execute asynchronous callbacks in this order:Considerations
I have
tick()
andtickAsync()
proxying to adoTick()
so that they can share code, sincetick()
was the largest of the current calls. If it's preferable to separate them, I'll be happy to do that.Right now you can surround
tick()
with a try/catch in order to catch any errors that the timers might throw. If one is thrown,tick()
will rethrow the first thrown after all timer callbacks are executed. Users oftickAsync()
might also think this would catch any errors that a promise might throw, especially if the error is unhandled by user code. However, this would not be the case (and cannot be since we let the system event loop execute the promise callbacks). I don't think there's any workaround to this, other than to just know it exists and to add it to the documentation. The user will have to test for errors directly using the promise that rejected with it.Above, I mentioned that it semi-relies on that order. When using native promises, if that order is used by the js engine, then all promise callbacks created from a timer and any promise callbacks that get queue from those callbacks (etc for all nested callbacks) will be fully executed before the next timer is called by lolex. This obviously relies on the js engine executing promise callbacks until none are available before calling any set* timer callbacks. If the user is using a promise polyfill that relies on setImmediate, however, then it will not possible to execute all callbacks, because lolex must push a callback onto the set* timer stack before giving up control to let the js engine process any pending promise callbacks. If those promise callbacks create more promise callbacks, then they will be after the lolex callback in the queue and thus not possible to execute reliably.
I have
if
statements surrounding all async code. Theif
statement is checking for the presence ofglobal.Promise
. However, these methods don't have to rely on promises. They could just as easily accept a callback and return nothing. The theoretical callback-based async methods could then just be wrapped in apromisify
call for users that want to use them as promises.I don't know how much this makes sense though, since these new async calls were created specifically to handle promises created in a timer, and they don't work as reliably for async code that uses the
setImmediate
family.If a user is testing code in an environment without native promises and they are using callbacks, they are testing code that either
a) is not strictly async (does everything in the same event tick)
b) uses
process.nextTick
orsetImmediate/setTimeout
orc) relies on native async calls like
fs.open()
.As I mentioned, these methods can't reliably handle the
setImmediate
family and any code relying on engine-level async code wouldn't really be able to use lolex anyways.Therefore the only situation that a callback version makes sense would be in an environment where there aren't native promises and the user is testing asynchronous code that relies on
process.nextTick
. I don't know how common this is so I don't know if it's worth pursuing. I'm also of the opinion that new code should be using promises wherever possible, but I know this is a library so I'll let you as the core team decide if there should be a callback version.(Side note: I've included my other pull request's commit with this one so there won't be a merge conflict later since they're modifying the same code. If that pull request ends up being rejected, I'll remove that code from this one)