// 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 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; /// Whether Timers created by this FakeAsync will include a creation stack /// trace in [FakeAsync.pendingTimersDebugString]. final bool includeTimerStackTrace; /// 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 = {}; /// All the current pending timers. List get pendingTimers => _timers.toList(growable: false); /// The debug strings for all the current pending timers. List 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, this.includeTimerStackTrace = true}) { final 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.value(duration, 'duration', 'Must not be negative'); } _elapsed += duration; final 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 [FakeAsync.new]. /// /// [`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 Function(FakeAsync self) callback) => runZoned( () => withClock(_clock, () => callback(this)), zoneSpecification: ZoneSpecification( createTimer: (_, __, zone, duration, callback) => _createTimer(duration, zone, callback, false), createPeriodicTimer: (_, __, zone, duration, callback) => _createTimer(duration, zone, callback, true), scheduleMicrotask: (_, __, zone, microtask) => _microtasks.add(() { zone.runGuarded(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, }) { final absoluteTimeout = _elapsed + timeout; _fireTimersWhile((timer) { if (timer._nextCall > absoluteTimeout) { // TODO(nweiz): Make this a [TimeoutException]. throw StateError('Exceeded timeout $timeout while flushing timers'); } // Always run timer if it's due. if (timer._nextCall <= elapsed) return true; // If no timers are due, continue running timers // (and advancing time to their next due time) // if flushing periodic timers, // or if there is any non-periodic timer left. return flushPeriodicTimers || _timers.any((timer) => !timer.isPeriodic); }); } /// 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(); while (_timers.isNotEmpty) { final 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, Zone zone, Function callback, bool periodic, ) { final timer = FakeTimer._( duration, zone, callback, periodic, this, includeStackTrace: includeTimerStackTrace, ); _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]. /// /// Periodic timers attempt to be isochronous. They trigger as soon as possible /// after a multiple of the [duration] has passed since they started, /// independently of when prior callbacks actually ran. /// This behavior matches VM timers. /// /// Repeating web timers instead reschedule themselves a [duration] after /// their last callback ended, which shifts the timing both if a callback /// is delayed or if it runs for a long time. In return it guarantees /// that there is always at least [duration] between two callbacks. class FakeTimer implements Timer { /// The zone to run the callback in. final Zone _zone; /// 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. Duration _nextCall; /// The current stack trace when this timer was created. /// /// If [FakeAsync.includeTimerStackTrace] is set to false then accessing /// this field will throw a [TypeError]. StackTrace get creationStackTrace => _creationStackTrace!; final StackTrace? _creationStackTrace; var _tick = 0; @override int get tick => _tick; /// Returns debugging information to try to identify the source of the /// [Timer]. String get debugString => 'Timer (duration: $duration, periodic: $isPeriodic)' '${_creationStackTrace != null ? ', created:\n$creationStackTrace' : ''}'; FakeTimer._( Duration duration, this._zone, this._callback, this.isPeriodic, this._async, { bool includeStackTrace = true, }) : duration = duration < Duration.zero ? (duration = Duration.zero) : duration, _nextCall = _async._elapsed + duration, _creationStackTrace = includeStackTrace ? StackTrace.current : null; @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); assert(_nextCall <= _async._elapsed); _tick++; if (isPeriodic) { _nextCall += duration; while (_nextCall < _async._elapsed) { _tick++; _nextCall += duration; } _zone.runUnaryGuarded(_callback as void Function(Timer), this); } else { cancel(); _zone.runGuarded(_callback as void Function()); } } }