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
Add FakeClock #98
Add FakeClock #98
Conversation
My initial plans for this are to use it to test a throttle/debounce implementation for quiver.streams. fyi: I tried to replace |
@justinfagnani @yjbanov @cbracken anyone had a chance to look at this yet? Need to get this added before I can start working on #116. |
Thanks for the reminder. Busy quarter :) Looking at it right now. |
I think you are taking it a bit too far with zones and async. Clock is much simpler in design than http://sinonjs.org/docs/#clock. All it does is tell you current time and time relative to current time. It's fully synchronous. A FakeClock implementation should only let you manipulate current time, and do that synchronously. If you want to advance clock asynchronously you should setup a separate (fake)timer to do that. Setting up zones should also come from outside. I think the following interface is all it should have:
That said, I think there is value in the async and zone utilities you added. I just think they belong in separate libraries. I think what you are trying to achieve can be done via three orthogonal utilities: FakeClock, FakeTimer and FakeZone. They should be usable independently or together. Then you can easily build your own utility that advances clocks using a time and binds things to specific zones. Anybody else have different thoughts? |
Thanks Yegor. I agree with keeping Clock simple, see #92. In this vein, I considered separating the read/write parts of the API, similar to Completer/Future and SreamController/Stream. That would look like: class ClockAdvancer {
Future advance(Duration duration);
void advanceSync(Duration duration);
Zone get zone;
Clock get clock;
} I initially decided that that wasn't necessary since APIs will take a I do not agree with a separate FakeTimer/CreateTimer/CreateTimerPeriodic API. Callers should not have to know how their callee (and all of its transitive callees) internally use Timers. Instead they should only have to know that the callee depends on the current time, by taking a Clock as a dependency. The same is true for FakeStopwatch, APIs which use stopwatches should just take a Clock as a dependency, and then pass this Clock (or a closure of it's Timer usage can be very complex. For example with #116 which is my initial use case for this, both periodic and single shot timers are getting created and canceled all the time and in very dynamic ways. Another example, core APIs which internally use Timers such as Future.delayed, Future.timeout, Stream.periodic, and Stream.timeout will never provide a CreateTimer or similar dependency, so these core APIs couldn't be used within that framework. So let me explain how this all works... Luckily, the createTimer and createPeriodicTimer ZoneSpecification hooks allow us to externally observe how Timers are being used, so this PR takes advantage of that. All timers created in the zone are stored for later usage with When the time is advanced synchronously (advanceSync), the timers should not be called. This simulates blocking or expensive calls, such as a sync File system access. Asynchronous time advancement (advance), is the more common case, and is where Timers come in to play. When calling advance, any stored Timers scheduled to expire within that time advancement frame are executed, in the correct order, at the correct time, and only if they were not already canceled. Each timer is called in its own event loop frame to robustly simulate real timer behavior. For example, microtasks need to get called between Timers. It uses the real (root zone's) Timer.run to schedule the next timer expiration in the next event loop, so we don't have to actually wait the specified amount of time. Calling a timer callback can cause new Timers to be created or canceled, so each time it searches the entire set of stored timers for the next scheduled. When a timer is canceled or expires (if non-periodic), it is removed from the storage mechanism to make later searches quicker and avoid memory leaks. Once there are no more active timers, or the clock has advanced the specified amount, then the Future returned by advance is completed. As you can see the management of these fake timers in coordination with the advancement of the clock, is quite complex, so this PR attempts to encapsulate all this in an easy to use API so users don't have to come up with one-off solutions for each test they write. Does that help? |
Thanks for the detailed explanation. That's roughly how I understood your intention, and I agree that it is very useful. There is, however, a different approach to this that Angular.dart adopted: https://github.com/angular/angular.dart/blob/master/lib/mock/zone.dart The combination of async, microLeap and clockTick allow you to test a good range of async code. There are a couple of nice properties about this approach:
Example:
I was thinking of porting it to Quiver at some point. Could you have a look and give your thoughts? |
Thanks for the link, didn't know about it. After looking it over, it appears to be pretty limited and often incorrect for time simulation. Here's what I found:
This PR covers all of this, and has tests for most of it. I guess If we don't want to marry to Clock, could just provide a now method instead: class ClockAdvancer {
// ...
DateTime now();
}
// ...
var clock = new Clock(clockAdvancer.now); In your example, you had to update Thoughts? |
@yjbanov PTAL I removed the dependency on Clock as suggested, and renamed to FakeTime. I also replaced the exposed test('testedFunc', () {
var time = new FakeTime(initialTime: initialTime);
return time.run(() {
testedFunc(now: time.now);
return time.advance(duration).then((_) => expect(...));
});
}); |
@yjbanov @justinfagnani Any other changes I can make to get this landed? Thanks. |
Sorry Sean, end of quarter madness the last two weeks over here. I've been On Fri, Apr 4, 2014 at 1:20 PM, Sean Eagan notifications@github.com wrote:
|
Meeting @justinfagnani Monday to discuss it. |
Great! Here's some other stuff to consider: Rename test('testedFunc', () => FakeTime.run(initialTime, (FakeTime time) {
testedFunc(now: time.now);
return time.advance(duration).then((_) => expect(...));
})); But then |
Better yet:
This way The signature of
The purpose of Note how I covertly renamed |
Oh, and to match the Dart SDK package structure, we should move this under |
Actually, I take the rename part back. |
PTAL I love the idea of separating the absolute time from the advancement of time. I don't like having 2 separate interfaces for advancing time, and having to tie those together for the common case. Synchronous advancement of time is less common, so having a separate synchronous FakeClock would be error prone and confusing. And a push model like I accomplished the same separation of absolute time by replacing I also kept test('testedFunc', () => new FakeTime().run((time) {
testedFunc(now: () => initialTime.add(time.elapsed));
return time.elapse(duration).then((_) => expect(...));
})); |
This reverts commit 279b0c1. Conflicts: lib/testing/src/async/fake_time.dart test/testing/async/fake_time_test.dart
The goal of Also, I think the refactoring of |
@yjbanov Agreed. Previous commit changed |
@justinfagnani @yjbanov PTAL |
timer._nextCall.millisecondsSinceEpoch <= _now.millisecondsSinceEpoch || | ||
(_elapsingTo != null && | ||
timer._nextCall.millisecondsSinceEpoch <= | ||
_elapsingTo.millisecondsSinceEpoch) |
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 here we need only check _nextCall <= _elapsingTo
, because _elapsingTo
is never null
and _now <= _elapsingTo
holds, making _nextCall <= _now
and _elapsingTo != null
checks redundant.
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.
Agree that it cannot be null, fixed.
Calls to elapseBlocking from timers/microtasks called during elapse, can cause the current fake time to surpass elapsingTo, in which case those timers expiring all the way up to the current time need to be included. Otherwise, the state of the timers and the clock will be out of sync on the return from elapse, and it would be surprising to have a bunch of timers called on the next call to say elapse(Duration.ZERO)
.
So I now update _elapsingTo in elapseBlocking when _elapsingTo is surpassed by the clock.
This version looks great. I only have a couple of cosmetic suggestions for the implementation. One last bit I'm still not sure about is the name. This class not only fakes time, but more precisely Dart's entire event loop. What do you think of I have some ideas for expanding the functionality of this class, but I think for an initial version this is great. @justinfagnani, @cbracken, could one of you have a look at the code w.r.t. style? I've been staring at this code for too long :) |
initialTime is only relevant to the Clock, which is not always needed, so it should not be a constructor parameter. This allows initialTime to be a required argument, and thus not be named, and not having to default it to `new DateTime.now()` which introduces indeterminism to tests.
Yes, the timer throttling TODO is still relevant. Nested timer chains (including periodic timers) which run without advancing the clock (zero duraton) will lead to an infinite loop causing Regarding the name, I like |
@justinfagnani any chance we can get this merged this week? Thanks! |
@yjbanov @justinfagnani @cbracken @anyone ? @bueller ? |
@seaneagan, I'm ready to merge your PR as soon as you rename FakeTime/fake_time to FakeAsync/fake_async. |
@yjbanov Thanks. Can you please address my comments above about why I prefer FakeTime. As it currently exists, the purpose of this class is to fake the passage of time, not the event loop, and not all things async. As I mentioned, it's currently only faking microtasks to allow |
@seaneagan, I understand your reasoning. There is a high level of isomorphism between the operation of the event loop and the the passage of time, so I am actually not worried about the descriptiveness of either "time" or "async" (or even "entropy"). The primary reason I'd like this to be called And yes, I think we should expand it to include flushing of microtasks, etc. I have lots of use-cases for this. But not in this PR. It can be added incrementally. In the future, when I/O can be faked, it will likely be through zones too, and we'll be able to expand the functionality further without carrying the "time" bias. |
We could really use a tie-breaking vote here. Can you help us out? @yjbanov I don't agree that I/O (or HTTP or DOM) will be faked by Zones in the future, and thus not by this class, and more importantly, should not be. You said above: "Orthogonality is part of Quiver's approach. It is primarily a collection of small independent utilities." i.e. "do one thing well". For this class, that one thing is faking time, thus FakeTime. I think the |
ok, FakeAsync it is then. Thanks in advance for getting this merged quickly! Can't wait to finally start using it! |
Thank you, Sean, for you contribution and patience :) |
This is similar to:
http://sinonjs.org/docs/#clock
(I think this could replace CreateTimer and CreateTimerPeriodic, which IMO exposes too much of the internal implementation of whatever is taking it is a dependency.)