| // 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. |
| |
| part of quiver.testing.async; |
| |
| /// A mechanism to make time-dependent units testable. |
| /// |
| /// 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 (blocking or expensive calls) can also be |
| /// simulated using [elapseBlocking]. |
| /// |
| /// To allow the unit under test to tell time, it can receive a [Clock] as a |
| /// dependency, and default it to [const Clock()] in production, but then use |
| /// [clock] in test code. |
| /// |
| /// Example: |
| /// |
| /// test('testedFunc', () { |
| /// new FakeAsync().run((async) { |
| /// testedFunc(clock: async.getClock(initialTime)); |
| /// async.elapse(duration); |
| /// expect(...) |
| /// }); |
| /// }); |
| abstract class FakeAsync { |
| factory FakeAsync() = _FakeAsync; |
| |
| /// Returns a fake [Clock] whose time can is elapsed by calls to [elapse] and |
| /// [elapseBlocking]. |
| /// |
| /// The returned clock starts at [initialTime], and calls to [elapse] and |
| /// [elapseBlocking] advance the clock, even if they occured before the call |
| /// to this method. |
| /// |
| /// The clock can be passed as a dependency to the unit under test. |
| Clock getClock(DateTime initialTime); |
| |
| /// Simulates the asynchronous passage of time. |
| /// |
| /// **This should only be called from within the zone used by [run].** |
| /// |
| /// If [duration] is negative, the returned future completes with an |
| /// [ArgumentError]. |
| /// |
| /// If a previous call to [elapse] has not yet completed, throws a |
| /// [StateError]. |
| /// |
| /// Any Timers created within the zone used by [run] which are to expire |
| /// at or before the new time after [duration] has elapsed are run. |
| /// The microtask queue is processed surrounding each timer. When a timer is |
| /// run, the [clock] will have been advanced by the timer's specified |
| /// duration. Calls to [elapseBlocking] from within these timers and |
| /// microtasks which cause the [clock] to elapse more than the specified |
| /// [duration], can cause more timers to expire and thus be called. |
| /// |
| /// Once all expired timers are processed, the [clock] is advanced (if |
| /// necessary) to the time this method was called + [duration]. |
| void elapse(Duration duration); |
| |
| /// Simulates the synchronous passage of time, resulting from blocking or |
| /// expensive calls. |
| /// |
| /// Neither timers nor microtasks are run during this call. Upon return, the |
| /// [clock] will have been advanced by [duration]. |
| /// |
| /// If [duration] is negative, throws an [ArgumentError]. |
| void elapseBlocking(Duration duration); |
| |
| /// Runs [callback] in a [Zone] with fake timer and microtask scheduling. |
| /// |
| /// Uses |
| /// [ZoneSpecification.createTimer], [ZoneSpecification.createPeriodicTimer], |
| /// and [ZoneSpecification.scheduleMicrotask] to store callbacks for later |
| /// execution within the zone via calls to [elapse]. |
| /// |
| /// Calls [callback] with `this` as argument and returns the result returned |
| /// by [callback]. |
| dynamic run(callback(FakeAsync self)); |
| |
| /// Runs all remaining microtasks, including those scheduled as a result of |
| /// running them, until there are no more microtasks scheduled. |
| /// |
| /// Does not run timers. |
| void flushMicrotasks(); |
| |
| /// Runs all timers until no timers remain (subject to [flushPeriodicTimers] |
| /// option), including those scheduled as a result of running them. |
| /// |
| /// [timeout] lets you set the maximum amount of time the flushing will take. |
| /// Throws a [StateError] if the [timeout] is exceeded. The default timeout |
| /// is 1 hour. [timeout] is relative to the elapsed time. |
| void flushTimers( |
| {Duration timeout: const Duration(hours: 1), |
| bool flushPeriodicTimers: true}); |
| |
| /// The number of created periodic timers that have not been canceled. |
| int get periodicTimerCount; |
| |
| /// The number of pending non periodic timers that have not been canceled. |
| int get nonPeriodicTimerCount; |
| |
| /// The number of pending microtasks. |
| int get microtaskCount; |
| } |
| |
| class _FakeAsync implements FakeAsync { |
| Duration _elapsed = Duration.zero; |
| Duration _elapsingTo; |
| Queue<Function> _microtasks = new Queue(); |
| Set<_FakeTimer> _timers = new Set<_FakeTimer>(); |
| |
| @override |
| Clock getClock(DateTime initialTime) => |
| new Clock(() => initialTime.add(_elapsed)); |
| |
| @override |
| void elapse(Duration duration) { |
| if (duration.inMicroseconds < 0) { |
| throw new ArgumentError('Cannot call elapse with negative duration'); |
| } |
| if (_elapsingTo != null) { |
| throw new StateError('Cannot elapse until previous elapse is complete.'); |
| } |
| _elapsingTo = _elapsed + duration; |
| _drainTimersWhile((_FakeTimer next) => next._nextCall <= _elapsingTo); |
| _elapseTo(_elapsingTo); |
| _elapsingTo = null; |
| } |
| |
| @override |
| void elapseBlocking(Duration duration) { |
| if (duration.inMicroseconds < 0) { |
| throw new ArgumentError('Cannot call elapse with negative duration'); |
| } |
| _elapsed += duration; |
| if (_elapsingTo != null && _elapsed > _elapsingTo) { |
| _elapsingTo = _elapsed; |
| } |
| } |
| |
| @override |
| void flushMicrotasks() { |
| _drainMicrotasks(); |
| } |
| |
| @override |
| void flushTimers( |
| {Duration timeout: const Duration(hours: 1), |
| bool flushPeriodicTimers: true}) { |
| final absoluteTimeout = _elapsed + timeout; |
| _drainTimersWhile((_FakeTimer timer) { |
| if (timer._nextCall > absoluteTimeout) { |
| throw new StateError( |
| 'Exceeded timeout ${timeout} while flushing timers'); |
| } |
| if (flushPeriodicTimers) { |
| return _timers.isNotEmpty; |
| } else { |
| // translation: drain every timer (periodic or not) that will occur up |
| // until the latest non-periodic timer |
| return _timers.any((_FakeTimer timer) => |
| !timer._isPeriodic || timer._nextCall <= _elapsed); |
| } |
| }); |
| } |
| |
| @override |
| run(callback(FakeAsync self)) { |
| if (_zone == null) { |
| _zone = Zone.current.fork(specification: _zoneSpec); |
| } |
| var result; |
| _zone.runGuarded(() { |
| result = callback(this); |
| }); |
| return result; |
| } |
| |
| Zone _zone; |
| |
| @override |
| int get periodicTimerCount => |
| _timers.where((_FakeTimer timer) => timer._isPeriodic).length; |
| |
| @override |
| int get nonPeriodicTimerCount => |
| _timers.where((_FakeTimer timer) => !timer._isPeriodic).length; |
| |
| @override |
| int get microtaskCount => _microtasks.length; |
| |
| ZoneSpecification get _zoneSpec => new ZoneSpecification( |
| createTimer: (_, __, ___, Duration duration, Function callback) { |
| return _createTimer(duration, callback, false); |
| }, createPeriodicTimer: |
| (_, __, ___, Duration duration, Function callback) { |
| return _createTimer(duration, callback, true); |
| }, scheduleMicrotask: (_, __, ___, Function microtask) { |
| _microtasks.add(microtask); |
| }); |
| |
| _drainTimersWhile(bool predicate(_FakeTimer timer)) { |
| _drainMicrotasks(); |
| _FakeTimer next; |
| while ((next = _getNextTimer()) != null && predicate(next)) { |
| _runTimer(next); |
| _drainMicrotasks(); |
| } |
| } |
| |
| _elapseTo(Duration to) { |
| if (to > _elapsed) { |
| _elapsed = to; |
| } |
| } |
| |
| Timer _createTimer(Duration duration, Function callback, bool isPeriodic) { |
| var timer = new _FakeTimer._(duration, callback, isPeriodic, this); |
| _timers.add(timer); |
| return timer; |
| } |
| |
| _FakeTimer _getNextTimer() { |
| return _timers.isEmpty |
| ? null |
| : _timers.reduce((t1, t2) => t1._nextCall <= t2._nextCall ? t1 : t2); |
| } |
| |
| _runTimer(_FakeTimer timer) { |
| assert(timer.isActive); |
| _elapseTo(timer._nextCall); |
| if (timer._isPeriodic) { |
| timer._callback(timer); |
| timer._nextCall += timer._duration; |
| } else { |
| _timers.remove(timer); |
| timer._callback(); |
| } |
| } |
| |
| _drainMicrotasks() { |
| while (_microtasks.isNotEmpty) { |
| _microtasks.removeFirst()(); |
| } |
| } |
| |
| _hasTimer(_FakeTimer timer) => _timers.contains(timer); |
| |
| _cancelTimer(_FakeTimer timer) => _timers.remove(timer); |
| } |
| |
| class _FakeTimer implements Timer { |
| final Duration _duration; |
| final Function _callback; |
| final bool _isPeriodic; |
| final _FakeAsync _time; |
| Duration _nextCall; |
| |
| // TODO: In browser JavaScript, timers can only run every 4 milliseconds once |
| // sufficiently nested: |
| // http://www.w3.org/TR/html5/webappapis.html#timer-nesting-level |
| // Without some sort of delay this can lead to infinitely looping timers. |
| // What do the dart VM and dart2js timers do here? |
| static const _minDuration = Duration.zero; |
| |
| _FakeTimer._(Duration duration, this._callback, this._isPeriodic, this._time) |
| : _duration = duration < _minDuration ? _minDuration : duration { |
| _nextCall = _time._elapsed + _duration; |
| } |
| |
| bool get isActive => _time._hasTimer(this); |
| |
| cancel() => _time._cancelTimer(this); |
| |
| @override |
| // TODO: Dart 2.0 requires this method to be implemented. |
| // ignore: override_on_non_overriding_getter |
| int get tick { |
| throw new UnimplementedError("tick"); |
| } |
| } |