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 2 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 `immediately` 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', () => {
});
```

`immediately` option is used to retry the failed test immediately.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just trying to clarify. Feel free to rework.

Suggested change
`immediately` option is used to retry the failed test immediately.
`immediately` 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 test in a file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice point, now it's more clear, I'll update PR.


```js
jest.retryTimes(3, {immediately: 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
@@ -1,5 +1,44 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Test Retries immediately retry 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__/immediatelyRetry.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__/immediatelyRetry.test.js:17:18)

PASS __tests__/immediatelyRetry.test.js
✓ retryTimes set
✓ truthy test"
`;

exports[`Test Retries logs error(s) before retry 1`] = `
"LOGGING RETRY ERRORS retryTimes set
RETRY 1
Expand Down
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('immediately retry after failed test', () => {
const logMessage = `console.log
FIRST TRUTHY TEST

at Object.log (__tests__/immediatelyRetry.test.js:14:13)

console.log
SECOND TRUTHY TEST

at Object.log (__tests__/immediatelyRetry.test.js:21:11)`;

const result = runJest('test-retries', ['immediatelyRetry.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__/immediatelyRetry.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, {immediately: true, logErrorsBeforeRetry: 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();
});
51 changes: 35 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 {IMMEDIATELY, RETRY_TIMES, WAIT_BEFORE_RETRY} from './types';
import {
callAsyncCircusFn,
getAllHooksForDescribe,
Expand Down Expand Up @@ -78,11 +78,32 @@ 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)[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 +112,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
SimenB marked this conversation as resolved.
Show resolved Hide resolved
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 +138,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 IMMEDIATELY = Symbol.for('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
*
* `immediately` is the flag to retry the failed tests immediately after
* failure
*
* @remarks
* Only available with `jest-circus` runner.
*/
retryTimes(
numRetries: number,
options?: {logErrorsBeforeRetry?: boolean; waitBeforeRetry?: number},
options?: {
logErrorsBeforeRetry?: boolean;
waitBeforeRetry?: number;
immediately?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice to keep alphabetical order.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

},
): Jest;
/**
* Exhausts tasks queued by `setImmediate()`.
Expand Down
2 changes: 2 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 immediatelySybmbol = Symbol.for('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,7 @@ export default class Runtime {
options?.logErrorsBeforeRetry;
this._environment.global[waitBeforeRetrySymbol] =
options?.waitBeforeRetry;
this._environment.global[immediatelySybmbol] = options?.immediately;

return jestObject;
};
Expand Down
Loading