diff --git a/CHANGELOG.md b/CHANGELOG.md index 944c19d56f65..0fdda8c60f77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/Configuration.md b/docs/Configuration.md index 894e840f4b83..0a9225714561 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -516,6 +516,18 @@ Default: `'prettier'` Sets the path to the [`prettier`](https://prettier.io/) node module used to update inline snapshots. +### `prioritySequence` [array] + +Default: `[]` + +Use `prioritySequence` configuration can run certain tests first with giving sequence. + +```json +{ + "prioritySequence": ["/b.test.js", "/a.test.js"] +} +``` + ### `projects` [array] Default: `undefined` diff --git a/e2e/__tests__/__snapshots__/showConfig.test.ts.snap b/e2e/__tests__/__snapshots__/showConfig.test.ts.snap index 8b27dc1ba115..b904515901ff 100644 --- a/e2e/__tests__/__snapshots__/showConfig.test.ts.snap +++ b/e2e/__tests__/__snapshots__/showConfig.test.ts.snap @@ -110,6 +110,7 @@ exports[`--showConfig outputs config info and exits 1`] = ` "notify": false, "notifyMode": "failure-change", "passWithNoTests": false, + "prioritySequence": [], "projects": null, "rootDir": "<>", "runTestsByPath": false, diff --git a/e2e/__tests__/prioritySequence.test.ts b/e2e/__tests__/prioritySequence.test.ts new file mode 100644 index 000000000000..68719478cadd --- /dev/null +++ b/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']), + ]); +}); diff --git a/e2e/priority-sequence/a.test.js b/e2e/priority-sequence/a.test.js new file mode 100644 index 000000000000..26bc094ab2f9 --- /dev/null +++ b/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', () => {}); +}); diff --git a/e2e/priority-sequence/b.test.js b/e2e/priority-sequence/b.test.js new file mode 100644 index 000000000000..a963f9e55c63 --- /dev/null +++ b/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', () => {}); +}); diff --git a/e2e/priority-sequence/c.test.js b/e2e/priority-sequence/c.test.js new file mode 100644 index 000000000000..dcfc1029ef22 --- /dev/null +++ b/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', () => {}); +}); diff --git a/e2e/priority-sequence/d.test.js b/e2e/priority-sequence/d.test.js new file mode 100644 index 000000000000..a1b2d5c00309 --- /dev/null +++ b/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', () => {}); +}); diff --git a/e2e/priority-sequence/e.test.js b/e2e/priority-sequence/e.test.js new file mode 100644 index 000000000000..a0aa259a827a --- /dev/null +++ b/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', () => {}); +}); diff --git a/e2e/priority-sequence/package.json b/e2e/priority-sequence/package.json new file mode 100644 index 000000000000..cc5dcafc5d5a --- /dev/null +++ b/e2e/priority-sequence/package.json @@ -0,0 +1,5 @@ +{ + "jest": { + "prioritySequence": ["/d.test.js", "/b.test.js", "/c.test.js"] + } +} diff --git a/jest.config.js b/jest.config.js index 603dd266b03a..966883a5455d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -23,6 +23,7 @@ module.exports = { 'website/.*', 'e2e/runtime-internal-module-registry/__mocks__', ], + prioritySequence: [], projects: ['', '/examples/*/'], setupFilesAfterEnv: ['/testSetupFile.js'], snapshotSerializers: [ diff --git a/packages/jest-config/src/Defaults.ts b/packages/jest-config/src/Defaults.ts index 10fed37c6da5..369194997e0c 100644 --- a/packages/jest-config/src/Defaults.ts +++ b/packages/jest-config/src/Defaults.ts @@ -50,6 +50,7 @@ const defaultOptions: Config.DefaultOptions = { notifyMode: 'failure-change', preset: null, prettierPath: 'prettier', + prioritySequence: [], projects: null, resetMocks: false, resetModules: false, diff --git a/packages/jest-config/src/ValidConfig.ts b/packages/jest-config/src/ValidConfig.ts index fb25df1599bc..df0bca0b56c2 100644 --- a/packages/jest-config/src/ValidConfig.ts +++ b/packages/jest-config/src/ValidConfig.ts @@ -78,6 +78,7 @@ const initialOptions: Config.InitialOptions = { onlyChanged: false, preset: 'react-native', prettierPath: '/node_modules/prettier', + prioritySequence: [], projects: ['project-a', 'project-b/'], reporters: [ 'default', diff --git a/packages/jest-config/src/index.ts b/packages/jest-config/src/index.ts index 45bebcafcdac..09db40085cba 100644 --- a/packages/jest-config/src/index.ts +++ b/packages/jest-config/src/index.ts @@ -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, diff --git a/packages/jest-config/src/normalize.ts b/packages/jest-config/src/normalize.ts index e43bd523af8a..4e1830b1f276 100644 --- a/packages/jest-config/src/normalize.ts +++ b/packages/jest-config/src/normalize.ts @@ -519,6 +519,7 @@ export default function normalize( case 'collectCoverageOnlyFrom': value = normalizeCollectCoverageOnlyFrom(oldOptions, key); break; + case 'prioritySequence': case 'setupFiles': case 'setupFilesAfterEnv': case 'snapshotSerializers': diff --git a/packages/jest-core/src/TestScheduler.ts b/packages/jest-core/src/TestScheduler.ts index 62c9ae2ceedd..75ca53dea8c0 100644 --- a/packages/jest-core/src/TestScheduler.ts +++ b/packages/jest-core/src/TestScheduler.ts @@ -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) => { diff --git a/packages/jest-core/src/TestSequencer.ts b/packages/jest-core/src/TestSequencer.ts index fc1ef479c517..6f3f708f7212 100644 --- a/packages/jest-core/src/TestSequencer.ts +++ b/packages/jest-core/src/TestSequencer.ts @@ -80,7 +80,7 @@ export default class TestSequencer { * from the file other than its size. * */ - sort(tests: Array): Array { + sort(tests: Array, prioritySequence: Array = []): Array { const stats: {[path: string]: number} = {}; const fileSize = ({path, context: {hasteFS}}: Test) => stats[path] || (stats[path] = hasteFS.getSize(path) || 0); @@ -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 diff --git a/packages/jest-core/src/__tests__/testSchedulerHelper.test.js b/packages/jest-core/src/__tests__/testSchedulerHelper.test.js index 3fc2043f3eb4..3380b82e2ee1 100644 --- a/packages/jest-core/src/__tests__/testSchedulerHelper.test.js +++ b/packages/jest-core/src/__tests__/testSchedulerHelper.test.js @@ -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); }, ); diff --git a/packages/jest-core/src/__tests__/test_sequencer.test.js b/packages/jest-core/src/__tests__/test_sequencer.test.js index 2e0c8d00f0ea..7793f8c187ea 100644 --- a/packages/jest-core/src/__tests__/test_sequencer.test.js +++ b/packages/jest-core/src/__tests__/test_sequencer.test.js @@ -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.'); diff --git a/packages/jest-core/src/runJest.ts b/packages/jest-core/src/runJest.ts index fad2435d8ee0..f037a8fd06a9 100644 --- a/packages/jest-core/src/runJest.ts +++ b/packages/jest-core/src/runJest.ts @@ -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))); diff --git a/packages/jest-core/src/testSchedulerHelper.ts b/packages/jest-core/src/testSchedulerHelper.ts index b14e677cd20b..e1367f3a07ec 100644 --- a/packages/jest-core/src/testSchedulerHelper.ts +++ b/packages/jest-core/src/testSchedulerHelper.ts @@ -14,6 +14,7 @@ export function shouldRunInBand( isWatchMode: boolean, maxWorkers: number, timings: Array, + prioritySequence: Array, ) { /** * Run in band if we only have one test or one worker available, unless we @@ -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; diff --git a/packages/jest-types/src/Config.ts b/packages/jest-types/src/Config.ts index 76e4d06ef343..8df4045aa276 100644 --- a/packages/jest-types/src/Config.ts +++ b/packages/jest-types/src/Config.ts @@ -67,6 +67,7 @@ export type DefaultOptions = { notifyMode: string; preset: string | null | undefined; prettierPath: string | null | undefined; + prioritySequence: Array | null | undefined; projects: Array | null | undefined; resetMocks: boolean; resetModules: boolean; @@ -166,6 +167,7 @@ export type InitialOptions = { preprocessorIgnorePatterns?: Array; preset?: string | null | undefined; prettierPath?: string | null | undefined; + prioritySequence?: Array; projects?: Array; replname?: string | null | undefined; resetMocks?: boolean; @@ -281,6 +283,7 @@ export type GlobalConfig = { onlyChanged: boolean; onlyFailures: boolean; passWithNoTests: boolean; + prioritySequence: Array; projects: Array; replname: string | null | undefined; reporters: Array;