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

feat(jest-message-util): Add support for Error cause in test and it #13935

Merged
merged 26 commits into from
Feb 24, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
81bb4c0
Properly report Error cause from test/it
dubzzz Feb 19, 2023
e972e71
fix lint
dubzzz Feb 19, 2023
e2ffe8b
rework paths in tests
dubzzz Feb 19, 2023
3c1cceb
add support for old versions of node
dubzzz Feb 19, 2023
d7524b2
update snap
dubzzz Feb 19, 2023
e42a55e
Update packages/jest-message-util/src/index.ts
dubzzz Feb 19, 2023
56ebf8c
Update packages/jest-message-util/src/index.ts
dubzzz Feb 19, 2023
b0514ee
fix compilation
dubzzz Feb 19, 2023
156a1cf
extract error stack computation function
dubzzz Feb 20, 2023
8e763e7
rename option failureDetails
dubzzz Feb 20, 2023
046875a
update changelog
dubzzz Feb 20, 2023
f556c7a
add failures.test.ts
dubzzz Feb 20, 2023
39bf317
add error with cause support on node 14
dubzzz Feb 21, 2023
0e24922
update error with cause builder
dubzzz Feb 21, 2023
cf96b70
add missing failureDetails: []
dubzzz Feb 21, 2023
d4b5e47
fix new case for jasmine
dubzzz Feb 21, 2023
22df9f7
alter test to run well on jasmine too
dubzzz Feb 21, 2023
b0bd8a1
Revert "alter test to run well on jasmine too"
dubzzz Feb 21, 2023
238590e
Revert "fix new case for jasmine"
dubzzz Feb 21, 2023
808336b
only test for non-jasmine
dubzzz Feb 21, 2023
54df836
Merge remote-tracking branch 'origin/master' into rewrite-stack-extra…
dubzzz Feb 21, 2023
fdd56ca
merge
dubzzz Feb 21, 2023
5b3c4ad
Merge branch 'main' into rewrite-stack-extraction
SimenB Feb 22, 2023
56422b5
Merge remote-tracking branch 'origin/master' into rewrite-stack-extra…
dubzzz Feb 23, 2023
81295f7
simplify and extract nested if
dubzzz Feb 23, 2023
c6fa4a9
type error
dubzzz Feb 23, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,46 @@ exports[`formatStackTrace prints code frame and stacktrace 1`] = `
"
`;

exports[`formatStackTrace should properly handle deeply nested causes 1`] = `
"<bold><red> <bold>● </intensity><bold>Error with cause test</color></intensity>

intercepted by f
<dim></intensity>
<dim> <dim>at f (</intensity><dim>cause.test.js<dim>:15:11)</intensity><dim></intensity>
<dim> <dim>at Object.f (</intensity><dim>cause.test.js<dim>:20:5)</intensity><dim></intensity>

Cause:
intercepted by g
<dim></intensity>
<dim> <dim>at g (</intensity><dim>cause.test.js<dim>:8:11)</intensity><dim></intensity>
<dim> <dim>at g (</intensity><dim>cause.test.js<dim>:13:5)</intensity><dim></intensity>
<dim> <dim>at Object.f (</intensity><dim>cause.test.js<dim>:20:5)</intensity><dim></intensity>

Cause:
boom
<dim></intensity>
<dim> <dim>at h (</intensity><dim>cause.test.js<dim>:2:9)</intensity><dim></intensity>
<dim> <dim>at h (</intensity><dim>cause.test.js<dim>:6:5)</intensity><dim></intensity>
<dim> <dim>at g (</intensity><dim>cause.test.js<dim>:13:5)</intensity><dim></intensity>
<dim> <dim>at Object.f (</intensity><dim>cause.test.js<dim>:20:5)</intensity><dim></intensity>
"
`;

exports[`formatStackTrace should properly handle string causes 1`] = `
"<bold><red> <bold>● </intensity><bold>Error with string cause test</color></intensity>

boom
<dim></intensity>
<dim> <dim>at f (</intensity><dim>cause.test.js<dim>:15:11)</intensity><dim></intensity>
<dim> <dim>at Object.f (</intensity><dim>cause.test.js<dim>:20:5)</intensity><dim></intensity>

Cause:
string cause
<dim></intensity>
<dim></intensity>
"
`;

