Skip to content

Commit c72f4f2

Browse files
jugglinmikenovemberborn
authored andcommitted
Include anonymous functions in stacktraces (#1508)
Closes #1313.
1 parent eebf26e commit c72f4f2

17 files changed

+196
-223
lines changed

lib/beautify-stack.js

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ let ignoreStackLines = [];
88

99
const avaInternals = /\/ava\/(?:lib\/)?[\w-]+\.js:\d+:\d+\)?$/;
1010
const avaDependencies = /\/node_modules\/(?:bluebird|empower-core|(?:ava\/node_modules\/)?(?:babel-runtime|core-js))\//;
11+
const stackFrameLine = /^.+( \(.+:[0-9]+:[0-9]+\)|:[0-9]+:[0-9]+)$/;
1112

1213
if (!debug.enabled) {
1314
ignoreStackLines = StackUtils.nodeInternals();
@@ -17,21 +18,55 @@ if (!debug.enabled) {
1718

1819
const stackUtils = new StackUtils({internals: ignoreStackLines});
1920

21+
function extractFrames(stack) {
22+
return stack
23+
.split('\n')
24+
.map(line => line.trim())
25+
.filter(line => stackFrameLine.test(line))
26+
.join('\n');
27+
}
28+
29+
/**
30+
* Given a string value of the format generated for the `stack` property of a
31+
* V8 error object, return a string that contains only stack frame information
32+
* for frames that have relevance to the consumer.
33+
*
34+
* For example, given the following string value:
35+
*
36+
* ```
37+
* Error
38+
* at inner (/home/ava/ex.js:7:12)
39+
* at /home/ava/ex.js:12:5
40+
* at outer (/home/ava/ex.js:13:4)
41+
* at Object.<anonymous> (/home/ava/ex.js:14:3)
42+
* at Module._compile (module.js:570:32)
43+
* at Object.Module._extensions..js (module.js:579:10)
44+
* at Module.load (module.js:487:32)
45+
* at tryModuleLoad (module.js:446:12)
46+
* at Function.Module._load (module.js:438:3)
47+
* at Module.runMain (module.js:604:10)
48+
* ```
49+
*
50+
* ...this function returns the following string value:
51+
*
52+
* ```
53+
* inner (/home/ava/ex.js:7:12)
54+
* /home/ava/ex.js:12:5
55+
* outer (/home/ava/ex.js:13:4)
56+
* Object.<anonymous> (/home/ava/ex.js:14:3)
57+
* ```
58+
*/
2059
module.exports = stack => {
2160
if (!stack) {
2261
return '';
2362
}
2463

64+
stack = extractFrames(stack);
2565
// Workaround for https://github.com/tapjs/stack-utils/issues/14
2666
// TODO: fix it in `stack-utils`
2767
stack = cleanStack(stack);
2868

29-
const title = stack.split('\n')[0];
30-
const lines = stackUtils
31-
.clean(stack)
32-
.split('\n')
33-
.map(x => ` ${x}`)
34-
.join('\n');
35-
36-
return `${title}\n${lines}`;
69+
return stackUtils.clean(stack)
70+
// Remove the trailing newline inserted by the `stack-utils` module
71+
.trim();
3772
};

lib/extract-stack.js

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

lib/reporters/mini.js

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ const cross = require('figures').cross;
1111
const indentString = require('indent-string');
1212
const ansiEscapes = require('ansi-escapes');
1313
const trimOffNewlines = require('trim-off-newlines');
14-
const extractStack = require('../extract-stack');
1514
const codeExcerpt = require('../code-excerpt');
1615
const colors = require('../colors');
1716
const formatSerializedError = require('./format-serialized-error');
@@ -207,9 +206,9 @@ class MiniReporter {
207206
}
208207

209208
if (test.error.stack) {
210-
const extracted = extractStack(test.error.stack);
211-
if (extracted.includes('\n')) {
212-
status += '\n' + indentString(colors.errorStack(extracted), 2) + '\n';
209+
const stack = test.error.stack;
210+
if (stack.includes('\n')) {
211+
status += '\n' + indentString(colors.errorStack(stack), 2) + '\n';
213212
}
214213
}
215214

@@ -228,14 +227,14 @@ class MiniReporter {
228227
status += ' ' + colors.error(cross + ' ' + err.message) + '\n\n';
229228
} else {
230229
const title = err.type === 'rejection' ? 'Unhandled Rejection' : 'Uncaught Exception';
231-
let description = err.stack ? err.stack.trimRight() : JSON.stringify(err);
232-
description = description.split('\n');
233-
const errorTitle = err.name ? description[0] : 'Threw non-error: ' + description[0];
234-
const errorStack = description.slice(1).join('\n');
235-
236230
status += ' ' + colors.title(title) + '\n';
237-
status += ' ' + colors.stack(errorTitle) + '\n';
238-
status += colors.errorStack(errorStack) + '\n\n';
231+
232+
if (err.name) {
233+
status += ' ' + colors.stack(err.summary) + '\n';
234+
status += colors.errorStack(err.stack) + '\n\n';
235+
} else {
236+
status += ' Threw non-error: ' + err.summary + '\n';
237+
}
239238
}
240239
});
241240
}

lib/reporters/tap.js

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,6 @@ const format = require('util').format;
33
const indentString = require('indent-string');
44
const stripAnsi = require('strip-ansi');
55
const yaml = require('js-yaml');
6-
const extractStack = require('../extract-stack');
7-
8-
// Parses stack trace and extracts original function name, file name and line
9-
function getSourceFromStack(stack) {
10-
return extractStack(stack).split('\n')[0];
11-
}
126

