chore(test runner): move all cli options into ConfigCLIOverrides

This commit is contained in:
Dmitry Gozman 2024-03-06 10:38:00 -08:00
parent d125ff4d39
commit 4e0727048e
10 changed files with 59 additions and 61 deletions

View file

@ -41,20 +41,12 @@ export const defaultTimeout = 30000;
export class FullConfigInternal { export class FullConfigInternal {
readonly config: FullConfig; readonly config: FullConfig;
readonly globalOutputDir: string;
readonly configDir: string; readonly configDir: string;
readonly configCLIOverrides: ConfigCLIOverrides; readonly configCLIOverrides: ConfigCLIOverrides;
readonly ignoreSnapshots: boolean; readonly ignoreSnapshots: boolean;
readonly preserveOutputDir: boolean;
readonly webServers: Exclude<FullConfig['webServer'], null>[]; readonly webServers: Exclude<FullConfig['webServer'], null>[];
readonly plugins: TestRunnerPluginRegistration[]; readonly plugins: TestRunnerPluginRegistration[];
readonly projects: FullProjectInternal[] = []; readonly projects: FullProjectInternal[] = [];
cliArgs: string[] = [];
cliGrep: string | undefined;
cliGrepInvert: string | undefined;
cliProjectFilter?: string[];
cliListOnly = false;
cliPassWithNoTests?: boolean;
testIdMatcher?: Matcher; testIdMatcher?: Matcher;
defineConfigWasUsed = false; defineConfigWasUsed = false;
@ -69,8 +61,6 @@ export class FullConfigInternal {
this.configDir = configDir; this.configDir = configDir;
this.configCLIOverrides = configCLIOverrides; this.configCLIOverrides = configCLIOverrides;
this.globalOutputDir = takeFirst(configCLIOverrides.outputDir, pathResolve(configDir, userConfig.outputDir), throwawayArtifactsPath, path.resolve(process.cwd()));
this.preserveOutputDir = configCLIOverrides.preserveOutputDir || false;
this.ignoreSnapshots = takeFirst(configCLIOverrides.ignoreSnapshots, userConfig.ignoreSnapshots, false); this.ignoreSnapshots = takeFirst(configCLIOverrides.ignoreSnapshots, userConfig.ignoreSnapshots, false);
const privateConfiguration = (userConfig as any)['@playwright/test']; const privateConfiguration = (userConfig as any)['@playwright/test'];
this.plugins = (privateConfiguration?.plugins || []).map((p: any) => ({ factory: p })); this.plugins = (privateConfiguration?.plugins || []).map((p: any) => ({ factory: p }));
@ -138,6 +128,13 @@ export class FullConfigInternal {
external: userConfig.build?.external || [], external: userConfig.build?.external || [],
}); });
this.config.projects = this.projects.map(p => p.project); this.config.projects = this.projects.map(p => p.project);
if (configCLIOverrides.ignoreProjectDependencies) {
for (const project of this.projects) {
project.deps = [];
project.teardown = undefined;
}
}
} }
private _assignUniqueProjectIds(projects: FullProjectInternal[]) { private _assignUniqueProjectIds(projects: FullProjectInternal[]) {

View file

@ -100,17 +100,11 @@ async function loadUserConfig(location: ConfigLocation): Promise<Config> {
return object as Config; return object as Config;
} }
export async function loadConfig(location: ConfigLocation, overrides?: ConfigCLIOverrides, ignoreProjectDependencies = false): Promise<FullConfigInternal> { export async function loadConfig(location: ConfigLocation, overrides?: ConfigCLIOverrides): Promise<FullConfigInternal> {
const userConfig = await loadUserConfig(location); const userConfig = await loadUserConfig(location);
validateConfig(location.resolvedConfigFile || '<default config>', userConfig); validateConfig(location.resolvedConfigFile || '<default config>', userConfig);
const fullConfig = new FullConfigInternal(location, userConfig, overrides || {}); const fullConfig = new FullConfigInternal(location, userConfig, overrides || {});
fullConfig.defineConfigWasUsed = !!(userConfig as any)[kDefineConfigWasUsed]; fullConfig.defineConfigWasUsed = !!(userConfig as any)[kDefineConfigWasUsed];
if (ignoreProjectDependencies) {
for (const project of fullConfig.projects) {
project.deps = [];
project.teardown = undefined;
}
}
return fullConfig; return fullConfig;
} }
@ -316,7 +310,7 @@ export function resolveConfigFile(configFileOrDirectory: string): string | undef
} }
} }
export async function loadConfigFromFileRestartIfNeeded(configFile: string | undefined, overrides?: ConfigCLIOverrides, ignoreDeps?: boolean): Promise<FullConfigInternal | null> { export async function loadConfigFromFileRestartIfNeeded(configFile: string | undefined, overrides?: ConfigCLIOverrides): Promise<FullConfigInternal | null> {
const configFileOrDirectory = configFile ? path.resolve(process.cwd(), configFile) : process.cwd(); const configFileOrDirectory = configFile ? path.resolve(process.cwd(), configFile) : process.cwd();
const resolvedConfigFile = resolveConfigFile(configFileOrDirectory); const resolvedConfigFile = resolveConfigFile(configFileOrDirectory);
if (restartWithExperimentalTsEsm(resolvedConfigFile)) if (restartWithExperimentalTsEsm(resolvedConfigFile))
@ -325,7 +319,7 @@ export async function loadConfigFromFileRestartIfNeeded(configFile: string | und
configDir: resolvedConfigFile ? path.dirname(resolvedConfigFile) : configFileOrDirectory, configDir: resolvedConfigFile ? path.dirname(resolvedConfigFile) : configFileOrDirectory,
resolvedConfigFile, resolvedConfigFile,
}; };
return await loadConfig(location, overrides, ignoreDeps); return await loadConfig(location, overrides);
} }
export async function loadEmptyConfigForMergeReports() { export async function loadEmptyConfigForMergeReports() {

View file

@ -20,6 +20,13 @@ import type { ConfigLocation, FullConfigInternal } from './config';
import type { ReporterDescription, TestInfoError, TestStatus } from '../../types/test'; import type { ReporterDescription, TestInfoError, TestStatus } from '../../types/test';
export type ConfigCLIOverrides = { export type ConfigCLIOverrides = {
cliArgs?: string[];
grep?: string;
grepInvert?: string;
projectFilter?: string[];
listOnly?: boolean;
passWithNoTests?: boolean;
ignoreProjectDependencies?: boolean;
forbidOnly?: boolean; forbidOnly?: boolean;
fullyParallel?: boolean; fullyParallel?: boolean;
globalTimeout?: number; globalTimeout?: number;

View file

@ -152,17 +152,10 @@ Examples:
async function runTests(args: string[], opts: { [key: string]: any }) { async function runTests(args: string[], opts: { [key: string]: any }) {
await startProfiling(); await startProfiling();
const config = await loadConfigFromFileRestartIfNeeded(opts.config, overridesFromOptions(opts), opts.deps === false); const config = await loadConfigFromFileRestartIfNeeded(opts.config, overridesFromOptions(opts, args));
if (!config) if (!config)
return; return;
config.cliArgs = args;
config.cliGrep = opts.grep as string | undefined;
config.cliGrepInvert = opts.grepInvert as string | undefined;
config.cliListOnly = !!opts.list;
config.cliProjectFilter = opts.project || undefined;
config.cliPassWithNoTests = !!opts.passWithNoTests;
const runner = new Runner(config); const runner = new Runner(config);
let status: FullResult['status']; let status: FullResult['status'];
if (opts.ui || opts.uiHost || opts.uiPort) if (opts.ui || opts.uiHost || opts.uiPort)
@ -226,9 +219,16 @@ async function mergeReports(reportDir: string | undefined, opts: { [key: string]
gracefullyProcessExitDoNotHang(0); gracefullyProcessExitDoNotHang(0);
} }
function overridesFromOptions(options: { [key: string]: any }): ConfigCLIOverrides { function overridesFromOptions(options: { [key: string]: any }, args: string[]): ConfigCLIOverrides {
const shardPair = options.shard ? options.shard.split('/').map((t: string) => parseInt(t, 10)) : undefined; const shardPair = options.shard ? options.shard.split('/').map((t: string) => parseInt(t, 10)) : undefined;
const overrides: ConfigCLIOverrides = { const overrides: ConfigCLIOverrides = {
cliArgs: args,
grep: options.grep as string | undefined,
grepInvert: options.grepInvert as string | undefined,
projectFilter: options.project || undefined,
listOnly: !!options.list,
passWithNoTests: !!options.passWithNoTests,
ignoreProjectDependencies: options.deps === false,
forbidOnly: options.forbidOnly ? true : undefined, forbidOnly: options.forbidOnly ? true : undefined,
fullyParallel: options.fullyParallel ? true : undefined, fullyParallel: options.fullyParallel ? true : undefined,
globalTimeout: options.globalTimeout ? parseInt(options.globalTimeout, 10) : undefined, globalTimeout: options.globalTimeout ? parseInt(options.globalTimeout, 10) : undefined,

View file

@ -36,11 +36,11 @@ export async function collectProjectsAndTestFiles(testRun: TestRun, doNotRunTest
const config = testRun.config; const config = testRun.config;
const fsCache = new Map(); const fsCache = new Map();
const sourceMapCache = new Map(); const sourceMapCache = new Map();
const cliFileMatcher = config.cliArgs.length ? createFileMatcherFromArguments(config.cliArgs) : null; const cliFileMatcher = config.configCLIOverrides.cliArgs?.length ? createFileMatcherFromArguments(config.configCLIOverrides.cliArgs) : null;
// 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[]>();
const filteredProjects = filterProjects(config.projects, config.cliProjectFilter); const filteredProjects = filterProjects(config.projects, config.configCLIOverrides.projectFilter);
for (const project of filteredProjects) { for (const project of filteredProjects) {
const files = await collectFilesForProject(project, fsCache); const files = await collectFilesForProject(project, fsCache);
allFilesForProject.set(project, files); allFilesForProject.set(project, files);
@ -131,9 +131,9 @@ export async function createRootSuite(testRun: TestRun, errors: TestError[], sho
// Filter all the projects using grep, testId, file names. // Filter all the projects using grep, testId, file names.
{ {
// Interpret cli parameters. // Interpret cli parameters.
const cliFileFilters = createFileFiltersFromArguments(config.cliArgs); const cliFileFilters = createFileFiltersFromArguments(config.configCLIOverrides.cliArgs || []);
const grepMatcher = config.cliGrep ? createTitleMatcher(forceRegExp(config.cliGrep)) : () => true; const grepMatcher = config.configCLIOverrides.grep ? createTitleMatcher(forceRegExp(config.configCLIOverrides.grep)) : () => true;
const grepInvertMatcher = config.cliGrepInvert ? createTitleMatcher(forceRegExp(config.cliGrepInvert)) : () => false; const grepInvertMatcher = config.configCLIOverrides.grepInvert ? createTitleMatcher(forceRegExp(config.configCLIOverrides.grepInvert)) : () => false;
const cliTitleMatcher = (title: string) => !grepInvertMatcher(title) && grepMatcher(title); const cliTitleMatcher = (title: string) => !grepInvertMatcher(title) && grepMatcher(title);
// Filter file suites for all projects. // Filter file suites for all projects.

View file

@ -77,7 +77,7 @@ 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.configCLIOverrides.listOnly;
const deadline = config.config.globalTimeout ? monotonicTime() + config.config.globalTimeout : 0; const deadline = config.config.globalTimeout ? monotonicTime() + config.config.globalTimeout : 0;
// Legacy webServer support. // Legacy webServer support.

View file

@ -175,7 +175,7 @@ function createRemoveOutputDirsTask(): Task<TestRun> {
return { return {
setup: async ({ config }) => { 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.configCLIOverrides.projectFilter);
projects.forEach(p => outputDirs.add(p.project.outputDir)); projects.forEach(p => outputDirs.add(p.project.outputDir));
await Promise.all(Array.from(outputDirs).map(outputDir => removeFolders([outputDir]).then(async ([error]) => { await Promise.all(Array.from(outputDirs).map(outputDir => removeFolders([outputDir]).then(async ([error]) => {
@ -203,8 +203,8 @@ function createLoadTask(mode: 'out-of-process' | 'in-process', options: { filter
testRun.rootSuite = await createRootSuite(testRun, options.failOnLoadErrors ? errors : softErrors, !!options.filterOnly); testRun.rootSuite = await createRootSuite(testRun, options.failOnLoadErrors ? errors : softErrors, !!options.filterOnly);
testRun.failureTracker.onRootSuite(testRun.rootSuite); testRun.failureTracker.onRootSuite(testRun.rootSuite);
// Fail when no tests. // Fail when no tests.
if (options.failOnLoadErrors && !testRun.rootSuite.allTests().length && !testRun.config.cliPassWithNoTests && !testRun.config.config.shard) { if (options.failOnLoadErrors && !testRun.rootSuite.allTests().length && !testRun.config.configCLIOverrides.passWithNoTests && !testRun.config.config.shard) {
if (testRun.config.cliArgs.length) { if (testRun.config.configCLIOverrides.cliArgs?.length) {
throw new Error([ throw new Error([
`No tests found.`, `No tests found.`,
`Make sure that arguments are regular expressions matching test files.`, `Make sure that arguments are regular expressions matching test files.`,

View file

@ -118,7 +118,7 @@ class Dispatcher implements TestServerInterface {
env: NodeJS.ProcessEnv; env: NodeJS.ProcessEnv;
}) { }) {
const config = await this._loadConfig(params.configFile); const config = await this._loadConfig(params.configFile);
config.cliArgs = params.locations || []; config.configCLIOverrides.cliArgs = params.locations || [];
const wireReporter = await createReporterForTestServer(config, params.reporter, 'list', message => this._dispatchEvent('report', message)); const wireReporter = await createReporterForTestServer(config, params.reporter, 'list', message => this._dispatchEvent('report', message));
const reporter = new InternalReporter(new Multiplexer([wireReporter])); const reporter = new InternalReporter(new Multiplexer([wireReporter]));
const taskRunner = createTaskRunnerForList(config, reporter, 'out-of-process', { failOnLoadErrors: true }); const taskRunner = createTaskRunnerForList(config, reporter, 'out-of-process', { failOnLoadErrors: true });
@ -164,10 +164,10 @@ class Dispatcher implements TestServerInterface {
}; };
const config = await this._loadConfig(params.configFile, overrides); const config = await this._loadConfig(params.configFile, overrides);
config.cliListOnly = false; config.configCLIOverrides.listOnly = false;
config.cliArgs = params.locations || []; config.configCLIOverrides.cliArgs = params.locations || [];
config.cliGrep = params.grep; config.configCLIOverrides.grep = params.grep;
config.cliProjectFilter = params.projects?.length ? params.projects : undefined; config.configCLIOverrides.projectFilter = params.projects?.length ? params.projects : undefined;
const wireReporter = await createReporterForTestServer(config, params.reporter, 'test', message => this._dispatchEvent('report', message)); const wireReporter = await createReporterForTestServer(config, params.reporter, 'test', message => this._dispatchEvent('report', message));
const configReporters = await createReporters(config, 'test'); const configReporters = await createReporters(config, 'test');

View file

@ -43,8 +43,8 @@ class UIMode {
constructor(config: FullConfigInternal) { constructor(config: FullConfigInternal) {
this._config = config; this._config = config;
process.env.PW_LIVE_TRACE_STACKS = '1'; process.env.PW_LIVE_TRACE_STACKS = '1';
config.cliListOnly = false; config.configCLIOverrides.listOnly = false;
config.cliPassWithNoTests = true; config.configCLIOverrides.passWithNoTests = true;
config.config.preserveOutput = 'always'; config.config.preserveOutput = 'always';
for (const p of config.projects) { for (const p of config.projects) {
@ -168,7 +168,7 @@ class UIMode {
private async _listTests() { private async _listTests() {
const reporter = new InternalReporter(new TeleReporterEmitter(e => this._dispatchEvent('listReport', e), { omitBuffers: true })); const reporter = new InternalReporter(new TeleReporterEmitter(e => this._dispatchEvent('listReport', e), { omitBuffers: true }));
this._config.cliListOnly = true; this._config.configCLIOverrides.listOnly = true;
this._config.testIdMatcher = undefined; this._config.testIdMatcher = undefined;
const taskRunner = createTaskRunnerForList(this._config, reporter, 'out-of-process', { failOnLoadErrors: false }); const taskRunner = createTaskRunnerForList(this._config, reporter, 'out-of-process', { failOnLoadErrors: false });
const testRun = new TestRun(this._config, reporter); const testRun = new TestRun(this._config, reporter);
@ -190,8 +190,8 @@ class UIMode {
await this._stopTests(); await this._stopTests();
const testIdSet = testIds ? new Set<string>(testIds) : null; const testIdSet = testIds ? new Set<string>(testIds) : null;
this._config.cliListOnly = false; this._config.configCLIOverrides.listOnly = false;
this._config.cliProjectFilter = projects.length ? projects : undefined; this._config.configCLIOverrides.projectFilter = projects.length ? projects : undefined;
this._config.testIdMatcher = id => !testIdSet || testIdSet.has(id); this._config.testIdMatcher = id => !testIdSet || testIdSet.has(id);
const reporters = await createReporters(this._config, 'ui'); const reporters = await createReporters(this._config, 'ui');

View file

@ -39,8 +39,8 @@ class FSWatcher {
private _timer: NodeJS.Timeout | undefined; private _timer: NodeJS.Timeout | undefined;
async update(config: FullConfigInternal) { async update(config: FullConfigInternal) {
const commandLineFileMatcher = config.cliArgs.length ? createFileMatcherFromArguments(config.cliArgs) : () => true; const commandLineFileMatcher = config.configCLIOverrides.cliArgs?.length ? createFileMatcherFromArguments(config.configCLIOverrides.cliArgs) : () => true;
const projects = filterProjects(config.projects, config.cliProjectFilter); const projects = filterProjects(config.projects, config.configCLIOverrides.projectFilter);
const projectClosure = buildProjectsClosure(projects); const projectClosure = buildProjectsClosure(projects);
const projectFilters = new Map<FullProjectInternal, Matcher>(); const projectFilters = new Map<FullProjectInternal, Matcher>();
for (const [project, type] of projectClosure) { for (const [project, type] of projectClosure) {
@ -107,7 +107,7 @@ class FSWatcher {
export async function runWatchModeLoop(config: FullConfigInternal): Promise<FullResult['status']> { export async function runWatchModeLoop(config: FullConfigInternal): Promise<FullResult['status']> {
// Reset the settings that don't apply to watch. // Reset the settings that don't apply to watch.
config.cliPassWithNoTests = true; config.configCLIOverrides.passWithNoTests = true;
for (const p of config.projects) for (const p of config.projects)
p.project.retries = 0; p.project.retries = 0;
@ -172,7 +172,7 @@ export async function runWatchModeLoop(config: FullConfigInternal): Promise<Full
}).catch(() => ({ projectNames: null })); }).catch(() => ({ projectNames: null }));
if (!projectNames) if (!projectNames)
continue; continue;
config.cliProjectFilter = projectNames.length ? projectNames : undefined; config.configCLIOverrides.projectFilter = projectNames.length ? projectNames : undefined;
await fsWatcher.update(config); await fsWatcher.update(config);
await runTests(config, failedTestIdCollector); await runTests(config, failedTestIdCollector);
lastRun = { type: 'regular' }; lastRun = { type: 'regular' };
@ -188,9 +188,9 @@ export async function runWatchModeLoop(config: FullConfigInternal): Promise<Full
if (filePattern === null) if (filePattern === null)
continue; continue;
if (filePattern.trim()) if (filePattern.trim())
config.cliArgs = filePattern.split(' '); config.configCLIOverrides.cliArgs = filePattern.split(' ');
else else
config.cliArgs = []; config.configCLIOverrides.cliArgs = [];
await fsWatcher.update(config); await fsWatcher.update(config);
await runTests(config, failedTestIdCollector); await runTests(config, failedTestIdCollector);
lastRun = { type: 'regular' }; lastRun = { type: 'regular' };
@ -206,9 +206,9 @@ export async function runWatchModeLoop(config: FullConfigInternal): Promise<Full
if (testPattern === null) if (testPattern === null)
continue; continue;
if (testPattern.trim()) if (testPattern.trim())
config.cliGrep = testPattern; config.configCLIOverrides.grep = testPattern;
else else
config.cliGrep = undefined; config.configCLIOverrides.grep = undefined;
await fsWatcher.update(config); await fsWatcher.update(config);
await runTests(config, failedTestIdCollector); await runTests(config, failedTestIdCollector);
lastRun = { type: 'regular' }; lastRun = { type: 'regular' };
@ -263,7 +263,7 @@ async function runChangedTests(config: FullConfigInternal, failedTestIdCollector
// Collect all the affected projects, follow project dependencies. // Collect all the affected projects, follow project dependencies.
// 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.cliProjectFilter); const projects = filterProjects(config.projects, config.configCLIOverrides.projectFilter);
const projectClosure = buildProjectsClosure(projects); const projectClosure = buildProjectsClosure(projects);
const affectedProjects = affectedProjectsClosure([...projectClosure.keys()], [...filesByProject.keys()]); const affectedProjects = affectedProjectsClosure([...projectClosure.keys()], [...filesByProject.keys()]);
const affectsAnyDependency = [...affectedProjects].some(p => projectClosure.get(p) === 'dependency'); const affectsAnyDependency = [...affectedProjects].some(p => projectClosure.get(p) === 'dependency');
@ -386,11 +386,11 @@ function printConfiguration(config: FullConfigInternal, title?: string) {
const packageManagerCommand = getPackageManagerExecCommand(); const packageManagerCommand = getPackageManagerExecCommand();
const tokens: string[] = []; const tokens: string[] = [];
tokens.push(`${packageManagerCommand} playwright test`); tokens.push(`${packageManagerCommand} playwright test`);
tokens.push(...(config.cliProjectFilter || [])?.map(p => colors.blue(`--project ${p}`))); tokens.push(...(config.configCLIOverrides.projectFilter || [])?.map(p => colors.blue(`--project ${p}`)));
if (config.cliGrep) if (config.configCLIOverrides.grep)
tokens.push(colors.red(`--grep ${config.cliGrep}`)); tokens.push(colors.red(`--grep ${config.configCLIOverrides.grep}`));
if (config.cliArgs) if (config.configCLIOverrides.cliArgs)
tokens.push(...config.cliArgs.map(a => colors.bold(a))); tokens.push(...config.configCLIOverrides.cliArgs.map(a => colors.bold(a)));
if (title) if (title)
tokens.push(colors.dim(`(${title})`)); tokens.push(colors.dim(`(${title})`));
if (seq) if (seq)