exports[`formatStackTrace should strip node internals 1`] = `
"<bold><red> <bold>● </intensity><bold>Unix test</color></intensity>

Expand Down Expand Up @@ -118,12 +158,12 @@ exports[`should return the error cause if there is one 1`] = `

Test exception

<dim>at Object.<anonymous> (</intensity>packages/jest-message-util/src/__tests__/messages.test.ts<dim>:418:17)</intensity>
<dim>at Object.<anonymous> (</intensity>packages/jest-message-util/src/__tests__/messages.test.ts<dim>:566:17)</intensity>

Cause:
Cause Error

<dim>at Object.<anonymous> (</intensity>packages/jest-message-util/src/__tests__/messages.test.ts<dim>:421:17)</intensity>
<dim>at Object.<anonymous> (</intensity>packages/jest-message-util/src/__tests__/messages.test.ts<dim>:569:17)</intensity>

"
`;
154 changes: 151 additions & 3 deletions packages/jest-message-util/src/__tests__/messages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ const unixStackTrace =
at Object.it (build/__tests__/messages-test.js:45:41)
at Object.<anonymous> (../jest-jasmine2/build/jasmine-pit.js:35:32)
at attemptAsync (../jest-jasmine2/build/jasmine-2.4.1.js:1919:24)`;
const unixError = new Error(unixStackTrace.replace(/\n\s*at [\s\s]*/m, ''));
unixError.stack = unixStackTrace;

const assertionStack =
' ' +
Expand All @@ -56,6 +58,10 @@ const assertionStack =
at process._tickCallback (internal/process/next_tick.js:188:7)
at internal/process/next_tick.js:188:7
`;
const assertionError = new Error(
assertionStack.replace(/\n\s*at [\s\s]*/m, ''),
);
assertionError.stack = assertionStack;

const vendorStack =
' ' +
Expand Down Expand Up @@ -83,6 +89,90 @@ const babelStack =
\u001b[90m 22 | \u001b[39m )\u001b[33m;\u001b[39m
\u001b[90m 23 | \u001b[39m } \u001b[36melse\u001b[39m \u001b[36mif\u001b[39m (\u001b[36mtypeof\u001b[39m render \u001b[33m!==\u001b[39m \u001b[32m'function'\u001b[39m) {\u001b[0m
`;
const babelError = new Error(babelStack.replace(/\n\s*at [\s\s]*/m, ''));
babelError.stack = babelStack;

function buildErrorWithCause(message: string, cause: unknown): Error {
const error = new Error(message, {cause});
if (cause !== error.cause) {
// Error with cause not supported in legacy versions of node, we just polyfill it
Object.assign(error, {cause});
}
return error;
}

const errorWithCauseNestedNested = new Error('boom');
errorWithCauseNestedNested.stack = `Error: boom
at h (cause.test.js:2:9)
at h (cause.test.js:6:5)
at g (cause.test.js:13:5)
at Object.f (cause.test.js:20:5)
at Promise.then.completed (node_modules/jest-circus/build/utils.js:293:28)
at new Promise (<anonymous>)
at callAsyncCircusFn (node_modules/jest-circus/build/utils.js:226:10)
at _callCircusTest (node_modules/jest-circus/build/run.js:248:40)
at _runTest (node_modules/jest-circus/build/run.js:184:3)
at _runTestsForDescribeBlock (node_modules/jest-circus/build/run.js:86:9)
at run (node_modules/jest-circus/build/run.js:26:3)
at runAndTransformResultsToJestFormat (node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapterInit.js:120:21)
at jestAdapter (node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapter.js:74:19)
at runTestInternal (node_modules/jest-runner/build/runTest.js:281:16)
at runTest (node_modules/jest-runner/build/runTest.js:341:7)`;

