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 4 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
18 changes: 16 additions & 2 deletions e2e/__tests__/testRetries.test.ts
Expand Up @@ -19,7 +19,8 @@ describe('Test Retries', () => {
'e2e/test-retries/',
outputFileName,
);

const logTestErrorsBeforeRetryErrorMessage =
'Errors that caused Jest to retry test: ';
afterAll(() => {
fs.unlinkSync(outputFilePath);
});
Expand All @@ -29,6 +30,20 @@ describe('Test Retries', () => {

expect(result.exitCode).toEqual(0);
expect(result.failed).toBe(false);
expect(result.stdout.includes(logTestErrorsBeforeRetryErrorMessage)).toBe(
false,
);
});

it('logs error(s) before retry', () => {
const result = runJest('test-retries', [
'logTestErrorsBeforeRetries.test.js',
]);
expect(result.exitCode).toEqual(0);
expect(result.failed).toBe(false);
expect(result.stdout.includes(logTestErrorsBeforeRetryErrorMessage)).toBe(
true,
);
});

it('reporter shows more than 1 invocation if test is retried', () => {
Expand All @@ -55,7 +70,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
19 changes: 19 additions & 0 deletions e2e/test-retries/__tests__/logTestErrorsBeforeRetries.test.js
@@ -0,0 +1,19 @@
/**
* 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);
jest.setLogTestErrorsBeforeRetry(true);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
jest.retryTimes(3);
jest.setLogTestErrorsBeforeRetry(true);
jest.retryTimes(3, {logErrorsBeforeRetry: true});=

I'd rather we just add an options bag to the existent call than adding a new method entirely

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Makes sense, I've updated it. Do we want to handle the Symbol differently as well? Right now, there is still an additional symbol for the logErrorsBeforeRetry.

it('retryTimes set', () => {
i++;
if (i === 2) {
expect(true).toBeTruthy();
} else {
expect(true).toBeFalsy();
}
});
7 changes: 6 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_TEST_ERRORS_BEFORE_RETRY, RETRY_TIMES} from './types';
import {
callAsyncCircusFn,
getAllHooksForDescribe,
Expand Down Expand Up @@ -44,6 +44,8 @@ const _runTestsForDescribeBlock = async (

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

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

while (numRetriesAvailable > 0 && test.errors.length > 0) {
if (logTestErrorsBeforeRetry) {
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_TEST_ERRORS_BEFORE_RETRY = Symbol.for(
'LOG_TEST_ERRORS_BEFORE_RETRY',
) as unknown as 'LOG_TEST_ERRORS_BEFORE_RETRY';
declare global {
namespace NodeJS {
interface Global {
STATE_SYM_SYMBOL: Circus.State;
RETRY_TIMES_SYMBOL: string;
TEST_TIMEOUT_SYMBOL: number;
LOG_TEST_ERRORS_BEFORE_RETRY: boolean;
expect: typeof expect;
}
}
Expand Down
9 changes: 9 additions & 0 deletions packages/jest-environment/src/index.ts
Expand Up @@ -193,11 +193,20 @@ 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`!
*/
retryTimes(numRetries: number): Jest;

/**
* This sets whether Jest will log the test errors before a retry happens
* When tests fail, whether due to an error or timeout, it is unclear
* what the reason for the failure is because no logging takes place.
*/
setLogTestErrorsBeforeRetry(condition: boolean): Jest;

/**
* Exhausts tasks queued by setImmediate().
*
Expand Down
8 changes: 8 additions & 0 deletions 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 logTestErrorsBeforeRetry = Symbol.for('LOG_TEST_ERRORS_BEFORE_RETRY');

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

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

const setLogTestErrorsBeforeRetry = (condition: boolean) => {
// @ts-expect-error: https://github.com/Microsoft/TypeScript/issues/24587
this._environment.global[logTestErrorsBeforeRetry] = condition;
return jestObject;
};

const retryTimes = (numTestRetries: number) => {
// @ts-expect-error: https://github.com/Microsoft/TypeScript/issues/24587
this._environment.global[retryTimesSymbol] = numTestRetries;
Expand Down Expand Up @@ -1997,6 +2004,7 @@ export default class Runtime {
runAllTicks: () => _getFakeTimers().runAllTicks(),
runAllTimers: () => _getFakeTimers().runAllTimers(),
runOnlyPendingTimers: () => _getFakeTimers().runOnlyPendingTimers(),
setLogTestErrorsBeforeRetry,
setMock: (moduleName: string, mock: unknown) =>
setMockFactory(moduleName, () => mock),
setSystemTime: (now?: number | Date) => {
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.setLogTestErrorsBeforeRetry(true));
expectType<Mock<Promise<string>, []>>(
jest
.fn(() => Promise.resolve('string value'))
Expand Down