Repository: sinonjs/lolex
Pull request #45 (2016-03-23 13:52:32)

Add runAll method to run timers until empty
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.

fatso83: Seems like a useful addition. Not super sure about the telescoping method signatures, but I think it's alright for now. Merging.
fatso83: Ah, damn. I cannot merge this as long as the status checks are failing. 

@mantoni : did we not conclude that running the tests on PRs would fail in the SauceLabs config? If so, could you somehow disable SauceLabs for PRs? 
mantoni: @fatso83 Yes, [I did this for Sinon](https://github.com/sinonjs/sinon/commit/66123fea53b8fe43cb3acba9ded7c3a115c8ba2a) but not yet for Lolex. You can quickly do the same change here, or I can do it later tonight.
fatso83: @mantoni Thanks for the link. Made PR #46 now to address it. Will probably just merge it myself once the checks complete.
mantoni: @karl Can you please rebase to pull in the fix to get the build passing?
karl: @mantoni Done!

Patch:
diff --git a/Readme.md b/Readme.md
index ddbca5c..ab2e334 100644
--- a/Readme.md
+++ b/Readme.md
@@ -97,15 +97,18 @@ without arguments.
 
 ## API Reference
 
-### `var clock = lolex.createClock([now])`
+### `var clock = lolex.createClock([now[, loopLimit]])`
 
 Creates a clock. The default
-[epoch](https://en.wikipedia.org/wiki/Epoch_%28reference_date%29) is `0`. Now
-may be a number (in milliseconds) or a Date object.
+[epoch](https://en.wikipedia.org/wiki/Epoch_%28reference_date%29) is `0`.
 
-### `var clock = lolex.install([context[, now[, toFake]]])`
+The `now` argument may be a number (in milliseconds) or a Date object.
 
-### `var clock = lolex.install([now[, toFake]])`
+The `loopLimit` argument sets the maximum number of timers that will be run when calling `runAll()` before assuming that we have an infinite loop and throwing an error. The default is `1000`.
+
+### `var clock = lolex.install([context[, now[, toFake[, loopLimit]]]])`
+
+### `var clock = lolex.install([now[, toFake[, loopLimit]]])`
 
 Creates a clock and installs it onto the `context` object, or globally. The
 `now` argument is the same as in `lolex.createClock()`.
@@ -115,6 +118,8 @@ pick from `setTimeout`, `clearTimeout`, `setImmediate`, `clearImmediate`,
 `setInterval`, `clearInterval`, and `Date`. E.g. `lolex.install(["setTimeout",
 "clearTimeout"])`.
 
+The `loopLimit` argument is the same as in `lolex.createClock()`.
+
 ### `var id = clock.setTimeout(callback, timeout)`
 
 Schedules the callback to be fired once `timeout` milliseconds have ticked by.
@@ -178,6 +183,14 @@ callbacks.
 
 Advances the clock to the the moment of the first scheduled timer, firing it.
 
+### `clock.runAll()`
+
+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.
+
 ### `clock.setSystemTime([now])`
 
 This simulates a user changing the system clock while your program is running.
diff --git a/src/lolex-src.js b/src/lolex-src.js
index c2e42dc..4fe7b54 100644
--- a/src/lolex-src.js
+++ b/src/lolex-src.js
@@ -407,12 +407,15 @@
 
     exports.timers = timers;
 
-    function createClock(now) {
+    function createClock(now, loopLimit) {
+        loopLimit = loopLimit || 1000;
+
         var clock = {
             now: getEpoch(now),
             hrNow: 0,
             timeouts: {},
-            Date: createDate()
+            Date: createDate(),
+            loopLimit: loopLimit
         };
 
         clock.Date.clock = clock;
@@ -517,6 +520,20 @@
             }
         };
 
+        clock.runAll = function runAll() {
+            var numTimers, i;
+            for (i = 0; i < clock.loopLimit; i++) {
+                numTimers = Object.keys(clock.timers).length;
+                if (numTimers === 0) {
+                    return clock.now;
+                }
+
+                clock.next();
+            }
+
+            throw new Error('Aborting after running ' + clock.loopLimit + 'timers, assuming an infinite loop!');
+        };
+
         clock.reset = function reset() {
             clock.timers = {};
         };
@@ -564,7 +581,7 @@
     }
     exports.createClock = createClock;
 
-    exports.install = function install(target, now, toFake) {
+    exports.install = function install(target, now, toFake, loopLimit) {
         var i,
             l;
 
@@ -578,7 +595,7 @@
             target = global;
         }
 
-        var clock = createClock(now);
+        var clock = createClock(now, loopLimit);
 
         clock.uninstall = function () {
             uninstall(clock, target);


Test patch:
diff --git a/test/lolex-test.js b/test/lolex-test.js
index 3f00771..f5c7af3 100644
--- a/test/lolex-test.js
+++ b/test/lolex-test.js
@@ -798,6 +798,81 @@ describe("lolex", function () {
         });
     });
 
+    describe('runAll', function() {
+
+        it('runs all timers', function() {
+            this.clock = lolex.createClock();
+            var spies = [sinon.spy(), sinon.spy()];
+            this.clock.setTimeout(spies[0], 10);
+            this.clock.setTimeout(spies[1], 50);
+
+            this.clock.runAll();
+
+            assert(spies[0].called);
+            assert(spies[1].called);
+        });
+
+        it('new timers added while running are also run', function() {
+            this.clock = lolex.createClock();
+            var test = this;
+            var spies = [
+                sinon.spy(function() {
+                    test.clock.setTimeout(spies[1], 50);
+                }),
+                sinon.spy()
+            ];
+
+            // Spy calls another setTimeout
+            this.clock.setTimeout(spies[0], 10);
+
+            this.clock.runAll();
+
+            assert(spies[0].called);
+            assert(spies[1].called);
+        });
+
+        it('throws before allowing infinite recursion', function() {
+            this.clock = lolex.createClock();
+            var test = this;
+            var recursiveCallback = function() {
+                test.clock.setTimeout(recursiveCallback, 10);
+            };
+
+            this.clock.setTimeout(recursiveCallback, 10);
+
+            assert.exception(function () {
+                test.clock.runAll();
+            });
+        });
+
+        it('the loop limit can be set when creating a clock', function() {
+            this.clock = lolex.createClock(0, 1);
+
+            var spies = [sinon.spy(), sinon.spy()];
+            this.clock.setTimeout(spies[0], 10);
+            this.clock.setTimeout(spies[1], 50);
+
+            assert.exception(function () {
+                test.clock.runAll();
+            });
+        });
+
+        it('the loop limit can be set when installing a clock', function() {
+            this.clock = lolex.install(0, null, 1);
+
+            var spies = [sinon.spy(), sinon.spy()];
+            setTimeout(spies[0], 10);
+            setTimeout(spies[1], 50);
+
+            assert.exception(function () {
+                test.clock.runAll();
+            });
+
+            this.clock.uninstall();
+        });
+
+    });
+
     describe("clearTimeout", function () {
 
         beforeEach(function () {