Skip to content

Commit

Permalink
Merge pull request #237 from dominykas/async-upgrade-2019
Browse files Browse the repository at this point in the history
Upgrade lolex with async versions of all timer-executing calls (2019)
  • Loading branch information
fatso83 committed Oct 14, 2019
2 parents c03fb3e + d2fc2ca commit 5c0f0f8
Show file tree
Hide file tree
Showing 4 changed files with 1,722 additions and 54 deletions.
20 changes: 16 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,31 +224,40 @@ Only available in Node.js, mimics `process.nextTick` to enable completely synchr
Only available in browser environments, mimicks performance.now().


### `clock.tick(time)`
### `clock.tick(time)` / `await clock.tickAsync(time)`

Advance the clock, firing callbacks if necessary. `time` may be the number of
milliseconds to advance the clock by or a human-readable string. Valid string
formats are `"08"` for eight seconds, `"01:00"` for one minute and `"02:34:10"`
for two hours, 34 minutes and ten seconds.

### `clock.next()`
The `tickAsync()` will also break the event loop, allowing any scheduled promise
callbacks to execute _before_ running the timers.

### `clock.next()` / `await clock.nextAsync()`

Advances the clock to the the moment of the first scheduled timer, firing it.

The `nextAsync()` will also break the event loop, allowing any scheduled promise
callbacks to execute _before_ running the timers.

### `clock.reset()`

Removes all timers and ticks without firing them, and sets `now` to `config.now`
that was provided to `lolex.install` or to `0` if `config.now` was not provided.
Useful to reset the state of the clock without having to `uninstall` and `install` it.

### `clock.runAll()`
### `clock.runAll()` / `await clock.runAllAsync()`

This runs all pending timers until there are none remaining. If new timers are added while it is executing they will be run as well.

This makes it easier to run asynchronous tests to completion without worrying about the number of timers they use, or the delays in those timers.

It runs a maximum of `loopLimit` times after which it assumes there is an infinite loop of timers and throws an error.

The `runAllAsync()` will also break the event loop, allowing any scheduled promise
callbacks to execute _before_ running the timers.

### `clock.runMicrotasks()`

This runs all pending microtasks scheduled with `nextTick` but none of the timers and is mostly useful for libraries using lolex underneath and for running `nextTick` items without any timers.
Expand All @@ -258,7 +267,7 @@ This runs all pending microtasks scheduled with `nextTick` but none of the timer
Advances the clock to the next frame, firing all scheduled animation frame callbacks,
if any, for that frame as well as any other timers scheduled along the way.

### `clock.runToLast()`
### `clock.runToLast()` / `await clock.runToLastAsync()`

This takes note of the last scheduled timer when it is run, and advances the
clock to that time firing callbacks as necessary.
Expand All @@ -269,6 +278,9 @@ would occur before this time.
This is useful when you want to run a test to completion, but the test recursively
sets timers that would cause `runAll` to trigger an infinite loop warning.

The `runToLastAsync()` will also break the event loop, allowing any scheduled promise
callbacks to execute _before_ running the timers.

### `clock.setSystemTime([now])`

