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

Support error logging before jest retry #12201

Merged
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,9 @@

### Features

- `[jest-circus]` Support error logging before retry` ([#12201](https://github.com/facebook/jest/pull/12201))
SimenB marked this conversation as resolved.
Show resolved Hide resolved


### Fixes

- `[@jest/transform]` Update dependency package `pirates` to 4.0.4 ([#12136](https://github.com/facebook/jest/pull/12136))
Expand Down
13 changes: 11 additions & 2 deletions docs/JestObjectAPI.md
Expand Up @@ -714,9 +714,9 @@ Example:
jest.setTimeout(1000); // 1 second
```

### `jest.retryTimes()`
### `jest.retryTimes(numRetries, options)`

Runs failed tests n-times until they pass or until the max number of retries is exhausted. This only works with the default [jest-circus](https://github.com/facebook/jest/tree/main/packages/jest-circus) runner!
Runs failed tests n-times until they pass or until the max number of retries is exhausted. `options` are optional. This only works with the default [jest-circus](https://github.com/facebook/jest/tree/main/packages/jest-circus) runner!

Example in a test:

Expand All @@ -727,4 +727,13 @@ test('will fail', () => {
});
```

If `logErrorsBeforeRetry` is enabled, Jest will log the error(s) that caused the test to fail to the console, providing visibility on why a retry occurred.

```js
jest.retryTimes(3, {logErrorsBeforeRetry: true});
test('will fail', () => {
expect(true).toBe(false);
});
```

Returns the `jest` object for chaining.
13 changes: 12 additions & 1 deletion e2e/__tests__/testRetries.test.ts
Expand Up @@ -19,6 +19,8 @@ describe('Test Retries', () => {
'e2e/test-retries/',
outputFileName,
);
const logErrorsBeforeRetryErrorMessage =
'Errors that caused Jest to retry test: ';

afterAll(() => {
fs.unlinkSync(outputFilePath);
Expand All @@ -29,6 +31,16 @@ describe('Test Retries', () => {

expect(result.exitCode).toEqual(0);
expect(result.failed).toBe(false);
expect(result.stdout.includes(logErrorsBeforeRetryErrorMessage)).toBe(
false,
);
SimenB marked this conversation as resolved.
Show resolved Hide resolved
});

it('logs error(s) before retry', () => {
const result = runJest('test-retries', ['logErrorsBeforeRetries.test.js']);
expect(result.exitCode).toEqual(0);
expect(result.failed).toBe(false);
expect(result.stdout.includes(logErrorsBeforeRetryErrorMessage)).toBe(true);
SimenB marked this conversation as resolved.
Show resolved Hide resolved
});

it('reporter shows more than 1 invocation if test is retried', () => {
Expand All @@ -55,7 +67,6 @@ describe('Test Retries', () => {
`Can't parse the JSON result from ${outputFileName}, ${err.toString()}`,
);
}

SimenB marked this conversation as resolved.
Show resolved Hide resolved
expect(jsonResult.numPassedTests).toBe(0);
expect(jsonResult.numFailedTests).toBe(1);
expect(jsonResult.numPendingTests).toBe(0);
Expand Down
18 changes: 18 additions & 0 deletions e2e/test-retries/__tests__/logErrorsBeforeRetries.test.js
@@ -0,0 +1,18 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';

let i = 0;
jest.retryTimes(3, {logErrorsBeforeRetry: true});
it('retryTimes set', () => {
i++;
if (i === 2) {
expect(true).toBeTruthy();
} else {
expect(true).toBeFalsy();
}
});
6 changes: 5 additions & 1 deletion packages/jest-circus/src/run.ts
Expand Up @@ -7,7 +7,7 @@

