Skip to content

Commit 26a9831

Browse files
authored
Fully update internal state before invoking periodic-timer callback (dart-archive/fake_async#89)
Fixes dart-lang/fake_async#88. Currently when a periodic timer's callback gets invoked, its `_nextCall` is still the time of the current call, not the next one. If the timer callback itself calls `flushTimers` or `elapse`, this causes the same timer to immediately get called again. Fortunately the fix is easy: update `_nextCall` just before invoking `_callback`, instead of just after. --- To work through why this is a complete fix (and doesn't leave further bugs of this kind still to be fixed): After this fix, the call to the timer's callback is a tail call from `FakeTimer._fire`. Because the call site of `FakeTimer._fire` is immediately followed by `flushMicrotasks()`, this means calling other `FakeAsync` methods from the timer callback is no different from doing so in a subsequent microtask. Moreover, when running timers from `flushTimers`, if after the `flushMicrotasks` call this turns out to be the last timer to run, then `flushTimers` will return with no further updates to the state. So when the timer callback is invoked (in that case), the whole `FakeAsync` state must already be in a state that `flushTimers` would have been happy to leave it in. (And there's no special cleanup that it does only after a non-last timer.) Similarly, when running timers from `elapse` (the only other possibility), the only difference from a state that `elapse` would be happy to leave things in is that `_elapsingTo` is still set. That field affects only `elapse` and `elapseBlocking`; and those are both designed to handle being called from within `elapse`.
1 parent 4d08590 commit 26a9831

File tree

3 files changed

+21
-1
lines changed

3 files changed

+21
-1
lines changed

pkgs/fake_async/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
## 1.3.2-wip
22

33
* Require Dart 3.3
4+
* Fix bug where a `flushTimers` or `elapse` call from within
5+
the callback of a periodic timer would immediately invoke
6+
the same timer.
47

58
## 1.3.1
69

pkgs/fake_async/lib/fake_async.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,9 +320,9 @@ class FakeTimer implements Timer {
320320
assert(isActive);
321321
_tick++;
322322
if (isPeriodic) {
323+
_nextCall += duration;
323324
// ignore: avoid_dynamic_calls
324325
_callback(this);
325-
_nextCall += duration;
326326
} else {
327327
cancel();
328328
// ignore: avoid_dynamic_calls

pkgs/fake_async/test/fake_async_test.dart

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,23 @@ void main() {
586586
expect(ticks, [1, 2]);
587587
});
588588
});
589+
590+
test('should update periodic timer state before invoking callback', () {
591+
// Regression test for: https://github.com/dart-lang/fake_async/issues/88
592+
FakeAsync().run((async) {
593+
final log = <String>[];
594+
Timer.periodic(const Duration(seconds: 2), (timer) {
595+
log.add('periodic ${timer.tick}');
596+
async.elapse(Duration.zero);
597+
});
598+
Timer(const Duration(seconds: 3), () {
599+
log.add('single');
600+
});
601+
602+
async.flushTimers(flushPeriodicTimers: false);
603+
expect(log, ['periodic 1', 'single']);
604+
});
605+
});
589606
});
590607

591608
group('clock', () {

0 commit comments

Comments
 (0)