chore: decouple test groups from root suite as much as possible (#21731)
This commit is contained in:
parent
f47a8a677c
commit
f37f38e553
|
|
@ -30,12 +30,6 @@ import { buildFileSuiteForProject, filterByFocusedLine, filterByTestIds, filterO
|
|||
import { createTestGroups, filterForShard, type TestGroup } from './testGroups';
|
||||
import { dependenciesForTestFile } from '../common/compilationCache';
|
||||
|
||||
export type ProjectWithTestGroups = {
|
||||
project: FullProjectInternal;
|
||||
projectSuite: Suite;
|
||||
testGroups: TestGroup[];
|
||||
};
|
||||
|
||||
export async function collectProjectsAndTestFiles(config: FullConfigInternal, projectsToIgnore: Set<FullProjectInternal>, additionalFileMatcher: Matcher | undefined) {
|
||||
const fsCache = new Map();
|
||||
const sourceMapCache = new Map();
|
||||
|
|
@ -120,7 +114,7 @@ export async function loadFileSuites(mode: 'out-of-process' | 'in-process', conf
|
|||
return fileSuitesByProject;
|
||||
}
|
||||
|
||||
export async function createRootSuiteAndTestGroups(config: FullConfigInternal, fileSuitesByProject: Map<FullProjectInternal, Suite[]>, errors: TestError[], shouldFilterOnly: boolean): Promise<{ rootSuite: Suite, projectsWithTestGroups: ProjectWithTestGroups[] }> {
|
||||
export async function createRootSuite(config: FullConfigInternal, fileSuitesByProject: Map<FullProjectInternal, Suite[]>, errors: TestError[], shouldFilterOnly: boolean): Promise<Suite> {
|
||||
// Create root suite, where each child will be a project suite with cloned file suites inside it.
|
||||
const rootSuite = new Suite('', 'root');
|
||||
|
||||
|
|
@ -150,55 +144,39 @@ export async function createRootSuiteAndTestGroups(config: FullConfigInternal, f
|
|||
if (shouldFilterOnly)
|
||||
filterOnly(rootSuite);
|
||||
|
||||
// Create test groups for top-level projects.
|
||||
let projectsWithTestGroups: ProjectWithTestGroups[] = [];
|
||||
for (const projectSuite of rootSuite.suites) {
|
||||
const project = projectSuite.project() as FullProjectInternal;
|
||||
const testGroups = createTestGroups(projectSuite, config.workers);
|
||||
projectsWithTestGroups.push({ project, projectSuite, testGroups });
|
||||
}
|
||||
|
||||
// Shard only the top-level projects.
|
||||
if (config.shard) {
|
||||
const allTestGroups: TestGroup[] = [];
|
||||
for (const { testGroups } of projectsWithTestGroups)
|
||||
allTestGroups.push(...testGroups);
|
||||
const shardedTestGroups = filterForShard(config.shard, allTestGroups);
|
||||
// Create test groups for top-level projects.
|
||||
const testGroups: TestGroup[] = [];
|
||||
for (const projectSuite of rootSuite.suites)
|
||||
testGroups.push(...createTestGroups(projectSuite, config.workers));
|
||||
|
||||
const shardedTests = new Set<TestCase>();
|
||||
for (const group of shardedTestGroups) {
|
||||
// Shard test groups.
|
||||
const testGroupsInThisShard = filterForShard(config.shard, testGroups);
|
||||
const testsInThisShard = new Set<TestCase>();
|
||||
for (const group of testGroupsInThisShard) {
|
||||
for (const test of group.tests)
|
||||
shardedTests.add(test);
|
||||
testsInThisShard.add(test);
|
||||
}
|
||||
|
||||
// Update project suites and test groups.
|
||||
for (const p of projectsWithTestGroups) {
|
||||
p.testGroups = p.testGroups.filter(group => shardedTestGroups.has(group));
|
||||
filterTestsRemoveEmptySuites(p.projectSuite, test => shardedTests.has(test));
|
||||
}
|
||||
|
||||
// Remove now-empty top-level projects.
|
||||
projectsWithTestGroups = projectsWithTestGroups.filter(p => p.testGroups.length > 0);
|
||||
// Update project suites, removing empty ones.
|
||||
filterTestsRemoveEmptySuites(rootSuite, test => testsInThisShard.has(test));
|
||||
}
|
||||
|
||||
// Now prepend dependency projects.
|
||||
{
|
||||
// Filtering only and sharding might have reduced the number of top-level projects.
|
||||
// Build the project closure to only include dependencies that are still needed.
|
||||
const projectClosure = new Set(buildProjectsClosure(projectsWithTestGroups.map(p => p.project)));
|
||||
const projectClosure = new Set(buildProjectsClosure(rootSuite.suites.map(suite => suite.project() as FullProjectInternal)));
|
||||
|
||||
// Clone file suites for dependency projects.
|
||||
for (const [project, fileSuites] of fileSuitesByProject) {
|
||||
if (project._internal.type === 'dependency' && projectClosure.has(project)) {
|
||||
const projectSuite = await createProjectSuite(fileSuites, project, { cliFileFilters: [], cliTitleMatcher: undefined });
|
||||
rootSuite._prependSuite(projectSuite);
|
||||
const testGroups = createTestGroups(projectSuite, config.workers);
|
||||
projectsWithTestGroups.push({ project, projectSuite, testGroups });
|
||||
}
|
||||
if (project._internal.type === 'dependency' && projectClosure.has(project))
|
||||
rootSuite._prependSuite(await createProjectSuite(fileSuites, project, { cliFileFilters: [], cliTitleMatcher: undefined }));
|
||||
}
|
||||
}
|
||||
|
||||
return { rootSuite, projectsWithTestGroups };
|
||||
return rootSuite;
|
||||
}
|
||||
|
||||
async function createProjectSuite(fileSuites: Suite[], project: FullProjectInternal, options: { cliFileFilters: TestFileFilter[], cliTitleMatcher?: Matcher, testIdMatcher?: Matcher }): Promise<Suite> {
|
||||
|
|
|
|||
|
|
@ -21,26 +21,33 @@ import { debug, rimraf } from 'playwright-core/lib/utilsBundle';
|
|||
import { Dispatcher } from './dispatcher';
|
||||
import type { TestRunnerPluginRegistration } from '../plugins';
|
||||
import type { Multiplexer } from '../reporters/multiplexer';
|
||||
import type { TestGroup } from '../runner/testGroups';
|
||||
import { createTestGroups, type TestGroup } from '../runner/testGroups';
|
||||
import type { Task } from './taskRunner';
|
||||
import { TaskRunner } from './taskRunner';
|
||||
import type { Suite } from '../common/test';
|
||||
import type { FullConfigInternal, FullProjectInternal } from '../common/types';
|
||||
import { collectProjectsAndTestFiles, createRootSuiteAndTestGroups, loadFileSuites, loadGlobalHook, type ProjectWithTestGroups } from './loadUtils';
|
||||
import { collectProjectsAndTestFiles, createRootSuite, loadFileSuites, loadGlobalHook } from './loadUtils';
|
||||
import type { Matcher } from '../util';
|
||||
|
||||
const removeFolderAsync = promisify(rimraf);
|
||||
const readDirAsync = promisify(fs.readdir);
|
||||
|
||||
type ProjectWithTestGroups = {
|
||||
project: FullProjectInternal;
|
||||
projectSuite: Suite;
|
||||
testGroups: TestGroup[];
|
||||
};
|
||||
|
||||
type Phase = {
|
||||
dispatcher: Dispatcher,
|
||||
projects: ProjectWithTestGroups[]
|
||||
};
|
||||
|
||||
export type TaskRunnerState = {
|
||||
reporter: Multiplexer;
|
||||
config: FullConfigInternal;
|
||||
rootSuite?: Suite;
|
||||
projectsWithTestGroups?: ProjectWithTestGroups[];
|
||||
phases: {
|
||||
dispatcher: Dispatcher,
|
||||
projects: ProjectWithTestGroups[]
|
||||
}[];
|
||||
phases: Phase[];
|
||||
};
|
||||
|
||||
export function createTaskRunner(config: FullConfigInternal, reporter: Multiplexer): TaskRunner<TaskRunnerState> {
|
||||
|
|
@ -153,9 +160,7 @@ function createLoadTask(mode: 'out-of-process' | 'in-process', shouldFilterOnly:
|
|||
const { config } = context;
|
||||
const filesToRunByProject = await collectProjectsAndTestFiles(config, projectsToIgnore, additionalFileMatcher);
|
||||
const fileSuitesByProject = await loadFileSuites(mode, config, filesToRunByProject, errors);
|
||||
const loaded = await createRootSuiteAndTestGroups(config, fileSuitesByProject, errors, shouldFilterOnly);
|
||||
context.rootSuite = loaded.rootSuite;
|
||||
context.projectsWithTestGroups = loaded.projectsWithTestGroups;
|
||||
context.rootSuite = await createRootSuite(config, fileSuitesByProject, errors, shouldFilterOnly);
|
||||
// Fail when no tests.
|
||||
if (!context.rootSuite.allTests().length && !config._internal.passWithNoTests && !config.shard)
|
||||
throw new Error(`No tests found`);
|
||||
|
|
@ -167,24 +172,32 @@ function createPhasesTask(): Task<TaskRunnerState> {
|
|||
context.config._internal.maxConcurrentTestGroups = 0;
|
||||
|
||||
const processed = new Set<FullProjectInternal>();
|
||||
for (let i = 0; i < context.projectsWithTestGroups!.length; i++) {
|
||||
const projectToSuite = new Map(context.rootSuite!.suites.map(suite => [suite.project() as FullProjectInternal, suite]));
|
||||
for (let i = 0; i < projectToSuite.size; i++) {
|
||||
// Find all projects that have all their dependencies processed by previous phases.
|
||||
const phase: ProjectWithTestGroups[] = [];
|
||||
for (const projectWithTestGroups of context.projectsWithTestGroups!) {
|
||||
if (processed.has(projectWithTestGroups.project))
|
||||
const phaseProjects: FullProjectInternal[] = [];
|
||||
for (const project of projectToSuite.keys()) {
|
||||
if (processed.has(project))
|
||||
continue;
|
||||
if (projectWithTestGroups.project._internal.deps.find(p => !processed.has(p)))
|
||||
if (project._internal.deps.find(p => !processed.has(p)))
|
||||
continue;
|
||||
phase.push(projectWithTestGroups);
|
||||
phaseProjects.push(project);
|
||||
}
|
||||
|
||||
// Create a new phase.
|
||||
for (const projectWithTestGroups of phase)
|
||||
processed.add(projectWithTestGroups.project);
|
||||
if (phase.length) {
|
||||
const testGroupsInPhase = phase.reduce((acc, projectWithTestGroups) => acc + projectWithTestGroups.testGroups.length, 0);
|
||||
debug('pw:test:task')(`created phase #${context.phases.length} with ${phase.map(p => p.project.name).sort()} projects, ${testGroupsInPhase} testGroups`);
|
||||
context.phases.push({ dispatcher: new Dispatcher(context.config, context.reporter), projects: phase });
|
||||
for (const project of phaseProjects)
|
||||
processed.add(project);
|
||||
if (phaseProjects.length) {
|
||||
let testGroupsInPhase = 0;
|
||||
const phase: Phase = { dispatcher: new Dispatcher(context.config, context.reporter), projects: [] };
|
||||
context.phases.push(phase);
|
||||
for (const project of phaseProjects) {
|
||||
const projectSuite = projectToSuite.get(project)!;
|
||||
const testGroups = createTestGroups(projectSuite, context.config.workers);
|
||||
phase.projects.push({ project, projectSuite, testGroups });
|
||||
testGroupsInPhase += testGroups.length;
|
||||
}
|
||||
debug('pw:test:task')(`created phase #${context.phases.length} with ${phase.projects.map(p => p.project.name).sort()} projects, ${testGroupsInPhase} testGroups`);
|
||||
context.config._internal.maxConcurrentTestGroups = Math.max(context.config._internal.maxConcurrentTestGroups, testGroupsInPhase);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue