@@ -152,7 +152,7 @@ class FakeAsync {
152
152
/// Throws an [ArgumentError] if [duration] is negative.
153
153
void elapseBlocking (Duration duration) {
154
154
if (duration.inMicroseconds < 0 ) {
155
- throw ArgumentError ( 'Cannot call elapse with negative duration ' );
155
+ throw ArgumentError . value (duration, 'duration' , 'Must not be negative' );
156
156
}
157
157
158
158
_elapsed += duration;
@@ -178,15 +178,18 @@ class FakeAsync {
178
178
///
179
179
/// Note: it's usually more convenient to use [fakeAsync] rather than creating
180
180
/// a [FakeAsync] object and calling [run] manually.
181
- T run <T >(T Function (FakeAsync self) callback) =>
182
- runZoned (() => withClock (_clock, () => callback (this )),
183
- zoneSpecification: ZoneSpecification (
184
- createTimer: (_, __, ___, duration, callback) =>
185
- _createTimer (duration, callback, false ),
186
- createPeriodicTimer: (_, __, ___, duration, callback) =>
187
- _createTimer (duration, callback, true ),
188
- scheduleMicrotask: (_, __, ___, microtask) =>
189
- _microtasks.add (microtask)));
181
+ T run <T >(T Function (FakeAsync self) callback) => runZoned (
182
+ () => withClock (_clock, () => callback (this )),
183
+ zoneSpecification: ZoneSpecification (
184
+ createTimer: (_, __, zone, duration, callback) =>
185
+ _createTimer (duration, zone, callback, false ),
186
+ createPeriodicTimer: (_, __, zone, duration, callback) =>
187
+ _createTimer (duration, zone, callback, true ),
188
+ scheduleMicrotask: (_, __, zone, microtask) => _microtasks.add (() {
189
+ zone.runGuarded (microtask);
190
+ }),
191
+ ),
192
+ );
190
193
191
194
/// Runs all pending microtasks scheduled within a call to [run] or
192
195
/// [fakeAsync] until there are no more microtasks scheduled.
@@ -207,23 +210,25 @@ class FakeAsync {
207
210
/// The [timeout] controls how much fake time may elapse before a [StateError]
208
211
/// is thrown. This ensures that a periodic timer doesn't cause this method to
209
212
/// deadlock. It defaults to one hour.
210
- void flushTimers (
211
- {Duration timeout = const Duration (hours: 1 ),
212
- bool flushPeriodicTimers = true }) {
213
+ void flushTimers ({
214
+ Duration timeout = const Duration (hours: 1 ),
215
+ bool flushPeriodicTimers = true ,
216
+ }) {
213
217
final absoluteTimeout = _elapsed + timeout;
214
218
_fireTimersWhile ((timer) {
215
219
if (timer._nextCall > absoluteTimeout) {
216
220
// TODO(nweiz): Make this a [TimeoutException].
217
221
throw StateError ('Exceeded timeout $timeout while flushing timers' );
218
222
}
219
223
220
- if (flushPeriodicTimers) return _timers.isNotEmpty;
224
+ // Always run timer if it's due.
225
+ if (timer._nextCall <= elapsed) return true ;
221
226
222
- // Continue firing timers until the only ones left are periodic *and*
223
- // every periodic timer has had a change to run against the final
224
- // value of [_elapsed].
225
- return _timers
226
- .any ((timer) => ! timer.isPeriodic || timer._nextCall <= _elapsed );
227
+ // If no timers are due, continue running timers
228
+ // (and advancing time to their next due time)
229
+ // if flushing periodic timers,
230
+ // or if there is any non-periodic timer left.
231
+ return flushPeriodicTimers || _timers .any ((timer) => ! timer.isPeriodic);
227
232
});
228
233
}
229
234
@@ -234,9 +239,7 @@ class FakeAsync {
234
239
/// timer fires, [_elapsed] is updated to the appropriate duration.
235
240
void _fireTimersWhile (bool Function (FakeTimer timer) predicate) {
236
241
flushMicrotasks ();
237
- for (;;) {
238
- if (_timers.isEmpty) break ;
239
-
242
+ while (_timers.isNotEmpty) {
240
243
final timer = minBy (_timers, (FakeTimer timer) => timer._nextCall)! ;
241
244
if (! predicate (timer)) break ;
242
245
@@ -248,9 +251,20 @@ class FakeAsync {
248
251
249
252
/// Creates a new timer controlled by `this` that fires [callback] after
250
253
/// [duration] (or every [duration] if [periodic] is `true` ).
251
- Timer _createTimer (Duration duration, Function callback, bool periodic) {
252
- final timer = FakeTimer ._(duration, callback, periodic, this ,
253
- includeStackTrace: includeTimerStackTrace);
254
+ Timer _createTimer (
255
+ Duration duration,
256
+ Zone zone,
257
+ Function callback,
258
+ bool periodic,
259
+ ) {
260
+ final timer = FakeTimer ._(
261
+ duration,
262
+ zone,
263
+ callback,
264
+ periodic,
265
+ this ,
266
+ includeStackTrace: includeTimerStackTrace,
267
+ );
254
268
_timers.add (timer);
255
269
return timer;
256
270
}
@@ -262,7 +276,20 @@ class FakeAsync {
262
276
}
263
277
264
278
/// An implementation of [Timer] that's controlled by a [FakeAsync] .
279
+ ///
280
+ /// Periodic timers attempt to be isochronous. They trigger as soon as possible
281
+ /// after a multiple of the [duration] has passed since they started,
282
+ /// independently of when prior callbacks actually ran.
283
+ /// This behavior matches VM timers.
284
+ ///
285
+ /// Repeating web timers instead reschedule themselves a [duration] after
286
+ /// their last callback ended, which shifts the timing both if a callback
287
+ /// is delayed or if it runs for a long time. In return it guarantees
288
+ /// that there is always at least [duration] between two callbacks.
265
289
class FakeTimer implements Timer {
290
+ /// The zone to run the callback in.
291
+ final Zone _zone;
292
+
266
293
/// If this is periodic, the time that should elapse between firings of this
267
294
/// timer.
268
295
///
@@ -283,7 +310,7 @@ class FakeTimer implements Timer {
283
310
284
311
/// The value of [FakeAsync._elapsed] at (or after) which this timer should be
285
312
/// fired.
286
- late Duration _nextCall;
313
+ Duration _nextCall;
287
314
288
315
/// The current stack trace when this timer was created.
289
316
///
@@ -302,12 +329,17 @@ class FakeTimer implements Timer {
302
329
String get debugString => 'Timer (duration: $duration , periodic: $isPeriodic )'
303
330
'${_creationStackTrace != null ? ', created:\n $creationStackTrace ' : '' }' ;
304
331
305
- FakeTimer ._(Duration duration, this ._callback, this .isPeriodic, this ._async,
306
- {bool includeStackTrace = true })
307
- : duration = duration < Duration .zero ? Duration .zero : duration,
308
- _creationStackTrace = includeStackTrace ? StackTrace .current : null {
309
- _nextCall = _async._elapsed + this .duration;
310
- }
332
+ FakeTimer ._(
333
+ Duration duration,
334
+ this ._zone,
335
+ this ._callback,
336
+ this .isPeriodic,
337
+ this ._async, {
338
+ bool includeStackTrace = true ,
339
+ }) : duration =
340
+ duration < Duration .zero ? (duration = Duration .zero) : duration,
341
+ _nextCall = _async._elapsed + duration,
342
+ _creationStackTrace = includeStackTrace ? StackTrace .current : null ;
311
343
312
344
@override
313
345
bool get isActive => _async._timers.contains (this );
@@ -318,15 +350,18 @@ class FakeTimer implements Timer {
318
350
/// Fires this timer's callback and updates its state as necessary.
319
351
void _fire () {
320
352
assert (isActive);
353
+ assert (_nextCall <= _async._elapsed);
321
354
_tick++ ;
322
355
if (isPeriodic) {
323
356
_nextCall += duration;
324
- // ignore: avoid_dynamic_calls
325
- _callback (this );
357
+ while (_nextCall < _async._elapsed) {
358
+ _tick++ ;
359
+ _nextCall += duration;
360
+ }
361
+ _zone.runUnaryGuarded (_callback as void Function (Timer ), this );
326
362
} else {
327
363
cancel ();
328
- // ignore: avoid_dynamic_calls
329
- _callback ();
364
+ _zone.runGuarded (_callback as void Function ());
330
365
}
331
366
}
332
367
}
0 commit comments