-
Notifications
You must be signed in to change notification settings - Fork 16
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 FakeAsync.runNextTimer #85
base: master
Are you sure you want to change the base?
Conversation
If we reach this line, `_timers.isNotEmpty` must be true. That's because this callback's only call site is the line `if(!predicate(timer)) break;` in `_fireTimersWhile`, and there's a check a few lines above there that would have broken out of the loop if `_timers` were empty.
This version is exactly equivalent via Boolean algebra, and will make for a bit simpler of a diff in the next refactor.
I think this looks like a sensible API. I'll double check this doesn't impact existing internal usage. @lrhn any concerns? |
if (timer._nextCall > absoluteTimeout) { | ||
// TODO(nweiz): Make this a [TimeoutException]. | ||
throw StateError('Exceeded timeout $timeout while flushing timers'); | ||
for (;;) { |
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.
Usually use while (true) {
for this pattern in Dart.
@@ -138,7 +138,7 @@ class FakeAsync { | |||
} | |||
|
|||
_elapsingTo = _elapsed + duration; | |||
_fireTimersWhile((next) => next._nextCall <= _elapsingTo!); | |||
while (runNextTimer(timeout: _elapsingTo! - _elapsed)) {} |
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 make an internal helper _runNextTimer([Duration? until])
that doesn't take a duration relative to now, but takes the actual end time (duration since initialTime
).
That's the time (_elapsingTo
) that you already have here, and it's the first thing that runNextTimer
computes internally anyway.
It avoids repeatedly doing _elapsingTo - _elapsed
.
So:
final elapsingTo = _elapsingTo = _elapsed + duration;
while (_runNextTimer(elapsingTo)) {}
Then the public function would be:
bool runNextTimer({Duration? timeout]) {
if (timeout == null) return runNextTimer();
var timeoutTime = _elapsed + timeout;
if (_runNextTimer(timeoutTime)) {
return true;
} else {
_elapsed = timeoutTime;
return false;
}
}
(I suggest advancing time by at least the [timeout]. If not, it should be renamed to something else, like before
. A timeout
only applies if time has actually advanced.)
/// timer runs, [elapsed] is updated to the appropriate value. | ||
/// | ||
/// The [timeout] controls how much fake time may elapse. If non-null, | ||
/// then timers further in the future than the given duration will be ignored. |
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 would still advance the time by timeout
before returning false.
Otherwise the name should be different. A timeout triggering means that the time has passed.
I can see that flushTimers
doesn't do that, but that's because it throws an error if all (non-periodic) timers are not flushed before the timeout is up, and presumably the test fails then, so advancing the time doesn't matter.
That is, the timeout
parameter to runNextTimer
is not the same as the one to flushTimers
. The latter means "it's an error if we reach it", the former is just "only try to go this far".
If we change the call in flushTimers
to the internal _runNextTimer
, and it's only the public runNextTimer
that advance time on a false
result, then we won't change the behavior of flushTimers
.
// 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 comment
The 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 _timers
was a priority queue. And if it tracked the number of periodic timers on the side, this check would just be _timers.length == _periodicTimerCount
. All this repeated iteration only works because it's for testing only, and there aren't that many timers in one test.)
/// The [timeout] controls how much fake time may elapse. If non-null, | ||
/// then timers further in the future than the given duration will be ignored. | ||
/// | ||
/// Returns true if a timer was run, false otherwise. |
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 code-quote true
and false
. Generally try to code-quote literals when I refer to language-semantic values, like these, void
and null
, "done"
or 42
, to distinguish them from plain text containg the same symbols as English words or numbers.
/// | ||
/// Returns true if a timer was run, false otherwise. | ||
bool runNextTimer({Duration? timeout}) { | ||
final absoluteTimeout = timeout == null ? null : _elapsed + 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.
That is, I'd start the internal bool _runNextTimer(Duration? absoluteTimeout) {
here instead.
flushMicrotasks(); | ||
for (;;) { | ||
if (_timers.isEmpty) break; | ||
/// Microtasks are flushed before and after the timer runs. Before the |
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.
Mention that the current microtask queue is flushed whether or not there is a timer in range.
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.
No problem with the API, but suggestions on structuring the implementation.
One change request: Make a reached timeout actually advance the time to that timeout time.
Consider suggesting use-cases for the method in its doc. Something like:
/// Running only one timer at a time, rather than just advancing time,
/// can be used if a test wants to simulate other asynchronous non-timer events
/// between timer events, for example simulating UI updates
/// or port communication.
(Or something. That's what I could come up with.)
Maybe also say:
/// Notice that after a call this method, the time may have been advanced
/// to where multiple timers are due. Doing an `elapse(Duration.zero)` afterwards
/// may trigger more timers.
Before adding this method, the only way to advance time was elapse
and flushTimers
, which both always runs all due timers before exiting.
We should probably check that there is no code assuming that the timers in _timers
are not yet due. (Probably not an issue, the code is pretty basic.)
(Should we have an isTimerDue
check?)
No impact to internal usage, so this is safe to land after review. |
Fixes #84.
This method is like flushTimers, but runs just one timer and then returns.
That allows the caller to write their own loop similar to flushTimers
but with custom logic of their own.
Contribution guidelines:
dart format
.Note that many Dart repos have a weekly cadence for reviewing PRs - please allow for some latency before initial review feedback.