Skip to content

Commit 1e13b97

Browse files
committed
test: fix test runner does not print error cause
1 parent 143deae commit 1e13b97

8 files changed

+557
-23
lines changed

lib/internal/test_runner/reporter/spec.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,12 @@ class SpecReporter extends Transform {
7171
// If this test has had children - it was already reported, so slightly modify the output
7272
return `${prefix}${indent}${color}${symbols['arrow:right']}${white}${title}\n`;
7373
}
74-
const error = this.#formatError(data.details?.error, indent);
74+
let error = '';
75+
try {
76+
error = this.#formatError(data.details?.error, indent);
77+
} catch (err) {
78+
error = this.#formatError(err, indent);
79+
}
7580
if (skippedSubtest) {
7681
color = gray;
7782
symbol = symbols['hyphen:minus'];

lib/internal/test_runner/reporter/tap.js

+33-5
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,34 @@ const {
33
ArrayPrototypeForEach,
44
ArrayPrototypeJoin,
55
ArrayPrototypePush,
6+
ArrayPrototypeSome,
67
NumberPrototypeToFixed,
78
ObjectEntries,
9+
ObjectPrototypeHasOwnProperty,
810
RegExpPrototypeSymbolReplace,
911
SafeMap,
12+
StringPrototypeRepeat,
1013
StringPrototypeReplaceAll,
1114
StringPrototypeSplit,
12-
StringPrototypeRepeat,
15+
StringPrototypeStartsWith,
1316
} = primordials;
1417
const { inspectWithNoCustomRetry } = require('internal/errors');
1518
const { isError, kEmptyObject } = require('internal/util');
19+
const { inspect } = require('internal/util/inspect');
1620
const { relative } = require('path');
1721
const kDefaultIndent = ' '; // 4 spaces
18-
const kFrameStartRegExp = /^ {4}at /;
22+
const kFrameStartRegExp = /[{}]/;
1923
const kLineBreakRegExp = /\n|\r\n/;
2024
const kDefaultTAPVersion = 13;
2125
const inspectOptions = { colors: false, breakLength: Infinity };
2226
let testModule; // Lazy loaded due to circular dependency.
27+
const keywords = [
28+
'...',
29+
'failureType',
30+
'cause',
31+
'code',
32+
'Error',
33+
];
2334

2435
function lazyLoadTest() {
2536
testModule ??= require('internal/test_runner/test');
@@ -210,7 +221,7 @@ function jsToYaml(indent, name, value) {
210221
// If the ERR_TEST_FAILURE came from an error provided by user code,
211222
// then try to unwrap the original error message and stack.
212223
if (code === 'ERR_TEST_FAILURE' && kUnwrapErrors.has(failureType)) {
213-
errStack = cause?.stack ?? errStack;
224+
errStack = getErrorStack(cause, errStack);
214225
errCode = cause?.code ?? errCode;
215226
errName = cause?.name ?? errName;
216227
if (isAssertionLike(cause)) {
@@ -249,11 +260,17 @@ function jsToYaml(indent, name, value) {
249260
(frame) => {
250261
const processed = RegExpPrototypeSymbolReplace(
251262
kFrameStartRegExp,
252-
frame,
263+
ArrayPrototypeJoin(
264+
StringPrototypeSplit(frame, ' at'),
265+
'',
266+
),
253267
'',
254268
);
255269

256-
if (processed.length > 0 && processed.length !== frame.length) {
270+
const trimmedProcessed = processed.trim();
271+
if (trimmedProcessed.length > 0 && processed !== kDefaultIndent && (processed.length !== frame.length ||
272+
StringPrototypeStartsWith(trimmedProcessed, '[cause]')) &&
273+
!ArrayPrototypeSome(keywords, (str) => StringPrototypeStartsWith(processed, str))) {
257274
ArrayPrototypePush(frames, processed);
258275
}
259276
},
@@ -275,4 +292,15 @@ function isAssertionLike(value) {
275292
return value && typeof value === 'object' && 'expected' in value && 'actual' in value;
276293
}
277294

295+
function getErrorStack(cause, errStack) {
296+
try {
297+
if (cause !== null && ObjectPrototypeHasOwnProperty(cause, 'cause')) {
298+
return inspect(cause);
299+
}
300+
return cause?.stack ?? errStack;
301+
} catch (err) {
302+
return inspect(err);
303+
}
304+
}
305+
278306
module.exports = tapReporter;

test/message/test_runner_describe_it.out

+3-3
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,9 @@ not ok 14 - async assertion fail
118118
failureType: 'testCodeFailure'
119119
error: |-
120120
Expected values to be strictly equal:
121-
121+
122122
true !== false
123-
123+
124124
code: 'ERR_ASSERTION'
125125
name: 'AssertionError'
126126
expected: false
@@ -569,7 +569,7 @@ not ok 56 - describe async throw fails
569569
error: 'test timed out after 5ms'
570570
code: 'ERR_TEST_FAILURE'
571571
stack: |-
572-
async Promise.all (index 0)
572+
async Promise.all (index 0)
573573
...
574574
# Subtest: timed out callback test
575575
not ok 2 - timed out callback test

test/message/test_runner_output.js

+40
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,46 @@ test('unfinished test with unhandledRejection', async () => {
384384
});
385385
});
386386

387+
test('should print error cause', () => {
388+
Error.stackTraceLimit = 4;
389+
throw new Error('foo', { cause: new Error('bar') });
390+
});
391+
392+
test('should print error cause for nested errors', () => {
393+
throw new Error('a', { cause: new Error('b', { cause:
394+
new Error('c', { cause: new Error('d', { cause: new Error('e') }) }) }) });
395+
});
396+
397+
test('should handle cycles in error', () => {
398+
const outer = new Error('b', { cause: null });
399+
outer.cause = new Error('c', { cause: new Error('d', { cause: outer }) });
400+
throw outer;
401+
});
402+
403+
test('should handle primitive, undefined and null cause', () => {
404+
test('primitive cause', () => {
405+
throw new Error('foo', { cause: 'something went wrong' });
406+
});
407+
test('undefined cause', () => {
408+
throw new Error('foo', { cause: undefined });
409+
});
410+
test('null cause', () => {
411+
throw new Error('foo', { cause: null });
412+
});
413+
});
414+
415+
test('should handle case when cause throw', () => {
416+
const error = new Error();
417+
Reflect.defineProperty(error, 'cause', { get() { throw new Error('bar'); } });
418+
throw error;
419+
});
420+
421+
test('should handle case when stack throw', () => {
422+
const error = new Error();
423+
Reflect.defineProperty(error, 'stack', { get() { throw new Error('bar'); } });
424+
throw error;
425+
});
426+
387427
// Verify that uncaught exceptions outside of any tests are handled after the
388428
// test harness has finished bootstrapping itself.
389429
setImmediate(() => {

test/message/test_runner_output.out

+142-5
Original file line numberDiff line numberDiff line change
@@ -624,8 +624,145 @@ not ok 64 - unfinished test with unhandledRejection
624624
*
625625
*
626626
...
627+
# Subtest: should print error cause
628+
not ok 65 - should print error cause
629+
---
630+
duration_ms: *
631+
failureType: 'testCodeFailure'
632+
error: 'foo'
633+
code: 'ERR_TEST_FAILURE'
634+
stack: |-
635+
*
636+
*
637+
*
638+
*
639+
[cause]: Error: bar
640+
*
641+
*
642+
*
643+
*
644+
...
645+
# Subtest: should print error cause for nested errors
646+
not ok 66 - should print error cause for nested errors
647+
---
648+
duration_ms: *
649+
failureType: 'testCodeFailure'
650+
error: 'a'
651+
code: 'ERR_TEST_FAILURE'
652+
stack: |-
653+
*
654+
*
655+
*
656+
*
657+
[cause]: Error: b
658+
*
659+
*
660+
*
661+
*
662+
[cause]: Error: c
663+
*
664+
*
665+
*
666+
*
667+
[cause]: [Error]
668+
...
669+
# Subtest: should handle cycles in error
670+
not ok 67 - should handle cycles in error
671+
---
672+
duration_ms: *
673+
failureType: 'testCodeFailure'
674+
error: 'b'
675+
code: 'ERR_TEST_FAILURE'
676+
stack: |-
677+
*
678+
*
679+
*
680+
*
681+
[cause]: Error: c
682+
*
683+
*
684+
*
685+
*
686+
[cause]: Error: d
687+
*
688+
*
689+
*
690+
*
691+
[cause]: [Circular *1]
692+
...
693+
# Subtest: should handle primitive, undefined and null cause
694+
# Subtest: primitive cause
695+
not ok 1 - primitive cause
696+
---
697+
duration_ms: *
698+
failureType: 'testCodeFailure'
699+
error: 'foo'
700+
code: 'ERR_TEST_FAILURE'
701+
stack: |-
702+
*
703+
*
704+
*
705+
*
706+
[cause]: 'something went wrong'
707+
...
708+
# Subtest: undefined cause
709+
not ok 2 - undefined cause
710+
---
711+
duration_ms: *
712+
failureType: 'testCodeFailure'
713+
error: 'foo'
714+
code: 'ERR_TEST_FAILURE'
715+
stack: |-
716+
*
717+
*
718+
*
719+
*
720+
[cause]: undefined
721+
...
722+
# Subtest: null cause
723+
not ok 3 - null cause
724+
---
725+
duration_ms: *
726+
failureType: 'cancelledByParent'
727+
error: 'test did not finish before its parent and was cancelled'
728+
code: 'ERR_TEST_FAILURE'
729+
...
730+
1..3
731+
not ok 68 - should handle primitive, undefined and null cause
732+
---
733+
duration_ms: *
734+
failureType: 'subtestsFailed'
735+
error: '3 subtests failed'
736+
code: 'ERR_TEST_FAILURE'
737+
...
738+
# Subtest: should handle case when cause throw
739+
not ok 69 - should handle case when cause throw
740+
---
741+
duration_ms: *
742+
failureType: 'testCodeFailure'
743+
error: ''
744+
code: 'ERR_TEST_FAILURE'
745+
stack: |-
746+
*
747+
*
748+
*
749+
*
750+
...
751+
# Subtest: should handle case when stack throw
752+
not ok 70 - should handle case when stack throw
753+
---
754+
duration_ms: *
755+
failureType: 'testCodeFailure'
756+
error: ''
757+
code: 'ERR_TEST_FAILURE'
758+
stack: |-
759+
*
760+
*
761+
*
762+
*
763+
...
627764
# Subtest: invalid subtest fail
628-
not ok 65 - invalid subtest fail
765+
not ok 71 - invalid subtest fail
629766
---
630767
duration_ms: *
631768
failureType: 'parentAlreadyFinished'
@@ -634,19 +771,19 @@ not ok 65 - invalid subtest fail
634771
stack: |-
635772
*
636773
...
637-
1..65
774+
1..71
638775
# Warning: Test "unhandled rejection - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event.
639776
# Warning: Test "async unhandled rejection - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from async unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event.
640777
# Warning: A resource generated asynchronous activity after the test ended. This activity created the error "Error: uncaught from outside of a test" which triggered an uncaughtException event, caught by the test runner.
641778
# Warning: Test "immediate throw - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: thrown from immediate throw fail" and would have caused the test to fail, but instead triggered an uncaughtException event.
642779
# Warning: Test "immediate reject - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from immediate reject fail" and would have caused the test to fail, but instead triggered an unhandledRejection event.
643780
# Warning: Test "callback called twice in different ticks" generated asynchronous activity after the test ended. This activity created the error "Error [ERR_TEST_FAILURE]: callback invoked multiple times" and would have caused the test to fail, but instead triggered an uncaughtException event.
644781
# Warning: Test "callback async throw after done" generated asynchronous activity after the test ended. This activity created the error "Error: thrown from callback async throw after done" and would have caused the test to fail, but instead triggered an uncaughtException event.
645-
# tests 79
782+
# tests 88
646783
# suites 0
647784
# pass 37
648-
# fail 24
649-
# cancelled 3
785+
# fail 32
786+
# cancelled 4
650787
# skipped 10
651788
# todo 5
652789
# duration_ms *

0 commit comments

Comments
 (0)