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
|
* 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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
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;
|
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
|
||||||
|
|
|
||||||
|
|
@ -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 }) => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue