Skip to content
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

Closed
wants to merge 1 commit into from

Conversation

pulkit-30
Copy link
Contributor

fix #44656

code:

import test from 'node:test';

test('should print error cause', () => {
  throw new Error('foo', {cause: new Error('bar')});
});

before: test runner doesn't print Error cause

TAP version 13
# Subtest: should print error cause
not ok 1 - should print error cause
  ---
  duration_ms: 0.818167
  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)
    Test.run (node:internal/test_runner/test:549:25)
    Test.start (node:internal/test_runner/test:465:17)
    test (node:internal/test_runner/harness:171:18)
    file:///Users/pulkitgupta/Desktop/node/_test.mjs:3:1
    ModuleJob.run (node:internal/modules/esm/module_job:193:25)
  ...
1..1
# tests 1
# pass 0
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 5.839667

after changes:

TAP version 13
# Subtest: should print error cause
not ok 1 - should print error cause
  ---
  duration_ms: 0.648959
  failureType: 'testCodeFailure'
  error: 'foo'
  code: 'ERR_TEST_FAILURE'
  stack: |-
    Error: foo
    TestContext.<anonymous> (file:///Users/pulkitgupta/Desktop/node/_test.mjs:4:9)
    Test.runInAsyncScope (node:async_hooks:206:9)
        ... 4 lines matching cause stack trace ...
    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)
    }
  ...
1..1
# tests 1
# pass 0
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 5.896167

@nodejs-github-bot
Copy link
Collaborator

Review requested:

  • @nodejs/test_runner

@nodejs-github-bot nodejs-github-bot added dont-land-on-v14.x needs-ci PRs that need a full CI run. test_runner Issues and PRs related to the test runner subsystem. labels Feb 8, 2023
@@ -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;
Copy link
Member

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?

Copy link
Contributor

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') }) }) }) })

Copy link
Member

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?

Copy link
Member

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.

@MoLow
Copy link
Member

MoLow commented Feb 9, 2023

please also make sure (with a test?) that the cause is shown both when running with --test and without (the TAP & yaml are serialized and deserialized when running with --test)

*
*
*
*
Copy link
Member

@MoLow MoLow Feb 9, 2023

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

Copy link
Contributor

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.

@pulkit-30
Copy link
Contributor Author

Thanks for reviews @cjihrig @ljharb @benjamingr @MoLow,
I will update PR with required changes ASAP 🚀
also looking at failing tests

@pulkit-30
Copy link
Contributor Author

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

@ljharb @cjihrig

@@ -10,10 +10,12 @@ const {
StringPrototypeReplaceAll,
StringPrototypeSplit,
StringPrototypeRepeat,
StringPrototypeStartsWith
Copy link
Contributor

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');
Copy link
Contributor

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)
Copy link
Contributor

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)
Copy link
Contributor

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'

@@ -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')});
Copy link
Contributor

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.

*
*
*
*
Copy link
Contributor

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.

@@ -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')});
Copy link
Member

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.

@pulkit-30 pulkit-30 requested review from cjihrig and MoLow and removed request for cjihrig February 14, 2023 16:59
Comment on lines 405 to 409
*
*
*
*
*
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
*
*
*
*
*
*
failureType: 'multipleCallbackInvocations',
cause: 'callback invoked multiple times',
code: 'ERR_TEST_FAILURE'
*

Comment on lines 450 to 454
*
*
*
*
*
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
*
*
*
*
*
*
failureType: 'multipleCallbackInvocations',
cause: 'callback invoked multiple times',
code: 'ERR_TEST_FAILURE'
*

Comment on lines 636 to 535
*
*
*
*
*
*
*
*
*
*
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
*
*
*
*
*
*
*
*
*
*
*
*
*
*
[cause]: Error: bar
*
*
*
*
*

Comment on lines 655 to 672
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
[cause]: Error: b
*
*
*
*
[cause]: Error: c
*
*
*
*
[cause]: [Error]
*
*
*

Comment on lines 682 to 685
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
[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', () => {
Copy link
Member

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

Copy link
Contributor Author

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?

Copy link
Contributor

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?

const error = new Error;
Reflect.defineProperty(error, 'cause', { get() { throw null } });
throw error;

@@ -447,6 +447,10 @@ not ok 50 - callback called twice in future tick
code: 'ERR_TEST_FAILURE'
stack: |-
*
*
Copy link
Member

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)
Copy link
Member

@MoLow MoLow Feb 14, 2023

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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.)

@cjihrig
Copy link
Contributor

cjihrig commented May 10, 2023

This has conflicts now and should be superseded by #47867. Closing. Thanks for the PR though.

@cjihrig cjihrig closed this May 10, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs-ci PRs that need a full CI run. test_runner Issues and PRs related to the test runner subsystem.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Test runner does not print Error cause
9 participants