Skip to content

Commit 9a9351d

Browse files
Retain (most of) Node.js internals in stack traces
This retains most of the Node.js internals in stack traces for the mini and verbose reporters. Internals are dimmed in the output. Call sites are prefixed with a small pointer. Reporter logs are now tested for each Node.js version separately. Stack beautification now takes place in the main process, not the workers. Fixes #2110. Co-authored-by: Mark Wubben <mark@novemberborn.net>
1 parent 3c0fc03 commit 9a9351d

File tree

77 files changed

+3633
-370
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

77 files changed

+3633
-370
lines changed

lib/beautify-stack.js

Lines changed: 0 additions & 75 deletions
This file was deleted.

lib/reporters/beautify-stack.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
'use strict';
2+
const StackUtils = require('stack-utils');
3+
4+
const stackUtils = new StackUtils({
5+
ignoredPackages: [
6+
'@ava/babel',
7+
'@ava/require-precompiled',
8+
'@ava/typescript',
9+
'append-transform',
10+
'ava',
11+
'empower-core',
12+
'esm',
13+
'nyc'
14+
],
15+
internals: [
16+
// AVA internals, which ignoredPackages don't ignore when we run our own unit tests.
17+
/\/ava\/(?:lib\/|lib\/worker\/)?[\w-]+\.js:\d+:\d+\)?$/,
18+
// Only ignore Node.js internals that really are not useful for debugging.
19+
...StackUtils.nodeInternals().filter(regexp => !/\(internal/.test(regexp.source)),
20+
/\(internal\/process\/task_queues\.js:\d+:\d+\)$/,
21+
/\(internal\/modules\/cjs\/.+?\.js:\d+:\d+\)$/,
22+
/async Promise\.all \(index/,
23+
/new Promise \(<anonymous>\)/
24+
]
25+
});
26+
27+
/*
28+
* Given a string value of the format generated for the `stack` property of a
29+
* V8 error object, return a string that contains only stack frame information
30+
* for frames that have relevance to the consumer.
31+
*
32+
* For example, given the following string value:
33+
*
34+
* ```
35+
* Error
36+
* at inner (/home/ava/ex.js:7:12)
37+
* at /home/ava/ex.js:12:5
38+
* at outer (/home/ava/ex.js:13:4)
39+
* at Object.<anonymous> (/home/ava/ex.js:14:3)
40+
* at Module._compile (module.js:570:32)
41+
* at Object.Module._extensions..js (module.js:579:10)
42+
* at Module.load (module.js:487:32)
43+
* at tryModuleLoad (module.js:446:12)
44+
* at Function.Module._load (module.js:438:3)
45+
* at Module.runMain (module.js:604:10)
46+
* ```
47+
*
48+
* ...this function returns the following string value:
49+
*
50+
* ```
51+
* inner (/home/ava/ex.js:7:12)
52+
* /home/ava/ex.js:12:5
53+
* outer (/home/ava/ex.js:13:4)
54+
* Object.<anonymous> (/home/ava/ex.js:14:3)
55+
* Module._compile (module.js:570:32)
56+
* Object.Module._extensions..js (module.js:579:10)
57+
* Module.load (module.js:487:32)
58+
* tryModuleLoad (module.js:446:12)
59+
* Function.Module._load (module.js:438:3)
60+
* Module.runMain (module.js:604:10)
61+
* ```
62+
*/
63+
module.exports = stack => {
64+
if (!stack) {
65+
return [];
66+
}
67+
68+
return stackUtils.clean(stack).trim().split('\n');
69+
};

lib/reporters/colors.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ module.exports = {
1111
duration: chalk.gray.dim,
1212
errorSource: chalk.gray,
1313
errorStack: chalk.gray,
14+
errorStackInternal: chalk.gray.dim,
1415
stack: chalk.red,
1516
information: chalk.magenta
1617
};

lib/reporters/mini.js

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const indentString = require('indent-string');
99
const ora = require('ora');
1010
const plur = require('plur');
1111
const trimOffNewlines = require('trim-off-newlines');
12+
const beautifyStack = require('./beautify-stack');
1213

1314
const chalk = require('../chalk').get();
1415
const codeExcerpt = require('../code-excerpt');
@@ -18,6 +19,8 @@ const improperUsageMessages = require('./improper-usage-messages');
1819
const prefixTitle = require('./prefix-title');
1920
const whileCorked = require('./while-corked');
2021

22+
const nodeInternals = require('stack-utils').nodeInternals();
23+
2124
class LineWriter extends stream.Writable {
2225
constructor(dest, spinner) {
2326
super();
@@ -322,13 +325,27 @@ class MiniReporter {
322325

323326
if (evt.err.stack) {
324327
const {stack} = evt.err;
325-
if (stack.includes(os.EOL)) {
328+
if (stack.includes('\n')) {
326329
this.lineWriter.writeLine();
327-
this.lineWriter.writeLine(colors.errorStack(stack));
330+
this.lineWriter.writeLine(this.formatErrorStack(evt.err));
328331
}
329332
}
330333
}
331334

335+
formatErrorStack(error) {
336+
if (error.shouldBeautifyStack) {
337+
return beautifyStack(error.stack).map(line => {
338+
if (nodeInternals.some(internal => internal.test(line))) {
339+
return colors.errorStackInternal(`${figures.pointerSmall} ${line}`);
340+
}
341+
342+
return colors.errorStack(`${figures.pointerSmall} ${line}`);
343+
}).join('\n');
344+
}
345+
346+
return error.stack;
347+
}
348+
332349
writeLogs(evt) {
333350
if (evt.logs) {
334351
for (const log of evt.logs) {

lib/reporters/tap.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const stripAnsi = require('strip-ansi');
77
const supertap = require('supertap');
88
const indentString = require('indent-string');
99

10+
const beautifyStack = require('./beautify-stack');
1011
const prefixTitle = require('./prefix-title');
1112

1213
function dumpError(error) {
@@ -42,7 +43,7 @@ function dumpError(error) {
4243
}
4344

4445
if (error.stack) {
45-
object.at = error.stack;
46+
object.at = error.shouldBeautifyStack ? beautifyStack(error.stack).join('\n') : error.stack;
4647
}
4748

4849
return object;

lib/reporters/verbose.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const indentString = require('indent-string');
88
const plur = require('plur');
99
const prettyMs = require('pretty-ms');
1010
const trimOffNewlines = require('trim-off-newlines');
11+
const beautifyStack = require('./beautify-stack');
1112

1213
const chalk = require('../chalk').get();
1314
const codeExcerpt = require('../code-excerpt');
@@ -17,6 +18,8 @@ const improperUsageMessages = require('./improper-usage-messages');
1718
const prefixTitle = require('./prefix-title');
1819
const whileCorked = require('./while-corked');
1920

21+
const nodeInternals = require('stack-utils').nodeInternals();
22+
2023
class LineWriter extends stream.Writable {
2124
constructor(dest) {
2225
super();
@@ -264,11 +267,25 @@ class VerboseReporter {
264267
const {stack} = evt.err;
265268
if (stack.includes('\n')) {
266269
this.lineWriter.writeLine();
267-
this.lineWriter.writeLine(colors.errorStack(stack));
270+
this.lineWriter.writeLine(this.formatErrorStack(evt.err));
268271
}
269272
}
270273
}
271274

275+
formatErrorStack(error) {
276+
if (error.shouldBeautifyStack) {
277+
return beautifyStack(error.stack).map(line => {
278+
if (nodeInternals.some(internal => internal.test(line))) {
279+
return colors.errorStackInternal(`${figures.pointerSmall} ${line}`);
280+
}
281+
282+
return colors.errorStack(`${figures.pointerSmall} ${line}`);
283+
}).join('\n');
284+
}
285+
286+
return error.stack;
287+
}
288+
272289
writePendingTests(evt) {
273290
for (const [file, testsInFile] of evt.pendingTests) {
274291
if (testsInFile.size === 0) {

lib/runner.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ class Runner extends Emittery {
347347
this.emit('stateChange', {
348348
type: 'test-failed',
349349
title: result.title,
350-
err: serializeError('Test failure', true, result.error),
350+
err: serializeError('Test failure', true, result.error, this.file),
351351
duration: result.duration,
352352
knownFailing: result.metadata.failing,
353353
logs: result.logs

lib/serialize-error.js

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ const path = require('path');
33
const cleanYamlObject = require('clean-yaml-object');
44
const concordance = require('concordance');
55
const isError = require('is-error');
6+
const slash = require('slash');
67
const StackUtils = require('stack-utils');
78
const assert = require('./assert');
8-
const beautifyStack = require('./beautify-stack');
99
const concordanceOptions = require('./concordance-options').default;
1010

1111
function isAvaAssertionError(source) {
@@ -17,13 +17,29 @@ function filter(propertyName, isRoot) {
1717
}
1818

1919
const stackUtils = new StackUtils();
20-
function extractSource(stack) {
21-
if (!stack) {
20+
function extractSource(stack, testFile) {
21+
if (!stack || !testFile) {
2222
return null;
2323
}
2424

25-
const firstStackLine = stack.split('\n')[0];
26-
return stackUtils.parseLine(firstStackLine);
25+
// Normalize the test file so it matches `callSite.file`.
26+
const relFile = path.relative(process.cwd(), testFile);
27+
const normalizedFile = process.platform === 'win32' ? slash(relFile) : relFile;
28+
for (const line of stack.split('\n')) {
29+
try {
30+
const callSite = stackUtils.parseLine(line);
31+
if (callSite.file === normalizedFile) {
32+
return {
33+
isDependency: false,
34+
isWithinProject: true,
35+
file: path.resolve(process.cwd(), callSite.file),
36+
line: callSite.line
37+
};
38+
}
39+
} catch {}
40+
}
41+
42+
return null;
2743
}
2844

2945
function buildSource(source) {
@@ -51,22 +67,19 @@ function buildSource(source) {
5167
};
5268
}
5369

54-
function trySerializeError(err, shouldBeautifyStack) {
55-
let stack = err.savedError ? err.savedError.stack : err.stack;
56-
57-
if (shouldBeautifyStack) {
58-
stack = beautifyStack(stack);
59-
}
70+
function trySerializeError(err, shouldBeautifyStack, testFile) {
71+
const stack = err.savedError ? err.savedError.stack : err.stack;
6072

6173
const retval = {
6274
avaAssertionError: isAvaAssertionError(err),
6375
nonErrorObject: false,
64-
source: buildSource(extractSource(stack)),
65-
stack
76+
source: extractSource(stack, testFile),
77+
stack,
78+
shouldBeautifyStack
6679
};
6780

6881
if (err.actualStack) {
69-
retval.stack = shouldBeautifyStack ? beautifyStack(err.actualStack) : err.actualStack;
82+
retval.stack = err.actualStack;
7083
}
7184

7285
if (retval.avaAssertionError) {
@@ -133,7 +146,7 @@ function trySerializeError(err, shouldBeautifyStack) {
133146
return retval;
134147
}
135148

136-
function serializeError(origin, shouldBeautifyStack, err) {
149+
function serializeError(origin, shouldBeautifyStack, err, testFile) {
137150
if (!isError(err)) {
138151
return {
139152
avaAssertionError: false,
@@ -143,8 +156,8 @@ function serializeError(origin, shouldBeautifyStack, err) {
143156
}
144157

145158
try {
146-
return trySerializeError(err, shouldBeautifyStack);
147-
} catch (_) {
159+
return trySerializeError(err, shouldBeautifyStack, testFile);
160+
} catch {
148161
const replacement = new Error(`${origin}: Could not serialize error`);
149162
return {
150163
avaAssertionError: false,

0 commit comments

Comments
 (0)