Skip to content

Commit

Permalink
Add stack trace to code recursively scheduling timers
Browse files Browse the repository at this point in the history
Use stack information for jobs and timers to provide more context when
an infinite loop error is thrown.

Fixes sinonjs#230
  • Loading branch information
alistairjcbrown authored and itayperry committed May 1, 2021
1 parent d71630d commit a4fb0b4
Show file tree
Hide file tree
Showing 2 changed files with 129 additions and 14 deletions.
47 changes: 33 additions & 14 deletions src/fake-timers-src.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,30 @@ function withGlobal(_global) {
throw new TypeError("now should be milliseconds since UNIX epoch");
}

function getInfiniteLoopError(loopLimit, job) {
var infiniteLoopError = new Error(
"Aborting after running " +
loopLimit +
" timers, assuming an infinite loop!"
);

var sliceSize = job.type ? 4 : 3;

infiniteLoopError.stack =
infiniteLoopError +
"\n" +
(job.type || "Microtask") +
" - " +
(job.func.name || "anonymous") +
"\n" +
job.error.stack
.split("\n")
.slice(sliceSize)
.join("\n");

return infiniteLoopError;
}

function inRange(from, to, timer) {
return timer && timer.callAt >= from && timer.callAt <= to;
}
Expand Down Expand Up @@ -288,11 +312,7 @@ function withGlobal(_global) {
var job = clock.jobs[i];
job.func.apply(null, job.args);
if (clock.loopLimit && i > clock.loopLimit) {
throw new Error(
"Aborting after running " +
clock.loopLimit +
" timers, assuming an infinite loop!"
);
throw getInfiniteLoopError(clock.loopLimit, job);
}
}
clock.jobs = [];
Expand All @@ -314,6 +334,7 @@ function withGlobal(_global) {
);
}
}
timer.error = new Error();

timer.type = timer.immediate ? "Immediate" : "Timeout";

Expand Down Expand Up @@ -810,6 +831,7 @@ function withGlobal(_global) {
return enqueueJob(clock, {
func: func,
args: Array.prototype.slice.call(arguments, 1),
error: new Error()
});
};

Expand Down Expand Up @@ -1115,11 +1137,8 @@ function withGlobal(_global) {
clock.next();
}

throw new Error(
"Aborting after running " +
clock.loopLimit +
" timers, assuming an infinite loop!"
);
var excessJob = firstTimer(clock);
throw getInfiniteLoopError(clock.loopLimit, excessJob);
};

clock.runToFrame = function runToFrame() {
Expand Down Expand Up @@ -1155,11 +1174,11 @@ function withGlobal(_global) {
return;
}

var excessJob = firstTimer(clock);
reject(
new Error(
"Aborting after running " +
clock.loopLimit +
" timers, assuming an infinite loop!"
getInfiniteLoopError(
clock.loopLimit,
excessJob
)
);
} catch (e) {
Expand Down
96 changes: 96 additions & 0 deletions test/fake-timers-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4681,3 +4681,99 @@ describe("#187 - Support timeout.refresh in node environments", function () {
clock.uninstall();
});
});

describe("loop limit stack trace", function() {
var test,
expectedMessage =
"Aborting after running 5 timers, assuming an infinite loop!";

beforeEach(function() {
test = this;
this.clock = FakeTimers.install({ loopLimit: 5 });
});

afterEach(function() {
this.clock.uninstall();
});

describe("microtasks", function() {
beforeEach(function() {
function recursiveQueueMicroTask() {
test.clock.queueMicrotask(recursiveQueueMicroTask);
}
recursiveQueueMicroTask();
});

it("provides a stack trace for running microtasks", function() {
var caughtError = false;

try {
test.clock.runMicrotasks();
} catch (err) {
caughtError = true;
assert.equals(err.message, expectedMessage);
assert.equals(
new RegExp(
"Error: " +
expectedMessage +
"\\s+Microtask - recursiveQueueMicroTask\\s+at recursiveQueueMicroTask"
).test(err.stack),
true
);
}
assert.equals(caughtError, true);
});
});

describe("timeouts", function() {
beforeEach(function() {
function recursiveCreateTimer() {
setTimeout(function recursiveCreateTimerTimeout() {
recursiveCreateTimer();
}, 10);
}
recursiveCreateTimer();
});

it("provides a stack trace for running all async", function() {
var catchSpy = sinon.spy();

return test.clock
.runAllAsync()
.catch(catchSpy)
.then(function() {
assert(catchSpy.calledOnce);
var err = catchSpy.firstCall.args[0];
assert.equals(err.message, expectedMessage);
assert.equals(
new RegExp(
"Error: " +
expectedMessage +
"\\s+Timeout - recursiveCreateTimerTimeout\\s+at recursiveCreateTimer"
).test(err.stack),
true
);
});
});

it("provides a stack trace for running all sync", function() {
var caughtError = false;

try {
test.clock.runAll();
} catch (err) {
caughtError = true;
assert.equals(err.message, expectedMessage);
assert.equals(
new RegExp(
"Error: " +
expectedMessage +
"\\s+Timeout - recursiveCreateTimerTimeout\\s+at recursiveCreateTimer"
).test(err.stack),
true
);
}
assert.equals(caughtError, true);
});
});
});

0 comments on commit a4fb0b4

Please sign in to comment.