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 * since: v1.34
- type: ?<[string]> - 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. 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[]) { function resolveProjectDependencies(projects: FullProjectInternal[]) {
const teardownToSetup = new Map<FullProjectInternal, FullProjectInternal>(); const teardownSet = new Set<FullProjectInternal>();
for (const project of projects) { for (const project of projects) {
for (const dependencyName of project.project.dependencies) { for (const dependencyName of project.project.dependencies) {
const dependencies = projects.filter(p => p.project.name === dependencyName); 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}`); throw new Error(`Project teardowns should have unique names, reading ${project.project.teardown}`);
const teardown = teardowns[0]; const teardown = teardowns[0];
project.teardown = teardown; project.teardown = teardown;
if (teardownToSetup.has(teardown)) teardownSet.add(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);
} }
} }
for (const teardown of teardownToSetup.keys()) { for (const teardown of teardownSet) {
if (teardown.deps.length) if (teardown.deps.length)
throw new Error(`Teardown project ${teardown.project.name} must not have dependencies`); throw new Error(`Teardown project ${teardown.project.name} must not have dependencies`);
} }
for (const project of projects) { for (const project of projects) {
for (const dep of project.deps) { 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}`); 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; return result;
} }
export function buildTeardownToSetupMap(projects: FullProjectInternal[]): Map<FullProjectInternal, FullProjectInternal> { export function buildTeardownToSetupsMap(projects: FullProjectInternal[]): Map<FullProjectInternal, FullProjectInternal[]> {
const result = new Map<FullProjectInternal, FullProjectInternal>(); const result = new Map<FullProjectInternal, FullProjectInternal[]>();
for (const project of projects) { for (const project of projects) {
if (project.teardown) if (project.teardown) {
result.set(project.teardown, project); const setups = result.get(project.teardown) || [];
setups.push(project);
result.set(project.teardown, setups);
}
} }
return result; return result;
} }
@ -78,7 +81,7 @@ export function buildProjectsClosure(projects: FullProjectInternal[]): Map<FullP
return result; 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, []]))); const reverseDeps = new Map<FullProjectInternal, FullProjectInternal[]>(projects.map(p => ([p, []])));
for (const project of projects) { for (const project of projects) {
for (const dep of project.deps) for (const dep of project.deps)
@ -97,7 +100,8 @@ export function buildDependentProjects(forProject: FullProjectInternal, projects
if (project.teardown) if (project.teardown)
visit(depth + 1, project.teardown); visit(depth + 1, project.teardown);
}; };
visit(0, forProject); for (const forProject of forProjects)
visit(0, forProject);
return result; return result;
} }

View file

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

View file

@ -285,7 +285,7 @@ export interface FullProject<TestArgs = {}, WorkerArgs = {}> {
*/ */
retries: number; 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. * cleanup any resources acquired by this project.
* *
* Passing `--no-deps` argument ignores * Passing `--no-deps` argument ignores
@ -6325,7 +6325,7 @@ interface TestProject {
snapshotPathTemplate?: string; 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. * cleanup any resources acquired by this project.
* *
* Passing `--no-deps` argument ignores * 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`); 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({ const result = await runInlineTest({
'playwright.config.ts': ` 'playwright.config.ts': `
module.exports = { module.exports = {
projects: [ projects: [
{ name: 'A', teardown: 'C' }, { name: 'A', teardown: 'D' },
{ name: 'B', teardown: 'C' }, { name: 'B', teardown: 'D' },
{ name: 'C' }, { name: 'D' },
], ],
};`, };`,
'test.spec.ts': ` 'test.spec.ts': `
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
test('test', () => {}); test('test', async ({}, testInfo) => {
console.log('\\n%%' + testInfo.project.name);
});
`, `,
}); }, { workers: 1 });
expect(result.exitCode).toBe(1); expect(result.exitCode).toBe(0);
expect(result.output).toContain(`Project C can not be designated as teardown to multiple projects (A and B)`); expect(result.passed).toBe(3);
expect(result.outputLines).toEqual(['A', 'B', 'D']);
}); });
test('should only apply --repeat-each to top-level', async ({ runInlineTest }) => { test('should only apply --repeat-each to top-level', async ({ runInlineTest }) => {