Skip to content

Commit

Permalink
Retry - Add stack trace to code recursively scheduling timers #325 (#375
Browse files Browse the repository at this point in the history
)

* Add stack trace to code recursively scheduling timers

Use stack information for jobs and timers to provide more context when
an infinite loop error is thrown.

Fixes #230

Add flag for approaching infinite loop

Use a flag when approaching infinite loop detection which will enable
an error object to be attached to the job, which can then be used for
generating the stack output.

Update for PR feedback

- Safely update stack property using `defineProperty`, wrapped in try/catch
- Tests for more clock functions to confirm stack trace provided
- Stack trace slice automatically calculated based on clock function position

Remove unnecessary durations and use globals

- Use global setInterval and setImmediate to match setTimeout use
- Remove unnecessary duration parameter from setImmediate and setImmediate

add use of prettier

Firefox and Chrome tests pass after RegExp fix

Chrome shows stack messages differently, and uses the words: 'at' and 'Object', Firefox does not - that has created two problems: first, the stack trace messages that are shown to the user are a bit different (Firefox always has at least one more line at the start of the stack list - usually internal functions that are not supposed to appear). Second, the RegExp tried to match parts of the strings that only exist in Chrome, failing the Firefox tests)

prettier used

* small RegExp fix for node env + linting

* improving test-coverage statistics

* at attempt to ignore certain condition in nyc coverage

Co-authored-by: Alistair Brown <[email protected]>
  • Loading branch information
itayperry and alistairjcbrown authored Jun 8, 2021
1 parent dbc606a commit 54c2e8d
Show file tree
Hide file tree
Showing 2 changed files with 410 additions and 11 deletions.
103 changes: 92 additions & 11 deletions src/fake-timers-src.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,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 @@ -288,6 +300,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 @@ -411,12 +484,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 @@ -441,6 +515,10 @@ function withGlobal(_global) {
}
}

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

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

if (timer.hasOwnProperty("delay")) {
Expand Down Expand Up @@ -997,6 +1075,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 @@ -1304,20 +1383,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 @@ -1337,13 +1418,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 @@ -1353,14 +1436,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

0 comments on commit 54c2e8d

Please sign in to comment.