Skip to content

Commit

Permalink
chore: inside out the config & project internal (#22260)
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelfeldman committed Apr 7, 2023
1 parent 02ca63b commit a42567d
Show file tree
Hide file tree
Showing 48 changed files with 557 additions and 559 deletions.
30 changes: 15 additions & 15 deletions packages/playwright-test/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ import { Runner } from './runner/runner';
import { stopProfiling, startProfiling } from 'playwright-core/lib/utils';
import { experimentalLoaderOption, fileIsModule } from './util';
import { showHTMLReport } from './reporters/html';
import { baseFullConfig, builtInReporters, ConfigLoader, defaultTimeout, kDefaultConfigFiles, resolveConfigFile } from './common/configLoader';
import type { TraceMode } from './common/types';
import { ConfigLoader, kDefaultConfigFiles, resolveConfigFile } from './common/configLoader';
import type { ConfigCLIOverrides } from './common/ipc';
import type { FullResult } from '../reporter';
import type { TraceMode } from '../types/test';
import { baseFullConfig, builtInReporters, defaultTimeout } from './common/config';
import type { FullConfigInternal } from './common/config';

export function addTestCommands(program: Command) {
addTestCommand(program);
Expand Down Expand Up @@ -127,20 +129,18 @@ async function runTests(args: string[], opts: { [key: string]: any }) {
return;

const configLoader = new ConfigLoader(overrides);
let config: FullConfigInternal;
if (resolvedConfigFile)
await configLoader.loadConfigFile(resolvedConfigFile);
config = await configLoader.loadConfigFile(resolvedConfigFile, opts.deps === false);
else
await configLoader.loadEmptyConfig(configFileOrDirectory);
if (opts.deps === false)
configLoader.ignoreProjectDependencies();
config = await configLoader.loadEmptyConfig(configFileOrDirectory);

const config = configLoader.fullConfig();
config._internal.cliArgs = args;
config._internal.cliGrep = opts.grep as string | undefined;
config._internal.cliGrepInvert = opts.grepInvert as string | undefined;
config._internal.listOnly = !!opts.list;
config._internal.cliProjectFilter = opts.project || undefined;
config._internal.passWithNoTests = !!opts.passWithNoTests;
config.cliArgs = args;
config.cliGrep = opts.grep as string | undefined;
config.cliGrepInvert = opts.grepInvert as string | undefined;
config.listOnly = !!opts.list;
config.cliProjectFilter = opts.project || undefined;
config.passWithNoTests = !!opts.passWithNoTests;

const runner = new Runner(config);
let status: FullResult['status'];
Expand All @@ -166,8 +166,8 @@ async function listTestFiles(opts: { [key: string]: any }) {
return;

const configLoader = new ConfigLoader();
const runner = new Runner(configLoader.fullConfig());
await configLoader.loadConfigFile(resolvedConfigFile);
const config = await configLoader.loadConfigFile(resolvedConfigFile);
const runner = new Runner(config);
const report = await runner.listTestFiles(opts.project);
write(JSON.stringify(report), () => {
process.exit(0);
Expand Down
255 changes: 255 additions & 0 deletions packages/playwright-test/src/common/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
/**
* Copyright Microsoft Corporation. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import path from 'path';
import os from 'os';
import type { Config, Fixtures, Project, ReporterDescription } from '../../types/test';
import type { Location } from '../../types/testReporter';
import type { TestRunnerPluginRegistration } from '../plugins';
import type { Matcher } from '../util';
import { mergeObjects } from '../util';
import type { ConfigCLIOverrides } from './ipc';
import type { FullConfig, FullProject } from '../../types/test';

export type FixturesWithLocation = {
fixtures: Fixtures;
location: Location;
};
export type Annotation = { type: string, description?: string };

export const defaultTimeout = 30000;

export class FullConfigInternal {
readonly config: FullConfig;
globalOutputDir = path.resolve(process.cwd());
configDir = '';
configCLIOverrides: ConfigCLIOverrides = {};
storeDir = '';
maxConcurrentTestGroups = 0;
ignoreSnapshots = false;
webServers: Exclude<FullConfig['webServer'], null>[] = [];
plugins: TestRunnerPluginRegistration[] = [];
listOnly = false;
cliArgs: string[] = [];
cliGrep: string | undefined;
cliGrepInvert: string | undefined;
cliProjectFilter?: string[];
testIdMatcher?: Matcher;
passWithNoTests?: boolean;
defineConfigWasUsed = false;
projects: FullProjectInternal[] = [];

static from(config: FullConfig): FullConfigInternal {
return (config as any)[configInternalSymbol];
}

constructor(configDir: string, configFile: string | undefined, config: Config, throwawayArtifactsPath: string) {
this.configDir = configDir;
this.config = { ...baseFullConfig };
(this.config as any)[configInternalSymbol] = this;
this.storeDir = path.resolve(configDir, (config as any)._storeDir || 'playwright');
this.globalOutputDir = takeFirst(config.outputDir, throwawayArtifactsPath, path.resolve(process.cwd()));
this.ignoreSnapshots = takeFirst(config.ignoreSnapshots, false);
this.plugins = ((config as any)._plugins || []).map((p: any) => ({ factory: p }));

this.config.configFile = configFile;
this.config.rootDir = config.testDir || configDir;
this.config.forbidOnly = takeFirst(config.forbidOnly, baseFullConfig.forbidOnly);
this.config.fullyParallel = takeFirst(config.fullyParallel, baseFullConfig.fullyParallel);
this.config.globalSetup = takeFirst(config.globalSetup, baseFullConfig.globalSetup);
this.config.globalTeardown = takeFirst(config.globalTeardown, baseFullConfig.globalTeardown);
this.config.globalTimeout = takeFirst(config.globalTimeout, baseFullConfig.globalTimeout);
this.config.grep = takeFirst(config.grep, baseFullConfig.grep);
this.config.grepInvert = takeFirst(config.grepInvert, baseFullConfig.grepInvert);
this.config.maxFailures = takeFirst(config.maxFailures, baseFullConfig.maxFailures);
this.config.preserveOutput = takeFirst(config.preserveOutput, baseFullConfig.preserveOutput);
this.config.reporter = takeFirst(resolveReporters(config.reporter, configDir), baseFullConfig.reporter);
this.config.reportSlowTests = takeFirst(config.reportSlowTests, baseFullConfig.reportSlowTests);
this.config.quiet = takeFirst(config.quiet, baseFullConfig.quiet);
this.config.shard = takeFirst(config.shard, baseFullConfig.shard);
this.config.updateSnapshots = takeFirst(config.updateSnapshots, baseFullConfig.updateSnapshots);

const workers = takeFirst(config.workers, '50%');
if (typeof workers === 'string') {
if (workers.endsWith('%')) {
const cpus = os.cpus().length;
this.config.workers = Math.max(1, Math.floor(cpus * (parseInt(workers, 10) / 100)));
} else {
this.config.workers = parseInt(workers, 10);
}
} else {
this.config.workers = workers;
}

const webServers = takeFirst(config.webServer, baseFullConfig.webServer);
if (Array.isArray(webServers)) { // multiple web server mode
// Due to previous choices, this value shows up to the user in globalSetup as part of FullConfig. Arrays are not supported by the old type.
this.config.webServer = null;
this.webServers = webServers;
} else if (webServers) { // legacy singleton mode
this.config.webServer = webServers;
this.webServers = [webServers];
}
this.config.metadata = takeFirst(config.metadata, baseFullConfig.metadata);
this.projects = (config.projects || [config]).map(p => this._resolveProject(config, p, throwawayArtifactsPath));
resolveProjectDependencies(this.projects);
this._assignUniqueProjectIds(this.projects);
this.config.projects = this.projects.map(p => p.project);
}

private _assignUniqueProjectIds(projects: FullProjectInternal[]) {
const usedNames = new Set();
for (const p of projects) {
const name = p.project.name || '';
for (let i = 0; i < projects.length; ++i) {
const candidate = name + (i ? i : '');
if (usedNames.has(candidate))
continue;
p.id = candidate;
usedNames.add(candidate);
break;
}
}
}

private _resolveProject(config: Config, projectConfig: Project, throwawayArtifactsPath: string): FullProjectInternal {
// Resolve all config dirs relative to configDir.
if (projectConfig.testDir !== undefined)
projectConfig.testDir = path.resolve(this.configDir, projectConfig.testDir);
if (projectConfig.outputDir !== undefined)
projectConfig.outputDir = path.resolve(this.configDir, projectConfig.outputDir);
if (projectConfig.snapshotDir !== undefined)
projectConfig.snapshotDir = path.resolve(this.configDir, projectConfig.snapshotDir);
return new FullProjectInternal(config, this, projectConfig, throwawayArtifactsPath);
}
}

export class FullProjectInternal {
readonly project: FullProject;
id = '';
fullConfig: FullConfigInternal;
fullyParallel: boolean;
expect: Project['expect'];
respectGitIgnore: boolean;
deps: FullProjectInternal[] = [];
snapshotPathTemplate: string;

static from(project: FullProject): FullProjectInternal {
return (project as any)[projectInternalSymbol];
}

constructor(config: Config, fullConfig: FullConfigInternal, projectConfig: Project, throwawayArtifactsPath: string) {
this.fullConfig = fullConfig;

const testDir = takeFirst(projectConfig.testDir, config.testDir, fullConfig.configDir);

const outputDir = takeFirst(projectConfig.outputDir, config.outputDir, path.join(throwawayArtifactsPath, 'test-results'));
const snapshotDir = takeFirst(projectConfig.snapshotDir, config.snapshotDir, testDir);
const name = takeFirst(projectConfig.name, config.name, '');

const defaultSnapshotPathTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}';
this.snapshotPathTemplate = takeFirst(projectConfig.snapshotPathTemplate, config.snapshotPathTemplate, defaultSnapshotPathTemplate);

this.project = {
grep: takeFirst(projectConfig.grep, config.grep, baseFullConfig.grep),
grepInvert: takeFirst(projectConfig.grepInvert, config.grepInvert, baseFullConfig.grepInvert),
outputDir,
repeatEach: takeFirst(projectConfig.repeatEach, config.repeatEach, 1),
retries: takeFirst(projectConfig.retries, config.retries, 0),
metadata: takeFirst(projectConfig.metadata, config.metadata, undefined),
name,
testDir,
snapshotDir,
testIgnore: takeFirst(projectConfig.testIgnore, config.testIgnore, []),
testMatch: takeFirst(projectConfig.testMatch, config.testMatch, '**/?(*.)@(spec|test).?(m)[jt]s?(x)'),
timeout: takeFirst(projectConfig.timeout, config.timeout, defaultTimeout),
use: mergeObjects(config.use, projectConfig.use),
dependencies: projectConfig.dependencies || [],
};
(this.project as any)[projectInternalSymbol] = this;
this.fullyParallel = takeFirst(projectConfig.fullyParallel, config.fullyParallel, undefined);
this.expect = takeFirst(projectConfig.expect, config.expect, {});
this.respectGitIgnore = !projectConfig.testDir && !config.testDir;
}
}

export const baseFullConfig: FullConfig = {
forbidOnly: false,
fullyParallel: false,
globalSetup: null,
globalTeardown: null,
globalTimeout: 0,
grep: /.*/,
grepInvert: null,
maxFailures: 0,
metadata: {},
preserveOutput: 'always',
projects: [],
reporter: [[process.env.CI ? 'dot' : 'list']],
reportSlowTests: { max: 5, threshold: 15000 },
rootDir: path.resolve(process.cwd()),
quiet: false,
shard: null,
updateSnapshots: 'missing',
version: require('../../package.json').version,
workers: 0,
webServer: null,
};

export function takeFirst<T>(...args: (T | undefined)[]): T {
for (const arg of args) {
if (arg !== undefined)
return arg;
}
return undefined as any as T;
}

function resolveReporters(reporters: Config['reporter'], rootDir: string): ReporterDescription[] | undefined {
return toReporters(reporters as any)?.map(([id, arg]) => {
if (builtInReporters.includes(id as any))
return [id, arg];
return [require.resolve(id, { paths: [rootDir] }), arg];
});
}

function resolveProjectDependencies(projects: FullProjectInternal[]) {
for (const project of projects) {
for (const dependencyName of project.project.dependencies) {
const dependencies = projects.filter(p => p.project.name === dependencyName);
if (!dependencies.length)
throw new Error(`Project '${project.project.name}' depends on unknown project '${dependencyName}'`);
if (dependencies.length > 1)
throw new Error(`Project dependencies should have unique names, reading ${dependencyName}`);
project.deps.push(...dependencies);
}
}
}

export function toReporters(reporters: BuiltInReporter | ReporterDescription[] | undefined): ReporterDescription[] | undefined {
if (!reporters)
return;
if (typeof reporters === 'string')
return [[reporters]];
return reporters;
}

export const builtInReporters = ['list', 'line', 'dot', 'json', 'junit', 'null', 'github', 'html'] as const;
export type BuiltInReporter = typeof builtInReporters[number];

export type ContextReuseMode = 'none' | 'force' | 'when-possible';

const configInternalSymbol = Symbol('configInternalSymbol');
const projectInternalSymbol = Symbol('projectInternalSymbol');

0 comments on commit a42567d

Please sign in to comment.