blob: 463eecdb8bbbdd9378853d2e3de3dbb22a06a78d [file] [log] [blame]
// Copyright 2014 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:async';
import 'package:clock/clock.dart';
import 'package:fake_async/fake_async.dart';
import 'package:test/test.dart';
void main() {
final initialTime = DateTime(2000);
final elapseBy = const Duration(days: 1);
test('should set initial time', () {
expect(FakeAsync().getClock(initialTime).now(), initialTime);
});
group('elapseBlocking', () {
test('should elapse time without calling timers', () {
Timer(elapseBy ~/ 2, neverCalled);
FakeAsync().elapseBlocking(elapseBy);
});
test('should elapse time by the specified amount', () {
final async = FakeAsync()..elapseBlocking(elapseBy);
expect(async.elapsed, elapseBy);
});
test('should throw when called with a negative duration', () {
expect(() => FakeAsync().elapseBlocking(const Duration(days: -1)),
throwsArgumentError);
});
});
group('elapse', () {
test('should elapse time by the specified amount', () {
FakeAsync().run((async) {
async.elapse(elapseBy);
expect(async.elapsed, elapseBy);
});
});
test('should throw ArgumentError when called with a negative duration', () {
expect(() => FakeAsync().elapse(const Duration(days: -1)),
throwsArgumentError);
});
test('should throw when called before previous call is complete', () {
FakeAsync().run((async) {
Timer(elapseBy ~/ 2, expectAsync0(() {
expect(() => async.elapse(elapseBy), throwsStateError);
}));
async.elapse(elapseBy);
});
});
group('when creating timers', () {
test('should call timers expiring before or at end time', () {
FakeAsync().run((async) {
Timer(elapseBy ~/ 2, expectAsync0(() {}));
Timer(elapseBy, expectAsync0(() {}));
async.elapse(elapseBy);
});
});
test('should call timers expiring due to elapseBlocking', () {
FakeAsync().run((async) {
Timer(elapseBy, () => async.elapseBlocking(elapseBy));
Timer(elapseBy * 2, expectAsync0(() {}));
async.elapse(elapseBy);
expect(async.elapsed, elapseBy * 2);
});
});
test('should call timers at their scheduled time', () {
FakeAsync().run((async) {
Timer(elapseBy ~/ 2, expectAsync0(() {
expect(async.elapsed, elapseBy ~/ 2);
}));
final periodicCalledAt = <Duration>[];
Timer.periodic(
elapseBy ~/ 2, (_) => periodicCalledAt.add(async.elapsed));
async.elapse(elapseBy);
expect(periodicCalledAt, [elapseBy ~/ 2, elapseBy]);
});
});
test('should not call timers expiring after end time', () {
FakeAsync().run((async) {
Timer(elapseBy * 2, neverCalled);
async.elapse(elapseBy);
});
});
test('should not call canceled timers', () {
FakeAsync().run((async) {
Timer(elapseBy ~/ 2, neverCalled).cancel();
async.elapse(elapseBy);
});
});
test('should call periodic timers each time the duration elapses', () {
FakeAsync().run((async) {
Timer.periodic(elapseBy ~/ 10, expectAsync1((_) {}, count: 10));
async.elapse(elapseBy);
});
});
test('should call timers occurring at the same time in FIFO order', () {
FakeAsync().run((async) {
final log = <String>[];
Timer(elapseBy ~/ 2, () => log.add('1'));
Timer(elapseBy ~/ 2, () => log.add('2'));
async.elapse(elapseBy);
expect(log, ['1', '2']);
});
});
test('should maintain FIFO order even with periodic timers', () {
FakeAsync().run((async) {
final log = <String>[];
Timer.periodic(elapseBy ~/ 2, (_) => log.add('periodic 1'));
Timer(elapseBy ~/ 2, () => log.add('delayed 1'));
Timer(elapseBy, () => log.add('delayed 2'));
Timer.periodic(elapseBy, (_) => log.add('periodic 2'));
async.elapse(elapseBy);
expect(log, [
'periodic 1',
'delayed 1',
'periodic 1',
'delayed 2',
'periodic 2'
]);
});
});
test('should process microtasks surrounding each timer', () {
FakeAsync().run((async) {
var microtaskCalls = 0;
var timerCalls = 0;
void scheduleMicrotasks() {
for (var i = 0; i < 5; i++) {
scheduleMicrotask(() => microtaskCalls++);
}
}
scheduleMicrotasks();
Timer.periodic(elapseBy ~/ 5, (_) {
timerCalls++;
expect(microtaskCalls, 5 * timerCalls);
scheduleMicrotasks();
});
async.elapse(elapseBy);
expect(timerCalls, 5);
expect(microtaskCalls, 5 * (timerCalls + 1));
});
});
test('should pass the periodic timer itself to callbacks', () {
FakeAsync().run((async) {
late Timer constructed;
constructed = Timer.periodic(elapseBy, expectAsync1((passed) {
expect(passed, same(constructed));
}));
async.elapse(elapseBy);
});
});
test('should call microtasks before advancing time', () {
FakeAsync().run((async) {
scheduleMicrotask(expectAsync0(() {
expect(async.elapsed, Duration.zero);
}));
async.elapse(const Duration(minutes: 1));
});
});
test('should add event before advancing time', () {
FakeAsync().run((async) {
final controller = StreamController<void>();
expect(controller.stream.first.then((_) {
expect(async.elapsed, Duration.zero);
}), completes);
controller.add(null);
async.elapse(const Duration(minutes: 1));
});
});
test('should increase negative duration timers to zero duration', () {
FakeAsync().run((async) {
final negativeDuration = const Duration(days: -1);
Timer(negativeDuration, expectAsync0(() {
expect(async.elapsed, Duration.zero);
}));
async.elapse(const Duration(minutes: 1));
});
});
test('should not be additive with elapseBlocking', () {
FakeAsync().run((async) {
Timer(Duration.zero, () => async.elapseBlocking(elapseBy * 5));
async.elapse(elapseBy);
expect(async.elapsed, elapseBy * 5);
});
});
group('isActive', () {
test('should be false after timer is run', () {
FakeAsync().run((async) {
final timer = Timer(elapseBy ~/ 2, () {});
async.elapse(elapseBy);
expect(timer.isActive, isFalse);
});
});
test('should be true after periodic timer is run', () {
FakeAsync().run((async) {
final timer = Timer.periodic(elapseBy ~/ 2, (_) {});
async.elapse(elapseBy);
expect(timer.isActive, isTrue);
});
});
test('should be false after timer is canceled', () {
FakeAsync().run((async) {
final timer = Timer(elapseBy ~/ 2, () {})..cancel();
expect(timer.isActive, isFalse);
});
});
});
test('should work with new Future()', () {
FakeAsync().run((async) {
Future(expectAsync0(() {}));
async.elapse(Duration.zero);
});
});
test('should work with Future.delayed', () {
FakeAsync().run((async) {
Future.delayed(elapseBy, expectAsync0(() {}));
async.elapse(elapseBy);
});
});
test('should work with Future.timeout', () {
FakeAsync().run((async) {
final completer = Completer<void>();
expect(completer.future.timeout(elapseBy ~/ 2),
throwsA(const TypeMatcher<TimeoutException>()));
async.elapse(elapseBy);
completer.complete();
});
});
// TODO: Pausing and resuming the timeout Stream doesn't work since
// it uses `new Stopwatch()`.
//
// See https://code.google.com/p/dart/issues/detail?id=18149
test('should work with Stream.periodic', () {
FakeAsync().run((async) {
expect(Stream.periodic(const Duration(minutes: 1), (i) => i),
emitsInOrder([0, 1, 2]));
async.elapse(const Duration(minutes: 3));
});
});
test('should work with Stream.timeout', () {
FakeAsync().run((async) {
final controller = StreamController<int>();
final timed = controller.stream.timeout(const Duration(minutes: 2));
final events = <int>[];
final errors = <Object>[];
timed.listen(events.add, onError: errors.add);
controller.add(0);
async.elapse(const Duration(minutes: 1));
expect(events, [0]);
async.elapse(const Duration(minutes: 1));
expect(errors, hasLength(1));
expect(errors.first, const TypeMatcher<TimeoutException>());
});
});
});
});
group('flushMicrotasks', () {
test('should flush a microtask', () {
FakeAsync().run((async) {
Future.microtask(expectAsync0(() {}));
async.flushMicrotasks();
});
});
test('should flush microtasks scheduled by microtasks in order', () {
FakeAsync().run((async) {
final log = <int>[];
scheduleMicrotask(() {
log.add(1);
scheduleMicrotask(() => log.add(3));
});
scheduleMicrotask(() => log.add(2));
async.flushMicrotasks();
expect(log, [1, 2, 3]);
});
});
test('should not run timers', () {
FakeAsync().run((async) {
final log = <int>[];
scheduleMicrotask(() => log.add(1));
Timer.run(() => log.add(2));
Timer.periodic(const Duration(seconds: 1), (_) => log.add(2));
async.flushMicrotasks();
expect(log, [1]);
});
});
});
group('flushTimers', () {
test('should flush timers in FIFO order', () {
FakeAsync().run((async) {
final log = <int>[];
Timer.run(() {
log.add(1);
Timer(elapseBy, () => log.add(3));
});
Timer.run(() => log.add(2));
async.flushTimers(timeout: elapseBy * 2);
expect(log, [1, 2, 3]);
expect(async.elapsed, elapseBy);
});
});
test(
'should run collateral periodic timers with non-periodic first if '
'scheduled first', () {
FakeAsync().run((async) {
final log = <String>[];
Timer(const Duration(seconds: 2), () => log.add('delayed'));
Timer.periodic(const Duration(seconds: 1), (_) => log.add('periodic'));
async.flushTimers(flushPeriodicTimers: false);
expect(log, ['periodic', 'delayed', 'periodic']);
});
});
test(
'should run collateral periodic timers with periodic first '
'if scheduled first', () {
FakeAsync().run((async) {
final log = <String>[];
Timer.periodic(const Duration(seconds: 1), (_) => log.add('periodic'));
Timer(const Duration(seconds: 2), () => log.add('delayed'));
async.flushTimers(flushPeriodicTimers: false);
expect(log, ['periodic', 'periodic', 'delayed']);
});
});
test('should time out', () {
FakeAsync().run((async) {
// Schedule 3 timers. All but the last one should fire.
for (var delay in [30, 60, 90]) {
Timer(Duration(minutes: delay),
expectAsync0(() {}, count: delay == 90 ? 0 : 1));
}
expect(() => async.flushTimers(), throwsStateError);
});
});
test('should time out a chain of timers', () {
FakeAsync().run((async) {
var count = 0;
void createTimer() {
Timer(const Duration(minutes: 30), () {
count++;
createTimer();
});
}
createTimer();
expect(() => async.flushTimers(timeout: const Duration(hours: 2)),
throwsStateError);
expect(count, 4);
});
});
test('should time out periodic timers', () {
FakeAsync().run((async) {
Timer.periodic(
const Duration(minutes: 30), expectAsync1((_) {}, count: 2));
expect(() => async.flushTimers(timeout: const Duration(hours: 1)),
throwsStateError);
});
});
test('should flush periodic timers', () {
FakeAsync().run((async) {
var count = 0;
Timer.periodic(const Duration(minutes: 30), (timer) {
if (count == 3) timer.cancel();
count++;
});
async.flushTimers(timeout: const Duration(hours: 20));
expect(count, 4);
});
});
test('should compute absolute timeout as elapsed + timeout', () {
FakeAsync().run((async) {
var count = 0;
void createTimer() {
Timer(const Duration(minutes: 30), () {
count++;
if (count < 4) createTimer();
});
}
createTimer();
async
..elapse(const Duration(hours: 1))
..flushTimers(timeout: const Duration(hours: 1));
expect(count, 4);
});
});
});
group('stats', () {
test('should report the number of pending microtasks', () {
FakeAsync().run((async) {
expect(async.microtaskCount, 0);
scheduleMicrotask(() {});
expect(async.microtaskCount, 1);
scheduleMicrotask(() {});
expect(async.microtaskCount, 2);
async.flushMicrotasks();
expect(async.microtaskCount, 0);
});
});
test('it should report the number of pending periodic timers', () {
FakeAsync().run((async) {
expect(async.periodicTimerCount, 0);
final timer = Timer.periodic(const Duration(minutes: 30), (_) {});
expect(async.periodicTimerCount, 1);
Timer.periodic(const Duration(minutes: 20), (_) {});
expect(async.periodicTimerCount, 2);
async.elapse(const Duration(minutes: 20));
expect(async.periodicTimerCount, 2);
timer.cancel();
expect(async.periodicTimerCount, 1);
});
});
test('it should report the number of pending non periodic timers', () {
FakeAsync().run((async) {
expect(async.nonPeriodicTimerCount, 0);
final timer = Timer(const Duration(minutes: 30), () {});
expect(async.nonPeriodicTimerCount, 1);
Timer(const Duration(minutes: 20), () {});
expect(async.nonPeriodicTimerCount, 2);
async.elapse(const Duration(minutes: 25));
expect(async.nonPeriodicTimerCount, 1);
timer.cancel();
expect(async.nonPeriodicTimerCount, 0);
});
});
test('should report debugging information of pending timers', () {
FakeAsync().run((fakeAsync) {
expect(fakeAsync.pendingTimers, isEmpty);
final nonPeriodic =
Timer(const Duration(seconds: 1), () {}) as FakeTimer;
final periodic =
Timer.periodic(const Duration(seconds: 2), (Timer timer) {})
as FakeTimer;
final debugInfo = fakeAsync.pendingTimers;
expect(debugInfo.length, 2);
expect(
debugInfo,
containsAll([
nonPeriodic,
periodic,
]),
);
const thisFileName = 'fake_async_test.dart';
expect(nonPeriodic.debugString, contains(':01.0'));
expect(nonPeriodic.debugString, contains('periodic: false'));
expect(nonPeriodic.debugString, contains(thisFileName));
expect(periodic.debugString, contains(':02.0'));
expect(periodic.debugString, contains('periodic: true'));
expect(periodic.debugString, contains(thisFileName));
});
});
test(
'should report debugging information of pending timers excluding '
'stack traces', () {
FakeAsync(includeTimerStackTrace: false).run((fakeAsync) {
expect(fakeAsync.pendingTimers, isEmpty);
final nonPeriodic =
Timer(const Duration(seconds: 1), () {}) as FakeTimer;
final periodic =
Timer.periodic(const Duration(seconds: 2), (Timer timer) {})
as FakeTimer;
final debugInfo = fakeAsync.pendingTimers;
expect(debugInfo.length, 2);
expect(
debugInfo,
containsAll([
nonPeriodic,
periodic,
]),
);
const thisFileName = 'fake_async_test.dart';
expect(nonPeriodic.debugString, contains(':01.0'));
expect(nonPeriodic.debugString, contains('periodic: false'));
expect(nonPeriodic.debugString, isNot(contains(thisFileName)));
expect(periodic.debugString, contains(':02.0'));
expect(periodic.debugString, contains('periodic: true'));
expect(periodic.debugString, isNot(contains(thisFileName)));
});
});
});
group('timers', () {
test("should become inactive as soon as they're invoked", () {
return FakeAsync().run((async) {
late Timer timer;
timer = Timer(elapseBy, expectAsync0(() {
expect(timer.isActive, isFalse);
}));
expect(timer.isActive, isTrue);
async.elapse(elapseBy);
expect(timer.isActive, isFalse);
});
});
test('should increment tick in a non-periodic timer', () {
return FakeAsync().run((async) {
late Timer timer;
timer = Timer(elapseBy, expectAsync0(() {
expect(timer.tick, 1);
}));
expect(timer.tick, 0);
async.elapse(elapseBy);
});
});
test('should increment tick in a periodic timer', () {
return FakeAsync().run((async) {
final ticks = <int>[];
Timer.periodic(
elapseBy,
expectAsync1((timer) {
ticks.add(timer.tick);
}, count: 2));
async
..elapse(elapseBy)
..elapse(elapseBy);
expect(ticks, [1, 2]);
});
});
test('should update periodic timer state before invoking callback', () {
// Regression test for: https://github.com/dart-lang/fake_async/issues/88
FakeAsync().run((async) {
final log = <String>[];
Timer.periodic(const Duration(seconds: 2), (timer) {
log.add('periodic ${timer.tick}');
async.elapse(Duration.zero);
});
Timer(const Duration(seconds: 3), () {
log.add('single');
});
async.flushTimers(flushPeriodicTimers: false);
expect(log, ['periodic 1', 'single']);
});
});
});
group('clock', () {
test('updates following elapse()', () {
FakeAsync().run((async) {
final before = clock.now();
async.elapse(elapseBy);
expect(clock.now(), before.add(elapseBy));
});
});
test('updates following elapseBlocking()', () {
FakeAsync().run((async) {
final before = clock.now();
async.elapseBlocking(elapseBy);
expect(clock.now(), before.add(elapseBy));
});
});
group('starts at', () {
test('the time at which the FakeAsync was created', () {
final start = DateTime.now();
FakeAsync().run((async) {
expect(clock.now(), _closeToTime(start));
async.elapse(elapseBy);
expect(clock.now(), _closeToTime(start.add(elapseBy)));
});
});
test('the value of clock.now()', () {
final start = DateTime(1990, 8, 11);
withClock(Clock.fixed(start), () {
FakeAsync().run((async) {
expect(clock.now(), start);
async.elapse(elapseBy);
expect(clock.now(), start.add(elapseBy));
});
});
});
test('an explicit value', () {
final start = DateTime(1990, 8, 11);
FakeAsync(initialTime: start).run((async) {
expect(clock.now(), start);
async.elapse(elapseBy);
expect(clock.now(), start.add(elapseBy));
});
});
});
});
}
/// Returns a matcher that asserts that a [DateTime] is within 100ms of
/// [expected].
Matcher _closeToTime(DateTime expected) => predicate(
(actual) =>
expected.difference(actual as DateTime).inMilliseconds.abs() < 100,
'is close to $expected');