Skip to content

Commit

Permalink
feat: add retryImmediately option to jest.retryTimes (#14977)
Browse files Browse the repository at this point in the history
  • Loading branch information
Vadimchesh authored Mar 26, 2024
1 parent 366e8fb commit 1d682f2
Show file tree
Hide file tree
Showing 10 changed files with 146 additions and 17 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

- `[jest-circus, jest-cli, jest-config]` Add `waitNextEventLoopTurnForUnhandledRejectionEvents` flag to minimise performance impact of correct detection of unhandled promise rejections introduced in [#14315](https://github.com/jestjs/jest/pull/14315) ([#14681](https://github.com/jestjs/jest/pull/14681))
- `[jest-circus]` Add a `waitBeforeRetry` option to `jest.retryTimes` ([#14738](https://github.com/jestjs/jest/pull/14738))
- `[jest-circus]` Add a `retryImmediately` option to `jest.retryTimes` ([#14696](https://github.com/jestjs/jest/pull/14696))
- `[jest-circus, jest-jasmine2]` Allow `setupFilesAfterEnv` to export an async function ([#10962](https://github.com/jestjs/jest/issues/10962))
- `[jest-config]` [**BREAKING**] Add `mts` and `cts` to default `moduleFileExtensions` config ([#14369](https://github.com/facebook/jest/pull/14369))
- `[jest-config]` [**BREAKING**] Update `testMatch` and `testRegex` default option for supporting `mjs`, `cjs`, `mts`, and `cts` ([#14584](https://github.com/jestjs/jest/pull/14584))
Expand Down
10 changes: 10 additions & 0 deletions docs/JestObjectAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -1139,6 +1139,16 @@ test('will fail', () => {
});
```

`retryImmediately` option is used to retry the failed test immediately after the failure. If this option is not specified, the tests are retried after Jest is finished running all other tests in the file.

```js
jest.retryTimes(3, {retryImmediately: true});

test('will fail', () => {
expect(true).toBe(false);
});
```

Returns the `jest` object for chaining.

:::caution
Expand Down
39 changes: 39 additions & 0 deletions e2e/__tests__/__snapshots__/testRetries.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,42 @@ exports[`Test Retries wait before retry with fake timers 1`] = `
PASS __tests__/waitBeforeRetryFakeTimers.test.js
✓ retryTimes set with fake timers"
`;

exports[`Test Retries with flag retryImmediately retry immediately after failed test 1`] = `
"LOGGING RETRY ERRORS retryTimes set
RETRY 1
expect(received).toBeFalsy()
Received: true
15 | expect(true).toBeTruthy();
16 | } else {
> 17 | expect(true).toBeFalsy();
| ^
18 | }
19 | });
20 | it('truthy test', () => {
at Object.toBeFalsy (__tests__/retryImmediately.test.js:17:18)
RETRY 2
expect(received).toBeFalsy()
Received: true
15 | expect(true).toBeTruthy();
16 | } else {
> 17 | expect(true).toBeFalsy();
| ^
18 | }
19 | });
20 | it('truthy test', () => {
at Object.toBeFalsy (__tests__/retryImmediately.test.js:17:18)
PASS __tests__/retryImmediately.test.js
retryTimes set
truthy test"
`;
20 changes: 20 additions & 0 deletions e2e/__tests__/testRetries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,26 @@ describe('Test Retries', () => {
expect(extractSummary(result.stderr).rest).toMatchSnapshot();
});

it('with flag retryImmediately retry immediately after failed test', () => {
const logMessage = `console.log
FIRST TRUTHY TEST
at Object.log (__tests__/retryImmediately.test.js:14:13)
console.log
SECOND TRUTHY TEST
at Object.log (__tests__/retryImmediately.test.js:21:11)`;

const result = runJest('test-retries', ['retryImmediately.test.js']);
const stdout = result.stdout.trim();
expect(result.exitCode).toBe(0);
expect(result.failed).toBe(false);
expect(result.stderr).toContain(logErrorsBeforeRetryErrorMessage);
expect(stdout).toBe(logMessage);
expect(extractSummary(result.stderr).rest).toMatchSnapshot();
});

it('reporter shows more than 1 invocation if test is retried', () => {
let jsonResult;

Expand Down
23 changes: 23 additions & 0 deletions e2e/test-retries/__tests__/retryImmediately.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';

jest.retryTimes(3, {logErrorsBeforeRetry: true, retryImmediately: true});
let i = 0;
it('retryTimes set', () => {
i++;
if (i === 3) {
console.log('FIRST TRUTHY TEST');
expect(true).toBeTruthy();
} else {
expect(true).toBeFalsy();
}
});
it('truthy test', () => {
console.log('SECOND TRUTHY TEST');
expect(true).toBeTruthy();
});
52 changes: 36 additions & 16 deletions packages/jest-circus/src/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import shuffleArray, {
rngBuilder,
} from './shuffleArray';
import {dispatch, getState} from './state';
import {RETRY_TIMES, WAIT_BEFORE_RETRY} from './types';
import {RETRY_IMMEDIATELY, RETRY_TIMES, WAIT_BEFORE_RETRY} from './types';
import {
callAsyncCircusFn,
getAllHooksForDescribe,
Expand Down Expand Up @@ -78,11 +78,33 @@ const _runTestsForDescribeBlock = async (
(global as Global.Global)[WAIT_BEFORE_RETRY] as string,
10,
) || 0;

const retryImmediately: boolean =
// eslint-disable-next-line no-restricted-globals
((global as Global.Global)[RETRY_IMMEDIATELY] as any) || false;

const deferredRetryTests = [];

if (rng) {
describeBlock.children = shuffleArray(describeBlock.children, rng);
}

const rerunTest = async (test: Circus.TestEntry) => {
let numRetriesAvailable = retryTimes;

while (numRetriesAvailable > 0 && test.errors.length > 0) {
// Clear errors so retries occur
await dispatch({name: 'test_retry', test});

if (waitBeforeRetry > 0) {
await new Promise(resolve => setTimeout(resolve, waitBeforeRetry));
}

await _runTest(test, isSkipped);
numRetriesAvailable--;
}
};

for (const child of describeBlock.children) {
switch (child.type) {
case 'describeBlock': {
Expand All @@ -91,12 +113,22 @@ const _runTestsForDescribeBlock = async (
}
case 'test': {
const hasErrorsBeforeTestRun = child.errors.length > 0;
const hasRetryTimes = retryTimes > 0;
await _runTest(child, isSkipped);

// If immediate retry is set, we retry the test immediately after the first run
if (
retryImmediately &&
hasErrorsBeforeTestRun === false &&
retryTimes > 0 &&
child.errors.length > 0
hasRetryTimes
) {
await rerunTest(child);
}

if (
hasErrorsBeforeTestRun === false &&
hasRetryTimes &&
!retryImmediately
) {
deferredRetryTests.push(child);
}
Expand All @@ -107,19 +139,7 @@ const _runTestsForDescribeBlock = async (

// Re-run failed tests n-times if configured
for (const test of deferredRetryTests) {
let numRetriesAvailable = retryTimes;

while (numRetriesAvailable > 0 && test.errors.length > 0) {
// Clear errors so retries occur
await dispatch({name: 'test_retry', test});

if (waitBeforeRetry > 0) {
await new Promise(resolve => setTimeout(resolve, waitBeforeRetry));
}

await _runTest(test, isSkipped);
numRetriesAvailable--;
}
await rerunTest(test);
}

if (!isSkipped) {
Expand Down
1 change: 1 addition & 0 deletions packages/jest-circus/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

export const STATE_SYM = Symbol('JEST_STATE_SYMBOL');
export const RETRY_TIMES = Symbol.for('RETRY_TIMES');
export const RETRY_IMMEDIATELY = Symbol.for('RETRY_IMMEDIATELY');
export const WAIT_BEFORE_RETRY = Symbol.for('WAIT_BEFORE_RETRY');
// To pass this value from Runtime object to state we need to use global[sym]
export const TEST_TIMEOUT_SYMBOL = Symbol.for('TEST_TIMEOUT_SYMBOL');
Expand Down
9 changes: 8 additions & 1 deletion packages/jest-environment/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,12 +300,19 @@ export interface Jest {
*
* `waitBeforeRetry` is the number of milliseconds to wait before retrying
*
* `retryImmediately` is the flag to retry the failed test immediately after
* failure
*
* @remarks
* Only available with `jest-circus` runner.
*/
retryTimes(
numRetries: number,
options?: {logErrorsBeforeRetry?: boolean; waitBeforeRetry?: number},
options?: {
logErrorsBeforeRetry?: boolean;
retryImmediately?: boolean;
waitBeforeRetry?: number;
},
): Jest;
/**
* Exhausts tasks queued by `setImmediate()`.
Expand Down
3 changes: 3 additions & 0 deletions packages/jest-runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ type ResolveOptions = Parameters<typeof require.resolve>[1] & {
const testTimeoutSymbol = Symbol.for('TEST_TIMEOUT_SYMBOL');
const retryTimesSymbol = Symbol.for('RETRY_TIMES');
const waitBeforeRetrySymbol = Symbol.for('WAIT_BEFORE_RETRY');
const retryImmediatelySybmbol = Symbol.for('RETRY_IMMEDIATELY');
const logErrorsBeforeRetrySymbol = Symbol.for('LOG_ERRORS_BEFORE_RETRY');

const NODE_MODULES = `${path.sep}node_modules${path.sep}`;
Expand Down Expand Up @@ -2292,6 +2293,8 @@ export default class Runtime {
options?.logErrorsBeforeRetry;
this._environment.global[waitBeforeRetrySymbol] =
options?.waitBeforeRetry;
this._environment.global[retryImmediatelySybmbol] =
options?.retryImmediately;

return jestObject;
};
Expand Down
5 changes: 5 additions & 0 deletions packages/jest-types/__typetests__/jest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,11 @@ expect(jest.retryTimes(3, {logErrorsBeforeRetry: 'all'})).type.toRaiseError();
expect(jest.retryTimes({logErrorsBeforeRetry: true})).type.toRaiseError();
expect(jest.retryTimes(3, {waitBeforeRetry: 1000})).type.toEqual<typeof jest>();
expect(jest.retryTimes(3, {waitBeforeRetry: true})).type.toRaiseError();
expect(jest.retryTimes(3, {retryImmediately: true})).type.toEqual<
typeof jest
>();
expect(jest.retryTimes(3, {retryImmediately: 'now'})).type.toRaiseError();
expect(jest.retryTimes(3, {retryImmediately: 1000})).type.toRaiseError();
expect(jest.retryTimes({logErrorsBeforeRetry: 'all'})).type.toRaiseError();
expect(jest.retryTimes()).type.toRaiseError();

Expand Down

0 comments on commit 1d682f2

Please sign in to comment.