diff --git a/CHANGELOG.md b/CHANGELOG.md index fabc33277c85..023b5e5831ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,11 @@ - `[jest-cli]` Improve chai support (with detailed output, to match jest exceptions) ([#8454](https://github.com/facebook/jest/pull/8454)) - `[*]` Manage the global timeout with `--testTimeout` command line argument. ([#8456](https://github.com/facebook/jest/pull/8456)) - `[pretty-format]` Render custom displayName of memoized components +- `[jest-validate]` Allow `maxWorkers` as part of the `jest.config.js` ([#8565](https://github.com/facebook/jest/pull/8565)) ### Fixes +- `[jest-cli]` Allow `--maxWorkers` to work with % input again ([#8565](https://github.com/facebook/jest/pull/8565)) - `[babel-plugin-jest-hoist]` Expand list of whitelisted globals in global mocks ([#8429](https://github.com/facebook/jest/pull/8429) - `[jest-core]` Make watch plugin initialization errors look nice ([#8422](https://github.com/facebook/jest/pull/8422)) - `[jest-snapshot]` Prevent inline snapshots from drifting when inline snapshots are updated ([#8492](https://github.com/facebook/jest/pull/8492)) diff --git a/packages/jest-cli/src/__tests__/cli/args.test.ts b/packages/jest-cli/src/__tests__/cli/args.test.ts index 8b1c3a291637..dfe1c80b4f02 100644 --- a/packages/jest-cli/src/__tests__/cli/args.test.ts +++ b/packages/jest-cli/src/__tests__/cli/args.test.ts @@ -50,10 +50,15 @@ describe('check', () => { it('raises an exception if maxWorkers is specified with no number', () => { const argv = ({maxWorkers: undefined} as unknown) as Config.Argv; expect(() => check(argv)).toThrow( - 'The --maxWorkers (-w) option requires a number to be specified', + '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(); + }); + it('raises an exception if config is not a valid JSON string', () => { const argv = {config: 'x:1'} as Config.Argv; expect(() => check(argv)).toThrow( diff --git a/packages/jest-cli/src/cli/args.ts b/packages/jest-cli/src/cli/args.ts index 7bba9a038621..2ac8d63fd529 100644 --- a/packages/jest-cli/src/cli/args.ts +++ b/packages/jest-cli/src/cli/args.ts @@ -42,8 +42,9 @@ export const check = (argv: Config.Argv) => { if (argv.hasOwnProperty('maxWorkers') && argv.maxWorkers === undefined) { throw new Error( - 'The --maxWorkers (-w) option requires a number to be specified.\n' + + 'The --maxWorkers (-w) option requires a number or string to be specified.\n' + 'Example usage: jest --maxWorkers 2\n' + + 'Example usage: jest --maxWorkers 50%\n' + 'Or did you mean --watch?', ); } @@ -349,7 +350,7 @@ export const options = { 'will spawn for running tests. This defaults to the number of the ' + 'cores available on your machine. (its usually best not to override ' + 'this default)', - type: 'number' as 'number', + type: 'string' as 'string', }, moduleDirectories: { description: diff --git a/packages/jest-cli/src/init/__tests__/__snapshots__/init.test.js.snap b/packages/jest-cli/src/init/__tests__/__snapshots__/init.test.js.snap index 9c645a4a956a..768ccc23e640 100644 --- a/packages/jest-cli/src/init/__tests__/__snapshots__/init.test.js.snap +++ b/packages/jest-cli/src/init/__tests__/__snapshots__/init.test.js.snap @@ -81,6 +81,9 @@ module.exports = { // A set of global variables that need to be available in all test environments // globals: {}, + // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. + // maxWorkers: \\"50%\\", + // An array of directory names to be searched recursively up from the requiring module's location // moduleDirectories: [ // \\"node_modules\\" diff --git a/packages/jest-config/src/Defaults.ts b/packages/jest-config/src/Defaults.ts index 4058e7a59f18..26c40f2f912f 100644 --- a/packages/jest-config/src/Defaults.ts +++ b/packages/jest-config/src/Defaults.ts @@ -40,6 +40,7 @@ const defaultOptions: Config.DefaultOptions = { throwOnModuleCollision: false, }, maxConcurrency: 5, + maxWorkers: '50%', moduleDirectories: ['node_modules'], moduleFileExtensions: ['js', 'json', 'jsx', 'ts', 'tsx', 'node'], moduleNameMapper: {}, diff --git a/packages/jest-config/src/Descriptions.ts b/packages/jest-config/src/Descriptions.ts index e9088b6849e3..3ff6d9b38b11 100644 --- a/packages/jest-config/src/Descriptions.ts +++ b/packages/jest-config/src/Descriptions.ts @@ -37,6 +37,8 @@ const descriptions: {[key in keyof Config.InitialOptions]: string} = { 'A path to a module which exports an async function that is triggered once after all test suites', globals: 'A set of global variables that need to be available in all test environments', + maxWorkers: + 'The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.', moduleDirectories: "An array of directory names to be searched recursively up from the requiring module's location", moduleFileExtensions: 'An array of file extensions your modules use', diff --git a/packages/jest-config/src/ValidConfig.ts b/packages/jest-config/src/ValidConfig.ts index ee69c72b178f..15676c43254c 100644 --- a/packages/jest-config/src/ValidConfig.ts +++ b/packages/jest-config/src/ValidConfig.ts @@ -63,6 +63,7 @@ const initialOptions: Config.InitialOptions = { lastCommit: false, logHeapUsage: true, maxConcurrency: 5, + maxWorkers: '50%', moduleDirectories: ['node_modules'], moduleFileExtensions: ['js', 'json', 'jsx', 'ts', 'tsx', 'node'], moduleLoader: '', diff --git a/packages/jest-config/src/getMaxWorkers.ts b/packages/jest-config/src/getMaxWorkers.ts index bd1adf1cecc5..d1d0a2c3a6a8 100644 --- a/packages/jest-config/src/getMaxWorkers.ts +++ b/packages/jest-config/src/getMaxWorkers.ts @@ -10,29 +10,34 @@ import {Config} from '@jest/types'; export default function getMaxWorkers( argv: Partial>, + defaultOptions?: Partial>, ): number { if (argv.runInBand) { return 1; } else if (argv.maxWorkers) { - // TODO: How to type this properly? Should probably use `coerce` from `yargs` - const maxWorkers = argv.maxWorkers; - const parsed = parseInt(maxWorkers as string, 10); - - if ( - typeof maxWorkers === 'string' && - maxWorkers.trim().endsWith('%') && - parsed > 0 && - parsed <= 100 - ) { - const cpus = os.cpus().length; - const workers = Math.floor((parsed / 100) * cpus); - return workers >= 1 ? workers : 1; - } - - return parsed > 0 ? parsed : 1; + return parseWorkers(argv.maxWorkers); + } else if (defaultOptions && defaultOptions.maxWorkers) { + return parseWorkers(defaultOptions.maxWorkers); } else { // In watch mode, Jest should be unobtrusive and not use all available CPUs. const cpus = os.cpus() ? os.cpus().length : 1; return Math.max(argv.watch ? Math.floor(cpus / 2) : cpus - 1, 1); } } + +const parseWorkers = (maxWorkers: string | number): number => { + const parsed = parseInt(maxWorkers.toString(), 10); + + if ( + typeof maxWorkers === 'string' && + maxWorkers.trim().endsWith('%') && + parsed > 0 && + parsed <= 100 + ) { + const cpus = os.cpus().length; + const workers = Math.floor((parsed / 100) * cpus); + return workers >= 1 ? workers : 1; + } + + return parsed > 0 ? parsed : 1; +}; diff --git a/packages/jest-config/src/normalize.ts b/packages/jest-config/src/normalize.ts index d31047ac538d..cf429d39862e 100644 --- a/packages/jest-config/src/normalize.ts +++ b/packages/jest-config/src/normalize.ts @@ -928,7 +928,7 @@ export default function normalize( (newOptions.maxConcurrency as unknown) as string, 10, ); - newOptions.maxWorkers = getMaxWorkers(argv); + newOptions.maxWorkers = getMaxWorkers(argv, options); if (newOptions.testRegex!.length && options.testMatch) { throw createConfigError( diff --git a/packages/jest-types/src/Config.ts b/packages/jest-types/src/Config.ts index 00a78f0bd1ca..1bd054592c49 100644 --- a/packages/jest-types/src/Config.ts +++ b/packages/jest-types/src/Config.ts @@ -55,6 +55,7 @@ export type DefaultOptions = { globalSetup: string | null | undefined; globalTeardown: string | null | undefined; haste: HasteConfig; + maxWorkers: number | string; maxConcurrency: number; moduleDirectories: Array; moduleFileExtensions: Array; @@ -156,6 +157,7 @@ export type InitialOptions = { listTests?: boolean; mapCoverage?: boolean; maxConcurrency?: number; + maxWorkers: number | string; moduleDirectories?: Array; moduleFileExtensions?: Array; moduleLoader?: Path; diff --git a/packages/jest-validate/src/validate.ts b/packages/jest-validate/src/validate.ts index c3fd9d211f17..6a01ba8b7a9d 100644 --- a/packages/jest-validate/src/validate.ts +++ b/packages/jest-validate/src/validate.ts @@ -6,8 +6,8 @@ */ import {ValidationOptions} from './types'; - import defaultConfig from './defaultConfig'; +import {ValidationError} from './utils'; let hasDeprecationWarnings = false; @@ -46,6 +46,21 @@ const _validate = ( ); hasDeprecationWarnings = hasDeprecationWarnings || isDeprecatedKey; + } else if (allowsMultipleTypes(key)) { + const value = config[key]; + + if ( + typeof options.condition === 'function' && + typeof options.error === 'function' + ) { + if (key === 'maxWorkers' && !isOfTypeStringOrNumber(value)) { + throw new ValidationError( + 'Validation Error', + `${key} has to be of type string or number`, + `maxWorkers=50% or\nmaxWorkers=3`, + ); + } + } } else if (Object.hasOwnProperty.call(exampleConfig, key)) { if ( typeof options.condition === 'function' && @@ -76,6 +91,10 @@ const _validate = ( return {hasDeprecationWarnings}; }; +const allowsMultipleTypes = (key: string): boolean => key === 'maxWorkers'; +const isOfTypeStringOrNumber = (value: any): boolean => + typeof value === 'number' || typeof value === 'string'; + const validate = (config: Record, options: ValidationOptions) => { hasDeprecationWarnings = false;