const errorWithCauseNested = buildErrorWithCause(
'intercepted by g',
errorWithCauseNestedNested,
);
errorWithCauseNested.stack = `Error: intercepted by g
at g (cause.test.js:8:11)
at g (cause.test.js:13:5)
at Object.f (cause.test.js:20:5)
at Promise.then.completed (node_modules/jest-circus/build/utils.js:293:28)
at new Promise (<anonymous>)
at callAsyncCircusFn (node_modules/jest-circus/build/utils.js:226:10)
at _callCircusTest (node_modules/jest-circus/build/run.js:248:40)
at _runTest (node_modules/jest-circus/build/run.js:184:3)
at _runTestsForDescribeBlock (node_modules/jest-circus/build/run.js:86:9)
at run (node_modules/jest-circus/build/run.js:26:3)
at runAndTransformResultsToJestFormat (node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapterInit.js:120:21)
at jestAdapter (node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapter.js:74:19)
at runTestInternal (node_modules/jest-runner/build/runTest.js:281:16)
at runTest (node_modules/jest-runner/build/runTest.js:341:7)`;

const errorWithCause = buildErrorWithCause(
'intercepted by f',
errorWithCauseNested,
);
errorWithCause.stack = `Error: intercepted by f
at f (cause.test.js:15:11)
at Object.f (cause.test.js:20:5)
at Promise.then.completed (node_modules/jest-circus/build/utils.js:293:28)
at new Promise (<anonymous>)
at callAsyncCircusFn (node_modules/jest-circus/build/utils.js:226:10)
at _callCircusTest (node_modules/jest-circus/build/run.js:248:40)
at _runTest (node_modules/jest-circus/build/run.js:184:3)
at _runTestsForDescribeBlock (node_modules/jest-circus/build/run.js:86:9)
at run (node_modules/jest-circus/build/run.js:26:3)
at runAndTransformResultsToJestFormat (node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapterInit.js:120:21)
at jestAdapter (node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapter.js:74:19)
at runTestInternal (node_modules/jest-runner/build/runTest.js:281:16)
at runTest (node_modules/jest-runner/build/runTest.js:341:7)`;

const errorWithStringCause = buildErrorWithCause('boom', 'string cause');
errorWithStringCause.stack = `Error: boom
at f (cause.test.js:15:11)
at Object.f (cause.test.js:20:5)
at Promise.then.completed (node_modules/jest-circus/build/utils.js:293:28)
at new Promise (<anonymous>)
at callAsyncCircusFn (node_modules/jest-circus/build/utils.js:226:10)
at _callCircusTest (node_modules/jest-circus/build/run.js:248:40)
at _runTest (node_modules/jest-circus/build/run.js:184:3)
at _runTestsForDescribeBlock (node_modules/jest-circus/build/run.js:86:9)
at run (node_modules/jest-circus/build/run.js:26:3)
at runAndTransformResultsToJestFormat (node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapterInit.js:120:21)
at jestAdapter (node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapter.js:74:19)
at runTestInternal (node_modules/jest-runner/build/runTest.js:281:16)
at runTest (node_modules/jest-runner/build/runTest.js:341:7)`;

