Add clearTimeout to Timer and MockTimer

This commit is contained in:
Christopher Willis-Ford 2019-02-11 12:14:44 -08:00
parent 46a1ae83a0
commit 5a17bb1451
5 changed files with 155 additions and 69 deletions

View file

@ -95,9 +95,19 @@ class Timer {
* Call a handler function after a specified amount of time has elapsed. * Call a handler function after a specified amount of time has elapsed.
* @param {function} handler - function to call after the timeout * @param {function} handler - function to call after the timeout
* @param {number} timeout - number of milliseconds to delay before calling the handler * @param {number} timeout - number of milliseconds to delay before calling the handler
* @returns {number} - the ID of the new timeout
*/ */
setTimeout (handler, timeout) { setTimeout (handler, timeout) {
global.setTimeout(handler, timeout); return global.setTimeout(handler, timeout);
}
/**
* Clear a timeout from the pending timeout pool.
* @param {number} timeoutId - the ID returned by `setTimeout()`
* @memberof Timer
*/
clearTimeout (timeoutId) {
global.clearTimeout(timeoutId);
} }
} }

View file

@ -28,10 +28,18 @@ class MockTimer {
this.startTime = 0; this.startTime = 0;
/** /**
* Array of pending timeout callbacks * The ID to use the next time `setTimeout` is called.
* @type {Array.<Object>} * @type {number}
*/ */
this._timeouts = []; this._nextTimeoutId = 1;
/**
* Map of timeout ID to pending timeout callback info.
* @type {Map.<Object>}
* @property {number} time - the time at/after which this handler should run
* @property {Function} handler - the handler to call when the time comes
*/
this._timeouts = new Map();
} }
/** /**
@ -85,14 +93,35 @@ class MockTimer {
* Guaranteed to happen in between "ticks" of JavaScript. * Guaranteed to happen in between "ticks" of JavaScript.
* @param {function} handler - function to call after the timeout * @param {function} handler - function to call after the timeout
* @param {number} timeout - number of milliseconds to delay before calling the handler * @param {number} timeout - number of milliseconds to delay before calling the handler
* @returns {number} - the ID of the new timeout.
* @memberof MockTimer * @memberof MockTimer
*/ */
setTimeout (handler, timeout) { setTimeout (handler, timeout) {
this._timeouts.push({ const timeoutId = this._nextTimeoutId++;
this._timeouts.set(timeoutId, {
time: this._mockTime + timeout, time: this._mockTime + timeout,
handler handler
}); });
this._runTimeouts(); this._runTimeouts();
return timeoutId;
}
/**
* Clear a particular timeout from the pending timeout pool.
* @param {number} timeoutId - the value returned from `setTimeout()`
* @memberof MockTimer
*/
clearTimeout (timeoutId) {
this._timeouts.delete(timeoutId);
}
/**
* WARNING: this method has no equivalent in `Timer`. Do not use this method outside of tests!
* @returns {boolean} - true if there are any pending timeouts, false otherwise.
* @memberof MockTimer
*/
hasTimeouts () {
return this._timeouts.size > 0;
} }
/** /**
@ -101,15 +130,17 @@ class MockTimer {
*/ */
_runTimeouts () { _runTimeouts () {
const ready = []; const ready = [];
const waiting = [];
// partition timeout records by whether or not they're ready to call this._timeouts.forEach((timeoutRecord, timeoutId) => {
this._timeouts.forEach(o => { const isReady = timeoutRecord.time <= this._mockTime;
const isReady = o.time <= this._mockTime; if (isReady) {
(isReady ? ready : waiting).push(o); ready.push(timeoutRecord);
this._timeouts.delete(timeoutId);
}
}); });
this._timeouts = waiting; // sort so that earlier timeouts run before later timeouts
ready.sort((a, b) => a.time < b.time);
// next tick, call everything that's ready // next tick, call everything that's ready
global.setTimeout(() => { global.setTimeout(() => {

89
test/unit/mock-timer.js Normal file
View file

@ -0,0 +1,89 @@
const test = require('tap').test;
const MockTimer = require('../fixtures/mock-timer');
test('spec', t => {
const timer = new MockTimer();
t.type(MockTimer, 'function');
t.type(timer, 'object');
// Most members of MockTimer mimic members of Timer.
t.type(timer.startTime, 'number');
t.type(timer.time, 'function');
t.type(timer.start, 'function');
t.type(timer.timeElapsed, 'function');
t.type(timer.setTimeout, 'function');
t.type(timer.clearTimeout, 'function');
// A few members of MockTimer have no Timer equivalent and should only be used in tests.
t.type(timer.hasTimeouts, 'function');
t.end();
});
test('time', t => {
const timer = new MockTimer();
const delta = 1;
const time1 = timer.time();
const time2 = timer.time();
timer.advanceMockTime(delta);
const time3 = timer.time();
t.equal(time1, time2);
t.equal(time2 + delta, time3);
t.end();
});
test('start / timeElapsed', t => new Promise(resolve => {
const timer = new MockTimer();
const halfDelay = 1;
const fullDelay = halfDelay + halfDelay;
timer.start();
let timeoutCalled = 0;
// Wait and measure timer
timer.setTimeout(() => {
t.equal(timeoutCalled, 0);
++timeoutCalled;
const timeElapsed = timer.timeElapsed();
t.equal(timeElapsed, fullDelay);
t.end();
resolve();
}, fullDelay);
// this should not trigger the callback
timer.advanceMockTime(halfDelay);
// give the mock timer a chance to run tasks
global.setTimeout(() => {
// we've only mock-waited for half the delay so it should not have run yet
t.equal(timeoutCalled, 0);
// this should trigger the callback
timer.advanceMockTime(halfDelay);
}, 0);
}));
test('clearTimeout / hasTimeouts', t => new Promise((resolve, reject) => {
const timer = new MockTimer();
const timeoutId = timer.setTimeout(() => {
reject(new Error('Canceled task ran'));
}, 1);
timer.setTimeout(() => {
resolve('Non-canceled task ran');
t.end();
}, 2);
timer.clearTimeout(timeoutId);
while (timer.hasTimeouts()) {
timer.advanceMockTime(1);
}
}));

View file

@ -1,58 +0,0 @@
const test = require('tap').test;
const MockTimer = require('../fixtures/mock-timer');
test('spec', t => {
const timer = new MockTimer();
t.type(MockTimer, 'function');
t.type(timer, 'object');
t.type(timer.startTime, 'number');
t.type(timer.time, 'function');
t.type(timer.start, 'function');
t.type(timer.timeElapsed, 'function');
t.end();
});
test('time', t => {
const timer = new MockTimer();
const time1 = timer.time();
const time2 = timer.time();
timer.advanceMockTime(1);
const time3 = timer.time();
t.ok(time1 === time2);
t.ok(time2 < time3);
t.end();
});
test('start / timeElapsed', t => {
const timer = new MockTimer();
const halfDelay = 50;
const fullDelay = halfDelay + halfDelay;
timer.start();
let timeoutCalled = 0;
// Wait and measure timer
timer.setTimeout(() => {
t.ok(timeoutCalled === 0);
++timeoutCalled;
const timeElapsed = timer.timeElapsed();
t.ok(timeElapsed === fullDelay);
t.end();
}, fullDelay);
// this should not call the callback
timer.advanceMockTime(halfDelay);
t.ok(timeoutCalled === 0);
// this should call the callback
timer.advanceMockTime(halfDelay);
});

View file

@ -11,6 +11,8 @@ test('spec', t => {
t.type(timer.time, 'function'); t.type(timer.time, 'function');
t.type(timer.start, 'function'); t.type(timer.start, 'function');
t.type(timer.timeElapsed, 'function'); t.type(timer.timeElapsed, 'function');
t.type(timer.setTimeout, 'function');
t.type(timer.clearTimeout, 'function');
t.end(); t.end();
}); });
@ -40,3 +42,15 @@ test('start / timeElapsed', t => {
t.end(); t.end();
}, delay); }, delay);
}); });
test('setTimeout / clearTimeout', t => new Promise((resolve, reject) => {
const timer = new Timer();
const cancelId = timer.setTimeout(() => {
reject(new Error('Canceled task ran'));
}, 1);
timer.setTimeout(() => {
resolve('Non-canceled task ran');
t.end();
}, 2);
timer.clearTimeout(cancelId);
}));