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 committed May 17, 2020
1 parent 2ebd23c commit 8e38995
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 15 deletions.
50 changes: 35 additions & 15 deletions src/fake-timers-src.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,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 @@ -252,11 +276,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 @@ -267,6 +287,8 @@ function withGlobal(_global) {
throw new Error("Callback must be provided to timer calls");
}

timer.error = new Error();

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

if (timer.hasOwnProperty("delay")) {
Expand Down Expand Up @@ -767,7 +789,8 @@ function withGlobal(_global) {
clock.nextTick = function nextTick(func) {
return enqueueJob(clock, {
func: func,
args: Array.prototype.slice.call(arguments, 1)
args: Array.prototype.slice.call(arguments, 1),
error: new Error()
});
};

Expand Down Expand Up @@ -1073,11 +1096,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 @@ -1113,11 +1133,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 @@ -4618,3 +4618,99 @@ describe("issue #315 - praseInt if delay is not a number", 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 8e38995

Please sign in to comment.