Skip to content

Commit

Permalink
Add async stack traces for circus (#6281)
Browse files Browse the repository at this point in the history
* Add async stack traces for circus

* fix failing test

* fix flow errors

* pretty
  • Loading branch information
SimenB authored and aaronabramov committed May 26, 2018
1 parent 87a1eb8 commit 0d5fcdc
Show file tree
Hide file tree
Showing 9 changed files with 190 additions and 84 deletions.
10 changes: 2 additions & 8 deletions integration-tests/__tests__/__snapshots__/failures.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,6 @@ exports[`not throwing Error objects 5`] = `
9 | });
10 |
at packages/jest-jasmine2/build/jasmine/Spec.js:85:20
at __tests__/during_tests.test.js:7:1
● Boolean thrown during test
Expand All @@ -159,7 +158,6 @@ exports[`not throwing Error objects 5`] = `
13 | throw false;
14 | });
at packages/jest-jasmine2/build/jasmine/Spec.js:85:20
at __tests__/during_tests.test.js:11:1
● undefined thrown during test
Expand All @@ -174,7 +172,6 @@ exports[`not throwing Error objects 5`] = `
18 | throw undefined;
19 | });
at packages/jest-jasmine2/build/jasmine/Spec.js:85:20
at __tests__/during_tests.test.js:16:1
● Object thrown during test
Expand All @@ -196,7 +193,6 @@ exports[`not throwing Error objects 5`] = `
23 | throw deepObject;
24 | });
at packages/jest-jasmine2/build/jasmine/Spec.js:85:20
at __tests__/during_tests.test.js:21:1
● Error during test
Expand Down Expand Up @@ -229,7 +225,7 @@ exports[`not throwing Error objects 5`] = `
done(non-error)
Failed: Object {
thrown: Object {
\\"notAnError\\": Array [
Object {
\\"hello\\": true,
Expand All @@ -246,12 +242,11 @@ exports[`not throwing Error objects 5`] = `
38 |
39 | test('returned promise rejection', () => {
at packages/jest-jasmine2/build/jasmine/Env.js:537:34
at __tests__/during_tests.test.js:36:3
returned promise rejection
Failed: Object {
thrown: Object {
\\"notAnError\\": Array [
Object {
\\"hello\\": true,
Expand All @@ -268,7 +263,6 @@ exports[`not throwing Error objects 5`] = `
41 | });
42 |
at packages/jest-jasmine2/build/jasmine_async.js:102:24
at __tests__/during_tests.test.js:39:1
"
Expand Down
26 changes: 16 additions & 10 deletions integration-tests/__tests__/failures.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,31 @@ const normalizeDots = text => text.replace(/\.{1,}$/gm, '.');

SkipOnWindows.suite();

function cleanStderr(stderr) {
const {rest} = extractSummary(stderr);
return rest
.replace(/.*(jest-jasmine2|jest-circus).*\n/g, '')
.replace(new RegExp('Failed: Object {', 'g'), 'thrown: Object {');
}

test('not throwing Error objects', () => {
let stderr;
stderr = runJest(dir, ['throw_number.test.js']).stderr;
expect(extractSummary(stderr).rest).toMatchSnapshot();
expect(cleanStderr(stderr)).toMatchSnapshot();
stderr = runJest(dir, ['throw_string.test.js']).stderr;
expect(extractSummary(stderr).rest).toMatchSnapshot();
expect(cleanStderr(stderr)).toMatchSnapshot();
stderr = runJest(dir, ['throw_object.test.js']).stderr;
expect(extractSummary(stderr).rest).toMatchSnapshot();
expect(cleanStderr(stderr)).toMatchSnapshot();
stderr = runJest(dir, ['assertion_count.test.js']).stderr;
expect(extractSummary(stderr).rest).toMatchSnapshot();
expect(cleanStderr(stderr)).toMatchSnapshot();
stderr = runJest(dir, ['during_tests.test.js']).stderr;
expect(extractSummary(stderr).rest).toMatchSnapshot();
expect(cleanStderr(stderr)).toMatchSnapshot();
});

test('works with node assert', () => {
const nodeMajorVersion = Number(process.versions.node.split('.')[0]);
const {stderr} = runJest(dir, ['node_assertion_error.test.js']);
let summary = normalizeDots(extractSummary(stderr).rest);
let summary = normalizeDots(cleanStderr(stderr));

// Node 9 started to include the error for `doesNotThrow`
// https://github.com/nodejs/node/pull/12167
Expand Down Expand Up @@ -127,7 +134,6 @@ test('works with node assert', () => {
68 | });
69 |
at packages/jest-jasmine2/build/jasmine/Spec.js:85:20
at __tests__/node_assertion_error.test.js:66:1
`;

Expand All @@ -141,7 +147,7 @@ test('works with node assert', () => {
test('works with assertions in separate files', () => {
const {stderr} = runJest(dir, ['test_macro.test.js']);

expect(normalizeDots(extractSummary(stderr).rest)).toMatchSnapshot();
expect(normalizeDots(cleanStderr(stderr))).toMatchSnapshot();
});

test('works with async failures', () => {
Expand All @@ -158,7 +164,7 @@ test('works with async failures', () => {
test('works with snapshot failures', () => {
const {stderr} = runJest(dir, ['snapshot.test.js']);

const result = normalizeDots(extractSummary(stderr).rest);
const result = normalizeDots(cleanStderr(stderr));

expect(
result.substring(0, result.indexOf('Snapshot Summary')),
Expand All @@ -168,7 +174,7 @@ test('works with snapshot failures', () => {
test('works with named snapshot failures', () => {
const {stderr} = runJest(dir, ['snapshot_named.test.js']);

const result = normalizeDots(extractSummary(stderr).rest);
const result = normalizeDots(cleanStderr(stderr));

expect(
result.substring(0, result.indexOf('Snapshot Summary')),
Expand Down
3 changes: 2 additions & 1 deletion packages/jest-circus/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"jest-matcher-utils": "^23.0.0",
"jest-message-util": "^23.0.0",
"jest-snapshot": "^23.0.0",
"jest-util": "^23.0.0"
"jest-util": "^23.0.0",
"pretty-format": "^23.0.0"
},
"devDependencies": {
"jest-runtime": "^23.0.0"
Expand Down
33 changes: 23 additions & 10 deletions packages/jest-circus/src/event_handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,33 +43,42 @@ const handler: EventHandler = (event, state): void => {
}
case 'add_hook': {
const {currentDescribeBlock} = state;
const {fn, hookType: type, timeout} = event;
const {asyncError, fn, hookType: type, timeout} = event;
const parent = currentDescribeBlock;
currentDescribeBlock.hooks.push({fn, parent, timeout, type});
currentDescribeBlock.hooks.push({asyncError, fn, parent, timeout, type});
break;
}
case 'add_test': {
const {currentDescribeBlock} = state;
const {fn, mode, testName: name, timeout} = event;
const test = makeTest(fn, mode, name, currentDescribeBlock, timeout);
test.mode === 'only' && (state.hasFocusedTests = true);
const {asyncError, fn, mode, testName: name, timeout} = event;
const test = makeTest(
fn,
mode,
name,
currentDescribeBlock,
timeout,
asyncError,
);
if (test.mode === 'only') {
state.hasFocusedTests = true;
}
currentDescribeBlock.tests.push(test);
break;
}
case 'hook_failure': {
const {test, describeBlock, error, hook} = event;
const {type} = hook;
const {asyncError, type} = hook;

if (type === 'beforeAll') {
invariant(describeBlock, 'always present for `*All` hooks');
addErrorToEachTestUnderDescribe(describeBlock, error);
addErrorToEachTestUnderDescribe(describeBlock, error, asyncError);
} else if (type === 'afterAll') {
// Attaching `afterAll` errors to each test makes execution flow
// too complicated, so we'll consider them to be global.
state.unhandledErrors.push(error);
state.unhandledErrors.push([error, asyncError]);
} else {
invariant(test, 'always present for `*Each` hooks');
test.errors.push(error);
test.errors.push([error, asyncError]);
}
break;
}
Expand All @@ -87,7 +96,11 @@ const handler: EventHandler = (event, state): void => {
break;
}
case 'test_fn_failure': {
event.test.errors.push(event.error);
const {
error,
test: {asyncError},
} = event;
event.test.errors.push([error, asyncError]);
break;
}
case 'run_start': {
Expand Down
79 changes: 46 additions & 33 deletions packages/jest-circus/src/format_node_assert_errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@
* @flow strict-local
*/

import type {DiffOptions} from 'jest-diff/src/diff_strings.js';
import type {DiffOptions} from 'jest-diff/src/diff_strings';
import type {Event, State} from 'types/Circus';

import {printExpected, printReceived} from 'jest-matcher-utils';
import chalk from 'chalk';
import diff from 'jest-diff';
import prettyFormat from 'pretty-format';

type AssertionError = {|
actual: ?string,
Expand All @@ -34,31 +35,40 @@ const assertOperatorsMap = {
const humanReadableOperators = {
deepEqual: 'to deeply equal',
deepStrictEqual: 'to deeply and strictly equal',
equal: 'to be equal',
notDeepEqual: 'not to deeply equal',
notDeepStrictEqual: 'not to deeply and strictly equal',
notEqual: 'to not be equal',
notStrictEqual: 'not be strictly equal',
strictEqual: 'to strictly be equal',
};

export default (event: Event, state: State) => {
switch (event.name) {
case 'test_done': {
let assert;
try {
// Use indirect require so that Metro Bundler does not attempt to
// bundle `assert`, which does not exist in React Native.
// eslint-disable-next-line no-useless-call
assert = require.call(null, 'assert');
event.test.errors = event.test.errors.map(error => {
return error instanceof assert.AssertionError
? assertionErrorMessage(error, {expand: state.expand})
: error;
});
} catch (error) {
// We are running somewhere where `assert` isn't available, like a
// browser or React Native. Since assert isn't available, presumably
// none of the errors we get through this event listener will be
// `AssertionError`s, so we don't need to do anything.
break;
}
event.test.errors = event.test.errors.map(errors => {
let error;
if (Array.isArray(errors)) {
const [originalError, asyncError] = errors;

if (originalError == null) {
error = asyncError;
} else if (!originalError.stack) {
error = asyncError;

error.message = originalError.message
? originalError.message
: `thrown: ${prettyFormat(originalError, {maxDepth: 3})}`;
} else {
error = originalError;
}
} else {
error = errors;
}
return error.code === 'ERR_ASSERTION'
? {message: assertionErrorMessage(error, {expand: state.expand})}
: errors;
});
}
}
};
Expand All @@ -76,12 +86,15 @@ const getOperatorName = (operator: ?string, stack: string) => {
return '';
};

const operatorMessage = (operator: ?string, negator: boolean) =>
typeof operator === 'string'
? operator.startsWith('!') || operator.startsWith('=')
? `${negator ? 'not ' : ''}to be (operator: ${operator}):\n`
: `${humanReadableOperators[operator] || operator} to:\n`
const operatorMessage = (operator: ?string) => {
const niceOperatorName = getOperatorName(operator, '');
// $FlowFixMe: we default to the operator itself, so holes in the map doesn't matter
const humanReadableOperator = humanReadableOperators[niceOperatorName];

return typeof operator === 'string'
? `${humanReadableOperator || niceOperatorName} to:\n`
: '';
};

const assertThrowingMatcherHint = (operatorName: string) => {
return (
Expand Down Expand Up @@ -114,13 +127,13 @@ const assertMatcherHint = (operator: ?string, operatorName: string) => {
};

function assertionErrorMessage(error: AssertionError, options: DiffOptions) {
const {expected, actual, message, operator, stack} = error;
const {expected, actual, generatedMessage, message, operator, stack} = error;
const diffString = diff(expected, actual, options);
const negator =
typeof operator === 'string' &&
(operator.startsWith('!') || operator.startsWith('not'));
const hasCustomMessage = !error.generatedMessage;
const hasCustomMessage = !generatedMessage;
const operatorName = getOperatorName(operator, stack);
const trimmedStack = stack
.replace(message, '')
.replace(/AssertionError(.*)/g, '');

if (operatorName === 'doesNotThrow') {
return (
Expand All @@ -130,7 +143,7 @@ function assertionErrorMessage(error: AssertionError, options: DiffOptions) {
chalk.reset(`Instead, it threw:\n`) +
` ${printReceived(actual)}` +
chalk.reset(hasCustomMessage ? '\n\nMessage:\n ' + message : '') +
stack.replace(/AssertionError(.*)/g, '')
trimmedStack
);
}

Expand All @@ -141,19 +154,19 @@ function assertionErrorMessage(error: AssertionError, options: DiffOptions) {
chalk.reset(`Expected the function to throw an error.\n`) +
chalk.reset(`But it didn't throw anything.`) +
chalk.reset(hasCustomMessage ? '\n\nMessage:\n ' + message : '') +
stack.replace(/AssertionError(.*)/g, '')
trimmedStack
);
}

return (
assertMatcherHint(operator, operatorName) +
'\n\n' +
chalk.reset(`Expected value ${operatorMessage(operator, negator)}`) +
chalk.reset(`Expected value ${operatorMessage(operator)}`) +
` ${printExpected(expected)}\n` +
chalk.reset(`Received:\n`) +
` ${printReceived(actual)}` +
chalk.reset(hasCustomMessage ? '\n\nMessage:\n ' + message : '') +
(diffString ? `\n\nDifference:\n\n${diffString}` : '') +
stack.replace(/AssertionError(.*)/g, '')
trimmedStack
);
}
Loading

0 comments on commit 0d5fcdc

Please sign in to comment.