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: add retryImmediately option to jest.retryTimes (#14696) #14977

Merged
merged 5 commits into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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 &&
SimenB marked this conversation as resolved.
Show resolved Hide resolved
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<
SimenB marked this conversation as resolved.
Show resolved Hide resolved
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
Loading