Skip to content

Commit

Permalink
Add FakeAsync.runNextTimer
Browse files Browse the repository at this point in the history
Fixes #84.
  • Loading branch information
gnprice committed Jun 6, 2024
1 parent fb5f59c commit b798185
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 24 deletions.
57 changes: 33 additions & 24 deletions lib/fake_async.dart
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ class FakeAsync {
}

_elapsingTo = _elapsed + duration;
_fireTimersWhile((next) => next._nextCall <= _elapsingTo!);
while (runNextTimer(timeout: _elapsingTo! - _elapsed)) {}
_elapseTo(_elapsingTo!);
_elapsingTo = null;
}
Expand Down Expand Up @@ -211,41 +211,50 @@ 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');
}

for (;;) {
// 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) {
return !_timers
.every((timer) => timer.isPeriodic && timer._nextCall > _elapsed);
if (_timers
.every((timer) => timer.isPeriodic && timer._nextCall > _elapsed)) {
break;
}
}

return true;
});
if (!runNextTimer(timeout: absoluteTimeout - _elapsed)) {
if (_timers.isEmpty) break;

// 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.
///
/// 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) {
flushMicrotasks();
for (;;) {
if (_timers.isEmpty) break;
/// Microtasks are flushed before and after the timer runs. Before the
/// 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.
///
/// Returns true if a timer was run, false otherwise.
bool runNextTimer({Duration? timeout}) {
final absoluteTimeout = timeout == null ? null : _elapsed + timeout;

final timer = minBy(_timers, (FakeTimer timer) => timer._nextCall)!;
if (!predicate(timer)) break;
flushMicrotasks();

_elapseTo(timer._nextCall);
timer._fire();
flushMicrotasks();
if (_timers.isEmpty) return false;
final timer = minBy(_timers, (FakeTimer timer) => timer._nextCall)!;
if (absoluteTimeout != null && timer._nextCall > absoluteTimeout) {
return false;
}

_elapseTo(timer._nextCall);
timer._fire();
flushMicrotasks();
return true;
}

/// Creates a new timer controlled by `this` that fires [callback] after
Expand Down
95 changes: 95 additions & 0 deletions test/fake_async_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,101 @@ void main() {
});
});

group('runNextTimer', () {
test('should run the earliest timer', () {
FakeAsync().run((async) {
var last = 0;
Timer(const Duration(days: 2), () => last = 2);
Timer(const Duration(days: 1), () => last = 1);
Timer(const Duration(days: 3), () => last = 3);
expect(async.runNextTimer(), true);
expect(last, 1);
});
});

test('should return false if no timers exist', () {
FakeAsync().run((async) {
expect(async.runNextTimer(), false);
});
});

test('should run microtasks before choosing timer', () {
FakeAsync().run((async) {
var last = 0;
Timer(const Duration(days: 2), () => last = 2);
scheduleMicrotask(() => Timer(const Duration(days: 1), () => last = 1));
expect(async.runNextTimer(), true);
expect(last, 1);
expect(async.runNextTimer(), true);
expect(last, 2);
});
});

test('should run microtasks before deciding no timers exist', () {
FakeAsync().run((async) {
var last = 0;
scheduleMicrotask(() => Timer(const Duration(days: 1), () => last = 1));
expect(async.runNextTimer(), true);
expect(last, 1);
});
});

test('should run microtasks after timer', () {
FakeAsync().run((async) {
var ran = false;
Timer.run(() => scheduleMicrotask(() => ran = true));
expect(async.runNextTimer(), true);
expect(ran, true);
});
});

test('should update elapsed before running timer', () {
FakeAsync().run((async) {
Duration? time;
Timer(const Duration(days: 1), () => time = async.elapsed);
expect(async.runNextTimer(), true);
expect(time, const Duration(days: 1));
});
});

test('should apply timeout', () {
FakeAsync().run((async) {
var ran = false;
Timer(const Duration(days: 1), () => ran = true);
expect(async.runNextTimer(timeout: const Duration(hours: 1)), false);
expect(ran, false);
});
});

test('should apply timeout as non-strict bound', () {
FakeAsync().run((async) {
var ran = false;
Timer(const Duration(hours: 1), () => ran = true);
expect(async.runNextTimer(timeout: const Duration(hours: 1)), true);
expect(ran, true);
});
});

test('should apply timeout relative to current time', () {
FakeAsync().run((async) {
var ran = false;
Timer(const Duration(hours: 3), () => ran = true);
async.elapse(const Duration(hours: 2));
expect(async.runNextTimer(timeout: const Duration(hours: 2)), true);
expect(ran, true);
});
});

test('should have no timeout by default', () {
FakeAsync().run((async) {
var ran = false;
Timer(const Duration(microseconds: 1 << 52), () => ran = true);
expect(async.runNextTimer(), true);
expect(ran, true);
});
});
});

group('stats', () {
test('should report the number of pending microtasks', () {
FakeAsync().run((async) {
Expand Down

0 comments on commit b798185

Please sign in to comment.