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 |