Skip to content

Commit 6388b2f

Browse files
committed
Core: Exclude or grey internal frames from stack traces in TAP reporter
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.
1 parent 6abf4de commit 6388b2f

File tree

9 files changed

+92
-31
lines changed

9 files changed

+92
-31
lines changed

src/core/stacktrace.js

Lines changed: 83 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
//
33
// This should reduce a raw stack trace like this:
44
//
5-
// > foo.broken()@/src/foo.js
6-
// > Bar@/src/bar.js
5+
// > foo.broken()@/example/foo.js
6+
// > Bar@/example/bar.js
77
// > @/test/bar.test.js
88
// > @/lib/qunit.js:500:12
99
// > @/lib/qunit.js:100:28
@@ -13,8 +13,8 @@
1313
//
1414
// and shorten it to show up until the end of the user's bar.test.js code.
1515
//
16-
// > foo.broken()@/src/foo.js
17-
// > Bar@/src/bar.js
16+
// > foo.broken()@/example/foo.js
17+
// > Bar@/example/bar.js
1818
// > @/test/bar.test.js
1919
//
2020
// QUnit will obtain one example trace (once per process/pageload suffices),
@@ -31,22 +31,89 @@
3131
//
3232
// See also:
3333
// - https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack
34-
//
35-
const fileName = (sourceFromStacktrace(0) || '')
36-
// Global replace, because a frame like localhost:4000/lib/qunit.js:1234:50,
37-
// would otherwise (harmlessly, but uselessly) remove only the port (first match).
38-
// https://github.com/qunitjs/qunit/issues/1769
39-
.replace(/(:\d+)+\)?/g, '')
40-
// Remove anything prior to the last slash (Unix/Windows) from the last frame,
41-
// leaving only "qunit.js".
42-
.replace(/.+[/\\]/, '');
34+
35+
function qunitFileName () {
36+
let error = new Error();
37+
if (!error.stack) {
38+
// Copy of sourceFromStacktrace() to avoid circular dependency
39+
// Support: IE 9-11
40+
try {
41+
throw error;
42+
} catch (err) {
43+
error = err;
44+
}
45+
}
46+
return (error.stack || '')
47+
// Copy of extractStacktrace() to avoid circular dependency
48+
// Support: V8/Chrome
49+
.replace(/^error$\n/im, '')
50+
.split('\n')[0]
51+
// Global replace, because a frame like localhost:4000/lib/qunit.js:1234:50,
52+
// would otherwise (harmlessly, but uselessly) remove only the port (first match).
53+
// https://github.com/qunitjs/qunit/issues/1769
54+
.replace(/(:\d+)+\)?/g, '')
55+
// Remove anything prior to the last slash (Unix/Windows) from the last frame,
56+
// leaving only "qunit.js".
57+
.replace(/.+[/\\]/, '');
58+
}
59+
60+
const fileName = qunitFileName();
61+
62+
/**
63+
* Responsibilities:
64+
* - For internal errors from QUnit itself, remove the first qunit.js frames.
65+
* - For errors in Node.js, format any remaining qunit.js and node:internal
66+
* frames as internal (i.e. grey out).
67+
*
68+
* @param {string} Error#stack
69+
* @param {Function} formatInternal Format a string in an "internal" color
70+
* @param {string|null} [eToString] Error#toString() to help remove
71+
* noise from Node.js/V8 stack traces.
72+
*/
73+
export function annotateStacktrace (stack, formatInternal, eToString = null) {
74+
const frames = stack.split('\n');
75+
const annotated = [];
76+
if (eToString && eToString.indexOf(frames[0]) !== -1) {
77+
// In Firefox and Safari e.stack starts with frame 0, but in V8 (Chrome/Node.js),
78+
// e.stack starts first stringified message. Preserve this separately,
79+
// so that, below, we can distinguish between internal frames on top
80+
// (to remove) vs later internal frames (to format differently).
81+
annotated.push(frames.shift());
82+
}
83+
let initialInternal = true;
84+
for (let i = 0; i < frames.length; i++) {
85+
const frame = frames[i];
86+
const isInternal = (
87+
(fileName && frame.indexOf(fileName) !== -1)
88+
// Support Node 16+: ESM-style
89+
// "at wrap (node:internal/modules/cjs/loader:1)"
90+
|| frame.indexOf('node:internal/') !== -1
91+
// Support Node 10-14 (CJS-style)
92+
// "at load (internal/modules/cjs/loader.js:7)"
93+
|| frame.match(/^\s+at .+\(internal[^)]*\)$/)
94+
);
95+
if (!isInternal) {
96+
initialInternal = false;
97+
}
98+
// Remove initial internal frames entirely.
99+
if (!initialInternal) {
100+
annotated.push(isInternal ? formatInternal(frame) : frame);
101+
}
102+
}
103+
104+
return annotated.join('\n');
105+
}
43106

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

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

