chore: minimize configLoader use (#20431)

This commit is contained in:
Pavel Feldman 2023-01-27 12:44:15 -08:00 committed by GitHub
parent bb01d99458
commit b91bb1af9a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 177 additions and 188 deletions

View File

@ -25,9 +25,8 @@ import { experimentalLoaderOption, fileIsModule } from './util';
import type { TestFileFilter } from './util';
import { createTitleMatcher } from './util';
import { showHTMLReport } from './reporters/html';
import { baseFullConfig, defaultTimeout, kDefaultConfigFiles, resolveConfigFile } from './common/configLoader';
import { baseFullConfig, builtInReporters, ConfigLoader, defaultTimeout, kDefaultConfigFiles, resolveConfigFile } from './common/configLoader';
import type { TraceMode } from './common/types';
import { builtInReporters } from './runner/reporters';
import type { ConfigCLIOverrides } from './common/ipc';
export function addTestCommands(program: Command) {
@ -151,11 +150,12 @@ async function runTests(args: string[], opts: { [key: string]: any }) {
if (restartWithExperimentalTsEsm(resolvedConfigFile))
return;
const runner = new Runner(overrides);
const configLoader = new ConfigLoader(overrides);
const runner = new Runner(configLoader.fullConfig());
if (resolvedConfigFile)
await runner.loadConfigFromResolvedFile(resolvedConfigFile);
await configLoader.loadConfigFile(resolvedConfigFile);
else
await runner.loadEmptyConfig(configFileOrDirectory);
await configLoader.loadEmptyConfig(configFileOrDirectory);
const testFileFilters: TestFileFilter[] = args.map(arg => {
const match = /^(.*?):(\d+):?(\d+)?$/.exec(arg);
@ -193,8 +193,9 @@ async function listTestFiles(opts: { [key: string]: any }) {
if (restartWithExperimentalTsEsm(resolvedConfigFile))
return;
const runner = new Runner();
await runner.loadConfigFromResolvedFile(resolvedConfigFile);
const configLoader = new ConfigLoader();
const runner = new Runner(configLoader.fullConfig());
await configLoader.loadConfigFile(resolvedConfigFile);
const report = await runner.listTestFiles(opts.project);
write(JSON.stringify(report), () => {
process.exit(0);

View File

@ -6,6 +6,3 @@
[transform.ts]
../third_party/tsconfig-loader.ts
[configLoader.ts]
../runner/reporters.ts

View File

@ -18,9 +18,7 @@ import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { isRegExp } from 'playwright-core/lib/utils';
import type { Reporter } from '../../types/testReporter';
import type { ConfigCLIOverrides, SerializedConfig } from './ipc';
import { builtInReporters, toReporters } from '../runner/reporters';
import { requireOrImport } from './transform';
import type { Config, FullConfigInternal, FullProjectInternal, Project, ReporterDescription } from './types';
import { errorWithFile, getPackageJsonPath, mergeObjects } from '../util';
@ -28,14 +26,11 @@ import { errorWithFile, getPackageJsonPath, mergeObjects } from '../util';
export const defaultTimeout = 30000;
export class ConfigLoader {
private _configCLIOverrides: ConfigCLIOverrides;
private _fullConfig: FullConfigInternal;
private _configDir: string = '';
private _configFile: string | undefined;
constructor(configCLIOverrides?: ConfigCLIOverrides) {
this._configCLIOverrides = configCLIOverrides || {};
this._fullConfig = { ...baseFullConfig };
this._fullConfig._configCLIOverrides = configCLIOverrides || {};
}
static async deserialize(data: SerializedConfig): Promise<ConfigLoader> {
@ -48,11 +43,10 @@ export class ConfigLoader {
}
async loadConfigFile(file: string): Promise<FullConfigInternal> {
if (this._configFile)
if (this._fullConfig.configFile)
throw new Error('Cannot load two config files');
const config = await this._requireOrImportDefaultObject(file) as Config;
this._configFile = file;
await this._processConfigObject(config, path.dirname(file));
const config = await requireOrImportDefaultObject(file) as Config;
await this._processConfigObject(config, path.dirname(file), file);
return this._fullConfig;
}
@ -61,35 +55,35 @@ export class ConfigLoader {
return {};
}
private async _processConfigObject(config: Config, configDir: string) {
private async _processConfigObject(config: Config, configDir: string, configFile?: string) {
// 1. Validate data provided in the config file.
validateConfig(this._configFile || '<default config>', config);
validateConfig(configFile || '<default config>', config);
// 2. Override settings from CLI.
config.forbidOnly = takeFirst(this._configCLIOverrides.forbidOnly, config.forbidOnly);
config.fullyParallel = takeFirst(this._configCLIOverrides.fullyParallel, config.fullyParallel);
config.globalTimeout = takeFirst(this._configCLIOverrides.globalTimeout, config.globalTimeout);
config.maxFailures = takeFirst(this._configCLIOverrides.maxFailures, config.maxFailures);
config.outputDir = takeFirst(this._configCLIOverrides.outputDir, config.outputDir);
config.quiet = takeFirst(this._configCLIOverrides.quiet, config.quiet);
config.repeatEach = takeFirst(this._configCLIOverrides.repeatEach, config.repeatEach);
config.retries = takeFirst(this._configCLIOverrides.retries, config.retries);
if (this._configCLIOverrides.reporter)
config.reporter = toReporters(this._configCLIOverrides.reporter as any);
config.shard = takeFirst(this._configCLIOverrides.shard, config.shard);
config.timeout = takeFirst(this._configCLIOverrides.timeout, config.timeout);
config.updateSnapshots = takeFirst(this._configCLIOverrides.updateSnapshots, config.updateSnapshots);
config.ignoreSnapshots = takeFirst(this._configCLIOverrides.ignoreSnapshots, config.ignoreSnapshots);
if (this._configCLIOverrides.projects && config.projects)
const configCLIOverrides = this._fullConfig._configCLIOverrides;
config.forbidOnly = takeFirst(configCLIOverrides.forbidOnly, config.forbidOnly);
config.fullyParallel = takeFirst(configCLIOverrides.fullyParallel, config.fullyParallel);
config.globalTimeout = takeFirst(configCLIOverrides.globalTimeout, config.globalTimeout);
config.maxFailures = takeFirst(configCLIOverrides.maxFailures, config.maxFailures);
config.outputDir = takeFirst(configCLIOverrides.outputDir, config.outputDir);
config.quiet = takeFirst(configCLIOverrides.quiet, config.quiet);
config.repeatEach = takeFirst(configCLIOverrides.repeatEach, config.repeatEach);
config.retries = takeFirst(configCLIOverrides.retries, config.retries);
if (configCLIOverrides.reporter)
config.reporter = toReporters(configCLIOverrides.reporter as any);
config.shard = takeFirst(configCLIOverrides.shard, config.shard);
config.timeout = takeFirst(configCLIOverrides.timeout, config.timeout);
config.updateSnapshots = takeFirst(configCLIOverrides.updateSnapshots, config.updateSnapshots);
config.ignoreSnapshots = takeFirst(configCLIOverrides.ignoreSnapshots, config.ignoreSnapshots);
if (configCLIOverrides.projects && config.projects)
throw new Error(`Cannot use --browser option when configuration file defines projects. Specify browserName in the projects instead.`);
config.projects = takeFirst(this._configCLIOverrides.projects, config.projects as any);
config.workers = takeFirst(this._configCLIOverrides.workers, config.workers);
config.use = mergeObjects(config.use, this._configCLIOverrides.use);
config.projects = takeFirst(configCLIOverrides.projects, config.projects as any);
config.workers = takeFirst(configCLIOverrides.workers, config.workers);
config.use = mergeObjects(config.use, configCLIOverrides.use);
for (const project of config.projects || [])
this._applyCLIOverridesToProject(project);
// 3. Resolve config.
this._configDir = configDir;
const packageJsonPath = getPackageJsonPath(configDir);
const packageJsonDir = packageJsonPath ? path.dirname(packageJsonPath) : undefined;
const throwawayArtifactsPath = packageJsonDir || process.cwd();
@ -109,8 +103,8 @@ export class ConfigLoader {
this._fullConfig._configDir = configDir;
this._fullConfig._storeDir = path.resolve(configDir, '.playwright-store');
this._fullConfig.configFile = this._configFile;
this._fullConfig.rootDir = config.testDir || this._configDir;
this._fullConfig.configFile = configFile;
this._fullConfig.rootDir = config.testDir || configDir;
this._fullConfig._globalOutputDir = takeFirst(config.outputDir, throwawayArtifactsPath, baseFullConfig._globalOutputDir);
this._fullConfig.forbidOnly = takeFirst(config.forbidOnly, baseFullConfig.forbidOnly);
this._fullConfig.fullyParallel = takeFirst(config.fullyParallel, baseFullConfig.fullyParallel);
@ -169,46 +163,30 @@ export class ConfigLoader {
}
}
async loadGlobalHook(file: string): Promise<(config: FullConfigInternal) => any> {
return this._requireOrImportDefaultFunction(path.resolve(this._fullConfig.rootDir, file), false);
}
async loadReporter(file: string): Promise<new (arg?: any) => Reporter> {
return this._requireOrImportDefaultFunction(path.resolve(this._fullConfig.rootDir, file), true);
}
fullConfig(): FullConfigInternal {
return this._fullConfig;
}
serializedConfig(): SerializedConfig {
const result: SerializedConfig = {
configFile: this._configFile,
configDir: this._configDir,
configCLIOverrides: this._configCLIOverrides,
};
return result;
}
private _applyCLIOverridesToProject(projectConfig: Project) {
projectConfig.fullyParallel = takeFirst(this._configCLIOverrides.fullyParallel, projectConfig.fullyParallel);
projectConfig.outputDir = takeFirst(this._configCLIOverrides.outputDir, projectConfig.outputDir);
projectConfig.repeatEach = takeFirst(this._configCLIOverrides.repeatEach, projectConfig.repeatEach);
projectConfig.retries = takeFirst(this._configCLIOverrides.retries, projectConfig.retries);
projectConfig.timeout = takeFirst(this._configCLIOverrides.timeout, projectConfig.timeout);
projectConfig.use = mergeObjects(projectConfig.use, this._configCLIOverrides.use);
const configCLIOverrides = this._fullConfig._configCLIOverrides;
projectConfig.fullyParallel = takeFirst(configCLIOverrides.fullyParallel, projectConfig.fullyParallel);
projectConfig.outputDir = takeFirst(configCLIOverrides.outputDir, projectConfig.outputDir);
projectConfig.repeatEach = takeFirst(configCLIOverrides.repeatEach, projectConfig.repeatEach);
projectConfig.retries = takeFirst(configCLIOverrides.retries, projectConfig.retries);
projectConfig.timeout = takeFirst(configCLIOverrides.timeout, projectConfig.timeout);
projectConfig.use = mergeObjects(projectConfig.use, configCLIOverrides.use);
}
private _resolveProject(config: Config, fullConfig: FullConfigInternal, projectConfig: Project, throwawayArtifactsPath: string): FullProjectInternal {
// Resolve all config dirs relative to configDir.
if (projectConfig.testDir !== undefined)
projectConfig.testDir = path.resolve(this._configDir, projectConfig.testDir);
projectConfig.testDir = path.resolve(fullConfig._configDir, projectConfig.testDir);
if (projectConfig.outputDir !== undefined)
projectConfig.outputDir = path.resolve(this._configDir, projectConfig.outputDir);
projectConfig.outputDir = path.resolve(fullConfig._configDir, projectConfig.outputDir);
if (projectConfig.snapshotDir !== undefined)
projectConfig.snapshotDir = path.resolve(this._configDir, projectConfig.snapshotDir);
projectConfig.snapshotDir = path.resolve(fullConfig._configDir, projectConfig.snapshotDir);
const testDir = takeFirst(projectConfig.testDir, config.testDir, this._configDir);
const testDir = takeFirst(projectConfig.testDir, config.testDir, fullConfig._configDir);
const respectGitIgnore = !projectConfig.testDir && !config.testDir;
const outputDir = takeFirst(projectConfig.outputDir, config.outputDir, path.join(throwawayArtifactsPath, 'test-results'));
@ -239,22 +217,13 @@ export class ConfigLoader {
use: mergeObjects(config.use, projectConfig.use),
};
}
}
private async _requireOrImportDefaultFunction(file: string, expectConstructor: boolean) {
let func = await requireOrImport(file);
if (func && typeof func === 'object' && ('default' in func))
func = func['default'];
if (typeof func !== 'function')
throw errorWithFile(file, `file must export a single ${expectConstructor ? 'class' : 'function'}.`);
return func;
}
private async _requireOrImportDefaultObject(file: string) {
let object = await requireOrImport(file);
if (object && typeof object === 'object' && ('default' in object))
object = object['default'];
return object;
}
async function requireOrImportDefaultObject(file: string) {
let object = await requireOrImport(file);
if (object && typeof object === 'object' && ('default' in object))
object = object['default'];
return object;
}
function takeFirst<T>(...args: (T | undefined)[]): T {
@ -462,6 +431,7 @@ export const baseFullConfig: FullConfigInternal = {
_webServers: [],
_globalOutputDir: path.resolve(process.cwd()),
_configDir: '',
_configCLIOverrides: {},
_storeDir: '',
_maxConcurrentTestGroups: 0,
_ignoreSnapshots: false,
@ -513,3 +483,14 @@ export function resolveConfigFile(configFileOrDirectory: string): string | null
return configFile!;
}
}
export const builtInReporters = ['list', 'line', 'dot', 'json', 'junit', 'null', 'github', 'html'] as const;
export type BuiltInReporter = typeof builtInReporters[number];
export function toReporters(reporters: BuiltInReporter | ReporterDescription[] | undefined): ReporterDescription[] | undefined {
if (!reporters)
return;
if (typeof reporters === 'string')
return [[reporters]];
return reporters;
}

View File

@ -32,3 +32,13 @@ export function setCurrentlyLoadingFileSuite(suite: Suite | undefined) {
export function currentlyLoadingFileSuite() {
return currentFileSuite;
}
export function currentExpectTimeout(options: { timeout?: number }) {
const testInfo = currentTestInfo();
if (options.timeout !== undefined)
return options.timeout;
let defaultExpectTimeout = testInfo?.project._expect?.timeout;
if (typeof defaultExpectTimeout === 'undefined')
defaultExpectTimeout = 5000;
return defaultExpectTimeout;
}

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
import type { TestInfoError, TestStatus } from './types';
import type { FullConfigInternal, TestInfoError, TestStatus } from './types';
export type ConfigCLIOverrides = {
forbidOnly?: boolean;
@ -120,3 +120,12 @@ export type TestOutputPayload = {
export type TeardownErrorsPayload = {
fatalErrors: TestInfoError[];
};
export function serializeConfig(config: FullConfigInternal): SerializedConfig {
const result: SerializedConfig = {
configFile: config.configFile,
configDir: config._configDir,
configCLIOverrides: config._configCLIOverrides,
};
return result;
}

View File

@ -16,6 +16,7 @@
import type { Fixtures, TestInfoError, Project } from '../../types/test';
import type { Location } from '../../types/testReporter';
import type { ConfigCLIOverrides } from './ipc';
import type { FullConfig as FullConfigPublic, FullProject as FullProjectPublic } from './types';
export * from '../../types/test';
export type { Location } from '../../types/testReporter';
@ -44,6 +45,7 @@ export interface TestStepInternal {
export interface FullConfigInternal extends FullConfigPublic {
_globalOutputDir: string;
_configDir: string;
_configCLIOverrides: ConfigCLIOverrides;
_storeDir: string;
_maxConcurrentTestGroups: number;
_ignoreSnapshots: boolean;

View File

@ -19,26 +19,27 @@ import { ConfigLoader } from '../common/configLoader';
import { ProcessRunner } from '../common/process';
import { loadTestFilesInProcess } from '../common/testLoader';
import type { LoadError } from '../common/fixtures';
import type { FullConfigInternal } from '../common/types';
export class LoaderMain extends ProcessRunner {
private _config: SerializedConfig;
private _configLoaderPromise: Promise<ConfigLoader> | undefined;
private _serializedConfig: SerializedConfig;
private _configPromise: Promise<FullConfigInternal> | undefined;
constructor(config: SerializedConfig) {
constructor(serializedConfig: SerializedConfig) {
super();
this._config = config;
this._serializedConfig = serializedConfig;
}
private _configLoader(): Promise<ConfigLoader> {
if (!this._configLoaderPromise)
this._configLoaderPromise = ConfigLoader.deserialize(this._config);
return this._configLoaderPromise;
private _config(): Promise<FullConfigInternal> {
if (!this._configPromise)
this._configPromise = ConfigLoader.deserialize(this._serializedConfig).then(configLoader => configLoader.fullConfig());
return this._configPromise;
}
async loadTestFiles(params: { files: string[] }) {
const loadErrors: LoadError[] = [];
const configLoader = await this._configLoader();
const rootSuite = await loadTestFilesInProcess(configLoader.fullConfig(), params.files, loadErrors);
const config = await this._config();
const rootSuite = await loadTestFilesInProcess(config, params.files, loadErrors);
return { rootSuite: rootSuite._deepSerialize(), loadErrors };
}
}

View File

@ -42,8 +42,8 @@ import {
} from './matchers';
import { toMatchSnapshot, toHaveScreenshot } from './toMatchSnapshot';
import type { Expect } from '../common/types';
import { currentTestInfo } from '../common/globals';
import { serializeError, captureStackTrace, currentExpectTimeout } from '../util';
import { currentTestInfo, currentExpectTimeout } from '../common/globals';
import { serializeError, captureStackTrace } from '../util';
import {
expect as expectLibrary,
INVERTED_COLOR,

View File

@ -16,8 +16,9 @@
import type { Expect } from '../common/types';
import type { ParsedStackTrace } from '../util';
import { expectTypes, callLogText, currentExpectTimeout, captureStackTrace } from '../util';
import { expectTypes, callLogText, captureStackTrace } from '../util';
import { matcherHint } from './matcherHint';
import { currentExpectTimeout } from '../common/globals';
export async function toBeTruthy(
this: ReturnType<Expect['getState']>,

View File

@ -16,10 +16,11 @@
import type { Expect } from '../common/types';
import { expectTypes } from '../util';
import { callLogText, currentExpectTimeout } from '../util';
import { callLogText } from '../util';
import type { ParsedStackTrace } from 'playwright-core/lib/utils';
import { captureStackTrace } from 'playwright-core/lib/utils';
import { matcherHint } from './matcherHint';
import { currentExpectTimeout } from '../common/globals';
// Omit colon and one or more spaces, so can call getLabelPrinter.
const EXPECTED_LABEL = 'Expected';

View File

@ -18,13 +18,13 @@ import type { Locator, Page } from 'playwright-core';
import type { Page as PageEx } from 'playwright-core/lib/client/page';
import type { Locator as LocatorEx } from 'playwright-core/lib/client/locator';
import type { Expect } from '../common/types';
import { currentTestInfo } from '../common/globals';
import { currentTestInfo, currentExpectTimeout } from '../common/globals';
import type { ImageComparatorOptions, Comparator } from 'playwright-core/lib/utils';
import { getComparator } from 'playwright-core/lib/utils';
import type { PageScreenshotOptions } from 'playwright-core/types/types';
import {
addSuffixToFilePath, serializeError, sanitizeForFilePath,
trimLongString, callLogText, currentExpectTimeout,
trimLongString, callLogText,
expectTypes, captureStackTrace } from '../util';
import { colors } from 'playwright-core/lib/utilsBundle';
import fs from 'fs';

View File

@ -19,12 +19,13 @@ import type { ExpectedTextValue } from '@protocol/channels';
import { isRegExp, isString } from 'playwright-core/lib/utils';
import type { Expect } from '../common/types';
import type { ParsedStackTrace } from '../util';
import { expectTypes, callLogText, currentExpectTimeout, captureStackTrace } from '../util';
import { expectTypes, callLogText, captureStackTrace } from '../util';
import {
printReceivedStringContainExpectedResult,
printReceivedStringContainExpectedSubstring
} from './expect';
import { matcherHint } from './matcherHint';
import { currentExpectTimeout } from '../common/globals';
export async function toMatchText(
this: ReturnType<Expect['getState']>,

View File

@ -15,14 +15,15 @@
*/
import type { TestBeginPayload, TestEndPayload, DonePayload, TestOutputPayload, StepBeginPayload, StepEndPayload, TeardownErrorsPayload, RunPayload, SerializedConfig } from '../common/ipc';
import { serializeConfig } from '../common/ipc';
import type { TestResult, Reporter, TestStep, TestError } from '../../types/testReporter';
import type { Suite } from '../common/test';
import type { ConfigLoader } from '../common/configLoader';
import type { ProcessExitData } from './processHost';
import type { TestCase } from '../common/test';
import { ManualPromise } from 'playwright-core/lib/utils';
import { WorkerHost } from './workerHost';
import type { TestGroup } from './testGroups';
import type { FullConfigInternal } from '../common/types';
type TestResultData = {
result: TestResult;
@ -42,13 +43,13 @@ export class Dispatcher {
private _isStopped = false;
private _testById = new Map<string, TestData>();
private _configLoader: ConfigLoader;
private _config: FullConfigInternal;
private _reporter: Reporter;
private _hasWorkerErrors = false;
private _failureCount = 0;
constructor(configLoader: ConfigLoader, testGroups: TestGroup[], reporter: Reporter) {
this._configLoader = configLoader;
constructor(config: FullConfigInternal, testGroups: TestGroup[], reporter: Reporter) {
this._config = config;
this._reporter = reporter;
this._queue = testGroups;
for (const group of testGroups) {
@ -125,7 +126,7 @@ export class Dispatcher {
// 2. Start the worker if it is down.
if (!worker) {
worker = this._createWorker(job, index, this._configLoader.serializedConfig());
worker = this._createWorker(job, index, serializeConfig(this._config));
this._workerSlots[index].worker = worker;
worker.on('exit', () => this._workerSlots[index].worker = undefined);
await worker.start();
@ -169,7 +170,7 @@ export class Dispatcher {
async run() {
this._workerSlots = [];
// 1. Allocate workers.
for (let i = 0; i < this._configLoader.fullConfig().workers; i++)
for (let i = 0; i < this._config.workers; i++)
this._workerSlots.push({ busy: false });
// 2. Schedule enough jobs.
for (let i = 0; i < this._workerSlots.length; i++)
@ -488,7 +489,7 @@ export class Dispatcher {
}
private _hasReachedMaxFailures() {
const maxFailures = this._configLoader.fullConfig().maxFailures;
const maxFailures = this._config.maxFailures;
return maxFailures > 0 && this._failureCount >= maxFailures;
}
@ -496,7 +497,7 @@ export class Dispatcher {
if (result.status !== 'skipped' && result.status !== test.expectedStatus)
++this._failureCount;
this._reporter.onTestEnd?.(test, result);
const maxFailures = this._configLoader.fullConfig().maxFailures;
const maxFailures = this._config.maxFailures;
if (maxFailures && this._failureCount === maxFailures)
this.stop().catch(e => {});
}

View File

@ -15,8 +15,7 @@
*/
import path from 'path';
import type { TestError } from '../../types/testReporter';
import type { ConfigLoader } from '../common/configLoader';
import type { Reporter, TestError } from '../../types/testReporter';
import type { LoadError } from '../common/fixtures';
import { LoaderHost } from './loaderHost';
import type { Multiplexer } from '../reporters/multiplexer';
@ -24,9 +23,12 @@ import { createRootSuite, filterOnly, filterSuite } from '../common/suiteUtils';
import type { Suite, TestCase } from '../common/test';
import { loadTestFilesInProcess } from '../common/testLoader';
import type { FullConfigInternal } from '../common/types';
import { errorWithFile } from '../util';
import type { Matcher, TestFileFilter } from '../util';
import { createFileMatcher } from '../util';
import { collectFilesForProjects, collectProjects } from './projectUtils';
import { requireOrImport } from '../common/transform';
import { serializeConfig } from '../common/ipc';
type LoadOptions = {
listOnly: boolean;
@ -36,8 +38,7 @@ type LoadOptions = {
passWithNoTests?: boolean;
};
export async function loadAllTests(configLoader: ConfigLoader, reporter: Multiplexer, options: LoadOptions, errors: TestError[]): Promise<Suite> {
const config = configLoader.fullConfig();
export async function loadAllTests(config: FullConfigInternal, reporter: Multiplexer, options: LoadOptions, errors: TestError[]): Promise<Suite> {
const projects = collectProjects(config, options.projectFilter);
const filesByProject = await collectFilesForProjects(projects, options.testFileFilters);
const allTestFiles = new Set<string>();
@ -45,7 +46,7 @@ export async function loadAllTests(configLoader: ConfigLoader, reporter: Multipl
files.forEach(file => allTestFiles.add(file));
// Load all tests.
const preprocessRoot = await loadTests(configLoader, reporter, allTestFiles, errors);
const preprocessRoot = await loadTests(config, reporter, allTestFiles, errors);
// Complain about duplicate titles.
errors.push(...createDuplicateTitlesErrors(config, preprocessRoot));
@ -67,10 +68,10 @@ export async function loadAllTests(configLoader: ConfigLoader, reporter: Multipl
return await createRootSuite(preprocessRoot, options.testTitleMatcher, filesByProject);
}
async function loadTests(configLoader: ConfigLoader, reporter: Multiplexer, testFiles: Set<string>, errors: TestError[]): Promise<Suite> {
async function loadTests(config: FullConfigInternal, reporter: Multiplexer, testFiles: Set<string>, errors: TestError[]): Promise<Suite> {
if (process.env.PW_TEST_OOP_LOADER) {
const loaderHost = new LoaderHost();
await loaderHost.start(configLoader.serializedConfig());
await loaderHost.start(serializeConfig(config));
try {
return await loaderHost.loadTestFiles([...testFiles], reporter);
} finally {
@ -79,7 +80,7 @@ async function loadTests(configLoader: ConfigLoader, reporter: Multiplexer, test
}
const loadErrors: LoadError[] = [];
try {
return await loadTestFilesInProcess(configLoader.fullConfig(), [...testFiles], loadErrors);
return await loadTestFilesInProcess(config, [...testFiles], loadErrors);
} finally {
errors.push(...loadErrors);
}
@ -140,3 +141,20 @@ function buildItemLocation(rootDir: string, testOrSuite: Suite | TestCase) {
return '';
return `${path.relative(rootDir, testOrSuite.location.file)}:${testOrSuite.location.line}`;
}
async function requireOrImportDefaultFunction(file: string, expectConstructor: boolean) {
let func = await requireOrImport(file);
if (func && typeof func === 'object' && ('default' in func))
func = func['default'];
if (typeof func !== 'function')
throw errorWithFile(file, `file must export a single ${expectConstructor ? 'class' : 'function'}.`);
return func;
}
export function loadGlobalHook(config: FullConfigInternal, file: string): Promise<(config: FullConfigInternal) => any> {
return requireOrImportDefaultFunction(path.resolve(config.rootDir, file), false);
}
export function loadReporter(config: FullConfigInternal, file: string): Promise<new (arg?: any) => Reporter> {
return requireOrImportDefaultFunction(path.resolve(config.rootDir, file), true);
}

View File

@ -16,7 +16,6 @@
import path from 'path';
import type { Reporter, TestError } from '../../types/testReporter';
import type { ConfigLoader } from '../common/configLoader';
import { formatError } from '../reporters/base';
import DotReporter from '../reporters/dot';
import EmptyReporter from '../reporters/empty';
@ -28,9 +27,11 @@ import LineReporter from '../reporters/line';
import ListReporter from '../reporters/list';
import { Multiplexer } from '../reporters/multiplexer';
import type { Suite } from '../common/test';
import type { FullConfigInternal, ReporterDescription } from '../common/types';
import type { FullConfigInternal } from '../common/types';
import { loadReporter } from './loadUtils';
import type { BuiltInReporter } from '../common/configLoader';
export async function createReporter(configLoader: ConfigLoader, list: boolean) {
export async function createReporter(config: FullConfigInternal, list: boolean) {
const defaultReporters: {[key in BuiltInReporter]: new(arg: any) => Reporter} = {
dot: list ? ListModeReporter : DotReporter,
line: list ? ListModeReporter : LineReporter,
@ -42,17 +43,17 @@ export async function createReporter(configLoader: ConfigLoader, list: boolean)
html: HtmlReporter,
};
const reporters: Reporter[] = [];
for (const r of configLoader.fullConfig().reporter) {
for (const r of config.reporter) {
const [name, arg] = r;
if (name in defaultReporters) {
reporters.push(new defaultReporters[name as keyof typeof defaultReporters](arg));
} else {
const reporterConstructor = await configLoader.loadReporter(name);
const reporterConstructor = await loadReporter(config, name);
reporters.push(new reporterConstructor(arg));
}
}
if (process.env.PW_TEST_REPORTER) {
const reporterConstructor = await configLoader.loadReporter(process.env.PW_TEST_REPORTER);
const reporterConstructor = await loadReporter(config, process.env.PW_TEST_REPORTER);
reporters.push(new reporterConstructor());
}
@ -98,14 +99,3 @@ export class ListModeReporter implements Reporter {
console.error('\n' + formatError(this.config, error, false).message);
}
}
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];

View File

@ -17,7 +17,6 @@
import { monotonicTime } from 'playwright-core/lib/utils';
import type { FullResult } from '../../types/testReporter';
import { ConfigLoader } from '../common/configLoader';
import type { TestRunnerPlugin } from '../plugins';
import { setRunnerToAddPluginsTo } from '../plugins';
import { dockerPlugin } from '../plugins/dockerPlugin';
@ -26,9 +25,8 @@ import { collectFilesForProjects, collectProjects } from './projectUtils';
import { createReporter } from './reporters';
import { createTaskRunner } from './tasks';
import type { TaskRunnerState } from './tasks';
import type { Config, FullConfigInternal } from '../common/types';
import type { FullConfigInternal } from '../common/types';
import type { Matcher, TestFileFilter } from '../util';
import type { ConfigCLIOverrides } from '../common/ipc';
export type RunOptions = {
listOnly: boolean;
@ -39,11 +37,11 @@ export type RunOptions = {
};
export class Runner {
private _configLoader: ConfigLoader;
private _config: FullConfigInternal;
private _plugins: TestRunnerPlugin[] = [];
constructor(configCLIOverrides?: ConfigCLIOverrides) {
this._configLoader = new ConfigLoader(configCLIOverrides);
constructor(config: FullConfigInternal) {
this._config = config;
setRunnerToAddPluginsTo(this);
}
@ -51,16 +49,8 @@ export class Runner {
this._plugins.push(plugin);
}
async loadConfigFromResolvedFile(resolvedConfigFile: string): Promise<FullConfigInternal> {
return await this._configLoader.loadConfigFile(resolvedConfigFile);
}
loadEmptyConfig(configFileOrDirectory: string): Promise<Config> {
return this._configLoader.loadEmptyConfig(configFileOrDirectory);
}
async listTestFiles(projectNames: string[] | undefined): Promise<any> {
const projects = collectProjects(this._configLoader.fullConfig(), projectNames);
const projects = collectProjects(this._config, projectNames);
const filesByProject = await collectFilesForProjects(projects, []);
const report: any = {
projects: []
@ -75,7 +65,7 @@ export class Runner {
}
async runAllTests(options: RunOptions): Promise<FullResult['status']> {
const config = this._configLoader.fullConfig();
const config = this._config;
const deadline = config.globalTimeout ? monotonicTime() + config.globalTimeout : 0;
// Legacy webServer support.
@ -83,12 +73,11 @@ export class Runner {
// Docker support.
this._plugins.push(dockerPlugin);
const reporter = await createReporter(this._configLoader, options.listOnly);
const reporter = await createReporter(config, options.listOnly);
const taskRunner = createTaskRunner(config, reporter, this._plugins, options);
const context: TaskRunnerState = {
config,
configLoader: this._configLoader,
options,
reporter,
};

View File

@ -18,7 +18,6 @@ import fs from 'fs';
import path from 'path';
import { promisify } from 'util';
import { colors, rimraf } from 'playwright-core/lib/utilsBundle';
import type { ConfigLoader } from '../common/configLoader';
import { Dispatcher } from './dispatcher';
import type { TestRunnerPlugin } from '../plugins';
import type { Multiplexer } from '../reporters/multiplexer';
@ -28,7 +27,7 @@ import type { Task } from './taskRunner';
import { TaskRunner } from './taskRunner';
import type { Suite } from '../common/test';
import type { FullConfigInternal } from '../common/types';
import { loadAllTests } from './loadUtils';
import { loadAllTests, loadGlobalHook } from './loadUtils';
import type { Matcher, TestFileFilter } from '../util';
const removeFolderAsync = promisify(rimraf);
@ -46,7 +45,6 @@ export type TaskRunnerState = {
options: TaskRunnerOptions;
reporter: Multiplexer;
config: FullConfigInternal;
configLoader: ConfigLoader;
rootSuite?: Suite;
testGroups?: TestGroup[];
dispatcher?: Dispatcher;
@ -90,10 +88,10 @@ export function createPluginSetupTask(plugin: TestRunnerPlugin): Task<TaskRunner
}
export function createGlobalSetupTask(): Task<TaskRunnerState> {
return async ({ config, configLoader }) => {
const setupHook = config.globalSetup ? await configLoader.loadGlobalHook(config.globalSetup) : undefined;
const teardownHook = config.globalTeardown ? await configLoader.loadGlobalHook(config.globalTeardown) : undefined;
const globalSetupResult = setupHook ? await setupHook(configLoader.fullConfig()) : undefined;
return async ({ config }) => {
const setupHook = config.globalSetup ? await loadGlobalHook(config, config.globalSetup) : undefined;
const teardownHook = config.globalTeardown ? await loadGlobalHook(config, config.globalTeardown) : undefined;
const globalSetupResult = setupHook ? await setupHook(config) : undefined;
return async () => {
if (typeof globalSetupResult === 'function')
await globalSetupResult();
@ -104,7 +102,7 @@ export function createGlobalSetupTask(): Task<TaskRunnerState> {
export function createSetupWorkersTask(): Task<TaskRunnerState> {
return async params => {
const { config, configLoader, testGroups, reporter } = params;
const { config, testGroups, reporter } = params;
if (config._ignoreSnapshots) {
reporter.onStdOut(colors.dim([
'NOTE: running with "ignoreSnapshots" option. All of the following asserts are silently ignored:',
@ -114,7 +112,7 @@ export function createSetupWorkersTask(): Task<TaskRunnerState> {
].join('\n')));
}
const dispatcher = new Dispatcher(configLoader, testGroups!, reporter);
const dispatcher = new Dispatcher(config, testGroups!, reporter);
params.dispatcher = dispatcher;
return async () => {
await dispatcher.stop();
@ -123,8 +121,7 @@ export function createSetupWorkersTask(): Task<TaskRunnerState> {
}
export function createRemoveOutputDirsTask(): Task<TaskRunnerState> {
return async ({ options, configLoader }) => {
const config = configLoader.fullConfig();
return async ({ config, options }) => {
const outputDirs = new Set<string>();
for (const p of config.projects) {
if (!options.projectFilter || options.projectFilter.includes(p.name))
@ -147,8 +144,8 @@ export function createRemoveOutputDirsTask(): Task<TaskRunnerState> {
function createLoadTask(): Task<TaskRunnerState> {
return async (context, errors) => {
const { config, reporter, options, configLoader } = context;
const rootSuite = await loadAllTests(configLoader, reporter, options, errors);
const { config, reporter, options } = context;
const rootSuite = await loadAllTests(config, reporter, options, errors);
const testGroups = options.listOnly ? [] : createTestGroups(rootSuite.suites, config.workers);
context.rootSuite = rootSuite;

View File

@ -23,7 +23,6 @@ import { colors, debug, minimatch } from 'playwright-core/lib/utilsBundle';
import type { TestInfoError, Location } from './common/types';
import { calculateSha1, isRegExp, isString, captureStackTrace as coreCaptureStackTrace } from 'playwright-core/lib/utils';
import { isInternalFileName } from 'playwright-core/lib/utils';
import { currentTestInfo } from './common/globals';
import type { ParsedStackTrace } from 'playwright-core/lib/utils';
export type { ParsedStackTrace };
@ -31,7 +30,7 @@ export type { ParsedStackTrace };
const PLAYWRIGHT_CORE_PATH = path.dirname(require.resolve('playwright-core'));
const EXPECT_PATH = require.resolve('./common/expectBundle');
const EXPECT_PATH_IMPL = require.resolve('./common/expectBundleImpl');
const PLAYWRIGHT_TEST_PATH = path.join(__dirname, '../..');
const PLAYWRIGHT_TEST_PATH = path.join(__dirname, '..');
function filterStackTrace(e: Error) {
if (process.env.PWDEBUGIMPL)
@ -243,16 +242,6 @@ Call log:
`;
}
export function currentExpectTimeout(options: { timeout?: number }) {
const testInfo = currentTestInfo();
if (options.timeout !== undefined)
return options.timeout;
let defaultExpectTimeout = testInfo?.project._expect?.timeout;
if (typeof defaultExpectTimeout === 'undefined')
defaultExpectTimeout = 5000;
return defaultExpectTimeout;
}
const folderToPackageJsonPath = new Map<string, string>();
export function getPackageJsonPath(folderPath: string): string {

View File

@ -21,7 +21,7 @@ import type { TestBeginPayload, TestEndPayload, RunPayload, DonePayload, WorkerI
import { setCurrentTestInfo } from '../common/globals';
import { ConfigLoader } from '../common/configLoader';
import type { Suite, TestCase } from '../common/test';
import type { Annotation, FullProjectInternal, TestInfoError } from '../common/types';
import type { Annotation, FullConfigInternal, FullProjectInternal, TestInfoError } from '../common/types';
import { FixtureRunner } from '../common/fixtures';
import { ManualPromise } from 'playwright-core/lib/utils';
import { TestInfoImpl } from '../common/testInfo';
@ -36,7 +36,7 @@ const removeFolderAsync = util.promisify(rimraf);
export class WorkerMain extends ProcessRunner {
private _params: WorkerInitParams;
private _configLoader!: ConfigLoader;
private _config!: FullConfigInternal;
private _testLoader!: TestLoader;
private _project!: FullProjectInternal;
private _poolBuilder!: PoolBuilder;
@ -190,12 +190,13 @@ export class WorkerMain extends ProcessRunner {
}
private async _loadIfNeeded() {
if (this._configLoader)
if (this._config)
return;
this._configLoader = await ConfigLoader.deserialize(this._params.config);
this._testLoader = new TestLoader(this._configLoader.fullConfig());
this._project = this._configLoader.fullConfig().projects.find(p => p._id === this._params.projectId)!;
const configLoader = await ConfigLoader.deserialize(this._params.config);
this._config = configLoader.fullConfig();
this._testLoader = new TestLoader(this._config);
this._project = this._config.projects.find(p => p._id === this._params.projectId)!;
this._poolBuilder = PoolBuilder.createForWorker(this._project);
}
@ -251,7 +252,7 @@ export class WorkerMain extends ProcessRunner {
}
private async _runTest(test: TestCase, retry: number, nextTest: TestCase | undefined) {
const testInfo = new TestInfoImpl(this._configLoader.fullConfig(), this._project, this._params, test, retry,
const testInfo = new TestInfoImpl(this._config, this._project, this._params, test, retry,
stepBeginPayload => this.dispatchEvent('stepBegin', stepBeginPayload),
stepEndPayload => this.dispatchEvent('stepEnd', stepEndPayload));
@ -485,8 +486,8 @@ export class WorkerMain extends ProcessRunner {
setCurrentTestInfo(null);
this.dispatchEvent('testEnd', buildTestEndPayload(testInfo));
const preserveOutput = this._configLoader.fullConfig().preserveOutput === 'always' ||
(this._configLoader.fullConfig().preserveOutput === 'failures-only' && testInfo._isFailure());
const preserveOutput = this._config.preserveOutput === 'always' ||
(this._config.preserveOutput === 'failures-only' && testInfo._isFailure());
if (!preserveOutput)
await removeFolderAsync(testInfo.outputDir).catch(e => {});
}