| OLD | NEW |
| (Empty) |
| 1 // Copyright 2014 Google Inc. All Rights Reserved. | |
| 2 // | |
| 3 // Licensed under the Apache License, Version 2.0 (the "License"); | |
| 4 // you may not use this file except in compliance with the License. | |
| 5 // You may obtain a copy of the License at | |
| 6 // | |
| 7 // http://www.apache.org/licenses/LICENSE-2.0 | |
| 8 // | |
| 9 // Unless required by applicable law or agreed to in writing, software | |
| 10 // distributed under the License is distributed on an "AS IS" BASIS, | |
| 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 12 // See the License for the specific language governing permissions and | |
| 13 // limitations under the License. | |
| 14 | |
| 15 part of quiver.testing.async; | |
| 16 | |
| 17 /// A mechanism to make time-dependent units testable. | |
| 18 /// | |
| 19 /// Test code can be passed as a callback to [run], which causes it to be run in | |
| 20 /// a [Zone] which fakes timer and microtask creation, such that they are run | |
| 21 /// during calls to [elapse] which simulates the asynchronous passage of time. | |
| 22 /// | |
| 23 /// The synchronous passage of time (blocking or expensive calls) can also be | |
| 24 /// simulated using [elapseBlocking]. | |
| 25 /// | |
| 26 /// To allow the unit under test to tell time, it can receive a [Clock] as a | |
| 27 /// dependency, and default it to [const Clock()] in production, but then use | |
| 28 /// [clock] in test code. | |
| 29 /// | |
| 30 /// Example: | |
| 31 /// | |
| 32 /// test('testedFunc', () { | |
| 33 /// new FakeAsync().run((async) { | |
| 34 /// testedFunc(clock: async.getClock(initialTime)); | |
| 35 /// async.elapse(duration); | |
| 36 /// expect(...) | |
| 37 /// }); | |
| 38 /// }); | |
| 39 abstract class FakeAsync { | |
| 40 factory FakeAsync() = _FakeAsync; | |
| 41 | |
| 42 FakeAsync._(); | |
| 43 | |
| 44 /// Returns a fake [Clock] whose time can is elapsed by calls to [elapse] and | |
| 45 /// [elapseBlocking]. | |
| 46 /// | |
| 47 /// The returned clock starts at [initialTime], and calls to [elapse] and | |
| 48 /// [elapseBlocking] advance the clock, even if they occured before the call | |
| 49 /// to this method. | |
| 50 /// | |
| 51 /// The clock can be passed as a dependency to the unit under test. | |
| 52 Clock getClock(DateTime initialTime); | |
| 53 | |
| 54 /// Simulates the asynchronous passage of time. | |
| 55 /// | |
| 56 /// **This should only be called from within the zone used by [run].** | |
| 57 /// | |
| 58 /// If [duration] is negative, the returned future completes with an | |
| 59 /// [ArgumentError]. | |
| 60 /// | |
| 61 /// If a previous call to [elapse] has not yet completed, throws a | |
| 62 /// [StateError]. | |
| 63 /// | |
| 64 /// Any Timers created within the zone used by [run] which are to expire | |
| 65 /// at or before the new time after [duration] has elapsed are run. | |
| 66 /// The microtask queue is processed surrounding each timer. When a timer is | |
| 67 /// run, the [clock] will have been advanced by the timer's specified | |
| 68 /// duration. Calls to [elapseBlocking] from within these timers and | |
| 69 /// microtasks which cause the [clock] to elapse more than the specified | |
| 70 /// [duration], can cause more timers to expire and thus be called. | |
| 71 /// | |
| 72 /// Once all expired timers are processed, the [clock] is advanced (if | |
| 73 /// necessary) to the time this method was called + [duration]. | |
| 74 void elapse(Duration duration); | |
| 75 | |
| 76 /// Simulates the synchronous passage of time, resulting from blocking or | |
| 77 /// expensive calls. | |
| 78 /// | |
| 79 /// Neither timers nor microtasks are run during this call. Upon return, the | |
| 80 /// [clock] will have been advanced by [duration]. | |
| 81 /// | |
| 82 /// If [duration] is negative, throws an [ArgumentError]. | |
| 83 void elapseBlocking(Duration duration); | |
| 84 | |
| 85 /// Runs [callback] in a [Zone] with fake timer and microtask scheduling. | |
| 86 /// | |
| 87 /// Uses | |
| 88 /// [ZoneSpecification.createTimer], [ZoneSpecification.createPeriodicTimer], | |
| 89 /// and [ZoneSpecification.scheduleMicrotask] to store callbacks for later | |
| 90 /// execution within the zone via calls to [elapse]. | |
| 91 /// | |
| 92 /// [callback] is called with `this` as argument. | |
| 93 run(callback(FakeAsync self)); | |
| 94 | |
| 95 /// Runs all remaining microtasks, including those scheduled as a result of | |
| 96 /// running them, until there are no more microtasks scheduled. | |
| 97 /// | |
| 98 /// Does not run timers. | |
| 99 void flushMicrotasks(); | |
| 100 | |
| 101 /// Runs all timers until no timers remain (subject to [flushPeriodicTimers] | |
| 102 /// option), including those scheduled as a result of running them. | |
| 103 /// | |
| 104 /// [timeout] lets you set the maximum amount of time the flushing will take. | |
| 105 /// Throws a [StateError] if the [timeout] is exceeded. The default timeout | |
| 106 /// is 1 hour. [timeout] is relative to the elapsed time. | |
| 107 void flushTimers({Duration timeout: const Duration(hours: 1), | |
| 108 bool flushPeriodicTimers: true}); | |
| 109 | |
| 110 /// The number of created periodic timers that have not been canceled. | |
| 111 int get periodicTimerCount; | |
| 112 | |
| 113 /// The number of pending non periodic timers that have not been canceled. | |
| 114 int get nonPeriodicTimerCount; | |
| 115 | |
| 116 /// The number of pending microtasks. | |
| 117 int get microtaskCount; | |
| 118 } | |
| 119 | |
| 120 class _FakeAsync extends FakeAsync { | |
| 121 Duration _elapsed = Duration.ZERO; | |
| 122 Duration _elapsingTo; | |
| 123 Queue<Function> _microtasks = new Queue(); | |
| 124 Set<_FakeTimer> _timers = new Set<_FakeTimer>(); | |
| 125 | |
| 126 _FakeAsync() : super._() { | |
| 127 _elapsed; | |
| 128 } | |
| 129 | |
| 130 @override | |
| 131 Clock getClock(DateTime initialTime) => | |
| 132 new Clock(() => initialTime.add(_elapsed)); | |
| 133 | |
| 134 @override | |
| 135 void elapse(Duration duration) { | |
| 136 if (duration.inMicroseconds < 0) { | |
| 137 throw new ArgumentError('Cannot call elapse with negative duration'); | |
| 138 } | |
| 139 if (_elapsingTo != null) { | |
| 140 throw new StateError('Cannot elapse until previous elapse is complete.'); | |
| 141 } | |
| 142 _elapsingTo = _elapsed + duration; | |
| 143 _drainTimersWhile((_FakeTimer next) => next._nextCall <= _elapsingTo); | |
| 144 _elapseTo(_elapsingTo); | |
| 145 _elapsingTo = null; | |
| 146 } | |
| 147 | |
| 148 @override | |
| 149 void elapseBlocking(Duration duration) { | |
| 150 if (duration.inMicroseconds < 0) { | |
| 151 throw new ArgumentError('Cannot call elapse with negative duration'); | |
| 152 } | |
| 153 _elapsed += duration; | |
| 154 if (_elapsingTo != null && _elapsed > _elapsingTo) { | |
| 155 _elapsingTo = _elapsed; | |
| 156 } | |
| 157 } | |
| 158 | |
| 159 @override | |
| 160 void flushMicrotasks() { | |
| 161 _drainMicrotasks(); | |
| 162 } | |
| 163 | |
| 164 @override | |
| 165 void flushTimers({Duration timeout: const Duration(hours: 1), | |
| 166 bool flushPeriodicTimers: true}) { | |
| 167 final absoluteTimeout = _elapsed + timeout; | |
| 168 _drainTimersWhile((_FakeTimer timer) { | |
| 169 if (timer._nextCall > absoluteTimeout) { | |
| 170 throw new StateError( | |
| 171 'Exceeded timeout ${timeout} while flushing timers'); | |
| 172 } | |
| 173 if (flushPeriodicTimers) { | |
| 174 return _timers.isNotEmpty; | |
| 175 } else { | |
| 176 // translation: keep draining while non-periodic timers exist | |
| 177 return _timers.any((_FakeTimer timer) => !timer._isPeriodic); | |
| 178 } | |
| 179 }); | |
| 180 } | |
| 181 | |
| 182 @override | |
| 183 run(callback(FakeAsync self)) { | |
| 184 if (_zone == null) { | |
| 185 _zone = Zone.current.fork(specification: _zoneSpec); | |
| 186 } | |
| 187 return _zone.runGuarded(() => callback(this)); | |
| 188 } | |
| 189 Zone _zone; | |
| 190 | |
| 191 @override | |
| 192 int get periodicTimerCount => | |
| 193 _timers.where((_FakeTimer timer) => timer._isPeriodic).length; | |
| 194 | |
| 195 @override | |
| 196 int get nonPeriodicTimerCount => | |
| 197 _timers.where((_FakeTimer timer) => !timer._isPeriodic).length; | |
| 198 | |
| 199 @override | |
| 200 int get microtaskCount => _microtasks.length; | |
| 201 | |
| 202 ZoneSpecification get _zoneSpec => new ZoneSpecification( | |
| 203 createTimer: (_, __, ___, Duration duration, Function callback) { | |
| 204 return _createTimer(duration, callback, false); | |
| 205 }, createPeriodicTimer: (_, __, ___, Duration duration, Function callback) { | |
| 206 return _createTimer(duration, callback, true); | |
| 207 }, scheduleMicrotask: (_, __, ___, Function microtask) { | |
| 208 _microtasks.add(microtask); | |
| 209 }); | |
| 210 | |
| 211 _drainTimersWhile(bool predicate(_FakeTimer)) { | |
| 212 _drainMicrotasks(); | |
| 213 _FakeTimer next; | |
| 214 while ((next = _getNextTimer()) != null && predicate(next)) { | |
| 215 _runTimer(next); | |
| 216 _drainMicrotasks(); | |
| 217 } | |
| 218 } | |
| 219 | |
| 220 _elapseTo(Duration to) { | |
| 221 if (to > _elapsed) { | |
| 222 _elapsed = to; | |
| 223 } | |
| 224 } | |
| 225 | |
| 226 Timer _createTimer(Duration duration, Function callback, bool isPeriodic) { | |
| 227 var timer = new _FakeTimer._(duration, callback, isPeriodic, this); | |
| 228 _timers.add(timer); | |
| 229 return timer; | |
| 230 } | |
| 231 | |
| 232 _FakeTimer _getNextTimer() { | |
| 233 return min(_timers, | |
| 234 (timer1, timer2) => timer1._nextCall.compareTo(timer2._nextCall)); | |
| 235 } | |
| 236 | |
| 237 _runTimer(_FakeTimer timer) { | |
| 238 assert(timer.isActive); | |
| 239 _elapseTo(timer._nextCall); | |
| 240 if (timer._isPeriodic) { | |
| 241 timer._callback(timer); | |
| 242 timer._nextCall += timer._duration; | |
| 243 } else { | |
| 244 timer._callback(); | |
| 245 _timers.remove(timer); | |
| 246 } | |
| 247 } | |
| 248 | |
| 249 _drainMicrotasks() { | |
| 250 while (_microtasks.isNotEmpty) { | |
| 251 _microtasks.removeFirst()(); | |
| 252 } | |
| 253 } | |
| 254 | |
| 255 _hasTimer(_FakeTimer timer) => _timers.contains(timer); | |
| 256 | |
| 257 _cancelTimer(_FakeTimer timer) => _timers.remove(timer); | |
| 258 } | |
| 259 | |
| 260 class _FakeTimer implements Timer { | |
| 261 final Duration _duration; | |
| 262 final Function _callback; | |
| 263 final bool _isPeriodic; | |
| 264 final _FakeAsync _time; | |
| 265 Duration _nextCall; | |
| 266 | |
| 267 // TODO: In browser JavaScript, timers can only run every 4 milliseconds once | |
| 268 // sufficiently nested: | |
| 269 // http://www.w3.org/TR/html5/webappapis.html#timer-nesting-level | |
| 270 // Without some sort of delay this can lead to infinitely looping timers. | |
| 271 // What do the dart VM and dart2js timers do here? | |
| 272 static const _minDuration = Duration.ZERO; | |
| 273 | |
| 274 _FakeTimer._(Duration duration, this._callback, this._isPeriodic, this._time) | |
| 275 : _duration = duration < _minDuration ? _minDuration : duration { | |
| 276 _nextCall = _time._elapsed + _duration; | |
| 277 } | |
| 278 | |
| 279 bool get isActive => _time._hasTimer(this); | |
| 280 | |
| 281 cancel() => _time._cancelTimer(this); | |
| 282 } | |
| OLD | NEW |