From 3052d3b45618f3d126e30751d52b81c165f60e91 Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Thu, 3 Mar 2022 22:19:33 +1100 Subject: [PATCH 01/53] feat: implement --shard option #6270 --- .github/workflows/nodejs.yml | 78 +++++++++++++-- docs/CLI.md | 10 ++ .../jest-cli/src/__tests__/cli/args.test.ts | 98 +++++++++++++------ packages/jest-cli/src/cli/args.ts | 34 +++++++ .../src/__tests__/normalize.test.ts | 10 ++ packages/jest-config/src/index.ts | 1 + packages/jest-config/src/normalize.ts | 13 +++ packages/jest-core/src/runJest.ts | 1 + .../src/__tests__/test_sequencer.test.js | 71 ++++++++++++++ packages/jest-test-sequencer/src/index.ts | 22 +++++ packages/jest-types/src/Config.ts | 7 ++ 11 files changed, 308 insertions(+), 37 deletions(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 50871be0f1ea..5e9a35df6ec5 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -65,8 +65,42 @@ jobs: run: yarn lint:prettier:ci - name: check copyright headers run: yarn check-copyright-headers - test: - name: Node v${{ matrix.node-version }} on ${{ matrix.os }} + + test-1: + name: Node v${{ matrix.node-version }} on ${{ matrix.os }} (1/2) + strategy: + fail-fast: false + matrix: + node-version: [12.x, 14.x, 16.x, 17.x] + os: [ubuntu-latest, macOS-latest, windows-latest] + runs-on: ${{ matrix.os }} + needs: prepare-yarn-cache + + steps: + - name: Set git config + shell: bash + run: | + git config --global core.autocrlf false + git config --global core.symlinks true + if: runner.os == 'Windows' + - uses: actions/checkout@v3 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: yarn + - name: install + run: yarn --immutable + - name: build + run: yarn build:js + - name: Get number of CPU cores + id: cpu-cores + uses: SimenB/github-actions-cpu-cores@v1 + - name: run tests + run: yarn test-ci-partial:parallel --max-workers ${{ steps.cpu-cores.outputs.count }} --shard=1/2 + + test-2: + name: Node v${{ matrix.node-version }} on ${{ matrix.os }} (2/2) strategy: fail-fast: false matrix: @@ -96,10 +130,42 @@ jobs: id: cpu-cores uses: SimenB/github-actions-cpu-cores@v1 - name: run tests - run: yarn test-ci-partial:parallel --max-workers ${{ steps.cpu-cores.outputs.count }} + run: yarn test-ci-partial:parallel --max-workers ${{ steps.cpu-cores.outputs.count }} --shard=2/2 + + test-jasmine-1: + name: Node LTS on ${{ matrix.os }} using jest-jasmine2 (1/2) + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macOS-latest, windows-latest] + runs-on: ${{ matrix.os }} + needs: prepare-yarn-cache + + steps: + - name: Set git config + shell: bash + run: | + git config --global core.autocrlf false + git config --global core.symlinks true + if: runner.os == 'Windows' + - uses: actions/checkout@v3 + - name: Use Node.js LTS + uses: actions/setup-node@v3 + with: + node-version: lts/* + cache: yarn + - name: install + run: yarn --immutable + - name: build + run: yarn build:js + - name: Get number of CPU cores + id: cpu-cores + uses: SimenB/github-actions-cpu-cores@v1 + - name: run tests using jest-jasmine + run: yarn jest-jasmine-ci --max-workers ${{ steps.cpu-cores.outputs.count }} --shard=1/2 - test-jasmine: - name: Node LTS on ${{ matrix.os }} using jest-jasmine2 + test-jasmine-2: + name: Node LTS on ${{ matrix.os }} using jest-jasmine2 (2/2) strategy: fail-fast: false matrix: @@ -128,7 +194,7 @@ jobs: id: cpu-cores uses: SimenB/github-actions-cpu-cores@v1 - name: run tests using jest-jasmine - run: yarn jest-jasmine-ci --max-workers ${{ steps.cpu-cores.outputs.count }} + run: yarn jest-jasmine-ci --max-workers ${{ steps.cpu-cores.outputs.count }} --shard=2/2 test-coverage: name: Node LTS on Ubuntu with coverage diff --git a/docs/CLI.md b/docs/CLI.md index d465b78d34b3..37e7df52e37c 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -338,6 +338,16 @@ 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/__tests__/cli/args.test.ts b/packages/jest-cli/src/__tests__/cli/args.test.ts index ec302f70d734..7fafeeed7bf1 100644 --- a/packages/jest-cli/src/__tests__/cli/args.test.ts +++ b/packages/jest-cli/src/__tests__/cli/args.test.ts @@ -11,84 +11,78 @@ import {constants} from 'jest-config'; import {buildArgv} from '../../cli'; import {check} from '../../cli/args'; +const argv = (input: Partial): Config.Argv => input as Config.Argv; + describe('check', () => { it('returns true if the arguments are valid', () => { - const argv = {} as Config.Argv; - expect(check(argv)).toBe(true); + expect(check(argv({}))).toBe(true); }); it('raises an exception if runInBand and maxWorkers are both specified', () => { - const argv = {maxWorkers: 2, runInBand: true} as Config.Argv; - expect(() => check(argv)).toThrow( + expect(() => check(argv({maxWorkers: 2, runInBand: true}))).toThrow( 'Both --runInBand and --maxWorkers were specified', ); }); it('raises an exception if onlyChanged and watchAll are both specified', () => { - const argv = {onlyChanged: true, watchAll: true} as Config.Argv; - expect(() => check(argv)).toThrow( + expect(() => check(argv({onlyChanged: true, watchAll: true}))).toThrow( 'Both --onlyChanged and --watchAll were specified', ); }); it('raises an exception if onlyFailures and watchAll are both specified', () => { - const argv = {onlyFailures: true, watchAll: true} as Config.Argv; - expect(() => check(argv)).toThrow( + expect(() => check(argv({onlyFailures: true, watchAll: true}))).toThrow( 'Both --onlyFailures and --watchAll were specified', ); }); it('raises an exception when lastCommit and watchAll are both specified', () => { - const argv = {lastCommit: true, watchAll: true} as Config.Argv; - expect(() => check(argv)).toThrow( + expect(() => check(argv({lastCommit: true, watchAll: true}))).toThrow( 'Both --lastCommit and --watchAll were specified', ); }); it('raises an exception if findRelatedTests is specified with no file paths', () => { - const argv = { - _: [] as Array, - findRelatedTests: true, - } as Config.Argv; - expect(() => check(argv)).toThrow( + expect(() => + check( + argv({ + _: [], + findRelatedTests: true, + }), + ), + ).toThrow( 'The --findRelatedTests option requires file paths to be specified', ); }); it('raises an exception if maxWorkers is specified with no number', () => { - const argv = {maxWorkers: undefined} as unknown as Config.Argv; - expect(() => check(argv)).toThrow( + expect(() => check(argv({maxWorkers: undefined}))).toThrow( 'The --maxWorkers (-w) option requires a number or string to be specified', ); }); it('allows maxWorkers to be a %', () => { - const argv = {maxWorkers: '50%'} as unknown as Config.Argv; - expect(() => check(argv)).not.toThrow(); + expect(() => check(argv({maxWorkers: '50%'}))).not.toThrow(); }); test.each(constants.JEST_CONFIG_EXT_ORDER.map(e => e.substring(1)))( 'allows using "%s" file for --config option', ext => { + expect(() => check(argv({config: `jest.config.${ext}`}))).not.toThrow(); expect(() => - check({config: `jest.config.${ext}`} as Config.Argv), - ).not.toThrow(); - expect(() => - check({config: `../test/test/my_conf.${ext}`} as Config.Argv), + check(argv({config: `../test/test/my_conf.${ext}`})), ).not.toThrow(); }, ); it('raises an exception if selectProjects is not provided any project names', () => { - const argv: Config.Argv = {selectProjects: []} as Config.Argv; - expect(() => check(argv)).toThrow( + expect(() => check(argv({selectProjects: []}))).toThrow( 'The --selectProjects option requires the name of at least one project to be specified.\n', ); }); it('raises an exception if config is not a valid JSON string', () => { - const argv = {config: 'x:1'} as Config.Argv; - expect(() => check(argv)).toThrow( + expect(() => check(argv({config: 'x:1'}))).toThrow( 'The --config option requires a JSON string literal, or a file path with one of these extensions: .js, .ts, .mjs, .cjs, .json', ); }); @@ -97,13 +91,55 @@ describe('check', () => { const message = 'The --config option requires a JSON string literal, or a file path with one of these extensions: .js, .ts, .mjs, .cjs, .json'; - expect(() => check({config: 'jest.configjs'} as Config.Argv)).toThrow( - message, + expect(() => check(argv({config: 'jest.configjs'}))).toThrow(message); + expect(() => check(argv({config: 'jest.config.exe'}))).toThrow(message); + }); + + it('raises an exception if shard has wrong format', () => { + expect(() => check(argv({shard: 'mumblemuble'}))).toThrow( + /string in the format of \//, + ); + }); + + it('raises an exception if shard pair has to many items', () => { + expect(() => check(argv({shard: '1/2/2'}))).toThrow( + /string in the format of \//, + ); + }); + + it('raises an exception if shard has floating points', () => { + expect(() => check(argv({shard: '1.0/1'}))).toThrow( + /string in the format of \//, + ); + }); + + it('raises an exception if first item in shard pair is no number', () => { + expect(() => check(argv({shard: 'a/1'}))).toThrow( + /string in the format of \//, + ); + }); + + it('raises an exception if second item in shard pair is no number', () => { + expect(() => check(argv({shard: '1/a'}))).toThrow( + /string in the format of \//, + ); + }); + + it('raises an exception if shard is zero-indexed', () => { + expect(() => check(argv({shard: '0/1'}))).toThrow( + /requires 1-based values, received 0/, ); - expect(() => check({config: 'jest.config.exe'} as Config.Argv)).toThrow( - message, + }); + + it('raises an exception if shard index is larger than shard count', () => { + expect(() => check(argv({shard: '2/1'}))).toThrow( + /requires to be lower or equal than /, ); }); + + it('allows valid shard format', () => { + expect(() => check(argv({shard: '1/2'}))).not.toThrow(); + }); }); describe('buildArgv', () => { diff --git a/packages/jest-cli/src/cli/args.ts b/packages/jest-cli/src/cli/args.ts index 92c13247f646..1d2f140404d5 100644 --- a/packages/jest-cli/src/cli/args.ts +++ b/packages/jest-cli/src/cli/args.ts @@ -87,6 +87,34 @@ export function check(argv: Config.Argv): true { ); } + if (argv.shard) { + const shardPair = argv?.shard + .split('/') + .filter(d => /^\d+$/.test(d)) + .map(d => parseInt(d, 10)) + .filter((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 +549,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/__tests__/normalize.test.ts b/packages/jest-config/src/__tests__/normalize.test.ts index 7c96711a8c89..ccca146c1c54 100644 --- a/packages/jest-config/src/__tests__/normalize.test.ts +++ b/packages/jest-config/src/__tests__/normalize.test.ts @@ -1972,3 +1972,13 @@ describe('moduleLoader', () => { expect(console.warn).toMatchSnapshot(); }); }); + +describe('shards', () => { + it('should be object if defined', async () => { + const {options} = await normalize({rootDir: '/root/'}, { + shard: '1/2', + } as Config.Argv); + + expect(options.shard).toEqual({shardCount: 2, shardIndex: 1}); + }); +}); diff --git a/packages/jest-config/src/index.ts b/packages/jest-config/src/index.ts index 1e1448bdf285..229bdf750921 100644 --- a/packages/jest-config/src/index.ts +++ b/packages/jest-config/src/index.ts @@ -154,6 +154,7 @@ const groupOptions = ( reporters: options.reporters, rootDir: options.rootDir, runTestsByPath: options.runTestsByPath, + shard: options.shard, silent: options.silent, skipFilter: options.skipFilter, snapshotFormat: options.snapshotFormat, diff --git a/packages/jest-config/src/normalize.ts b/packages/jest-config/src/normalize.ts index 36e64007f5da..3930dc1b4164 100644 --- a/packages/jest-config/src/normalize.ts +++ b/packages/jest-config/src/normalize.ts @@ -1220,6 +1220,19 @@ export default async function normalize( newOptions.logHeapUsage = false; } + if (argv.shard) { + const [shardIndex, shardCount] = argv?.shard + .split('/') + .filter(d => /^\d+$/.test(d)) + .map(d => parseInt(d, 10)) + .filter((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/__tests__/test_sequencer.test.js b/packages/jest-test-sequencer/src/__tests__/test_sequencer.test.js index f61386f11795..c5000067d106 100644 --- a/packages/jest-test-sequencer/src/__tests__/test_sequencer.test.js +++ b/packages/jest-test-sequencer/src/__tests__/test_sequencer.test.js @@ -276,3 +276,74 @@ test('works with multiple contexts', () => { '/test-c.js': [SUCCESS, 3], }); }); + +test('does not shard by default', () => { + const tests = sequencer.shard(toTests(['/test-a.js', '/test-ab.js'])); + expect(tests.map(test => test.path)).toEqual(['/test-a.js', '/test-ab.js']); +}); + +test('return first shard', () => { + const tests = sequencer.shard( + toTests(['/test-a.js', '/test-abc.js', '/test-ab.js']), + { + shardCount: 2, + shardIndex: 1, + }, + ); + + expect(tests.map(test => test.path)).toEqual(['/test-a.js', '/test-ab.js']); +}); + +test('return second shard', () => { + const tests = sequencer.shard( + toTests(['/test-ab.js', '/test-abc.js', '/test-a.js']), + { + shardCount: 2, + shardIndex: 2, + }, + ); + + expect(tests.map(test => test.path)).toEqual(['/test-abc.js']); +}); + +test('return third shard', () => { + const tests = sequencer.shard( + toTests(['/test-abc.js', '/test-a.js', '/test-ab.js']), + { + shardCount: 3, + shardIndex: 3, + }, + ); + + expect(tests.map(test => test.path)).toEqual(['/test-abc.js']); +}); + +test('returns expected 100/10 shards', () => { + const allTests = new Array(100).fill(true).map((_, i) => `/${i}.js`); + + const shards = new Array(10).fill(true).map((_, i) => + sequencer.shard(allTests, { + shardCount: 10, + shardIndex: i + 1, + }), + ); + + expect(shards.map(shard => shard.length)).toEqual([ + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + ]); +}); + +test('returns expected 100/8 shards', () => { + const allTests = new Array(100).fill(true).map((_, i) => `/${i}.js`); + + const shards = new Array(8).fill(true).map((_, i) => + sequencer.shard(allTests, { + shardCount: 8, + shardIndex: i + 1, + }), + ); + + expect(shards.map(shard => shard.length)).toEqual([ + 13, 13, 13, 13, 13, 13, 13, 9, + ]); +}); diff --git a/packages/jest-test-sequencer/src/index.ts b/packages/jest-test-sequencer/src/index.ts index 32b0b98040e8..b3f2c7ed5da3 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 - 1); + const shardEnd = shardSize * options.shardIndex; + + return [...tests] + .sort((a, b) => (a.path > b.path ? 1 : -1)) + .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; From 9a02991e176857052b655debcd541796cfa393cf Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Fri, 4 Mar 2022 21:02:49 +1100 Subject: [PATCH 02/53] fix: remove unneeded optional chain Co-authored-by: Simen Bekkhus --- packages/jest-config/src/normalize.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/jest-config/src/normalize.ts b/packages/jest-config/src/normalize.ts index 3930dc1b4164..01e020f82332 100644 --- a/packages/jest-config/src/normalize.ts +++ b/packages/jest-config/src/normalize.ts @@ -1221,7 +1221,7 @@ export default async function normalize( } if (argv.shard) { - const [shardIndex, shardCount] = argv?.shard + const [shardIndex, shardCount] = argv.shard .split('/') .filter(d => /^\d+$/.test(d)) .map(d => parseInt(d, 10)) From 1bdab889e8598021d40c85f4cb947bda9fba61a8 Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Fri, 4 Mar 2022 21:25:17 +1100 Subject: [PATCH 03/53] fix: validate shard option in runJest too --- .../jest-cli/src/__tests__/cli/args.test.ts | 46 ------------------ packages/jest-cli/src/cli/args.ts | 30 ++---------- .../src/__tests__/parseShardPair.test.ts | 47 +++++++++++++++++++ packages/jest-config/src/index.ts | 1 + packages/jest-config/src/normalize.ts | 12 +---- packages/jest-config/src/parseShardPair.ts | 47 +++++++++++++++++++ 6 files changed, 102 insertions(+), 81 deletions(-) create mode 100644 packages/jest-config/src/__tests__/parseShardPair.test.ts create mode 100644 packages/jest-config/src/parseShardPair.ts diff --git a/packages/jest-cli/src/__tests__/cli/args.test.ts b/packages/jest-cli/src/__tests__/cli/args.test.ts index 7fafeeed7bf1..f40f37aeb873 100644 --- a/packages/jest-cli/src/__tests__/cli/args.test.ts +++ b/packages/jest-cli/src/__tests__/cli/args.test.ts @@ -94,52 +94,6 @@ describe('check', () => { expect(() => check(argv({config: 'jest.configjs'}))).toThrow(message); expect(() => check(argv({config: 'jest.config.exe'}))).toThrow(message); }); - - it('raises an exception if shard has wrong format', () => { - expect(() => check(argv({shard: 'mumblemuble'}))).toThrow( - /string in the format of \//, - ); - }); - - it('raises an exception if shard pair has to many items', () => { - expect(() => check(argv({shard: '1/2/2'}))).toThrow( - /string in the format of \//, - ); - }); - - it('raises an exception if shard has floating points', () => { - expect(() => check(argv({shard: '1.0/1'}))).toThrow( - /string in the format of \//, - ); - }); - - it('raises an exception if first item in shard pair is no number', () => { - expect(() => check(argv({shard: 'a/1'}))).toThrow( - /string in the format of \//, - ); - }); - - it('raises an exception if second item in shard pair is no number', () => { - expect(() => check(argv({shard: '1/a'}))).toThrow( - /string in the format of \//, - ); - }); - - it('raises an exception if shard is zero-indexed', () => { - expect(() => check(argv({shard: '0/1'}))).toThrow( - /requires 1-based values, received 0/, - ); - }); - - it('raises an exception if shard index is larger than shard count', () => { - expect(() => check(argv({shard: '2/1'}))).toThrow( - /requires to be lower or equal than /, - ); - }); - - it('allows valid shard format', () => { - expect(() => check(argv({shard: '1/2'}))).not.toThrow(); - }); }); describe('buildArgv', () => { diff --git a/packages/jest-cli/src/cli/args.ts b/packages/jest-cli/src/cli/args.ts index 1d2f140404d5..53eda7b6ede1 100644 --- a/packages/jest-cli/src/cli/args.ts +++ b/packages/jest-cli/src/cli/args.ts @@ -6,7 +6,7 @@ */ import type {Config} from '@jest/types'; -import {constants, isJSONString} from 'jest-config'; +import {constants, isJSONString, parseShardPairResult} from 'jest-config'; export function check(argv: Config.Argv): true { if ( @@ -88,30 +88,10 @@ export function check(argv: Config.Argv): true { } if (argv.shard) { - const shardPair = argv?.shard - .split('/') - .filter(d => /^\d+$/.test(d)) - .map(d => parseInt(d, 10)) - .filter((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', - ); + const result = parseShardPairResult(argv.shard); + if (result instanceof Error) { + result.message += '\n Example usage: jest --shard 1/5'; + throw result; } } diff --git a/packages/jest-config/src/__tests__/parseShardPair.test.ts b/packages/jest-config/src/__tests__/parseShardPair.test.ts new file mode 100644 index 000000000000..f0978beb30f9 --- /dev/null +++ b/packages/jest-config/src/__tests__/parseShardPair.test.ts @@ -0,0 +1,47 @@ +import {parseShardPair} from '../parseShardPair'; + +it('raises an exception if shard has wrong format', () => { + expect(() => parseShardPair('mumble')).toThrow( + /string in the format of \//, + ); +}); + +it('raises an exception if shard pair has to many items', () => { + expect(() => parseShardPair('1/2/3')).toThrow( + /string in the format of \//, + ); +}); + +it('raises an exception if shard has floating points', () => { + expect(() => parseShardPair('1.0/1')).toThrow( + /string in the format of \//, + ); +}); + +it('raises an exception if first item in shard pair is no number', () => { + expect(() => parseShardPair('a/1')).toThrow( + /string in the format of \//, + ); +}); + +it('raises an exception if second item in shard pair is no number', () => { + expect(() => parseShardPair('1/a')).toThrow( + /string in the format of \//, + ); +}); + +it('raises an exception if shard is zero-indexed', () => { + expect(() => parseShardPair('0/1')).toThrow( + /requires 1-based values, received 0/, + ); +}); + +it('raises an exception if shard index is larger than shard count', () => { + expect(() => parseShardPair('2/1')).toThrow( + /requires to be lower or equal than /, + ); +}); + +it('allows valid shard format', () => { + expect(() => parseShardPair('1/2')).not.toThrow(); +}); diff --git a/packages/jest-config/src/index.ts b/packages/jest-config/src/index.ts index 229bdf750921..29f2c0af2bfe 100644 --- a/packages/jest-config/src/index.ts +++ b/packages/jest-config/src/index.ts @@ -18,6 +18,7 @@ import {isJSONString, replaceRootDirInPath} from './utils'; export {isJSONString} from './utils'; export {default as normalize} from './normalize'; +export {parseShardPair, parseShardPairResult} from './parseShardPair'; export {default as deprecationEntries} from './Deprecated'; export {replaceRootDirInPath} from './utils'; export {default as defaults} from './Defaults'; diff --git a/packages/jest-config/src/normalize.ts b/packages/jest-config/src/normalize.ts index 01e020f82332..6a09c510435f 100644 --- a/packages/jest-config/src/normalize.ts +++ b/packages/jest-config/src/normalize.ts @@ -34,6 +34,7 @@ import VALID_CONFIG from './ValidConfig'; import {getDisplayNameColor} from './color'; import {DEFAULT_JS_PATTERN, DEFAULT_REPORTER_LABEL} from './constants'; import getMaxWorkers from './getMaxWorkers'; +import {parseShardPair} from './parseShardPair'; import setFromArgv from './setFromArgv'; import { BULLET, @@ -1221,16 +1222,7 @@ export default async function normalize( } if (argv.shard) { - const [shardIndex, shardCount] = argv.shard - .split('/') - .filter(d => /^\d+$/.test(d)) - .map(d => parseInt(d, 10)) - .filter((shard: number) => !Number.isNaN(shard)); - - newOptions.shard = { - shardCount, - shardIndex, - }; + newOptions.shard = parseShardPair(argv.shard); } return { diff --git a/packages/jest-config/src/parseShardPair.ts b/packages/jest-config/src/parseShardPair.ts new file mode 100644 index 000000000000..7585d19bbae0 --- /dev/null +++ b/packages/jest-config/src/parseShardPair.ts @@ -0,0 +1,47 @@ +export interface ShardPair { + shardCount: number; + shardIndex: number; +} + +export const parseShardPair = (pair: string): ShardPair => { + const parseResult = parseShardPairResult(pair); + + if (parseResult instanceof Error) { + throw parseResult; + } + + return parseResult; +}; + +export const parseShardPairResult = (pair: string): Error | ShardPair => { + const shardPair = pair + .split('/') + .filter(d => /^\d+$/.test(d)) + .map(d => parseInt(d, 10)) + .filter((shard: number) => !Number.isNaN(shard)); + + const [shardIndex, shardCount] = shardPair; + + if (shardPair.length !== 2) { + throw new Error( + 'The shard option requires a string in the format of /.', + ); + } + + if (shardIndex === 0 || shardCount === 0) { + throw new Error( + 'The shard option requires 1-based values, received 0 in the pair.', + ); + } + + if (shardIndex > shardCount) { + throw new Error( + 'The shard option / requires to be lower or equal than .', + ); + } + + return { + shardCount, + shardIndex, + }; +}; From edcd51b0a7476bf6bb517ecd6fde7831c43330f9 Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Fri, 4 Mar 2022 21:31:23 +1100 Subject: [PATCH 04/53] fix: simplify .shard control flow --- packages/jest-core/src/runJest.ts | 4 +++- packages/jest-test-sequencer/src/index.ts | 23 ++++++++--------------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/packages/jest-core/src/runJest.ts b/packages/jest-core/src/runJest.ts index d087dc2c565f..e7590e28ec44 100644 --- a/packages/jest-core/src/runJest.ts +++ b/packages/jest-core/src/runJest.ts @@ -192,7 +192,9 @@ export default async function runJest({ }), ); - allTests = sequencer.shard?.(allTests, globalConfig.shard) ?? allTests; + allTests = globalConfig.shard + ? 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 b3f2c7ed5da3..0330a85c5f66 100644 --- a/packages/jest-test-sequencer/src/index.ts +++ b/packages/jest-test-sequencer/src/index.ts @@ -70,21 +70,14 @@ 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 - 1); - const shardEnd = shardSize * options.shardIndex; - - return [...tests] - .sort((a, b) => (a.path > b.path ? 1 : -1)) - .slice(shardStart, shardEnd); - } - - return tests; + shard(tests: Array, options: SortOptions): Array { + const shardSize = Math.ceil(tests.length / options.shardCount); + const shardStart = shardSize * (options.shardIndex - 1); + const shardEnd = shardSize * options.shardIndex; + + return [...tests] + .sort((a, b) => (a.path > b.path ? 1 : -1)) + .slice(shardStart, shardEnd); } /** From 8a6d66fb052366787344b6bc532ca6ae495411d3 Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Fri, 4 Mar 2022 21:49:35 +1100 Subject: [PATCH 05/53] test: add shard e2e test --- e2e/__tests__/shard.test.ts | 52 +++++++++++++++++++++++++++++++++++ e2e/shard/__tests__/1.test.js | 13 +++++++++ e2e/shard/__tests__/2.test.js | 13 +++++++++ e2e/shard/__tests__/3.test.js | 13 +++++++++ e2e/shard/package.json | 5 ++++ 5 files changed, 96 insertions(+) create mode 100644 e2e/__tests__/shard.test.ts create mode 100644 e2e/shard/__tests__/1.test.js create mode 100644 e2e/shard/__tests__/2.test.js create mode 100644 e2e/shard/__tests__/3.test.js create mode 100644 e2e/shard/package.json diff --git a/e2e/__tests__/shard.test.ts b/e2e/__tests__/shard.test.ts new file mode 100644 index 000000000000..6f7efbafa092 --- /dev/null +++ b/e2e/__tests__/shard.test.ts @@ -0,0 +1,52 @@ +/** + * 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 * as path from 'path'; +import runJest from '../runJest'; + +test('--shard=1/1', () => { + const result = runJest('shard', ['--shard=1/1', '--listTests']); + + const paths = result.stdout + .split('\n') + .filter(Boolean) + .map(file => path.basename(file)); + + expect(paths).toEqual(['3.test.js', '2.test.js', '1.test.js']); +}); + +test('--shard=1/2', () => { + const result = runJest('shard', ['--shard=1/2', '--listTests']); + + const paths = result.stdout + .split('\n') + .filter(Boolean) + .map(file => path.basename(file)); + + expect(paths).toEqual(['2.test.js', '1.test.js']); +}); + +test('--shard=2/2', () => { + const result = runJest('shard', ['--shard=2/2', '--listTests']); + + const paths = result.stdout + .split('\n') + .filter(Boolean) + .map(file => path.basename(file)); + + expect(paths).toEqual(['3.test.js']); +}); + +test('--shard=4/4', () => { + const result = runJest('shard', ['--shard=4/4', '--listTests']); + + const paths = result.stdout + .split('\n') + .filter(Boolean) + .map(file => path.basename(file)); + + expect(paths).toEqual([]); +}); diff --git a/e2e/shard/__tests__/1.test.js b/e2e/shard/__tests__/1.test.js new file mode 100644 index 000000000000..8362bdfa2248 --- /dev/null +++ b/e2e/shard/__tests__/1.test.js @@ -0,0 +1,13 @@ +/** + * 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'; + +test('1-1', () => expect(2).toBe(2)); + +test('1-2', () => expect(2).toBe(2)); + +test('1-3', () => expect(2).toBe(2)); diff --git a/e2e/shard/__tests__/2.test.js b/e2e/shard/__tests__/2.test.js new file mode 100644 index 000000000000..50faeecd1743 --- /dev/null +++ b/e2e/shard/__tests__/2.test.js @@ -0,0 +1,13 @@ +/** + * 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'; + +test('2-1', () => expect(2).toBe(2)); + +test('2-2', () => expect(2).toBe(2)); + +test('2-3', () => expect(2).toBe(2)); diff --git a/e2e/shard/__tests__/3.test.js b/e2e/shard/__tests__/3.test.js new file mode 100644 index 000000000000..1ddf79ff3a36 --- /dev/null +++ b/e2e/shard/__tests__/3.test.js @@ -0,0 +1,13 @@ +/** + * 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'; + +test('3-1', () => expect(2).toBe(2)); + +test('3-2', () => expect(2).toBe(2)); + +test('3-3', () => expect(2).toBe(2)); diff --git a/e2e/shard/package.json b/e2e/shard/package.json new file mode 100644 index 000000000000..148788b25446 --- /dev/null +++ b/e2e/shard/package.json @@ -0,0 +1,5 @@ +{ + "jest": { + "testEnvironment": "node" + } +} From fc7574db48d4da55112ba5bd9b1c7ad082256230 Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Fri, 4 Mar 2022 21:56:48 +1100 Subject: [PATCH 06/53] ci: try dogfooding on circleci --- .circleci/config.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 450c196c3dce..a17f143542c1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -19,6 +19,8 @@ jobs: parameters: node-version: type: string + shard: + type: string working_directory: ~/jest executor: node/default steps: @@ -27,7 +29,7 @@ jobs: node-version: << parameters.node-version >> - node/install-packages: *install - run: - command: yarn test-ci-partial + command: yarn test-ci-partial --shard=<< parameters.shard >> - store_test_results: path: reports/junit @@ -49,8 +51,9 @@ workflows: build-and-deploy: jobs: - test-node: - name: test-node-partial-<< matrix.node-version >> + name: test-node-partial-<< matrix.node-version >>-<< matrix.shard >> matrix: parameters: node-version: ['12', '14', '16', '17'] - - test-jest-jasmine + shard: ['1/3', '2/3', '3/3'] + - test-jest-jasmine \ No newline at end of file From d524ecf8afe7b1b972fa9ca6a60d0777bff072e5 Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Fri, 4 Mar 2022 22:07:52 +1100 Subject: [PATCH 07/53] test: fix failing test --- .../jest-test-sequencer/src/__tests__/test_sequencer.test.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/jest-test-sequencer/src/__tests__/test_sequencer.test.js b/packages/jest-test-sequencer/src/__tests__/test_sequencer.test.js index c5000067d106..ef316ba1a819 100644 --- a/packages/jest-test-sequencer/src/__tests__/test_sequencer.test.js +++ b/packages/jest-test-sequencer/src/__tests__/test_sequencer.test.js @@ -278,7 +278,10 @@ test('works with multiple contexts', () => { }); test('does not shard by default', () => { - const tests = sequencer.shard(toTests(['/test-a.js', '/test-ab.js'])); + const tests = sequencer.shard(toTests(['/test-a.js', '/test-ab.js']), { + shardCount: 1, + shardIndex: 1, + }); expect(tests.map(test => test.path)).toEqual(['/test-a.js', '/test-ab.js']); }); From 392f2f237202f3419a8c29a638c8c60774fabaa0 Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Fri, 4 Mar 2022 22:17:02 +1100 Subject: [PATCH 08/53] docs: add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37f90b8d5235..522168e21202 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ - `[jest-test-result]` Add duration property to JSON test output ([#12518](https://github.com/facebook/jest/pull/12518)) - `[jest-worker]` [**BREAKING**] Allow only absolute `workerPath` ([#12343](https://github.com/facebook/jest/pull/12343)) - `[pretty-format]` New `maxWidth` parameter ([#12402](https://github.com/facebook/jest/pull/12402)) +- `[jest-cli, jest-core]` Add `--shard` parameter for distributed parallel test execution ([#12546](https://github.com/facebook/jest/pull/12546)) ### Fixes From d507ae547a0dc44ef0cedff868ac9b85333e8c50 Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Fri, 4 Mar 2022 22:22:00 +1100 Subject: [PATCH 09/53] ci: simplify circleci config --- .circleci/config.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index a17f143542c1..3c238a5d1c3c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -19,30 +19,30 @@ jobs: parameters: node-version: type: string - shard: - type: string working_directory: ~/jest executor: node/default + parallelism: 4 steps: - checkout - node/install: node-version: << parameters.node-version >> - node/install-packages: *install - run: - command: yarn test-ci-partial --shard=<< parameters.shard >> + command: yarn test-ci-partial --shard=$(expr $CIRCLE_NODE_INDEX + 1)/$CIRCLE_NODE_TOTAL - store_test_results: path: reports/junit test-jest-jasmine: working_directory: ~/jest executor: node/default + parallelism: 4 steps: - checkout - node/install: node-version: lts/* - node/install-packages: *install - run: - command: JEST_JASMINE=1 yarn test-ci-partial && JEST_JASMINE=1 yarn test-leak + command: JEST_JASMINE=1 yarn test-ci-partial && JEST_JASMINE=1 yarn test-leak --shard=$(expr $CIRCLE_NODE_INDEX + 1)/$CIRCLE_NODE_TOTAL - store_test_results: path: reports/junit @@ -51,9 +51,8 @@ workflows: build-and-deploy: jobs: - test-node: - name: test-node-partial-<< matrix.node-version >>-<< matrix.shard >> + name: test-node-partial-<< matrix.node-version >> matrix: parameters: node-version: ['12', '14', '16', '17'] - shard: ['1/3', '2/3', '3/3'] - test-jest-jasmine \ No newline at end of file From 002e9d5dc7662e608e3a7863cc8cecc69323070c Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Sat, 5 Mar 2022 20:29:13 +1100 Subject: [PATCH 10/53] Apply formatting suggestion Co-authored-by: Simen Bekkhus --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 3c238a5d1c3c..b731ff452d1d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -55,4 +55,4 @@ workflows: matrix: parameters: node-version: ['12', '14', '16', '17'] - - test-jest-jasmine \ No newline at end of file + - test-jest-jasmine From 17a2f41cece5cb49cc4ad37e38f80cf3d9d2edb5 Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Sat, 5 Mar 2022 20:33:16 +1100 Subject: [PATCH 11/53] Grammar fix Co-authored-by: Simen Bekkhus --- docs/CLI.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/CLI.md b/docs/CLI.md index 37e7df52e37c..365a9be6ea69 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -340,7 +340,7 @@ A list of paths to modules that run some code to configure or to set up the test ### `--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: +Shard suite to execute them on multiple machines. For example, to split the suite into three shards, each running one third of the tests: ``` jest --shard=1/3 From c2cc7587cf1e5ddbd25e3bc5485f5596b3f67b58 Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Sat, 5 Mar 2022 20:43:04 +1100 Subject: [PATCH 12/53] fix: validate only once --- packages/jest-cli/src/cli/args.ts | 10 +--------- packages/jest-config/src/index.ts | 1 - packages/jest-config/src/parseShardPair.ts | 12 +----------- 3 files changed, 2 insertions(+), 21 deletions(-) diff --git a/packages/jest-cli/src/cli/args.ts b/packages/jest-cli/src/cli/args.ts index 53eda7b6ede1..f61af29d7772 100644 --- a/packages/jest-cli/src/cli/args.ts +++ b/packages/jest-cli/src/cli/args.ts @@ -6,7 +6,7 @@ */ import type {Config} from '@jest/types'; -import {constants, isJSONString, parseShardPairResult} from 'jest-config'; +import {constants, isJSONString} from 'jest-config'; export function check(argv: Config.Argv): true { if ( @@ -87,14 +87,6 @@ export function check(argv: Config.Argv): true { ); } - if (argv.shard) { - const result = parseShardPairResult(argv.shard); - if (result instanceof Error) { - result.message += '\n Example usage: jest --shard 1/5'; - throw result; - } - } - return true; } diff --git a/packages/jest-config/src/index.ts b/packages/jest-config/src/index.ts index 29f2c0af2bfe..229bdf750921 100644 --- a/packages/jest-config/src/index.ts +++ b/packages/jest-config/src/index.ts @@ -18,7 +18,6 @@ import {isJSONString, replaceRootDirInPath} from './utils'; export {isJSONString} from './utils'; export {default as normalize} from './normalize'; -export {parseShardPair, parseShardPairResult} from './parseShardPair'; export {default as deprecationEntries} from './Deprecated'; export {replaceRootDirInPath} from './utils'; export {default as defaults} from './Defaults'; diff --git a/packages/jest-config/src/parseShardPair.ts b/packages/jest-config/src/parseShardPair.ts index 7585d19bbae0..3b5965dc3953 100644 --- a/packages/jest-config/src/parseShardPair.ts +++ b/packages/jest-config/src/parseShardPair.ts @@ -4,16 +4,6 @@ export interface ShardPair { } export const parseShardPair = (pair: string): ShardPair => { - const parseResult = parseShardPairResult(pair); - - if (parseResult instanceof Error) { - throw parseResult; - } - - return parseResult; -}; - -export const parseShardPairResult = (pair: string): Error | ShardPair => { const shardPair = pair .split('/') .filter(d => /^\d+$/.test(d)) @@ -30,7 +20,7 @@ export const parseShardPairResult = (pair: string): Error | ShardPair => { if (shardIndex === 0 || shardCount === 0) { throw new Error( - 'The shard option requires 1-based values, received 0 in the pair.', + 'The shard option requires 1-based values, received 0 or lower in the pair.', ); } From 913693958f39161c39cdcdb294ea05f5f11b9def Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Sat, 5 Mar 2022 20:44:30 +1100 Subject: [PATCH 13/53] test: cover negative number validation --- packages/jest-config/src/__tests__/parseShardPair.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/jest-config/src/__tests__/parseShardPair.test.ts b/packages/jest-config/src/__tests__/parseShardPair.test.ts index f0978beb30f9..3115ff61a66f 100644 --- a/packages/jest-config/src/__tests__/parseShardPair.test.ts +++ b/packages/jest-config/src/__tests__/parseShardPair.test.ts @@ -30,6 +30,12 @@ it('raises an exception if second item in shard pair is no number', () => { ); }); +it('raises an exception if shard contains negative number', () => { + expect(() => parseShardPair('1/-1')).toThrow( + /string in the format of \//, + ); +}); + it('raises an exception if shard is zero-indexed', () => { expect(() => parseShardPair('0/1')).toThrow( /requires 1-based values, received 0/, From 0e27779e7fb8d20ac4ba294a9419bda73c5248f6 Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Sat, 5 Mar 2022 20:46:58 +1100 Subject: [PATCH 14/53] test: add clarifying comment --- e2e/__tests__/shard.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/e2e/__tests__/shard.test.ts b/e2e/__tests__/shard.test.ts index 6f7efbafa092..d8dee80baf79 100644 --- a/e2e/__tests__/shard.test.ts +++ b/e2e/__tests__/shard.test.ts @@ -48,5 +48,7 @@ test('--shard=4/4', () => { .filter(Boolean) .map(file => path.basename(file)); + // project only has 3 files + // shards > 3 are empty expect(paths).toEqual([]); }); From abe78060db05394db056ded995c89207238e4b77 Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Sat, 5 Mar 2022 21:10:42 +1100 Subject: [PATCH 15/53] test: throw if sharding on non-shardin test sequencer --- e2e/__tests__/shard.test.ts | 30 +++++++++++++++++++++++ e2e/shard/no-sharding-test-sequencer.js | 5 ++++ e2e/shard/sharding-test-sequencer.js | 8 ++++++ packages/jest-core/src/runJest.ts | 11 ++++++++- packages/jest-test-sequencer/src/index.ts | 9 ++++--- 5 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 e2e/shard/no-sharding-test-sequencer.js create mode 100644 e2e/shard/sharding-test-sequencer.js diff --git a/e2e/__tests__/shard.test.ts b/e2e/__tests__/shard.test.ts index d8dee80baf79..42cc72e54a6e 100644 --- a/e2e/__tests__/shard.test.ts +++ b/e2e/__tests__/shard.test.ts @@ -52,3 +52,33 @@ test('--shard=4/4', () => { // shards > 3 are empty expect(paths).toEqual([]); }); + +test('--shard=1/2 custom non-sharding test sequencer', () => { + const result = runJest('shard', [ + '--shard=1/2', + '--listTests', + '--testSequencer=./no-sharding-test-sequencer.js', + ]); + + expect(result).toMatchObject({ + failed: true, + stderr: expect.stringMatching( + /Shard (.*) requested, but test schedulder (.*) in (.*) has no shard method./, + ), + }); +}); + +test('--shard=1/2 custom sharding test sequencer', () => { + const result = runJest('shard', [ + '--shard=1/2', + '--listTests', + '--testSequencer=./sharding-test-sequencer.js', + ]); + + const paths = result.stdout + .split('\n') + .filter(Boolean) + .map(file => path.basename(file)); + + expect(paths).toEqual(['3.test.js']); +}); diff --git a/e2e/shard/no-sharding-test-sequencer.js b/e2e/shard/no-sharding-test-sequencer.js new file mode 100644 index 000000000000..b3bf543b6f38 --- /dev/null +++ b/e2e/shard/no-sharding-test-sequencer.js @@ -0,0 +1,5 @@ +module.exports = class NoShardingSequencer { + sort(test) { + return test; + } +}; diff --git a/e2e/shard/sharding-test-sequencer.js b/e2e/shard/sharding-test-sequencer.js new file mode 100644 index 000000000000..06efd66b5bb5 --- /dev/null +++ b/e2e/shard/sharding-test-sequencer.js @@ -0,0 +1,8 @@ +module.exports = class NoShardingSequencer { + shard(tests) { + return [tests[2]]; + } + sort(tests) { + return tests; + } +}; diff --git a/packages/jest-core/src/runJest.ts b/packages/jest-core/src/runJest.ts index e7590e28ec44..3e14721983ff 100644 --- a/packages/jest-core/src/runJest.ts +++ b/packages/jest-core/src/runJest.ts @@ -192,9 +192,18 @@ export default async function runJest({ }), ); + if (globalConfig.shard) { + if (typeof sequencer.shard !== 'function') { + throw new Error( + `Shard ${globalConfig.shard.shardIndex}/${globalConfig.shard.shardCount} requested, but test schedulder ${Sequencer.name} in ${globalConfig.testSequencer} has no shard method.`, + ); + } + } + allTests = globalConfig.shard - ? sequencer.shard(allTests, globalConfig.shard) + ? await 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 0330a85c5f66..0b6ac3c539b2 100644 --- a/packages/jest-test-sequencer/src/index.ts +++ b/packages/jest-test-sequencer/src/index.ts @@ -70,7 +70,10 @@ export default class TestSequencer { return cache; } - shard(tests: Array, options: SortOptions): Array { + shard( + tests: Array, + options: SortOptions, + ): Array | Promise> { const shardSize = Math.ceil(tests.length / options.shardCount); const shardStart = shardSize * (options.shardIndex - 1); const shardEnd = shardSize * options.shardIndex; @@ -98,7 +101,7 @@ export default class TestSequencer { * from the file other than its size. * */ - sort(tests: Array): Array { + sort(tests: Array): Array | Promise> { const stats: {[path: string]: number} = {}; const fileSize = ({path, context: {hasteFS}}: Test) => stats[path] || (stats[path] = hasteFS.getSize(path) || 0); @@ -127,7 +130,7 @@ export default class TestSequencer { }); } - allFailedTests(tests: Array): Array { + allFailedTests(tests: Array): Array | Promise> { const hasFailed = (cache: Cache, test: Test) => cache[test.path]?.[0] === FAIL; return this.sort( From a205153ccd9d4050bc80be06e6cfc0125a8a8b14 Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Sat, 5 Mar 2022 21:21:29 +1100 Subject: [PATCH 16/53] ci: use actions matrix --- .github/workflows/nodejs.yml | 75 +++--------------------------------- 1 file changed, 6 insertions(+), 69 deletions(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 5e9a35df6ec5..b2bc318591e0 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -66,13 +66,14 @@ jobs: - name: check copyright headers run: yarn check-copyright-headers - test-1: - name: Node v${{ matrix.node-version }} on ${{ matrix.os }} (1/2) + test: + name: Node v${{ matrix.node-version }} on ${{ matrix.os }} (${{ matrix.shard }}) strategy: fail-fast: false matrix: node-version: [12.x, 14.x, 16.x, 17.x] os: [ubuntu-latest, macOS-latest, windows-latest] + shard: ['1/4', '2/4', '3/4', '4/4'] runs-on: ${{ matrix.os }} needs: prepare-yarn-cache @@ -97,47 +98,15 @@ jobs: id: cpu-cores uses: SimenB/github-actions-cpu-cores@v1 - name: run tests - run: yarn test-ci-partial:parallel --max-workers ${{ steps.cpu-cores.outputs.count }} --shard=1/2 + run: yarn test-ci-partial:parallel --max-workers ${{ steps.cpu-cores.outputs.count }} --shard=${{ matrix.shard }} - test-2: - name: Node v${{ matrix.node-version }} on ${{ matrix.os }} (2/2) - strategy: - fail-fast: false - matrix: - node-version: [12.x, 14.x, 16.x, 17.x] - os: [ubuntu-latest, macOS-latest, windows-latest] - runs-on: ${{ matrix.os }} - needs: prepare-yarn-cache - - steps: - - name: Set git config - shell: bash - run: | - git config --global core.autocrlf false - git config --global core.symlinks true - if: runner.os == 'Windows' - - uses: actions/checkout@v3 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node-version }} - cache: yarn - - name: install - run: yarn --immutable - - name: build - run: yarn build:js - - name: Get number of CPU cores - id: cpu-cores - uses: SimenB/github-actions-cpu-cores@v1 - - name: run tests - run: yarn test-ci-partial:parallel --max-workers ${{ steps.cpu-cores.outputs.count }} --shard=2/2 - - test-jasmine-1: + test-jasmine: name: Node LTS on ${{ matrix.os }} using jest-jasmine2 (1/2) strategy: fail-fast: false matrix: os: [ubuntu-latest, macOS-latest, windows-latest] + shard: ['1/4', '2/4', '3/4', '4/4'] runs-on: ${{ matrix.os }} needs: prepare-yarn-cache @@ -164,38 +133,6 @@ jobs: - name: run tests using jest-jasmine run: yarn jest-jasmine-ci --max-workers ${{ steps.cpu-cores.outputs.count }} --shard=1/2 - test-jasmine-2: - name: Node LTS on ${{ matrix.os }} using jest-jasmine2 (2/2) - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macOS-latest, windows-latest] - runs-on: ${{ matrix.os }} - needs: prepare-yarn-cache - - steps: - - name: Set git config - shell: bash - run: | - git config --global core.autocrlf false - git config --global core.symlinks true - if: runner.os == 'Windows' - - uses: actions/checkout@v3 - - name: Use Node.js LTS - uses: actions/setup-node@v3 - with: - node-version: lts/* - cache: yarn - - name: install - run: yarn --immutable - - name: build - run: yarn build:js - - name: Get number of CPU cores - id: cpu-cores - uses: SimenB/github-actions-cpu-cores@v1 - - name: run tests using jest-jasmine - run: yarn jest-jasmine-ci --max-workers ${{ steps.cpu-cores.outputs.count }} --shard=2/2 - test-coverage: name: Node LTS on Ubuntu with coverage runs-on: ubuntu-latest From 6f1631d89d104032eb5178d19293b81357e726ea Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Sat, 5 Mar 2022 21:53:41 +1100 Subject: [PATCH 17/53] docs: x-reference between shard and testSequencer --- docs/CLI.md | 4 +- docs/Configuration.md | 36 +++++++++++- packages/jest-test-sequencer/src/index.ts | 69 +++++++++++++++++------ 3 files changed, 90 insertions(+), 19 deletions(-) diff --git a/docs/CLI.md b/docs/CLI.md index 365a9be6ea69..8d28f126a0a1 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -348,6 +348,8 @@ jest --shard=2/3 jest --shard=3/3 ``` +Please refer to [shard configuration](./Configuration.md#shard) + ### `--showConfig` Print your Jest config and then exits. @@ -399,7 +401,7 @@ Lets you specify a custom test runner. ### `--testSequencer=` -Lets you specify a custom test sequencer. Please refer to the documentation of the corresponding configuration property for details. +Lets you specify a custom test sequencer. Please refer to the [testSequencer configuration](Configuration.md#testsequencer-string) for details. ### `--testTimeout=` diff --git a/docs/Configuration.md b/docs/Configuration.md index 5cb7a173e093..4cb752e15087 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -957,6 +957,18 @@ Example `jest.setup.js` file jest.setTimeout(10000); // in milliseconds ``` +### `shard` \[string] + +The test suite shard to execute in a format of `(?\d+)/(?\d+)`. + +`shardIndex` describes which shard to select while `shardCount` controls the number of shards the suite should be split into. + +`shardIndex` and `shardCount` have to be 1-based, positive numbers, `shardIndex` has to lower than or equal to `shardCount`. + +When `shard` is specified the used [testSquencer](#testsequencer-string) implementation has to implement a `shard` method. + +Refer to [testSquencer](#testsequencer-string) on how to override the default sharding implementation. + ### `slowTestThreshold` \[number] Default: `5` @@ -1327,7 +1339,11 @@ An example of such function can be found in our default [jasmine2 test runner pa Default: `@jest/test-sequencer` -This option allows you to use a custom sequencer instead of Jest's default. `sort` may optionally return a Promise. +This option allows you to use a custom sequencer instead of Jest's default. + +`sort` may optionally return a Promise. + +`shard` may optionally return a Promise. Example: @@ -1337,6 +1353,24 @@ Sort test path alphabetically. const Sequencer = require('@jest/test-sequencer').default; class CustomSequencer extends Sequencer { + /** + * Select tests for shard requested via --shard=shardIndex/shardCount + * Sharding is applied before sorting + */ + shard(tests, { shardIndex, shardCount }) { + const shardSize = Math.ceil(tests.length / options.shardCount); + const shardStart = shardSize * (options.shardIndex - 1); + const shardEnd = shardSize * options.shardIndex; + + return [...tests] + .sort((a, b) => (a.path > b.path ? 1 : -1)) + .slice(shardStart, shardEnd); + } + + /** + * Sort test to determine order of execution + * Sorting is applied after sharding + */ sort(tests) { // Test structure information // https://github.com/facebook/jest/blob/6b8b1404a1d9254e7d5d90a8934087a9c9899dab/packages/jest-runner/src/types.ts#L17-L21 diff --git a/packages/jest-test-sequencer/src/index.ts b/packages/jest-test-sequencer/src/index.ts index 0b6ac3c539b2..242f33919880 100644 --- a/packages/jest-test-sequencer/src/index.ts +++ b/packages/jest-test-sequencer/src/index.ts @@ -17,7 +17,7 @@ type Cache = { [key: string]: [0 | 1, number]; }; -export type SortOptions = { +export type ShardOptions = { shardIndex: number; shardCount: number; }; @@ -70,9 +70,30 @@ export default class TestSequencer { return cache; } + /** + * Select tests for shard requested via --shard=shardIndex/shardCount + * Sharding is applied before sorting + * + * @param tests All tests + * @param options shardIndex and shardIndex to select + * + * @example + * ```typescript + * class CustomSequencer extends Sequencer { + * shard(tests, { shardIndex, shardCount }) { + * const shardSize = Math.ceil(tests.length / options.shardCount); + * const shardStart = shardSize * (options.shardIndex - 1); + * const shardEnd = shardSize * options.shardIndex; + * return [...tests] + * .sort((a, b) => (a.path > b.path ? 1 : -1)) + * .slice(shardStart, shardEnd); + * } + * } + * ``` + */ shard( tests: Array, - options: SortOptions, + options: ShardOptions, ): Array | Promise> { const shardSize = Math.ceil(tests.length / options.shardCount); const shardStart = shardSize * (options.shardIndex - 1); @@ -84,24 +105,38 @@ export default class TestSequencer { } /** - * Sorting tests is very important because it has a great impact on the - * user-perceived responsiveness and speed of the test run. - * - * If such information is on cache, tests are sorted based on: - * -> Has it failed during the last run ? - * Since it's important to provide the most expected feedback as quickly - * as possible. - * -> How long it took to run ? - * Because running long tests first is an effort to minimize worker idle - * time at the end of a long test run. - * And if that information is not available they are sorted based on file size - * since big test files usually take longer to complete. - * - * Note that a possible improvement would be to analyse other information - * from the file other than its size. + * Sort test to determine order of execution + * Sorting is applied after sharding + * @param tests * + * ```typescript + * class CustomSequencer extends Sequencer { + * sort(tests) { + * const copyTests = Array.from(tests); + * return [...tests].sort((a, b) => (a.path > b.path ? 1 : -1)); + * } + * } + * ``` */ sort(tests: Array): Array | Promise> { + /** + * Sorting tests is very important because it has a great impact on the + * user-perceived responsiveness and speed of the test run. + * + * If such information is on cache, tests are sorted based on: + * -> Has it failed during the last run ? + * Since it's important to provide the most expected feedback as quickly + * as possible. + * -> How long it took to run ? + * Because running long tests first is an effort to minimize worker idle + * time at the end of a long test run. + * And if that information is not available they are sorted based on file size + * since big test files usually take longer to complete. + * + * Note that a possible improvement would be to analyse other information + * from the file other than its size. + * + */ const stats: {[path: string]: number} = {}; const fileSize = ({path, context: {hasteFS}}: Test) => stats[path] || (stats[path] = hasteFS.getSize(path) || 0); From f558cd9dc2971e0311c4e86658cfaa08e23bce17 Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Sat, 5 Mar 2022 22:39:37 +1100 Subject: [PATCH 18/53] feat: use jump consistent hashing --- packages/jest-test-sequencer/package.json | 1 + .../src/__tests__/test_sequencer.test.js | 507 +++++++++--------- packages/jest-test-sequencer/src/index.ts | 14 +- yarn.lock | 8 + 4 files changed, 277 insertions(+), 253 deletions(-) diff --git a/packages/jest-test-sequencer/package.json b/packages/jest-test-sequencer/package.json index 82f8ad6c2adc..886cb8245274 100644 --- a/packages/jest-test-sequencer/package.json +++ b/packages/jest-test-sequencer/package.json @@ -18,6 +18,7 @@ }, "dependencies": { "@jest/test-result": "^28.0.0-alpha.6", + "@subspace/jump-consistent-hash": "^1.1.1", "graceful-fs": "^4.2.9", "jest-haste-map": "^28.0.0-alpha.6", "jest-runtime": "^28.0.0-alpha.6" diff --git a/packages/jest-test-sequencer/src/__tests__/test_sequencer.test.js b/packages/jest-test-sequencer/src/__tests__/test_sequencer.test.js index ef316ba1a819..4c5e33f53508 100644 --- a/packages/jest-test-sequencer/src/__tests__/test_sequencer.test.js +++ b/packages/jest-test-sequencer/src/__tests__/test_sequencer.test.js @@ -55,274 +55,279 @@ beforeEach(() => { sequencer = new TestSequencer(); }); -test('sorts by file size if there is no timing information', () => { - expect(sequencer.sort(toTests(['/test-a.js', '/test-ab.js']))).toEqual([ - {context, duration: undefined, path: '/test-ab.js'}, - {context, duration: undefined, path: '/test-a.js'}, - ]); -}); +// test('sorts by file size if there is no timing information', () => { +// expect(sequencer.sort(toTests(['/test-a.js', '/test-ab.js']))).toEqual([ +// {context, duration: undefined, path: '/test-ab.js'}, +// {context, duration: undefined, path: '/test-a.js'}, +// ]); +// }); -test('sorts based on timing information', () => { - fs.readFileSync.mockImplementationOnce(() => - JSON.stringify({ - '/test-a.js': [SUCCESS, 5], - '/test-ab.js': [SUCCESS, 3], - }), - ); - expect(sequencer.sort(toTests(['/test-a.js', '/test-ab.js']))).toEqual([ - {context, duration: 5, path: '/test-a.js'}, - {context, duration: 3, path: '/test-ab.js'}, - ]); -}); +// test('sorts based on timing information', () => { +// fs.readFileSync.mockImplementationOnce(() => +// JSON.stringify({ +// '/test-a.js': [SUCCESS, 5], +// '/test-ab.js': [SUCCESS, 3], +// }), +// ); +// expect(sequencer.sort(toTests(['/test-a.js', '/test-ab.js']))).toEqual([ +// {context, duration: 5, path: '/test-a.js'}, +// {context, duration: 3, path: '/test-ab.js'}, +// ]); +// }); -test('sorts based on failures and timing information', () => { - fs.readFileSync.mockImplementationOnce(() => - JSON.stringify({ - '/test-a.js': [SUCCESS, 5], - '/test-ab.js': [FAIL, 0], - '/test-c.js': [FAIL, 6], - '/test-d.js': [SUCCESS, 2], - }), - ); - expect( - sequencer.sort( - toTests(['/test-a.js', '/test-ab.js', '/test-c.js', '/test-d.js']), - ), - ).toEqual([ - {context, duration: 6, path: '/test-c.js'}, - {context, duration: 0, path: '/test-ab.js'}, - {context, duration: 5, path: '/test-a.js'}, - {context, duration: 2, path: '/test-d.js'}, - ]); -}); +// test('sorts based on failures and timing information', () => { +// fs.readFileSync.mockImplementationOnce(() => +// JSON.stringify({ +// '/test-a.js': [SUCCESS, 5], +// '/test-ab.js': [FAIL, 0], +// '/test-c.js': [FAIL, 6], +// '/test-d.js': [SUCCESS, 2], +// }), +// ); +// expect( +// sequencer.sort( +// toTests(['/test-a.js', '/test-ab.js', '/test-c.js', '/test-d.js']), +// ), +// ).toEqual([ +// {context, duration: 6, path: '/test-c.js'}, +// {context, duration: 0, path: '/test-ab.js'}, +// {context, duration: 5, path: '/test-a.js'}, +// {context, duration: 2, path: '/test-d.js'}, +// ]); +// }); -test('sorts based on failures, timing information and file size', () => { - fs.readFileSync.mockImplementationOnce(() => - 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', - ]), - ), - ).toEqual([ - {context, duration: undefined, path: '/test-efg.js'}, - {context, duration: undefined, path: '/test-c.js'}, - {context, duration: 1, path: '/test-ab.js'}, - {context, duration: 5, path: '/test-a.js'}, - {context, duration: 2, path: '/test-d.js'}, - ]); -}); +// test('sorts based on failures, timing information and file size', () => { +// fs.readFileSync.mockImplementationOnce(() => +// 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', +// ]), +// ), +// ).toEqual([ +// {context, duration: undefined, path: '/test-efg.js'}, +// {context, duration: undefined, path: '/test-c.js'}, +// {context, duration: 1, path: '/test-ab.js'}, +// {context, duration: 5, path: '/test-a.js'}, +// {context, duration: 2, path: '/test-d.js'}, +// ]); +// }); -test('writes the cache based on results without existing cache', () => { - fs.readFileSync.mockImplementationOnce(() => { - throw new Error('File does not exist.'); - }); - - const testPaths = ['/test-a.js', '/test-b.js', '/test-c.js']; - const tests = sequencer.sort(toTests(testPaths)); - sequencer.cacheResults(tests, { - testResults: [ - { - numFailingTests: 0, - perfStats: {end: 2, runtime: 1, start: 1}, - testFilePath: '/test-a.js', - }, - { - numFailingTests: 0, - perfStats: {end: 0, runtime: 0, start: 0}, - skipped: true, - testFilePath: '/test-b.js', - }, - { - numFailingTests: 1, - perfStats: {end: 4, runtime: 3, start: 1}, - testFilePath: '/test-c.js', - }, - { - numFailingTests: 1, - perfStats: {end: 2, runtime: 1, start: 1}, - testFilePath: '/test-x.js', - }, - ], - }); - const fileData = JSON.parse(fs.writeFileSync.mock.calls[0][1]); - expect(fileData).toEqual({ - '/test-a.js': [SUCCESS, 1], - '/test-c.js': [FAIL, 3], - }); -}); +// test('writes the cache based on results without existing cache', () => { +// fs.readFileSync.mockImplementationOnce(() => { +// throw new Error('File does not exist.'); +// }); -test('returns failed tests in sorted order', () => { - fs.readFileSync.mockImplementationOnce(() => - JSON.stringify({ - '/test-a.js': [SUCCESS, 5], - '/test-ab.js': [FAIL, 1], - '/test-c.js': [FAIL], - }), - ); - const testPaths = ['/test-a.js', '/test-ab.js', '/test-c.js']; - expect(sequencer.allFailedTests(toTests(testPaths))).toEqual([ - {context, duration: undefined, path: '/test-c.js'}, - {context, duration: 1, path: '/test-ab.js'}, - ]); -}); +// const testPaths = ['/test-a.js', '/test-b.js', '/test-c.js']; +// const tests = sequencer.sort(toTests(testPaths)); +// sequencer.cacheResults(tests, { +// testResults: [ +// { +// numFailingTests: 0, +// perfStats: {end: 2, runtime: 1, start: 1}, +// testFilePath: '/test-a.js', +// }, +// { +// numFailingTests: 0, +// perfStats: {end: 0, runtime: 0, start: 0}, +// skipped: true, +// testFilePath: '/test-b.js', +// }, +// { +// numFailingTests: 1, +// perfStats: {end: 4, runtime: 3, start: 1}, +// testFilePath: '/test-c.js', +// }, +// { +// numFailingTests: 1, +// perfStats: {end: 2, runtime: 1, start: 1}, +// testFilePath: '/test-x.js', +// }, +// ], +// }); +// const fileData = JSON.parse(fs.writeFileSync.mock.calls[0][1]); +// expect(fileData).toEqual({ +// '/test-a.js': [SUCCESS, 1], +// '/test-c.js': [FAIL, 3], +// }); +// }); -test('writes the cache based on the results', () => { - fs.readFileSync.mockImplementationOnce(() => - JSON.stringify({ - '/test-a.js': [SUCCESS, 5], - '/test-b.js': [FAIL, 1], - '/test-c.js': [FAIL], - }), - ); +// test('returns failed tests in sorted order', () => { +// fs.readFileSync.mockImplementationOnce(() => +// JSON.stringify({ +// '/test-a.js': [SUCCESS, 5], +// '/test-ab.js': [FAIL, 1], +// '/test-c.js': [FAIL], +// }), +// ); +// const testPaths = ['/test-a.js', '/test-ab.js', '/test-c.js']; +// expect(sequencer.allFailedTests(toTests(testPaths))).toEqual([ +// {context, duration: undefined, path: '/test-c.js'}, +// {context, duration: 1, path: '/test-ab.js'}, +// ]); +// }); - const testPaths = ['/test-a.js', '/test-b.js', '/test-c.js']; - const tests = sequencer.sort(toTests(testPaths)); - sequencer.cacheResults(tests, { - testResults: [ - { - numFailingTests: 0, - perfStats: {end: 2, runtime: 1, start: 1}, - testFilePath: '/test-a.js', - }, - { - numFailingTests: 0, - perfStats: {end: 0, runtime: 0, start: 0}, - skipped: true, - testFilePath: '/test-b.js', - }, - { - numFailingTests: 1, - perfStats: {end: 4, runtime: 3, start: 1}, - testFilePath: '/test-c.js', - }, - { - numFailingTests: 1, - perfStats: {end: 2, runtime: 1, start: 1}, - testFilePath: '/test-x.js', - }, - ], - }); - const fileData = JSON.parse(fs.writeFileSync.mock.calls[0][1]); - expect(fileData).toEqual({ - '/test-a.js': [SUCCESS, 1], - '/test-b.js': [FAIL, 1], - '/test-c.js': [FAIL, 3], - }); -}); +// test('writes the cache based on the results', () => { +// fs.readFileSync.mockImplementationOnce(() => +// JSON.stringify({ +// '/test-a.js': [SUCCESS, 5], +// '/test-b.js': [FAIL, 1], +// '/test-c.js': [FAIL], +// }), +// ); -test('works with multiple contexts', () => { - fs.readFileSync.mockImplementationOnce(cacheName => - cacheName.startsWith(`${path.sep}cache${path.sep}`) - ? JSON.stringify({ - '/test-a.js': [SUCCESS, 5], - '/test-b.js': [FAIL, 1], - }) - : JSON.stringify({ - '/test-c.js': [FAIL], - }), - ); +// const testPaths = ['/test-a.js', '/test-b.js', '/test-c.js']; +// const tests = sequencer.sort(toTests(testPaths)); +// sequencer.cacheResults(tests, { +// testResults: [ +// { +// numFailingTests: 0, +// perfStats: {end: 2, runtime: 1, start: 1}, +// testFilePath: '/test-a.js', +// }, +// { +// numFailingTests: 0, +// perfStats: {end: 0, runtime: 0, start: 0}, +// skipped: true, +// testFilePath: '/test-b.js', +// }, +// { +// numFailingTests: 1, +// perfStats: {end: 4, runtime: 3, start: 1}, +// testFilePath: '/test-c.js', +// }, +// { +// numFailingTests: 1, +// perfStats: {end: 2, runtime: 1, start: 1}, +// testFilePath: '/test-x.js', +// }, +// ], +// }); +// const fileData = JSON.parse(fs.writeFileSync.mock.calls[0][1]); +// expect(fileData).toEqual({ +// '/test-a.js': [SUCCESS, 1], +// '/test-b.js': [FAIL, 1], +// '/test-c.js': [FAIL, 3], +// }); +// }); - const testPaths = [ - {context, duration: null, path: '/test-a.js'}, - {context, duration: null, path: '/test-b.js'}, - {context: secondContext, duration: null, path: '/test-c.js'}, - ]; - const tests = sequencer.sort(testPaths); - sequencer.cacheResults(tests, { - testResults: [ - { - numFailingTests: 0, - perfStats: {end: 2, runtime: 1, start: 1}, - testFilePath: '/test-a.js', - }, - { - numFailingTests: 0, - perfStats: {end: 0, runtime: 1, start: 0}, - skipped: true, - testFilePath: '/test-b.js', - }, - { - numFailingTests: 0, - perfStats: {end: 4, runtime: 3, start: 1}, - testFilePath: '/test-c.js', - }, - { - numFailingTests: 1, - perfStats: {end: 2, runtime: 1, start: 1}, - testFilePath: '/test-x.js', - }, - ], - }); - const fileDataA = JSON.parse(fs.writeFileSync.mock.calls[0][1]); - expect(fileDataA).toEqual({ - '/test-a.js': [SUCCESS, 1], - '/test-b.js': [FAIL, 1], - }); - const fileDataB = JSON.parse(fs.writeFileSync.mock.calls[1][1]); - expect(fileDataB).toEqual({ - '/test-c.js': [SUCCESS, 3], - }); -}); +// test('works with multiple contexts', () => { +// fs.readFileSync.mockImplementationOnce(cacheName => +// cacheName.startsWith(`${path.sep}cache${path.sep}`) +// ? JSON.stringify({ +// '/test-a.js': [SUCCESS, 5], +// '/test-b.js': [FAIL, 1], +// }) +// : JSON.stringify({ +// '/test-c.js': [FAIL], +// }), +// ); -test('does not shard by default', () => { - const tests = sequencer.shard(toTests(['/test-a.js', '/test-ab.js']), { - shardCount: 1, - shardIndex: 1, - }); - expect(tests.map(test => test.path)).toEqual(['/test-a.js', '/test-ab.js']); -}); +// const testPaths = [ +// {context, duration: null, path: '/test-a.js'}, +// {context, duration: null, path: '/test-b.js'}, +// {context: secondContext, duration: null, path: '/test-c.js'}, +// ]; +// const tests = sequencer.sort(testPaths); +// sequencer.cacheResults(tests, { +// testResults: [ +// { +// numFailingTests: 0, +// perfStats: {end: 2, runtime: 1, start: 1}, +// testFilePath: '/test-a.js', +// }, +// { +// numFailingTests: 0, +// perfStats: {end: 0, runtime: 1, start: 0}, +// skipped: true, +// testFilePath: '/test-b.js', +// }, +// { +// numFailingTests: 0, +// perfStats: {end: 4, runtime: 3, start: 1}, +// testFilePath: '/test-c.js', +// }, +// { +// numFailingTests: 1, +// perfStats: {end: 2, runtime: 1, start: 1}, +// testFilePath: '/test-x.js', +// }, +// ], +// }); +// const fileDataA = JSON.parse(fs.writeFileSync.mock.calls[0][1]); +// expect(fileDataA).toEqual({ +// '/test-a.js': [SUCCESS, 1], +// '/test-b.js': [FAIL, 1], +// }); +// const fileDataB = JSON.parse(fs.writeFileSync.mock.calls[1][1]); +// expect(fileDataB).toEqual({ +// '/test-c.js': [SUCCESS, 3], +// }); +// }); -test('return first shard', () => { - const tests = sequencer.shard( - toTests(['/test-a.js', '/test-abc.js', '/test-ab.js']), - { - shardCount: 2, - shardIndex: 1, - }, - ); +// test('does not shard by default', () => { +// const tests = sequencer.shard(toTests(['/test-a.js', '/test-ab.js']), { +// shardCount: 1, +// shardIndex: 1, +// }); - expect(tests.map(test => test.path)).toEqual(['/test-a.js', '/test-ab.js']); -}); +// expect(tests.map(test => test.path)).toEqual(['/test-ab.js', '/test-a.js']); +// }); -test('return second shard', () => { - const tests = sequencer.shard( - toTests(['/test-ab.js', '/test-abc.js', '/test-a.js']), - { - shardCount: 2, - shardIndex: 2, - }, - ); +// test('return first shard', () => { +// const tests = sequencer.shard( +// toTests(['/test-a.js', '/test-abc.js', '/test-ab.js']), +// { +// shardCount: 2, +// shardIndex: 1, +// }, +// ); - expect(tests.map(test => test.path)).toEqual(['/test-abc.js']); -}); +// console.log( +// '1/2', +// tests.map(test => test.path), +// ); +// expect(tests.map(test => test.path)).toEqual(['/test-ab.js', '/test-abc.js']); +// }); -test('return third shard', () => { - const tests = sequencer.shard( - toTests(['/test-abc.js', '/test-a.js', '/test-ab.js']), - { - shardCount: 3, - shardIndex: 3, - }, - ); +// test('return second shard', () => { +// const tests = sequencer.shard( +// toTests(['/test-a.js', '/test-abc.js', '/test-ab.js']), +// { +// shardCount: 2, +// shardIndex: 2, +// }, +// ); - expect(tests.map(test => test.path)).toEqual(['/test-abc.js']); -}); +// expect(tests.map(test => test.path)).toEqual(['/test-a.js']); +// }); + +// test('return third shard', () => { +// const tests = sequencer.shard( +// toTests(['/test-abc.js', '/test-a.js', '/test-ab.js']), +// { +// shardCount: 3, +// shardIndex: 3, +// }, +// ); + +// expect(tests.map(test => test.path)).toEqual(['/test-abc.js']); +// }); test('returns expected 100/10 shards', () => { - const allTests = new Array(100).fill(true).map((_, i) => `/${i}.js`); + const allTests = toTests(new Array(100).fill(true).map((_, i) => `/${i}.js`)); const shards = new Array(10).fill(true).map((_, i) => sequencer.shard(allTests, { @@ -337,7 +342,7 @@ test('returns expected 100/10 shards', () => { }); test('returns expected 100/8 shards', () => { - const allTests = new Array(100).fill(true).map((_, i) => `/${i}.js`); + const allTests = toTests(new Array(100).fill(true).map((_, i) => `/${i}.js`)); const shards = new Array(8).fill(true).map((_, i) => sequencer.shard(allTests, { diff --git a/packages/jest-test-sequencer/src/index.ts b/packages/jest-test-sequencer/src/index.ts index 242f33919880..f81fb1d4c055 100644 --- a/packages/jest-test-sequencer/src/index.ts +++ b/packages/jest-test-sequencer/src/index.ts @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +import {jumpConsistentHash} from '@subspace/jump-consistent-hash'; import * as fs from 'graceful-fs'; import type {AggregatedResult, Test} from '@jest/test-result'; import HasteMap from 'jest-haste-map'; @@ -100,8 +101,17 @@ export default class TestSequencer { const shardEnd = shardSize * options.shardIndex; return [...tests] - .sort((a, b) => (a.path > b.path ? 1 : -1)) - .slice(shardStart, shardEnd); + .map(test => { + const path = new TextEncoder().encode(test.path.padEnd(8, '.')); + + return { + hash: jumpConsistentHash(path, options.shardCount), + test, + }; + }) + .sort((a, b) => (a.hash > b.hash ? 1 : -1)) + .slice(shardStart, shardEnd) + .map(result => result.test); } /** diff --git a/yarn.lock b/yarn.lock index 51361c52e0ee..51ef399c84b8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2818,6 +2818,7 @@ __metadata: resolution: "@jest/test-sequencer@workspace:packages/jest-test-sequencer" dependencies: "@jest/test-result": ^28.0.0-alpha.6 + "@subspace/jump-consistent-hash": ^1.1.1 "@types/graceful-fs": ^4.1.3 graceful-fs: ^4.2.9 jest-haste-map: ^28.0.0-alpha.6 @@ -4418,6 +4419,13 @@ __metadata: languageName: node linkType: hard +"@subspace/jump-consistent-hash@npm:^1.1.1": + version: 1.1.1 + resolution: "@subspace/jump-consistent-hash@npm:1.1.1" + checksum: 1aeb3872de541b2794c81ba6dcc209c7cf5656fdae7a383b78d810d0b0aa0d37354d5a8b553147644ce8b0be0f9d1816efb0553b651b2007b5508f7263108101 + languageName: node + linkType: hard + "@surma/rollup-plugin-off-main-thread@npm:^2.2.3": version: 2.2.3 resolution: "@surma/rollup-plugin-off-main-thread@npm:2.2.3" From 1effb4569cbcfb2814947aaa66e2357b58770290 Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Sat, 5 Mar 2022 22:41:06 +1100 Subject: [PATCH 19/53] docs: fix typo Co-authored-by: Simen Bekkhus --- docs/Configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Configuration.md b/docs/Configuration.md index 4cb752e15087..b6f98c17a9fc 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -963,7 +963,7 @@ The test suite shard to execute in a format of `(?\d+)/(? Date: Sat, 5 Mar 2022 22:41:21 +1100 Subject: [PATCH 20/53] docs: contract Co-authored-by: Simen Bekkhus --- docs/Configuration.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/Configuration.md b/docs/Configuration.md index b6f98c17a9fc..7e53c466f05d 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -1341,9 +1341,7 @@ Default: `@jest/test-sequencer` This option allows you to use a custom sequencer instead of Jest's default. -`sort` may optionally return a Promise. - -`shard` may optionally return a Promise. +Both `sort` and `shard` may optionally return a `Promise`. Example: From 61536d8bce27526d7467c72c76579e98eb888eab Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Sat, 5 Mar 2022 22:41:55 +1100 Subject: [PATCH 21/53] docs: no relative marker Co-authored-by: Simen Bekkhus --- docs/CLI.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/CLI.md b/docs/CLI.md index 8d28f126a0a1..c015e00b621b 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -348,7 +348,7 @@ jest --shard=2/3 jest --shard=3/3 ``` -Please refer to [shard configuration](./Configuration.md#shard) +Please refer to [shard configuration](Configuration.md#shard) ### `--showConfig` From d0366ef1d1e7beac26e416bbae81ed97ee808569 Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Sat, 5 Mar 2022 22:42:48 +1100 Subject: [PATCH 22/53] docs: fix typo Co-authored-by: Simen Bekkhus --- packages/jest-core/src/runJest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/jest-core/src/runJest.ts b/packages/jest-core/src/runJest.ts index 3e14721983ff..48836765e8c7 100644 --- a/packages/jest-core/src/runJest.ts +++ b/packages/jest-core/src/runJest.ts @@ -195,7 +195,7 @@ export default async function runJest({ if (globalConfig.shard) { if (typeof sequencer.shard !== 'function') { throw new Error( - `Shard ${globalConfig.shard.shardIndex}/${globalConfig.shard.shardCount} requested, but test schedulder ${Sequencer.name} in ${globalConfig.testSequencer} has no shard method.`, + `Shard ${globalConfig.shard.shardIndex}/${globalConfig.shard.shardCount} requested, but test sequencer ${Sequencer.name} in ${globalConfig.testSequencer} has no shard method.`, ); } } From e598dada3fdc393ac754bbfaced23d022d04c8c6 Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Sat, 5 Mar 2022 22:43:30 +1100 Subject: [PATCH 23/53] fix: remove unneeded guard Co-authored-by: Simen Bekkhus --- packages/jest-core/src/runJest.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/jest-core/src/runJest.ts b/packages/jest-core/src/runJest.ts index 48836765e8c7..5831ab60af5e 100644 --- a/packages/jest-core/src/runJest.ts +++ b/packages/jest-core/src/runJest.ts @@ -198,12 +198,9 @@ export default async function runJest({ `Shard ${globalConfig.shard.shardIndex}/${globalConfig.shard.shardCount} requested, but test sequencer ${Sequencer.name} in ${globalConfig.testSequencer} has no shard method.`, ); } + allTests = await sequencer.shard(allTests, globalConfig.shard) } - allTests = globalConfig.shard - ? await sequencer.shard(allTests, globalConfig.shard) - : allTests; - allTests = await sequencer.sort(allTests); if (globalConfig.listTests) { From e045336cd5595971f87a16b7700ab326b8a95681 Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Sat, 5 Mar 2022 22:53:09 +1100 Subject: [PATCH 24/53] fix: clean up debris --- e2e/__tests__/shard.test.ts | 8 +- .../src/__tests__/test_sequencer.test.js | 540 +++++++++--------- 2 files changed, 274 insertions(+), 274 deletions(-) diff --git a/e2e/__tests__/shard.test.ts b/e2e/__tests__/shard.test.ts index 42cc72e54a6e..a197869a33ef 100644 --- a/e2e/__tests__/shard.test.ts +++ b/e2e/__tests__/shard.test.ts @@ -15,7 +15,7 @@ test('--shard=1/1', () => { .filter(Boolean) .map(file => path.basename(file)); - expect(paths).toEqual(['3.test.js', '2.test.js', '1.test.js']); + expect(paths).toEqual(['1.test.js', '2.test.js', '3.test.js']); }); test('--shard=1/2', () => { @@ -26,7 +26,7 @@ test('--shard=1/2', () => { .filter(Boolean) .map(file => path.basename(file)); - expect(paths).toEqual(['2.test.js', '1.test.js']); + expect(paths).toEqual(['2.test.js', '3.test.js']); }); test('--shard=2/2', () => { @@ -37,7 +37,7 @@ test('--shard=2/2', () => { .filter(Boolean) .map(file => path.basename(file)); - expect(paths).toEqual(['3.test.js']); + expect(paths).toEqual(['1.test.js']); }); test('--shard=4/4', () => { @@ -63,7 +63,7 @@ test('--shard=1/2 custom non-sharding test sequencer', () => { expect(result).toMatchObject({ failed: true, stderr: expect.stringMatching( - /Shard (.*) requested, but test schedulder (.*) in (.*) has no shard method./, + /Shard (.*) requested, but test sequencer (.*) in (.*) has no shard method./, ), }); }); diff --git a/packages/jest-test-sequencer/src/__tests__/test_sequencer.test.js b/packages/jest-test-sequencer/src/__tests__/test_sequencer.test.js index 4c5e33f53508..987d6fb4f686 100644 --- a/packages/jest-test-sequencer/src/__tests__/test_sequencer.test.js +++ b/packages/jest-test-sequencer/src/__tests__/test_sequencer.test.js @@ -55,276 +55,276 @@ beforeEach(() => { sequencer = new TestSequencer(); }); -// test('sorts by file size if there is no timing information', () => { -// expect(sequencer.sort(toTests(['/test-a.js', '/test-ab.js']))).toEqual([ -// {context, duration: undefined, path: '/test-ab.js'}, -// {context, duration: undefined, path: '/test-a.js'}, -// ]); -// }); - -// test('sorts based on timing information', () => { -// fs.readFileSync.mockImplementationOnce(() => -// JSON.stringify({ -// '/test-a.js': [SUCCESS, 5], -// '/test-ab.js': [SUCCESS, 3], -// }), -// ); -// expect(sequencer.sort(toTests(['/test-a.js', '/test-ab.js']))).toEqual([ -// {context, duration: 5, path: '/test-a.js'}, -// {context, duration: 3, path: '/test-ab.js'}, -// ]); -// }); - -// test('sorts based on failures and timing information', () => { -// fs.readFileSync.mockImplementationOnce(() => -// JSON.stringify({ -// '/test-a.js': [SUCCESS, 5], -// '/test-ab.js': [FAIL, 0], -// '/test-c.js': [FAIL, 6], -// '/test-d.js': [SUCCESS, 2], -// }), -// ); -// expect( -// sequencer.sort( -// toTests(['/test-a.js', '/test-ab.js', '/test-c.js', '/test-d.js']), -// ), -// ).toEqual([ -// {context, duration: 6, path: '/test-c.js'}, -// {context, duration: 0, path: '/test-ab.js'}, -// {context, duration: 5, path: '/test-a.js'}, -// {context, duration: 2, path: '/test-d.js'}, -// ]); -// }); - -// test('sorts based on failures, timing information and file size', () => { -// fs.readFileSync.mockImplementationOnce(() => -// 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', -// ]), -// ), -// ).toEqual([ -// {context, duration: undefined, path: '/test-efg.js'}, -// {context, duration: undefined, path: '/test-c.js'}, -// {context, duration: 1, path: '/test-ab.js'}, -// {context, duration: 5, path: '/test-a.js'}, -// {context, duration: 2, path: '/test-d.js'}, -// ]); -// }); - -// test('writes the cache based on results without existing cache', () => { -// fs.readFileSync.mockImplementationOnce(() => { -// throw new Error('File does not exist.'); -// }); - -// const testPaths = ['/test-a.js', '/test-b.js', '/test-c.js']; -// const tests = sequencer.sort(toTests(testPaths)); -// sequencer.cacheResults(tests, { -// testResults: [ -// { -// numFailingTests: 0, -// perfStats: {end: 2, runtime: 1, start: 1}, -// testFilePath: '/test-a.js', -// }, -// { -// numFailingTests: 0, -// perfStats: {end: 0, runtime: 0, start: 0}, -// skipped: true, -// testFilePath: '/test-b.js', -// }, -// { -// numFailingTests: 1, -// perfStats: {end: 4, runtime: 3, start: 1}, -// testFilePath: '/test-c.js', -// }, -// { -// numFailingTests: 1, -// perfStats: {end: 2, runtime: 1, start: 1}, -// testFilePath: '/test-x.js', -// }, -// ], -// }); -// const fileData = JSON.parse(fs.writeFileSync.mock.calls[0][1]); -// expect(fileData).toEqual({ -// '/test-a.js': [SUCCESS, 1], -// '/test-c.js': [FAIL, 3], -// }); -// }); - -// test('returns failed tests in sorted order', () => { -// fs.readFileSync.mockImplementationOnce(() => -// JSON.stringify({ -// '/test-a.js': [SUCCESS, 5], -// '/test-ab.js': [FAIL, 1], -// '/test-c.js': [FAIL], -// }), -// ); -// const testPaths = ['/test-a.js', '/test-ab.js', '/test-c.js']; -// expect(sequencer.allFailedTests(toTests(testPaths))).toEqual([ -// {context, duration: undefined, path: '/test-c.js'}, -// {context, duration: 1, path: '/test-ab.js'}, -// ]); -// }); - -// test('writes the cache based on the results', () => { -// fs.readFileSync.mockImplementationOnce(() => -// JSON.stringify({ -// '/test-a.js': [SUCCESS, 5], -// '/test-b.js': [FAIL, 1], -// '/test-c.js': [FAIL], -// }), -// ); - -// const testPaths = ['/test-a.js', '/test-b.js', '/test-c.js']; -// const tests = sequencer.sort(toTests(testPaths)); -// sequencer.cacheResults(tests, { -// testResults: [ -// { -// numFailingTests: 0, -// perfStats: {end: 2, runtime: 1, start: 1}, -// testFilePath: '/test-a.js', -// }, -// { -// numFailingTests: 0, -// perfStats: {end: 0, runtime: 0, start: 0}, -// skipped: true, -// testFilePath: '/test-b.js', -// }, -// { -// numFailingTests: 1, -// perfStats: {end: 4, runtime: 3, start: 1}, -// testFilePath: '/test-c.js', -// }, -// { -// numFailingTests: 1, -// perfStats: {end: 2, runtime: 1, start: 1}, -// testFilePath: '/test-x.js', -// }, -// ], -// }); -// const fileData = JSON.parse(fs.writeFileSync.mock.calls[0][1]); -// expect(fileData).toEqual({ -// '/test-a.js': [SUCCESS, 1], -// '/test-b.js': [FAIL, 1], -// '/test-c.js': [FAIL, 3], -// }); -// }); - -// test('works with multiple contexts', () => { -// fs.readFileSync.mockImplementationOnce(cacheName => -// cacheName.startsWith(`${path.sep}cache${path.sep}`) -// ? JSON.stringify({ -// '/test-a.js': [SUCCESS, 5], -// '/test-b.js': [FAIL, 1], -// }) -// : JSON.stringify({ -// '/test-c.js': [FAIL], -// }), -// ); - -// const testPaths = [ -// {context, duration: null, path: '/test-a.js'}, -// {context, duration: null, path: '/test-b.js'}, -// {context: secondContext, duration: null, path: '/test-c.js'}, -// ]; -// const tests = sequencer.sort(testPaths); -// sequencer.cacheResults(tests, { -// testResults: [ -// { -// numFailingTests: 0, -// perfStats: {end: 2, runtime: 1, start: 1}, -// testFilePath: '/test-a.js', -// }, -// { -// numFailingTests: 0, -// perfStats: {end: 0, runtime: 1, start: 0}, -// skipped: true, -// testFilePath: '/test-b.js', -// }, -// { -// numFailingTests: 0, -// perfStats: {end: 4, runtime: 3, start: 1}, -// testFilePath: '/test-c.js', -// }, -// { -// numFailingTests: 1, -// perfStats: {end: 2, runtime: 1, start: 1}, -// testFilePath: '/test-x.js', -// }, -// ], -// }); -// const fileDataA = JSON.parse(fs.writeFileSync.mock.calls[0][1]); -// expect(fileDataA).toEqual({ -// '/test-a.js': [SUCCESS, 1], -// '/test-b.js': [FAIL, 1], -// }); -// const fileDataB = JSON.parse(fs.writeFileSync.mock.calls[1][1]); -// expect(fileDataB).toEqual({ -// '/test-c.js': [SUCCESS, 3], -// }); -// }); - -// test('does not shard by default', () => { -// const tests = sequencer.shard(toTests(['/test-a.js', '/test-ab.js']), { -// shardCount: 1, -// shardIndex: 1, -// }); - -// expect(tests.map(test => test.path)).toEqual(['/test-ab.js', '/test-a.js']); -// }); - -// test('return first shard', () => { -// const tests = sequencer.shard( -// toTests(['/test-a.js', '/test-abc.js', '/test-ab.js']), -// { -// shardCount: 2, -// shardIndex: 1, -// }, -// ); - -// console.log( -// '1/2', -// tests.map(test => test.path), -// ); -// expect(tests.map(test => test.path)).toEqual(['/test-ab.js', '/test-abc.js']); -// }); - -// test('return second shard', () => { -// const tests = sequencer.shard( -// toTests(['/test-a.js', '/test-abc.js', '/test-ab.js']), -// { -// shardCount: 2, -// shardIndex: 2, -// }, -// ); - -// expect(tests.map(test => test.path)).toEqual(['/test-a.js']); -// }); - -// test('return third shard', () => { -// const tests = sequencer.shard( -// toTests(['/test-abc.js', '/test-a.js', '/test-ab.js']), -// { -// shardCount: 3, -// shardIndex: 3, -// }, -// ); - -// expect(tests.map(test => test.path)).toEqual(['/test-abc.js']); -// }); +test('sorts by file size if there is no timing information', () => { + expect(sequencer.sort(toTests(['/test-a.js', '/test-ab.js']))).toEqual([ + {context, duration: undefined, path: '/test-ab.js'}, + {context, duration: undefined, path: '/test-a.js'}, + ]); +}); + +test('sorts based on timing information', () => { + fs.readFileSync.mockImplementationOnce(() => + JSON.stringify({ + '/test-a.js': [SUCCESS, 5], + '/test-ab.js': [SUCCESS, 3], + }), + ); + expect(sequencer.sort(toTests(['/test-a.js', '/test-ab.js']))).toEqual([ + {context, duration: 5, path: '/test-a.js'}, + {context, duration: 3, path: '/test-ab.js'}, + ]); +}); + +test('sorts based on failures and timing information', () => { + fs.readFileSync.mockImplementationOnce(() => + JSON.stringify({ + '/test-a.js': [SUCCESS, 5], + '/test-ab.js': [FAIL, 0], + '/test-c.js': [FAIL, 6], + '/test-d.js': [SUCCESS, 2], + }), + ); + expect( + sequencer.sort( + toTests(['/test-a.js', '/test-ab.js', '/test-c.js', '/test-d.js']), + ), + ).toEqual([ + {context, duration: 6, path: '/test-c.js'}, + {context, duration: 0, path: '/test-ab.js'}, + {context, duration: 5, path: '/test-a.js'}, + {context, duration: 2, path: '/test-d.js'}, + ]); +}); + +test('sorts based on failures, timing information and file size', () => { + fs.readFileSync.mockImplementationOnce(() => + 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', + ]), + ), + ).toEqual([ + {context, duration: undefined, path: '/test-efg.js'}, + {context, duration: undefined, path: '/test-c.js'}, + {context, duration: 1, path: '/test-ab.js'}, + {context, duration: 5, path: '/test-a.js'}, + {context, duration: 2, path: '/test-d.js'}, + ]); +}); + +test('writes the cache based on results without existing cache', () => { + fs.readFileSync.mockImplementationOnce(() => { + throw new Error('File does not exist.'); + }); + + const testPaths = ['/test-a.js', '/test-b.js', '/test-c.js']; + const tests = sequencer.sort(toTests(testPaths)); + sequencer.cacheResults(tests, { + testResults: [ + { + numFailingTests: 0, + perfStats: {end: 2, runtime: 1, start: 1}, + testFilePath: '/test-a.js', + }, + { + numFailingTests: 0, + perfStats: {end: 0, runtime: 0, start: 0}, + skipped: true, + testFilePath: '/test-b.js', + }, + { + numFailingTests: 1, + perfStats: {end: 4, runtime: 3, start: 1}, + testFilePath: '/test-c.js', + }, + { + numFailingTests: 1, + perfStats: {end: 2, runtime: 1, start: 1}, + testFilePath: '/test-x.js', + }, + ], + }); + const fileData = JSON.parse(fs.writeFileSync.mock.calls[0][1]); + expect(fileData).toEqual({ + '/test-a.js': [SUCCESS, 1], + '/test-c.js': [FAIL, 3], + }); +}); + +test('returns failed tests in sorted order', () => { + fs.readFileSync.mockImplementationOnce(() => + JSON.stringify({ + '/test-a.js': [SUCCESS, 5], + '/test-ab.js': [FAIL, 1], + '/test-c.js': [FAIL], + }), + ); + const testPaths = ['/test-a.js', '/test-ab.js', '/test-c.js']; + expect(sequencer.allFailedTests(toTests(testPaths))).toEqual([ + {context, duration: undefined, path: '/test-c.js'}, + {context, duration: 1, path: '/test-ab.js'}, + ]); +}); + +test('writes the cache based on the results', () => { + fs.readFileSync.mockImplementationOnce(() => + JSON.stringify({ + '/test-a.js': [SUCCESS, 5], + '/test-b.js': [FAIL, 1], + '/test-c.js': [FAIL], + }), + ); + + const testPaths = ['/test-a.js', '/test-b.js', '/test-c.js']; + const tests = sequencer.sort(toTests(testPaths)); + sequencer.cacheResults(tests, { + testResults: [ + { + numFailingTests: 0, + perfStats: {end: 2, runtime: 1, start: 1}, + testFilePath: '/test-a.js', + }, + { + numFailingTests: 0, + perfStats: {end: 0, runtime: 0, start: 0}, + skipped: true, + testFilePath: '/test-b.js', + }, + { + numFailingTests: 1, + perfStats: {end: 4, runtime: 3, start: 1}, + testFilePath: '/test-c.js', + }, + { + numFailingTests: 1, + perfStats: {end: 2, runtime: 1, start: 1}, + testFilePath: '/test-x.js', + }, + ], + }); + const fileData = JSON.parse(fs.writeFileSync.mock.calls[0][1]); + expect(fileData).toEqual({ + '/test-a.js': [SUCCESS, 1], + '/test-b.js': [FAIL, 1], + '/test-c.js': [FAIL, 3], + }); +}); + +test('works with multiple contexts', () => { + fs.readFileSync.mockImplementationOnce(cacheName => + cacheName.startsWith(`${path.sep}cache${path.sep}`) + ? JSON.stringify({ + '/test-a.js': [SUCCESS, 5], + '/test-b.js': [FAIL, 1], + }) + : JSON.stringify({ + '/test-c.js': [FAIL], + }), + ); + + const testPaths = [ + {context, duration: null, path: '/test-a.js'}, + {context, duration: null, path: '/test-b.js'}, + {context: secondContext, duration: null, path: '/test-c.js'}, + ]; + const tests = sequencer.sort(testPaths); + sequencer.cacheResults(tests, { + testResults: [ + { + numFailingTests: 0, + perfStats: {end: 2, runtime: 1, start: 1}, + testFilePath: '/test-a.js', + }, + { + numFailingTests: 0, + perfStats: {end: 0, runtime: 1, start: 0}, + skipped: true, + testFilePath: '/test-b.js', + }, + { + numFailingTests: 0, + perfStats: {end: 4, runtime: 3, start: 1}, + testFilePath: '/test-c.js', + }, + { + numFailingTests: 1, + perfStats: {end: 2, runtime: 1, start: 1}, + testFilePath: '/test-x.js', + }, + ], + }); + const fileDataA = JSON.parse(fs.writeFileSync.mock.calls[0][1]); + expect(fileDataA).toEqual({ + '/test-a.js': [SUCCESS, 1], + '/test-b.js': [FAIL, 1], + }); + const fileDataB = JSON.parse(fs.writeFileSync.mock.calls[1][1]); + expect(fileDataB).toEqual({ + '/test-c.js': [SUCCESS, 3], + }); +}); + +test('does not shard by default', () => { + const tests = sequencer.shard(toTests(['/test-a.js', '/test-ab.js']), { + shardCount: 1, + shardIndex: 1, + }); + + expect(tests.map(test => test.path)).toEqual(['/test-ab.js', '/test-a.js']); +}); + +test('return first shard', () => { + const tests = sequencer.shard( + toTests(['/test-a.js', '/test-abc.js', '/test-ab.js']), + { + shardCount: 2, + shardIndex: 1, + }, + ); + + console.log( + '1/2', + tests.map(test => test.path), + ); + expect(tests.map(test => test.path)).toEqual(['/test-ab.js', '/test-abc.js']); +}); + +test('return second shard', () => { + const tests = sequencer.shard( + toTests(['/test-a.js', '/test-abc.js', '/test-ab.js']), + { + shardCount: 2, + shardIndex: 2, + }, + ); + + expect(tests.map(test => test.path)).toEqual(['/test-a.js']); +}); + +test('return third shard', () => { + const tests = sequencer.shard( + toTests(['/test-abc.js', '/test-a.js', '/test-ab.js']), + { + shardCount: 3, + shardIndex: 3, + }, + ); + + expect(tests.map(test => test.path)).toEqual(['/test-abc.js']); +}); test('returns expected 100/10 shards', () => { const allTests = toTests(new Array(100).fill(true).map((_, i) => `/${i}.js`)); From 1ef0f95f4a754085935816920ced51647a93f875 Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Sat, 5 Mar 2022 22:54:43 +1100 Subject: [PATCH 25/53] fix: apply format --- packages/jest-core/src/runJest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/jest-core/src/runJest.ts b/packages/jest-core/src/runJest.ts index 5831ab60af5e..7e7bf5b86501 100644 --- a/packages/jest-core/src/runJest.ts +++ b/packages/jest-core/src/runJest.ts @@ -198,7 +198,7 @@ export default async function runJest({ `Shard ${globalConfig.shard.shardIndex}/${globalConfig.shard.shardCount} requested, but test sequencer ${Sequencer.name} in ${globalConfig.testSequencer} has no shard method.`, ); } - allTests = await sequencer.shard(allTests, globalConfig.shard) + allTests = await sequencer.shard(allTests, globalConfig.shard); } allTests = await sequencer.sort(allTests); From 42f5276f9963746543a8b63756cdd24a1ab272f7 Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Sat, 5 Mar 2022 23:13:47 +1100 Subject: [PATCH 26/53] style: apply formatting --- docs/Configuration.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/Configuration.md b/docs/Configuration.md index 7e53c466f05d..c8f196ee1522 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -957,7 +957,7 @@ Example `jest.setup.js` file jest.setTimeout(10000); // in milliseconds ``` -### `shard` \[string] +### `shard` \[string] The test suite shard to execute in a format of `(?\d+)/(?\d+)`. @@ -1339,7 +1339,7 @@ An example of such function can be found in our default [jasmine2 test runner pa Default: `@jest/test-sequencer` -This option allows you to use a custom sequencer instead of Jest's default. +This option allows you to use a custom sequencer instead of Jest's default. Both `sort` and `shard` may optionally return a `Promise`. @@ -1355,7 +1355,7 @@ class CustomSequencer extends Sequencer { * Select tests for shard requested via --shard=shardIndex/shardCount * Sharding is applied before sorting */ - shard(tests, { shardIndex, shardCount }) { + shard(tests, {shardIndex, shardCount}) { const shardSize = Math.ceil(tests.length / options.shardCount); const shardStart = shardSize * (options.shardIndex - 1); const shardEnd = shardSize * options.shardIndex; From 4cc8728329614b990fabd13642cf7893b306cb40 Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Sat, 5 Mar 2022 23:32:00 +1100 Subject: [PATCH 27/53] feat: use sha1 for test spreading --- e2e/__tests__/shard.test.ts | 4 ++-- .../snapshotEscapeSubstitution.test.js.snap | 3 +++ .../__tests__/handle-property-matchers.test.js | 5 +++++ .../__tests__/first-snapshot-fails-second-passes.test.js | 4 ++++ packages/jest-test-sequencer/package.json | 1 - .../src/__tests__/test_sequencer.test.js | 4 ++-- packages/jest-test-sequencer/src/index.ts | 9 +++++---- yarn.lock | 8 -------- 8 files changed, 21 insertions(+), 17 deletions(-) create mode 100644 e2e/snapshot-escape/__tests__/__snapshots__/snapshotEscapeSubstitution.test.js.snap create mode 100644 e2e/to-match-inline-snapshot/__tests__/handle-property-matchers.test.js create mode 100644 e2e/to-match-snapshot/__tests__/first-snapshot-fails-second-passes.test.js diff --git a/e2e/__tests__/shard.test.ts b/e2e/__tests__/shard.test.ts index a197869a33ef..720b55154437 100644 --- a/e2e/__tests__/shard.test.ts +++ b/e2e/__tests__/shard.test.ts @@ -15,7 +15,7 @@ test('--shard=1/1', () => { .filter(Boolean) .map(file => path.basename(file)); - expect(paths).toEqual(['1.test.js', '2.test.js', '3.test.js']); + expect(paths).toEqual(['1.test.js', '3.test.js', '2.test.js']); }); test('--shard=1/2', () => { @@ -26,7 +26,7 @@ test('--shard=1/2', () => { .filter(Boolean) .map(file => path.basename(file)); - expect(paths).toEqual(['2.test.js', '3.test.js']); + expect(paths).toEqual(['3.test.js', '2.test.js']); }); test('--shard=2/2', () => { diff --git a/e2e/snapshot-escape/__tests__/__snapshots__/snapshotEscapeSubstitution.test.js.snap b/e2e/snapshot-escape/__tests__/__snapshots__/snapshotEscapeSubstitution.test.js.snap new file mode 100644 index 000000000000..5763722d1fa8 --- /dev/null +++ b/e2e/snapshot-escape/__tests__/__snapshots__/snapshotEscapeSubstitution.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`escape substitution 1`] = `"\${banana}"`; diff --git a/e2e/to-match-inline-snapshot/__tests__/handle-property-matchers.test.js b/e2e/to-match-inline-snapshot/__tests__/handle-property-matchers.test.js new file mode 100644 index 000000000000..db68387569c8 --- /dev/null +++ b/e2e/to-match-inline-snapshot/__tests__/handle-property-matchers.test.js @@ -0,0 +1,5 @@ +test('handles property matchers', () => { + expect({createdAt: new Date()}).toMatchInlineSnapshot({ + createdAt: expect.any(Date), + }); +}); diff --git a/e2e/to-match-snapshot/__tests__/first-snapshot-fails-second-passes.test.js b/e2e/to-match-snapshot/__tests__/first-snapshot-fails-second-passes.test.js new file mode 100644 index 000000000000..7bbf65733133 --- /dev/null +++ b/e2e/to-match-snapshot/__tests__/first-snapshot-fails-second-passes.test.js @@ -0,0 +1,4 @@ +test('snapshots', () => { + expect('apple').toMatchSnapshot(); + expect('banana').toMatchSnapshot(); +}); diff --git a/packages/jest-test-sequencer/package.json b/packages/jest-test-sequencer/package.json index 886cb8245274..82f8ad6c2adc 100644 --- a/packages/jest-test-sequencer/package.json +++ b/packages/jest-test-sequencer/package.json @@ -18,7 +18,6 @@ }, "dependencies": { "@jest/test-result": "^28.0.0-alpha.6", - "@subspace/jump-consistent-hash": "^1.1.1", "graceful-fs": "^4.2.9", "jest-haste-map": "^28.0.0-alpha.6", "jest-runtime": "^28.0.0-alpha.6" diff --git a/packages/jest-test-sequencer/src/__tests__/test_sequencer.test.js b/packages/jest-test-sequencer/src/__tests__/test_sequencer.test.js index 987d6fb4f686..e323dec692b1 100644 --- a/packages/jest-test-sequencer/src/__tests__/test_sequencer.test.js +++ b/packages/jest-test-sequencer/src/__tests__/test_sequencer.test.js @@ -299,7 +299,7 @@ test('return first shard', () => { '1/2', tests.map(test => test.path), ); - expect(tests.map(test => test.path)).toEqual(['/test-ab.js', '/test-abc.js']); + expect(tests.map(test => test.path)).toEqual(['/test-ab.js', '/test-a.js']); }); test('return second shard', () => { @@ -311,7 +311,7 @@ test('return second shard', () => { }, ); - expect(tests.map(test => test.path)).toEqual(['/test-a.js']); + expect(tests.map(test => test.path)).toEqual(['/test-abc.js']); }); test('return third shard', () => { diff --git a/packages/jest-test-sequencer/src/index.ts b/packages/jest-test-sequencer/src/index.ts index f81fb1d4c055..731dbfe74d93 100644 --- a/packages/jest-test-sequencer/src/index.ts +++ b/packages/jest-test-sequencer/src/index.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import {jumpConsistentHash} from '@subspace/jump-consistent-hash'; +import * as crypto from 'crypto'; import * as fs from 'graceful-fs'; import type {AggregatedResult, Test} from '@jest/test-result'; import HasteMap from 'jest-haste-map'; @@ -102,14 +102,15 @@ export default class TestSequencer { return [...tests] .map(test => { - const path = new TextEncoder().encode(test.path.padEnd(8, '.')); + const shasum = crypto.createHash('sha1'); + shasum.update(test.path); return { - hash: jumpConsistentHash(path, options.shardCount), + hash: shasum.digest('hex'), test, }; }) - .sort((a, b) => (a.hash > b.hash ? 1 : -1)) + .sort((a, b) => (a.hash > b.hash ? -1 : 1)) .slice(shardStart, shardEnd) .map(result => result.test); } diff --git a/yarn.lock b/yarn.lock index 51ef399c84b8..51361c52e0ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2818,7 +2818,6 @@ __metadata: resolution: "@jest/test-sequencer@workspace:packages/jest-test-sequencer" dependencies: "@jest/test-result": ^28.0.0-alpha.6 - "@subspace/jump-consistent-hash": ^1.1.1 "@types/graceful-fs": ^4.1.3 graceful-fs: ^4.2.9 jest-haste-map: ^28.0.0-alpha.6 @@ -4419,13 +4418,6 @@ __metadata: languageName: node linkType: hard -"@subspace/jump-consistent-hash@npm:^1.1.1": - version: 1.1.1 - resolution: "@subspace/jump-consistent-hash@npm:1.1.1" - checksum: 1aeb3872de541b2794c81ba6dcc209c7cf5656fdae7a383b78d810d0b0aa0d37354d5a8b553147644ce8b0be0f9d1816efb0553b651b2007b5508f7263108101 - languageName: node - linkType: hard - "@surma/rollup-plugin-off-main-thread@npm:^2.2.3": version: 2.2.3 resolution: "@surma/rollup-plugin-off-main-thread@npm:2.2.3" From d8e69b4f3188f938378d802b1bfbab929d230b1a Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Sat, 5 Mar 2022 23:44:22 +1100 Subject: [PATCH 28/53] test: relax assertions regarding ordering --- e2e/__tests__/shard.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/e2e/__tests__/shard.test.ts b/e2e/__tests__/shard.test.ts index 720b55154437..b6bad3632e62 100644 --- a/e2e/__tests__/shard.test.ts +++ b/e2e/__tests__/shard.test.ts @@ -15,7 +15,9 @@ test('--shard=1/1', () => { .filter(Boolean) .map(file => path.basename(file)); - expect(paths).toEqual(['1.test.js', '3.test.js', '2.test.js']); + expect(paths).toEqual( + expect.arrayContaining(['1.test.js', '2.test.js', '3.test.js']), + ); }); test('--shard=1/2', () => { @@ -26,7 +28,7 @@ test('--shard=1/2', () => { .filter(Boolean) .map(file => path.basename(file)); - expect(paths).toEqual(['3.test.js', '2.test.js']); + expect(paths).toEqual(expect.arrayContaining(['2.test.js', '3.test.js'])); }); test('--shard=2/2', () => { @@ -38,6 +40,7 @@ test('--shard=2/2', () => { .map(file => path.basename(file)); expect(paths).toEqual(['1.test.js']); + expect(paths).not.toEqual(expect.arrayContaining(['2.test.js', '3.test.js'])); }); test('--shard=4/4', () => { From d3cebc4cfb12d12228e4e5d712e9028b97e0fc33 Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Sat, 5 Mar 2022 23:46:33 +1100 Subject: [PATCH 29/53] ci: use matrix parameter --- .github/workflows/nodejs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index b2bc318591e0..fb1810b28dae 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -131,7 +131,7 @@ jobs: id: cpu-cores uses: SimenB/github-actions-cpu-cores@v1 - name: run tests using jest-jasmine - run: yarn jest-jasmine-ci --max-workers ${{ steps.cpu-cores.outputs.count }} --shard=1/2 + run: yarn jest-jasmine-ci --max-workers ${{ steps.cpu-cores.outputs.count }} --shard=${{ matrix.shard }} test-coverage: name: Node LTS on Ubuntu with coverage From 6f2a6336f1a2b26d0c9a1296ead5dc8d583a43e5 Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Sat, 5 Mar 2022 23:48:00 +1100 Subject: [PATCH 30/53] ci: chard correct execution --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b731ff452d1d..f6614bab3220 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -42,7 +42,7 @@ jobs: node-version: lts/* - node/install-packages: *install - run: - command: JEST_JASMINE=1 yarn test-ci-partial && JEST_JASMINE=1 yarn test-leak --shard=$(expr $CIRCLE_NODE_INDEX + 1)/$CIRCLE_NODE_TOTAL + command: JEST_JASMINE=1 yarn test-ci-partial --shard=$(expr $CIRCLE_NODE_INDEX + 1)/$CIRCLE_NODE_TOTAL && JEST_JASMINE=1 yarn test-leak - store_test_results: path: reports/junit From 2986f1de9cdd9edf13f48328c32c1dfa38e7031b Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Sun, 6 Mar 2022 00:05:32 +1100 Subject: [PATCH 31/53] feat: replace hardcoded shard config --- .github/workflows/nodejs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index fb1810b28dae..cb8c811d762e 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -101,7 +101,7 @@ jobs: run: yarn test-ci-partial:parallel --max-workers ${{ steps.cpu-cores.outputs.count }} --shard=${{ matrix.shard }} test-jasmine: - name: Node LTS on ${{ matrix.os }} using jest-jasmine2 (1/2) + name: Node LTS on ${{ matrix.os }} using jest-jasmine2 (${{ matrix.shard }}) strategy: fail-fast: false matrix: From 2a08e13cbe94be908f15870d45cf186938b4f069 Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Sun, 6 Mar 2022 00:09:22 +1100 Subject: [PATCH 32/53] fix: remove snapshot checkins --- .../__snapshots__/snapshotEscapeSubstitution.test.js.snap | 3 --- .../__tests__/handle-property-matchers.test.js | 5 ----- .../__tests__/first-snapshot-fails-second-passes.test.js | 4 ---- 3 files changed, 12 deletions(-) delete mode 100644 e2e/snapshot-escape/__tests__/__snapshots__/snapshotEscapeSubstitution.test.js.snap delete mode 100644 e2e/to-match-inline-snapshot/__tests__/handle-property-matchers.test.js delete mode 100644 e2e/to-match-snapshot/__tests__/first-snapshot-fails-second-passes.test.js diff --git a/e2e/snapshot-escape/__tests__/__snapshots__/snapshotEscapeSubstitution.test.js.snap b/e2e/snapshot-escape/__tests__/__snapshots__/snapshotEscapeSubstitution.test.js.snap deleted file mode 100644 index 5763722d1fa8..000000000000 --- a/e2e/snapshot-escape/__tests__/__snapshots__/snapshotEscapeSubstitution.test.js.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`escape substitution 1`] = `"\${banana}"`; diff --git a/e2e/to-match-inline-snapshot/__tests__/handle-property-matchers.test.js b/e2e/to-match-inline-snapshot/__tests__/handle-property-matchers.test.js deleted file mode 100644 index db68387569c8..000000000000 --- a/e2e/to-match-inline-snapshot/__tests__/handle-property-matchers.test.js +++ /dev/null @@ -1,5 +0,0 @@ -test('handles property matchers', () => { - expect({createdAt: new Date()}).toMatchInlineSnapshot({ - createdAt: expect.any(Date), - }); -}); diff --git a/e2e/to-match-snapshot/__tests__/first-snapshot-fails-second-passes.test.js b/e2e/to-match-snapshot/__tests__/first-snapshot-fails-second-passes.test.js deleted file mode 100644 index 7bbf65733133..000000000000 --- a/e2e/to-match-snapshot/__tests__/first-snapshot-fails-second-passes.test.js +++ /dev/null @@ -1,4 +0,0 @@ -test('snapshots', () => { - expect('apple').toMatchSnapshot(); - expect('banana').toMatchSnapshot(); -}); From 8b60f60cf64e937e59d1aaf35b3b58f4535a503c Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Sun, 6 Mar 2022 00:30:16 +1100 Subject: [PATCH 33/53] docs: add missing copyright headers --- e2e/shard/no-sharding-test-sequencer.js | 6 ++++++ e2e/shard/sharding-test-sequencer.js | 6 ++++++ packages/jest-config/src/__tests__/parseShardPair.test.ts | 6 ++++++ packages/jest-config/src/parseShardPair.ts | 6 ++++++ 4 files changed, 24 insertions(+) diff --git a/e2e/shard/no-sharding-test-sequencer.js b/e2e/shard/no-sharding-test-sequencer.js index b3bf543b6f38..a29b222af93a 100644 --- a/e2e/shard/no-sharding-test-sequencer.js +++ b/e2e/shard/no-sharding-test-sequencer.js @@ -1,3 +1,9 @@ +/** + * 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. + */ module.exports = class NoShardingSequencer { sort(test) { return test; diff --git a/e2e/shard/sharding-test-sequencer.js b/e2e/shard/sharding-test-sequencer.js index 06efd66b5bb5..ea271e930e77 100644 --- a/e2e/shard/sharding-test-sequencer.js +++ b/e2e/shard/sharding-test-sequencer.js @@ -1,3 +1,9 @@ +/** + * 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. + */ module.exports = class NoShardingSequencer { shard(tests) { return [tests[2]]; diff --git a/packages/jest-config/src/__tests__/parseShardPair.test.ts b/packages/jest-config/src/__tests__/parseShardPair.test.ts index 3115ff61a66f..67eef0deba49 100644 --- a/packages/jest-config/src/__tests__/parseShardPair.test.ts +++ b/packages/jest-config/src/__tests__/parseShardPair.test.ts @@ -1,3 +1,9 @@ +/** + * 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 {parseShardPair} from '../parseShardPair'; it('raises an exception if shard has wrong format', () => { diff --git a/packages/jest-config/src/parseShardPair.ts b/packages/jest-config/src/parseShardPair.ts index 3b5965dc3953..1801dea0cf6d 100644 --- a/packages/jest-config/src/parseShardPair.ts +++ b/packages/jest-config/src/parseShardPair.ts @@ -1,3 +1,9 @@ +/** + * 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. + */ export interface ShardPair { shardCount: number; shardIndex: number; From 2aed48b9fd3ec737ecd1d98167abfaf766774b6d Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sat, 5 Mar 2022 18:39:25 +0100 Subject: [PATCH 34/53] move changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 522168e21202..1396884ddbb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - `[expect]` Expose `AsymmetricMatchers`, `MatcherFunction` and `MatcherFunctionWithState` interfaces ([#12363](https://github.com/facebook/jest/pull/12363), [#12376](https://github.com/facebook/jest/pull/12376)) - `[jest-circus, jest-jasmine2]` Allowed classes and functions as `describe` and `it`/`test` names ([#12484](https://github.com/facebook/jest/pull/12484)) - `[jest-cli, jest-config]` [**BREAKING**] Remove `testURL` config, use `testEnvironmentOptions.url` instead ([#10797](https://github.com/facebook/jest/pull/10797)) +- `[jest-cli, jest-core]` Add `--shard` parameter for distributed parallel test execution ([#12546](https://github.com/facebook/jest/pull/12546)) - `[jest-config]` [**BREAKING**] Stop shipping `jest-environment-jsdom` by default ([#12354](https://github.com/facebook/jest/pull/12354)) - `[jest-config]` [**BREAKING**] Stop shipping `jest-jasmine2` by default ([#12355](https://github.com/facebook/jest/pull/12355)) - `[jest-config, @jest/types]` Add `ci` to `GlobalConfig` ([#12378](https://github.com/facebook/jest/pull/12378)) @@ -39,7 +40,6 @@ - `[jest-test-result]` Add duration property to JSON test output ([#12518](https://github.com/facebook/jest/pull/12518)) - `[jest-worker]` [**BREAKING**] Allow only absolute `workerPath` ([#12343](https://github.com/facebook/jest/pull/12343)) - `[pretty-format]` New `maxWidth` parameter ([#12402](https://github.com/facebook/jest/pull/12402)) -- `[jest-cli, jest-core]` Add `--shard` parameter for distributed parallel test execution ([#12546](https://github.com/facebook/jest/pull/12546)) ### Fixes From 1b612396e3c533e804a832cb6e2625948e39393f Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sat, 5 Mar 2022 18:40:11 +0100 Subject: [PATCH 35/53] rename --- e2e/shard/no-sharding-test-sequencer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/e2e/shard/no-sharding-test-sequencer.js b/e2e/shard/no-sharding-test-sequencer.js index a29b222af93a..10c40335432b 100644 --- a/e2e/shard/no-sharding-test-sequencer.js +++ b/e2e/shard/no-sharding-test-sequencer.js @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ module.exports = class NoShardingSequencer { - sort(test) { - return test; + sort(tests) { + return tests; } }; From 9316f6f7eed9f578f95f87b63e4c37c79662bb21 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sat, 5 Mar 2022 18:44:36 +0100 Subject: [PATCH 36/53] doc tweaks --- docs/CLI.md | 2 +- docs/Configuration.md | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/CLI.md b/docs/CLI.md index 20e4de97a972..1d0b65a7f11d 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -348,7 +348,7 @@ jest --shard=2/3 jest --shard=3/3 ``` -Please refer to [shard configuration](Configuration.md#shard) +Please refer to [shard configuration](Configuration.md#shard). ### `--showConfig` diff --git a/docs/Configuration.md b/docs/Configuration.md index 717a4cfaa1ed..8757cdf96306 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -963,11 +963,11 @@ The test suite shard to execute in a format of `(?\d+)/(? Date: Sat, 5 Mar 2022 18:52:45 +0100 Subject: [PATCH 37/53] tweak test --- e2e/__tests__/shard.test.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/e2e/__tests__/shard.test.ts b/e2e/__tests__/shard.test.ts index b6bad3632e62..568edc9815ef 100644 --- a/e2e/__tests__/shard.test.ts +++ b/e2e/__tests__/shard.test.ts @@ -13,11 +13,10 @@ test('--shard=1/1', () => { const paths = result.stdout .split('\n') .filter(Boolean) - .map(file => path.basename(file)); + .map(file => path.basename(file)) + .sort(); - expect(paths).toEqual( - expect.arrayContaining(['1.test.js', '2.test.js', '3.test.js']), - ); + expect(paths).toEqual(['1.test.js', '2.test.js', '3.test.js']); }); test('--shard=1/2', () => { @@ -26,9 +25,10 @@ test('--shard=1/2', () => { const paths = result.stdout .split('\n') .filter(Boolean) - .map(file => path.basename(file)); + .map(file => path.basename(file)) + .sort(); - expect(paths).toEqual(expect.arrayContaining(['2.test.js', '3.test.js'])); + expect(paths).toEqual(['1.test.js', '3.test.js']); }); test('--shard=2/2', () => { @@ -39,8 +39,7 @@ test('--shard=2/2', () => { .filter(Boolean) .map(file => path.basename(file)); - expect(paths).toEqual(['1.test.js']); - expect(paths).not.toEqual(expect.arrayContaining(['2.test.js', '3.test.js'])); + expect(paths).toEqual(['2.test.js']); }); test('--shard=4/4', () => { From fe1eb3438ffd30329c9fde8cb5af46576402b5e5 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sat, 5 Mar 2022 18:57:08 +0100 Subject: [PATCH 38/53] tweak spacing in cli args descriptions --- packages/jest-cli/src/cli/args.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/jest-cli/src/cli/args.ts b/packages/jest-cli/src/cli/args.ts index f61af29d7772..57c55a59f129 100644 --- a/packages/jest-cli/src/cli/args.ts +++ b/packages/jest-cli/src/cli/args.ts @@ -361,7 +361,7 @@ export const options = { description: 'An array of file extensions your modules use. If you ' + 'require modules without specifying a file extension, these are the ' + - 'extensions Jest will look for. ', + 'extensions Jest will look for.', string: true, type: 'array', }, @@ -510,21 +510,21 @@ export const options = { setupFiles: { description: 'A list of paths to modules that run some code to configure or ' + - 'set up the testing environment before each test. ', + 'set up the testing environment before each test.', string: true, type: 'array', }, setupFilesAfterEnv: { description: 'A list of paths to modules that run some code to configure or ' + - 'set up the testing framework before each test ', + 'set up the testing framework before each test', 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"', + 'the form "current/all". 1-based, for example "3/5".', type: 'string', }, showConfig: { From 734748e86f4e0bab5d5125b7586d84bc81d98724 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sat, 5 Mar 2022 18:57:17 +0100 Subject: [PATCH 39/53] strings, not regex, in test --- .../src/__tests__/parseShardPair.test.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/jest-config/src/__tests__/parseShardPair.test.ts b/packages/jest-config/src/__tests__/parseShardPair.test.ts index 67eef0deba49..c74b1cdd57a2 100644 --- a/packages/jest-config/src/__tests__/parseShardPair.test.ts +++ b/packages/jest-config/src/__tests__/parseShardPair.test.ts @@ -8,49 +8,49 @@ import {parseShardPair} from '../parseShardPair'; it('raises an exception if shard has wrong format', () => { expect(() => parseShardPair('mumble')).toThrow( - /string in the format of \//, + 'string in the format of /', ); }); it('raises an exception if shard pair has to many items', () => { expect(() => parseShardPair('1/2/3')).toThrow( - /string in the format of \//, + 'string in the format of /', ); }); it('raises an exception if shard has floating points', () => { expect(() => parseShardPair('1.0/1')).toThrow( - /string in the format of \//, + 'string in the format of /', ); }); it('raises an exception if first item in shard pair is no number', () => { expect(() => parseShardPair('a/1')).toThrow( - /string in the format of \//, + 'string in the format of /', ); }); it('raises an exception if second item in shard pair is no number', () => { expect(() => parseShardPair('1/a')).toThrow( - /string in the format of \//, + 'string in the format of /', ); }); it('raises an exception if shard contains negative number', () => { expect(() => parseShardPair('1/-1')).toThrow( - /string in the format of \//, + 'string in the format of /', ); }); it('raises an exception if shard is zero-indexed', () => { expect(() => parseShardPair('0/1')).toThrow( - /requires 1-based values, received 0/, + 'requires 1-based values, received 0', ); }); it('raises an exception if shard index is larger than shard count', () => { expect(() => parseShardPair('2/1')).toThrow( - /requires to be lower or equal than /, + 'requires to be lower or equal than ', ); }); From 8fbace85a9b8f2c631793396dfb0798b0706acf7 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sat, 5 Mar 2022 18:58:18 +0100 Subject: [PATCH 40/53] remove inferred type annotation --- packages/jest-config/src/parseShardPair.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/jest-config/src/parseShardPair.ts b/packages/jest-config/src/parseShardPair.ts index 1801dea0cf6d..a9ac7c2e2af9 100644 --- a/packages/jest-config/src/parseShardPair.ts +++ b/packages/jest-config/src/parseShardPair.ts @@ -14,7 +14,7 @@ export const parseShardPair = (pair: string): ShardPair => { .split('/') .filter(d => /^\d+$/.test(d)) .map(d => parseInt(d, 10)) - .filter((shard: number) => !Number.isNaN(shard)); + .filter(shard => !Number.isNaN(shard)); const [shardIndex, shardCount] = shardPair; From a542b82e0f9078949f1b5bfd385cbd4de0645eee Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sat, 5 Mar 2022 18:59:52 +0100 Subject: [PATCH 41/53] Update docs/CLI.md --- docs/CLI.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/CLI.md b/docs/CLI.md index 1d0b65a7f11d..5f2d17156b7f 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -401,7 +401,7 @@ Lets you specify a custom test runner. ### `--testSequencer=` -Lets you specify a custom test sequencer. Please refer to the [testSequencer configuration](Configuration.md#testsequencer-string) for details. +Lets you specify a custom test sequencer. Please refer to the [`testSequencer` configuration](Configuration.md#testsequencer-string) for details. ### `--testTimeout=` From 0d4fb3f8022d10fa66280c65c40cc0dc6c07da61 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sat, 5 Mar 2022 19:00:34 +0100 Subject: [PATCH 42/53] Update docs/Configuration.md --- docs/Configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Configuration.md b/docs/Configuration.md index 8757cdf96306..5bb098fe0ed1 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -965,7 +965,7 @@ The test suite shard to execute in a format of `(?\d+)/(? Date: Sat, 5 Mar 2022 19:35:26 +0100 Subject: [PATCH 43/53] use relative path in hash --- e2e/__tests__/shard.test.ts | 4 ++-- packages/jest-test-sequencer/package.json | 3 ++- packages/jest-test-sequencer/src/index.ts | 15 +++++++++++---- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/e2e/__tests__/shard.test.ts b/e2e/__tests__/shard.test.ts index 568edc9815ef..9747d00ef61f 100644 --- a/e2e/__tests__/shard.test.ts +++ b/e2e/__tests__/shard.test.ts @@ -28,7 +28,7 @@ test('--shard=1/2', () => { .map(file => path.basename(file)) .sort(); - expect(paths).toEqual(['1.test.js', '3.test.js']); + expect(paths).toEqual(['2.test.js', '3.test.js']); }); test('--shard=2/2', () => { @@ -39,7 +39,7 @@ test('--shard=2/2', () => { .filter(Boolean) .map(file => path.basename(file)); - expect(paths).toEqual(['2.test.js']); + expect(paths).toEqual(['1.test.js']); }); test('--shard=4/4', () => { diff --git a/packages/jest-test-sequencer/package.json b/packages/jest-test-sequencer/package.json index 82f8ad6c2adc..1027e340003e 100644 --- a/packages/jest-test-sequencer/package.json +++ b/packages/jest-test-sequencer/package.json @@ -20,7 +20,8 @@ "@jest/test-result": "^28.0.0-alpha.6", "graceful-fs": "^4.2.9", "jest-haste-map": "^28.0.0-alpha.6", - "jest-runtime": "^28.0.0-alpha.6" + "jest-runtime": "^28.0.0-alpha.6", + "slash": "^3.0.0" }, "devDependencies": { "@types/graceful-fs": "^4.1.3" diff --git a/packages/jest-test-sequencer/src/index.ts b/packages/jest-test-sequencer/src/index.ts index 731dbfe74d93..55faadd4abae 100644 --- a/packages/jest-test-sequencer/src/index.ts +++ b/packages/jest-test-sequencer/src/index.ts @@ -6,7 +6,9 @@ */ import * as crypto from 'crypto'; +import * as path from 'path'; import * as fs from 'graceful-fs'; +import slash = require('slash'); import type {AggregatedResult, Test} from '@jest/test-result'; import HasteMap from 'jest-haste-map'; import type {Context} from 'jest-runtime'; @@ -100,13 +102,18 @@ export default class TestSequencer { const shardStart = shardSize * (options.shardIndex - 1); const shardEnd = shardSize * options.shardIndex; - return [...tests] + return tests .map(test => { - const shasum = crypto.createHash('sha1'); - shasum.update(test.path); + const relativeTestPath = path.relative( + test.context.config.rootDir, + test.path, + ); return { - hash: shasum.digest('hex'), + hash: crypto + .createHash('sha1') + .update(slash(relativeTestPath)) + .digest('hex'), test, }; }) From 3362237f729d11381d6174407a18e58bbb23168c Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sat, 5 Mar 2022 19:38:16 +0100 Subject: [PATCH 44/53] lockfile --- yarn.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/yarn.lock b/yarn.lock index 942cdb79e518..1587371a1686 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2857,6 +2857,7 @@ __metadata: graceful-fs: ^4.2.9 jest-haste-map: ^28.0.0-alpha.6 jest-runtime: ^28.0.0-alpha.6 + slash: ^3.0.0 languageName: unknown linkType: soft From 74b1a0c2f8ab94bb4b73d0569cd8f1b04e7030bb Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sat, 5 Mar 2022 19:44:05 +0100 Subject: [PATCH 45/53] localeCompare, not number conversion --- e2e/__tests__/shard.test.ts | 4 ++-- packages/jest-test-sequencer/src/index.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/e2e/__tests__/shard.test.ts b/e2e/__tests__/shard.test.ts index 9747d00ef61f..568edc9815ef 100644 --- a/e2e/__tests__/shard.test.ts +++ b/e2e/__tests__/shard.test.ts @@ -28,7 +28,7 @@ test('--shard=1/2', () => { .map(file => path.basename(file)) .sort(); - expect(paths).toEqual(['2.test.js', '3.test.js']); + expect(paths).toEqual(['1.test.js', '3.test.js']); }); test('--shard=2/2', () => { @@ -39,7 +39,7 @@ test('--shard=2/2', () => { .filter(Boolean) .map(file => path.basename(file)); - expect(paths).toEqual(['1.test.js']); + expect(paths).toEqual(['2.test.js']); }); test('--shard=4/4', () => { diff --git a/packages/jest-test-sequencer/src/index.ts b/packages/jest-test-sequencer/src/index.ts index 55faadd4abae..7a197ada6040 100644 --- a/packages/jest-test-sequencer/src/index.ts +++ b/packages/jest-test-sequencer/src/index.ts @@ -117,7 +117,7 @@ export default class TestSequencer { test, }; }) - .sort((a, b) => (a.hash > b.hash ? -1 : 1)) + .sort((a, b) => a.hash.localeCompare(b.hash)) .slice(shardStart, shardEnd) .map(result => result.test); } From 798fd40db505ad186d6845f793c9f47229bcf927 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sat, 5 Mar 2022 19:55:35 +0100 Subject: [PATCH 46/53] unit test --- packages/jest-test-sequencer/package.json | 1 + ...quencer.test.js => test_sequencer.test.ts} | 65 ++++++++++--------- packages/jest-test-sequencer/tsconfig.json | 3 +- yarn.lock | 1 + 4 files changed, 37 insertions(+), 33 deletions(-) rename packages/jest-test-sequencer/src/__tests__/{test_sequencer.test.js => test_sequencer.test.ts} (86%) diff --git a/packages/jest-test-sequencer/package.json b/packages/jest-test-sequencer/package.json index 1027e340003e..353817e074a4 100644 --- a/packages/jest-test-sequencer/package.json +++ b/packages/jest-test-sequencer/package.json @@ -24,6 +24,7 @@ "slash": "^3.0.0" }, "devDependencies": { + "@jest/test-utils": "^28.0.0-alpha.6", "@types/graceful-fs": "^4.1.3" }, "engines": { diff --git a/packages/jest-test-sequencer/src/__tests__/test_sequencer.test.js b/packages/jest-test-sequencer/src/__tests__/test_sequencer.test.ts similarity index 86% rename from packages/jest-test-sequencer/src/__tests__/test_sequencer.test.js rename to packages/jest-test-sequencer/src/__tests__/test_sequencer.test.ts index e323dec692b1..569a88a23fb9 100644 --- a/packages/jest-test-sequencer/src/__tests__/test_sequencer.test.js +++ b/packages/jest-test-sequencer/src/__tests__/test_sequencer.test.ts @@ -6,7 +6,10 @@ */ import * as path from 'path'; -import * as fs from 'graceful-fs'; +import * as mockedFs from 'graceful-fs'; +import type {Test} from '@jest/test-result'; +import {makeProjectConfig} from '@jest/test-utils'; +import type {Context} from 'jest-runtime'; import TestSequencer from '../index'; jest.mock('graceful-fs', () => ({ @@ -17,34 +20,36 @@ jest.mock('graceful-fs', () => ({ const FAIL = 0; const SUCCESS = 1; -let sequencer; +let sequencer: TestSequencer; -const context = { - config: { +const fs = jest.mocked(mockedFs); + +const context: Context = { + config: makeProjectConfig({ cache: true, cacheDirectory: '/cache', haste: {}, name: 'test', - }, + }), hasteFS: { getSize: path => path.length, }, }; -const secondContext = { - config: { +const secondContext: Context = { + config: makeProjectConfig({ cache: true, cacheDirectory: '/cache2', haste: {}, name: 'test2', - }, + }), hasteFS: { getSize: path => path.length, }, }; -const toTests = paths => - paths.map(path => ({ +const toTests = (paths: Array) => + paths.map(path => ({ context, duration: undefined, path, @@ -125,13 +130,13 @@ test('sorts based on failures, timing information and file size', () => { ]); }); -test('writes the cache based on results without existing cache', () => { +test('writes the cache based on results without existing cache', async () => { fs.readFileSync.mockImplementationOnce(() => { throw new Error('File does not exist.'); }); const testPaths = ['/test-a.js', '/test-b.js', '/test-c.js']; - const tests = sequencer.sort(toTests(testPaths)); + const tests = await sequencer.sort(toTests(testPaths)); sequencer.cacheResults(tests, { testResults: [ { @@ -179,7 +184,7 @@ test('returns failed tests in sorted order', () => { ]); }); -test('writes the cache based on the results', () => { +test('writes the cache based on the results', async () => { fs.readFileSync.mockImplementationOnce(() => JSON.stringify({ '/test-a.js': [SUCCESS, 5], @@ -189,7 +194,7 @@ test('writes the cache based on the results', () => { ); const testPaths = ['/test-a.js', '/test-b.js', '/test-c.js']; - const tests = sequencer.sort(toTests(testPaths)); + const tests = await sequencer.sort(toTests(testPaths)); sequencer.cacheResults(tests, { testResults: [ { @@ -223,7 +228,7 @@ test('writes the cache based on the results', () => { }); }); -test('works with multiple contexts', () => { +test('works with multiple contexts', async () => { fs.readFileSync.mockImplementationOnce(cacheName => cacheName.startsWith(`${path.sep}cache${path.sep}`) ? JSON.stringify({ @@ -240,7 +245,7 @@ test('works with multiple contexts', () => { {context, duration: null, path: '/test-b.js'}, {context: secondContext, duration: null, path: '/test-c.js'}, ]; - const tests = sequencer.sort(testPaths); + const tests = await sequencer.sort(testPaths); sequencer.cacheResults(tests, { testResults: [ { @@ -277,8 +282,8 @@ test('works with multiple contexts', () => { }); }); -test('does not shard by default', () => { - const tests = sequencer.shard(toTests(['/test-a.js', '/test-ab.js']), { +test('does not shard by default', async () => { + const tests = await sequencer.shard(toTests(['/test-a.js', '/test-ab.js']), { shardCount: 1, shardIndex: 1, }); @@ -286,27 +291,23 @@ test('does not shard by default', () => { expect(tests.map(test => test.path)).toEqual(['/test-ab.js', '/test-a.js']); }); -test('return first shard', () => { - const tests = sequencer.shard( +test('return first shard', async () => { + const tests = await sequencer.shard( toTests(['/test-a.js', '/test-abc.js', '/test-ab.js']), { - shardCount: 2, + shardCount: 3, shardIndex: 1, }, ); - console.log( - '1/2', - tests.map(test => test.path), - ); - expect(tests.map(test => test.path)).toEqual(['/test-ab.js', '/test-a.js']); + expect(tests.map(test => test.path)).toEqual(['/test-ab.js']); }); -test('return second shard', () => { - const tests = sequencer.shard( +test('return second shard', async () => { + const tests = await sequencer.shard( toTests(['/test-a.js', '/test-abc.js', '/test-ab.js']), { - shardCount: 2, + shardCount: 3, shardIndex: 2, }, ); @@ -314,8 +315,8 @@ test('return second shard', () => { expect(tests.map(test => test.path)).toEqual(['/test-abc.js']); }); -test('return third shard', () => { - const tests = sequencer.shard( +test('return third shard', async () => { + const tests = await sequencer.shard( toTests(['/test-abc.js', '/test-a.js', '/test-ab.js']), { shardCount: 3, @@ -323,7 +324,7 @@ test('return third shard', () => { }, ); - expect(tests.map(test => test.path)).toEqual(['/test-abc.js']); + expect(tests.map(test => test.path)).toEqual(['/test-a.js']); }); test('returns expected 100/10 shards', () => { diff --git a/packages/jest-test-sequencer/tsconfig.json b/packages/jest-test-sequencer/tsconfig.json index afb827643b6e..42d9cb3d4f42 100644 --- a/packages/jest-test-sequencer/tsconfig.json +++ b/packages/jest-test-sequencer/tsconfig.json @@ -9,6 +9,7 @@ "references": [ {"path": "../jest-haste-map"}, {"path": "../jest-runtime"}, - {"path": "../jest-test-result"} + {"path": "../jest-test-result"}, + {"path": "../jest-test-utils"} ] } diff --git a/yarn.lock b/yarn.lock index 1587371a1686..7ecfa4b2d8be 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2853,6 +2853,7 @@ __metadata: resolution: "@jest/test-sequencer@workspace:packages/jest-test-sequencer" dependencies: "@jest/test-result": ^28.0.0-alpha.6 + "@jest/test-utils": ^28.0.0-alpha.6 "@types/graceful-fs": ^4.1.3 graceful-fs: ^4.2.9 jest-haste-map: ^28.0.0-alpha.6 From 128047ff2550c3b7838c6d32dcf3e2f3d3a2bdd3 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sat, 5 Mar 2022 19:58:12 +0100 Subject: [PATCH 47/53] type errors in test --- .../src/__tests__/test_sequencer.test.ts | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/packages/jest-test-sequencer/src/__tests__/test_sequencer.test.ts b/packages/jest-test-sequencer/src/__tests__/test_sequencer.test.ts index 569a88a23fb9..7d7a937ab5a9 100644 --- a/packages/jest-test-sequencer/src/__tests__/test_sequencer.test.ts +++ b/packages/jest-test-sequencer/src/__tests__/test_sequencer.test.ts @@ -327,14 +327,16 @@ test('return third shard', async () => { expect(tests.map(test => test.path)).toEqual(['/test-a.js']); }); -test('returns expected 100/10 shards', () => { +test('returns expected 100/10 shards', async () => { const allTests = toTests(new Array(100).fill(true).map((_, i) => `/${i}.js`)); - const shards = new Array(10).fill(true).map((_, i) => - sequencer.shard(allTests, { - shardCount: 10, - shardIndex: i + 1, - }), + const shards = await Promise.all( + new Array(10).fill(true).map((_, i) => + sequencer.shard(allTests, { + shardCount: 10, + shardIndex: i + 1, + }), + ), ); expect(shards.map(shard => shard.length)).toEqual([ @@ -342,14 +344,16 @@ test('returns expected 100/10 shards', () => { ]); }); -test('returns expected 100/8 shards', () => { +test('returns expected 100/8 shards', async () => { const allTests = toTests(new Array(100).fill(true).map((_, i) => `/${i}.js`)); - const shards = new Array(8).fill(true).map((_, i) => - sequencer.shard(allTests, { - shardCount: 8, - shardIndex: i + 1, - }), + const shards = await Promise.all( + new Array(8).fill(true).map((_, i) => + sequencer.shard(allTests, { + shardCount: 8, + shardIndex: i + 1, + }), + ), ); expect(shards.map(shard => shard.length)).toEqual([ From 6facf864f2818b95adb996dc5030a620e60bdadb Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sat, 5 Mar 2022 20:06:02 +0100 Subject: [PATCH 48/53] oops --- packages/jest-test-sequencer/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/jest-test-sequencer/tsconfig.json b/packages/jest-test-sequencer/tsconfig.json index 42d9cb3d4f42..70822da7c647 100644 --- a/packages/jest-test-sequencer/tsconfig.json +++ b/packages/jest-test-sequencer/tsconfig.json @@ -10,6 +10,6 @@ {"path": "../jest-haste-map"}, {"path": "../jest-runtime"}, {"path": "../jest-test-result"}, - {"path": "../jest-test-utils"} + {"path": "../test-utils"} ] } From be178735cdc247f860d5eb323dcd49ab29bd95be Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sat, 5 Mar 2022 20:15:49 +0100 Subject: [PATCH 49/53] move docs and mention in troubleshooting --- docs/CLI.md | 12 +++++++++--- docs/Configuration.md | 12 ------------ docs/Troubleshooting.md | 12 ++++++++++++ 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/docs/CLI.md b/docs/CLI.md index 5f2d17156b7f..d1cc0aecd2ae 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -340,7 +340,15 @@ A list of paths to modules that run some code to configure or to set up the test ### `--shard` -Shard suite to execute them on multiple machines. For example, to split the suite into three shards, each running one third of the tests: +The test suite shard to execute in a format of `(?\d+)/(?\d+)`. + +`shardIndex` describes which shard to select while `shardCount` controls the number of shards the suite should be split into. + +`shardIndex` and `shardCount` have to be 1-based, positive numbers, and `shardIndex` has to be lower than or equal to `shardCount`. + +When `shard` is specified the configured [`testSquencer`](Configuration.md#testsequencer-string) has to implement a `shard` method. + +For example, to split the suite into three shards, each running one third of the tests: ``` jest --shard=1/3 @@ -348,8 +356,6 @@ jest --shard=2/3 jest --shard=3/3 ``` -Please refer to [shard configuration](Configuration.md#shard). - ### `--showConfig` Print your Jest config and then exits. diff --git a/docs/Configuration.md b/docs/Configuration.md index 5bb098fe0ed1..038a2917e832 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -957,18 +957,6 @@ Example `jest.setup.js` file jest.setTimeout(10000); // in milliseconds ``` -### `shard` \[string] - -The test suite shard to execute in a format of `(?\d+)/(?\d+)`. - -`shardIndex` describes which shard to select while `shardCount` controls the number of shards the suite should be split into. - -`shardIndex` and `shardCount` have to be 1-based, positive numbers, and `shardIndex` has to be lower than or equal to `shardCount`. - -When `shard` is specified the configured [`testSquencer`](#testsequencer-string) implementation has to implement a `shard` method. - -Refer to [`testSquencer`](#testsequencer-string) on how to override the default sharding implementation. - ### `slowTestThreshold` \[number] Default: `5` diff --git a/docs/Troubleshooting.md b/docs/Troubleshooting.md index 1ecee62f5936..5c9782875500 100644 --- a/docs/Troubleshooting.md +++ b/docs/Troubleshooting.md @@ -182,6 +182,18 @@ jest --maxWorkers=4 yarn test --maxWorkers=4 ``` +If you use GitHub Actions, you can use [`github-actions-cpu-cores`](https://github.com/SimenB/github-actions-cpu-cores) to detect number of CPUs, and pass that to Jest. + +```yaml +- name: Get number of CPU cores + id: cpu-cores + uses: SimenB/github-actions-cpu-cores@v1 +- name: run tests + run: yarn jest --max-workers ${{ steps.cpu-cores.outputs.count }} +``` + +Another thing you can do is use the [`shard`](CLI.md#--shard) flag to parallelize the test run across multiple machines. + ## `coveragePathIgnorePatterns` seems to not have any effect. Make sure you are not using the `babel-plugin-istanbul` plugin. Jest wraps Istanbul, and therefore also tells Istanbul what files to instrument with coverage collection. When using `babel-plugin-istanbul`, every file that is processed by Babel will have coverage collection code, hence it is not being ignored by `coveragePathIgnorePatterns`. From 70d073f912484d71d45cf9bf22505c072a57c091 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sat, 5 Mar 2022 20:34:06 +0100 Subject: [PATCH 50/53] maybe? --- packages/jest-test-sequencer/src/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/jest-test-sequencer/src/index.ts b/packages/jest-test-sequencer/src/index.ts index 7a197ada6040..956d7c46775d 100644 --- a/packages/jest-test-sequencer/src/index.ts +++ b/packages/jest-test-sequencer/src/index.ts @@ -104,15 +104,15 @@ export default class TestSequencer { return tests .map(test => { - const relativeTestPath = path.relative( - test.context.config.rootDir, - test.path, + const relativeTestPath = path.posix.relative( + slash(test.context.config.rootDir), + slash(test.path), ); return { hash: crypto .createHash('sha1') - .update(slash(relativeTestPath)) + .update(relativeTestPath) .digest('hex'), test, }; From 37488088fdaaef2a0fea5595068c06802402a8d6 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sun, 6 Mar 2022 00:58:38 +0100 Subject: [PATCH 51/53] compare manually --- packages/jest-test-sequencer/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/jest-test-sequencer/src/index.ts b/packages/jest-test-sequencer/src/index.ts index 956d7c46775d..4b103f8a7725 100644 --- a/packages/jest-test-sequencer/src/index.ts +++ b/packages/jest-test-sequencer/src/index.ts @@ -117,7 +117,7 @@ export default class TestSequencer { test, }; }) - .sort((a, b) => a.hash.localeCompare(b.hash)) + .sort((a, b) => (a.hash < b.hash ? -1 : a.hash > b.hash ? 1 : 0)) .slice(shardStart, shardEnd) .map(result => result.test); } From 75014e93a7902dbe56c8c2823d889bda3a3d7c82 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sun, 6 Mar 2022 09:31:20 +0100 Subject: [PATCH 52/53] maybe --- e2e/shard/sharding-test-sequencer.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/e2e/shard/sharding-test-sequencer.js b/e2e/shard/sharding-test-sequencer.js index ea271e930e77..f3c38ba8f008 100644 --- a/e2e/shard/sharding-test-sequencer.js +++ b/e2e/shard/sharding-test-sequencer.js @@ -6,7 +6,11 @@ */ module.exports = class NoShardingSequencer { shard(tests) { - return [tests[2]]; + return [ + Array.from(tests).sort((a, b) => + a.path < b.path ? -1 : a.path > b.path ? 1 : 0, + )[2], + ]; } sort(tests) { return tests; From a2bb7295eac46c2fc47ff26b8b53f95b8f232ff8 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sun, 6 Mar 2022 09:44:43 +0100 Subject: [PATCH 53/53] shard coverage run --- .github/workflows/nodejs.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index cb8c811d762e..7fdbb93b150c 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -134,7 +134,11 @@ jobs: run: yarn jest-jasmine-ci --max-workers ${{ steps.cpu-cores.outputs.count }} --shard=${{ matrix.shard }} test-coverage: - name: Node LTS on Ubuntu with coverage + name: Node LTS on Ubuntu with coverage (${{ matrix.shard }}) + strategy: + fail-fast: false + matrix: + shard: ['1/4', '2/4', '3/4', '4/4'] runs-on: ubuntu-latest needs: prepare-yarn-cache @@ -154,7 +158,7 @@ jobs: uses: SimenB/github-actions-cpu-cores@v1 - name: run tests with coverage run: | - yarn jest-coverage --color --config jest.config.ci.js --max-workers ${{ steps.cpu-cores.outputs.count }} + yarn jest-coverage --color --config jest.config.ci.js --max-workers ${{ steps.cpu-cores.outputs.count }} --shard=${{ matrix.shard }} yarn test-leak - name: map coverage run: node ./scripts/mapCoverage.js