chore: introduce the concept of a test run (#22243)
This commit is contained in:
parent
9ced1e278d
commit
09f072de09
|
|
@ -216,7 +216,6 @@ export class ConfigLoader {
|
||||||
return {
|
return {
|
||||||
_internal: {
|
_internal: {
|
||||||
id: '',
|
id: '',
|
||||||
type: 'top-level',
|
|
||||||
fullConfig: fullConfig,
|
fullConfig: fullConfig,
|
||||||
fullyParallel: takeFirst(projectConfig.fullyParallel, config.fullyParallel, undefined),
|
fullyParallel: takeFirst(projectConfig.fullyParallel, config.fullyParallel, undefined),
|
||||||
expect: takeFirst(projectConfig.expect, config.expect, {}),
|
expect: takeFirst(projectConfig.expect, config.expect, {}),
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,6 @@ export interface FullConfigInternal extends FullConfigPublic {
|
||||||
|
|
||||||
type ProjectInternal = {
|
type ProjectInternal = {
|
||||||
id: string;
|
id: string;
|
||||||
type: 'top-level' | 'dependency';
|
|
||||||
fullConfig: FullConfigInternal;
|
fullConfig: FullConfigInternal;
|
||||||
fullyParallel: boolean;
|
fullyParallel: boolean;
|
||||||
expect: Project['expect'];
|
expect: Project['expect'];
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import type { FullConfigInternal, FullProjectInternal } from '../common/types';
|
||||||
import { createFileMatcherFromArguments, createFileFiltersFromArguments, createTitleMatcher, errorWithFile, forceRegExp } from '../util';
|
import { createFileMatcherFromArguments, createFileFiltersFromArguments, createTitleMatcher, errorWithFile, forceRegExp } from '../util';
|
||||||
import type { Matcher, TestFileFilter } from '../util';
|
import type { Matcher, TestFileFilter } from '../util';
|
||||||
import { buildProjectsClosure, collectFilesForProject, filterProjects } from './projectUtils';
|
import { buildProjectsClosure, collectFilesForProject, filterProjects } from './projectUtils';
|
||||||
|
import type { TestRun } from './tasks';
|
||||||
import { requireOrImport } from '../common/transform';
|
import { requireOrImport } from '../common/transform';
|
||||||
import { buildFileSuiteForProject, filterByFocusedLine, filterByTestIds, filterOnly, filterTestsRemoveEmptySuites } from '../common/suiteUtils';
|
import { buildFileSuiteForProject, filterByFocusedLine, filterByTestIds, filterOnly, filterTestsRemoveEmptySuites } from '../common/suiteUtils';
|
||||||
import { createTestGroups, filterForShard, type TestGroup } from './testGroups';
|
import { createTestGroups, filterForShard, type TestGroup } from './testGroups';
|
||||||
|
|
@ -30,7 +31,8 @@ import { dependenciesForTestFile } from '../common/compilationCache';
|
||||||
import { sourceMapSupport } from '../utilsBundle';
|
import { sourceMapSupport } from '../utilsBundle';
|
||||||
import type { RawSourceMap } from 'source-map';
|
import type { RawSourceMap } from 'source-map';
|
||||||
|
|
||||||
export async function collectProjectsAndTestFiles(config: FullConfigInternal, projectsToIgnore: Set<FullProjectInternal>, additionalFileMatcher: Matcher | undefined) {
|
export async function collectProjectsAndTestFiles(testRun: TestRun, additionalFileMatcher: Matcher | undefined) {
|
||||||
|
const config = testRun.config;
|
||||||
const fsCache = new Map();
|
const fsCache = new Map();
|
||||||
const sourceMapCache = new Map();
|
const sourceMapCache = new Map();
|
||||||
const cliFileMatcher = config._internal.cliArgs.length ? createFileMatcherFromArguments(config._internal.cliArgs) : null;
|
const cliFileMatcher = config._internal.cliArgs.length ? createFileMatcherFromArguments(config._internal.cliArgs) : null;
|
||||||
|
|
@ -38,8 +40,6 @@ export async function collectProjectsAndTestFiles(config: FullConfigInternal, pr
|
||||||
// First collect all files for the projects in the command line, don't apply any file filters.
|
// First collect all files for the projects in the command line, don't apply any file filters.
|
||||||
const allFilesForProject = new Map<FullProjectInternal, string[]>();
|
const allFilesForProject = new Map<FullProjectInternal, string[]>();
|
||||||
for (const project of filterProjects(config.projects, config._internal.cliProjectFilter)) {
|
for (const project of filterProjects(config.projects, config._internal.cliProjectFilter)) {
|
||||||
if (projectsToIgnore.has(project))
|
|
||||||
continue;
|
|
||||||
const files = await collectFilesForProject(project, fsCache);
|
const files = await collectFilesForProject(project, fsCache);
|
||||||
allFilesForProject.set(project, files);
|
allFilesForProject.set(project, files);
|
||||||
}
|
}
|
||||||
|
|
@ -63,22 +63,26 @@ export async function collectProjectsAndTestFiles(config: FullConfigInternal, pr
|
||||||
}
|
}
|
||||||
|
|
||||||
// (Re-)add all files for dependent projects, disregard filters.
|
// (Re-)add all files for dependent projects, disregard filters.
|
||||||
const projectClosure = buildProjectsClosure([...filesToRunByProject.keys()]).filter(p => !projectsToIgnore.has(p));
|
const projectClosure = buildProjectsClosure([...filesToRunByProject.keys()]);
|
||||||
for (const project of projectClosure) {
|
for (const [project, type] of projectClosure) {
|
||||||
if (project._internal.type === 'dependency') {
|
if (type === 'dependency') {
|
||||||
filesToRunByProject.delete(project);
|
filesToRunByProject.delete(project);
|
||||||
const files = allFilesForProject.get(project) || await collectFilesForProject(project, fsCache);
|
const files = allFilesForProject.get(project) || await collectFilesForProject(project, fsCache);
|
||||||
filesToRunByProject.set(project, files);
|
filesToRunByProject.set(project, files);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return filesToRunByProject;
|
testRun.projects = [...filesToRunByProject.keys()];
|
||||||
|
testRun.projectFiles = filesToRunByProject;
|
||||||
|
testRun.projectType = projectClosure;
|
||||||
|
testRun.projectSuites = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadFileSuites(mode: 'out-of-process' | 'in-process', config: FullConfigInternal, filesToRunByProject: Map<FullProjectInternal, string[]>, errors: TestError[]): Promise<Map<FullProjectInternal, Suite[]>> {
|
export async function loadFileSuites(testRun: TestRun, mode: 'out-of-process' | 'in-process', errors: TestError[]) {
|
||||||
// Determine all files to load.
|
// Determine all files to load.
|
||||||
|
const config = testRun.config;
|
||||||
const allTestFiles = new Set<string>();
|
const allTestFiles = new Set<string>();
|
||||||
for (const files of filesToRunByProject.values())
|
for (const files of testRun.projectFiles.values())
|
||||||
files.forEach(file => allTestFiles.add(file));
|
files.forEach(file => allTestFiles.add(file));
|
||||||
|
|
||||||
// Load test files.
|
// Load test files.
|
||||||
|
|
@ -107,15 +111,14 @@ export async function loadFileSuites(mode: 'out-of-process' | 'in-process', conf
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect file suites for each project.
|
// Collect file suites for each project.
|
||||||
const fileSuitesByProject = new Map<FullProjectInternal, Suite[]>();
|
for (const [project, files] of testRun.projectFiles) {
|
||||||
for (const [project, files] of filesToRunByProject) {
|
|
||||||
const suites = files.map(file => fileSuiteByFile.get(file)).filter(Boolean) as Suite[];
|
const suites = files.map(file => fileSuiteByFile.get(file)).filter(Boolean) as Suite[];
|
||||||
fileSuitesByProject.set(project, suites);
|
testRun.projectSuites.set(project, suites);
|
||||||
}
|
}
|
||||||
return fileSuitesByProject;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createRootSuite(config: FullConfigInternal, fileSuitesByProject: Map<FullProjectInternal, Suite[]>, errors: TestError[], shouldFilterOnly: boolean): Promise<Suite> {
|
export async function createRootSuite(testRun: TestRun, errors: TestError[], shouldFilterOnly: boolean): Promise<Suite> {
|
||||||
|
const config = testRun.config;
|
||||||
// Create root suite, where each child will be a project suite with cloned file suites inside it.
|
// Create root suite, where each child will be a project suite with cloned file suites inside it.
|
||||||
const rootSuite = new Suite('', 'root');
|
const rootSuite = new Suite('', 'root');
|
||||||
|
|
||||||
|
|
@ -128,8 +131,8 @@ export async function createRootSuite(config: FullConfigInternal, fileSuitesByPr
|
||||||
const cliTitleMatcher = (title: string) => !grepInvertMatcher(title) && grepMatcher(title);
|
const cliTitleMatcher = (title: string) => !grepInvertMatcher(title) && grepMatcher(title);
|
||||||
|
|
||||||
// Clone file suites for top-level projects.
|
// Clone file suites for top-level projects.
|
||||||
for (const [project, fileSuites] of fileSuitesByProject) {
|
for (const [project, fileSuites] of testRun.projectSuites) {
|
||||||
if (project._internal.type === 'top-level')
|
if (testRun.projectType.get(project) === 'top-level')
|
||||||
rootSuite._addSuite(await createProjectSuite(fileSuites, project, { cliFileFilters, cliTitleMatcher, testIdMatcher: config._internal.testIdMatcher }));
|
rootSuite._addSuite(await createProjectSuite(fileSuites, project, { cliFileFilters, cliTitleMatcher, testIdMatcher: config._internal.testIdMatcher }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -168,11 +171,11 @@ export async function createRootSuite(config: FullConfigInternal, fileSuitesByPr
|
||||||
{
|
{
|
||||||
// Filtering only and sharding might have reduced the number of top-level 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.
|
// Build the project closure to only include dependencies that are still needed.
|
||||||
const projectClosure = new Set(buildProjectsClosure(rootSuite.suites.map(suite => suite.project() as FullProjectInternal)));
|
const projectClosure = new Map(buildProjectsClosure(rootSuite.suites.map(suite => suite.project() as FullProjectInternal)));
|
||||||
|
|
||||||
// Clone file suites for dependency projects.
|
// Clone file suites for dependency projects.
|
||||||
for (const [project, fileSuites] of fileSuitesByProject) {
|
for (const [project, fileSuites] of testRun.projectSuites) {
|
||||||
if (project._internal.type === 'dependency' && projectClosure.has(project))
|
if (testRun.projectType.get(project) === 'dependency' && projectClosure.has(project))
|
||||||
rootSuite._prependSuite(await createProjectSuite(fileSuites, project, { cliFileFilters: [], cliTitleMatcher: undefined }));
|
rootSuite._prependSuite(await createProjectSuite(fileSuites, project, { cliFileFilters: [], cliTitleMatcher: undefined }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,24 +49,22 @@ export function filterProjects(projects: FullProjectInternal[], projectNames?: s
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildProjectsClosure(projects: FullProjectInternal[]): FullProjectInternal[] {
|
export function buildProjectsClosure(projects: FullProjectInternal[]): Map<FullProjectInternal, 'top-level' | 'dependency'> {
|
||||||
const result = new Set<FullProjectInternal>();
|
const result = new Map<FullProjectInternal, 'top-level' | 'dependency'>();
|
||||||
const visit = (depth: number, project: FullProjectInternal) => {
|
const visit = (depth: number, project: FullProjectInternal) => {
|
||||||
if (depth > 100) {
|
if (depth > 100) {
|
||||||
const error = new Error('Circular dependency detected between projects.');
|
const error = new Error('Circular dependency detected between projects.');
|
||||||
error.stack = '';
|
error.stack = '';
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
if (depth)
|
result.set(project, depth ? 'dependency' : 'top-level');
|
||||||
project._internal.type = 'dependency';
|
|
||||||
result.add(project);
|
|
||||||
project._internal.deps.map(visit.bind(undefined, depth + 1));
|
project._internal.deps.map(visit.bind(undefined, depth + 1));
|
||||||
};
|
};
|
||||||
for (const p of projects)
|
for (const p of projects)
|
||||||
p._internal.type = 'top-level';
|
result.set(p, 'top-level');
|
||||||
for (const p of projects)
|
for (const p of projects)
|
||||||
visit(0, p);
|
visit(0, p);
|
||||||
return [...result];
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function collectFilesForProject(project: FullProjectInternal, fsCache = new Map<string, string[]>()): Promise<string[]> {
|
export async function collectFilesForProject(project: FullProjectInternal, fsCache = new Map<string, string[]>()): Promise<string[]> {
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,7 @@ import type { FullResult } from '../../types/testReporter';
|
||||||
import { webServerPluginsForConfig } from '../plugins/webServerPlugin';
|
import { webServerPluginsForConfig } from '../plugins/webServerPlugin';
|
||||||
import { collectFilesForProject, filterProjects } from './projectUtils';
|
import { collectFilesForProject, filterProjects } from './projectUtils';
|
||||||
import { createReporter } from './reporters';
|
import { createReporter } from './reporters';
|
||||||
import { createTaskRunner, createTaskRunnerForList } from './tasks';
|
import { TestRun, createTaskRunner, createTaskRunnerForList } from './tasks';
|
||||||
import type { TaskRunnerState } from './tasks';
|
|
||||||
import type { FullConfigInternal } from '../common/types';
|
import type { FullConfigInternal } from '../common/types';
|
||||||
import { colors } from 'playwright-core/lib/utilsBundle';
|
import { colors } from 'playwright-core/lib/utilsBundle';
|
||||||
import { runWatchModeLoop } from './watchMode';
|
import { runWatchModeLoop } from './watchMode';
|
||||||
|
|
@ -60,12 +59,7 @@ export class Runner {
|
||||||
const taskRunner = listOnly ? createTaskRunnerForList(config, reporter, 'in-process')
|
const taskRunner = listOnly ? createTaskRunnerForList(config, reporter, 'in-process')
|
||||||
: createTaskRunner(config, reporter);
|
: createTaskRunner(config, reporter);
|
||||||
|
|
||||||
const context: TaskRunnerState = {
|
const testRun = new TestRun(config, reporter);
|
||||||
config,
|
|
||||||
reporter,
|
|
||||||
phases: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
reporter.onConfigure(config);
|
reporter.onConfigure(config);
|
||||||
|
|
||||||
if (!listOnly && config._internal.ignoreSnapshots) {
|
if (!listOnly && config._internal.ignoreSnapshots) {
|
||||||
|
|
@ -77,9 +71,9 @@ export class Runner {
|
||||||
].join('\n')));
|
].join('\n')));
|
||||||
}
|
}
|
||||||
|
|
||||||
const taskStatus = await taskRunner.run(context, deadline);
|
const taskStatus = await taskRunner.run(testRun, deadline);
|
||||||
let status: FullResult['status'] = 'passed';
|
let status: FullResult['status'] = 'passed';
|
||||||
if (context.phases.find(p => p.dispatcher.hasWorkerErrors()) || context.rootSuite?.allTests().some(test => !test.ok()))
|
if (testRun.phases.find(p => p.dispatcher.hasWorkerErrors()) || testRun.rootSuite?.allTests().some(test => !test.ok()))
|
||||||
status = 'failed';
|
status = 'failed';
|
||||||
if (status === 'passed' && taskStatus !== 'passed')
|
if (status === 'passed' && taskStatus !== 'passed')
|
||||||
status = taskStatus;
|
status = taskStatus;
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,10 @@ import type { Multiplexer } from '../reporters/multiplexer';
|
||||||
import { createTestGroups, type TestGroup } from '../runner/testGroups';
|
import { createTestGroups, type TestGroup } from '../runner/testGroups';
|
||||||
import type { Task } from './taskRunner';
|
import type { Task } from './taskRunner';
|
||||||
import { TaskRunner } from './taskRunner';
|
import { TaskRunner } from './taskRunner';
|
||||||
import type { Suite } from '../common/test';
|
|
||||||
import type { FullConfigInternal, FullProjectInternal } from '../common/types';
|
import type { FullConfigInternal, FullProjectInternal } from '../common/types';
|
||||||
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';
|
||||||
|
|
||||||
const removeFolderAsync = promisify(rimraf);
|
const removeFolderAsync = promisify(rimraf);
|
||||||
const readDirAsync = promisify(fs.readdir);
|
const readDirAsync = promisify(fs.readdir);
|
||||||
|
|
@ -38,40 +38,49 @@ type ProjectWithTestGroups = {
|
||||||
testGroups: TestGroup[];
|
testGroups: TestGroup[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type Phase = {
|
export type Phase = {
|
||||||
dispatcher: Dispatcher,
|
dispatcher: Dispatcher,
|
||||||
projects: ProjectWithTestGroups[]
|
projects: ProjectWithTestGroups[]
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TaskRunnerState = {
|
export class TestRun {
|
||||||
reporter: Multiplexer;
|
readonly reporter: Multiplexer;
|
||||||
config: FullConfigInternal;
|
readonly config: FullConfigInternal;
|
||||||
rootSuite?: Suite;
|
rootSuite: Suite | undefined = undefined;
|
||||||
phases: Phase[];
|
readonly phases: Phase[] = [];
|
||||||
};
|
projects: FullProjectInternal[] = [];
|
||||||
|
projectFiles: Map<FullProjectInternal, string[]> = new Map();
|
||||||
|
projectType: Map<FullProjectInternal, 'top-level' | 'dependency'> = new Map();
|
||||||
|
projectSuites: Map<FullProjectInternal, Suite[]> = new Map();
|
||||||
|
|
||||||
export function createTaskRunner(config: FullConfigInternal, reporter: Multiplexer): TaskRunner<TaskRunnerState> {
|
constructor(config: FullConfigInternal, reporter: Multiplexer) {
|
||||||
const taskRunner = new TaskRunner<TaskRunnerState>(reporter, config.globalTimeout);
|
this.config = config;
|
||||||
|
this.reporter = reporter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createTaskRunner(config: FullConfigInternal, reporter: Multiplexer): TaskRunner<TestRun> {
|
||||||
|
const taskRunner = new TaskRunner<TestRun>(reporter, config.globalTimeout);
|
||||||
addGlobalSetupTasks(taskRunner, config);
|
addGlobalSetupTasks(taskRunner, config);
|
||||||
taskRunner.addTask('load tests', createLoadTask('in-process', true));
|
taskRunner.addTask('load tests', createLoadTask('in-process', true));
|
||||||
addRunTasks(taskRunner, config);
|
addRunTasks(taskRunner, config);
|
||||||
return taskRunner;
|
return taskRunner;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createTaskRunnerForWatchSetup(config: FullConfigInternal, reporter: Multiplexer): TaskRunner<TaskRunnerState> {
|
export function createTaskRunnerForWatchSetup(config: FullConfigInternal, reporter: Multiplexer): TaskRunner<TestRun> {
|
||||||
const taskRunner = new TaskRunner<TaskRunnerState>(reporter, 0);
|
const taskRunner = new TaskRunner<TestRun>(reporter, 0);
|
||||||
addGlobalSetupTasks(taskRunner, config);
|
addGlobalSetupTasks(taskRunner, config);
|
||||||
return taskRunner;
|
return taskRunner;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createTaskRunnerForWatch(config: FullConfigInternal, reporter: Multiplexer, projectsToIgnore?: Set<FullProjectInternal>, additionalFileMatcher?: Matcher): TaskRunner<TaskRunnerState> {
|
export function createTaskRunnerForWatch(config: FullConfigInternal, reporter: Multiplexer, additionalFileMatcher?: Matcher): TaskRunner<TestRun> {
|
||||||
const taskRunner = new TaskRunner<TaskRunnerState>(reporter, 0);
|
const taskRunner = new TaskRunner<TestRun>(reporter, 0);
|
||||||
taskRunner.addTask('load tests', createLoadTask('out-of-process', true, projectsToIgnore, additionalFileMatcher));
|
taskRunner.addTask('load tests', createLoadTask('out-of-process', true, additionalFileMatcher));
|
||||||
addRunTasks(taskRunner, config);
|
addRunTasks(taskRunner, config);
|
||||||
return taskRunner;
|
return taskRunner;
|
||||||
}
|
}
|
||||||
|
|
||||||
function addGlobalSetupTasks(taskRunner: TaskRunner<TaskRunnerState>, config: FullConfigInternal) {
|
function addGlobalSetupTasks(taskRunner: TaskRunner<TestRun>, config: FullConfigInternal) {
|
||||||
for (const plugin of config._internal.plugins)
|
for (const plugin of config._internal.plugins)
|
||||||
taskRunner.addTask('plugin setup', createPluginSetupTask(plugin));
|
taskRunner.addTask('plugin setup', createPluginSetupTask(plugin));
|
||||||
if (config.globalSetup || config.globalTeardown)
|
if (config.globalSetup || config.globalTeardown)
|
||||||
|
|
@ -79,7 +88,7 @@ function addGlobalSetupTasks(taskRunner: TaskRunner<TaskRunnerState>, config: Fu
|
||||||
taskRunner.addTask('clear output', createRemoveOutputDirsTask());
|
taskRunner.addTask('clear output', createRemoveOutputDirsTask());
|
||||||
}
|
}
|
||||||
|
|
||||||
function addRunTasks(taskRunner: TaskRunner<TaskRunnerState>, config: FullConfigInternal) {
|
function addRunTasks(taskRunner: TaskRunner<TestRun>, config: FullConfigInternal) {
|
||||||
taskRunner.addTask('create phases', createPhasesTask());
|
taskRunner.addTask('create phases', createPhasesTask());
|
||||||
taskRunner.addTask('report begin', async ({ reporter, rootSuite }) => {
|
taskRunner.addTask('report begin', async ({ reporter, rootSuite }) => {
|
||||||
reporter.onBegin?.(config, rootSuite!);
|
reporter.onBegin?.(config, rootSuite!);
|
||||||
|
|
@ -92,8 +101,8 @@ function addRunTasks(taskRunner: TaskRunner<TaskRunnerState>, config: FullConfig
|
||||||
return taskRunner;
|
return taskRunner;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createTaskRunnerForList(config: FullConfigInternal, reporter: Multiplexer, mode: 'in-process' | 'out-of-process'): TaskRunner<TaskRunnerState> {
|
export function createTaskRunnerForList(config: FullConfigInternal, reporter: Multiplexer, mode: 'in-process' | 'out-of-process'): TaskRunner<TestRun> {
|
||||||
const taskRunner = new TaskRunner<TaskRunnerState>(reporter, config.globalTimeout);
|
const taskRunner = new TaskRunner<TestRun>(reporter, config.globalTimeout);
|
||||||
taskRunner.addTask('load tests', createLoadTask(mode, false));
|
taskRunner.addTask('load tests', createLoadTask(mode, false));
|
||||||
taskRunner.addTask('report begin', async ({ reporter, rootSuite }) => {
|
taskRunner.addTask('report begin', async ({ reporter, rootSuite }) => {
|
||||||
reporter.onBegin?.(config, rootSuite!);
|
reporter.onBegin?.(config, rootSuite!);
|
||||||
|
|
@ -102,7 +111,7 @@ export function createTaskRunnerForList(config: FullConfigInternal, reporter: Mu
|
||||||
return taskRunner;
|
return taskRunner;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createPluginSetupTask(plugin: TestRunnerPluginRegistration): Task<TaskRunnerState> {
|
function createPluginSetupTask(plugin: TestRunnerPluginRegistration): Task<TestRun> {
|
||||||
return async ({ config, reporter }) => {
|
return async ({ config, reporter }) => {
|
||||||
if (typeof plugin.factory === 'function')
|
if (typeof plugin.factory === 'function')
|
||||||
plugin.instance = await plugin.factory();
|
plugin.instance = await plugin.factory();
|
||||||
|
|
@ -113,14 +122,14 @@ function createPluginSetupTask(plugin: TestRunnerPluginRegistration): Task<TaskR
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createPluginBeginTask(plugin: TestRunnerPluginRegistration): Task<TaskRunnerState> {
|
function createPluginBeginTask(plugin: TestRunnerPluginRegistration): Task<TestRun> {
|
||||||
return async ({ rootSuite }) => {
|
return async ({ rootSuite }) => {
|
||||||
await plugin.instance?.begin?.(rootSuite!);
|
await plugin.instance?.begin?.(rootSuite!);
|
||||||
return () => plugin.instance?.end?.();
|
return () => plugin.instance?.end?.();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createGlobalSetupTask(): Task<TaskRunnerState> {
|
function createGlobalSetupTask(): Task<TestRun> {
|
||||||
return async ({ config }) => {
|
return async ({ config }) => {
|
||||||
const setupHook = config.globalSetup ? await loadGlobalHook(config, config.globalSetup) : undefined;
|
const setupHook = config.globalSetup ? await loadGlobalHook(config, config.globalSetup) : undefined;
|
||||||
const teardownHook = config.globalTeardown ? await loadGlobalHook(config, config.globalTeardown) : undefined;
|
const teardownHook = config.globalTeardown ? await loadGlobalHook(config, config.globalTeardown) : undefined;
|
||||||
|
|
@ -133,7 +142,7 @@ function createGlobalSetupTask(): Task<TaskRunnerState> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createRemoveOutputDirsTask(): Task<TaskRunnerState> {
|
function createRemoveOutputDirsTask(): Task<TestRun> {
|
||||||
return async ({ config }) => {
|
return async ({ config }) => {
|
||||||
const outputDirs = new Set<string>();
|
const outputDirs = new Set<string>();
|
||||||
for (const p of config.projects) {
|
for (const p of config.projects) {
|
||||||
|
|
@ -155,24 +164,23 @@ function createRemoveOutputDirsTask(): Task<TaskRunnerState> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createLoadTask(mode: 'out-of-process' | 'in-process', shouldFilterOnly: boolean, projectsToIgnore = new Set<FullProjectInternal>(), additionalFileMatcher?: Matcher): Task<TaskRunnerState> {
|
function createLoadTask(mode: 'out-of-process' | 'in-process', shouldFilterOnly: boolean, additionalFileMatcher?: Matcher): Task<TestRun> {
|
||||||
return async (context, errors) => {
|
return async (testRun, errors) => {
|
||||||
const { config } = context;
|
await collectProjectsAndTestFiles(testRun, additionalFileMatcher);
|
||||||
const filesToRunByProject = await collectProjectsAndTestFiles(config, projectsToIgnore, additionalFileMatcher);
|
await loadFileSuites(testRun, mode, errors);
|
||||||
const fileSuitesByProject = await loadFileSuites(mode, config, filesToRunByProject, errors);
|
testRun.rootSuite = await createRootSuite(testRun, errors, shouldFilterOnly);
|
||||||
context.rootSuite = await createRootSuite(config, fileSuitesByProject, errors, shouldFilterOnly);
|
|
||||||
// Fail when no tests.
|
// Fail when no tests.
|
||||||
if (!context.rootSuite.allTests().length && !config._internal.passWithNoTests && !config.shard)
|
if (!testRun.rootSuite.allTests().length && !testRun.config._internal.passWithNoTests && !testRun.config.shard)
|
||||||
throw new Error(`No tests found`);
|
throw new Error(`No tests found`);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createPhasesTask(): Task<TaskRunnerState> {
|
function createPhasesTask(): Task<TestRun> {
|
||||||
return async context => {
|
return async testRun => {
|
||||||
context.config._internal.maxConcurrentTestGroups = 0;
|
testRun.config._internal.maxConcurrentTestGroups = 0;
|
||||||
|
|
||||||
const processed = new Set<FullProjectInternal>();
|
const processed = new Set<FullProjectInternal>();
|
||||||
const projectToSuite = new Map(context.rootSuite!.suites.map(suite => [suite.project() as FullProjectInternal, suite]));
|
const projectToSuite = new Map(testRun.rootSuite!.suites.map(suite => [suite.project() as FullProjectInternal, suite]));
|
||||||
for (let i = 0; i < projectToSuite.size; i++) {
|
for (let i = 0; i < projectToSuite.size; i++) {
|
||||||
// Find all projects that have all their dependencies processed by previous phases.
|
// Find all projects that have all their dependencies processed by previous phases.
|
||||||
const phaseProjects: FullProjectInternal[] = [];
|
const phaseProjects: FullProjectInternal[] = [];
|
||||||
|
|
@ -189,22 +197,22 @@ function createPhasesTask(): Task<TaskRunnerState> {
|
||||||
processed.add(project);
|
processed.add(project);
|
||||||
if (phaseProjects.length) {
|
if (phaseProjects.length) {
|
||||||
let testGroupsInPhase = 0;
|
let testGroupsInPhase = 0;
|
||||||
const phase: Phase = { dispatcher: new Dispatcher(context.config, context.reporter), projects: [] };
|
const phase: Phase = { dispatcher: new Dispatcher(testRun.config, testRun.reporter), projects: [] };
|
||||||
context.phases.push(phase);
|
testRun.phases.push(phase);
|
||||||
for (const project of phaseProjects) {
|
for (const project of phaseProjects) {
|
||||||
const projectSuite = projectToSuite.get(project)!;
|
const projectSuite = projectToSuite.get(project)!;
|
||||||
const testGroups = createTestGroups(projectSuite, context.config.workers);
|
const testGroups = createTestGroups(projectSuite, testRun.config.workers);
|
||||||
phase.projects.push({ project, projectSuite, testGroups });
|
phase.projects.push({ project, projectSuite, testGroups });
|
||||||
testGroupsInPhase += testGroups.length;
|
testGroupsInPhase += testGroups.length;
|
||||||
}
|
}
|
||||||
debug('pw:test:task')(`created phase #${context.phases.length} with ${phase.projects.map(p => p.project.name).sort()} projects, ${testGroupsInPhase} testGroups`);
|
debug('pw:test:task')(`created phase #${testRun.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);
|
testRun.config._internal.maxConcurrentTestGroups = Math.max(testRun.config._internal.maxConcurrentTestGroups, testGroupsInPhase);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createWorkersTask(): Task<TaskRunnerState> {
|
function createWorkersTask(): Task<TestRun> {
|
||||||
return async ({ phases }) => {
|
return async ({ phases }) => {
|
||||||
return async () => {
|
return async () => {
|
||||||
for (const { dispatcher } of phases.reverse())
|
for (const { dispatcher } of phases.reverse())
|
||||||
|
|
@ -213,9 +221,9 @@ function createWorkersTask(): Task<TaskRunnerState> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createRunTestsTask(): Task<TaskRunnerState> {
|
function createRunTestsTask(): Task<TestRun> {
|
||||||
return async context => {
|
return async testRun => {
|
||||||
const { phases } = context;
|
const { phases } = testRun;
|
||||||
const successfulProjects = new Set<FullProjectInternal>();
|
const successfulProjects = new Set<FullProjectInternal>();
|
||||||
const extraEnvByProjectId: EnvByProjectId = new Map();
|
const extraEnvByProjectId: EnvByProjectId = new Map();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,7 @@ import type { FullConfigInternal } from '../common/types';
|
||||||
import { Multiplexer } from '../reporters/multiplexer';
|
import { Multiplexer } from '../reporters/multiplexer';
|
||||||
import { TeleReporterEmitter } from '../reporters/teleEmitter';
|
import { TeleReporterEmitter } from '../reporters/teleEmitter';
|
||||||
import { createReporter } from './reporters';
|
import { createReporter } from './reporters';
|
||||||
import type { TaskRunnerState } from './tasks';
|
import { TestRun, createTaskRunnerForList, createTaskRunnerForWatch, createTaskRunnerForWatchSetup } from './tasks';
|
||||||
import { createTaskRunnerForList, createTaskRunnerForWatch, createTaskRunnerForWatchSetup } from './tasks';
|
|
||||||
import { chokidar } from '../utilsBundle';
|
import { chokidar } from '../utilsBundle';
|
||||||
import type { FSWatcher } from 'chokidar';
|
import type { FSWatcher } from 'chokidar';
|
||||||
import { open } from '../utilsBundle';
|
import { open } from '../utilsBundle';
|
||||||
|
|
@ -72,12 +71,8 @@ class UIMode {
|
||||||
const reporter = new Multiplexer([new ListReporter()]);
|
const reporter = new Multiplexer([new ListReporter()]);
|
||||||
const taskRunner = createTaskRunnerForWatchSetup(this._config, reporter);
|
const taskRunner = createTaskRunnerForWatchSetup(this._config, reporter);
|
||||||
reporter.onConfigure(this._config);
|
reporter.onConfigure(this._config);
|
||||||
const context: TaskRunnerState = {
|
const testRun = new TestRun(this._config, reporter);
|
||||||
config: this._config,
|
const { status, cleanup: globalCleanup } = await taskRunner.runDeferCleanup(testRun, 0);
|
||||||
reporter,
|
|
||||||
phases: [],
|
|
||||||
};
|
|
||||||
const { status, cleanup: globalCleanup } = await taskRunner.runDeferCleanup(context, 0);
|
|
||||||
await reporter.onExit({ status });
|
await reporter.onExit({ status });
|
||||||
if (status !== 'passed') {
|
if (status !== 'passed') {
|
||||||
await globalCleanup();
|
await globalCleanup();
|
||||||
|
|
@ -156,10 +151,10 @@ class UIMode {
|
||||||
this._config._internal.listOnly = true;
|
this._config._internal.listOnly = true;
|
||||||
this._config._internal.testIdMatcher = undefined;
|
this._config._internal.testIdMatcher = undefined;
|
||||||
const taskRunner = createTaskRunnerForList(this._config, reporter, 'out-of-process');
|
const taskRunner = createTaskRunnerForList(this._config, reporter, 'out-of-process');
|
||||||
const context: TaskRunnerState = { config: this._config, reporter, phases: [] };
|
const testRun = new TestRun(this._config, reporter);
|
||||||
clearCompilationCache();
|
clearCompilationCache();
|
||||||
reporter.onConfigure(this._config);
|
reporter.onConfigure(this._config);
|
||||||
const status = await taskRunner.run(context, 0);
|
const status = await taskRunner.run(testRun, 0);
|
||||||
await reporter.onExit({ status });
|
await reporter.onExit({ status });
|
||||||
|
|
||||||
const projectDirs = new Set<string>();
|
const projectDirs = new Set<string>();
|
||||||
|
|
@ -178,11 +173,11 @@ class UIMode {
|
||||||
const runReporter = new TeleReporterEmitter(e => this._dispatchEvent(e));
|
const runReporter = new TeleReporterEmitter(e => this._dispatchEvent(e));
|
||||||
const reporter = await createReporter(this._config, 'ui', [runReporter]);
|
const reporter = await createReporter(this._config, 'ui', [runReporter]);
|
||||||
const taskRunner = createTaskRunnerForWatch(this._config, reporter);
|
const taskRunner = createTaskRunnerForWatch(this._config, reporter);
|
||||||
const context: TaskRunnerState = { config: this._config, reporter, phases: [] };
|
const testRun = new TestRun(this._config, reporter);
|
||||||
clearCompilationCache();
|
clearCompilationCache();
|
||||||
reporter.onConfigure(this._config);
|
reporter.onConfigure(this._config);
|
||||||
const stop = new ManualPromise();
|
const stop = new ManualPromise();
|
||||||
const run = taskRunner.run(context, 0, stop).then(async status => {
|
const run = taskRunner.run(testRun, 0, stop).then(async status => {
|
||||||
await reporter.onExit({ status });
|
await reporter.onExit({ status });
|
||||||
this._testRun = undefined;
|
this._testRun = undefined;
|
||||||
this._config._internal.testIdMatcher = undefined;
|
this._config._internal.testIdMatcher = undefined;
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,7 @@ import type { FullConfigInternal, FullProjectInternal } from '../common/types';
|
||||||
import { Multiplexer } from '../reporters/multiplexer';
|
import { Multiplexer } from '../reporters/multiplexer';
|
||||||
import { createFileMatcher, createFileMatcherFromArguments } from '../util';
|
import { createFileMatcher, createFileMatcherFromArguments } from '../util';
|
||||||
import type { Matcher } from '../util';
|
import type { Matcher } from '../util';
|
||||||
import { createTaskRunnerForWatch, createTaskRunnerForWatchSetup } from './tasks';
|
import { TestRun, createTaskRunnerForWatch, createTaskRunnerForWatchSetup } from './tasks';
|
||||||
import type { TaskRunnerState } from './tasks';
|
|
||||||
import { buildProjectsClosure, filterProjects } from './projectUtils';
|
import { buildProjectsClosure, filterProjects } from './projectUtils';
|
||||||
import { clearCompilationCache, collectAffectedTestFiles } from '../common/compilationCache';
|
import { clearCompilationCache, collectAffectedTestFiles } from '../common/compilationCache';
|
||||||
import type { FullResult } from 'packages/playwright-test/reporter';
|
import type { FullResult } from 'packages/playwright-test/reporter';
|
||||||
|
|
@ -45,13 +44,13 @@ class FSWatcher {
|
||||||
const projects = filterProjects(config.projects, config._internal.cliProjectFilter);
|
const projects = filterProjects(config.projects, config._internal.cliProjectFilter);
|
||||||
const projectClosure = buildProjectsClosure(projects);
|
const projectClosure = buildProjectsClosure(projects);
|
||||||
const projectFilters = new Map<FullProjectInternal, Matcher>();
|
const projectFilters = new Map<FullProjectInternal, Matcher>();
|
||||||
for (const project of projectClosure) {
|
for (const [project, type] of projectClosure) {
|
||||||
const testMatch = createFileMatcher(project.testMatch);
|
const testMatch = createFileMatcher(project.testMatch);
|
||||||
const testIgnore = createFileMatcher(project.testIgnore);
|
const testIgnore = createFileMatcher(project.testIgnore);
|
||||||
projectFilters.set(project, file => {
|
projectFilters.set(project, file => {
|
||||||
if (!file.startsWith(project.testDir) || !testMatch(file) || testIgnore(file))
|
if (!file.startsWith(project.testDir) || !testMatch(file) || testIgnore(file))
|
||||||
return false;
|
return false;
|
||||||
return project._internal.type === 'dependency' || commandLineFileMatcher(file);
|
return type === 'dependency' || commandLineFileMatcher(file);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -60,7 +59,7 @@ class FSWatcher {
|
||||||
if (this._watcher)
|
if (this._watcher)
|
||||||
await this._watcher.close();
|
await this._watcher.close();
|
||||||
|
|
||||||
this._watcher = chokidar.watch(projectClosure.map(p => p.testDir), { ignoreInitial: true }).on('all', async (event, file) => {
|
this._watcher = chokidar.watch([...projectClosure.keys()].map(p => p.testDir), { ignoreInitial: true }).on('all', async (event, file) => {
|
||||||
if (event !== 'add' && event !== 'change')
|
if (event !== 'add' && event !== 'change')
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
@ -115,14 +114,10 @@ export async function runWatchModeLoop(config: FullConfigInternal): Promise<Full
|
||||||
|
|
||||||
// Perform global setup.
|
// Perform global setup.
|
||||||
const reporter = await createReporter(config, 'watch');
|
const reporter = await createReporter(config, 'watch');
|
||||||
const context: TaskRunnerState = {
|
const testRun = new TestRun(config, reporter);
|
||||||
config,
|
|
||||||
reporter,
|
|
||||||
phases: [],
|
|
||||||
};
|
|
||||||
const taskRunner = createTaskRunnerForWatchSetup(config, reporter);
|
const taskRunner = createTaskRunnerForWatchSetup(config, reporter);
|
||||||
reporter.onConfigure(config);
|
reporter.onConfigure(config);
|
||||||
const { status, cleanup: globalCleanup } = await taskRunner.runDeferCleanup(context, 0);
|
const { status, cleanup: globalCleanup } = await taskRunner.runDeferCleanup(testRun, 0);
|
||||||
if (status !== 'passed')
|
if (status !== 'passed')
|
||||||
return await globalCleanup();
|
return await globalCleanup();
|
||||||
|
|
||||||
|
|
@ -266,14 +261,13 @@ async function runChangedTests(config: FullConfigInternal, failedTestIdCollector
|
||||||
// Prepare to exclude all the projects that do not depend on this file, as if they did not exist.
|
// Prepare to exclude all the projects that do not depend on this file, as if they did not exist.
|
||||||
const projects = filterProjects(config.projects, config._internal.cliProjectFilter);
|
const projects = filterProjects(config.projects, config._internal.cliProjectFilter);
|
||||||
const projectClosure = buildProjectsClosure(projects);
|
const projectClosure = buildProjectsClosure(projects);
|
||||||
const affectedProjects = affectedProjectsClosure(projectClosure, [...filesByProject.keys()]);
|
const affectedProjects = affectedProjectsClosure([...projectClosure.keys()], [...filesByProject.keys()]);
|
||||||
const affectsAnyDependency = [...affectedProjects].some(p => p._internal.type === 'dependency');
|
const affectsAnyDependency = [...affectedProjects].some(p => projectClosure.get(p) === 'dependency');
|
||||||
const projectsToIgnore = new Set(projectClosure.filter(p => !affectedProjects.has(p)));
|
|
||||||
|
|
||||||
// If there are affected dependency projects, do the full run, respect the original CLI.
|
// If there are affected dependency projects, do the full run, respect the original CLI.
|
||||||
// if there are no affected dependency projects, intersect CLI with dirty files
|
// if there are no affected dependency projects, intersect CLI with dirty files
|
||||||
const additionalFileMatcher = affectsAnyDependency ? () => true : (file: string) => testFiles.has(file);
|
const additionalFileMatcher = affectsAnyDependency ? () => true : (file: string) => testFiles.has(file);
|
||||||
await runTests(config, failedTestIdCollector, { projectsToIgnore, additionalFileMatcher, title: title || 'files changed' });
|
await runTests(config, failedTestIdCollector, { additionalFileMatcher, title: title || 'files changed' });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runTests(config: FullConfigInternal, failedTestIdCollector: Set<string>, options?: {
|
async function runTests(config: FullConfigInternal, failedTestIdCollector: Set<string>, options?: {
|
||||||
|
|
@ -283,19 +277,15 @@ async function runTests(config: FullConfigInternal, failedTestIdCollector: Set<s
|
||||||
}) {
|
}) {
|
||||||
printConfiguration(config, options?.title);
|
printConfiguration(config, options?.title);
|
||||||
const reporter = new Multiplexer([new ListReporter()]);
|
const reporter = new Multiplexer([new ListReporter()]);
|
||||||
const taskRunner = createTaskRunnerForWatch(config, reporter, options?.projectsToIgnore, options?.additionalFileMatcher);
|
const taskRunner = createTaskRunnerForWatch(config, reporter, options?.additionalFileMatcher);
|
||||||
const context: TaskRunnerState = {
|
const testRun = new TestRun(config, reporter);
|
||||||
config,
|
|
||||||
reporter,
|
|
||||||
phases: [],
|
|
||||||
};
|
|
||||||
clearCompilationCache();
|
clearCompilationCache();
|
||||||
reporter.onConfigure(config);
|
reporter.onConfigure(config);
|
||||||
const taskStatus = await taskRunner.run(context, 0);
|
const taskStatus = await taskRunner.run(testRun, 0);
|
||||||
let status: FullResult['status'] = 'passed';
|
let status: FullResult['status'] = 'passed';
|
||||||
|
|
||||||
let hasFailedTests = false;
|
let hasFailedTests = false;
|
||||||
for (const test of context.rootSuite?.allTests() || []) {
|
for (const test of testRun.rootSuite?.allTests() || []) {
|
||||||
if (test.outcome() === 'unexpected') {
|
if (test.outcome() === 'unexpected') {
|
||||||
failedTestIdCollector.add(test.id);
|
failedTestIdCollector.add(test.id);
|
||||||
hasFailedTests = true;
|
hasFailedTests = true;
|
||||||
|
|
@ -304,7 +294,7 @@ async function runTests(config: FullConfigInternal, failedTestIdCollector: Set<s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.phases.find(p => p.dispatcher.hasWorkerErrors()) || hasFailedTests)
|
if (testRun.phases.find(p => p.dispatcher.hasWorkerErrors()) || hasFailedTests)
|
||||||
status = 'failed';
|
status = 'failed';
|
||||||
if (status === 'passed' && taskStatus !== 'passed')
|
if (status === 'passed' && taskStatus !== 'passed')
|
||||||
status = taskStatus;
|
status = taskStatus;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue