From 6641e7359172d4b270a7ed861cab2ddb7cea7652 Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Thu, 3 Mar 2022 22:19:33 +1100 Subject: [PATCH] feat: implement --shard option #6270 --- docs/CLI.md | 11 +++++++++ packages/jest-cli/src/cli/args.ts | 27 +++++++++++++++++++++++ packages/jest-config/src/normalize.ts | 14 ++++++++++++ packages/jest-core/src/runJest.ts | 1 + packages/jest-test-sequencer/src/index.ts | 22 ++++++++++++++++++ packages/jest-types/src/Config.ts | 7 ++++++ 6 files changed, 82 insertions(+) diff --git a/docs/CLI.md b/docs/CLI.md index d465b78d34b3..85dc4244a2cc 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -338,6 +338,17 @@ Run only the tests of the specified projects. Jest uses the attribute `displayNa A list of paths to modules that run some code to configure or to set up the testing framework before each test. Beware that files imported by the setup scripts will not be mocked during testing. +### `--shard` + +Shard suite to execute in on multiple machines. For example, to split the suite into three shards, each running one third of the tests: + +``` +jest --shard=1/3 +jest --shard=2/3 +jest --shard=3/3 +``` + + ### `--showConfig` Print your Jest config and then exits. diff --git a/packages/jest-cli/src/cli/args.ts b/packages/jest-cli/src/cli/args.ts index 92c13247f646..975662762b50 100644 --- a/packages/jest-cli/src/cli/args.ts +++ b/packages/jest-cli/src/cli/args.ts @@ -87,6 +87,27 @@ export function check(argv: Config.Argv): true { ); } + if (argv.shard) { + const shardPair = argv?.shard + .split('/') + .map(parseInt) + .filter((shard: number) => typeof shard === 'number' && !Number.isNaN(shard)); + + if (shardPair.length !== 2) { + throw new Error(`The --shard option requires a string in the format of /.\n Example usage jest --shard=1/5`); + } + + const [shardIndex, shardCount] = shardPair; + + if (shardIndex === 0 || shardCount === 0) { + throw new Error(`The --shard option requires 1-based values, received 0 in the pair.\n Example usage jest --shard=1/5`); + } + + if (shardIndex > shardCount) { + throw new Error(`The --shard option / requires to be lower or equal than .\n Example usage jest --shard=1/5`); + } + } + return true; } @@ -521,6 +542,12 @@ export const options = { string: true, type: 'array', }, + shard: { + description: + 'Shard tests and execute only the selected shard, specify in ' + + 'the form "current/all". 1-based, for example "3/5"', + type: 'string' + }, showConfig: { description: 'Print your jest config and then exits.', type: 'boolean', diff --git a/packages/jest-config/src/normalize.ts b/packages/jest-config/src/normalize.ts index 36e64007f5da..607e3772a63e 100644 --- a/packages/jest-config/src/normalize.ts +++ b/packages/jest-config/src/normalize.ts @@ -1220,6 +1220,20 @@ export default async function normalize( newOptions.logHeapUsage = false; } + if (argv.shard) { + const [shardIndex, shardCount] = argv?.shard + .split('/') + .map(parseInt) + .filter( + (shard: number) => typeof shard === 'number' && !Number.isNaN(shard), + ); + + newOptions.shard = { + shardCount, + shardIndex, + }; + } + return { hasDeprecationWarnings, options: newOptions, diff --git a/packages/jest-core/src/runJest.ts b/packages/jest-core/src/runJest.ts index 6ef4122c1ab4..d087dc2c565f 100644 --- a/packages/jest-core/src/runJest.ts +++ b/packages/jest-core/src/runJest.ts @@ -192,6 +192,7 @@ export default async function runJest({ }), ); + allTests = sequencer.shard?.(allTests, globalConfig.shard) ?? allTests; allTests = await sequencer.sort(allTests); if (globalConfig.listTests) { diff --git a/packages/jest-test-sequencer/src/index.ts b/packages/jest-test-sequencer/src/index.ts index 32b0b98040e8..ba01f20a915a 100644 --- a/packages/jest-test-sequencer/src/index.ts +++ b/packages/jest-test-sequencer/src/index.ts @@ -17,6 +17,11 @@ type Cache = { [key: string]: [0 | 1, number]; }; +export type SortOptions = { + shardIndex: number; + shardCount: number; +}; + /** * The TestSequencer will ultimately decide which tests should run first. * It is responsible for storing and reading from a local cache @@ -65,6 +70,23 @@ export default class TestSequencer { return cache; } + shard( + tests: Array, + options: SortOptions = {shardCount: 1, shardIndex: 1}, + ): Array { + if (options?.shardCount > 1) { + const shardSize = Math.ceil(tests.length / options.shardCount); + const shardStart = shardSize * options.shardIndex; + const shardEnd = shardSize * (options.shardIndex + 1); + + return [...tests] + .sort((a, b) => a.path.localeCompare(b.path)) + .slice(shardStart, shardEnd); + } + + return tests; + } + /** * Sorting tests is very important because it has a great impact on the * user-perceived responsiveness and speed of the test run. diff --git a/packages/jest-types/src/Config.ts b/packages/jest-types/src/Config.ts index cf0f6ae07187..80243eb45512 100644 --- a/packages/jest-types/src/Config.ts +++ b/packages/jest-types/src/Config.ts @@ -279,6 +279,11 @@ type CoverageThreshold = { global: CoverageThresholdValue; }; +type ShardConfig = { + shardIndex: number; + shardCount: number; +}; + export type GlobalConfig = { bail: number; changedSince?: string; @@ -322,6 +327,7 @@ export type GlobalConfig = { reporters?: Array; runTestsByPath: boolean; rootDir: string; + shard?: ShardConfig; silent?: boolean; skipFilter: boolean; snapshotFormat: SnapshotFormat; @@ -464,6 +470,7 @@ export type Argv = Arguments< selectProjects: Array; setupFiles: Array; setupFilesAfterEnv: Array; + shard: string; showConfig: boolean; silent: boolean; snapshotSerializers: Array;