Skip to content

Commit

Permalink
Core: Exclude or grey internal frames from stack traces in TAP reporter
Browse files Browse the repository at this point in the history
Cherry-picked from 9fed286 (3.0.0-dev):

> Core: Exclude or grey internal frames from stack traces in TAP reporter
> Internal frames are those from qunit.js, or Node.js runtime.
>
> * Remove any internal frames from the top of the stack.
> * Grey out later internal frames anywhere in the stack.
>
> This change is applied to the TAP reporter, which the QUnit CLI uses
> by default.

Cherry-picked from 95105aa (3.0.0-dev):

> Fix fragile code in stracktrace.js that previously worked only because
> of Babel transformations masking a violation of the Temporal Dead Zone
> between `const fileName` and the functions it uses to compute that
> value.
  • Loading branch information
Krinkle committed Jan 18, 2025
1 parent 6abf4de commit 3c6a2e9
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 17 deletions.
97 changes: 82 additions & 15 deletions src/core/stacktrace.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
//
// This should reduce a raw stack trace like this:
//
// > foo.broken()@/src/foo.js
// > Bar@/src/bar.js
// > foo.broken()@/example/foo.js
// > Bar@/example/bar.js
// > @/test/bar.test.js
// > @/lib/qunit.js:500:12
// > @/lib/qunit.js:100:28
Expand All @@ -13,8 +13,8 @@
//
// and shorten it to show up until the end of the user's bar.test.js code.
//
// > foo.broken()@/src/foo.js
// > Bar@/src/bar.js
// > foo.broken()@/example/foo.js
// > Bar@/example/bar.js
// > @/test/bar.test.js
//
// QUnit will obtain one example trace (once per process/pageload suffices),
Expand All @@ -31,22 +31,88 @@
//
// See also:
// - https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack
//
const fileName = (sourceFromStacktrace(0) || '')
// Global replace, because a frame like localhost:4000/lib/qunit.js:1234:50,
// would otherwise (harmlessly, but uselessly) remove only the port (first match).
// https://github.com/qunitjs/qunit/issues/1769
.replace(/(:\d+)+\)?/g, '')
// Remove anything prior to the last slash (Unix/Windows) from the last frame,
// leaving only "qunit.js".
.replace(/.+[/\\]/, '');

function qunitFileName () {
let error = new Error();
if (!error.stack) {
// Copy of sourceFromStacktrace() to avoid circular dependency
// Support: IE 9-11
try {
throw error;
} catch (err) {
error = err;
}
}
return (error.stack || '')
// Copy of extractStacktrace() to avoid circular dependency
// Support: V8/Chrome
.replace(/^error$\n/im, '')
.split('\n')[0]
// Global replace, because a frame like localhost:4000/lib/qunit.js:1234:50,
// would otherwise (harmlessly, but uselessly) remove only the port (first match).
// https://github.com/qunitjs/qunit/issues/1769
.replace(/(:\d+)+\)?/g, '')
// Remove anything prior to the last slash (Unix/Windows) from the last frame,
// leaving only "qunit.js".
.replace(/.+[/\\]/, '');
}

const fileName = qunitFileName();

/**
* - For internal errors from QUnit itself, remove the first qunit.js frames.
* - For errors in Node.js, format any remaining qunit.js and node:internal
* frames as internal (i.e. grey out).
*/
export function annotateStacktrace (e, formatInternal) {
if (!e || !e.stack) {
return String(e);
}
const frames = e.stack.split('\n');
const annotated = [];
if (e.toString().indexOf(frames[0]) !== -1) {
// In Firefox and Safari e.stack starts with frame 0, but in V8 (Chrome/Node.js),
// e.stack starts first stringified message. Preserve this separately,
// so that, below, we can distinguish between internal frames on top
// (to remove) vs later internal frames (to format differently).
annotated.push(frames.shift());
}
let initialInternal = true;
for (let i = 0; i < frames.length; i++) {
const frame = frames[i];
const isInternal = (
(fileName && frame.indexOf(fileName) !== -1) ||
// Support Node 16+: ESM-style
// "at wrap (node:internal/modules/cjs/loader:1)"
frame.indexOf('node:internal/') !== -1 ||
// Support Node 10-14 (CJS-style)
// "at load (internal/modules/cjs/loader.js:7)"
frame.match(/^\s+at .+\(internal[^)]*\)$/) ||
// "at listOnTimeout (timers.js:263)"
frame.match(/^\s+at .+\([^/)][^)]*\)$/)
);
if (!isInternal) {
initialInternal = false;
}
// Remove initial internal frames entirely.
if (!initialInternal) {
annotated.push(isInternal ? formatInternal(frame) : frame);
}
}

return annotated.join('\n');
}

export function extractStacktrace (e, offset) {
offset = offset === undefined ? 4 : offset;

// Support: IE9, e.stack is not supported, we will return undefined
if (e && e.stack) {
const stack = e.stack.split('\n');
// In Firefox and Safari, e.stack starts immediately with the first frame.
//
// In V8 (Chrome/Node.js), the stack starts first with a stringified error message,
// and the real stack starting on line 2.
if (/^error$/i.test(stack[0])) {
stack.shift();
}
Expand All @@ -69,8 +135,9 @@ export function extractStacktrace (e, offset) {
export function sourceFromStacktrace (offset) {
let error = new Error();

// Support: Safari <=7 only, IE <=10 - 11 only
// Not all browsers generate the `stack` property for `new Error()`, see also #636
// Support: IE 9-11, iOS 7
// Not all browsers generate the `stack` property for `new Error()`
// See also https://github.com/qunitjs/qunit/issues/636
if (!error.stack) {
try {
throw error;
Expand Down
3 changes: 2 additions & 1 deletion src/reporters/TapReporter.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import kleur from 'kleur';
import { errorString } from '../core/utilities';
import { console } from '../globals';
import { annotateStacktrace } from '../core/stacktrace';

/**
* Format a given value into YAML.
Expand Down Expand Up @@ -276,7 +277,7 @@ export default class TapReporter {
out += `\n message: ${prettyYamlValue(errorString(error))}`;
out += `\n severity: ${prettyYamlValue('failed')}`;
if (error && error.stack) {
out += `\n stack: ${prettyYamlValue(error.stack + '\n')}`;
out += `\n stack: ${prettyYamlValue(annotateStacktrace(error, kleur.grey) + '\n')}`;
}
out += '\n ...';
this.log(out);
Expand Down
2 changes: 1 addition & 1 deletion test/cli/TapReporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ Bail out! Boo
severity: failed
stack: |
at Object.<anonymous> (/dev/null/test/unit/data.js:6:5)
at require (internal/helpers.js:22:18)
${kleur.grey(' at require (internal/helpers.js:22:18)')}
at /dev/null/src/example/foo.js:220:27
...
Bail out! ReferenceError: Boo is not defined
Expand Down

0 comments on commit 3c6a2e9

Please sign in to comment.