beforeEach(() => {
jest.clearAllMocks();
Expand All @@ -94,7 +184,7 @@ it('should exclude jasmine from stack trace for Unix paths.', () => {
{
ancestorTitles: [],
duration: undefined,
failureDetails: [],
failureDetails: [unixError],
failureMessages: [unixStackTrace],
fullName: 'full name',
invocations: undefined,
Expand Down Expand Up @@ -142,7 +232,7 @@ it('formatStackTrace should strip node internals', () => {
{
ancestorTitles: [],
duration: undefined,
failureDetails: [],
failureDetails: [assertionError],
failureMessages: [assertionStack],
fullName: 'full name',
invocations: undefined,
Expand Down Expand Up @@ -200,7 +290,7 @@ it('retains message in babel code frame error', () => {
{
ancestorTitles: [],
duration: undefined,
failureDetails: [],
failureDetails: [babelError],
failureMessages: [babelStack],
fullName: 'full name',
invocations: undefined,
Expand All @@ -223,6 +313,64 @@ it('retains message in babel code frame error', () => {
expect(messages).toMatchSnapshot();
});

it('formatStackTrace should properly handle deeply nested causes', () => {
const messages = formatResultsErrors(
[
{
ancestorTitles: [],
duration: undefined,
failureDetails: [errorWithCause],
failureMessages: [errorWithCause.stack || ''],
fullName: 'full name',
invocations: undefined,
location: null,
numPassingAsserts: 0,
retryReasons: undefined,
status: 'failed',
title: 'Error with cause test',
},
],
{
rootDir: '',
testMatch: [],
},
{
noStackTrace: false,
},
);

expect(messages).toMatchSnapshot();
});

it('formatStackTrace should properly handle string causes', () => {
const messages = formatResultsErrors(
[
{
ancestorTitles: [],
duration: undefined,
failureDetails: [errorWithStringCause],
failureMessages: [errorWithStringCause.stack || ''],
fullName: 'full name',
invocations: undefined,
location: null,
numPassingAsserts: 0,
retryReasons: undefined,
status: 'failed',
title: 'Error with string cause test',
},
],
{
rootDir: '',
testMatch: [],
},
{
noStackTrace: false,
},
);

expect(messages).toMatchSnapshot();
});

it('codeframe', () => {
jest
.mocked(readFileSync)
Expand Down
58 changes: 47 additions & 11 deletions packages/jest-message-util/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,11 @@ export const formatStackTrace = (
};

type FailedResults = Array<{
/** Stringified version of the error */
content: string;
/** Error */
rawError: unknown;
SimenB marked this conversation as resolved.
Show resolved Hide resolved
/** Execution result */
result: TestResult.AssertionResult;
}>;

Expand All @@ -370,8 +374,12 @@ export const formatResultsErrors = (
): string | null => {
const failedResults: FailedResults = testResults.reduce<FailedResults>(
(errors, result) => {
result.failureMessages.forEach(item => {
errors.push({content: checkForCommonEnvironmentErrors(item), result});
result.failureMessages.forEach((item, index) => {
errors.push({
content: item,
rawError: result.failureDetails[index],
result,
});
});
return errors;
},
Expand All @@ -383,15 +391,43 @@ export const formatResultsErrors = (
}

return failedResults
.map(({result, content}) => {
let {message, stack} = separateMessageFromStack(content);
stack = options.noStackTrace
? ''
: `${STACK_TRACE_COLOR(
formatStackTrace(stack, config, options, testPath),
)}\n`;
.map(({result, content, rawError}) => {
function formatErrorStack(errorOrStack: Error | string): string {
SimenB marked this conversation as resolved.
Show resolved Hide resolved
// The stack of new Error('message') contains both the message and the stack,
// thuw we need to sanitize and clean it for proper display using separateMessageFromStack.
const sourceStack =
typeof errorOrStack === 'string'
? errorOrStack
: errorOrStack.stack || '';
let {message, stack} = separateMessageFromStack(sourceStack);
stack = options.noStackTrace
? ''
: `${STACK_TRACE_COLOR(
formatStackTrace(stack, config, options, testPath),
)}\n`;

message = checkForCommonEnvironmentErrors(message);
message = indentAllLines(message);

let cause = '';
if (typeof errorOrStack !== 'string' && 'cause' in errorOrStack) {
dubzzz marked this conversation as resolved.
Show resolved Hide resolved
if (
typeof errorOrStack.cause === 'string' ||
types.isNativeError(errorOrStack.cause) ||
errorOrStack.cause instanceof Error
) {
const nestedCause = formatErrorStack(errorOrStack.cause);
cause = `\n${MESSAGE_INDENT}Cause:\n${nestedCause}`;
}
}

return `${message}\n${stack}${cause}`;
}

message = indentAllLines(message);
const rootErrorOrStack: Error | string =
types.isNativeError(rawError) || rawError instanceof Error
dubzzz marked this conversation as resolved.
Show resolved Hide resolved
? rawError
: content;

const title = `${chalk.bold.red(
TITLE_INDENT +
Expand All @@ -401,7 +437,7 @@ export const formatResultsErrors = (
result.title,
)}\n`;

return `${title}\n${message}\n${stack}`;
return `${title}\n${formatErrorStack(rootErrorOrStack)}`;
})
.join('\n');
};
Expand Down