137
function dumpError(error, includeMessage) {
148
const obj = Object.assign({}, error.object);
@@ -35,7 +29,7 @@ function dumpError(error, includeMessage) {
3529
}
3630

3731
if (error.stack) {
38-
obj.at = getSourceFromStack(error.stack);
32+
obj.at = error.stack.split('\n')[0];
3933
}
4034

4135
return ` ---\n${indentString(yaml.safeDump(obj).trim(), 4)}\n ...`;

lib/reporters/verbose.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ const figures = require('figures');
55
const chalk = require('chalk');
66
const plur = require('plur');
77
const trimOffNewlines = require('trim-off-newlines');
8-
const extractStack = require('../extract-stack');
98
const codeExcerpt = require('../code-excerpt');
109
const colors = require('../colors');
1110
const formatSerializedError = require('./format-serialized-error');
@@ -70,6 +69,7 @@ class VerboseReporter {
7069
let output = colors.error(types[err.type] + ':', err.file) + '\n';
7170

7271
if (err.stack) {
72+
output += ' ' + colors.stack(err.title || err.summary) + '\n';
7373
output += ' ' + colors.stack(err.stack) + '\n';
7474
} else {
7575
output += ' ' + colors.stack(JSON.stringify(err)) + '\n';
@@ -156,9 +156,9 @@ class VerboseReporter {
156156
}
157157

158158
if (test.error.stack) {
159-
const extracted = extractStack(test.error.stack);
160-
if (extracted.includes('\n')) {
161-
output += '\n' + indentString(colors.errorStack(extracted), 2) + '\n';
159+
const stack = test.error.stack;
160+
if (stack.includes('\n')) {
161+
output += '\n' + indentString(colors.errorStack(stack), 2) + '\n';
162162
}
163163
}
164164

lib/serialize-error.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ const cleanYamlObject = require('clean-yaml-object');
44
const StackUtils = require('stack-utils');
55
const assert = require('./assert');
66
const beautifyStack = require('./beautify-stack');
7-
const extractStack = require('./extract-stack');
87

98
function isAvaAssertionError(source) {
109
return source instanceof assert.AssertionError;
@@ -20,7 +19,7 @@ function extractSource(stack) {
2019
return null;
2120
}
2221

23-
const firstStackLine = extractStack(stack).split('\n')[0];
22+
const firstStackLine = stack.split('\n')[0];
2423
return stackUtils.parseLine(firstStackLine);
2524
}
2625
function buildSource(source) {
@@ -90,5 +89,11 @@ module.exports = error => {
9089
}
9190
}
9291

92+
if (typeof error.stack === 'string') {
93+
retval.summary = error.stack.split('\n')[0];
94+
} else {
95+
retval.summary = JSON.stringify(error);
96+
}
97+
9398
return retval;
9499
};

test/beautify-stack.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ const beautifyStack = proxyquire('../lib/beautify-stack', {
1010
}
1111
});
1212

13+
function fooFunc() {
14+
barFunc();
15+
}
16+
17+
function barFunc() {
18+
throw new Error();
19+
}
20+
1321
test('does not strip ava internals and dependencies from stack trace with debug enabled', t => {
1422
const beautify = proxyquire('../lib/beautify-stack', {
1523
debug() {
@@ -44,3 +52,16 @@ test('returns empty string without any arguments', t => {
4452
t.is(beautifyStack(), '');
4553
t.end();
4654
});
55+
56+
test('beautify stack - removes uninteresting lines', t => {
57+
try {
58+
fooFunc();
59+
} catch (err) {
60+
const stack = beautifyStack(err.stack);
61+
t.match(stack, /fooFunc/);
62+
t.match(stack, /barFunc/);
63+
t.match(err.stack, /Module._compile/);
64+
t.notMatch(stack, /Module\._compile/);
65+
t.end();
66+
}
67+
});

test/cli.js

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,10 @@ test('timeout', t => {
9191
});
9292
});
9393

94-
test('throwing a named function will report the to the console', t => {
95-
execCli('fixture/throw-named-function.js', (err, stdout, stderr) => {
94+
test('include anonymous functions in error reports', t => {
95+
execCli('fixture/error-in-anonymous-function.js', (err, stdout, stderr) => {
9696
t.ok(err);
97-
t.match(stderr, /function fooFn\(\) \{\}/);
98-
// TODO(jamestalmage)
99-
// t.ok(/1 uncaught exception[^s]/.test(stdout));
97+
t.match(stderr, /test\/fixture\/error-in-anonymous-function\.js:4:8/);
10098
t.end();
10199
});
102100
});
@@ -202,16 +200,6 @@ test('babel require hook only does not apply to source files', t => {
202200
});
203201
});
204202

205-
test('throwing a anonymous function will report the function to the console', t => {
206-
execCli('fixture/throw-anonymous-function.js', (err, stdout, stderr) => {
207-
t.ok(err);
208-
t.match(stderr, /\(\) => \{\}/);
209-
// TODO(jamestalmage)
210-
// t.ok(/1 uncaught exception[^s]/.test(stdout));
211-
t.end();
212-
});
213-
});
214-
215203
test('pkg-conf: defaults', t => {
216204
execCli([], {dirname: 'fixture/pkg-conf/defaults'}, err => {
217205
t.ifError(err);

test/extract-stack.js

Lines changed: 0 additions & 36 deletions
This file was deleted.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import test from '../../';
2+
3+
const getAnonymousFn = () => () => {
4+
throw new Error();
5+
};
6+
7+
test(t => {
8+
getAnonymousFn()();
9+
t.pass();
10+
});

0 commit comments

Comments
 (0)