From f518a5fac35872044b03ffe5c0890238a0ed1814 Mon Sep 17 00:00:00 2001 From: juliemr Date: Mon, 24 Apr 2017 15:32:21 -0700 Subject: [PATCH] Add a `flush` method to the FakeAsync zone spec The `flush` method advances time until all non-periodic timers are cleared from the queue. It has a configurable limit (default 20) of tasks that will be run before it gives up and throws an error, in order to avoid infinite loops when polling timeouts are present. Closes #735 --- lib/zone-spec/fake-async-test.ts | 54 +++++++++- test/zone-spec/fake-async-test.spec.ts | 142 +++++++++++++++++++++++++ 2 files changed, 191 insertions(+), 5 deletions(-) diff --git a/lib/zone-spec/fake-async-test.ts b/lib/zone-spec/fake-async-test.ts index e165d5b01..aefc773e4 100644 --- a/lib/zone-spec/fake-async-test.ts +++ b/lib/zone-spec/fake-async-test.ts @@ -13,6 +13,7 @@ func: Function; args: any[]; delay: number; + isPeriodic: boolean; } class Scheduler { @@ -26,13 +27,21 @@ constructor() {} - scheduleFunction(cb: Function, delay: number, args: any[] = [], id: number = -1): number { + scheduleFunction( + cb: Function, delay: number, args: any[] = [], isPeriodic: boolean = false, + id: number = -1): number { let currentId: number = id < 0 ? this.nextId++ : id; let endTime = this._currentTime + delay; // Insert so that scheduler queue remains sorted by end time. - let newEntry: - ScheduledFunction = {endTime: endTime, id: currentId, func: cb, args: args, delay: delay}; + let newEntry: ScheduledFunction = { + endTime: endTime, + id: currentId, + func: cb, + args: args, + delay: delay, + isPeriodic: isPeriodic + }; let i = 0; for (; i < this._schedulerQueue.length; i++) { let currentEntry = this._schedulerQueue[i]; @@ -73,6 +82,31 @@ } this._currentTime = finalTime; } + + flush(limit: number = 20): number { + const startTime = this._currentTime; + let count = 0; + while (this._schedulerQueue.length > 0) { + count++; + if (count > limit) { + throw new Error( + 'flush failed after reaching the limit of ' + limit + + ' tasks. Does your code use a polling timeout?'); + } + // If the only remaining tasks are periodic, finish flushing. + if (!(this._schedulerQueue.filter(task => !task.isPeriodic).length)) { + break; + } + let current = this._schedulerQueue.shift(); + this._currentTime = current.endTime; + let retval = current.func.apply(global, current.args); + if (!retval) { + // Uncaught exception in the current scheduled function. Stop processing the queue. + break; + } + } + return this._currentTime - startTime; + } } class FakeAsyncTestZoneSpec implements ZoneSpec { @@ -134,7 +168,7 @@ return () => { // Requeue the timer callback if it's not been canceled. if (this.pendingPeriodicTimers.indexOf(id) !== -1) { - this._scheduler.scheduleFunction(fn, interval, args, id); + this._scheduler.scheduleFunction(fn, interval, args, true, id); } }; } @@ -168,7 +202,7 @@ completers.onSuccess = this._requeuePeriodicTimer(cb, interval, args, id); // Queue the callback and dequeue the periodic timer only on error. - this._scheduler.scheduleFunction(cb, interval, args); + this._scheduler.scheduleFunction(cb, interval, args, true); this.pendingPeriodicTimers.push(id); return id; } @@ -209,6 +243,16 @@ flushErrors(); } + flush(): number { + FakeAsyncTestZoneSpec.assertInZone(); + this.flushMicrotasks(); + let elapsed = this._scheduler.flush(); + if (this._lastError !== null) { + this._resetLastErrorAndThrow(); + } + return elapsed; + } + // ZoneSpec implementation below. name: string; diff --git a/test/zone-spec/fake-async-test.spec.ts b/test/zone-spec/fake-async-test.spec.ts index 4d509bb75..cbbd5fd90 100644 --- a/test/zone-spec/fake-async-test.spec.ts +++ b/test/zone-spec/fake-async-test.spec.ts @@ -353,6 +353,148 @@ describe('FakeAsyncTestZoneSpec', () => { expect(testZoneSpec.pendingTimers.length).toBe(0); }); + describe('flushing all tasks', () => { + it('should flush all pending timers', () => { + fakeAsyncTestZone.run(() => { + let x = false; + let y = false; + let z = false; + + setTimeout(() => { + x = true; + }, 10); + setTimeout(() => { + y = true; + }, 100); + setTimeout(() => { + z = true; + }, 70); + + let elapsed = testZoneSpec.flush(); + + expect(elapsed).toEqual(100); + expect(x).toBe(true); + expect(y).toBe(true); + expect(z).toBe(true); + }); + }); + + it('should flush nested timers', () => { + fakeAsyncTestZone.run(() => { + let x = true; + let y = true; + setTimeout(() => { + x = true; + setTimeout(() => { + y = true; + }, 100); + }, 200); + + let elapsed = testZoneSpec.flush(); + + expect(elapsed).toEqual(300); + expect(x).toBe(true); + expect(y).toBe(true); + }); + }); + + it('should advance intervals', () => { + fakeAsyncTestZone.run(() => { + let x = false; + let y = false; + let z = 0; + + setTimeout(() => { + x = true; + }, 50); + setTimeout(() => { + y = true; + }, 141); + setInterval(() => { + z++; + }, 10); + + let elapsed = testZoneSpec.flush(); + + expect(elapsed).toEqual(141); + expect(x).toBe(true); + expect(y).toBe(true); + expect(z).toEqual(14); + }); + }); + + it('should not wait for intervals', () => { + fakeAsyncTestZone.run(() => { + let z = 0; + + setInterval(() => { + z++; + }, 10); + + let elapsed = testZoneSpec.flush(); + + expect(elapsed).toEqual(0); + expect(z).toEqual(0); + }); + }); + + + it('should process micro-tasks created in timers before next timers', () => { + fakeAsyncTestZone.run(() => { + let log: string[] = []; + + Promise.resolve(null).then((_) => log.push('microtask')); + + setTimeout(() => { + log.push('timer'); + Promise.resolve(null).then((_) => log.push('t microtask')); + }, 20); + + let id = setInterval(() => { + log.push('periodic timer'); + Promise.resolve(null).then((_) => log.push('pt microtask')); + }, 10); + + testZoneSpec.flush(); + expect(log).toEqual( + ['microtask', 'periodic timer', 'pt microtask', 'timer', 't microtask']); + }); + }); + + it('should throw the exception from tick for error thrown in timer callback', () => { + fakeAsyncTestZone.run(() => { + setTimeout(() => { + throw new Error('timer'); + }, 10); + expect(() => { + testZoneSpec.flush(); + }).toThrowError('timer'); + }); + // There should be no pending timers after the error in timer callback. + expect(testZoneSpec.pendingTimers.length).toBe(0); + }); + + it('should do something reasonable with polling timeouts', () => { + expect(() => { + fakeAsyncTestZone.run(() => { + let z = 0; + + let poll = () => { + setTimeout(() => { + z++; + poll(); + }, 10); + }; + + poll(); + testZoneSpec.flush(); + }); + }) + .toThrowError( + 'flush failed after reaching the limit of 20 tasks. Does your code use a polling timeout?'); + }); + }); + describe('outside of FakeAsync Zone', () => { it('calling flushMicrotasks should throw exception', () => { expect(() => {