fix(jest-circus) correct concurrent event ordering (#15381)

This commit is contained in:
Ian Mckay 2025-01-15 04:06:53 -08:00 committed by GitHub
parent 95f21e4f82
commit fba7764631
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 1167 additions and 111 deletions

6
.vscode/launch.json vendored
View File

@ -16,6 +16,12 @@
"windows": {
"program": "${workspaceFolder}/node_modules/jest/bin/jest"
}
},
{
"name": "Attach to jest",
"type": "node",
"request": "attach",
"port": 9229
}
]
}

View File

@ -62,6 +62,7 @@
- `[jest-circus]` Replace recursive `makeTestResults` implementation with iterative one ([#14760](https://github.com/jestjs/jest/pull/14760))
- `[jest-circus]` Omit `expect.hasAssertions()` errors if a test already has errors ([#14866](https://github.com/jestjs/jest/pull/14866))
- `[jest-circus, jest-expect, jest-snapshot]` Pass `test.failing` tests when containing failing snapshot matchers ([#14313](https://github.com/jestjs/jest/pull/14313))
- `[jest-circus]` Concurrent tests now emit jest circus events at the correct point and in the expected order. ([#15381](https://github.com/jestjs/jest/pull/15381))
- `[jest-cli]` [**BREAKING**] Validate CLI flags that require arguments receives them ([#14783](https://github.com/jestjs/jest/pull/14783))
- `[jest-config]` Make sure to respect `runInBand` option ([#14578](https://github.com/jestjs/jest/pull/14578))
- `[jest-config]` Support `testTimeout` in project config ([#14697](https://github.com/jestjs/jest/pull/14697))

View File

@ -0,0 +1,230 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`all passing runs the tests in the correct order 1`] = `
" console.log
beforeAll
at log (__tests__/concurrent.test.js:15:11)
console.log
START "one"
at log (__tests__/concurrent.test.js:15:11)
console.log
START "two"
at log (__tests__/concurrent.test.js:15:11)
console.log
START "three"
at log (__tests__/concurrent.test.js:15:11)
console.log
START "four"
at log (__tests__/concurrent.test.js:15:11)
console.log
START "five"
at log (__tests__/concurrent.test.js:15:11)
console.log
END: "three"
at log (__tests__/concurrent.test.js:15:11)
console.log
START "six"
at log (__tests__/concurrent.test.js:15:11)
console.log
END: "one"
at log (__tests__/concurrent.test.js:15:11)
console.log
START "seven"
at log (__tests__/concurrent.test.js:15:11)
console.log
END: "two"
at log (__tests__/concurrent.test.js:15:11)
console.log
START "eight"
at log (__tests__/concurrent.test.js:15:11)
console.log
END: "four"
at log (__tests__/concurrent.test.js:15:11)
console.log
START "nine"
at log (__tests__/concurrent.test.js:15:11)
console.log
END: "nine"
at log (__tests__/concurrent.test.js:15:11)
console.log
START "ten"
at log (__tests__/concurrent.test.js:15:11)
console.log
END: "five"
at log (__tests__/concurrent.test.js:15:11)
console.log
END: "six"
at log (__tests__/concurrent.test.js:15:11)
console.log
END: "seven"
at log (__tests__/concurrent.test.js:15:11)
console.log
END: "ten"
at log (__tests__/concurrent.test.js:15:11)
console.log
END: "eight"
at log (__tests__/concurrent.test.js:15:11)
console.log
afterAll
at log (__tests__/concurrent.test.js:15:11)
"
`;
exports[`with only runs the tests in the correct order 1`] = `
" console.log
beforeAll
at log (__tests__/concurrent-only.test.js:15:11)
console.log
START "four"
at log (__tests__/concurrent-only.test.js:15:11)
console.log
START "six"
at log (__tests__/concurrent-only.test.js:15:11)
console.log
START "nine"
at log (__tests__/concurrent-only.test.js:15:11)
console.log
END: "nine"
at log (__tests__/concurrent-only.test.js:15:11)
console.log
END: "six"
at log (__tests__/concurrent-only.test.js:15:11)
console.log
END: "four"
at log (__tests__/concurrent-only.test.js:15:11)
console.log
afterAll
at log (__tests__/concurrent-only.test.js:15:11)
"
`;
exports[`with skip runs the tests in the correct order 1`] = `
" console.log
beforeAll
at log (__tests__/concurrent-skip.test.js:15:11)
console.log
START "one"
at log (__tests__/concurrent-skip.test.js:15:11)
console.log
START "two"
at log (__tests__/concurrent-skip.test.js:15:11)
console.log
START "four"
at log (__tests__/concurrent-skip.test.js:15:11)
console.log
START "seven"
at log (__tests__/concurrent-skip.test.js:15:11)
console.log
START "eight"
at log (__tests__/concurrent-skip.test.js:15:11)
console.log
END: "one"
at log (__tests__/concurrent-skip.test.js:15:11)
console.log
START "ten"
at log (__tests__/concurrent-skip.test.js:15:11)
console.log
END: "two"
at log (__tests__/concurrent-skip.test.js:15:11)
console.log
END: "seven"
at log (__tests__/concurrent-skip.test.js:15:11)
console.log
END: "four"
at log (__tests__/concurrent-skip.test.js:15:11)
console.log
END: "eight"
at log (__tests__/concurrent-skip.test.js:15:11)
console.log
END: "ten"
at log (__tests__/concurrent-skip.test.js:15:11)
console.log
afterAll
at log (__tests__/concurrent-skip.test.js:15:11)
"
`;

View File

@ -94,21 +94,27 @@ exports[`works with all statuses 1`] = `
exports[`works with concurrent and only mode 1`] = `
"FAIL __tests__/worksWithConcurrentOnlyMode.test.js
block with concurrent
✕ failing passes = fails
✕ .add(1, 1)
✕ .add(1, 2)
✕ .add(2, 1)
✓ failing fails = passes
✕ .only.failing() should fail
✓ .only.failing() should pass
✕ .add(1, 1) .only.failing.each() should fail
✕ .add(1, 2) .only.failing.each() should fail
✕ .add(2, 1) .only.failing.each() should fail
✓ .add(1, 1) .only.failing.each() should pass
✓ .add(1, 2) .only.failing.each() should pass
✓ .add(2, 1) .only.failing.each() should pass
○ skipped skipped failing test
○ skipped .add(1, 1) skipped each
○ skipped .add(1, 2) skipped each
○ skipped .add(2, 1) skipped each
○ skipped skipped failing fails
● block with concurrent failing passes = fails
● block with concurrent .only.failing() should fail
Failing test passed even though it was supposed to fail. Remove \`.failing\` to remove error.
11 | });
12 |
> 13 | it.concurrent.only.failing('failing passes = fails', () => {
> 13 | it.concurrent.only.failing('.only.failing() should fail', () => {
| ^
14 | expect(10).toBe(10);
15 | });
@ -117,64 +123,67 @@ exports[`works with concurrent and only mode 1`] = `
at failing (__tests__/worksWithConcurrentOnlyMode.test.js:13:22)
at Object.describe (__tests__/worksWithConcurrentOnlyMode.test.js:8:1)
● block with concurrent .add(1, 1)
● block with concurrent .add(1, 1) .only.failing.each() should fail
Failing test passed even though it was supposed to fail. Remove \`.failing\` to remove error.
15 | });
16 |
> 17 | test.concurrent.only.failing.each([
19 | });
20 |
> 21 | test.concurrent.only.failing.each([
| ^
18 | {a: 1, b: 1, expected: 2},
19 | {a: 1, b: 2, expected: 3},
20 | {a: 2, b: 1, expected: 3},
22 | {a: 1, b: 1, expected: 2},
23 | {a: 1, b: 2, expected: 3},
24 | {a: 2, b: 1, expected: 3},
at each (__tests__/worksWithConcurrentOnlyMode.test.js:17:32)
at each (__tests__/worksWithConcurrentOnlyMode.test.js:21:32)
at Object.describe (__tests__/worksWithConcurrentOnlyMode.test.js:8:1)
● block with concurrent .add(1, 2)
● block with concurrent .add(1, 2) .only.failing.each() should fail
Failing test passed even though it was supposed to fail. Remove \`.failing\` to remove error.
15 | });
16 |
> 17 | test.concurrent.only.failing.each([
19 | });
20 |
> 21 | test.concurrent.only.failing.each([
| ^
18 | {a: 1, b: 1, expected: 2},
19 | {a: 1, b: 2, expected: 3},
20 | {a: 2, b: 1, expected: 3},
22 | {a: 1, b: 1, expected: 2},
23 | {a: 1, b: 2, expected: 3},
24 | {a: 2, b: 1, expected: 3},
at each (__tests__/worksWithConcurrentOnlyMode.test.js:17:32)
at each (__tests__/worksWithConcurrentOnlyMode.test.js:21:32)
at Object.describe (__tests__/worksWithConcurrentOnlyMode.test.js:8:1)
● block with concurrent .add(2, 1)
● block with concurrent .add(2, 1) .only.failing.each() should fail
Failing test passed even though it was supposed to fail. Remove \`.failing\` to remove error.
15 | });
16 |
> 17 | test.concurrent.only.failing.each([
19 | });
20 |
> 21 | test.concurrent.only.failing.each([
| ^
18 | {a: 1, b: 1, expected: 2},
19 | {a: 1, b: 2, expected: 3},
20 | {a: 2, b: 1, expected: 3},
22 | {a: 1, b: 1, expected: 2},
23 | {a: 1, b: 2, expected: 3},
24 | {a: 2, b: 1, expected: 3},
at each (__tests__/worksWithConcurrentOnlyMode.test.js:17:32)
at each (__tests__/worksWithConcurrentOnlyMode.test.js:21:32)
at Object.describe (__tests__/worksWithConcurrentOnlyMode.test.js:8:1)"
`;
exports[`works with concurrent mode 1`] = `
"FAIL __tests__/worksWithConcurrentMode.test.js
block with concurrent
✕ failing test
✕ failing passes = fails
✕ .add(1, 1)
✕ .add(1, 2)
✕ .add(2, 1)
✓ failing fails = passes
✕ test should fail
✕ .failing() should fail
✓ .failing() should pass
✕ .add(1, 1) .failing.each() should fail
✕ .add(1, 2) .failing.each() should fail
✕ .add(2, 1) .failing.each() should fail
✓ .add(1, 1) .failing.each() should pass
✓ .add(1, 2) .failing.each() should pass
✓ .add(2, 1) .failing.each() should pass
○ skipped skipped failing fails
● block with concurrent failing test
● block with concurrent test should fail
expect(received).toBe(expected) // Object.is equality
@ -182,22 +191,22 @@ exports[`works with concurrent mode 1`] = `
Received: 10
8 | describe('block with concurrent', () => {
9 | it('failing test', () => {
9 | it('test should fail', () => {
> 10 | expect(10).toBe(101);
| ^
11 | });
12 |
13 | it.concurrent.failing('failing passes = fails', () => {
13 | it.concurrent.failing('.failing() should fail', () => {
at Object.toBe (__tests__/worksWithConcurrentMode.test.js:10:16)
● block with concurrent failing passes = fails
● block with concurrent .failing() should fail
Failing test passed even though it was supposed to fail. Remove \`.failing\` to remove error.
11 | });
12 |
> 13 | it.concurrent.failing('failing passes = fails', () => {
> 13 | it.concurrent.failing('.failing() should fail', () => {
| ^
14 | expect(10).toBe(10);
15 | });
@ -206,49 +215,49 @@ exports[`works with concurrent mode 1`] = `
at failing (__tests__/worksWithConcurrentMode.test.js:13:17)
at Object.describe (__tests__/worksWithConcurrentMode.test.js:8:1)
● block with concurrent .add(1, 1)
● block with concurrent .add(1, 1) .failing.each() should fail
Failing test passed even though it was supposed to fail. Remove \`.failing\` to remove error.
15 | });
16 |
> 17 | test.concurrent.failing.each([
19 | });
20 |
> 21 | test.concurrent.failing.each([
| ^
18 | {a: 1, b: 1, expected: 2},
19 | {a: 1, b: 2, expected: 3},
20 | {a: 2, b: 1, expected: 3},
22 | {a: 1, b: 1, expected: 2},
23 | {a: 1, b: 2, expected: 3},
24 | {a: 2, b: 1, expected: 3},
at each (__tests__/worksWithConcurrentMode.test.js:17:27)
at each (__tests__/worksWithConcurrentMode.test.js:21:27)
at Object.describe (__tests__/worksWithConcurrentMode.test.js:8:1)
● block with concurrent .add(1, 2)
● block with concurrent .add(1, 2) .failing.each() should fail
Failing test passed even though it was supposed to fail. Remove \`.failing\` to remove error.
15 | });
16 |
> 17 | test.concurrent.failing.each([
19 | });
20 |
> 21 | test.concurrent.failing.each([
| ^
18 | {a: 1, b: 1, expected: 2},
19 | {a: 1, b: 2, expected: 3},
20 | {a: 2, b: 1, expected: 3},
22 | {a: 1, b: 1, expected: 2},
23 | {a: 1, b: 2, expected: 3},
24 | {a: 2, b: 1, expected: 3},
at each (__tests__/worksWithConcurrentMode.test.js:17:27)
at each (__tests__/worksWithConcurrentMode.test.js:21:27)
at Object.describe (__tests__/worksWithConcurrentMode.test.js:8:1)
● block with concurrent .add(2, 1)
● block with concurrent .add(2, 1) .failing.each() should fail
Failing test passed even though it was supposed to fail. Remove \`.failing\` to remove error.
15 | });
16 |
> 17 | test.concurrent.failing.each([
19 | });
20 |
> 21 | test.concurrent.failing.each([
| ^
18 | {a: 1, b: 1, expected: 2},
19 | {a: 1, b: 2, expected: 3},
20 | {a: 2, b: 1, expected: 3},
22 | {a: 1, b: 1, expected: 2},
23 | {a: 1, b: 2, expected: 3},
24 | {a: 2, b: 1, expected: 3},
at each (__tests__/worksWithConcurrentMode.test.js:17:27)
at each (__tests__/worksWithConcurrentMode.test.js:21:27)
at Object.describe (__tests__/worksWithConcurrentMode.test.js:8:1)"
`;

View File

@ -17,7 +17,7 @@ exports[`throws an error about unsupported modifier 1`] = `
at Object.failing (__tests__/statuses.test.js:22:4)
FAIL __tests__/worksWithConcurrentMode.test.js
● block with concurrent failing test
● block with concurrent test should fail
expect(received).toBe(expected) // Object.is equality
@ -25,12 +25,12 @@ FAIL __tests__/worksWithConcurrentMode.test.js
Received: 10
8 | describe('block with concurrent', () => {
9 | it('failing test', () => {
9 | it('test should fail', () => {
> 10 | expect(10).toBe(101);
| ^
11 | });
12 |
13 | it.concurrent.failing('failing passes = fails', () => {
13 | it.concurrent.failing('.failing() should fail', () => {
at Object.toBe (__tests__/worksWithConcurrentMode.test.js:10:16)
@ -40,7 +40,7 @@ FAIL __tests__/worksWithConcurrentMode.test.js
11 | });
12 |
> 13 | it.concurrent.failing('failing passes = fails', () => {
> 13 | it.concurrent.failing('.failing() should fail', () => {
| ^
14 | expect(10).toBe(10);
15 | });
@ -64,7 +64,7 @@ FAIL __tests__/worksWithConcurrentOnlyMode.test.js
| ^
11 | });
12 |
13 | it.concurrent.only.failing('failing passes = fails', () => {
13 | it.concurrent.only.failing('.only.failing() should fail', () => {
at Object.toBe (__tests__/worksWithConcurrentOnlyMode.test.js:10:16)
@ -74,7 +74,7 @@ FAIL __tests__/worksWithConcurrentOnlyMode.test.js
11 | });
12 |
> 13 | it.concurrent.only.failing('failing passes = fails', () => {
> 13 | it.concurrent.only.failing('.only.failing() should fail', () => {
| ^
14 | expect(10).toBe(10);
15 | });

View File

@ -1,5 +1,80 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Concurrent Test Retries with flag retryImmediately retry immediately after failed test 1`] = `
"LOGGING RETRY ERRORS retryable test 1
RETRY 1
expect(received).toBeFalsy()
Received: true
15 | expect(true).toBeTruthy();
16 | } else {
> 17 | expect(true).toBeFalsy();
| ^
18 | }
19 | });
20 |
at Object.toBeFalsy (__tests__/retryImmediatelyConcurrent.test.js:17:18)
RETRY 2
expect(received).toBeFalsy()
Received: true
15 | expect(true).toBeTruthy();
16 | } else {
> 17 | expect(true).toBeFalsy();
| ^
18 | }
19 | });
20 |
at Object.toBeFalsy (__tests__/retryImmediatelyConcurrent.test.js:17:18)
at async Promise.all (index 0)
LOGGING RETRY ERRORS retryable test 2
RETRY 1
expect(received).toBeFalsy()
Received: true
26 | expect(true).toBeTruthy();
27 | } else {
> 28 | expect(true).toBeFalsy();
| ^
29 | }
30 | });
31 | it.concurrent('truthy test', () => {
at Object.toBeFalsy (__tests__/retryImmediatelyConcurrent.test.js:28:18)
RETRY 2
expect(received).toBeFalsy()
Received: true
26 | expect(true).toBeTruthy();
27 | } else {
> 28 | expect(true).toBeFalsy();
| ^
29 | }
30 | });
31 | it.concurrent('truthy test', () => {
at Object.toBeFalsy (__tests__/retryImmediatelyConcurrent.test.js:28:18)
at async Promise.all (index 1)
PASS __tests__/retryImmediatelyConcurrent.test.js
✓ retryable test 1
✓ retryable test 2
✓ truthy test"
`;
exports[`Test Retries logs error(s) before retry 1`] = `
"LOGGING RETRY ERRORS retryTimes set
RETRY 1

View File

@ -0,0 +1,68 @@
/**
* 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.
*/
import exp = require('constants');
import {skipSuiteOnJasmine} from '@jest/test-utils';
import runJest, {json as runWithJson} from '../runJest';
skipSuiteOnJasmine();
describe('all passing', () => {
it('runs the correct number of tests', () => {
const {json, exitCode} = runWithJson('circus-concurrent', [
'concurrent.test.js',
]);
expect(exitCode).toBe(0);
expect(json.numTotalTests).toBe(10);
expect(json.numPassedTests).toBe(10);
expect(json.numFailedTests).toBe(0);
expect(json.numPendingTests).toBe(0);
});
it('runs the tests in the correct order', () => {
const {stdout} = runJest('circus-concurrent', ['concurrent.test.js']);
expect(stdout).toMatchSnapshot();
});
});
describe('with skip', () => {
it('runs the correct number of tests', () => {
const {json, exitCode} = runWithJson('circus-concurrent', [
'concurrent-skip.test.js',
]);
expect(exitCode).toBe(0);
expect(json.numTotalTests).toBe(10);
expect(json.numPassedTests).toBe(6);
expect(json.numFailedTests).toBe(0);
expect(json.numPendingTests).toBe(4);
});
it('runs the tests in the correct order', () => {
const {stdout} = runJest('circus-concurrent', ['concurrent-skip.test.js']);
expect(stdout).toMatchSnapshot();
});
});
describe('with only', () => {
it('runs the correct number of tests', () => {
const {json, exitCode} = runWithJson('circus-concurrent', [
'concurrent-only.test.js',
]);
expect(exitCode).toBe(0);
expect(json.numTotalTests).toBe(10);
expect(json.numPassedTests).toBe(3);
expect(json.numFailedTests).toBe(0);
expect(json.numPendingTests).toBe(7);
});
it('runs the tests in the correct order', () => {
const {stdout} = runJest('circus-concurrent', ['concurrent-only.test.js']);
expect(stdout).toMatchSnapshot();
});
});

View File

@ -173,3 +173,149 @@ describe('Test Retries', () => {
expect(jsonResult.testResults[0].testResults[0].invocations).toBe(1);
});
});
describe('Concurrent Test Retries', () => {
const outputFileName = 'retries.result.json';
const outputFilePath = path.join(
process.cwd(),
'e2e/test-retries/',
outputFileName,
);
const logErrorsBeforeRetryErrorMessage = 'LOGGING RETRY ERRORS';
afterAll(() => {
fs.unlinkSync(outputFilePath);
});
it('retries failed tests', () => {
const result = runJest('test-retries', ['e2eConcurrent.test.js']);
expect(result.exitCode).toBe(0);
expect(result.failed).toBe(false);
expect(result.stderr).not.toContain(logErrorsBeforeRetryErrorMessage);
});
it('with flag retryImmediately retry immediately after failed test', () => {
const logMessage = `console.log
FIRST TRUTHY TEST
at Object.log (__tests__/retryImmediatelyConcurrent.test.js:32:11)
console.log
SECOND TRUTHY TEST
at Object.log (__tests__/retryImmediatelyConcurrent.test.js:14:13)
at async Promise.all (index 0)
console.log
THIRD TRUTHY TEST
at Object.log (__tests__/retryImmediatelyConcurrent.test.js:25:13)
at async Promise.all (index 1)`;
const result = runJest('test-retries', [
'retryImmediatelyConcurrent.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;
const reporterConfig = {
reporters: [
['<rootDir>/reporters/RetryReporter.js', {output: outputFilePath}],
],
};
runJest('test-retries', [
'--config',
JSON.stringify(reporterConfig),
'__tests__/retryConcurrent.test.js',
]);
const testOutput = fs.readFileSync(outputFilePath, 'utf8');
try {
jsonResult = JSON.parse(testOutput);
} catch (error: any) {
throw new Error(
`Can't parse the JSON result from ${outputFileName}, ${error.toString()}`,
);
}
expect(jsonResult.numPassedTests).toBe(1);
expect(jsonResult.numFailedTests).toBe(1);
expect(jsonResult.numPendingTests).toBe(0);
expect(jsonResult.testResults[0].testResults[0].invocations).toBe(4);
expect(jsonResult.testResults[0].testResults[1].invocations).toBe(1);
});
it('reporter shows 1 invocation if tests are not retried', () => {
let jsonResult;
const reporterConfig = {
reporters: [
['<rootDir>/reporters/RetryReporter.js', {output: outputFilePath}],
],
};
runJest('test-retries', [
'--config',
JSON.stringify(reporterConfig),
'controlConcurrent.test.js',
]);
const testOutput = fs.readFileSync(outputFilePath, 'utf8');
try {
jsonResult = JSON.parse(testOutput);
} catch (error: any) {
throw new Error(
`Can't parse the JSON result from ${outputFileName}, ${error.toString()}`,
);
}
expect(jsonResult.numPassedTests).toBe(0);
expect(jsonResult.numFailedTests).toBe(1);
expect(jsonResult.numPendingTests).toBe(0);
expect(jsonResult.testResults[0].testResults[0].invocations).toBe(1);
});
it('tests are not retried if beforeAll hook failure occurs', () => {
let jsonResult;
const reporterConfig = {
reporters: [
['<rootDir>/reporters/RetryReporter.js', {output: outputFilePath}],
],
};
runJest('test-retries', [
'--config',
JSON.stringify(reporterConfig),
'beforeAllFailureConcurrent.test.js',
]);
const testOutput = fs.readFileSync(outputFilePath, 'utf8');
try {
jsonResult = JSON.parse(testOutput);
} catch (error: any) {
throw new Error(
`Can't parse the JSON result from ${outputFileName}, ${error.toString()}`,
);
}
expect(jsonResult.numPassedTests).toBe(0);
expect(jsonResult.numFailedTests).toBe(2);
expect(jsonResult.numPendingTests).toBe(0);
expect(jsonResult.testResults[0].testResults[0].invocations).toBe(1);
expect(jsonResult.testResults[0].testResults[1].invocations).toBe(1);
});
});

View File

@ -0,0 +1,63 @@
/**
* 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';
const {setTimeout} = require('timers/promises');
let delta = Date.now();
const includeDelta = false;
const marker = s => {
console.log(s, includeDelta ? `+${Date.now() - delta}ms` : '');
delta = Date.now();
};
beforeAll(() => marker('beforeAll'));
afterAll(() => marker('afterAll'));
beforeEach(() => marker('beforeEach'));
afterEach(() => marker('afterEach'));
const testFn = (name, delay, fail) => {
return async () => {
marker(`START "${name}"`);
await setTimeout(delay);
if (fail) {
throw new Error(`${name} failed`);
}
expect(name).toBe(name);
expect.assertions(1);
marker(`END: "${name}"`);
};
};
it.concurrent('one', testFn('one', 85));
it('two (sequential)', testFn('two (sequential)', 100));
describe('level 1', () => {
beforeEach(() => marker('beforeEach level 1'));
afterEach(() => marker('afterEach level 1'));
it.concurrent('three', testFn('three', 70));
it('four (sequential)', testFn('four (sequential)', 120));
describe('level 2', () => {
beforeEach(() => marker('beforeEach level 2'));
afterEach(() => marker('afterEach level 2'));
it.concurrent('five', testFn('five', 160));
it('six (sequential)', testFn('six (sequential)', 100));
});
it.concurrent('seven', testFn('seven', 100));
it.concurrent('eight', testFn('eight', 120));
});
it.concurrent('nine', testFn('nine', 20));
it.concurrent('ten', testFn('ten', 50));

View File

@ -0,0 +1,63 @@
/**
* 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';
const {setTimeout} = require('timers/promises');
let delta = Date.now();
const includeDelta = false;
const marker = s => {
console.log(s, includeDelta ? `+${Date.now() - delta}ms` : '');
delta = Date.now();
};
beforeAll(() => marker('beforeAll'));
afterAll(() => marker('afterAll'));
beforeEach(() => marker('beforeEach'));
afterEach(() => marker('afterEach'));
const testFn = (name, delay, fail) => {
return async () => {
marker(`START "${name}"`);
await setTimeout(delay);
if (fail) {
throw new Error(`${name} failed`);
}
expect(name).toBe(name);
expect.assertions(1);
marker(`END: "${name}"`);
};
};
it.concurrent('one', testFn('one', 85));
it.concurrent('two', testFn('two', 100, true));
describe('level 1', () => {
beforeEach(() => marker('beforeEach level 1'));
afterEach(() => marker('afterEach level 1'));
it.concurrent('three', testFn('three', 70));
it.concurrent.only('four', testFn('four', 120));
describe('level 2', () => {
beforeEach(() => marker('beforeEach level 2'));
afterEach(() => marker('afterEach level 2'));
it.concurrent('five', testFn('five', 160, true));
it.concurrent.only('six', testFn('six', 100));
});
it.concurrent('seven', testFn('seven', 100));
it.concurrent('eight', testFn('eight', 120));
});
it.concurrent.only('nine', testFn('nine', 20));
it.concurrent('ten', testFn('ten', 50));

View File

@ -0,0 +1,63 @@
/**
* 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';
const {setTimeout} = require('timers/promises');
let delta = Date.now();
const includeDelta = false;
const marker = s => {
console.log(s, includeDelta ? `+${Date.now() - delta}ms` : '');
delta = Date.now();
};
beforeAll(() => marker('beforeAll'));
afterAll(() => marker('afterAll'));
beforeEach(() => marker('beforeEach'));
afterEach(() => marker('afterEach'));
const testFn = (name, delay, fail) => {
return async () => {
marker(`START "${name}"`);
await setTimeout(delay);
if (fail) {
throw new Error(`${name} failed`);
}
expect(name).toBe(name);
expect.assertions(1);
marker(`END: "${name}"`);
};
};
it.concurrent('one', testFn('one', 85));
it.concurrent('two', testFn('two', 100));
describe('level 1', () => {
beforeEach(() => marker('beforeEach level 1'));
afterEach(() => marker('afterEach level 1'));
it.concurrent.skip('skipped three', testFn('three', 70));
it.concurrent('four', testFn('four', 120));
describe('level 2', () => {
beforeEach(() => marker('beforeEach level 2'));
afterEach(() => marker('afterEach level 2'));
it.concurrent.skip('five (skipped)', testFn('five', 160));
it.concurrent.skip('six (skipped)', testFn('six', 100));
});
it.concurrent('seven', testFn('seven', 100));
it.concurrent('eight', testFn('eight', 120));
});
it.concurrent.skip('nine (skipped)', testFn('nine', 20));
it.concurrent('ten', testFn('ten', 50));

View File

@ -0,0 +1,63 @@
/**
* 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';
const {setTimeout} = require('timers/promises');
let delta = Date.now();
const includeDelta = false;
const marker = s => {
console.log(s, includeDelta ? `+${Date.now() - delta}ms` : '');
delta = Date.now();
};
beforeAll(() => marker('beforeAll'));
afterAll(() => marker('afterAll'));
beforeEach(() => marker('beforeEach'));
afterEach(() => marker('afterEach'));
const testFn = (name, delay, fail) => {
return async () => {
marker(`START "${name}"`);
await setTimeout(delay);
if (fail) {
throw new Error(`${name} failed`);
}
expect(name).toBe(name);
expect.assertions(1);
marker(`END: "${name}"`);
};
};
it.concurrent('one', testFn('one', 85));
it.concurrent('two', testFn('two', 100));
describe('level 1', () => {
beforeEach(() => marker('beforeEach level 1'));
afterEach(() => marker('afterEach level 1'));
it.concurrent('three', testFn('three', 70));
it.concurrent('four', testFn('four', 120));
describe('level 2', () => {
beforeEach(() => marker('beforeEach level 2'));
afterEach(() => marker('afterEach level 2'));
it.concurrent('five', testFn('five', 160));
it.concurrent('six', testFn('six', 100));
});
it.concurrent('seven', testFn('seven', 100));
it.concurrent('eight', testFn('eight', 120));
});
it.concurrent('nine', testFn('nine', 20));
it.concurrent('ten', testFn('ten', 50));

View File

@ -6,24 +6,32 @@
*/
describe('block with concurrent', () => {
it('failing test', () => {
it('test should fail', () => {
expect(10).toBe(101);
});
it.concurrent.failing('failing passes = fails', () => {
it.concurrent.failing('.failing() should fail', () => {
expect(10).toBe(10);
});
it.concurrent.failing('.failing() should pass', () => {
expect(10).toBe(101);
});
test.concurrent.failing.each([
{a: 1, b: 1, expected: 2},
{a: 1, b: 2, expected: 3},
{a: 2, b: 1, expected: 3},
])('.add($a, $b)', ({a, b, expected}) => {
])('.add($a, $b) .failing.each() should fail', ({a, b, expected}) => {
expect(a + b).toBe(expected);
});
it.concurrent.failing('failing fails = passes', () => {
expect(10).toBe(101);
test.concurrent.failing.each([
{a: 1, b: 1, expected: 2},
{a: 1, b: 2, expected: 3},
{a: 2, b: 1, expected: 3},
])('.add($a, $b) .failing.each() should pass', ({a, b, expected}) => {
expect(a + b).toBe(expected + 10);
});
it.concurrent.skip.failing('skipped failing fails', () => {

View File

@ -10,20 +10,36 @@ describe('block with concurrent', () => {
expect(10).toBe(101);
});
it.concurrent.only.failing('failing passes = fails', () => {
it.concurrent.only.failing('.only.failing() should fail', () => {
expect(10).toBe(10);
});
it.concurrent.only.failing('.only.failing() should pass', () => {
expect(10).toBe(101);
});
test.concurrent.only.failing.each([
{a: 1, b: 1, expected: 2},
{a: 1, b: 2, expected: 3},
{a: 2, b: 1, expected: 3},
])('.add($a, $b)', ({a, b, expected}) => {
])('.add($a, $b) .only.failing.each() should fail', ({a, b, expected}) => {
expect(a + b).toBe(expected);
});
it.concurrent.only.failing('failing fails = passes', () => {
expect(10).toBe(101);
test.concurrent.only.failing.each([
{a: 1, b: 1, expected: 2},
{a: 1, b: 2, expected: 3},
{a: 2, b: 1, expected: 3},
])('.add($a, $b) .only.failing.each() should pass', ({a, b, expected}) => {
expect(a + b).toBe(expected + 10);
});
test.concurrent.failing.each([
{a: 1, b: 1, expected: 2},
{a: 1, b: 2, expected: 3},
{a: 2, b: 1, expected: 3},
])('.add($a, $b) skipped each', ({a, b, expected}) => {
expect(a + b).toBe(expected + 10);
});
it.concurrent.failing('skipped failing fails', () => {

View File

@ -0,0 +1,21 @@
/**
* 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);
beforeAll(() => {
throw new Error('Failure in beforeAll');
});
it.concurrent('should not be retried because hook failure occurred', () => {
throw new Error('should not be invoked');
});
it.concurrent('should fail due to the beforeAll', () => {
expect(10).toBe(10);
});

View File

@ -0,0 +1,11 @@
/**
* 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';
it('retryTimes not set', () => {
expect(true).toBeFalsy();
});

View File

@ -0,0 +1,29 @@
/**
* 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';
const fs = require('fs');
const path = require('path');
const countPath = path.join(__dirname, '.tries');
beforeAll(() => {
fs.writeFileSync(countPath, '0', 'utf8');
});
jest.retryTimes(3);
it.concurrent('retries', () => {
const tries = Number.parseInt(fs.readFileSync(countPath, 'utf8'), 10);
fs.writeFileSync(countPath, `${tries + 1}`, 'utf8');
expect(tries).toBe(3);
});
afterAll(() => {
// cleanup
fs.unlinkSync(countPath);
});

View File

@ -0,0 +1,17 @@
/**
* 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);
it.concurrent('retryTimes set', () => {
expect(true).toBeFalsy();
});
it.concurrent('truthy test', () => {
expect(true).toBeTruthy();
});

View File

@ -0,0 +1,34 @@
/**
* 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 i1 = 0;
it.concurrent('retryable test 1', () => {
i1++;
if (i1 === 3) {
console.log('SECOND TRUTHY TEST');
expect(true).toBeTruthy();
} else {
expect(true).toBeFalsy();
}
});
let i2 = 0;
it.concurrent('retryable test 2', () => {
i2++;
if (i2 === 3) {
console.log('THIRD TRUTHY TEST');
expect(true).toBeTruthy();
} else {
expect(true).toBeFalsy();
}
});
it.concurrent('truthy test', () => {
console.log('FIRST TRUTHY TEST');
expect(true).toBeTruthy();
});

View File

@ -1,5 +1,77 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`concurrent 1`] = `
"start_describe_definition: describe
add_hook: beforeEach
add_hook: afterEach
add_test: one
add_test: two
add_test: three
finish_describe_definition: describe
run_start
run_describe_start: ROOT_DESCRIBE_BLOCK
run_describe_start: describe
test_start: one
test_start: two
test_start: three
test_started: one
test_started: two
test_started: three
test_fn_start: one
test_fn_start: two
test_fn_start: three
hello one
hello two
hello three
test_fn_failure: one
test_fn_success: two
test_fn_success: three
test_done: one
test_done: two
test_done: three
run_describe_finish: describe
run_describe_finish: ROOT_DESCRIBE_BLOCK
run_finish
unhandledErrors: 0"
`;
exports[`concurrent.each 1`] = `
"start_describe_definition: describe
add_hook: beforeEach
add_hook: afterEach
add_test: one
add_test: two
add_test: three
finish_describe_definition: describe
run_start
run_describe_start: ROOT_DESCRIBE_BLOCK
run_describe_start: describe
test_start: one
test_start: two
test_start: three
test_started: one
test_started: two
test_started: three
test_fn_start: one
test_fn_start: two
test_fn_start: three
hello one
hello two
hello three
test_fn_success: one
test_fn_success: two
test_fn_success: three
test_done: one
test_done: two
test_done: three
run_describe_finish: describe
run_describe_finish: ROOT_DESCRIBE_BLOCK
run_finish
unhandledErrors: 0"
`;
exports[`failures 1`] = `
"start_describe_definition: describe
add_hook: beforeEach

View File

@ -42,3 +42,44 @@ test('failures', () => {
expect(stdout).toMatchSnapshot();
});
test('concurrent', () => {
const {stdout} = runTest(`
describe('describe', () => {
beforeEach(() => {});
afterEach(() => { throw new Error('banana')});
test.concurrent('one', () => {
console.log('hello one');
throw new Error('kentucky')
});
test.concurrent('two', () => {
console.log('hello two');
});
test.concurrent('three', async () => {
console.log('hello three');
await Promise.resolve();
});
})
`);
expect(stdout).toMatchSnapshot();
});
test('concurrent.each', () => {
const {stdout} = runTest(`
describe('describe', () => {
beforeEach(() => {});
afterEach(() => { throw new Error('banana')});
test.concurrent.each([
['one'],
['two'],
['three'],
])('%s', async (name) => {
console.log('hello %s', name);
await Promise.resolve();
});
})
`);
expect(stdout).toMatchSnapshot();
});

View File

@ -30,6 +30,7 @@ const {setTimeout} = globalThis;
type ConcurrentTestEntry = Omit<Circus.TestEntry, 'fn'> & {
fn: Circus.ConcurrentTestFn;
done: Promise<void>;
};
const run = async (): Promise<Circus.RunResult> => {
@ -63,7 +64,7 @@ const _runTestsForDescribeBlock = async (
if (isRootBlock) {
const concurrentTests = collectConcurrentTests(describeBlock);
if (concurrentTests.length > 0) {
startTestsConcurrently(concurrentTests);
startTestsConcurrently(concurrentTests, isSkipped);
}
}
@ -81,7 +82,7 @@ const _runTestsForDescribeBlock = async (
const retryImmediately: boolean =
((globalThis as Global.Global)[RETRY_IMMEDIATELY] as any) || false;
const deferredRetryTests = [];
const deferredRetryTests: Array<Circus.TestEntry> = [];
if (rng) {
describeBlock.children = shuffleArray(describeBlock.children, rng);
@ -103,6 +104,27 @@ const _runTestsForDescribeBlock = async (
}
};
const handleRetry = async (
test: Circus.TestEntry,
hasErrorsBeforeTestRun: boolean,
hasRetryTimes: boolean,
) => {
// no retry if the test passed or had errors before the test ran
if (test.errors.length === 0 || hasErrorsBeforeTestRun || !hasRetryTimes) {
return;
}
if (!retryImmediately) {
deferredRetryTests.push(test);
return;
}
// If immediate retry is set, we retry the test immediately after the first run
await rerunTest(test);
};
const concurrentTests = [];
for (const child of describeBlock.children) {
switch (child.type) {
case 'describeBlock': {
@ -112,29 +134,24 @@ 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 &&
hasRetryTimes
) {
await rerunTest(child);
}
if (
hasErrorsBeforeTestRun === false &&
hasRetryTimes &&
!retryImmediately
) {
deferredRetryTests.push(child);
if (child.concurrent) {
concurrentTests.push(
(child as ConcurrentTestEntry).done.then(() =>
handleRetry(child, hasErrorsBeforeTestRun, hasRetryTimes),
),
);
} else {
await _runTest(child, isSkipped);
await handleRetry(child, hasErrorsBeforeTestRun, hasRetryTimes);
}
break;
}
}
}
// wait for concurrent tests to finish
await Promise.all(concurrentTests);
// Re-run failed tests n-times if configured
for (const test of deferredRetryTests) {
await rerunTest(test);
@ -155,23 +172,23 @@ function collectConcurrentTests(
if (describeBlock.mode === 'skip') {
return [];
}
const {hasFocusedTests, testNamePattern} = getState();
return describeBlock.children.flatMap(child => {
switch (child.type) {
case 'describeBlock':
return collectConcurrentTests(child);
case 'test':
const skip =
!child.concurrent ||
child.mode === 'skip' ||
(hasFocusedTests && child.mode !== 'only') ||
(testNamePattern && !testNamePattern.test(getTestID(child)));
return skip ? [] : [child as ConcurrentTestEntry];
if (child.concurrent) {
return [child as ConcurrentTestEntry];
}
return [];
}
});
}
function startTestsConcurrently(concurrentTests: Array<ConcurrentTestEntry>) {
function startTestsConcurrently(
concurrentTests: Array<ConcurrentTestEntry>,
parentSkipped: boolean,
) {
const mutex = pLimit(getState().maxConcurrency);
const testNameStorage = new AsyncLocalStorage<string>();
jestExpect.setState({
@ -179,13 +196,16 @@ function startTestsConcurrently(concurrentTests: Array<ConcurrentTestEntry>) {
});
for (const test of concurrentTests) {
try {
const testFn = test.fn;
const promise = mutex(() => testNameStorage.run(getTestID(test), testFn));
const promise = mutex(() =>
testNameStorage.run(getTestID(test), () =>
_runTest(test, parentSkipped),
),
);
// Avoid triggering the uncaught promise rejection handler in case the
// test fails before being awaited on.
// eslint-disable-next-line @typescript-eslint/no-empty-function
promise.catch(() => {});
test.fn = () => promise;
test.done = promise;
} catch (error) {
test.fn = () => {
throw error;