| // 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 'dart:collection'; |
| |
| import 'package:clock/clock.dart'; |
| import 'package:collection/collection.dart'; |
| |
| /// The type of a microtask callback. |
| typedef _Microtask = void Function(); |
| |
| /// Runs [callback] in a [Zone] where all asynchrony is controlled by an |
| /// instance of [FakeAsync]. |
| /// |
| /// All [Future]s, [Stream]s, [Timer]s, microtasks, and other time-based |
| /// asynchronous features used within [callback] are controlled by calls to |
| /// [FakeAsync.elapse] rather than the passing of real time. |
| /// |
| /// The [`clock`][] property will be set to a clock that reports the fake |
| /// elapsed time. By default, it starts at the time [fakeAsync] was created |
| /// (according to [`clock.now()`][]), but this can be controlled by passing |
| /// [initialTime]. |
| /// |
| /// [`clock`]: https://www.dartdocs.org/documentation/clock/latest/clock/clock.html |
| /// [`clock.now()`]: https://www.dartdocs.org/documentation/clock/latest/clock/Clock/now.html |
| /// |
| /// Returns the result of [callback]. |
| T fakeAsync<T>(T Function(FakeAsync async) callback, {DateTime? initialTime}) => |
| FakeAsync(initialTime: initialTime).run(callback); |
| |
| /// A class that mocks out the passage of time within a [Zone]. |
| /// |
| /// Test code can be passed as a callback to [run], which causes it to be run in |
| /// a [Zone] which fakes timer and microtask creation, such that they are run |
| /// during calls to [elapse] which simulates the asynchronous passage of time. |
| /// |
| /// The synchronous passage of time (as from blocking or expensive calls) can |
| /// also be simulated using [elapseBlocking]. |
| class FakeAsync { |
| /// The value of [clock] within [run]. |
| late final Clock _clock; |
| |
| /// The amount of fake time that's elapsed since this [FakeAsync] was |
| /// created. |
| Duration get elapsed => _elapsed; |
| var _elapsed = Duration.zero; |
| |
| /// The fake time at which the current call to [elapse] will finish running. |
| /// |
| /// This is `null` if there's no current call to [elapse]. |
| Duration? _elapsingTo; |
| |
| /// Tasks that are scheduled to run when fake time progresses. |
| final _microtasks = Queue<_Microtask>(); |
| |
| /// All timers created within [run]. |
| final _timers = <FakeTimer>{}; |
| |
| /// All the current pending timers. |
| List<FakeTimer> get pendingTimers => _timers.toList(growable: false); |
| |
| /// The debug strings for all the current pending timers. |
| List<String> get pendingTimersDebugString => |
| pendingTimers.map((timer) => timer.debugString).toList(growable: false); |
| |
| /// The number of active periodic timers created within a call to [run] or |
| /// [fakeAsync]. |
| int get periodicTimerCount => |
| _timers.where((timer) => timer.isPeriodic).length; |
| |
| /// The number of active non-periodic timers created within a call to [run] or |
| /// [fakeAsync]. |
| int get nonPeriodicTimerCount => |
| _timers.where((timer) => !timer.isPeriodic).length; |
| |
| /// The number of pending microtasks scheduled within a call to [run] or |
| /// [fakeAsync]. |
| int get microtaskCount => _microtasks.length; |
| |
| /// Creates a [FakeAsync]. |
| /// |
| /// Within [run], the [`clock`][] property will start at [initialTime] and |
| /// move forward as fake time elapses. |
| /// |
| /// [`clock`]: https://www.dartdocs.org/documentation/clock/latest/clock/clock.html |
| /// |
| /// Note: it's usually more convenient to use [fakeAsync] rather than creating |
| /// a [FakeAsync] object and calling [run] manually. |
| FakeAsync({DateTime? initialTime}) { |
| var nonNullInitialTime = initialTime ?? clock.now(); |
| _clock = Clock(() => nonNullInitialTime.add(elapsed)); |
| } |
| |
| /// Returns a fake [Clock] whose time can is elapsed by calls to [elapse] and |
| /// [elapseBlocking]. |
| /// |
| /// The returned clock starts at [initialTime] plus the fake time that's |
| /// already been elapsed. Further calls to [elapse] and [elapseBlocking] will |
| /// advance the clock as well. |
| /// |
| /// Note that it's usually easier to use the top-level [`clock`][] property. |
| /// Only call this function if you want a different [initialTime] than the |
| /// default. |
| /// |
| /// [`clock`]: https://www.dartdocs.org/documentation/clock/latest/clock/clock.html |
| Clock getClock(DateTime initialTime) => |
| Clock(() => initialTime.add(_elapsed)); |
| |
| /// Simulates the asynchronous passage of time. |
| /// |
| /// Throws an [ArgumentError] if [duration] is negative. Throws a [StateError] |
| /// if a previous call to [elapse] has not yet completed. |
| /// |
| /// Any timers created within [run] or [fakeAsync] will fire if their time is |
| /// within [duration]. The microtask queue is processed before and after each |
| /// timer fires. |
| void elapse(Duration duration) { |
| if (duration.inMicroseconds < 0) { |
| throw ArgumentError.value(duration, 'duration', 'may not be negative'); |
| } else if (_elapsingTo != null) { |
| throw StateError('Cannot elapse until previous elapse is complete.'); |
| } |
| |
| _elapsingTo = _elapsed + duration; |
| _fireTimersWhile((next) => next._nextCall <= _elapsingTo!); |
| _elapseTo(_elapsingTo!); |
| _elapsingTo = null; |
| } |
| |
| /// Simulates the synchronous passage of time, resulting from blocking or |
| /// expensive calls. |
| /// |
| /// Neither timers nor microtasks are run during this call, but if this is |
| /// called within [elapse] they may fire afterwards. |
| /// |
| /// Throws an [ArgumentError] if [duration] is negative. |
| void elapseBlocking(Duration duration) { |
| if (duration.inMicroseconds < 0) { |
| throw ArgumentError('Cannot call elapse with negative duration'); |
| } |
| |
| _elapsed += duration; |
| var elapsingTo = _elapsingTo; |
| if (elapsingTo != null && _elapsed > elapsingTo) _elapsingTo = _elapsed; |
| } |
| |
| /// Runs [callback] in a [Zone] where all asynchrony is controlled by `this`. |
| /// |
| /// All [Future]s, [Stream]s, [Timer]s, microtasks, and other time-based |
| /// asynchronous features used within [callback] are controlled by calls to |
| /// [elapse] rather than the passing of real time. |
| /// |
| /// The [`clock`][] property will be set to a clock that reports the fake |
| /// elapsed time. By default, it starts at the time the [FakeAsync] was |
| /// created (according to [`clock.now()`][]), but this can be controlled by |
| /// passing `initialTime` to [new FakeAsync]. |
| /// |
| /// [`clock`]: https://www.dartdocs.org/documentation/clock/latest/clock/clock.html |
| /// [`clock.now()`]: https://www.dartdocs.org/documentation/clock/latest/clock/Clock/now.html |
| /// |
| /// Calls [callback] with `this` as argument and returns its result. |
| /// |
| /// Note: it's usually more convenient to use [fakeAsync] rather than creating |
| /// a [FakeAsync] object and calling [run] manually. |
| T run<T>(T Function(FakeAsync self) callback) => |
| runZoned(() => withClock(_clock, () => callback(this)), |
| zoneSpecification: ZoneSpecification( |
| createTimer: (_, __, ___, duration, callback) => |
| _createTimer(duration, callback, false), |
| createPeriodicTimer: (_, __, ___, duration, callback) => |
| _createTimer(duration, callback, true), |
| scheduleMicrotask: (_, __, ___, microtask) => |
| _microtasks.add(microtask))); |
| |
| /// Runs all pending microtasks scheduled within a call to [run] or |
| /// [fakeAsync] until there are no more microtasks scheduled. |
| /// |
| /// Does not run timers. |
| void flushMicrotasks() { |
| while (_microtasks.isNotEmpty) { |
| _microtasks.removeFirst()(); |
| } |
| } |
| |
| /// Elapses time until there are no more active timers. |
| /// |
| /// If `flushPeriodicTimers` is `true` (the default), this will repeatedly run |
| /// periodic timers until they're explicitly canceled. Otherwise, this will |
| /// stop when the only active timers are periodic. |
| /// |
| /// The [timeout] controls how much fake time may elapse before a [StateError] |
| /// is thrown. This ensures that a periodic timer doesn't cause this method to |
| /// deadlock. It defaults to one hour. |
| void flushTimers( |
| {Duration timeout = const Duration(hours: 1), |
| bool flushPeriodicTimers = true}) { |
| var absoluteTimeout = _elapsed + timeout; |
| _fireTimersWhile((timer) { |
| if (timer._nextCall > absoluteTimeout) { |
| // TODO(nweiz): Make this a [TimeoutException]. |
| throw StateError('Exceeded timeout $timeout while flushing timers'); |
| } |
| |
| if (flushPeriodicTimers) return _timers.isNotEmpty; |
| |
| // 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); |
| }); |
| } |
| |
| /// Invoke the callback for each timer until [predicate] returns `false` for |
| /// the next timer that would be fired. |
| /// |
| /// 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; |
| |
| var timer = minBy(_timers, (FakeTimer timer) => timer._nextCall)!; |
| if (!predicate(timer)) break; |
| |
| _elapseTo(timer._nextCall); |
| timer._fire(); |
| flushMicrotasks(); |
| } |
| } |
| |
| /// Creates a new timer controlled by `this` that fires [callback] after |
| /// [duration] (or every [duration] if [periodic] is `true`). |
| Timer _createTimer(Duration duration, Function callback, bool periodic) { |
| var timer = FakeTimer._(duration, callback, periodic, this); |
| _timers.add(timer); |
| return timer; |
| } |
| |
| /// Sets [_elapsed] to [to] if [to] is longer than [_elapsed]. |
| void _elapseTo(Duration to) { |
| if (to > _elapsed) _elapsed = to; |
| } |
| } |
| |
| /// An implementation of [Timer] that's controlled by a [FakeAsync]. |
| class FakeTimer implements Timer { |
| /// If this is periodic, the time that should elapse between firings of this |
| /// timer. |
| /// |
| /// This is not used by non-periodic timers. |
| final Duration duration; |
| |
| /// The callback to invoke when the timer fires. |
| /// |
| /// For periodic timers, this is a `void Function(Timer)`. For non-periodic |
| /// timers, it's a `void Function()`. |
| final Function _callback; |
| |
| /// Whether this is a periodic timer. |
| final bool isPeriodic; |
| |
| /// The [FakeAsync] instance that controls this timer. |
| final FakeAsync _async; |
| |
| /// The value of [FakeAsync._elapsed] at (or after) which this timer should be |
| /// fired. |
| late Duration _nextCall; |
| |
| /// The current stack trace when this timer was created. |
| final creationStackTrace = StackTrace.current; |
| |
| @override |
| int get tick { |
| throw UnimplementedError('tick'); |
| } |
| |
| /// Returns debugging information to try to identify the source of the |
| /// [Timer]. |
| String get debugString => |
| 'Timer (duration: $duration, periodic: $isPeriodic), created:\n' |
| '$creationStackTrace'; |
| |
| FakeTimer._(Duration duration, this._callback, this.isPeriodic, this._async) |
| : duration = duration < Duration.zero ? Duration.zero : duration { |
| _nextCall = _async._elapsed + this.duration; |
| } |
| |
| @override |
| bool get isActive => _async._timers.contains(this); |
| |
| @override |
| void cancel() => _async._timers.remove(this); |
| |
| /// Fires this timer's callback and updates its state as necessary. |
| void _fire() { |
| assert(isActive); |
| if (isPeriodic) { |
| _callback(this); |
| _nextCall += duration; |
| } else { |
| cancel(); |
| _callback(); |
| } |
| } |
| } |