chore: revert project.{stage,run} (#18462)

This commit is contained in:
Yury Semikhatsky 2022-10-31 14:04:24 -07:00 committed by GitHub
parent fbfec18678
commit 7337dd4e28
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 86 additions and 659 deletions

View file

@ -261,21 +261,6 @@ Use [`method: Test.describe.configure`] to change the number of retries for a sp
Use [`property: TestConfig.retries`] to change this option for all projects.
## property: TestProject.run
* since: v1.28
- type: ?<[RunMode]<"default"|"always">>
If set to 'always' the project will always be executed regardless of previous failures in the same test run. If set to 'always' all tests from the project will run in each shard and won't be split. If omitted or set to 'default' the project will be skipped if there are test failures in the projects from the prior [`property: TestProject.stage`]'s.
## property: TestProject.stage
* since: v1.28
- type: ?<[int]>
An integer number that defines when the project should run relative to other projects. Each project runs in exactly
one stage. By default all projects run in stage 0. Stages with lower number run first. Several projects can run in
each stage. Execution order between projecs in the same stage is undefined. If any test from a stage fails all tests
from susequent stages are skipped, use [`property: TestProject.run`] to change this behavior.
## property: TestProject.testDir
* since: v1.10
- type: ?<[string]>

View file

@ -277,8 +277,6 @@ export class Loader {
const outputDir = takeFirst(projectConfig.outputDir, config.outputDir, path.join(throwawayArtifactsPath, 'test-results'));
const snapshotDir = takeFirst(projectConfig.snapshotDir, config.snapshotDir, testDir);
const name = takeFirst(projectConfig.name, config.name, '');
const stage = takeFirst(projectConfig.stage, 0);
const run = takeFirst(projectConfig.run, 'default');
let screenshotsDir = takeFirst((projectConfig as any).screenshotsDir, (config as any).screenshotsDir, path.join(testDir, '__screenshots__', process.platform, name));
if (process.env.PLAYWRIGHT_DOCKER) {
@ -298,8 +296,6 @@ export class Loader {
metadata: takeFirst(projectConfig.metadata, config.metadata, undefined),
name,
testDir,
run,
stage,
_respectGitIgnore: respectGitIgnore,
snapshotDir,
_screenshotsDir: screenshotsDir,
@ -617,16 +613,6 @@ function validateProject(file: string, project: Project, title: string) {
throw errorWithFile(file, `${title}.retries must be a non-negative number`);
}
if ('stage' in project && project.stage !== undefined) {
if (typeof project.stage !== 'number' || Math.floor(project.stage) !== project.stage)
throw errorWithFile(file, `${title}.stage must be an integer`);
}
if ('run' in project && project.run !== undefined) {
if (project.run !== 'default' && project.run !== 'always')
throw errorWithFile(file, `${title}.run must be one of 'default', 'always'.`);
}
if ('testDir' in project && project.testDir !== undefined) {
if (typeof project.testDir !== 'string')
throw errorWithFile(file, `${title}.testDir must be a string`);

View file

@ -17,7 +17,6 @@
import * as fs from 'fs';
import * as path from 'path';
import { assert } from 'playwright-core/lib/utils';
import { MultiMap } from 'playwright-core/lib/utils/multimap';
import { raceAgainstTimeout } from 'playwright-core/lib/utils/timeoutRunner';
import { colors, minimatch, rimraf } from 'playwright-core/lib/utilsBundle';
@ -52,9 +51,6 @@ const readDirAsync = promisify(fs.readdir);
const readFileAsync = promisify(fs.readFile);
export const kDefaultConfigFiles = ['playwright.config.ts', 'playwright.config.js', 'playwright.config.mjs'];
// Test run is a sequence of run phases aka stages.
type RunStage = FullProjectInternal[];
type RunOptions = {
listOnly?: boolean;
testFileFilters: TestFileFilter[];
@ -255,17 +251,10 @@ export class Runner {
return files;
}
private async _collectTestGroups(options: RunOptions, fatalErrors: TestError[]): Promise<{ rootSuite: Suite, concurrentTestGroups: TestGroup[][] }> {
private async _collectTestGroups(options: RunOptions, fatalErrors: TestError[]): Promise<{ rootSuite: Suite, testGroups: TestGroup[] }> {
const config = this._loader.fullConfig();
// Each entry is an array of test groups that can run concurrently. All
// test groups from the previos entries must finish before entry starts.
const concurrentTestGroups = [];
const rootSuite = new Suite('', 'root');
const projects = this._collectProjects(options.projectFilter);
const runStages = collectRunStages(projects);
assert(runStages.length > 0);
for (const stage of runStages) {
const filesByProject = await this._collectFiles(stage, fileMatcherFrom(options.testFileFilters));
const filesByProject = await this._collectFiles(projects, fileMatcherFrom(options.testFileFilters));
const allTestFiles = new Set<string>();
for (const files of filesByProject.values())
@ -305,7 +294,7 @@ export class Runner {
for (const fileSuite of preprocessRoot.suites)
fileSuites.set(fileSuite._requireFile, fileSuite);
const firstProjectSuiteIndex = rootSuite.suites.length;
const rootSuite = new Suite('', 'root');
for (const [project, files] of filesByProject) {
const grepMatcher = createTitleMatcher(project.grep);
const grepInvertMatcher = project.grepInvert ? createTitleMatcher(project.grepInvert) : null;
@ -332,14 +321,11 @@ export class Runner {
}
}
const projectSuites = rootSuite.suites.slice(firstProjectSuiteIndex);
const testGroups = createTestGroups(projectSuites, config.workers);
concurrentTestGroups.push(testGroups);
}
return { rootSuite, concurrentTestGroups };
const testGroups = createTestGroups(rootSuite.suites, config.workers);
return { rootSuite, testGroups };
}
private _filterForCurrentShard(rootSuite: Suite, concurrentTestGroups: TestGroup[][]) {
private _filterForCurrentShard(rootSuite: Suite, testGroups: TestGroup[]) {
const shard = this._loader.fullConfig().shard;
if (!shard)
return;
@ -348,9 +334,9 @@ export class Runner {
// - all tests from `run: 'always'` projects (non shardale) and
// - its portion of the shardable ones.
let shardableTotal = 0;
for (const projectSuite of rootSuite.suites) {
if (projectSuite.project()!.run !== 'always')
shardableTotal += projectSuite.allTests().length;
for (const group of testGroups) {
if (group.run !== 'always')
shardableTotal += group.tests.length;
}
const shardTests = new Set<TestCase>();
@ -364,10 +350,8 @@ export class Runner {
const from = shardSize * currentShard + Math.min(extraOne, currentShard);
const to = from + shardSize + (currentShard < extraOne ? 1 : 0);
let current = 0;
const shardConcurrentTestGroups = [];
for (const stage of concurrentTestGroups) {
const shardedStage: TestGroup[] = [];
for (const group of stage) {
const shardTestGroups = [];
for (const group of testGroups) {
let includeGroupInShard = false;
if (group.run === 'always') {
includeGroupInShard = true;
@ -379,16 +363,13 @@ export class Runner {
current += group.tests.length;
}
if (includeGroupInShard) {
shardedStage.push(group);
shardTestGroups.push(group);
for (const test of group.tests)
shardTests.add(test);
}
}
if (shardedStage.length)
shardConcurrentTestGroups.push(shardedStage);
}
concurrentTestGroups.length = 0;
concurrentTestGroups.push(...shardConcurrentTestGroups);
testGroups.length = 0;
testGroups.push(...shardTestGroups);
filterSuiteWithOnlySemantics(rootSuite, () => false, test => shardTests.has(test));
}
@ -398,15 +379,15 @@ export class Runner {
const fatalErrors: TestError[] = [];
// Each entry is an array of test groups that can be run concurrently. All
// test groups from the previos entries must finish before entry starts.
const { rootSuite, concurrentTestGroups } = await this._collectTestGroups(options, fatalErrors);
const { rootSuite, testGroups } = await this._collectTestGroups(options, fatalErrors);
// Fail when no tests.
if (!rootSuite.allTests().length && !options.passWithNoTests)
fatalErrors.push(createNoTestsError());
this._filterForCurrentShard(rootSuite, concurrentTestGroups);
this._filterForCurrentShard(rootSuite, testGroups);
config._maxConcurrentTestGroups = Math.max(...concurrentTestGroups.map(g => g.length));
config._maxConcurrentTestGroups = testGroups.length;
// Report begin
this._reporter.onBegin?.(config, rootSuite);
@ -447,7 +428,9 @@ export class Runner {
let hasWorkerErrors = false;
let previousStageFailed = false;
for (let testGroups of concurrentTestGroups) {
// TODO: will be project setups followed by tests.
const orderedTestGroups = [testGroups];
for (let testGroups of orderedTestGroups) {
if (previousStageFailed)
testGroups = this._skipTestsNotMarkedAsRunAlways(testGroups);
if (!testGroups.length)
@ -738,20 +721,6 @@ function buildItemLocation(rootDir: string, testOrSuite: Suite | TestCase) {
return `${path.relative(rootDir, testOrSuite.location.file)}:${testOrSuite.location.line}`;
}
function collectRunStages(projects: FullProjectInternal[]): RunStage[] {
const stages: RunStage[] = [];
const stageToProjects = new MultiMap<number, FullProjectInternal>();
for (const p of projects)
stageToProjects.set(p.stage, p);
const stageIds = Array.from(stageToProjects.keys());
stageIds.sort((a, b) => a - b);
for (const stage of stageIds) {
const projects = stageToProjects.get(stage);
stages.push(projects);
}
return stages;
}
function createTestGroups(projectSuites: Suite[], workers: number): TestGroup[] {
// This function groups tests that can be run together.
// Tests cannot be run together when:
@ -785,7 +754,7 @@ function createTestGroups(projectSuites: Suite[], workers: number): TestGroup[]
requireFile: test._requireFile,
repeatEachIndex: test.repeatEachIndex,
projectId: test._projectId,
run: test.parent.project()!.run,
run: 'default',
tests: [],
watchMode: false,
};

View file

@ -259,21 +259,6 @@ export interface FullProject<TestArgs = {}, WorkerArgs = {}> {
* all projects.
*/
retries: number;
/**
* An integer number that defines when the project should run relative to other projects. Each project runs in exactly one
* stage. By default all projects run in stage 0. Stages with lower number run first. Several projects can run in each
* stage. Execution order between projecs in the same stage is undefined. If any test from a stage fails all tests from
* susequent stages are skipped, use [testProject.run](https://playwright.dev/docs/api/class-testproject#test-project-run)
* to change this behavior.
*/
stage: number;
/**
* If set to 'always' the project will always be executed regardless of previous failures in the same test run. If set to
* 'always' all tests from the project will run in each shard and won't be split. If omitted or set to 'default' the
* project will be skipped if there are test failures in the projects from the prior
* [testProject.stage](https://playwright.dev/docs/api/class-testproject#test-project-stage)'s.
*/
run: 'default'|'always';
/**
* Directory that will be recursively scanned for test files. Defaults to the directory of the configuration file.
*
@ -4490,23 +4475,6 @@ interface TestProject {
*/
retries?: number;
/**
* If set to 'always' the project will always be executed regardless of previous failures in the same test run. If set to
* 'always' all tests from the project will run in each shard and won't be split. If omitted or set to 'default' the
* project will be skipped if there are test failures in the projects from the prior
* [testProject.stage](https://playwright.dev/docs/api/class-testproject#test-project-stage)'s.
*/
run?: "default"|"always";
/**
* An integer number that defines when the project should run relative to other projects. Each project runs in exactly one
* stage. By default all projects run in stage 0. Stages with lower number run first. Several projects can run in each
* stage. Execution order between projecs in the same stage is undefined. If any test from a stage fails all tests from
* susequent stages are skipped, use [testProject.run](https://playwright.dev/docs/api/class-testproject#test-project-run)
* to change this behavior.
*/
stage?: number;
/**
* Directory that will be recursively scanned for test files. Defaults to the directory of the configuration file.
*

View file

@ -480,60 +480,3 @@ test('should have correct types for the config', async ({ runTSC }) => {
});
expect(result.exitCode).toBe(0);
});
test('should throw when project.stage is not a number', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.ts': `
module.exports = {
projects: [
{ name: 'a', stage: 'foo' },
],
};
`,
'a.test.ts': `
const { test } = pwt;
test('pass', async () => {});
`
});
expect(result.exitCode).toBe(1);
expect(result.output).toContain(`config.projects[0].stage must be an integer`);
});
test('should throw when project.stage is not an integer', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.ts': `
module.exports = {
projects: [
{ name: 'a', stage: 3.14 },
],
};
`,
'a.test.ts': `
const { test } = pwt;
test('pass', async () => {});
`
});
expect(result.exitCode).toBe(1);
expect(result.output).toContain(`config.projects[0].stage must be an integer`);
});
test('should throw when project.run is not an expected string', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.ts': `
module.exports = {
projects: [
{ name: 'a', run: 'yes' },
],
};
`,
'a.test.ts': `
const { test } = pwt;
test('pass', async () => {});
`
});
expect(result.exitCode).toBe(1);
expect(result.output).toContain(`config.projects[0].run must be one of 'default', 'always'.`);
});

View file

@ -1,422 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { PlaywrightTestConfig, TestInfo, PlaywrightTestProject } from '@playwright/test';
import path from 'path';
import { test, expect } from './playwright-test-fixtures';
function createConfigWithProjects(names: string[], testInfo: TestInfo, projectTemplates?: { [name: string]: PlaywrightTestProject }): Record<string, string> {
const config: PlaywrightTestConfig = {
projects: names.map(name => ({ ...projectTemplates?.[name], name, testDir: testInfo.outputPath(name) })),
};
const files = {};
for (const name of names) {
files[`${name}/${name}.spec.ts`] = `
const { test } = pwt;
test('${name} test', async () => {
await new Promise(f => setTimeout(f, 100));
});`;
}
function replacer(key, value) {
if (value instanceof RegExp)
return `RegExp(${value.toString()})`;
else
return value;
}
files['playwright.config.ts'] = `
import * as path from 'path';
module.exports = ${JSON.stringify(config, replacer, 2)};
`.replace(/"RegExp\((.*)\)"/g, '$1');
return files;
}
type Timeline = { titlePath: string[], event: 'begin' | 'end' }[];
function formatTimeline(timeline: Timeline) {
return timeline.map(e => `${e.titlePath.slice(1).join(' > ')} [${e.event}]`).join('\n');
}
function projectNames(timeline: Timeline) {
const projectNames = Array.from(new Set(timeline.map(({ titlePath }) => titlePath[1])).keys());
projectNames.sort();
return projectNames;
}
function expectRunBefore(timeline: Timeline, before: string[], after: string[]) {
const begin = new Map<string, number>();
const end = new Map<string, number>();
for (let i = 0; i < timeline.length; i++) {
const projectName = timeline[i].titlePath[1];
const map = timeline[i].event === 'begin' ? begin : end;
const oldIndex = map.get(projectName) ?? i;
const newIndex = (timeline[i].event === 'begin') ? Math.min(i, oldIndex) : Math.max(i, oldIndex);
map.set(projectName, newIndex);
}
for (const b of before) {
for (const a of after) {
const bEnd = end.get(b) as number;
expect(bEnd === undefined, `Unknown project ${b}`).toBeFalsy();
const aBegin = begin.get(a) as number;
expect(aBegin === undefined, `Unknown project ${a}`).toBeFalsy();
if (bEnd < aBegin)
continue;
throw new Error(`Project '${b}' expected to finish before '${a}'\nTest run order was:\n${formatTimeline(timeline)}`);
}
}
}
test('should work for two projects', async ({ runGroups }, testInfo) => {
await test.step(`order a then b`, async () => {
const projectTemplates = {
'a': {
stage: 10
},
'b': {
stage: 20
},
};
const configWithFiles = createConfigWithProjects(['a', 'b'], testInfo, projectTemplates);
const { exitCode, passed, timeline } = await runGroups(configWithFiles);
expect(exitCode).toBe(0);
expect(passed).toBe(2);
expect(formatTimeline(timeline)).toEqual(`a > a${path.sep}a.spec.ts > a test [begin]
a > a${path.sep}a.spec.ts > a test [end]
b > b${path.sep}b.spec.ts > b test [begin]
b > b${path.sep}b.spec.ts > b test [end]`);
});
await test.step(`order b then a`, async () => {
const projectTemplates = {
'a': {
stage: 20
},
'b': {
stage: 10
},
};
const configWithFiles = createConfigWithProjects(['a', 'b'], testInfo, projectTemplates);
const { exitCode, passed, timeline } = await runGroups(configWithFiles);
expect(exitCode).toBe(0);
expect(passed).toBe(2);
expect(formatTimeline(timeline)).toEqual(`b > b${path.sep}b.spec.ts > b test [begin]
b > b${path.sep}b.spec.ts > b test [end]
a > a${path.sep}a.spec.ts > a test [begin]
a > a${path.sep}a.spec.ts > a test [end]`);
});
});
test('should order 1-3-1 projects', async ({ runGroups }, testInfo) => {
const projectTemplates = {
'e': {
stage: -100
},
'a': {
stage: 100
},
};
const configWithFiles = createConfigWithProjects(['a', 'b', 'c', 'd', 'e'], testInfo, projectTemplates);
const { exitCode, passed, timeline } = await runGroups(configWithFiles);
expect(exitCode).toBe(0);
expectRunBefore(timeline, ['e'], ['d', 'c', 'b']);
expectRunBefore(timeline, ['d', 'c', 'b'], ['a']);
expect(passed).toBe(5);
});
test('should order 2-2-2 projects', async ({ runGroups }, testInfo) => {
const projectTemplates = {
'a': {
stage: -30
},
'b': {
stage: -30
},
'e': {
stage: 40
},
'f': {
stage: 40
},
};
const configWithFiles = createConfigWithProjects(['a', 'b', 'c', 'd', 'e', 'f'], testInfo, projectTemplates);
const { exitCode, passed, timeline } = await runGroups(configWithFiles);
expect(exitCode).toBe(0);
expectRunBefore(timeline, ['a', 'b'], ['c', 'd']);
expectRunBefore(timeline, ['c', 'd'], ['e', 'f']);
expect(passed).toBe(6);
});
test('should order project according to stage 1-1-2-2', async ({ runGroups }, testInfo) => {
const projectTemplates = {
'a': {
stage: 10
},
'b': {
stage: 10
},
'd': {
stage: -10
},
'e': {
stage: -20
},
};
const configWithFiles = createConfigWithProjects(['a', 'b', 'c', 'd', 'e', 'f'], testInfo, projectTemplates);
const { exitCode, passed, timeline } = await runGroups(configWithFiles);
expect(exitCode).toBe(0);
expect(passed).toBe(6);
expect(projectNames(timeline)).toEqual(['a', 'b', 'c', 'd', 'e', 'f']);
expectRunBefore(timeline, ['e'], ['a', 'b', 'c', 'd', 'f']); // -20
expectRunBefore(timeline, ['d'], ['a', 'b', 'c', 'f']); // -10
expectRunBefore(timeline, ['c', 'f'], ['a', 'b']); // 0
expect(passed).toBe(6);
});
test('should work with project filter', async ({ runGroups }, testInfo) => {
const projectTemplates = {
'a': {
stage: 10
},
'b': {
stage: 10
},
'e': {
stage: -10
},
'f': {
stage: -10
},
};
const configWithFiles = createConfigWithProjects(['a', 'b', 'c', 'd', 'e', 'f'], testInfo, projectTemplates);
const { exitCode, passed, timeline } = await runGroups(configWithFiles, { project: ['b', 'c', 'e'] });
expect(exitCode).toBe(0);
expect(passed).toBe(3);
expect(projectNames(timeline)).toEqual(['b', 'c', 'e']);
expectRunBefore(timeline, ['e'], ['b', 'c']); // -10 < 0
expectRunBefore(timeline, ['c'], ['b']); // 0 < 10
expect(passed).toBe(3);
});
test('should skip after failire by default', async ({ runGroups }, testInfo) => {
const projectTemplates = {
'a': {
stage: 1
},
'b': {
stage: 2,
run: 'default'
},
'c': {
stage: 2
},
'd': {
stage: 4,
run: 'default' // this is not important as the test is skipped
},
'e': {
stage: 4
},
};
const configWithFiles = createConfigWithProjects(['a', 'b', 'c', 'd', 'e'], testInfo, projectTemplates);
configWithFiles[`b/b.spec.ts`] = `
const { test } = pwt;
test('b test', async () => {
expect(1).toBe(2);
});`;
configWithFiles[`d/d.spec.ts`] = `
const { test } = pwt;
test('d test', async () => {
expect(1).toBe(2);
});`;
const { exitCode, passed, failed, skipped, timeline } = await runGroups(configWithFiles);
expect(exitCode).toBe(1);
expect(failed).toBe(1);
expect(passed).toBe(2); // 'c' may either pass or be skipped.
expect(skipped).toBe(2);
expect(projectNames(timeline)).toEqual(['a', 'b', 'c', 'd', 'e']);
expectRunBefore(timeline, ['a'], ['b', 'c']); // 1 < 2
expectRunBefore(timeline, ['b', 'c'], ['d', 'e']); // 2 < 4
});
test('should run after failire if run:always', async ({ runGroups }, testInfo) => {
const projectTemplates = {
'a': {
stage: 1
},
'b': {
stage: 2,
run: 'default'
},
'c': {
stage: 2
},
'd': {
stage: 4,
run: 'always'
},
'e': {
stage: 4
},
'f': {
stage: 10,
run: 'always'
},
};
const configWithFiles = createConfigWithProjects(['a', 'b', 'c', 'd', 'e', 'f'], testInfo, projectTemplates);
configWithFiles[`b/b.spec.ts`] = `
const { test } = pwt;
test('b test', async () => {
expect(1).toBe(2);
});`;
const { exitCode, passed, failed, skipped, timeline } = await runGroups(configWithFiles);
expect(exitCode).toBe(1);
expect(passed).toBe(4);
expect(failed).toBe(1);
expect(skipped).toBe(1);
expect(projectNames(timeline)).toEqual(['a', 'b', 'c', 'd', 'e', 'f']);
expectRunBefore(timeline, ['a'], ['b', 'c']); // 1 < 2
expectRunBefore(timeline, ['b', 'c'], ['d', 'e']); // 2 < 4
expectRunBefore(timeline, ['d', 'e'], ['f']); // 4 < 10
});
test('should split project if no run: always', async ({ runGroups }, testInfo) => {
const files = {
'playwright.config.ts': `
module.exports = {
projects: [
{
stage: 10,
name: 'proj-1',
testMatch: /.*(a|b).test.ts/,
},
{
stage: 20,
name: 'proj-2',
testMatch: /.*c.test.ts/,
},
]
};`,
'a.test.ts': `
const { test } = pwt;
test('test1', async () => { });
test('test2', async () => { });
test('test3', async () => { });
test('test4', async () => { });
`,
'b.test.ts': `
const { test } = pwt;
test('test1', async () => { });
test('test2', async () => { });
`,
'c.test.ts': `
const { test } = pwt;
test('test1', async () => { });
test('test2', async () => { });
`,
};
{ // Shard 1/2
const { exitCode, passed, output } = await runGroups(files, { shard: '1/2' });
expect(output).toContain('Running 4 tests using 1 worker, shard 1 of 2');
expect(output).toContain('[proj-1] a.test.ts:6:7 test1');
expect(output).toContain('[proj-1] a.test.ts:7:7 test2');
expect(output).toContain('[proj-1] a.test.ts:8:7 test3');
expect(output).toContain('[proj-1] a.test.ts:9:7 test4');
expect(output).not.toContain('[proj-2]');
expect(output).not.toContain('b.test.ts');
expect(output).not.toContain('c.test.ts');
expect(exitCode).toBe(0);
expect(passed).toBe(4);
}
{ // Shard 2/2
const { exitCode, passed, output } = await runGroups(files, { shard: '2/2' });
expect(output).toContain('Running 4 tests using 1 worker, shard 2 of 2');
expect(output).toContain('[proj-1] b.test.ts:6:7 test1');
expect(output).toContain('[proj-1] b.test.ts:7:7 test2');
expect(output).toContain('[proj-2] c.test.ts:6:7 test1');
expect(output).toContain('[proj-2] c.test.ts:7:7 test2');
expect(output).not.toContain('a.test.ts');
expect(exitCode).toBe(0);
expect(passed).toBe(4);
}
});
test('should not split project with run: awlays', async ({ runGroups }, testInfo) => {
const files = {
'playwright.config.ts': `
module.exports = {
projects: [
{
stage: 10,
name: 'proj-1',
testMatch: /.*(a|b).test.ts/,
run: 'always',
},
{
stage: 20,
name: 'proj-2',
testMatch: /.*(c|d).test.ts/,
},
]
};`,
'a.test.ts': `
const { test } = pwt;
test('test1', async () => { });
test('test2', async () => { });
`,
'b.test.ts': `
const { test } = pwt;
test('test2', async () => { });
`,
'c.test.ts': `
const { test } = pwt;
test('test1', async () => { });
test('test2', async () => { });
test('test3', async () => { });
`,
'd.test.ts': `
const { test } = pwt;
test('test1', async () => { });
test('test2', async () => { });
`,
};
{ // Shard 1/2
const { exitCode, passed, output } = await runGroups(files, { shard: '1/2' });
expect(output).toContain('Running 6 tests using 2 workers, shard 1 of 2');
// proj-1 is non shardable => a.test.ts and b.test.ts should run in both shards.
expect(output).toContain('[proj-1] b.test.ts:6:7 test2');
expect(output).toContain('[proj-1] a.test.ts:6:7 test1');
expect(output).toContain('[proj-1] a.test.ts:7:7 test2');
expect(output).toContain('[proj-2] c.test.ts:6:7 test1');
expect(output).toContain('[proj-2] c.test.ts:7:7 test2');
expect(output).not.toContain('d.test.ts');
expect(exitCode).toBe(0);
expect(passed).toBe(6);
}
{ // Shard 1/2
const { exitCode, passed, output } = await runGroups(files, { shard: '2/2' });
expect(output).toContain('Running 5 tests using 2 workers, shard 2 of 2');
// proj-1 is non shardable => a.test.ts and b.test.ts should run in both shards.
expect(output).toContain('[proj-1] b.test.ts:6:7 test2');
expect(output).toContain('[proj-1] a.test.ts:6:7 test1');
expect(output).toContain('[proj-1] a.test.ts:7:7 test2');
expect(output).toContain('[proj-2] d.test.ts:6:7 test1');
expect(output).toContain('[proj-2] d.test.ts:7:7 test2');
expect(output).not.toContain('c.test.ts');
expect(exitCode).toBe(0);
expect(passed).toBe(5);
}
});

View file

@ -46,8 +46,6 @@ export interface FullProject<TestArgs = {}, WorkerArgs = {}> {
outputDir: string;
repeatEach: number;
retries: number;
stage: number;
run: 'default'|'always';
testDir: string;
testIgnore: string | RegExp | (string | RegExp)[];
testMatch: string | RegExp | (string | RegExp)[];