-
Notifications
You must be signed in to change notification settings - Fork 30.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
test: fix test runner does not print error cause #46576
Conversation
Review requested:
|
@@ -268,4 +275,8 @@ function isAssertionLike(value) { | |||
return value && typeof value === 'object' && 'expected' in value && 'actual' in value; | |||
} | |||
|
|||
function hasInnerCause(value) { | |||
return value?.cause?.cause?.stack ? true : false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a cause
can be any JS value, including a primitive, and undefined
. Causes should be printed whether they have a stack or not. Can you add some tests for non-error cause values?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you also add a test for something like this:
new Error('a', { cause: new Error('b', { cause: new Error('c', { cause: new Error('d', { cause: new Error('e') }) }) }) })
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm usually in the "don't worry about primordials" crowd but since this is error handling and debugging code it needs to be very resilient.
I would expect it to deal the following (all real cases from the past):
- What if someone subclassed
Error
with .cause
with a getter that throws? - What if
.stack
throws?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In addition to @chjirig's test case, please add one like this:
const outer = new Error('b', { cause: null });
outer.cause = new Error('c', { cause: new Error('d', { cause: outer }) }) });`
to ensure cycles are handled.
please also make sure (with a test?) that the cause is shown both when running with |
test/message/test_runner_output.out
Outdated
* | ||
* | ||
* | ||
* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
perhaps some of these lines (that are not a stack trace) should not be an asterisk
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with @MoLow here.
Thanks for reviews @cjihrig @ljharb @benjamingr @MoLow, |
test.mjs import test from 'node:test';
test('should print error cause', () => {
throw new Error('foo', {cause: new Error('bar')});
});
test('should print error cause for nested errors', () => {
throw new Error('a', { cause: new Error('b', { cause: new Error('c', { cause: new Error('d', { cause: new Error('e') }) }) }) })
});
test("should handle cycles in error", () => {
const outer = new Error('b', { cause: null });
outer.cause = new Error('c', { cause: new Error('d', { cause: outer }) });
throw outer;
}); output: TAP version 13
# Subtest: should print error cause
not ok 1 - should print error cause
---
duration_ms: 0.773584
failureType: 'testCodeFailure'
error: 'foo'
code: 'ERR_TEST_FAILURE'
stack: |-
TestContext.<anonymous> (file:///Users/pulkitgupta/Desktop/node/_test.mjs:4:9)
Test.runInAsyncScope (node:async_hooks:206:9)
ModuleJob.run (node:internal/modules/esm/module_job:193:25) {
[cause]: Error: bar
at TestContext.<anonymous> (file:///Users/pulkitgupta/Desktop/node/_test.mjs:4:34)
at Test.runInAsyncScope (node:async_hooks:206:9)
at Test.run (node:internal/test_runner/test:549:25)
at Test.start (node:internal/test_runner/test:465:17)
at test (node:internal/test_runner/harness:171:18)
at file:///Users/pulkitgupta/Desktop/node/_test.mjs:3:1
at ModuleJob.run (node:internal/modules/esm/module_job:193:25)
}
...
# Subtest: should print error cause for nested errors
not ok 2 - should print error cause for nested errors
---
duration_ms: 1.281542
failureType: 'testCodeFailure'
error: 'a'
code: 'ERR_TEST_FAILURE'
stack: |-
TestContext.<anonymous> (file:///Users/pulkitgupta/Desktop/node/_test.mjs:8:9)
Test.runInAsyncScope (node:async_hooks:206:9)
Test.run (node:internal/test_runner/test:577:10) {
[cause]: Error: b
at TestContext.<anonymous> (file:///Users/pulkitgupta/Desktop/node/_test.mjs:8:33)
at Test.runInAsyncScope (node:async_hooks:206:9)
at Test.run (node:internal/test_runner/test:577:10) {
[cause]: Error: c
at TestContext.<anonymous> (file:///Users/pulkitgupta/Desktop/node/_test.mjs:8:57)
at Test.runInAsyncScope (node:async_hooks:206:9)
at Test.run (node:internal/test_runner/test:577:10) {
[cause]: [Error]
}
}
}
...
# Subtest: should handle cycles in error
not ok 3 - should handle cycles in error
---
duration_ms: 0.057458
failureType: 'testCodeFailure'
error: 'b'
code: 'ERR_TEST_FAILURE'
stack: |-
TestContext.<anonymous> (file:///Users/pulkitgupta/Desktop/node/_test.mjs:12:17)
Test.runInAsyncScope (node:async_hooks:206:9)
async Test.processPendingSubtests (node:internal/test_runner/test:304:7) {
[cause]: Error: c
at TestContext.<anonymous> (file:///Users/pulkitgupta/Desktop/node/_test.mjs:13:17)
at Test.runInAsyncScope (node:async_hooks:206:9)
at async Test.processPendingSubtests (node:internal/test_runner/test:304:7) {
[cause]: Error: d
at TestContext.<anonymous> (file:///Users/pulkitgupta/Desktop/node/_test.mjs:13:41)
at Test.runInAsyncScope (node:async_hooks:206:9)
at async Test.processPendingSubtests (node:internal/test_runner/test:304:7) {
[cause]: [Circular *1]
}
}
}
...
1..3
# tests 3
# pass 0
# fail 3
# cancelled 0
# skipped 0
# todo 0
# duration_ms 6.749875 |
@@ -10,10 +10,12 @@ const { | |||
StringPrototypeReplaceAll, | |||
StringPrototypeSplit, | |||
StringPrototypeRepeat, | |||
StringPrototypeStartsWith |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please keep these sorted.
} = primordials; | ||
const { inspectWithNoCustomRetry } = require('internal/errors'); | ||
const { isError, kEmptyObject } = require('internal/util'); | ||
const { relative } = require('path'); | ||
const util = require('util'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should use internal/util/inspect.js I think. Also use destructuring to reduce the chance of things being tampered with.
|
||
if (cause?.cause) { | ||
const errCause = util.inspect(cause).split('\n').slice(1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
split()
, slice()
, filter()
, and join()
all need to use primordials.
|
||
if (cause?.cause) { | ||
const errCause = util.inspect(cause).split('\n').slice(1) | ||
.filter((line) => !StringPrototypeStartsWith(line.trim(), '...') && line) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like the && line
should be line &&
. Also, I would check for typeof line === 'string'
test/message/test_runner_output.js
Outdated
@@ -383,3 +383,17 @@ test('unfinished test with unhandledRejection', async () => { | |||
setTimeout(() => Promise.reject(new Error('bar'))); | |||
}); | |||
}); | |||
|
|||
test('should print error cause', () => { | |||
throw new Error('foo', {cause: new Error('bar')}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please run make lint
locally.
test/message/test_runner_output.out
Outdated
* | ||
* | ||
* | ||
* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with @MoLow here.
test/message/test_runner_output.js
Outdated
@@ -383,3 +383,17 @@ test('unfinished test with unhandledRejection', async () => { | |||
setTimeout(() => Promise.reject(new Error('bar'))); | |||
}); | |||
}); | |||
|
|||
test('should print error cause', () => { | |||
throw new Error('foo', {cause: new Error('bar')}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add Error.stackTraceLimit = 0;
to eliminate the stack traces from the output (and therefore the streams of asterisks) unless you’re intending to evaluate something in the stack trace. It’s annoying for tests to start failing because a refactoring changes the number of lines of a stack trace.
* | ||
* | ||
* | ||
* | ||
* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* | |
* | |
* | |
* | |
* | |
* | |
failureType: 'multipleCallbackInvocations', | |
cause: 'callback invoked multiple times', | |
code: 'ERR_TEST_FAILURE' | |
* |
* | ||
* | ||
* | ||
* | ||
* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* | |
* | |
* | |
* | |
* | |
* | |
failureType: 'multipleCallbackInvocations', | |
cause: 'callback invoked multiple times', | |
code: 'ERR_TEST_FAILURE' | |
* |
* | ||
* | ||
* | ||
* | ||
* | ||
* | ||
* | ||
* | ||
* | ||
* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* | |
* | |
* | |
* | |
* | |
* | |
* | |
* | |
* | |
* | |
* | |
* | |
* | |
* | |
[cause]: Error: bar | |
* | |
* | |
* | |
* | |
* |
* | ||
* | ||
* | ||
* | ||
* | ||
* | ||
* | ||
* | ||
* | ||
* | ||
* | ||
* | ||
* | ||
* | ||
* | ||
* | ||
* | ||
* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* | |
* | |
* | |
* | |
* | |
* | |
* | |
* | |
* | |
* | |
* | |
* | |
* | |
* | |
* | |
* | |
* | |
* | |
* | |
* | |
* | |
* | |
[cause]: Error: b | |
* | |
* | |
* | |
* | |
[cause]: Error: c | |
* | |
* | |
* | |
* | |
[cause]: [Error] | |
* | |
* | |
* |
* | ||
* | ||
* | ||
* | ||
* | ||
* | ||
* | ||
* | ||
* | ||
* | ||
* | ||
* | ||
* | ||
* | ||
* | ||
* | ||
* | ||
* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* | |
* | |
* | |
* | |
* | |
* | |
* | |
* | |
* | |
* | |
* | |
* | |
* | |
* | |
* | |
* | |
* | |
* | |
* | |
* | |
* | |
* | |
[cause]: Error: c | |
* | |
* | |
* | |
* | |
[cause]: Error: d | |
* | |
* | |
* | |
* | |
[cause]: [Circular *1] | |
* | |
* | |
* |
@@ -383,3 +383,19 @@ test('unfinished test with unhandledRejection', async () => { | |||
setTimeout(() => Promise.reject(new Error('bar'))); | |||
}); | |||
}); | |||
|
|||
test('should print error cause', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there are still some cases discussed here with no tests:
#46576 (comment)
- non error cause (primitives, undefined etc)
cause
/stack
throws
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cause/stack
throws
How can I test this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cause/stack
throwsHow can I test this?
const error = new Error;
Reflect.defineProperty(error, 'cause', { get() { throw null } });
throw error;
test/message/test_runner_output.out
Outdated
@@ -447,6 +447,10 @@ not ok 50 - callback called twice in future tick | |||
code: 'ERR_TEST_FAILURE' | |||
stack: |- | |||
* | |||
* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same suggestions I maid apply to this file as well
@@ -262,6 +262,61 @@ | |||
* | |||
* | |||
|
|||
should print error cause (*ms) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same suggestions I made apply to this file as well
@@ -209,7 +219,11 @@ function jsToYaml(indent, name, value) { | |||
// If the ERR_TEST_FAILURE came from an error provided by user code, | |||
// then try to unwrap the original error message and stack. | |||
if (code === 'ERR_TEST_FAILURE' && kUnwrapErrors.has(failureType)) { | |||
errStack = cause?.stack ?? errStack; | |||
if (cause?.cause) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (cause?.cause) { | |
if (cause != null && ObjectPrototypeHasOwnProperty(cause, 'cause')) { |
Otherwise it wouldn't detect a falsy cause (e.g. null
, undefined
, false
, empty string, etc.)
This has conflicts now and should be superseded by #47867. Closing. Thanks for the PR though. |
fix #44656
code:
before: test runner doesn't print
Error
causeafter changes: