feat(test): introduce fully parallel mode (#12446)

This commit is contained in:
Pavel Feldman 2022-03-01 18:12:21 -08:00 committed by GitHub
parent 61a6cdde70
commit 4b19d59ec5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 113 additions and 1 deletions

View file

@ -102,6 +102,14 @@ const config: PlaywrightTestConfig = {
export default config; export default config;
``` ```
## property: TestConfig.fullyParallel
- type: <[boolean]>
Playwright Test runs tests in parallel. In order to achieve that, it runs several worker processes that run at the same time.
By default, **test files** are run in parallel. Tests in a single file are run in order, in the same worker process.
You can configure entire test run to concurrently execute all tests in all files using this option.
## property: TestConfig.globalSetup ## property: TestConfig.globalSetup
- type: <[string]> - type: <[string]>

View file

@ -116,6 +116,14 @@ Configuration for the `expect` assertion library.
Use [`property: TestConfig.expect`] to change this option for all projects. Use [`property: TestConfig.expect`] to change this option for all projects.
## property: TestProject.fullyParallel
- type: <[boolean]>
Playwright Test runs tests in parallel. In order to achieve that, it runs several worker processes that run at the same time.
By default, **test files** are run in parallel. Tests in a single file are run in order, in the same worker process.
You can configure entire test project to concurrently run all tests in all files using this option.
## property: TestProject.metadata ## property: TestProject.metadata
- type: <[Object]> - type: <[Object]>

View file

@ -6,7 +6,8 @@ title: "Parallelism and sharding"
Playwright Test runs tests in parallel. In order to achieve that, it runs several worker processes that run at the same time. Playwright Test runs tests in parallel. In order to achieve that, it runs several worker processes that run at the same time.
- By default, **test files** are run in parallel. Tests in a single file are run in order, in the same worker process. - By default, **test files** are run in parallel. Tests in a single file are run in order, in the same worker process.
- Group tests with [`test.describe.parallel`](#parallelize-tests-in-a-single-file) to run **tests in a single file** in parallel. - Configure tests using [`test.describe.configure`](#parallelize-tests-in-a-single-file) to run **tests in a single file** in parallel.
- You can configure entire project to have all tests in all files to run in parallel using [`property: TestProject.fullyParallel`] or [`property: TestConfig.fullyParallel`]
- To **disable** parallelism limit the number of [workers to one](#disable-parallelism). - To **disable** parallelism limit the number of [workers to one](#disable-parallelism).
You can control the number of [parallel worker processes](#limit-workers) and [limit the number of failures](#limit-failures-and-fail-fast) in the whole test suite for efficiency. You can control the number of [parallel worker processes](#limit-workers) and [limit the number of failures](#limit-failures-and-fail-fast) in the whole test suite for efficiency.

View file

@ -40,6 +40,7 @@ export function addTestCommand(program: Command) {
command.option('--debug', `Run tests with Playwright Inspector. Shortcut for "PWDEBUG=1" environment variable and "--timeout=0 --maxFailures=1 --headed --workers=1" options`); command.option('--debug', `Run tests with Playwright Inspector. Shortcut for "PWDEBUG=1" environment variable and "--timeout=0 --maxFailures=1 --headed --workers=1" options`);
command.option('-c, --config <file>', `Configuration file, or a test directory with optional ${kDefaultConfigFiles.map(file => `"${file}"`).join('/')}`); command.option('-c, --config <file>', `Configuration file, or a test directory with optional ${kDefaultConfigFiles.map(file => `"${file}"`).join('/')}`);
command.option('--forbid-only', `Fail if test.only is called (default: false)`); command.option('--forbid-only', `Fail if test.only is called (default: false)`);
command.option('--fully-parallel', `Run all tests in parallel (default: false)`);
command.option('-g, --grep <grep>', `Only run tests matching this regular expression (default: ".*")`); command.option('-g, --grep <grep>', `Only run tests matching this regular expression (default: ".*")`);
command.option('-gv, --grep-invert <grep>', `Only run tests that do not match this regular expression`); command.option('-gv, --grep-invert <grep>', `Only run tests that do not match this regular expression`);
command.option('--global-timeout <timeout>', `Maximum time this test suite can run in milliseconds (default: unlimited)`); command.option('--global-timeout <timeout>', `Maximum time this test suite can run in milliseconds (default: unlimited)`);
@ -196,6 +197,7 @@ function overridesFromOptions(options: { [key: string]: any }): Config {
const shardPair = options.shard ? options.shard.split('/').map((t: string) => parseInt(t, 10)) : undefined; const shardPair = options.shard ? options.shard.split('/').map((t: string) => parseInt(t, 10)) : undefined;
return { return {
forbidOnly: options.forbidOnly ? true : undefined, forbidOnly: options.forbidOnly ? true : undefined,
fullyParallel: options.fullyParallel ? true : undefined,
globalTimeout: options.globalTimeout ? parseInt(options.globalTimeout, 10) : undefined, globalTimeout: options.globalTimeout ? parseInt(options.globalTimeout, 10) : undefined,
grep: options.grep ? forceRegExp(options.grep) : undefined, grep: options.grep ? forceRegExp(options.grep) : undefined,
grepInvert: options.grepInvert ? forceRegExp(options.grepInvert) : undefined, grepInvert: options.grepInvert ? forceRegExp(options.grepInvert) : undefined,

View file

@ -93,6 +93,7 @@ export class Loader {
this._fullConfig.rootDir = this._config.testDir || rootDir; this._fullConfig.rootDir = this._config.testDir || rootDir;
this._fullConfig.forbidOnly = takeFirst(this._configOverrides.forbidOnly, this._config.forbidOnly, baseFullConfig.forbidOnly); this._fullConfig.forbidOnly = takeFirst(this._configOverrides.forbidOnly, this._config.forbidOnly, baseFullConfig.forbidOnly);
this._fullConfig.fullyParallel = takeFirst(this._configOverrides.fullyParallel, this._config.fullyParallel, baseFullConfig.fullyParallel);
this._fullConfig.globalSetup = takeFirst(this._configOverrides.globalSetup, this._config.globalSetup, baseFullConfig.globalSetup); this._fullConfig.globalSetup = takeFirst(this._configOverrides.globalSetup, this._config.globalSetup, baseFullConfig.globalSetup);
this._fullConfig.globalTeardown = takeFirst(this._configOverrides.globalTeardown, this._config.globalTeardown, baseFullConfig.globalTeardown); this._fullConfig.globalTeardown = takeFirst(this._configOverrides.globalTeardown, this._config.globalTeardown, baseFullConfig.globalTeardown);
this._fullConfig.globalTimeout = takeFirst(this._configOverrides.globalTimeout, this._configOverrides.globalTimeout, this._config.globalTimeout, baseFullConfig.globalTimeout); this._fullConfig.globalTimeout = takeFirst(this._configOverrides.globalTimeout, this._configOverrides.globalTimeout, this._config.globalTimeout, baseFullConfig.globalTimeout);
@ -202,6 +203,7 @@ export class Loader {
if (!path.isAbsolute(snapshotDir)) if (!path.isAbsolute(snapshotDir))
snapshotDir = path.resolve(configDir, snapshotDir); snapshotDir = path.resolve(configDir, snapshotDir);
const fullProject: FullProject = { const fullProject: FullProject = {
fullyParallel: takeFirst(this._configOverrides.fullyParallel, projectConfig.fullyParallel, this._config.fullyParallel, undefined),
expect: takeFirst(this._configOverrides.expect, projectConfig.expect, this._config.expect, undefined), expect: takeFirst(this._configOverrides.expect, projectConfig.expect, this._config.expect, undefined),
outputDir, outputDir,
repeatEach: takeFirst(this._configOverrides.repeatEach, projectConfig.repeatEach, this._config.repeatEach, 1), repeatEach: takeFirst(this._configOverrides.repeatEach, projectConfig.repeatEach, this._config.repeatEach, 1),
@ -429,6 +431,7 @@ function validateProject(file: string, project: Project, title: string) {
const baseFullConfig: FullConfig = { const baseFullConfig: FullConfig = {
forbidOnly: false, forbidOnly: false,
fullyParallel: false,
globalSetup: null, globalSetup: null,
globalTeardown: null, globalTeardown: null,
globalTimeout: 0, globalTimeout: 0,

View file

@ -286,6 +286,8 @@ export class Runner {
for (const [project, files] of filesByProject) { for (const [project, files] of filesByProject) {
const projectSuite = new Suite(project.config.name); const projectSuite = new Suite(project.config.name);
projectSuite._projectConfig = project.config; projectSuite._projectConfig = project.config;
if (project.config.fullyParallel)
projectSuite._parallelMode = 'parallel';
rootSuite._addSuite(projectSuite); rootSuite._addSuite(projectSuite);
for (const file of files) { for (const file of files) {
const fileSuite = fileSuites.get(file); const fileSuite = fileSuites.get(file);

View file

@ -125,6 +125,14 @@ interface TestProject {
* all projects. * all projects.
*/ */
expect?: ExpectSettings; expect?: ExpectSettings;
/**
* Playwright Test runs tests in parallel. In order to achieve that, it runs several worker processes that run at the same
* time. By default, **test files** are run in parallel. Tests in a single file are run in order, in the same worker
* process.
*
* You can configure entire test project to concurrently run all tests in all files using this option.
*/
fullyParallel?: boolean;
/** /**
* Any JSON-serializable metadata that will be put directly to the test report. * Any JSON-serializable metadata that will be put directly to the test report.
*/ */
@ -437,6 +445,14 @@ interface TestConfig {
* *
*/ */
forbidOnly?: boolean; forbidOnly?: boolean;
/**
* Playwright Test runs tests in parallel. In order to achieve that, it runs several worker processes that run at the same
* time. By default, **test files** are run in parallel. Tests in a single file are run in order, in the same worker
* process.
*
* You can configure entire test run to concurrently execute all tests in all files using this option.
*/
fullyParallel?: boolean;
/** /**
* Path to the global setup file. This file will be required and run before all the tests. It must export a single function * Path to the global setup file. This file will be required and run before all the tests. It must export a single function
* that takes a [`TestConfig`] argument. * that takes a [`TestConfig`] argument.
@ -907,6 +923,14 @@ export interface FullConfig<TestArgs = {}, WorkerArgs = {}> {
* *
*/ */
forbidOnly: boolean; forbidOnly: boolean;
/**
* Playwright Test runs tests in parallel. In order to achieve that, it runs several worker processes that run at the same
* time. By default, **test files** are run in parallel. Tests in a single file are run in order, in the same worker
* process.
*
* You can configure entire test run to concurrently execute all tests in all files using this option.
*/
fullyParallel: boolean;
/** /**
* Path to the global setup file. This file will be required and run before all the tests. It must export a single function * Path to the global setup file. This file will be required and run before all the tests. It must export a single function
* that takes a [`TestConfig`] argument. * that takes a [`TestConfig`] argument.

View file

@ -49,6 +49,7 @@ const config: Config<CoverageWorkerOptions & PlaywrightWorkerOptions & Playwrigh
timeout: video ? 60000 : 30000, timeout: video ? 60000 : 30000,
globalTimeout: 5400000, globalTimeout: 5400000,
workers: process.env.CI ? 1 : undefined, workers: process.env.CI ? 1 : undefined,
fullyParallel: !process.env.CI,
forbidOnly: !!process.env.CI, forbidOnly: !!process.env.CI,
preserveOutput: process.env.CI ? 'failures-only' : 'always', preserveOutput: process.env.CI ? 'failures-only' : 'always',
retries: process.env.CI ? 3 : 0, retries: process.env.CI ? 3 : 0,

View file

@ -116,3 +116,63 @@ test('test.describe.parallel should work in describe', async ({ runInlineTest })
expect(result.output).toContain('%% worker=1'); expect(result.output).toContain('%% worker=1');
expect(result.output).toContain('%% worker=2'); expect(result.output).toContain('%% worker=2');
}); });
test('config.fullyParallel should work', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.ts': `
module.exports = { fullyParallel: true };
`,
'a.test.ts': `
const { test } = pwt;
test('test1', async ({}, testInfo) => {
console.log('\\n%% worker=' + testInfo.workerIndex);
await new Promise(f => setTimeout(f, 1000));
});
test('test2', async ({}, testInfo) => {
console.log('\\n%% worker=' + testInfo.workerIndex);
await new Promise(f => setTimeout(f, 1000));
});
test.describe('inner suite', () => {
test('test3', async ({}, testInfo) => {
console.log('\\n%% worker=' + testInfo.workerIndex);
await new Promise(f => setTimeout(f, 1000));
});
});
`,
}, { workers: 3 });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(3);
expect(result.output).toContain('%% worker=0');
expect(result.output).toContain('%% worker=1');
expect(result.output).toContain('%% worker=2');
});
test('project.fullyParallel should work', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.ts': `
module.exports = { projects: [ { fullyParallel: true } ] };
`,
'a.test.ts': `
const { test } = pwt;
test('test1', async ({}, testInfo) => {
console.log('\\n%% worker=' + testInfo.workerIndex);
await new Promise(f => setTimeout(f, 1000));
});
test('test2', async ({}, testInfo) => {
console.log('\\n%% worker=' + testInfo.workerIndex);
await new Promise(f => setTimeout(f, 1000));
});
test.describe('inner suite', () => {
test('test3', async ({}, testInfo) => {
console.log('\\n%% worker=' + testInfo.workerIndex);
await new Promise(f => setTimeout(f, 1000));
});
});
`,
}, { workers: 3 });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(3);
expect(result.output).toContain('%% worker=0');
expect(result.output).toContain('%% worker=1');
expect(result.output).toContain('%% worker=2');
});

View file

@ -59,6 +59,7 @@ type ExpectSettings = {
interface TestProject { interface TestProject {
expect?: ExpectSettings; expect?: ExpectSettings;
fullyParallel?: boolean;
metadata?: any; metadata?: any;
name?: string; name?: string;
snapshotDir?: string; snapshotDir?: string;
@ -117,6 +118,7 @@ type LiteralUnion<T extends U, U = string> = T | (U & { zz_IGNORE_ME?: never });
interface TestConfig { interface TestConfig {
forbidOnly?: boolean; forbidOnly?: boolean;
fullyParallel?: boolean;
globalSetup?: string; globalSetup?: string;
globalTeardown?: string; globalTeardown?: string;
globalTimeout?: number; globalTimeout?: number;
@ -153,6 +155,7 @@ export interface Config<TestArgs = {}, WorkerArgs = {}> extends TestConfig {
export interface FullConfig<TestArgs = {}, WorkerArgs = {}> { export interface FullConfig<TestArgs = {}, WorkerArgs = {}> {
forbidOnly: boolean; forbidOnly: boolean;
fullyParallel: boolean;
globalSetup: string | null; globalSetup: string | null;
globalTeardown: string | null; globalTeardown: string | null;
globalTimeout: number; globalTimeout: number;