chore(test runner): simplify code around running tasks (#32557)

This avoids complex boilerplate around `onConfigure`/`onEnd`/`onExit`
and managing the resulting status.
This commit is contained in:
Dmitry Gozman 2024-09-11 13:09:00 -07:00 committed by GitHub
parent 6f52834f74
commit 29a0f49e9b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 154 additions and 217 deletions

View File

@ -15,12 +15,11 @@
* limitations under the License. * limitations under the License.
*/ */
import { monotonicTime } from 'playwright-core/lib/utils';
import type { FullResult, TestError } from '../../types/testReporter'; import type { FullResult, TestError } from '../../types/testReporter';
import { webServerPluginsForConfig } from '../plugins/webServerPlugin'; import { webServerPluginsForConfig } from '../plugins/webServerPlugin';
import { collectFilesForProject, filterProjects } from './projectUtils'; import { collectFilesForProject, filterProjects } from './projectUtils';
import { createErrorCollectingReporter, createReporters } from './reporters'; import { createErrorCollectingReporter, createReporters } from './reporters';
import { TestRun, createTaskRunner, createTaskRunnerForClearCache, createTaskRunnerForDevServer, createTaskRunnerForList, createTaskRunnerForRelatedTestFiles } from './tasks'; import { TestRun, createClearCacheTask, createGlobalSetupTasks, createLoadTask, createPluginSetupTasks, createReportBeginTask, createRunTestsTasks, createStartDevServerTask, runTasks } from './tasks';
import type { FullConfigInternal } from '../common/config'; import type { FullConfigInternal } from '../common/config';
import { affectedTestFiles } from '../transform/compilationCache'; import { affectedTestFiles } from '../transform/compilationCache';
import { InternalReporter } from '../reporters/internalReporter'; import { InternalReporter } from '../reporters/internalReporter';
@ -69,7 +68,6 @@ export class Runner {
async runAllTests(): Promise<FullResult['status']> { async runAllTests(): Promise<FullResult['status']> {
const config = this._config; const config = this._config;
const listOnly = config.cliListOnly; const listOnly = config.cliListOnly;
const deadline = config.config.globalTimeout ? monotonicTime() + config.config.globalTimeout : 0;
// Legacy webServer support. // Legacy webServer support.
webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p })); webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p }));
@ -80,24 +78,15 @@ export class Runner {
await lastRun.filterLastFailed(); await lastRun.filterLastFailed();
const reporter = new InternalReporter([...reporters, lastRun]); const reporter = new InternalReporter([...reporters, lastRun]);
const taskRunner = listOnly ? createTaskRunnerForList( const tasks = listOnly ? [
config, createLoadTask('in-process', { failOnLoadErrors: true, filterOnly: false }),
reporter, createReportBeginTask(),
'in-process', ] : [
{ failOnLoadErrors: true }) : createTaskRunner(config, reporter); ...createGlobalSetupTasks(config),
createLoadTask('in-process', { filterOnly: true, failOnLoadErrors: true }),
const testRun = new TestRun(config); ...createRunTestsTasks(config),
reporter.onConfigure(config.config); ];
const status = await runTasks(new TestRun(config, reporter), tasks, config.config.globalTimeout);
const taskStatus = await taskRunner.run(testRun, deadline);
let status: FullResult['status'] = testRun.failureTracker.result();
if (status === 'passed' && taskStatus !== 'passed')
status = taskStatus;
const modifiedResult = await reporter.onEnd({ status });
if (modifiedResult && modifiedResult.status)
status = modifiedResult.status;
await reporter.onExit();
// Calling process.exit() might truncate large stdout/stderr output. // Calling process.exit() might truncate large stdout/stderr output.
// See https://github.com/nodejs/node/issues/6456. // See https://github.com/nodejs/node/issues/6456.
@ -110,12 +99,10 @@ export class Runner {
async findRelatedTestFiles(files: string[]): Promise<FindRelatedTestFilesReport> { async findRelatedTestFiles(files: string[]): Promise<FindRelatedTestFilesReport> {
const errorReporter = createErrorCollectingReporter(); const errorReporter = createErrorCollectingReporter();
const reporter = new InternalReporter([errorReporter]); const reporter = new InternalReporter([errorReporter]);
const taskRunner = createTaskRunnerForRelatedTestFiles(this._config, reporter, 'in-process', true); const status = await runTasks(new TestRun(this._config, reporter), [
const testRun = new TestRun(this._config); ...createPluginSetupTasks(this._config),
reporter.onConfigure(this._config.config); createLoadTask('in-process', { failOnLoadErrors: true, filterOnly: false, populateDependencies: true }),
const status = await taskRunner.run(testRun, 0); ]);
await reporter.onEnd({ status });
await reporter.onExit();
if (status !== 'passed') if (status !== 'passed')
return { errors: errorReporter.errors(), testFiles: [] }; return { errors: errorReporter.errors(), testFiles: [] };
return { testFiles: affectedTestFiles(files) }; return { testFiles: affectedTestFiles(files) };
@ -123,23 +110,21 @@ export class Runner {
async runDevServer() { async runDevServer() {
const reporter = new InternalReporter([createErrorCollectingReporter(true)]); const reporter = new InternalReporter([createErrorCollectingReporter(true)]);
const taskRunner = createTaskRunnerForDevServer(this._config, reporter, 'in-process', true); const status = await runTasks(new TestRun(this._config, reporter), [
const testRun = new TestRun(this._config); ...createPluginSetupTasks(this._config),
reporter.onConfigure(this._config.config); createLoadTask('in-process', { failOnLoadErrors: true, filterOnly: false }),
const status = await taskRunner.run(testRun, 0); createStartDevServerTask(),
await reporter.onEnd({ status }); { title: 'wait until interrupted', setup: async () => new Promise(() => {}) },
await reporter.onExit(); ]);
return { status }; return { status };
} }
async clearCache() { async clearCache() {
const reporter = new InternalReporter([createErrorCollectingReporter(true)]); const reporter = new InternalReporter([createErrorCollectingReporter(true)]);
const taskRunner = createTaskRunnerForClearCache(this._config, reporter, 'in-process', true); const status = await runTasks(new TestRun(this._config, reporter), [
const testRun = new TestRun(this._config); ...createPluginSetupTasks(this._config),
reporter.onConfigure(this._config.config); createClearCacheTask(this._config),
const status = await taskRunner.run(testRun, 0); ]);
await reporter.onEnd({ status });
await reporter.onExit();
return { status }; return { status };
} }
} }

View File

@ -19,31 +19,26 @@ import { ManualPromise, monotonicTime } from 'playwright-core/lib/utils';
import type { FullResult, TestError } from '../../types/testReporter'; import type { FullResult, TestError } from '../../types/testReporter';
import { SigIntWatcher } from './sigIntWatcher'; import { SigIntWatcher } from './sigIntWatcher';
import { serializeError } from '../util'; import { serializeError } from '../util';
import type { ReporterV2 } from '../reporters/reporterV2';
import type { InternalReporter } from '../reporters/internalReporter'; import type { InternalReporter } from '../reporters/internalReporter';
type TaskPhase<Context> = (reporter: ReporterV2, context: Context, errors: TestError[], softErrors: TestError[]) => Promise<void> | void; type TaskPhase<Context> = (context: Context, errors: TestError[], softErrors: TestError[]) => Promise<void> | void;
export type Task<Context> = { setup?: TaskPhase<Context>, teardown?: TaskPhase<Context> }; export type Task<Context> = { title: string, setup?: TaskPhase<Context>, teardown?: TaskPhase<Context> };
export class TaskRunner<Context> { export class TaskRunner<Context> {
private _tasks: { name: string, task: Task<Context> }[] = []; private _tasks: Task<Context>[] = [];
private _reporter: InternalReporter; private _reporter: InternalReporter;
private _hasErrors = false; private _hasErrors = false;
private _interrupted = false; private _interrupted = false;
private _isTearDown = false; private _isTearDown = false;
private _globalTimeoutForError: number; private _globalTimeoutForError: number;
static create<Context>(reporter: InternalReporter, globalTimeoutForError: number = 0) { constructor(reporter: InternalReporter, globalTimeoutForError: number) {
return new TaskRunner<Context>(reporter, globalTimeoutForError);
}
private constructor(reporter: InternalReporter, globalTimeoutForError: number) {
this._reporter = reporter; this._reporter = reporter;
this._globalTimeoutForError = globalTimeoutForError; this._globalTimeoutForError = globalTimeoutForError;
} }
addTask(name: string, task: Task<Context>) { addTask(task: Task<Context>) {
this._tasks.push({ name, task }); this._tasks.push(task);
} }
async run(context: Context, deadline: number, cancelPromise?: ManualPromise<void>): Promise<FullResult['status']> { async run(context: Context, deadline: number, cancelPromise?: ManualPromise<void>): Promise<FullResult['status']> {
@ -61,18 +56,18 @@ export class TaskRunner<Context> {
let currentTaskName: string | undefined; let currentTaskName: string | undefined;
const taskLoop = async () => { const taskLoop = async () => {
for (const { name, task } of this._tasks) { for (const task of this._tasks) {
currentTaskName = name; currentTaskName = task.title;
if (this._interrupted) if (this._interrupted)
break; break;
debug('pw:test:task')(`"${name}" started`); debug('pw:test:task')(`"${task.title}" started`);
const errors: TestError[] = []; const errors: TestError[] = [];
const softErrors: TestError[] = []; const softErrors: TestError[] = [];
try { try {
teardownRunner._tasks.unshift({ name: `teardown for ${name}`, task: { setup: task.teardown } }); teardownRunner._tasks.unshift({ title: `teardown for ${task.title}`, setup: task.teardown });
await task.setup?.(this._reporter, context, errors, softErrors); await task.setup?.(context, errors, softErrors);
} catch (e) { } catch (e) {
debug('pw:test:task')(`error in "${name}": `, e); debug('pw:test:task')(`error in "${task.title}": `, e);
errors.push(serializeError(e)); errors.push(serializeError(e));
} finally { } finally {
for (const error of [...softErrors, ...errors]) for (const error of [...softErrors, ...errors])
@ -83,7 +78,7 @@ export class TaskRunner<Context> {
this._hasErrors = true; this._hasErrors = true;
} }
} }
debug('pw:test:task')(`"${name}" finished`); debug('pw:test:task')(`"${task.title}" finished`);
} }
}; };

View File

@ -18,7 +18,7 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import { promisify } from 'util'; import { promisify } from 'util';
import { debug } from 'playwright-core/lib/utilsBundle'; import { debug } from 'playwright-core/lib/utilsBundle';
import { removeFolders } from 'playwright-core/lib/utils'; import { type ManualPromise, monotonicTime, removeFolders } from 'playwright-core/lib/utils';
import { Dispatcher, type EnvByProjectId } from './dispatcher'; import { Dispatcher, type EnvByProjectId } from './dispatcher';
import type { TestRunnerPluginRegistration } from '../plugins'; import type { TestRunnerPluginRegistration } from '../plugins';
import { createTestGroups, type TestGroup } from '../runner/testGroups'; import { createTestGroups, type TestGroup } from '../runner/testGroups';
@ -33,6 +33,7 @@ import { FailureTracker } from './failureTracker';
import { detectChangedTestFiles } from './vcs'; import { detectChangedTestFiles } from './vcs';
import type { InternalReporter } from '../reporters/internalReporter'; import type { InternalReporter } from '../reporters/internalReporter';
import { cacheDir } from '../transform/compilationCache'; import { cacheDir } from '../transform/compilationCache';
import type { FullResult } from '../../types/testReporter';
const readDirAsync = promisify(fs.readdir); const readDirAsync = promisify(fs.readdir);
@ -42,132 +43,100 @@ type ProjectWithTestGroups = {
testGroups: TestGroup[]; testGroups: TestGroup[];
}; };
export type Phase = { type Phase = {
dispatcher: Dispatcher, dispatcher: Dispatcher,
projects: ProjectWithTestGroups[] projects: ProjectWithTestGroups[]
}; };
export class TestRun { export class TestRun {
readonly config: FullConfigInternal; readonly config: FullConfigInternal;
readonly reporter: InternalReporter;
readonly failureTracker: FailureTracker; readonly failureTracker: FailureTracker;
rootSuite: Suite | undefined = undefined; rootSuite: Suite | undefined = undefined;
readonly phases: Phase[] = []; readonly phases: Phase[] = [];
projectFiles: Map<FullProjectInternal, string[]> = new Map(); projectFiles: Map<FullProjectInternal, string[]> = new Map();
projectSuites: Map<FullProjectInternal, Suite[]> = new Map(); projectSuites: Map<FullProjectInternal, Suite[]> = new Map();
constructor(config: FullConfigInternal) { constructor(config: FullConfigInternal, reporter: InternalReporter) {
this.config = config; this.config = config;
this.reporter = reporter;
this.failureTracker = new FailureTracker(config); this.failureTracker = new FailureTracker(config);
} }
} }
export function createTaskRunner(config: FullConfigInternal, reporter: InternalReporter): TaskRunner<TestRun> { export async function runTasks(testRun: TestRun, tasks: Task<TestRun>[], globalTimeout?: number, cancelPromise?: ManualPromise<void>) {
const taskRunner = TaskRunner.create<TestRun>(reporter, config.config.globalTimeout); const deadline = globalTimeout ? monotonicTime() + globalTimeout : 0;
addGlobalSetupTasks(taskRunner, config); const taskRunner = new TaskRunner<TestRun>(testRun.reporter, globalTimeout || 0);
taskRunner.addTask('load tests', createLoadTask('in-process', { filterOnly: true, failOnLoadErrors: true })); for (const task of tasks)
addRunTasks(taskRunner, config); taskRunner.addTask(task);
return taskRunner; testRun.reporter.onConfigure(testRun.config.config);
const status = await taskRunner.run(testRun, deadline, cancelPromise);
return await finishTaskRun(testRun, status);
} }
export function createTaskRunnerForWatchSetup(config: FullConfigInternal, reporter: InternalReporter): TaskRunner<TestRun> { export async function runTasksDeferCleanup(testRun: TestRun, tasks: Task<TestRun>[]) {
const taskRunner = TaskRunner.create<TestRun>(reporter); const taskRunner = new TaskRunner<TestRun>(testRun.reporter, 0);
addGlobalSetupTasks(taskRunner, config); for (const task of tasks)
return taskRunner; taskRunner.addTask(task);
testRun.reporter.onConfigure(testRun.config.config);
const { status, cleanup } = await taskRunner.runDeferCleanup(testRun, 0);
return { status: await finishTaskRun(testRun, status), cleanup };
} }
export function createTaskRunnerForTestServer(config: FullConfigInternal, reporter: InternalReporter): TaskRunner<TestRun> { async function finishTaskRun(testRun: TestRun, status: FullResult['status']) {
const taskRunner = TaskRunner.create<TestRun>(reporter); if (status === 'passed')
taskRunner.addTask('load tests', createLoadTask('out-of-process', { filterOnly: true, failOnLoadErrors: false, doNotRunDepsOutsideProjectFilter: true })); status = testRun.failureTracker.result();
addRunTasks(taskRunner, config); const modifiedResult = await testRun.reporter.onEnd({ status });
return taskRunner; if (modifiedResult && modifiedResult.status)
status = modifiedResult.status;
await testRun.reporter.onExit();
return status;
} }
function addGlobalSetupTasks(taskRunner: TaskRunner<TestRun>, config: FullConfigInternal) { export function createGlobalSetupTasks(config: FullConfigInternal) {
const tasks: Task<TestRun>[] = [];
if (!config.configCLIOverrides.preserveOutputDir && !process.env.PW_TEST_NO_REMOVE_OUTPUT_DIRS) if (!config.configCLIOverrides.preserveOutputDir && !process.env.PW_TEST_NO_REMOVE_OUTPUT_DIRS)
taskRunner.addTask('clear output', createRemoveOutputDirsTask()); tasks.push(createRemoveOutputDirsTask());
for (const plugin of config.plugins) tasks.push(...createPluginSetupTasks(config));
taskRunner.addTask('plugin setup', createPluginSetupTask(plugin));
if (config.config.globalSetup || config.config.globalTeardown) if (config.config.globalSetup || config.config.globalTeardown)
taskRunner.addTask('global setup', createGlobalSetupTask()); tasks.push(createGlobalSetupTask());
return tasks;
} }
function addRunTasks(taskRunner: TaskRunner<TestRun>, config: FullConfigInternal) { export function createRunTestsTasks(config: FullConfigInternal) {
taskRunner.addTask('create phases', createPhasesTask()); return [
taskRunner.addTask('report begin', createReportBeginTask()); createPhasesTask(),
for (const plugin of config.plugins) createReportBeginTask(),
taskRunner.addTask('plugin begin', createPluginBeginTask(plugin)); ...config.plugins.map(plugin => createPluginBeginTask(plugin)),
taskRunner.addTask('test suite', createRunTestsTask()); createRunTestsTask(),
return taskRunner; ];
} }
export function createTaskRunnerForList(config: FullConfigInternal, reporter: InternalReporter, mode: 'in-process' | 'out-of-process', options: { failOnLoadErrors: boolean }): TaskRunner<TestRun> { export function createClearCacheTask(config: FullConfigInternal): Task<TestRun> {
const taskRunner = TaskRunner.create<TestRun>(reporter, config.config.globalTimeout); return {
taskRunner.addTask('load tests', createLoadTask(mode, { ...options, filterOnly: false })); title: 'clear cache',
taskRunner.addTask('report begin', createReportBeginTask());
return taskRunner;
}
export function createTaskRunnerForListFiles(config: FullConfigInternal, reporter: InternalReporter): TaskRunner<TestRun> {
const taskRunner = TaskRunner.create<TestRun>(reporter, config.config.globalTimeout);
taskRunner.addTask('load tests', createListFilesTask());
taskRunner.addTask('report begin', createReportBeginTask());
return taskRunner;
}
export function createTaskRunnerForDevServer(config: FullConfigInternal, reporter: InternalReporter, mode: 'in-process' | 'out-of-process', setupAndWait: boolean): TaskRunner<TestRun> {
const taskRunner = TaskRunner.create<TestRun>(reporter, config.config.globalTimeout);
if (setupAndWait) {
for (const plugin of config.plugins)
taskRunner.addTask('plugin setup', createPluginSetupTask(plugin));
}
taskRunner.addTask('load tests', createLoadTask(mode, { failOnLoadErrors: true, filterOnly: false }));
taskRunner.addTask('start dev server', createStartDevServerTask());
if (setupAndWait) {
taskRunner.addTask('wait until interrupted', {
setup: async () => new Promise(() => {}),
});
}
return taskRunner;
}
export function createTaskRunnerForRelatedTestFiles(config: FullConfigInternal, reporter: InternalReporter, mode: 'in-process' | 'out-of-process', setupPlugins: boolean): TaskRunner<TestRun> {
const taskRunner = TaskRunner.create<TestRun>(reporter, config.config.globalTimeout);
if (setupPlugins) {
for (const plugin of config.plugins)
taskRunner.addTask('plugin setup', createPluginSetupTask(plugin));
}
taskRunner.addTask('load tests', createLoadTask(mode, { failOnLoadErrors: true, filterOnly: false, populateDependencies: true }));
return taskRunner;
}
export function createTaskRunnerForClearCache(config: FullConfigInternal, reporter: InternalReporter, mode: 'in-process' | 'out-of-process', setupPlugins: boolean): TaskRunner<TestRun> {
const taskRunner = TaskRunner.create<TestRun>(reporter, config.config.globalTimeout);
if (setupPlugins) {
for (const plugin of config.plugins)
taskRunner.addTask('plugin setup', createPluginSetupTask(plugin));
}
taskRunner.addTask('clear cache', {
setup: async () => { setup: async () => {
await removeDirAndLogToConsole(cacheDir); await removeDirAndLogToConsole(cacheDir);
for (const plugin of config.plugins) for (const plugin of config.plugins)
await plugin.instance?.clearCache?.(); await plugin.instance?.clearCache?.();
}, },
}); };
return taskRunner;
} }
function createReportBeginTask(): Task<TestRun> { export function createReportBeginTask(): Task<TestRun> {
return { return {
setup: async (reporter, { rootSuite }) => { title: 'report begin',
reporter.onBegin?.(rootSuite!); setup: async testRun => {
testRun.reporter.onBegin?.(testRun.rootSuite!);
}, },
teardown: async ({}) => {}, teardown: async ({}) => {},
}; };
} }
function createPluginSetupTask(plugin: TestRunnerPluginRegistration): Task<TestRun> { export function createPluginSetupTasks(config: FullConfigInternal): Task<TestRun>[] {
return { return config.plugins.map(plugin => ({
setup: async (reporter, { config }) => { title: 'plugin setup',
setup: async ({ reporter }) => {
if (typeof plugin.factory === 'function') if (typeof plugin.factory === 'function')
plugin.instance = await plugin.factory(); plugin.instance = await plugin.factory();
else else
@ -177,13 +146,14 @@ function createPluginSetupTask(plugin: TestRunnerPluginRegistration): Task<TestR
teardown: async () => { teardown: async () => {
await plugin.instance?.teardown?.(); await plugin.instance?.teardown?.();
}, },
}; }));
} }
function createPluginBeginTask(plugin: TestRunnerPluginRegistration): Task<TestRun> { function createPluginBeginTask(plugin: TestRunnerPluginRegistration): Task<TestRun> {
return { return {
setup: async (reporter, { rootSuite }) => { title: 'plugin begin',
await plugin.instance?.begin?.(rootSuite!); setup: async testRun => {
await plugin.instance?.begin?.(testRun.rootSuite!);
}, },
teardown: async () => { teardown: async () => {
await plugin.instance?.end?.(); await plugin.instance?.end?.();
@ -196,13 +166,14 @@ function createGlobalSetupTask(): Task<TestRun> {
let globalSetupFinished = false; let globalSetupFinished = false;
let teardownHook: any; let teardownHook: any;
return { return {
setup: async (reporter, { config }) => { title: 'global setup',
setup: async ({ config }) => {
const setupHook = config.config.globalSetup ? await loadGlobalHook(config, config.config.globalSetup) : undefined; const setupHook = config.config.globalSetup ? await loadGlobalHook(config, config.config.globalSetup) : undefined;
teardownHook = config.config.globalTeardown ? await loadGlobalHook(config, config.config.globalTeardown) : undefined; teardownHook = config.config.globalTeardown ? await loadGlobalHook(config, config.config.globalTeardown) : undefined;
globalSetupResult = setupHook ? await setupHook(config.config) : undefined; globalSetupResult = setupHook ? await setupHook(config.config) : undefined;
globalSetupFinished = true; globalSetupFinished = true;
}, },
teardown: async (reporter, { config }) => { teardown: async ({ config }) => {
if (typeof globalSetupResult === 'function') if (typeof globalSetupResult === 'function')
await globalSetupResult(); await globalSetupResult();
if (globalSetupFinished) if (globalSetupFinished)
@ -213,7 +184,8 @@ function createGlobalSetupTask(): Task<TestRun> {
function createRemoveOutputDirsTask(): Task<TestRun> { function createRemoveOutputDirsTask(): Task<TestRun> {
return { return {
setup: async (reporter, { config }) => { title: 'clear output',
setup: async ({ config }) => {
const outputDirs = new Set<string>(); const outputDirs = new Set<string>();
const projects = filterProjects(config.projects, config.cliProjectFilter); const projects = filterProjects(config.projects, config.cliProjectFilter);
projects.forEach(p => outputDirs.add(p.project.outputDir)); projects.forEach(p => outputDirs.add(p.project.outputDir));
@ -235,9 +207,10 @@ function createRemoveOutputDirsTask(): Task<TestRun> {
}; };
} }
function createListFilesTask(): Task<TestRun> { export function createListFilesTask(): Task<TestRun> {
return { return {
setup: async (reporter, testRun, errors) => { title: 'load tests',
setup: async (testRun, errors) => {
testRun.rootSuite = await createRootSuite(testRun, errors, false); testRun.rootSuite = await createRootSuite(testRun, errors, false);
testRun.failureTracker.onRootSuite(testRun.rootSuite); testRun.failureTracker.onRootSuite(testRun.rootSuite);
await collectProjectsAndTestFiles(testRun, false); await collectProjectsAndTestFiles(testRun, false);
@ -258,9 +231,10 @@ function createListFilesTask(): Task<TestRun> {
}; };
} }
function createLoadTask(mode: 'out-of-process' | 'in-process', options: { filterOnly: boolean, failOnLoadErrors: boolean, doNotRunDepsOutsideProjectFilter?: boolean, populateDependencies?: boolean }): Task<TestRun> { export function createLoadTask(mode: 'out-of-process' | 'in-process', options: { filterOnly: boolean, failOnLoadErrors: boolean, doNotRunDepsOutsideProjectFilter?: boolean, populateDependencies?: boolean }): Task<TestRun> {
return { return {
setup: async (reporter, testRun, errors, softErrors) => { title: 'load tests',
setup: async (testRun, errors, softErrors) => {
await collectProjectsAndTestFiles(testRun, !!options.doNotRunDepsOutsideProjectFilter); await collectProjectsAndTestFiles(testRun, !!options.doNotRunDepsOutsideProjectFilter);
await loadFileSuites(testRun, mode, options.failOnLoadErrors ? errors : softErrors); await loadFileSuites(testRun, mode, options.failOnLoadErrors ? errors : softErrors);
@ -294,7 +268,8 @@ function createLoadTask(mode: 'out-of-process' | 'in-process', options: { filter
function createPhasesTask(): Task<TestRun> { function createPhasesTask(): Task<TestRun> {
return { return {
setup: async (reporter, testRun) => { title: 'create phases',
setup: async testRun => {
let maxConcurrentTestGroups = 0; let maxConcurrentTestGroups = 0;
const processed = new Set<FullProjectInternal>(); const processed = new Set<FullProjectInternal>();
@ -325,7 +300,7 @@ function createPhasesTask(): Task<TestRun> {
processed.add(project); processed.add(project);
if (phaseProjects.length) { if (phaseProjects.length) {
let testGroupsInPhase = 0; let testGroupsInPhase = 0;
const phase: Phase = { dispatcher: new Dispatcher(testRun.config, reporter, testRun.failureTracker), projects: [] }; const phase: Phase = { dispatcher: new Dispatcher(testRun.config, testRun.reporter, testRun.failureTracker), projects: [] };
testRun.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)!;
@ -345,7 +320,8 @@ function createPhasesTask(): Task<TestRun> {
function createRunTestsTask(): Task<TestRun> { function createRunTestsTask(): Task<TestRun> {
return { return {
setup: async (reporter, { phases, failureTracker }) => { title: 'test suite',
setup: async ({ phases, failureTracker }) => {
const successfulProjects = new Set<FullProjectInternal>(); const successfulProjects = new Set<FullProjectInternal>();
const extraEnvByProjectId: EnvByProjectId = new Map(); const extraEnvByProjectId: EnvByProjectId = new Map();
const teardownToSetups = buildTeardownToSetupsMap(phases.map(phase => phase.projects.map(p => p.project)).flat()); const teardownToSetups = buildTeardownToSetupsMap(phases.map(phase => phase.projects.map(p => p.project)).flat());
@ -389,28 +365,29 @@ function createRunTestsTask(): Task<TestRun> {
} }
} }
}, },
teardown: async (reporter, { phases }) => { teardown: async ({ phases }) => {
for (const { dispatcher } of phases.reverse()) for (const { dispatcher } of phases.reverse())
await dispatcher.stop(); await dispatcher.stop();
}, },
}; };
} }
function createStartDevServerTask(): Task<TestRun> { export function createStartDevServerTask(): Task<TestRun> {
return { return {
setup: async (reporter, testRun, errors, softErrors) => { title: 'start dev server',
if (testRun.config.plugins.some(plugin => !!plugin.devServerCleanup)) { setup: async ({ config }, errors, softErrors) => {
if (config.plugins.some(plugin => !!plugin.devServerCleanup)) {
errors.push({ message: `DevServer is already running` }); errors.push({ message: `DevServer is already running` });
return; return;
} }
for (const plugin of testRun.config.plugins) for (const plugin of config.plugins)
plugin.devServerCleanup = await plugin.instance?.startDevServer?.(); plugin.devServerCleanup = await plugin.instance?.startDevServer?.();
if (!testRun.config.plugins.some(plugin => !!plugin.devServerCleanup)) if (!config.plugins.some(plugin => !!plugin.devServerCleanup))
errors.push({ message: `DevServer is not available in the package you are using. Did you mean to use component testing?` }); errors.push({ message: `DevServer is not available in the package you are using. Did you mean to use component testing?` });
}, },
teardown: async (reporter, testRun) => { teardown: async ({ config }) => {
for (const plugin of testRun.config.plugins) { for (const plugin of config.plugins) {
await plugin.devServerCleanup?.(); await plugin.devServerCleanup?.();
plugin.devServerCleanup = undefined; plugin.devServerCleanup = undefined;
} }

View File

@ -23,7 +23,7 @@ import type * as reporterTypes from '../../types/testReporter';
import { affectedTestFiles, collectAffectedTestFiles, dependenciesForTestFile } from '../transform/compilationCache'; import { affectedTestFiles, collectAffectedTestFiles, dependenciesForTestFile } from '../transform/compilationCache';
import type { ConfigLocation, FullConfigInternal } from '../common/config'; import type { ConfigLocation, FullConfigInternal } from '../common/config';
import { createErrorCollectingReporter, createReporterForTestServer, createReporters } from './reporters'; import { createErrorCollectingReporter, createReporterForTestServer, createReporters } from './reporters';
import { TestRun, createTaskRunnerForList, createTaskRunnerForTestServer, createTaskRunnerForWatchSetup, createTaskRunnerForListFiles, createTaskRunnerForDevServer, createTaskRunnerForRelatedTestFiles, createTaskRunnerForClearCache } from './tasks'; import { TestRun, runTasks, createLoadTask, createRunTestsTasks, createReportBeginTask, createListFilesTask, runTasksDeferCleanup, createClearCacheTask, createGlobalSetupTasks, createStartDevServerTask } from './tasks';
import { open } from 'playwright-core/lib/utilsBundle'; import { open } from 'playwright-core/lib/utilsBundle';
import ListReporter from '../reporters/list'; import ListReporter from '../reporters/list';
import { SigIntWatcher } from './sigIntWatcher'; import { SigIntWatcher } from './sigIntWatcher';
@ -150,17 +150,13 @@ export class TestServerDispatcher implements TestServerInterface {
if (!config) if (!config)
return { status: 'failed', report }; return { status: 'failed', report };
const taskRunner = createTaskRunnerForWatchSetup(config, reporter); const { status, cleanup } = await runTasksDeferCleanup(new TestRun(config, reporter), [
reporter.onConfigure(config.config); ...createGlobalSetupTasks(config),
const testRun = new TestRun(config); ]);
const { status, cleanup: globalCleanup } = await taskRunner.runDeferCleanup(testRun, 0); if (status !== 'passed')
await reporter.onEnd({ status }); await cleanup();
await reporter.onExit(); else
if (status !== 'passed') { this._globalSetup = { cleanup, report };
await globalCleanup();
return { report, status };
}
this._globalSetup = { cleanup: globalCleanup, report };
return { report, status }; return { report, status };
} }
@ -179,16 +175,13 @@ export class TestServerDispatcher implements TestServerInterface {
if (!config) if (!config)
return { report, status: 'failed' }; return { report, status: 'failed' };
const taskRunner = createTaskRunnerForDevServer(config, reporter, 'out-of-process', false); const { status, cleanup } = await runTasksDeferCleanup(new TestRun(config, reporter), [
const testRun = new TestRun(config); createLoadTask('out-of-process', { failOnLoadErrors: true, filterOnly: false }),
reporter.onConfigure(config.config); createStartDevServerTask(),
const { status, cleanup } = await taskRunner.runDeferCleanup(testRun, 0); ]);
await reporter.onEnd({ status }); if (status !== 'passed')
await reporter.onExit();
if (status !== 'passed') {
await cleanup(); await cleanup();
return { report, status }; else
}
this._devServer = { cleanup, report }; this._devServer = { cleanup, report };
return { report, status }; return { report, status };
} }
@ -205,13 +198,9 @@ export class TestServerDispatcher implements TestServerInterface {
const config = await this._loadConfigOrReportError(reporter); const config = await this._loadConfigOrReportError(reporter);
if (!config) if (!config)
return; return;
await runTasks(new TestRun(config, reporter), [
const taskRunner = createTaskRunnerForClearCache(config, reporter, 'out-of-process', false); createClearCacheTask(config),
const testRun = new TestRun(config); ]);
reporter.onConfigure(config.config);
const status = await taskRunner.run(testRun, 0);
await reporter.onEnd({ status });
await reporter.onExit();
} }
async listFiles(params: Parameters<TestServerInterface['listFiles']>[0]): ReturnType<TestServerInterface['listFiles']> { async listFiles(params: Parameters<TestServerInterface['listFiles']>[0]): ReturnType<TestServerInterface['listFiles']> {
@ -221,12 +210,10 @@ export class TestServerDispatcher implements TestServerInterface {
return { status: 'failed', report }; return { status: 'failed', report };
config.cliProjectFilter = params.projects?.length ? params.projects : undefined; config.cliProjectFilter = params.projects?.length ? params.projects : undefined;
const taskRunner = createTaskRunnerForListFiles(config, reporter); const status = await runTasks(new TestRun(config, reporter), [
reporter.onConfigure(config.config); createListFilesTask(),
const testRun = new TestRun(config); createReportBeginTask(),
const status = await taskRunner.run(testRun, 0); ]);
await reporter.onEnd({ status });
await reporter.onExit();
return { report, status }; return { report, status };
} }
@ -264,12 +251,10 @@ export class TestServerDispatcher implements TestServerInterface {
config.cliProjectFilter = params.projects?.length ? params.projects : undefined; config.cliProjectFilter = params.projects?.length ? params.projects : undefined;
config.cliListOnly = true; config.cliListOnly = true;
const taskRunner = createTaskRunnerForList(config, reporter, 'out-of-process', { failOnLoadErrors: false }); const status = await runTasks(new TestRun(config, reporter), [
const testRun = new TestRun(config); createLoadTask('out-of-process', { failOnLoadErrors: false, filterOnly: false }),
reporter.onConfigure(config.config); createReportBeginTask(),
const status = await taskRunner.run(testRun, 0); ]);
await reporter.onEnd({ status });
await reporter.onExit();
return { config, report, reporter, status }; return { config, report, reporter, status };
} }
@ -344,13 +329,12 @@ export class TestServerDispatcher implements TestServerInterface {
const configReporters = await createReporters(config, 'test', true); const configReporters = await createReporters(config, 'test', true);
const reporter = new InternalReporter([...configReporters, wireReporter]); const reporter = new InternalReporter([...configReporters, wireReporter]);
const taskRunner = createTaskRunnerForTestServer(config, reporter);
const testRun = new TestRun(config);
reporter.onConfigure(config.config);
const stop = new ManualPromise(); const stop = new ManualPromise();
const run = taskRunner.run(testRun, 0, stop).then(async status => { const tasks = [
await reporter.onEnd({ status }); createLoadTask('out-of-process', { filterOnly: true, failOnLoadErrors: false, doNotRunDepsOutsideProjectFilter: true }),
await reporter.onExit(); ...createRunTestsTasks(config),
];
const run = runTasks(new TestRun(config, reporter), tasks, 0, stop).then(async status => {
this._testRun = undefined; this._testRun = undefined;
return status; return status;
}); });
@ -373,13 +357,9 @@ export class TestServerDispatcher implements TestServerInterface {
const config = await this._loadConfigOrReportError(reporter); const config = await this._loadConfigOrReportError(reporter);
if (!config) if (!config)
return { errors: errorReporter.errors(), testFiles: [] }; return { errors: errorReporter.errors(), testFiles: [] };
const status = await runTasks(new TestRun(config, reporter), [
const taskRunner = createTaskRunnerForRelatedTestFiles(config, reporter, 'out-of-process', false); createLoadTask('out-of-process', { failOnLoadErrors: true, filterOnly: false, populateDependencies: true }),
const testRun = new TestRun(config); ]);
reporter.onConfigure(config.config);
const status = await taskRunner.run(testRun, 0);
await reporter.onEnd({ status });
await reporter.onExit();
if (status !== 'passed') if (status !== 'passed')
return { errors: errorReporter.errors(), testFiles: [] }; return { errors: errorReporter.errors(), testFiles: [] };
return { testFiles: affectedTestFiles(params.files) }; return { testFiles: affectedTestFiles(params.files) };