chore: try serializing the config instead of requiring it in the worker (#13839)

This commit is contained in:
Pavel Feldman 2022-04-29 15:05:08 -08:00 коммит произвёл GitHub
Родитель 4b682f9f13
Коммит a1b10c3856
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
10 изменённых файлов: 131 добавлений и 73 удалений

Просмотреть файл

@ -16,12 +16,15 @@
import type { TestError } from '../types/testReporter';
import type { ConfigCLIOverrides } from './runner';
import type { TestStatus } from './types';
import type { FullConfigInternal, TestStatus } from './types';
export type SerializedLoaderData = {
overrides: ConfigCLIOverrides;
configFile: { file: string } | { configDir: string };
config: FullConfigInternal;
configFile: string | undefined;
configDir: string;
overridesForLegacyConfigMode?: ConfigCLIOverrides;
};
export type WorkerInitParams = {
workerIndex: number;
parallelIndex: number;

Просмотреть файл

@ -25,7 +25,6 @@ import * as path from 'path';
import * as url from 'url';
import * as fs from 'fs';
import * as os from 'os';
import { ProjectImpl } from './project';
import type { BuiltInReporter, ConfigCLIOverrides } from './runner';
import type { Reporter } from '../types/testReporter';
import { builtInReporters } from './runner';
@ -45,7 +44,6 @@ export class Loader {
private _fullConfig: FullConfigInternal;
private _configDir: string = '';
private _configFile: string | undefined;
private _projects: ProjectImpl[] = [];
constructor(configCLIOverrides?: ConfigCLIOverrides) {
this._configCLIOverrides = configCLIOverrides || {};
@ -53,12 +51,20 @@ export class Loader {
}
static async deserialize(data: SerializedLoaderData): Promise<Loader> {
const loader = new Loader(data.overrides);
if ('file' in data.configFile)
await loader.loadConfigFile(data.configFile.file);
else
await loader.loadEmptyConfig(data.configFile.configDir);
return loader;
if (process.env.PLAYWRIGHT_LEGACY_CONFIG_MODE) {
const loader = new Loader(data.overridesForLegacyConfigMode);
if (data.configFile)
await loader.loadConfigFile(data.configFile);
else
await loader.loadEmptyConfig(data.configDir);
return loader;
} else {
const loader = new Loader();
loader._configFile = data.configFile;
loader._configDir = data.configDir;
loader._fullConfig = data.config;
return loader;
}
}
async loadConfigFile(file: string): Promise<FullConfigInternal> {
@ -107,6 +113,8 @@ export class Loader {
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);
for (const project of config.projects || [])
this._applyCLIOverridesToProject(project);
// 3. Run configure plugins phase.
for (const plugin of config.plugins || [])
@ -155,11 +163,7 @@ export class Loader {
this._fullConfig.workers = takeFirst(config.workers, baseFullConfig.workers);
this._fullConfig.webServer = takeFirst(config.webServer, baseFullConfig.webServer);
this._fullConfig._plugins = takeFirst(config.plugins, baseFullConfig._plugins);
const projects: Project[] = this._configCLIOverrides.projects || config.projects || [config];
for (const project of projects)
this._addProject(config, project, throwawayArtifactsPath);
this._fullConfig.projects = this._projects.map(p => p.config);
this._fullConfig.projects = (config.projects || [config]).map(p => this._resolveProject(config, p, throwawayArtifactsPath));
}
async loadTestFile(file: string, environment: 'runner' | 'worker') {
@ -228,18 +232,28 @@ export class Loader {
return this._fullConfig;
}
projects() {
return this._projects;
}
serialize(): SerializedLoaderData {
return {
configFile: this._configFile ? { file: this._configFile } : { configDir: this._configDir },
overrides: this._configCLIOverrides,
const result: SerializedLoaderData = {
configFile: this._configFile,
configDir: this._configDir,
config: this._fullConfig,
};
if (process.env.PLAYWRIGHT_LEGACY_CONFIG_MODE)
result.overridesForLegacyConfigMode = this._configCLIOverrides;
return result;
}
private _addProject(config: Config, projectConfig: Project, throwawayArtifactsPath: string) {
private _applyCLIOverridesToProject(projectConfig: Project) {
projectConfig.fullyParallel = takeFirst(this._configCLIOverrides.fullyParallel, projectConfig.fullyParallel);
projectConfig.grep = takeFirst(this._configCLIOverrides.grep, projectConfig.grep);
projectConfig.grepInvert = takeFirst(this._configCLIOverrides.grepInvert, projectConfig.grepInvert);
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);
}
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);
@ -250,21 +264,13 @@ export class Loader {
if (projectConfig.snapshotDir !== undefined)
projectConfig.snapshotDir = path.resolve(this._configDir, projectConfig.snapshotDir);
projectConfig.fullyParallel = takeFirst(this._configCLIOverrides.fullyParallel, projectConfig.fullyParallel);
projectConfig.grep = takeFirst(this._configCLIOverrides.grep, projectConfig.grep);
projectConfig.grepInvert = takeFirst(this._configCLIOverrides.grepInvert, projectConfig.grepInvert);
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);
const testDir = takeFirst(projectConfig.testDir, config.testDir, this._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 screenshotsDir = takeFirst((projectConfig as any).screenshotsDir, (config as any).screenshotsDir, path.join(testDir, '__screenshots__', process.platform, name));
const fullProject: FullProjectInternal = {
return {
_fullyParallel: takeFirst(projectConfig.fullyParallel, config.fullyParallel, undefined),
_expect: takeFirst(projectConfig.expect, config.expect, undefined),
grep: takeFirst(projectConfig.grep, config.grep, baseFullConfig.grep),
@ -282,7 +288,6 @@ export class Loader {
timeout: takeFirst(projectConfig.timeout, config.timeout, defaultTimeout),
use: mergeObjects(config.use, projectConfig.use),
};
this._projects.push(new ProjectImpl(fullProject, this._projects.length));
}
private async _requireOrImport(file: string) {

Просмотреть файл

@ -32,9 +32,9 @@ export class ProjectImpl {
this.index = index;
}
private buildTestTypePool(testType: TestTypeImpl): FixturePool {
private _buildTestTypePool(testType: TestTypeImpl): FixturePool {
if (!this.testTypePools.has(testType)) {
const fixtures = this.resolveFixtures(testType, this.config.use);
const fixtures = this._resolveFixtures(testType, this.config.use);
const pool = new FixturePool(fixtures);
this.testTypePools.set(testType, pool);
}
@ -42,9 +42,9 @@ export class ProjectImpl {
}
// TODO: we can optimize this function by building the pool inline in cloneSuite
private buildPool(test: TestCase): FixturePool {
private _buildPool(test: TestCase): FixturePool {
if (!this.testPools.has(test)) {
let pool = this.buildTestTypePool(test._testType);
let pool = this._buildTestTypePool(test._testType);
const parents: Suite[] = [];
for (let parent: Suite | undefined = test.parent; parent; parent = parent.parent)
@ -88,7 +88,7 @@ export class ProjectImpl {
to._entries.pop();
to.tests.pop();
} else {
const pool = this.buildPool(entry);
const pool = this._buildPool(entry);
test._workerHash = `run${this.index}-${pool.digest}-repeat${repeatEachIndex}`;
test._pool = pool;
}
@ -104,7 +104,7 @@ export class ProjectImpl {
return this._cloneEntries(suite, result, repeatEachIndex, filter, '') ? result : undefined;
}
private resolveFixtures(testType: TestTypeImpl, configUse: Fixtures): FixturesWithLocation[] {
private _resolveFixtures(testType: TestTypeImpl, configUse: Fixtures): FixturesWithLocation[] {
return testType.fixtures.map(f => {
const configKeys = new Set(Object.keys(configUse || {}));
const resolved = { ...f.fixtures };

Просмотреть файл

@ -37,8 +37,8 @@ import JSONReporter from './reporters/json';
import JUnitReporter from './reporters/junit';
import EmptyReporter from './reporters/empty';
import HtmlReporter from './reporters/html';
import type { ProjectImpl } from './project';
import type { Config } from './types';
import { ProjectImpl } from './project';
import type { Config, FullProjectInternal } from './types';
import type { FullConfigInternal } from './types';
import { raceAgainstTimeout } from 'playwright-core/lib/utils/timeoutRunner';
import { SigIntWatcher } from './sigIntWatcher';
@ -194,8 +194,8 @@ export class Runner {
};
for (const [project, files] of filesByProject) {
report.projects.push({
name: project.config.name,
testDir: path.resolve(configFile, project.config.testDir),
name: project.name,
testDir: path.resolve(configFile, project.testDir),
files: files
});
}
@ -207,7 +207,7 @@ export class Runner {
return await this._runFiles(list, filesByProject, testFileReFilters);
}
private async _collectFiles(testFileReFilters: FilePatternFilter[], projectNames?: string[]): Promise<Map<ProjectImpl, string[]>> {
private async _collectFiles(testFileReFilters: FilePatternFilter[], projectNames?: string[]): Promise<Map<FullProjectInternal, string[]>> {
const testFileFilter = testFileReFilters.length ? createFileMatcher(testFileReFilters.map(e => e.re)) : () => true;
let projectsToFind: Set<string> | undefined;
let unknownProjects: Map<string, string> | undefined;
@ -220,26 +220,27 @@ export class Runner {
unknownProjects!.set(name, n);
});
}
const projects = this._loader.projects().filter(project => {
const fullConfig = this._loader.fullConfig();
const projects = fullConfig.projects.filter(project => {
if (!projectsToFind)
return true;
const name = project.config.name.toLocaleLowerCase();
const name = project.name.toLocaleLowerCase();
unknownProjects!.delete(name);
return projectsToFind.has(name);
});
if (unknownProjects && unknownProjects.size) {
const names = this._loader.projects().map(p => p.config.name).filter(name => !!name);
const names = fullConfig.projects.map(p => p.name).filter(name => !!name);
if (!names.length)
throw new Error(`No named projects are specified in the configuration file`);
const unknownProjectNames = Array.from(unknownProjects.values()).map(n => `"${n}"`).join(', ');
throw new Error(`Project(s) ${unknownProjectNames} not found. Available named projects: ${names.map(name => `"${name}"`).join(', ')}`);
}
const files = new Map<ProjectImpl, string[]>();
const files = new Map<FullProjectInternal, string[]>();
for (const project of projects) {
const allFiles = await collectFiles(project.config.testDir);
const testMatch = createFileMatcher(project.config.testMatch);
const testIgnore = createFileMatcher(project.config.testIgnore);
const allFiles = await collectFiles(project.testDir);
const testMatch = createFileMatcher(project.testMatch);
const testIgnore = createFileMatcher(project.testIgnore);
const extensions = ['.js', '.ts', '.mjs', '.tsx', '.jsx'];
const testFileExtension = (file: string) => extensions.includes(path.extname(file));
const testFiles = allFiles.filter(file => !testIgnore(file) && testMatch(file) && testFileFilter(file) && testFileExtension(file));
@ -248,7 +249,7 @@ export class Runner {
return files;
}
private async _runFiles(list: boolean, filesByProject: Map<ProjectImpl, string[]>, testFileReFilters: FilePatternFilter[]): Promise<FullResult> {
private async _runFiles(list: boolean, filesByProject: Map<FullProjectInternal, string[]>, testFileReFilters: FilePatternFilter[]): Promise<FullResult> {
const allTestFiles = new Set<string>();
for (const files of filesByProject.values())
files.forEach(file => allTestFiles.add(file));
@ -293,19 +294,20 @@ export class Runner {
const outputDirs = new Set<string>();
const rootSuite = new Suite('');
for (const [project, files] of filesByProject) {
const grepMatcher = createTitleMatcher(project.config.grep);
const grepInvertMatcher = project.config.grepInvert ? createTitleMatcher(project.config.grepInvert) : null;
const projectSuite = new Suite(project.config.name);
projectSuite._projectConfig = project.config;
if (project.config._fullyParallel)
const projectImpl = new ProjectImpl(project, config.projects.indexOf(project));
const grepMatcher = createTitleMatcher(project.grep);
const grepInvertMatcher = project.grepInvert ? createTitleMatcher(project.grepInvert) : null;
const projectSuite = new Suite(project.name);
projectSuite._projectConfig = project;
if (project._fullyParallel)
projectSuite._parallelMode = 'parallel';
rootSuite._addSuite(projectSuite);
for (const file of files) {
const fileSuite = fileSuites.get(file);
if (!fileSuite)
continue;
for (let repeatEachIndex = 0; repeatEachIndex < project.config.repeatEach; repeatEachIndex++) {
const cloned = project.cloneFileSuite(fileSuite, repeatEachIndex, test => {
for (let repeatEachIndex = 0; repeatEachIndex < project.repeatEach; repeatEachIndex++) {
const cloned = projectImpl.cloneFileSuite(fileSuite, repeatEachIndex, test => {
const grepTitle = test.titlePath().join(' ');
if (grepInvertMatcher?.(grepTitle))
return false;
@ -315,7 +317,7 @@ export class Runner {
projectSuite._addSuite(cloned);
}
}
outputDirs.add(project.config.outputDir);
outputDirs.add(project.outputDir);
}
// 7. Fail when no tests.

Просмотреть файл

@ -85,12 +85,13 @@ export class TestInfoImpl implements TestInfo {
constructor(
loader: Loader,
projectImpl: ProjectImpl,
workerParams: WorkerInitParams,
test: TestCase,
retry: number,
addStepImpl: (data: Omit<TestStepInternal, 'complete'>) => TestStepInternal,
) {
this._projectImpl = loader.projects()[workerParams.projectIndex];
this._projectImpl = projectImpl;
this._test = test;
this._addStepImpl = addStepImpl;
this._startTime = monotonicTime();
@ -113,10 +114,10 @@ export class TestInfoImpl implements TestInfo {
this._timeoutManager = new TimeoutManager(this.project.timeout);
this.outputDir = (() => {
const sameName = loader.projects().filter(project => project.config.name === this.project.name);
const sameName = loader.fullConfig().projects.filter(project => project.name === this.project.name);
let uniqueProjectNamePathSegment: string;
if (sameName.length > 1)
uniqueProjectNamePathSegment = this.project.name + (sameName.indexOf(this._projectImpl) + 1);
uniqueProjectNamePathSegment = this.project.name + (sameName.indexOf(this._projectImpl.config) + 1);
else
uniqueProjectNamePathSegment = this.project.name;

Просмотреть файл

@ -23,7 +23,7 @@ import { setCurrentTestInfo } from './globals';
import { Loader } from './loader';
import type { Suite, TestCase } from './test';
import type { Annotation, TestError, TestStepInternal } from './types';
import type { ProjectImpl } from './project';
import { ProjectImpl } from './project';
import { FixtureRunner } from './fixtures';
import { ManualPromise } from 'playwright-core/lib/utils/manualPromise';
import { TestInfoImpl } from './testInfo';
@ -151,7 +151,7 @@ export class WorkerRunner extends EventEmitter {
return;
this._loader = await Loader.deserialize(this._params.loader);
this._project = this._loader.projects()[this._params.projectIndex];
this._project = new ProjectImpl(this._loader.fullConfig().projects[this._params.projectIndex], this._params.projectIndex);
}
async runTestGroup(runPayload: RunPayload) {
@ -207,7 +207,7 @@ export class WorkerRunner extends EventEmitter {
private async _runTest(test: TestCase, retry: number, nextTest: TestCase | undefined) {
let lastStepId = 0;
const testInfo = new TestInfoImpl(this._loader, this._params, test, retry, data => {
const testInfo = new TestInfoImpl(this._loader, this._project, this._params, test, retry, data => {
const stepId = `${data.category}@${data.title}@${++lastStepId}`;
let callbackHandled = false;
const step: TestStepInternal = {

Просмотреть файл

@ -215,6 +215,7 @@ type RunOptions = {
cwd?: string,
};
type Fixtures = {
legacyConfigLoader: boolean;
writeFiles: (files: Files) => Promise<string>;
runInlineTest: (files: Files, params?: Params, env?: Env, options?: RunOptions, beforeRunPlaywrightTest?: ({ baseDir }: { baseDir: string }) => Promise<void>) => Promise<RunResult>;
runTSC: (files: Files) => Promise<TSCResult>;
@ -224,15 +225,19 @@ export const test = base
.extend<CommonFixtures>(commonFixtures)
.extend<ServerFixtures, ServerWorkerOptions>(serverFixtures)
.extend<Fixtures>({
legacyConfigLoader: [false, { option: true }],
writeFiles: async ({}, use, testInfo) => {
await use(files => writeFiles(testInfo, files));
},
runInlineTest: async ({ childProcess }, use, testInfo: TestInfo) => {
runInlineTest: async ({ childProcess, legacyConfigLoader }, use, testInfo: TestInfo) => {
await use(async (files: Files, params: Params = {}, env: Env = {}, options: RunOptions = {}, beforeRunPlaywrightTest?: ({ baseDir: string }) => Promise<void>) => {
const baseDir = await writeFiles(testInfo, files);
if (beforeRunPlaywrightTest)
await beforeRunPlaywrightTest({ baseDir });
if (legacyConfigLoader)
env = { ...env, PLAYWRIGHT_LEGACY_CONFIG_MODE: '1' };
return await runPlaywrightTest(childProcess, baseDir, params, env, options);
});
},

Просмотреть файл

@ -29,7 +29,13 @@ const config: Config = {
workers: process.env.CI ? 1 : undefined,
preserveOutput: process.env.CI ? 'failures-only' : 'always',
projects: [
{ name: 'playwright-test' },
{
name: 'playwright-test'
},
{
name: 'playwright-test-legacy-config',
use: { legacyConfigLoader: true },
} as any,
],
reporter: process.env.CI ? [
['dot'],

Просмотреть файл

@ -16,7 +16,8 @@
import { test, expect } from './playwright-test-fixtures';
test('should work with connectOptions', async ({ runInlineTest }) => {
test('should work with connectOptions (legacy)', async ({ runInlineTest, legacyConfigLoader }) => {
test.skip(!legacyConfigLoader, 'Not supported in the new mode');
const result = await runInlineTest({
'playwright.config.js': `
module.exports = {
@ -49,6 +50,42 @@ test('should work with connectOptions', async ({ runInlineTest }) => {
expect(result.passed).toBe(1);
});
test('should work with connectOptions', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.js': `
module.exports = { plugins: [require('./plugin')] };
`,
'plugin.js': `
let server;
module.exports = {
configure: async (config) => {
server = await pwt.chromium.launchServer();
config.use = {
connectOptions: {
wsEndpoint: server.wsEndpoint()
}
};
},
teardown: async () => {
await server.close();
}
};
`,
'a.test.ts': `
const { test } = pwt;
test.use({ locale: 'fr-CH' });
test('pass', async ({ page }) => {
await page.setContent('<div>PASS</div>');
await expect(page.locator('div')).toHaveText('PASS');
expect(await page.evaluate(() => navigator.language)).toBe('fr-CH');
});
`,
});
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
});
test('should throw with bad connectOptions', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.js': `

Просмотреть файл

@ -16,7 +16,8 @@
import fs from 'fs';
import { test, expect } from './playwright-test-fixtures';
test('event order', async ({ runInlineTest }, testInfo) => {
test('event order', async ({ runInlineTest, legacyConfigLoader }, testInfo) => {
test.skip(legacyConfigLoader);
const log = testInfo.outputPath('logs.txt');
const result = await runInlineTest({
'log.ts': `
@ -87,8 +88,6 @@ test('event order', async ({ runInlineTest }, testInfo) => {
'a setup',
'b setup',
'globalSetup',
'a configure',
'b configure',
'baseURL a | b | ',
'globalTeardown',
'b teardown',