From a15fe50e7bc952e57f16619fcb550d8c78fa7aa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Greffier?= Date: Wed, 21 Sep 2022 20:17:36 +0200 Subject: [PATCH] feat(test runner): workers as percentage (#17400) Allows to set workers as a percentage of logical CPUs, for example "50%". Examples : ```bash npx playwright test --workers 3 npx playwright test --workers 50% ``` ```js const config: PlaywrightTestConfig = { // ... workers: '33%', } ``` --- docs/src/test-api/class-testconfig.md | 6 ++-- docs/src/test-configuration-js.md | 2 +- packages/playwright-test/src/cli.ts | 4 +-- packages/playwright-test/src/loader.ts | 24 +++++++++++----- packages/playwright-test/types/test.d.ts | 16 ++++++----- tests/playwright-test/config.spec.ts | 35 ++++++++++++++++++++++++ 6 files changed, 67 insertions(+), 20 deletions(-) diff --git a/docs/src/test-api/class-testconfig.md b/docs/src/test-api/class-testconfig.md index 067ab6a76c..33f2c91bdf 100644 --- a/docs/src/test-api/class-testconfig.md +++ b/docs/src/test-api/class-testconfig.md @@ -791,13 +791,13 @@ module.exports = config; ## property: TestConfig.workers * since: v1.10 -- type: ?<[int]> +- type: ?<[int]|[string]> -The maximum number of concurrent worker processes to use for parallelizing tests. +The maximum number of concurrent worker processes to use for parallelizing tests. Can also be set as percentage of logical CPU cores, e.g. `'50%'.` Playwright Test uses worker processes to run tests. There is always at least one worker process, but more can be used to speed up test execution. -Defaults to one half of the number of CPU cores. Learn more about [parallelism and sharding](../test-parallel.md) with Playwright Test. +Defaults to half of the number of logical CPU cores. Learn more about [parallelism and sharding](../test-parallel.md) with Playwright Test. ```js tab=js-js // playwright.config.js diff --git a/docs/src/test-configuration-js.md b/docs/src/test-configuration-js.md index 6016dfbf5e..efef9c4403 100644 --- a/docs/src/test-configuration-js.md +++ b/docs/src/test-configuration-js.md @@ -516,7 +516,7 @@ In addition to configuring [Browser] or [BrowserContext], videos or screenshots, - `testMatch`: Glob patterns or regular expressions that match test files. For example, `'**/todo-tests/*.spec.ts'`. By default, Playwright Test runs `.*(test|spec)\.(js|ts|mjs)` files. - `timeout`: Time in milliseconds given to each test. Learn more about [various timeouts](./test-timeouts.md). - `webServer: { command: string, port?: number, url?: string, ignoreHTTPSErrors?: boolean, timeout?: number, reuseExistingServer?: boolean, cwd?: string, env?: object }` - Launch a process and wait that it's ready before the tests will start. See [launch web server](./test-advanced.md#launching-a-development-web-server-during-the-tests) configuration for examples. -- `workers`: The maximum number of concurrent worker processes to use for parallelizing tests. +- `workers`: The maximum number of concurrent worker processes to use for parallelizing tests. Can also be set as percentage of logical CPU cores, e.g. `'50%'.` You can specify these options in the configuration file. Note that testing options are **top-level**, do not put them into the `use` section. diff --git a/packages/playwright-test/src/cli.ts b/packages/playwright-test/src/cli.ts index dd59b687d1..4d8a7832fa 100644 --- a/packages/playwright-test/src/cli.ts +++ b/packages/playwright-test/src/cli.ts @@ -100,7 +100,7 @@ function addTestCommand(program: Command) { command.option('-gv, --grep-invert ', `Only run tests that do not match this regular expression`); command.option('--global-timeout ', `Maximum time this test suite can run in milliseconds (default: unlimited)`); command.option('--ignore-snapshots', `Ignore screenshot and snapshot expectations`); - command.option('-j, --workers ', `Number of concurrent workers, use 1 to run in a single worker (default: number of CPU cores / 2)`); + command.option('-j, --workers ', `Number of concurrent workers or percentage of logical CPU cores, use 1 to run in a single worker (default: 50%)`); command.option('--list', `Collect all the tests and report them, but do not run`); command.option('--max-failures ', `Stop after the first N failures`); command.option('--output ', `Folder for output artifacts (default: "test-results")`); @@ -272,7 +272,7 @@ function overridesFromOptions(options: { [key: string]: any }): ConfigCLIOverrid timeout: options.timeout ? parseInt(options.timeout, 10) : undefined, ignoreSnapshots: options.ignoreSnapshots ? !!options.ignoreSnapshots : undefined, updateSnapshots: options.updateSnapshots ? 'all' as const : undefined, - workers: options.workers ? parseInt(options.workers, 10) : undefined, + workers: options.workers, }; } diff --git a/packages/playwright-test/src/loader.ts b/packages/playwright-test/src/loader.ts index ae4caa79c8..a3ab9bc222 100644 --- a/packages/playwright-test/src/loader.ts +++ b/packages/playwright-test/src/loader.ts @@ -29,7 +29,6 @@ import type { Reporter } from '../types/testReporter'; import { builtInReporters } from './runner'; import { isRegExp, calculateSha1 } from 'playwright-core/lib/utils'; import { serializeError } from './util'; -import { hostPlatform } from 'playwright-core/lib/utils/hostPlatform'; import { FixturePool, isFixtureOption } from './fixtures'; import type { TestTypeImpl } from './testType'; @@ -143,7 +142,19 @@ export class Loader { this._fullConfig.shard = takeFirst(config.shard, baseFullConfig.shard); this._fullConfig._ignoreSnapshots = takeFirst(config.ignoreSnapshots, baseFullConfig._ignoreSnapshots); this._fullConfig.updateSnapshots = takeFirst(config.updateSnapshots, baseFullConfig.updateSnapshots); - this._fullConfig.workers = takeFirst(config.workers, baseFullConfig.workers); + + const workers = takeFirst(config.workers, '50%'); + if (typeof workers === 'string') { + if (workers.endsWith('%')) { + const cpus = os.cpus().length; + this._fullConfig.workers = Math.max(1, Math.floor(cpus * (parseInt(workers, 10) / 100))); + } else { + this._fullConfig.workers = parseInt(workers, 10); + } + } else { + this._fullConfig.workers = workers; + } + const webServers = takeFirst(config.webServer, baseFullConfig.webServer); if (Array.isArray(webServers)) { // multiple web server mode // Due to previous choices, this value shows up to the user in globalSetup as part of FullConfig. Arrays are not supported by the old type. @@ -572,8 +583,10 @@ function validateConfig(file: string, config: Config) { } if ('workers' in config && config.workers !== undefined) { - if (typeof config.workers !== 'number' || config.workers <= 0) + if (typeof config.workers === 'number' && config.workers <= 0) throw errorWithFile(file, `config.workers must be a positive number`); + else if (typeof config.workers === 'string' && !config.workers.endsWith('%')) + throw errorWithFile(file, `config.workers must be a number or percentage`); } } @@ -631,9 +644,6 @@ function validateProject(file: string, project: Project, title: string) { } } -const cpus = os.cpus().length; -const workers = hostPlatform.startsWith('mac') && hostPlatform.endsWith('arm64') ? cpus : Math.ceil(cpus / 2); - export const baseFullConfig: FullConfigInternal = { forbidOnly: false, fullyParallel: false, @@ -654,7 +664,7 @@ export const baseFullConfig: FullConfigInternal = { shard: null, updateSnapshots: 'missing', version: require('../package.json').version, - workers, + workers: 0, webServer: null, _watchMode: false, _webServers: [], diff --git a/packages/playwright-test/types/test.d.ts b/packages/playwright-test/types/test.d.ts index 556933d456..de43cab5c7 100644 --- a/packages/playwright-test/types/test.d.ts +++ b/packages/playwright-test/types/test.d.ts @@ -936,13 +936,14 @@ interface TestConfig { updateSnapshots?: "all"|"none"|"missing"; /** - * The maximum number of concurrent worker processes to use for parallelizing tests. + * The maximum number of concurrent worker processes to use for parallelizing tests. Can also be set as percentage of + * logical CPU cores, e.g. `'50%'.` * * Playwright Test uses worker processes to run tests. There is always at least one worker process, but more can be used to * speed up test execution. * - * Defaults to one half of the number of CPU cores. Learn more about [parallelism and sharding](https://playwright.dev/docs/test-parallel) with - * Playwright Test. + * Defaults to half of the number of logical CPU cores. Learn more about [parallelism and sharding](https://playwright.dev/docs/test-parallel) + * with Playwright Test. * * ```js * // playwright.config.ts @@ -955,7 +956,7 @@ interface TestConfig { * ``` * */ - workers?: number;} + workers?: number|string;} /** * Playwright Test provides many options to configure how your tests are collected and executed, for example `timeout` or @@ -1210,13 +1211,14 @@ export interface FullConfig { */ updateSnapshots: 'all' | 'none' | 'missing'; /** - * The maximum number of concurrent worker processes to use for parallelizing tests. + * The maximum number of concurrent worker processes to use for parallelizing tests. Can also be set as percentage of + * logical CPU cores, e.g. `'50%'.` * * Playwright Test uses worker processes to run tests. There is always at least one worker process, but more can be used to * speed up test execution. * - * Defaults to one half of the number of CPU cores. Learn more about [parallelism and sharding](https://playwright.dev/docs/test-parallel) with - * Playwright Test. + * Defaults to half of the number of logical CPU cores. Learn more about [parallelism and sharding](https://playwright.dev/docs/test-parallel) + * with Playwright Test. * * ```js * // playwright.config.ts diff --git a/tests/playwright-test/config.spec.ts b/tests/playwright-test/config.spec.ts index a699168ee5..23082e0a60 100644 --- a/tests/playwright-test/config.spec.ts +++ b/tests/playwright-test/config.spec.ts @@ -393,6 +393,41 @@ test('should support ignoreSnapshots config option', async ({ runInlineTest }) = expect(result.passed).toBe(1); }); +test('should validate workers option set to percent', async ({ runInlineTest }, testInfo) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { + workers: '50%' + }; + `, + 'a.test.ts': ` + const { test } = pwt; + test('pass', async () => { + }); + ` + }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); +}); + +test('should throw when workers option is invalid', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { + workers: '' + }; + `, + 'a.test.ts': ` + const { test } = pwt; + test('pass', async () => { + }); + ` + }); + + expect(result.exitCode).toBe(1); + expect(result.output).toContain('config.workers must be a number or percentage'); +}); + test('should work with undefined values and base', async ({ runInlineTest }) => { const result = await runInlineTest({ 'playwright.config.ts': `