feat(teardown): allow the same project to be a teardown for multiple (#23074)
This commit is contained in:
parent
2d3ab74d22
commit
fc2e0e76bd
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
4
packages/playwright-test/types/test.d.ts
vendored
4
packages/playwright-test/types/test.d.ts
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue