Skip to content

Commit 3e7f8e8

Browse files
HinataKah0RafaelGSS
authored andcommitted
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: #47110 PR-URL: #47164 Reviewed-By: Moshe Atlow <moshe@atlow.co.il> Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
1 parent 2c2b07c commit 3e7f8e8

File tree

4 files changed

+315
-40
lines changed

4 files changed

+315
-40
lines changed

lib/internal/test_runner/reporter/spec.js

+57-35
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 });
@@ -60,54 +62,74 @@ class SpecReporter extends Transform {
6062
), `\n${indent} `);
6163
return `\n${indent} ${message}\n`;
6264
}
63-
#handleEvent({ type, data }) {
65+
#formatTestReport(type, data, prefix = '', indent = '', hasChildren = false, skippedSubtest = 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 = `${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}${white}${error}`;
80+
}
81+
#handleTestReportEvent(type, data) {
82+
const subtest = ArrayPrototypeShift(this.#stack); // This is the matching `test:start` event
83+
if (subtest) {
84+
assert(subtest.type === 'test:start');
85+
assert(subtest.data.nesting === data.nesting);
86+
assert(subtest.data.name === data.name);
87+
}
88+
let prefix = '';
89+
while (this.#stack.length) {
90+
// Report all the parent `test:start` events
91+
const parent = ArrayPrototypePop(this.#stack);
92+
assert(parent.type === 'test:start');
93+
const msg = parent.data;
94+
ArrayPrototypeUnshift(this.#reported, msg);
95+
prefix += `${this.#indent(msg.nesting)}${symbols['arrow:right']}${msg.name}\n`;
96+
}
97+
let hasChildren = false;
98+
if (this.#reported[0] && this.#reported[0].nesting === data.nesting && this.#reported[0].name === data.name) {
99+
ArrayPrototypeShift(this.#reported);
100+
hasChildren = true;
101+
}
102+
const skippedSubtest = subtest && data.skip && data.skip !== undefined;
103+
const indent = this.#indent(data.nesting);
104+
return `${this.#formatTestReport(type, data, prefix, indent, hasChildren, skippedSubtest)}\n`;
105+
}
106+
#handleEvent({ type, data }) {
67107
switch (type) {
68108
case 'test:fail':
69-
case 'test:pass': {
70-
const subtest = ArrayPrototypeShift(this.#stack); // This is the matching `test:start` event
71-
if (subtest) {
72-
assert(subtest.type === 'test:start');
73-
assert(subtest.data.nesting === data.nesting);
74-
assert(subtest.data.name === data.name);
75-
}
76-
let prefix = '';
77-
while (this.#stack.length) {
78-
// Report all the parent `test:start` events
79-
const parent = ArrayPrototypePop(this.#stack);
80-
assert(parent.type === 'test:start');
81-
const msg = parent.data;
82-
ArrayPrototypeUnshift(this.#reported, msg);
83-
prefix += `${this.#indent(msg.nesting)}${symbols['arrow:right']}${msg.name}\n`;
84-
}
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' : ''}`;
89-
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
91-
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'];
98-
}
99-
return `${prefix}${indent}${color}${symbol}${title}${error}${white}\n`;
100-
}
109+
ArrayPrototypePush(this.#failedTests, data);
110+
return this.#handleTestReportEvent(type, data);
111+
case 'test:pass':
112+
return this.#handleTestReportEvent(type, data);
101113
case 'test:start':
102114
ArrayPrototypeUnshift(this.#stack, { __proto__: null, data, type });
103115
break;
104116
case 'test:diagnostic':
105-
return `${color}${this.#indent(data.nesting)}${symbol}${data.message}${white}\n`;
117+
return `${colors[type]}${this.#indent(data.nesting)}${symbols[type]}${data.message}${white}\n`;
106118
}
107119
}
108120
_transform({ type, data }, encoding, callback) {
109121
callback(null, this.#handleEvent({ type, data }));
110122
}
123+
_flush(callback) {
124+
const results = [`\n${colors['test:fail']}${symbols['test:fail']}failing tests:${white}\n`];
125+
for (let i = 0; i < this.#failedTests.length; i++) {
126+
ArrayPrototypePush(results, this.#formatTestReport(
127+
'test:fail',
128+
this.#failedTests[i],
129+
));
130+
}
131+
callback(null, ArrayPrototypeJoin(results, '\n'));
132+
}
111133
}
112134

113135
module.exports = SpecReporter;

test/message/test_runner_output_spec_reporter.out

+209
Original file line numberDiff line numberDiff line change
@@ -283,3 +283,212 @@
283283
skipped 10
284284
todo 5
285285
duration_ms *
286+
287+
failing tests:
288+
289+
sync fail todo (*ms)
290+
Error: thrown from sync fail todo
291+
*
292+
*
293+
*
294+
*
295+
*
296+
*
297+
*
298+
299+
sync fail todo with message (*ms)
300+
Error: thrown from sync fail todo with message
301+
*
302+
*
303+
*
304+
*
305+
*
306+
*
307+
*
308+
309+
sync throw fail (*ms)
310+
Error: thrown from sync throw fail
311+
*
312+
*
313+
*
314+
*
315+
*
316+
*
317+
*
318+
319+
async throw fail (*ms)
320+
Error: thrown from async throw fail
321+
*
322+
*
323+
*
324+
*
325+
*
326+
*
327+
*
328+
329+
async skip fail (*ms)
330+
Error: thrown from async throw fail
331+
*
332+
*
333+
*
334+
*
335+
*
336+
*
337+
*
338+
339+
async assertion fail (*ms)
340+
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
341+
342+
true !== false
343+
344+
*
345+
*
346+
*
347+
*
348+
*
349+
*
350+
* {
351+
generatedMessage: true,
352+
code: 'ERR_ASSERTION',
353+
actual: true,
354+
expected: false,
355+
operator: 'strictEqual'
356+
}
357+
358+
reject fail (*ms)
359+
Error: rejected from reject fail
360+
*
361+
*
362+
*
363+
*
364+
*
365+
*
366+
*
367+
368+
+sync throw fail (*ms)
369+
Error: thrown from subtest sync throw fail
370+
*
371+
*
372+
*
373+
*
374+
*
375+
*
376+
*
377+
*
378+
*
379+
*
380+
381+
subtest sync throw fail (*ms)
382+
'1 subtest failed'
383+
384+
sync throw non-error fail (*ms)
385+
Symbol(thrown symbol from sync throw non-error fail)
386+
387+
+long running (*ms)
388+
'test did not finish before its parent and was cancelled'
389+
390+
top level (*ms)
391+
'1 subtest failed'
392+
393+
sync skip option is false fail (*ms)
394+
Error: this should be executed
395+
*
396+
*
397+
*
398+
*
399+
*
400+
*
401+
*
402+
403+
callback fail (*ms)
404+
Error: callback failure
405+
*
406+
*
407+
408+
callback also returns a Promise (*ms)
409+
'passed a callback but also returned a Promise'
410+
411+
callback throw (*ms)
412+
Error: thrown from callback throw
413+
*
414+
*
415+
*
416+
*
417+
*
418+
*
419+
*
420+
421+
callback called twice (*ms)
422+
'callback invoked multiple times'
423+
424+
callback called twice in future tick (*ms)
425+
Error [ERR_TEST_FAILURE]: callback invoked multiple times
426+
*
427+
failureType: 'multipleCallbackInvocations',
428+
cause: 'callback invoked multiple times',
429+
code: 'ERR_TEST_FAILURE'
430+
}
431+
432+
callback async throw (*ms)
433+
Error: thrown from callback async throw
434+
*
435+
*
436+
437+
custom inspect symbol fail (*ms)
438+
customized
439+
440+
custom inspect symbol that throws fail (*ms)
441+
{ foo: 1, [Symbol(nodejs.util.inspect.custom)]: [Function: [nodejs.util.inspect.custom]] }
442+
443+
sync throw fails at first (*ms)
444+
Error: thrown from subtest sync throw fails at first
445+
*
446+
*
447+
*
448+
*
449+
*
450+
*
451+
*
452+
*
453+
*
454+
*
455+
456+
sync throw fails at second (*ms)
457+
Error: thrown from subtest sync throw fails at second
458+
*
459+
*
460+
*
461+
*
462+
*
463+
*
464+
*
465+
*
466+
*
467+
*
468+
469+
subtest sync throw fails (*ms)
470+
'2 subtests failed'
471+
472+
timed out async test (*ms)
473+
'test timed out after 5ms'
474+
475+
timed out callback test (*ms)
476+
'test timed out after 5ms'
477+
478+
rejected thenable (*ms)
479+
'custom error'
480+
481+
unfinished test with uncaughtException (*ms)
482+
Error: foo
483+
*
484+
*
485+
*
486+
487+
unfinished test with unhandledRejection (*ms)
488+
Error: bar
489+
*
490+
*
491+
*
492+
493+
invalid subtest fail (*ms)
494+
'test could not be started because its parent finished'

test/pseudo-tty/test_runner_default_reporter.js

+4
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,7 @@ const test = require('node:test');
99
test('should pass', () => {});
1010
test('should fail', () => { throw new Error('fail'); });
1111
test('should skip', { skip: true }, () => {});
12+
test('parent', () => {
13+
test('should fail', () => { throw new Error('fail'); });
14+
test('should pass but parent fail', () => {});
15+
});

0 commit comments

Comments
 (0)