feat: add `retryImmediately` option to jest.retryTimes (#14977)

This commit is contained in:
Vadzim Lindo 2024-03-26 09:21:52 +01:00 committed by GitHub
parent 366e8fb5e8
commit 1d682f21c7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 154 additions and 25 deletions

View File

@ -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))

View File

@ -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

View File

@ -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"
`;

View File

@ -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;

View File

@ -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();
});

View File

@ -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,
@ -78,35 +78,18 @@ 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);
}
for (const child of describeBlock.children) {
switch (child.type) {
case 'describeBlock': {
await _runTestsForDescribeBlock(child, rng);
break;
}
case 'test': {
const hasErrorsBeforeTestRun = child.errors.length > 0;
await _runTest(child, isSkipped);
if (
hasErrorsBeforeTestRun === false &&
retryTimes > 0 &&
child.errors.length > 0
) {
deferredRetryTests.push(child);
}
break;
}
}
}
// Re-run failed tests n-times if configured
for (const test of deferredRetryTests) {
const rerunTest = async (test: Circus.TestEntry) => {
let numRetriesAvailable = retryTimes;
while (numRetriesAvailable > 0 && test.errors.length > 0) {
@ -120,6 +103,43 @@ const _runTestsForDescribeBlock = async (
await _runTest(test, isSkipped);
numRetriesAvailable--;
}
};
for (const child of describeBlock.children) {
switch (child.type) {
case 'describeBlock': {
await _runTestsForDescribeBlock(child, rng);
break;
}
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 &&
hasRetryTimes
) {
await rerunTest(child);
}
if (
hasErrorsBeforeTestRun === false &&
hasRetryTimes &&
!retryImmediately
) {
deferredRetryTests.push(child);
}
break;
}
}
}
// Re-run failed tests n-times if configured
for (const test of deferredRetryTests) {
await rerunTest(test);
}
if (!isSkipped) {

View File

@ -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');

View File

@ -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()`.

View File

@ -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}`;
@ -2292,6 +2293,8 @@ export default class Runtime {
options?.logErrorsBeforeRetry;
this._environment.global[waitBeforeRetrySymbol] =
options?.waitBeforeRetry;
this._environment.global[retryImmediatelySybmbol] =
options?.retryImmediately;
return jestObject;
};

View File

@ -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();