feat(teardown): allow the same project to be a teardown for multiple (#23074)

This commit is contained in:
Dmitry Gozman 2023-05-16 18:26:06 -07:00 committed by GitHub
parent 2d3ab74d22
commit fc2e0e76bd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 38 additions and 34 deletions

View file

@ -200,7 +200,7 @@ Use [`property: TestConfig.retries`] to change this option for all projects.
* since: v1.34
- type: ?<[string]>
Name of a project that needs to run after this and any dependent projects have finished. Teardown is useful to cleanup any resources acquired by this project.
Name of a project that needs to run after this and all dependent projects have finished. Teardown is useful to cleanup any resources acquired by this project.
Passing `--no-deps` argument ignores [`property: TestProject.teardown`] and behaves as if it was not specified.

View file

@ -209,7 +209,7 @@ function resolveReporters(reporters: Config['reporter'], rootDir: string): Repor
}
function resolveProjectDependencies(projects: FullProjectInternal[]) {
const teardownToSetup = new Map<FullProjectInternal, FullProjectInternal>();
const teardownSet = new Set<FullProjectInternal>();
for (const project of projects) {
for (const dependencyName of project.project.dependencies) {
const dependencies = projects.filter(p => p.project.name === dependencyName);
@ -227,18 +227,16 @@ function resolveProjectDependencies(projects: FullProjectInternal[]) {
throw new Error(`Project teardowns should have unique names, reading ${project.project.teardown}`);
const teardown = teardowns[0];
project.teardown = teardown;
if (teardownToSetup.has(teardown))
throw new Error(`Project ${teardown.project.name} can not be designated as teardown to multiple projects (${teardownToSetup.get(teardown)!.project.name} and ${project.project.name})`);
teardownToSetup.set(teardown, project);
teardownSet.add(teardown);
}
}
for (const teardown of teardownToSetup.keys()) {
for (const teardown of teardownSet) {
if (teardown.deps.length)
throw new Error(`Teardown project ${teardown.project.name} must not have dependencies`);
}
for (const project of projects) {
for (const dep of project.deps) {
if (teardownToSetup.has(dep))
if (teardownSet.has(dep))
throw new Error(`Project ${project.project.name} must not depend on a teardown project ${dep.project.name}`);
}
}

View file

@ -49,11 +49,14 @@ export function filterProjects(projects: FullProjectInternal[], projectNames?: s
return result;
}
export function buildTeardownToSetupMap(projects: FullProjectInternal[]): Map<FullProjectInternal, FullProjectInternal> {
const result = new Map<FullProjectInternal, FullProjectInternal>();
export function buildTeardownToSetupsMap(projects: FullProjectInternal[]): Map<FullProjectInternal, FullProjectInternal[]> {
const result = new Map<FullProjectInternal, FullProjectInternal[]>();
for (const project of projects) {
if (project.teardown)
result.set(project.teardown, project);
if (project.teardown) {
const setups = result.get(project.teardown) || [];
setups.push(project);
result.set(project.teardown, setups);
}
}
return result;
}
@ -78,7 +81,7 @@ export function buildProjectsClosure(projects: FullProjectInternal[]): Map<FullP
return result;
}
export function buildDependentProjects(forProject: FullProjectInternal, projects: FullProjectInternal[]): Set<FullProjectInternal> {
export function buildDependentProjects(forProjects: FullProjectInternal[], projects: FullProjectInternal[]): Set<FullProjectInternal> {
const reverseDeps = new Map<FullProjectInternal, FullProjectInternal[]>(projects.map(p => ([p, []])));
for (const project of projects) {
for (const dep of project.deps)
@ -97,7 +100,8 @@ export function buildDependentProjects(forProject: FullProjectInternal, projects
if (project.teardown)
visit(depth + 1, project.teardown);
};
visit(0, forProject);
for (const forProject of forProjects)
visit(0, forProject);
return result;
}

View file

@ -28,7 +28,7 @@ import type { FullConfigInternal, FullProjectInternal } from '../common/config';
import { collectProjectsAndTestFiles, createRootSuite, loadFileSuites, loadGlobalHook } from './loadUtils';
import type { Matcher } from '../util';
import type { Suite } from '../common/test';
import { buildDependentProjects, buildTeardownToSetupMap } from './projectUtils';
import { buildDependentProjects, buildTeardownToSetupsMap } from './projectUtils';
const removeFolderAsync = promisify(rimraf);
const readDirAsync = promisify(fs.readdir);
@ -184,12 +184,12 @@ function createPhasesTask(): Task<TestRun> {
const processed = new Set<FullProjectInternal>();
const projectToSuite = new Map(testRun.rootSuite!.suites.map(suite => [suite._fullProject!, suite]));
const allProjects = [...projectToSuite.keys()];
const teardownToSetup = buildTeardownToSetupMap(allProjects);
const teardownToSetupDependents = new Map<FullProjectInternal, FullProjectInternal[]>();
for (const [teardown, setup] of teardownToSetup) {
const closure = buildDependentProjects(setup, allProjects);
const teardownToSetups = buildTeardownToSetupsMap(allProjects);
const teardownToSetupsDependents = new Map<FullProjectInternal, FullProjectInternal[]>();
for (const [teardown, setups] of teardownToSetups) {
const closure = buildDependentProjects(setups, allProjects);
closure.delete(teardown);
teardownToSetupDependents.set(teardown, [...closure]);
teardownToSetupsDependents.set(teardown, [...closure]);
}
for (let i = 0; i < projectToSuite.size; i++) {
@ -198,7 +198,7 @@ function createPhasesTask(): Task<TestRun> {
for (const project of projectToSuite.keys()) {
if (processed.has(project))
continue;
const projectsThatShouldFinishFirst = [...project.deps, ...(teardownToSetupDependents.get(project) || [])];
const projectsThatShouldFinishFirst = [...project.deps, ...(teardownToSetupsDependents.get(project) || [])];
if (projectsThatShouldFinishFirst.find(p => !processed.has(p)))
continue;
phaseProjects.push(project);
@ -240,7 +240,7 @@ function createRunTestsTask(): Task<TestRun> {
const { phases } = testRun;
const successfulProjects = new Set<FullProjectInternal>();
const extraEnvByProjectId: EnvByProjectId = new Map();
const teardownToSetup = buildTeardownToSetupMap(phases.map(phase => phase.projects.map(p => p.project)).flat());
const teardownToSetups = buildTeardownToSetupsMap(phases.map(phase => phase.projects.map(p => p.project)).flat());
for (const { dispatcher, projects } of phases) {
// Each phase contains dispatcher and a set of test groups.
@ -252,9 +252,8 @@ function createRunTestsTask(): Task<TestRun> {
let extraEnv: Record<string, string | undefined> = {};
for (const dep of project.deps)
extraEnv = { ...extraEnv, ...extraEnvByProjectId.get(dep.id) };
const setupForTeardown = teardownToSetup.get(project);
if (setupForTeardown)
extraEnv = { ...extraEnv, ...extraEnvByProjectId.get(setupForTeardown.id) };
for (const setup of teardownToSetups.get(project) || [])
extraEnv = { ...extraEnv, ...extraEnvByProjectId.get(setup.id) };
extraEnvByProjectId.set(project.id, extraEnv);
const hasFailedDeps = project.deps.some(p => !successfulProjects.has(p));

View file

@ -285,7 +285,7 @@ export interface FullProject<TestArgs = {}, WorkerArgs = {}> {
*/
retries: number;
/**
* Name of a project that needs to run after this and any dependent projects have finished. Teardown is useful to
* Name of a project that needs to run after this and all dependent projects have finished. Teardown is useful to
* cleanup any resources acquired by this project.
*
* Passing `--no-deps` argument ignores
@ -6325,7 +6325,7 @@ interface TestProject {
snapshotPathTemplate?: string;
/**
* Name of a project that needs to run after this and any dependent projects have finished. Teardown is useful to
* Name of a project that needs to run after this and all dependent projects have finished. Teardown is useful to
* cleanup any resources acquired by this project.
*
* Passing `--no-deps` argument ignores

View file

@ -541,23 +541,26 @@ test('should complain about teardown having a dependency', async ({ runInlineTes
expect(result.output).toContain(`Teardown project B must not have dependencies`);
});
test('should complain about teardown used multiple times', async ({ runInlineTest }) => {
test('should support the same teardown used multiple times', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.ts': `
module.exports = {
projects: [
{ name: 'A', teardown: 'C' },
{ name: 'B', teardown: 'C' },
{ name: 'C' },
{ name: 'A', teardown: 'D' },
{ name: 'B', teardown: 'D' },
{ name: 'D' },
],
};`,
'test.spec.ts': `
import { test, expect } from '@playwright/test';
test('test', () => {});
test('test', async ({}, testInfo) => {
console.log('\\n%%' + testInfo.project.name);
});
`,
});
expect(result.exitCode).toBe(1);
expect(result.output).toContain(`Project C can not be designated as teardown to multiple projects (A and B)`);
}, { workers: 1 });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(3);
expect(result.outputLines).toEqual(['A', 'B', 'D']);
});
test('should only apply --repeat-each to top-level', async ({ runInlineTest }) => {