This simulates a user changing the system clock while your program is running.
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
"scripts": {
"lint": "eslint .",
"test-node": "mocha test/ integration-test/ -R dot --check-leaks",
"test-headless": "mochify --no-detect-globals",
"test-cloud": "mochify --wd --no-detect-globals",
"test-headless": "mochify --no-detect-globals --timeout=10000",
"test-cloud": "mochify --wd --no-detect-globals --timeout=10000",
"test": "npm run lint && npm run test-node && npm run test-headless",
"bundle": "browserify --no-detect-globals -s lolex -o lolex.js src/lolex-src.js",
"prepublishOnly": "npm run bundle",
Expand Down
260 changes: 212 additions & 48 deletions src/lolex-src.js
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,8 @@ function withGlobal(_global) {
return ks;
};

var originalSetTimeout = _global.setImmediate || _global.setTimeout;

/**
* @param start {Date|number} the system time - non-integer values are floored
* @param loopLimit {number} maximum number of timers that will be run when calling runAll()
Expand Down Expand Up @@ -810,10 +812,7 @@ function withGlobal(_global) {
runJobs(clock);
};

/**
* @param {tickValue} {String|Number} number of milliseconds or a human-readable value like "01:11:15"
*/
clock.tick = function tick(tickValue) {
function doTick(tickValue, isAsync, resolve, reject) {
var msFloat =
typeof tickValue === "number"
? tickValue
Expand All @@ -836,7 +835,12 @@ function withGlobal(_global) {
nanos = nanosTotal;
var tickFrom = clock.now;
var previous = clock.now;
var timer, firstException, oldNow;
var timer,
firstException,
oldNow,
nextPromiseTick,
compensationCheck,
postTimerCall;

clock.duringTick = true;

Expand All @@ -849,63 +853,122 @@ function withGlobal(_global) {
tickTo += clock.now - oldNow;
}

// perform each timer in the requested range
timer = firstTimerInRange(clock, tickFrom, tickTo);
while (timer && tickFrom <= tickTo) {
if (clock.timers[timer.id]) {
tickFrom = timer.callAt;
clock.now = timer.callAt;
oldNow = clock.now;
function doTickInner() {
// perform each timer in the requested range
timer = firstTimerInRange(clock, tickFrom, tickTo);
// eslint-disable-next-line no-unmodified-loop-condition
while (timer && tickFrom <= tickTo) {
if (clock.timers[timer.id]) {
tickFrom = timer.callAt;
clock.now = timer.callAt;
oldNow = clock.now;
try {
runJobs(clock);
callTimer(clock, timer);
} catch (e) {
firstException = firstException || e;
}

if (isAsync) {
// finish up after native setImmediate callback to allow
// all native es6 promises to process their callbacks after
// each timer fires.
originalSetTimeout(nextPromiseTick);
return;
}

compensationCheck();
}

postTimerCall();
}

// perform process.nextTick()s again
oldNow = clock.now;
runJobs(clock);
if (oldNow !== clock.now) {
// compensate for any setSystemTime() call during process.nextTick() callback
tickFrom += clock.now - oldNow;
tickTo += clock.now - oldNow;
}
clock.duringTick = false;

// corner case: during runJobs new timers were scheduled which could be in the range [clock.now, tickTo]
timer = firstTimerInRange(clock, tickFrom, tickTo);
if (timer) {
try {
runJobs(clock);
callTimer(clock, timer);
clock.tick(tickTo - clock.now); // do it all again - for the remainder of the requested range
} catch (e) {
firstException = firstException || e;
}
} else {
// no timers remaining in the requested range: move the clock all the way to the end
clock.now = tickTo;

// update nanos
nanos = nanosTotal;
}
if (firstException) {
throw firstException;
}

if (isAsync) {
resolve(clock.now);
} else {
return clock.now;
}
}

// compensate for any setSystemTime() call during timer callback
if (oldNow !== clock.now) {
tickFrom += clock.now - oldNow;
tickTo += clock.now - oldNow;
previous += clock.now - oldNow;
nextPromiseTick =
isAsync &&
function() {
try {
compensationCheck();
postTimerCall();
doTickInner();
} catch (e) {
reject(e);
}
};

compensationCheck = function() {
// compensate for any setSystemTime() call during timer callback
if (oldNow !== clock.now) {
tickFrom += clock.now - oldNow;
tickTo += clock.now - oldNow;
previous += clock.now - oldNow;
}
};

postTimerCall = function() {
timer = firstTimerInRange(clock, previous, tickTo);
previous = tickFrom;
}
};

// perform process.nextTick()s again
oldNow = clock.now;
runJobs(clock);
if (oldNow !== clock.now) {
// compensate for any setSystemTime() call during process.nextTick() callback
tickFrom += clock.now - oldNow;
tickTo += clock.now - oldNow;
}
clock.duringTick = false;

// corner case: during runJobs, new timers were scheduled which could be in the range [clock.now, tickTo]
timer = firstTimerInRange(clock, tickFrom, tickTo);
if (timer) {
try {
clock.tick(tickTo - clock.now); // do it all again - for the remainder of the requested range
} catch (e) {
firstException = firstException || e;
}
} else {
// no timers remaining in the requested range: move the clock all the way to the end
clock.now = tickTo;
return doTickInner();
}

// update nanos
nanos = nanosTotal;
}
if (firstException) {
throw firstException;
}
return clock.now;
/**
* @param {tickValue} {String|Number} number of milliseconds or a human-readable value like "01:11:15"
*/
clock.tick = function tick(tickValue) {
return doTick(tickValue, false);
};

if (typeof global.Promise !== "undefined") {
clock.tickAsync = function tickAsync(ms) {
return new global.Promise(function(resolve, reject) {
originalSetTimeout(function() {
try {
doTick(ms, true, resolve, reject);
} catch (e) {
reject(e);
}
});
});
};
}

clock.next = function next() {
runJobs(clock);
var timer = firstTimer(clock);
Expand All @@ -924,6 +987,42 @@ function withGlobal(_global) {
}
};

if (typeof global.Promise !== "undefined") {
clock.nextAsync = function nextAsync() {
return new global.Promise(function(resolve, reject) {
originalSetTimeout(function() {
try {
var timer = firstTimer(clock);
if (!timer) {
resolve(clock.now);
return;
}

var err;
clock.duringTick = true;
clock.now = timer.callAt;
try {
callTimer(clock, timer);
} catch (e) {
err = e;
}
clock.duringTick = false;

originalSetTimeout(function() {
if (err) {
reject(err);
} else {
resolve(clock.now);
}
});
} catch (e) {
reject(e);
}
});
});
};
}

clock.runAll = function runAll() {
var numTimers, i;
runJobs(clock);
Expand Down Expand Up @@ -951,6 +1050,52 @@ function withGlobal(_global) {
return clock.tick(getTimeToNextFrame());
};

if (typeof global.Promise !== "undefined") {
clock.runAllAsync = function runAllAsync() {
return new global.Promise(function(resolve, reject) {
var i = 0;
function doRun() {
originalSetTimeout(function() {
try {
var numTimers;
if (i < clock.loopLimit) {
if (!clock.timers) {
resolve(clock.now);
return;
}

numTimers = Object.keys(clock.timers)
.length;
if (numTimers === 0) {
resolve(clock.now);
return;
}

clock.next();

i++;

doRun();
return;
}

reject(
new Error(
"Aborting after running " +
clock.loopLimit +
" timers, assuming an infinite loop!"
)
);
} catch (e) {
reject(e);
}
});
}
doRun();
});
};
}

clock.runToLast = function runToLast() {
var timer = lastTimer(clock);
if (!timer) {
Expand All @@ -961,6 +1106,25 @@ function withGlobal(_global) {
return clock.tick(timer.callAt - clock.now);
};

if (typeof global.Promise !== "undefined") {
clock.runToLastAsync = function runToLastAsync() {
return new global.Promise(function(resolve, reject) {
originalSetTimeout(function() {
try {
var timer = lastTimer(clock);
if (!timer) {
resolve(clock.now);
}

resolve(clock.tickAsync(timer.callAt));
} catch (e) {
reject(e);
}
});
});
};
}

clock.reset = function reset() {
nanos = 0;
clock.timers = {};
Expand Down

0 comments on commit 5c0f0f8

Please sign in to comment.