Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Retry - Add stack trace to code recursively scheduling timers #325 #375

Merged
merged 4 commits into from
Jun 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 92 additions & 11 deletions src/fake-timers-src.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,18 @@ function withGlobal(_global) {
return isFinite(num);
}

var isNearInfiniteLimit = false;

function checkIsNearInfiniteLimit(clock, i) {
if (clock.loopLimit && i === clock.loopLimit - 1) {
isNearInfiniteLimit = true;
}
}

function resetIsNearInfiniteLimit() {
isNearInfiniteLimit = false;
}

/**
* Parse strings like "01:10:00" (meaning 1 hour, 10 minutes, 0 seconds) into
* number of milliseconds. This is used to support human-readable strings passed
Expand Down Expand Up @@ -235,6 +247,67 @@ function withGlobal(_global) {
return timer && timer.callAt >= from && timer.callAt <= to;
}

function getInfiniteLoopError(clock, job) {
var infiniteLoopError = new Error(
`Aborting after running ${clock.loopLimit} timers, assuming an infinite loop!`
);

// pattern never matched in Node
var computedTargetPattern = /target\.*[<|(|[].*?[>|\]|)]\s*/;
var clockMethodPattern = new RegExp(
String(Object.keys(clock).join("|"))
);

if (addTimerReturnsObject) {
// node.js environment
clockMethodPattern = new RegExp(
`\\s+at (Object\\.)?(?:${Object.keys(clock).join("|")})\\s+`
);
}

var matchedLineIndex = -1;
job.error.stack.split("\n").some(function (line, i) {
// If we've matched a computed target line (e.g. setTimeout) then we
// don't need to look any further. Return true to stop iterating.
var matchedComputedTarget = line.match(computedTargetPattern);
/* istanbul ignore if */
if (matchedComputedTarget) {
matchedLineIndex = i;
return true;
}

// If we've matched a clock method line, then there may still be
// others further down the trace. Return false to keep iterating.
var matchedClockMethod = line.match(clockMethodPattern);
if (matchedClockMethod) {
matchedLineIndex = i;
return false;
}

// If we haven't matched anything on this line, but we matched
// previously and set the matched line index, then we can stop.
// If we haven't matched previously, then we should keep iterating.
return matchedLineIndex >= 0;
});

var stack = `${infiniteLoopError}\n${job.type || "Microtask"} - ${
job.func.name || "anonymous"
}\n${job.error.stack
.split("\n")
.slice(matchedLineIndex + 1)
.join("\n")}`;

try {
Object.defineProperty(infiniteLoopError, "stack", {
value: stack,
});
} catch (e) {
// noop
}

return infiniteLoopError;
}

/**
* @param {Date} target
* @param {Date} source
Expand Down Expand Up @@ -358,12 +431,13 @@ function withGlobal(_global) {
for (var i = 0; i < clock.jobs.length; i++) {
var job = clock.jobs[i];
job.func.apply(null, job.args);

checkIsNearInfiniteLimit(clock, i);
if (clock.loopLimit && i > clock.loopLimit) {
throw new Error(
`Aborting after running ${clock.loopLimit} timers, assuming an infinite loop!`
);
throw getInfiniteLoopError(clock, job);
}
}
resetIsNearInfiniteLimit();
clock.jobs = [];
}

Expand All @@ -388,6 +462,10 @@ function withGlobal(_global) {
}
}

if (isNearInfiniteLimit) {
timer.error = new Error();
}

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

if (timer.hasOwnProperty("delay")) {
Expand Down Expand Up @@ -946,6 +1024,7 @@ function withGlobal(_global) {
return enqueueJob(clock, {
func: func,
args: Array.prototype.slice.call(arguments, 1),
error: isNearInfiniteLimit ? new Error() : null,
});
};

Expand Down Expand Up @@ -1253,20 +1332,22 @@ function withGlobal(_global) {
runJobs(clock);
for (i = 0; i < clock.loopLimit; i++) {
if (!clock.timers) {
resetIsNearInfiniteLimit();
return clock.now;
}

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

clock.next();
checkIsNearInfiniteLimit(clock, i);
}

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

clock.runToFrame = function runToFrame() {
Expand All @@ -1286,13 +1367,15 @@ function withGlobal(_global) {
var numTimers;
if (i < clock.loopLimit) {
if (!clock.timers) {
resetIsNearInfiniteLimit();
resolve(clock.now);
return;
}

numTimers = Object.keys(clock.timers)
.length;
if (numTimers === 0) {
resetIsNearInfiniteLimit();
resolve(clock.now);
return;
}
Expand All @@ -1302,14 +1385,12 @@ function withGlobal(_global) {
i++;

doRun();
checkIsNearInfiniteLimit(clock, i);
return;
}

reject(
new Error(
`Aborting after running ${clock.loopLimit} timers, assuming an infinite loop!`
)
);
var excessJob = firstTimer(clock);
reject(getInfiniteLoopError(clock, excessJob));
} catch (e) {
reject(e);
}
Expand Down
Loading