import type {Circus} from '@jest/types';
import {dispatch, getState} from './state';
import {RETRY_TIMES} from './types';
import {LOG_ERRORS_BEFORE_RETRY, RETRY_TIMES} from './types';
import {
callAsyncCircusFn,
getAllHooksForDescribe,
Expand Down Expand Up @@ -44,6 +44,7 @@ const _runTestsForDescribeBlock = async (

// Tests that fail and are retried we run after other tests
const retryTimes = parseInt(global[RETRY_TIMES], 10) || 0;
const logErrorsBeforeRetry = global[LOG_ERRORS_BEFORE_RETRY] || false;
const deferredRetryTests = [];

for (const child of describeBlock.children) {
Expand Down Expand Up @@ -73,6 +74,9 @@ const _runTestsForDescribeBlock = async (
let numRetriesAvailable = retryTimes;

while (numRetriesAvailable > 0 && test.errors.length > 0) {
if (logErrorsBeforeRetry) {
console.error('Errors that caused Jest to retry test: ', test.errors);
SimenB marked this conversation as resolved.
Show resolved Hide resolved
}
// Clear errors so retries occur
await dispatch({name: 'test_retry', test});

Expand Down
5 changes: 4 additions & 1 deletion packages/jest-circus/src/types.ts
Expand Up @@ -19,13 +19,16 @@ export const RETRY_TIMES = Symbol.for(
export const TEST_TIMEOUT_SYMBOL = Symbol.for(
'TEST_TIMEOUT_SYMBOL',
) as unknown as 'TEST_TIMEOUT_SYMBOL';

export const LOG_ERRORS_BEFORE_RETRY = Symbol.for(
'LOG_ERRORS_BEFORE_RETRY',
) as unknown as 'LOG_ERRORS_BEFORE_RETRY';
declare global {
namespace NodeJS {
interface Global {
STATE_SYM_SYMBOL: Circus.State;
RETRY_TIMES_SYMBOL: string;
TEST_TIMEOUT_SYMBOL: number;
LOG_ERRORS_BEFORE_RETRY: boolean;
expect: typeof expect;
}
}
Expand Down
10 changes: 9 additions & 1 deletion packages/jest-environment/src/index.ts
Expand Up @@ -193,11 +193,19 @@ export interface Jest {
*/
restoreAllMocks(): Jest;
mocked: typeof JestMockMocked;

/**
* Runs failed tests n-times until they pass or until the max number of
* retries is exhausted. This only works with `jest-circus`!
*
* If `logErrorsBeforeRetry` is enabled, Jest will log the error(s) that caused
* the test to fail to the console, providing visibility on why a retry occurred.
*/
retryTimes(numRetries: number): Jest;
retryTimes(
numRetries: number,
options?: {logErrorsBeforeRetry?: boolean},
): Jest;

/**
* Exhausts tasks queued by setImmediate().
*
Expand Down
12 changes: 11 additions & 1 deletion packages/jest-runtime/src/index.ts
Expand Up @@ -114,6 +114,7 @@ type ResolveOptions = Parameters<typeof require.resolve>[1] & {

const testTimeoutSymbol = Symbol.for('TEST_TIMEOUT_SYMBOL');
const retryTimesSymbol = Symbol.for('RETRY_TIMES');
const logErrorsBeforeRetrySymbol = Symbol.for('LOG_ERRORS_BEFORE_RETRY');

const NODE_MODULES = path.sep + 'node_modules' + path.sep;

Expand Down Expand Up @@ -1936,9 +1937,18 @@ export default class Runtime {
return jestObject;
};

const retryTimes = (numTestRetries: number) => {
const retryTimes = (
numTestRetries: number,
options?: {
logErrorsBeforeRetry?: boolean;
},
) => {
// @ts-expect-error: https://github.com/Microsoft/TypeScript/issues/24587
this._environment.global[retryTimesSymbol] = numTestRetries;
// @ts-expect-error: https://github.com/Microsoft/TypeScript/issues/24587
this._environment.global[logErrorsBeforeRetrySymbol] =
options?.logErrorsBeforeRetry;

return jestObject;
};

Expand Down
1 change: 1 addition & 0 deletions packages/jest-types/__typechecks__/jest.test.ts
Expand Up @@ -33,6 +33,7 @@ expectType<typeof jest>(jest.mock('moduleName', jest.fn(), {virtual: true}));
expectType<typeof jest>(jest.resetModules());
expectType<typeof jest>(jest.isolateModules(() => {}));
expectType<typeof jest>(jest.retryTimes(3));
expectType<typeof jest>(jest.retryTimes(3, {logErrorsBeforeRetry: true}));
expectType<Mock<Promise<string>, []>>(
jest
.fn(() => Promise.resolve('string value'))
Expand Down