Skip to content

Commit 3267b3c

Browse files
mihir254targos
authored andcommitted
test_runner: display failed test stack trace with dot reporter
PR-URL: #52655 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Moshe Atlow <moshe@atlow.co.il> Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
1 parent 7d7a762 commit 3267b3c

File tree

7 files changed

+376
-84
lines changed

7 files changed

+376
-84
lines changed

lib/internal/test_runner/reporter/dot.js

+12-2
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
'use strict';
2-
32
const {
3+
ArrayPrototypePush,
44
MathMax,
55
} = primordials;
6+
const colors = require('internal/util/colors');
7+
const { formatTestReport } = require('internal/test_runner/reporter/utils');
68

79
module.exports = async function* dot(source) {
810
let count = 0;
911
let columns = getLineLength();
10-
for await (const { type } of source) {
12+
const failedTests = [];
13+
for await (const { type, data } of source) {
1114
if (type === 'test:pass') {
1215
yield '.';
1316
}
1417
if (type === 'test:fail') {
1518
yield 'X';
19+
ArrayPrototypePush(failedTests, data);
1620
}
1721
if ((type === 'test:fail' || type === 'test:pass') && ++count === columns) {
1822
yield '\n';
@@ -23,6 +27,12 @@ module.exports = async function* dot(source) {
2327
}
2428
}
2529
yield '\n';
30+
if (failedTests.length > 0) {
31+
yield `\n${colors.red}Failed tests:${colors.white}\n\n`;
32+
for (const test of failedTests) {
33+
yield formatTestReport('test:fail', test);
34+
}
35+
}
2636
};
2737

2838
function getLineLength() {

lib/internal/test_runner/reporter/spec.js

+14-75
Original file line numberDiff line numberDiff line change
@@ -1,97 +1,35 @@
11
'use strict';
2-
32
const {
43
ArrayPrototypeJoin,
54
ArrayPrototypePop,
65
ArrayPrototypePush,
76
ArrayPrototypeShift,
87
ArrayPrototypeUnshift,
9-
RegExpPrototypeSymbolSplit,
10-
SafeMap,
11-
StringPrototypeRepeat,
12-
hardenRegExp,
138
} = primordials;
149
const assert = require('assert');
1510
const Transform = require('internal/streams/transform');
16-
const { inspectWithNoCustomRetry } = require('internal/errors');
1711
const colors = require('internal/util/colors');
1812
const { kSubtestsFailed } = require('internal/test_runner/test');
1913
const { getCoverageReport } = require('internal/test_runner/utils');
2014
const { relative } = require('path');
15+
const {
16+
formatTestReport,
17+
indent,
18+
reporterColorMap,
19+
reporterUnicodeSymbolMap,
20+
} = require('internal/test_runner/reporter/utils');
2121

22-
const symbols = {
23-
'__proto__': null,
24-
'test:fail': '\u2716 ',
25-
'test:pass': '\u2714 ',
26-
'test:diagnostic': '\u2139 ',
27-
'test:coverage': '\u2139 ',
28-
'arrow:right': '\u25B6 ',
29-
'hyphen:minus': '\uFE63 ',
30-
};
3122
class SpecReporter extends Transform {
3223
#stack = [];
3324
#reported = [];
34-
#indentMemo = new SafeMap();
3525
#failedTests = [];
3626
#cwd = process.cwd();
37-
#inspectOptions;
38-
#colors;
3927

4028
constructor() {
4129
super({ __proto__: null, writableObjectMode: true });
4230
colors.refresh();
43-
this.#inspectOptions = { __proto__: null, colors: colors.shouldColorize(process.stdout), breakLength: Infinity };
44-
this.#colors = {
45-
'__proto__': null,
46-
'test:fail': colors.red,
47-
'test:pass': colors.green,
48-
'test:diagnostic': colors.blue,
49-
};
5031
}
5132

52-
#indent(nesting) {
53-
let value = this.#indentMemo.get(nesting);
54-
if (value === undefined) {
55-
value = StringPrototypeRepeat(' ', nesting);
56-
this.#indentMemo.set(nesting, value);
57-
}
58-
59-
return value;
60-
}
61-
#formatError(error, indent) {
62-
if (!error) return '';
63-
const err = error.code === 'ERR_TEST_FAILURE' ? error.cause : error;
64-
const message = ArrayPrototypeJoin(
65-
RegExpPrototypeSymbolSplit(
66-
hardenRegExp(/\r?\n/),
67-
inspectWithNoCustomRetry(err, this.#inspectOptions),
68-
), `\n${indent} `);
69-
return `\n${indent} ${message}\n`;
70-
}
71-
#formatTestReport(type, data, prefix = '', indent = '', hasChildren = false) {
72-
let color = this.#colors[type] ?? colors.white;
73-
let symbol = symbols[type] ?? ' ';
74-
const { skip, todo } = data;
75-
const duration_ms = data.details?.duration_ms ? ` ${colors.gray}(${data.details.duration_ms}ms)${colors.white}` : '';
76-
let title = `${data.name}${duration_ms}`;
77-
78-
if (skip !== undefined) {
79-
title += ` # ${typeof skip === 'string' && skip.length ? skip : 'SKIP'}`;
80-
} else if (todo !== undefined) {
81-
title += ` # ${typeof todo === 'string' && todo.length ? todo : 'TODO'}`;
82-
}
83-
const error = this.#formatError(data.details?.error, indent);
84-
if (hasChildren) {
85-
// If this test has had children - it was already reported, so slightly modify the output
86-
const err = !error || data.details?.error?.failureType === 'subtestsFailed' ? '' : `\n${error}`;
87-
return `${prefix}${indent}${color}${symbols['arrow:right']}${colors.white}${title}${err}`;
88-
}
89-
if (skip !== undefined) {
90-
color = colors.gray;
91-
symbol = symbols['hyphen:minus'];
92-
}
93-
return `${prefix}${indent}${color}${symbol}${title}${colors.white}${error}`;
94-
}
9533
#handleTestReportEvent(type, data) {
9634
const subtest = ArrayPrototypeShift(this.#stack); // This is the matching `test:start` event
9735
if (subtest) {
@@ -106,15 +44,15 @@ class SpecReporter extends Transform {
10644
assert(parent.type === 'test:start');
10745
const msg = parent.data;
10846
ArrayPrototypeUnshift(this.#reported, msg);
109-
prefix += `${this.#indent(msg.nesting)}${symbols['arrow:right']}${msg.name}\n`;
47+
prefix += `${indent(msg.nesting)}${reporterUnicodeSymbolMap['arrow:right']}${msg.name}\n`;
11048
}
11149
let hasChildren = false;
11250
if (this.#reported[0] && this.#reported[0].nesting === data.nesting && this.#reported[0].name === data.name) {
11351
ArrayPrototypeShift(this.#reported);
11452
hasChildren = true;
11553
}
116-
const indent = this.#indent(data.nesting);
117-
return `${this.#formatTestReport(type, data, prefix, indent, hasChildren)}\n`;
54+
const indentation = indent(data.nesting);
55+
return `${formatTestReport(type, data, prefix, indentation, hasChildren)}\n`;
11856
}
11957
#handleEvent({ type, data }) {
12058
switch (type) {
@@ -132,9 +70,10 @@ class SpecReporter extends Transform {
13270
case 'test:stdout':
13371
return data.message;
13472
case 'test:diagnostic':
135-
return `${this.#colors[type]}${this.#indent(data.nesting)}${symbols[type]}${data.message}${colors.white}\n`;
73+
return `${reporterColorMap[type]}${indent(data.nesting)}${reporterUnicodeSymbolMap[type]}${data.message}${colors.white}\n`;
13674
case 'test:coverage':
137-
return getCoverageReport(this.#indent(data.nesting), data.summary, symbols['test:coverage'], colors.blue, true);
75+
return getCoverageReport(indent(data.nesting), data.summary,
76+
reporterUnicodeSymbolMap['test:coverage'], colors.blue, true);
13877
}
13978
}
14079
_transform({ type, data }, encoding, callback) {
@@ -145,10 +84,10 @@ class SpecReporter extends Transform {
14584
callback(null, '');
14685
return;
14786
}
148-
const results = [`\n${this.#colors['test:fail']}${symbols['test:fail']}failing tests:${colors.white}\n`];
87+
const results = [`\n${reporterColorMap['test:fail']}${reporterUnicodeSymbolMap['test:fail']}failing tests:${colors.white}\n`];
14988
for (let i = 0; i < this.#failedTests.length; i++) {
15089
const test = this.#failedTests[i];
151-
const formattedErr = this.#formatTestReport('test:fail', test);
90+
const formattedErr = formatTestReport('test:fail', test);
15291

15392
if (test.file) {
15493
const relPath = relative(this.#cwd, test.file);
+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
'use strict';
2+
const {
3+
ArrayPrototypeJoin,
4+
RegExpPrototypeSymbolSplit,
5+
SafeMap,
6+
StringPrototypeRepeat,
7+
hardenRegExp,
8+
} = primordials;
9+
const colors = require('internal/util/colors');
10+
const { inspectWithNoCustomRetry } = require('internal/errors');
11+
const indentMemo = new SafeMap();
12+
13+
const inspectOptions = {
14+
__proto__: null,
15+
colors: colors.shouldColorize(process.stdout),
16+
breakLength: Infinity,
17+
};
18+
19+
const reporterUnicodeSymbolMap = {
20+
'__proto__': null,
21+
'test:fail': '\u2716 ',
22+
'test:pass': '\u2714 ',
23+
'test:diagnostic': '\u2139 ',
24+
'test:coverage': '\u2139 ',
25+
'arrow:right': '\u25B6 ',
26+
'hyphen:minus': '\uFE63 ',
27+
};
28+
29+
const reporterColorMap = {
30+
'__proto__': null,
31+
get 'test:fail'() {
32+
return colors.red;
33+
},
34+
get 'test:pass'() {
35+
return colors.green;
36+
},
37+
get 'test:diagnostic'() {
38+
return colors.blue;
39+
},
40+
};
41+
42+
function indent(nesting) {
43+
let value = indentMemo.get(nesting);
44+
if (value === undefined) {
45+
value = StringPrototypeRepeat(' ', nesting);
46+
indentMemo.set(nesting, value);
47+
}
48+
return value;
49+
}
50+
51+
function formatError(error, indent) {
52+
if (!error) return '';
53+
const err = error.code === 'ERR_TEST_FAILURE' ? error.cause : error;
54+
const message = ArrayPrototypeJoin(
55+
RegExpPrototypeSymbolSplit(
56+
hardenRegExp(/\r?\n/),
57+
inspectWithNoCustomRetry(err, inspectOptions),
58+
), `\n${indent} `);
59+
return `\n${indent} ${message}\n`;
60+
}
61+
62+
function formatTestReport(type, data, prefix = '', indent = '', hasChildren = false) {
63+
let color = reporterColorMap[type] ?? colors.white;
64+
let symbol = reporterUnicodeSymbolMap[type] ?? ' ';
65+
const { skip, todo } = data;
66+
const duration_ms = data.details?.duration_ms ? ` ${colors.gray}(${data.details.duration_ms}ms)${colors.white}` : '';
67+
let title = `${data.name}${duration_ms}`;
68+
69+
if (skip !== undefined) {
70+
title += ` # ${typeof skip === 'string' && skip.length ? skip : 'SKIP'}`;
71+
} else if (todo !== undefined) {
72+
title += ` # ${typeof todo === 'string' && todo.length ? todo : 'TODO'}`;
73+
}
74+
const error = formatError(data.details?.error, indent);
75+
if (hasChildren) {
76+
// If this test has had children - it was already reported, so slightly modify the output
77+
const err = !error || data.details?.error?.failureType === 'subtestsFailed' ? '' : `\n${error}`;
78+
return `${prefix}${indent}${color}${reporterUnicodeSymbolMap['arrow:right']}${colors.white}${title}${err}`;
79+
}
80+
if (skip !== undefined) {
81+
color = colors.gray;
82+
symbol = reporterUnicodeSymbolMap['hyphen:minus'];
83+
}
84+
return `${prefix}${indent}${color}${symbol}${title}${colors.white}${error}`;
85+
}
86+
87+
module.exports = {
88+
__proto__: null,
89+
reporterUnicodeSymbolMap,
90+
reporterColorMap,
91+
formatTestReport,
92+
indent,
93+
};

0 commit comments

Comments
 (0)