Skip to content

Commit abd6ef9

Browse files
committed
test_runner: report failing tests after summary
Re-output failing tests after summary has been printed. This behavior follows other popular test runners (e.g. jest, mocha, etc...). Updated SpecReporter: 1. When there is a 'test:fail' event, the test will be stored. 2. After no more input, all the failed tests will be flushed. 3. Extract the logic for formatting a test report into a re-usable function. Fixes: nodejs#47110
1 parent 30d92e8 commit abd6ef9

File tree

3 files changed

+108
-17
lines changed

3 files changed

+108
-17
lines changed

lib/internal/test_runner/reporter/spec.js

+41-17
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
const {
44
ArrayPrototypeJoin,
55
ArrayPrototypePop,
6+
ArrayPrototypePush,
67
ArrayPrototypeShift,
78
ArrayPrototypeUnshift,
89
hardenRegExp,
@@ -36,6 +37,7 @@ class SpecReporter extends Transform {
3637
#stack = [];
3738
#reported = [];
3839
#indentMemo = new SafeMap();
40+
#failedTests = [];
3941

4042
constructor() {
4143
super({ writableObjectMode: true });
@@ -57,15 +59,29 @@ class SpecReporter extends Transform {
5759
RegExpPrototypeSymbolSplit(
5860
hardenRegExp(/\r?\n/),
5961
inspectWithNoCustomRetry(err, inspectOptions),
60-
), `\n${indent} `);
61-
return `\n${indent} ${message}\n`;
62+
), indent !== undefined ? `\n${indent} ` : '');
63+
return `${indent !== undefined ? `\n${indent} ` : '\n'}${message}\n`;
6264
}
63-
#handleEvent({ type, data }) {
65+
#formatTestReport(type, data, prefix = '', indent = undefined, hasChildren = false, skippedSubtest = false, showFilePath = false) {
6466
let color = colors[type] ?? white;
6567
let symbol = symbols[type] ?? ' ';
66-
68+
const duration_ms = data.details?.duration_ms ? ` ${gray}(${data.details.duration_ms}ms)${white}` : '';
69+
const title = `${showFilePath ? `${data.file} ` : ''}${data.name}${duration_ms}${skippedSubtest ? ' # SKIP' : ''}`;
70+
if (hasChildren) {
71+
// If this test has had children - it was already reported, so slightly modify the output
72+
return `${prefix}${indent ?? ''}${color}${symbols['arrow:right']}${white}${title}\n`;
73+
}
74+
const error = this.#formatError(data.details?.error, indent);
75+
if (skippedSubtest) {
76+
color = gray;
77+
symbol = symbols['hyphen:minus'];
78+
}
79+
return `${prefix}${indent ?? ''}${color}${symbol}${title}${error}${white}`;
80+
}
81+
#handleEvent({ type, data }) {
6782
switch (type) {
6883
case 'test:fail':
84+
ArrayPrototypePush(this.#failedTests, data);
6985
case 'test:pass': {
7086
const subtest = ArrayPrototypeShift(this.#stack); // This is the matching `test:start` event
7187
if (subtest) {
@@ -82,32 +98,40 @@ class SpecReporter extends Transform {
8298
ArrayPrototypeUnshift(this.#reported, msg);
8399
prefix += `${this.#indent(msg.nesting)}${symbols['arrow:right']}${msg.name}\n`;
84100
}
85-
const skippedSubtest = subtest && data.skip && data.skip !== undefined;
86-
const indent = this.#indent(data.nesting);
87-
const duration_ms = data.details?.duration_ms ? ` ${gray}(${data.details.duration_ms}ms)${white}` : '';
88-
const title = `${data.name}${duration_ms}${skippedSubtest ? ' # SKIP' : ''}`;
101+
let hasChildren = false;
89102
if (this.#reported[0] && this.#reported[0].nesting === data.nesting && this.#reported[0].name === data.name) {
90-
// If this test has had children - it was already reported, so slightly modify the output
91103
ArrayPrototypeShift(this.#reported);
92-
return `${prefix}${indent}${color}${symbols['arrow:right']}${white}${title}\n\n`;
93-
}
94-
const error = this.#formatError(data.details?.error, indent);
95-
if (skippedSubtest) {
96-
color = gray;
97-
symbol = symbols['hyphen:minus'];
104+
hasChildren = true;
98105
}
99-
return `${prefix}${indent}${color}${symbol}${title}${error}${white}\n`;
106+
const skippedSubtest = subtest && data.skip && data.skip !== undefined;
107+
const indent = this.#indent(data.nesting);
108+
return `${this.#formatTestReport(type, data, prefix, indent, hasChildren, skippedSubtest)}\n`;
100109
}
101110
case 'test:start':
102111
ArrayPrototypeUnshift(this.#stack, { __proto__: null, data, type });
103112
break;
104113
case 'test:diagnostic':
105-
return `${color}${this.#indent(data.nesting)}${symbol}${data.message}${white}\n`;
114+
return `${colors[type]}${this.#indent(data.nesting)}${symbols[type]}${data.message}${white}\n`;
106115
}
107116
}
108117
_transform({ type, data }, encoding, callback) {
109118
callback(null, this.#handleEvent({ type, data }));
110119
}
120+
_flush(callback) {
121+
const results = [`\n${colors['test:fail']}${symbols['test:fail']}failing tests:${white}\n`];
122+
for (let i = 0; i < this.#failedTests.length; i++) {
123+
ArrayPrototypePush(results, this.#formatTestReport(
124+
'test:fail',
125+
this.#failedTests[i],
126+
'',
127+
undefined,
128+
false,
129+
false,
130+
true,
131+
));
132+
}
133+
callback(null, ArrayPrototypeJoin(results, ''));
134+
}
111135
}
112136

113137
module.exports = SpecReporter;

test/message/test_runner_output_spec_reporter.out

+62
Original file line numberDiff line numberDiff line change
@@ -282,3 +282,65 @@
282282
skipped 10
283283
todo 5
284284
duration_ms *
285+
286+
failing tests:
287+
* sync fail todo (*ms)
288+
*
289+
* sync fail todo with message (*ms)
290+
*
291+
* sync throw fail (*ms)
292+
*
293+
* async throw fail (*ms)
294+
*
295+
* async skip fail (*ms)
296+
*
297+
* async assertion fail (*ms)
298+
*
299+
* reject fail (*ms)
300+
*
301+
* +sync throw fail (*ms)
302+
*
303+
* subtest sync throw fail (*ms)
304+
'1 subtest failed'
305+
* sync throw non-error fail (*ms)
306+
Symbol(thrown symbol from sync throw non-error fail)
307+
* +long running (*ms)
308+
'test did not finish before its parent and was cancelled'
309+
* top level (*ms)
310+
'1 subtest failed'
311+
* sync skip option is false fail (*ms)
312+
*
313+
* callback fail (*ms)
314+
*
315+
* callback also returns a Promise (*ms)
316+
'passed a callback but also returned a Promise'
317+
* callback throw (*ms)
318+
*
319+
* callback called twice (*ms)
320+
'callback invoked multiple times'
321+
* callback called twice in future tick (*ms)
322+
*
323+
* callback async throw (*ms)
324+
*
325+
* custom inspect symbol fail (*ms)
326+
customized
327+
* custom inspect symbol that throws fail (*ms)
328+
{ foo: 1, [Symbol(nodejs.util.inspect.custom)]: [Function: [nodejs.util.inspect.custom]] }
329+
* sync throw fails at first (*ms)
330+
*
331+
* sync throw fails at second (*ms)
332+
*
333+
* subtest sync throw fails (*ms)
334+
'2 subtests failed'
335+
* timed out async test (*ms)
336+
'test timed out after 5ms'
337+
* timed out callback test (*ms)
338+
'test timed out after 5ms'
339+
* rejected thenable (*ms)
340+
'custom error'
341+
* unfinished test with uncaughtException (*ms)
342+
*
343+
* unfinished test with unhandledRejection (*ms)
344+
*
345+
* invalid subtest fail (*ms)
346+
'test could not be started because its parent finished'

test/pseudo-tty/test_runner_default_reporter.out

+5
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,8 @@
1717
[34m* skipped 1[39m
1818
[34m* todo 0[39m
1919
[34m* duration_ms *[39m
20+
21+
[31m* failing tests:[39m
22+
[31m* should fail [90m(*ms)[39m
23+
*
24+
[39m

0 commit comments

Comments
 (0)