-
Notifications
You must be signed in to change notification settings - Fork 16
Add FakeAsync.runNextTimer #85
Changes from all commits
b22f6e4
fb5f59c
a78ba91
4916d20
ae309c9
158b5d8
02188dc
4fd4365
6227b63
6fb6a8c
48f268f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -138,7 +138,7 @@ class FakeAsync { | |
} | ||
|
||
_elapsingTo = _elapsed + duration; | ||
_fireTimersWhile((next) => next._nextCall <= _elapsingTo!); | ||
while (runNextTimer(until: _elapsingTo!)) {} | ||
_elapseTo(_elapsingTo!); | ||
_elapsingTo = null; | ||
} | ||
|
@@ -211,39 +211,63 @@ class FakeAsync { | |
{Duration timeout = const Duration(hours: 1), | ||
bool flushPeriodicTimers = true}) { | ||
final absoluteTimeout = _elapsed + timeout; | ||
_fireTimersWhile((timer) { | ||
if (timer._nextCall > absoluteTimeout) { | ||
// TODO(nweiz): Make this a [TimeoutException]. | ||
throw StateError('Exceeded timeout $timeout while flushing timers'); | ||
while (true) { | ||
// With [flushPeriodicTimers] false, continue firing timers only until | ||
// all remaining timers are periodic *and* every periodic timer has had | ||
// a chance to run against the final value of [_elapsed]. | ||
if (!flushPeriodicTimers) { | ||
if (_timers | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (Just a comment: So much of this code would be easier to read (and more efficient) if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed. |
||
.every((timer) => timer.isPeriodic && timer._nextCall > _elapsed)) { | ||
break; | ||
} | ||
} | ||
|
||
if (flushPeriodicTimers) return _timers.isNotEmpty; | ||
if (!runNextTimer(until: absoluteTimeout)) { | ||
if (_timers.isEmpty) break; | ||
|
||
// Continue firing timers until the only ones left are periodic *and* | ||
// every periodic timer has had a change to run against the final | ||
// value of [_elapsed]. | ||
return _timers | ||
.any((timer) => !timer.isPeriodic || timer._nextCall <= _elapsed); | ||
}); | ||
// TODO(nweiz): Make this a [TimeoutException]. | ||
throw StateError('Exceeded timeout $timeout while flushing timers'); | ||
} | ||
} | ||
} | ||
|
||
/// Invoke the callback for each timer until [predicate] returns `false` for | ||
/// the next timer that would be fired. | ||
/// Elapses time to run one timer, if any timer exists. | ||
/// | ||
/// Running one timer at a time, rather than just advancing time such as | ||
/// with [elapse] or [flushTimers], can be useful if a test wants to run | ||
/// its own logic between each timer event, for example to verify invariants | ||
/// or to end the test early in case of an error. | ||
Comment on lines
+236
to
+239
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adapted from the suggestion at #85 (review) . For this class of use case:
my thinking would be that the convenient way to handle those is to create timers that trigger them. By putting them all on the same timeline measured with For UI updates, in the case of Flutter with As I see it the key things that make this API valuable, in a way that can't be substituted for by just scheduling more timers, are that the test can have its own code run after each timer (however frequent or infrequent those might be on the fake-time timeline), and that it can break out of the loop at will. The use case I have myself, described at #84 and demo'ed at gnprice/zulip-flutter@1ad0a6f , is to do those things to check if an error has been recorded, and if so then to throw and stop running timers. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agree. If someone wants to simulate events, they still have to simulate when the event occurs, and then it might as well be treated as a timer event. (That also ensures that all prior due timers get handled first.) |
||
/// | ||
/// Microtasks are flushed before and after each timer is fired. Before each | ||
/// timer fires, [_elapsed] is updated to the appropriate duration. | ||
void _fireTimersWhile(bool Function(FakeTimer timer) predicate) { | ||
/// Microtasks are flushed before identifying the timer to run, | ||
/// and again after the timer runs. | ||
/// Before the timer runs, [elapsed] is updated to the appropriate value. | ||
/// | ||
/// When [until] is non-null, only timers due up until the given time | ||
/// will be considered, in terms of [elapsed]. | ||
/// | ||
/// Because multiple timers may be due at the same time, a call to this | ||
/// method may leave the time advanced to where other timers are due. | ||
/// Calling `elapse(Duration.zero)` afterwards may trigger more timers. | ||
/// | ||
/// If microtasks or timer callbacks make their own calls to methods on this | ||
/// [FakeAsync], then a call to this method may indirectly cause more timers | ||
/// to run beyond the timer it runs directly, and may cause [elapsed] to | ||
/// advance beyond [until]. Any such timers are ignored in the return value. | ||
/// | ||
/// Returns `true` if a timer was run, `false` otherwise. | ||
bool runNextTimer({Duration? until}) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this code re-entrancy safe? (Likely not, code rarely is if it's not designed for it.) That is, if someone calls There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, good question. In fact, when I stared at this code today to try to pin down an answer, I discovered a bug in the existing code: It's actually normal and useful, though, that Specifically, we write tests like test('thing gets handled', () => awaitFakeAsync((async) async {
await scheduleThing(delay: const Duration(seconds: 1));
async.elapse(const Duration(seconds: 1));
checkThingWasHandled();
})); where Fortunately I think
Beyond The possibility of re-entrancy does call for a bit of care, though, when looking at a call site of /// If microtasks or timer callbacks make their own calls to methods on this
/// [FakeAsync], then a call to this method may indirectly cause more timers
/// to run beyond the timer it runs directly, and may cause [elapsed] to
/// advance beyond [until]. Any such timers are ignored in the return value. In terms of the use cases I put in the doc:
|
||
flushMicrotasks(); | ||
for (;;) { | ||
if (_timers.isEmpty) break; | ||
|
||
final timer = minBy(_timers, (FakeTimer timer) => timer._nextCall)!; | ||
if (!predicate(timer)) break; | ||
|
||
_elapseTo(timer._nextCall); | ||
timer._fire(); | ||
flushMicrotasks(); | ||
if (_timers.isEmpty) return false; | ||
final timer = minBy(_timers, (FakeTimer timer) => timer._nextCall)!; | ||
if (until != null && timer._nextCall > until) { | ||
return false; | ||
} | ||
|
||
_elapseTo(timer._nextCall); | ||
timer._fire(); | ||
flushMicrotasks(); | ||
return true; | ||
} | ||
|
||
/// Creates a new timer controlled by `this` that fires [callback] after | ||
|
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.
Here we could run through all the timers, find the maximal
_nextCall
among them (or among the non-periodic ones if we ignore periodic timers), starting with "now", and if we don't find any later timers, we're done.If the latest timer is after
absoluteTimeout
, then cap it at absolute timeout and remember that we did that.The just keep
runNextTimer
until that time.If we stopped because of the timeout, handle that.
That seems more efficient than doing a complete scan of the timers after each event.
(Technically we don't know if all later timers were cancelled before we reached the timeout.)
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 could do that but it wouldn't change the asymptotics — we'd still be scanning through all the timers each time in order to find the next one to run. That's the
minBy
call inrunNextTimer
, and in the existing code in the loop body in_fireTimersWhile
.If we want to optimize this algorithmically, I think the way to do it would be to change the data structures. We could… actually what I was about to write is the same thing you suggested in this comment above: #85 (comment)
That's probably best kept in a separate PR, though.
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.
Ack. Only so much we can do locally can do when every later access is a linear scan too.
This should be fine.