72-
// Support: Safari <=7 only, IE <=10 - 11 only
73-
// Not all browsers generate the `stack` property for `new Error()`, see also #636
139+
// Support: IE 9-11, iOS 7
140+
// Not all browsers generate the `stack` property for `new Error()`
141+
// See also https://github.com/qunitjs/qunit/issues/636
74142
if (!error.stack) {
75143
try {
76144
throw error;

src/reporters/TapReporter.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import kleur from 'kleur';
22
import { errorString } from '../core/utilities';
33
import { console } from '../globals';
4+
import { annotateStacktrace } from '../core/stacktrace';
45

56
/**
67
* Format a given value into YAML.
@@ -264,7 +265,10 @@ export default class TapReporter {
264265
if (error.stack) {
265266
// Since stacks aren't user generated, take a bit of liberty by
266267
// adding a trailing new line to allow a straight-forward YAML Blocks.
267-
out += `\n stack: ${prettyYamlValue(error.stack + '\n')}`;
268+
const fmtStack = annotateStacktrace(error.stack, kleur.grey);
269+
if (fmtStack.length) {
270+
out += `\n stack: ${prettyYamlValue(fmtStack + '\n')}`;
271+
}
268272
}
269273

270274
out += '\n ...';
@@ -276,7 +280,10 @@ export default class TapReporter {
276280
out += `\n message: ${prettyYamlValue(errorString(error))}`;
277281
out += `\n severity: ${prettyYamlValue('failed')}`;
278282
if (error && error.stack) {
279-
out += `\n stack: ${prettyYamlValue(error.stack + '\n')}`;
283+
const fmtStack = annotateStacktrace(error.stack, kleur.grey, error.toString());
284+
if (fmtStack.length) {
285+
out += `\n stack: ${prettyYamlValue(fmtStack + '\n')}`;
286+
}
280287
}
281288
out += '\n ...';
282289
this.log(out);

test/cli/cli-main.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,6 @@ not ok 2 slow
3838
---
3939
message: Test took longer than 7ms; test timed out.
4040
severity: failed
41-
stack: |
42-
at internal
4341
...
4442
ok 3 config
4543
1..3

test/cli/fixtures/config-noglobals-add.tap.txt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ not ok 1 adds global var
66
---
77
message: Introduced global variable(s): dummyGlobal
88
severity: failed
9-
stack: |
10-
at qunit.js
119
...
1210
1..1
1311
# pass 0

test/cli/fixtures/config-noglobals-remove.tap.txt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ not ok 1 deletes global var
66
---
77
message: Deleted global variable(s): dummyGlobal
88
severity: failed
9-
stack: |
10-
at qunit.js
119
...
1210
1..1
1311
# pass 0

test/cli/fixtures/config-testTimeout.tap.txt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ not ok 1 slow
66
---
77
message: Test took longer than 10ms; test timed out.
88
severity: failed
9-
stack: |
10-
at internal
119
...
1210
1..1
1311
# pass 0

test/cli/fixtures/done-after-timeout.tap.txt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ not ok 1 times out before scheduled done is called
66
---
77
message: Test took longer than 10ms; test timed out.
88
severity: failed
9-
stack: |
10-
at internal
119
...
1210
1..1
1311
# pass 0

test/cli/fixtures/pending-async-after-timeout.tap.txt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ not ok 1 example
66
---
77
message: Test took longer than 10ms; test timed out.
88
severity: failed
9-
stack: |
10-
at internal
119
...
1210
1..1
1311
# pass 0

test/cli/fixtures/timeout.tap.txt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ not ok 1 timeout > first
66
---
77
message: Test took longer than 10ms; test timed out.
88
severity: failed
9-
stack: |
10-
at internal
119
...
1210
ok 2 timeout > second
1311
1..2

0 commit comments

Comments
 (0)