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

Add prioritySequence option #8209

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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 @@ -9,6 +9,9 @@
- `[jest-runner]` Support default exports for test environments ([#8163](https://github.com/facebook/jest/pull/8163))
- `[pretty-format]` Support React.Suspense ([#8180](https://github.com/facebook/jest/pull/8180))
- `[jest-snapshot]` Indent inline snapshots ([#8198](https://github.com/facebook/jest/pull/8198))
- `[jest-config]` Add `prioritySequence` option ([#8209](https://github.com/facebook/jest/pull/8209))
- `[jest-core]` Add `prioritySequence` option ([#8209](https://github.com/facebook/jest/pull/8209))
- `[jest-types]` Add `prioritySequence` option ([#8209](https://github.com/facebook/jest/pull/8209))

### Fixes

Expand Down
12 changes: 12 additions & 0 deletions docs/Configuration.md
Expand Up @@ -516,6 +516,18 @@ Default: `'prettier'`

Sets the path to the [`prettier`](https://prettier.io/) node module used to update inline snapshots.

### `prioritySequence` [array<string>]

Default: `[]`

Use `prioritySequence` configuration can run certain tests first with giving sequence.

```json
{
"prioritySequence": ["<rootDir>/b.test.js", "<rootDir>/a.test.js"]
}
```

### `projects` [array<string | ProjectConfig>]

Default: `undefined`
Expand Down
1 change: 1 addition & 0 deletions e2e/__tests__/__snapshots__/showConfig.test.ts.snap
Expand Up @@ -110,6 +110,7 @@ exports[`--showConfig outputs config info and exits 1`] = `
"notify": false,
"notifyMode": "failure-change",
"passWithNoTests": false,
"prioritySequence": [],
"projects": null,
"rootDir": "<<REPLACED_ROOT_DIR>>",
"runTestsByPath": false,
Expand Down
42 changes: 42 additions & 0 deletions e2e/__tests__/prioritySequence.test.ts
@@ -0,0 +1,42 @@
/**
* 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.
*/

import path from 'path';
import runJest from '../runJest';
import {extractSummary} from '../Utils';
const dir = path.resolve(__dirname, '../priority-sequence');

expect.extend({
toBeIn(received, arr) {
const isIn = arr.includes(received);
if (isIn)
return {
message: `expect ${received} not to be in [${arr.join(', ')}]`,
pass: true,
};
else
return {
message: `expect ${received} to be in [${arr.join(', ')}]`,
pass: false,
};
},
});

test('run prioritySequence first', () => {
const result = runJest(dir);
expect(result.status).toBe(0);
const sequence = extractSummary(result.stderr)
.rest.replace(/PASS /g, '')
.split('\n');
expect(sequence).toEqual([
'./d.test.js',
'./b.test.js',
'./c.test.js',
expect.toBeIn(['./a.test.js', './e.test.js']),
expect.toBeIn(['./a.test.js', './e.test.js']),
]);
});
10 changes: 10 additions & 0 deletions e2e/priority-sequence/a.test.js
@@ -0,0 +1,10 @@
/**
* 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.
*/

describe('a.test.js', () => {
test('test', () => {});
});
10 changes: 10 additions & 0 deletions e2e/priority-sequence/b.test.js
@@ -0,0 +1,10 @@
/**
* 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.
*/

describe('b.test.js', () => {
test('test', () => {});
});
10 changes: 10 additions & 0 deletions e2e/priority-sequence/c.test.js
@@ -0,0 +1,10 @@
/**
* 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.
*/

describe('c.test.js', () => {
test('test', () => {});
});
10 changes: 10 additions & 0 deletions e2e/priority-sequence/d.test.js
@@ -0,0 +1,10 @@
/**
* 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.
*/

describe('d.test.js', () => {
test('test', () => {});
});
10 changes: 10 additions & 0 deletions e2e/priority-sequence/e.test.js
@@ -0,0 +1,10 @@
/**
* 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.
*/

describe('e.test.js', () => {
test('test', () => {});
});
5 changes: 5 additions & 0 deletions e2e/priority-sequence/package.json
@@ -0,0 +1,5 @@
{
"jest": {
"prioritySequence": ["<rootDir>/d.test.js", "<rootDir>/b.test.js", "<rootDir>/c.test.js"]
}
}
1 change: 1 addition & 0 deletions jest.config.js
Expand Up @@ -23,6 +23,7 @@ module.exports = {
'website/.*',
'e2e/runtime-internal-module-registry/__mocks__',
],
prioritySequence: [],
projects: ['<rootDir>', '<rootDir>/examples/*/'],
setupFilesAfterEnv: ['<rootDir>/testSetupFile.js'],
snapshotSerializers: [
Expand Down
1 change: 1 addition & 0 deletions packages/jest-config/src/Defaults.ts
Expand Up @@ -50,6 +50,7 @@ const defaultOptions: Config.DefaultOptions = {
notifyMode: 'failure-change',
preset: null,
prettierPath: 'prettier',
prioritySequence: [],
projects: null,
resetMocks: false,
resetModules: false,
Expand Down
1 change: 1 addition & 0 deletions packages/jest-config/src/ValidConfig.ts
Expand Up @@ -78,6 +78,7 @@ const initialOptions: Config.InitialOptions = {
onlyChanged: false,
preset: 'react-native',
prettierPath: '<rootDir>/node_modules/prettier',
prioritySequence: [],
projects: ['project-a', 'project-b/'],
reporters: [
'default',
Expand Down
1 change: 1 addition & 0 deletions packages/jest-config/src/index.ts
Expand Up @@ -137,6 +137,7 @@ const groupOptions = (
onlyFailures: options.onlyFailures,
outputFile: options.outputFile,
passWithNoTests: options.passWithNoTests,
prioritySequence: options.prioritySequence,
projects: options.projects,
replname: options.replname,
reporters: options.reporters,
Expand Down
1 change: 1 addition & 0 deletions packages/jest-config/src/normalize.ts
Expand Up @@ -519,6 +519,7 @@ export default function normalize(
case 'collectCoverageOnlyFrom':
value = normalizeCollectCoverageOnlyFrom(oldOptions, key);
break;
case 'prioritySequence':
case 'setupFiles':
case 'setupFilesAfterEnv':
case 'snapshotSerializers':
Expand Down
1 change: 1 addition & 0 deletions packages/jest-core/src/TestScheduler.ts
Expand Up @@ -91,6 +91,7 @@ export default class TestScheduler {
this._globalConfig.watch || this._globalConfig.watchAll,
this._globalConfig.maxWorkers,
timings,
this._globalConfig.prioritySequence,
);

const onResult = async (test: Test, testResult: TestResult) => {
Expand Down
10 changes: 8 additions & 2 deletions packages/jest-core/src/TestSequencer.ts
Expand Up @@ -80,7 +80,7 @@ export default class TestSequencer {
* from the file other than its size.
*
*/
sort(tests: Array<Test>): Array<Test> {
sort(tests: Array<Test>, prioritySequence: Array<string> = []): Array<Test> {
const stats: {[path: string]: number} = {};
const fileSize = ({path, context: {hasteFS}}: Test) =>
stats[path] || (stats[path] = hasteFS.getSize(path) || 0);
Expand All @@ -96,7 +96,13 @@ export default class TestSequencer {
const failedA = hasFailed(cacheA, testA);
const failedB = hasFailed(cacheB, testB);
const hasTimeA = testA.duration != null;
if (failedA !== failedB) {
const priorityA = prioritySequence.indexOf(testA.path);
const priorityB = prioritySequence.indexOf(testB.path);
if (priorityA !== priorityB) {
if (priorityA === -1) return 1;
if (priorityB === -1) return -1;
return priorityA < priorityB ? -1 : 1;
} else if (failedA !== failedB) {
return failedA ? -1 : 1;
} else if (hasTimeA != (testB.duration != null)) {
// If only one of two tests has timing information, run it last
Expand Down
33 changes: 17 additions & 16 deletions packages/jest-core/src/__tests__/testSchedulerHelper.test.js
Expand Up @@ -23,23 +23,24 @@ const getTestMock = () => ({
const getTestsMock = () => [getTestMock(), getTestMock()];

test.each`
tests | watch | maxWorkers | timings | expectedResult
${[getTestMock()]} | ${true} | ${undefined} | ${[500, 500]} | ${true}
${getTestsMock()} | ${true} | ${1} | ${[2000, 500]} | ${true}
${getTestsMock()} | ${true} | ${2} | ${[2000, 500]} | ${false}
${[getTestMock()]} | ${true} | ${undefined} | ${[2000, 500]} | ${false}
${getTestMock()} | ${true} | ${undefined} | ${[500, 500]} | ${false}
${getTestsMock()} | ${false} | ${1} | ${[2000, 500]} | ${true}
${getTestMock()} | ${false} | ${2} | ${[2000, 500]} | ${false}
${[getTestMock()]} | ${false} | ${undefined} | ${[2000]} | ${true}
${getTestsMock()} | ${false} | ${undefined} | ${[500, 500]} | ${true}
${new Array(45)} | ${false} | ${undefined} | ${[500]} | ${false}
${getTestsMock()} | ${false} | ${undefined} | ${[2000, 500]} | ${false}
tests | watch | maxWorkers | timings | prioritySequence | expectedResult
${getTestMock()} | ${false} | ${undefined} | ${[500, 500]} | ${['/a.test.js']} | ${true}
${[getTestMock()]} | ${true} | ${undefined} | ${[500, 500]} | ${[]} | ${true}
${getTestsMock()} | ${true} | ${1} | ${[2000, 500]} | ${[]} | ${true}
${getTestsMock()} | ${true} | ${2} | ${[2000, 500]} | ${[]} | ${false}
${[getTestMock()]} | ${true} | ${undefined} | ${[2000, 500]} | ${[]} | ${false}
${getTestMock()} | ${true} | ${undefined} | ${[500, 500]} | ${[]} | ${false}
${getTestsMock()} | ${false} | ${1} | ${[2000, 500]} | ${[]} | ${true}
${getTestMock()} | ${false} | ${2} | ${[2000, 500]} | ${[]} | ${false}
${[getTestMock()]} | ${false} | ${undefined} | ${[2000]} | ${[]} | ${true}
${getTestsMock()} | ${false} | ${undefined} | ${[500, 500]} | ${[]} | ${true}
${new Array(45)} | ${false} | ${undefined} | ${[500]} | ${[]} | ${false}
${getTestsMock()} | ${false} | ${undefined} | ${[2000, 500]} | ${[]} | ${false}
`(
'shouldRunInBand() - should return $expectedResult for runInBand mode',
({tests, watch, maxWorkers, timings, expectedResult}) => {
expect(shouldRunInBand(tests, watch, maxWorkers, timings)).toBe(
expectedResult,
);
({tests, watch, maxWorkers, timings, prioritySequence, expectedResult}) => {
expect(
shouldRunInBand(tests, watch, maxWorkers, timings, prioritySequence),
).toBe(expectedResult);
},
);
30 changes: 30 additions & 0 deletions packages/jest-core/src/__tests__/test_sequencer.test.js
Expand Up @@ -124,6 +124,36 @@ test('sorts based on failures, timing information and file size', () => {
]);
});

test('sorts based on prioritySequence failures, timing information and file size', () => {
fs.readFileSync = jest.fn(() =>
JSON.stringify({
'/test-a.js': [SUCCESS, 5],
'/test-ab.js': [FAIL, 1],
'/test-c.js': [FAIL],
'/test-d.js': [SUCCESS, 2],
'/test-efg.js': [FAIL],
}),
);
expect(
sequencer.sort(
toTests([
'/test-a.js',
'/test-ab.js',
'/test-c.js',
'/test-d.js',
'/test-efg.js',
]),
['/test-d.js', '/test-a.js'],
),
).toEqual([
{context, duration: 2, path: '/test-d.js'},
{context, duration: 5, path: '/test-a.js'},
{context, duration: undefined, path: '/test-efg.js'},
{context, duration: undefined, path: '/test-c.js'},
{context, duration: 1, path: '/test-ab.js'},
]);
});

test('writes the cache based on results without existing cache', () => {
fs.readFileSync = jest.fn(() => {
throw new Error('File does not exist.');
Expand Down
2 changes: 1 addition & 1 deletion packages/jest-core/src/runJest.ts
Expand Up @@ -179,7 +179,7 @@ export default (async function runJest({
}),
);

allTests = sequencer.sort(allTests);
allTests = sequencer.sort(allTests, globalConfig.prioritySequence);

if (globalConfig.listTests) {
const testsPaths = Array.from(new Set(allTests.map(test => test.path)));
Expand Down
5 changes: 5 additions & 0 deletions packages/jest-core/src/testSchedulerHelper.ts
Expand Up @@ -14,6 +14,7 @@ export function shouldRunInBand(
isWatchMode: boolean,
maxWorkers: number,
timings: Array<number>,
prioritySequence: Array<string>,
) {
/**
* Run in band if we only have one test or one worker available, unless we
Expand All @@ -26,6 +27,10 @@ export function shouldRunInBand(
* force running in band.
* https://github.com/facebook/jest/blob/700e0dadb85f5dc8ff5dac6c7e98956690049734/packages/jest-config/src/getMaxWorkers.js#L14-L17
*/
if (prioritySequence && prioritySequence.length > 0) {
return true;
}

const areFastTests = timings.every(timing => timing < SLOW_TEST_TIME);
const oneWorkerOrLess = maxWorkers <= 1;
const oneTestOrLess = tests.length <= 1;
Expand Down
3 changes: 3 additions & 0 deletions packages/jest-types/src/Config.ts
Expand Up @@ -67,6 +67,7 @@ export type DefaultOptions = {
notifyMode: string;
preset: string | null | undefined;
prettierPath: string | null | undefined;
prioritySequence: Array<string> | null | undefined;
projects: Array<string | ProjectConfig> | null | undefined;
resetMocks: boolean;
resetModules: boolean;
Expand Down Expand Up @@ -166,6 +167,7 @@ export type InitialOptions = {
preprocessorIgnorePatterns?: Array<Glob>;
preset?: string | null | undefined;
prettierPath?: string | null | undefined;
prioritySequence?: Array<string>;
projects?: Array<Glob>;
replname?: string | null | undefined;
resetMocks?: boolean;
Expand Down Expand Up @@ -281,6 +283,7 @@ export type GlobalConfig = {
onlyChanged: boolean;
onlyFailures: boolean;
passWithNoTests: boolean;
prioritySequence: Array<string>;
projects: Array<Glob>;
replname: string | null | undefined;
reporters: Array<string | ReporterConfig>;
Expand Down