mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: split config and test loaders (#20175)
This commit is contained in:
parent
020dcd89fa
commit
d9d4070520
@ -23,10 +23,11 @@ import path from 'path';
|
|||||||
import { Runner, builtInReporters, kDefaultConfigFiles } from './runner';
|
import { Runner, builtInReporters, kDefaultConfigFiles } from './runner';
|
||||||
import type { ConfigCLIOverrides } from './runner';
|
import type { ConfigCLIOverrides } from './runner';
|
||||||
import { stopProfiling, startProfiling } from './profiler';
|
import { stopProfiling, startProfiling } from './profiler';
|
||||||
|
import { fileIsModule } from './util';
|
||||||
import type { TestFileFilter } from './util';
|
import type { TestFileFilter } from './util';
|
||||||
import { createTitleMatcher } from './util';
|
import { createTitleMatcher } from './util';
|
||||||
import { showHTMLReport } from './reporters/html';
|
import { showHTMLReport } from './reporters/html';
|
||||||
import { baseFullConfig, defaultTimeout, fileIsModule } from './loader';
|
import { baseFullConfig, defaultTimeout } from './configLoader';
|
||||||
import type { TraceMode } from './types';
|
import type { TraceMode } from './types';
|
||||||
|
|
||||||
export function addTestCommands(program: Command) {
|
export function addTestCommands(program: Command) {
|
||||||
|
|||||||
@ -17,40 +17,30 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { calculateSha1, isRegExp } from 'playwright-core/lib/utils';
|
import { isRegExp } from 'playwright-core/lib/utils';
|
||||||
import * as url from 'url';
|
|
||||||
import type { Reporter } from '../types/testReporter';
|
import type { Reporter } from '../types/testReporter';
|
||||||
import { FixturePool, isFixtureOption } from './fixtures';
|
import type { SerializedLoaderData } from './ipc';
|
||||||
import { setCurrentlyLoadingFileSuite } from './globals';
|
|
||||||
import type { SerializedLoaderData, WorkerIsolation } from './ipc';
|
|
||||||
import type { BuiltInReporter, ConfigCLIOverrides } from './runner';
|
import type { BuiltInReporter, ConfigCLIOverrides } from './runner';
|
||||||
import { builtInReporters } from './runner';
|
import { builtInReporters } from './runner';
|
||||||
import { Suite, type TestCase } from './test';
|
import { requireOrImport } from './transform';
|
||||||
import type { TestTypeImpl } from './testType';
|
import type { Config, FullConfigInternal, FullProjectInternal, Project, ReporterDescription } from './types';
|
||||||
import { installTransform } from './transform';
|
import { errorWithFile, getPackageJsonPath, mergeObjects } from './util';
|
||||||
import type { Config, Fixtures, FixturesWithLocation, FullConfigInternal, FullProjectInternal, Project, ReporterDescription } from './types';
|
|
||||||
import { errorWithFile, getPackageJsonPath, mergeObjects, serializeError } from './util';
|
|
||||||
|
|
||||||
export const defaultTimeout = 30000;
|
export const defaultTimeout = 30000;
|
||||||
|
|
||||||
// To allow multiple loaders in the same process without clearing require cache,
|
export class ConfigLoader {
|
||||||
// we make these maps global.
|
|
||||||
const cachedFileSuites = new Map<string, Suite>();
|
|
||||||
|
|
||||||
export class Loader {
|
|
||||||
private _configCLIOverrides: ConfigCLIOverrides;
|
private _configCLIOverrides: ConfigCLIOverrides;
|
||||||
private _fullConfig: FullConfigInternal;
|
private _fullConfig: FullConfigInternal;
|
||||||
private _configDir: string = '';
|
private _configDir: string = '';
|
||||||
private _configFile: string | undefined;
|
private _configFile: string | undefined;
|
||||||
private _projectSuiteBuilders = new Map<FullProjectInternal, ProjectSuiteBuilder>();
|
|
||||||
|
|
||||||
constructor(configCLIOverrides?: ConfigCLIOverrides) {
|
constructor(configCLIOverrides?: ConfigCLIOverrides) {
|
||||||
this._configCLIOverrides = configCLIOverrides || {};
|
this._configCLIOverrides = configCLIOverrides || {};
|
||||||
this._fullConfig = { ...baseFullConfig };
|
this._fullConfig = { ...baseFullConfig };
|
||||||
}
|
}
|
||||||
|
|
||||||
static async deserialize(data: SerializedLoaderData): Promise<Loader> {
|
static async deserialize(data: SerializedLoaderData): Promise<ConfigLoader> {
|
||||||
const loader = new Loader(data.configCLIOverrides);
|
const loader = new ConfigLoader(data.configCLIOverrides);
|
||||||
if (data.configFile)
|
if (data.configFile)
|
||||||
await loader.loadConfigFile(data.configFile);
|
await loader.loadConfigFile(data.configFile);
|
||||||
else
|
else
|
||||||
@ -182,49 +172,6 @@ export class Loader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadTestFile(file: string, environment: 'runner' | 'worker', phase: 'test' | 'projectSetup' | 'globalSetup') {
|
|
||||||
if (cachedFileSuites.has(file))
|
|
||||||
return cachedFileSuites.get(file)!;
|
|
||||||
const suite = new Suite(path.relative(this._fullConfig.rootDir, file) || path.basename(file), 'file');
|
|
||||||
suite._requireFile = file;
|
|
||||||
suite._phase = phase;
|
|
||||||
suite.location = { file, line: 0, column: 0 };
|
|
||||||
|
|
||||||
setCurrentlyLoadingFileSuite(suite);
|
|
||||||
try {
|
|
||||||
await this._requireOrImport(file);
|
|
||||||
cachedFileSuites.set(file, suite);
|
|
||||||
} catch (e) {
|
|
||||||
if (environment === 'worker')
|
|
||||||
throw e;
|
|
||||||
suite._loadError = serializeError(e);
|
|
||||||
} finally {
|
|
||||||
setCurrentlyLoadingFileSuite(undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// Test locations that we discover potentially have different file name.
|
|
||||||
// This could be due to either
|
|
||||||
// a) use of source maps or due to
|
|
||||||
// b) require of one file from another.
|
|
||||||
// Try fixing (a) w/o regressing (b).
|
|
||||||
|
|
||||||
const files = new Set<string>();
|
|
||||||
suite.allTests().map(t => files.add(t.location.file));
|
|
||||||
if (files.size === 1) {
|
|
||||||
// All tests point to one file.
|
|
||||||
const mappedFile = files.values().next().value;
|
|
||||||
if (suite.location.file !== mappedFile) {
|
|
||||||
// The file is different, check for a likely source map case.
|
|
||||||
if (path.extname(mappedFile) !== path.extname(suite.location.file))
|
|
||||||
suite.location.file = mappedFile;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return suite;
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadGlobalHook(file: string): Promise<(config: FullConfigInternal) => any> {
|
async loadGlobalHook(file: string): Promise<(config: FullConfigInternal) => any> {
|
||||||
return this._requireOrImportDefaultFunction(path.resolve(this._fullConfig.rootDir, file), false);
|
return this._requireOrImportDefaultFunction(path.resolve(this._fullConfig.rootDir, file), false);
|
||||||
}
|
}
|
||||||
@ -237,13 +184,6 @@ export class Loader {
|
|||||||
return this._fullConfig;
|
return this._fullConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
buildFileSuiteForProject(project: FullProjectInternal, suite: Suite, repeatEachIndex: number, filter: (test: TestCase) => boolean): Suite | undefined {
|
|
||||||
if (!this._projectSuiteBuilders.has(project))
|
|
||||||
this._projectSuiteBuilders.set(project, new ProjectSuiteBuilder(project));
|
|
||||||
const builder = this._projectSuiteBuilders.get(project)!;
|
|
||||||
return builder.cloneFileSuite(suite, 'isolate-pools', repeatEachIndex, filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
serialize(): SerializedLoaderData {
|
serialize(): SerializedLoaderData {
|
||||||
const result: SerializedLoaderData = {
|
const result: SerializedLoaderData = {
|
||||||
configFile: this._configFile,
|
configFile: this._configFile,
|
||||||
@ -305,21 +245,8 @@ export class Loader {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _requireOrImport(file: string) {
|
|
||||||
const revertBabelRequire = installTransform();
|
|
||||||
const isModule = fileIsModule(file);
|
|
||||||
try {
|
|
||||||
const esmImport = () => eval(`import(${JSON.stringify(url.pathToFileURL(file))})`);
|
|
||||||
if (isModule)
|
|
||||||
return await esmImport();
|
|
||||||
return require(file);
|
|
||||||
} finally {
|
|
||||||
revertBabelRequire();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _requireOrImportDefaultFunction(file: string, expectConstructor: boolean) {
|
private async _requireOrImportDefaultFunction(file: string, expectConstructor: boolean) {
|
||||||
let func = await this._requireOrImport(file);
|
let func = await requireOrImport(file);
|
||||||
if (func && typeof func === 'object' && ('default' in func))
|
if (func && typeof func === 'object' && ('default' in func))
|
||||||
func = func['default'];
|
func = func['default'];
|
||||||
if (typeof func !== 'function')
|
if (typeof func !== 'function')
|
||||||
@ -328,131 +255,13 @@ export class Loader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _requireOrImportDefaultObject(file: string) {
|
private async _requireOrImportDefaultObject(file: string) {
|
||||||
let object = await this._requireOrImport(file);
|
let object = await requireOrImport(file);
|
||||||
if (object && typeof object === 'object' && ('default' in object))
|
if (object && typeof object === 'object' && ('default' in object))
|
||||||
object = object['default'];
|
object = object['default'];
|
||||||
return object;
|
return object;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ProjectSuiteBuilder {
|
|
||||||
private _project: FullProjectInternal;
|
|
||||||
private _testTypePools = new Map<TestTypeImpl, FixturePool>();
|
|
||||||
private _testPools = new Map<TestCase, FixturePool>();
|
|
||||||
|
|
||||||
constructor(project: FullProjectInternal) {
|
|
||||||
this._project = project;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _buildTestTypePool(testType: TestTypeImpl): FixturePool {
|
|
||||||
if (!this._testTypePools.has(testType)) {
|
|
||||||
const fixtures = this._applyConfigUseOptions(testType, this._project.use || {});
|
|
||||||
const pool = new FixturePool(fixtures);
|
|
||||||
this._testTypePools.set(testType, pool);
|
|
||||||
}
|
|
||||||
return this._testTypePools.get(testType)!;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: we can optimize this function by building the pool inline in cloneSuite
|
|
||||||
private _buildPool(test: TestCase): FixturePool {
|
|
||||||
if (!this._testPools.has(test)) {
|
|
||||||
let pool = this._buildTestTypePool(test._testType);
|
|
||||||
|
|
||||||
const parents: Suite[] = [];
|
|
||||||
for (let parent: Suite | undefined = test.parent; parent; parent = parent.parent)
|
|
||||||
parents.push(parent);
|
|
||||||
parents.reverse();
|
|
||||||
|
|
||||||
for (const parent of parents) {
|
|
||||||
if (parent._use.length)
|
|
||||||
pool = new FixturePool(parent._use, pool, parent._type === 'describe');
|
|
||||||
for (const hook of parent._hooks)
|
|
||||||
pool.validateFunction(hook.fn, hook.type + ' hook', hook.location);
|
|
||||||
for (const modifier of parent._modifiers)
|
|
||||||
pool.validateFunction(modifier.fn, modifier.type + ' modifier', modifier.location);
|
|
||||||
}
|
|
||||||
|
|
||||||
pool.validateFunction(test.fn, 'Test', test.location);
|
|
||||||
this._testPools.set(test, pool);
|
|
||||||
}
|
|
||||||
return this._testPools.get(test)!;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _cloneEntries(from: Suite, to: Suite, workerIsolation: WorkerIsolation, repeatEachIndex: number, filter: (test: TestCase) => boolean): boolean {
|
|
||||||
for (const entry of from._entries) {
|
|
||||||
if (entry instanceof Suite) {
|
|
||||||
const suite = entry._clone();
|
|
||||||
suite._fileId = to._fileId;
|
|
||||||
to._addSuite(suite);
|
|
||||||
// Ignore empty titles, similar to Suite.titlePath().
|
|
||||||
if (!this._cloneEntries(entry, suite, workerIsolation, repeatEachIndex, filter)) {
|
|
||||||
to._entries.pop();
|
|
||||||
to.suites.pop();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const test = entry._clone();
|
|
||||||
to._addTest(test);
|
|
||||||
test.retries = this._project.retries;
|
|
||||||
for (let parentSuite: Suite | undefined = to; parentSuite; parentSuite = parentSuite.parent) {
|
|
||||||
if (parentSuite._retries !== undefined) {
|
|
||||||
test.retries = parentSuite._retries;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const repeatEachIndexSuffix = repeatEachIndex ? ` (repeat:${repeatEachIndex})` : '';
|
|
||||||
// At the point of the query, suite is not yet attached to the project, so we only get file, describe and test titles.
|
|
||||||
const testIdExpression = `[project=${this._project._id}]${test.titlePath().join('\x1e')}${repeatEachIndexSuffix}`;
|
|
||||||
const testId = to._fileId + '-' + calculateSha1(testIdExpression).slice(0, 20);
|
|
||||||
test.id = testId;
|
|
||||||
test.repeatEachIndex = repeatEachIndex;
|
|
||||||
test._projectId = this._project._id;
|
|
||||||
if (!filter(test)) {
|
|
||||||
to._entries.pop();
|
|
||||||
to.tests.pop();
|
|
||||||
} else {
|
|
||||||
const pool = this._buildPool(entry);
|
|
||||||
if (this._project._fullConfig._workerIsolation === 'isolate-pools')
|
|
||||||
test._workerHash = `run${this._project._id}-${pool.digest}-repeat${repeatEachIndex}`;
|
|
||||||
else
|
|
||||||
test._workerHash = `run${this._project._id}-repeat${repeatEachIndex}`;
|
|
||||||
test._pool = pool;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!to._entries.length)
|
|
||||||
return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
cloneFileSuite(suite: Suite, workerIsolation: WorkerIsolation, repeatEachIndex: number, filter: (test: TestCase) => boolean): Suite | undefined {
|
|
||||||
const result = suite._clone();
|
|
||||||
const relativeFile = path.relative(this._project.testDir, suite.location!.file).split(path.sep).join('/');
|
|
||||||
result._fileId = calculateSha1(relativeFile).slice(0, 20);
|
|
||||||
return this._cloneEntries(suite, result, workerIsolation, repeatEachIndex, filter) ? result : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _applyConfigUseOptions(testType: TestTypeImpl, configUse: Fixtures): FixturesWithLocation[] {
|
|
||||||
const configKeys = new Set(Object.keys(configUse));
|
|
||||||
if (!configKeys.size)
|
|
||||||
return testType.fixtures;
|
|
||||||
const result: FixturesWithLocation[] = [];
|
|
||||||
for (const f of testType.fixtures) {
|
|
||||||
result.push(f);
|
|
||||||
const optionsFromConfig: Fixtures = {};
|
|
||||||
for (const [key, value] of Object.entries(f.fixtures)) {
|
|
||||||
if (isFixtureOption(value) && configKeys.has(key))
|
|
||||||
(optionsFromConfig as any)[key] = [(configUse as any)[key], value[1]];
|
|
||||||
}
|
|
||||||
if (Object.entries(optionsFromConfig).length) {
|
|
||||||
// Add config options immediately after original option definition,
|
|
||||||
// so that any test.use() override it.
|
|
||||||
result.push({ fixtures: optionsFromConfig, location: { file: `project#${this._project._id}`, line: 1, column: 1 }, fromConfig: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function takeFirst<T>(...args: (T | undefined)[]): T {
|
function takeFirst<T>(...args: (T | undefined)[]): T {
|
||||||
for (const arg of args) {
|
for (const arg of args) {
|
||||||
if (arg !== undefined)
|
if (arg !== undefined)
|
||||||
@ -693,19 +502,3 @@ function resolveScript(id: string, rootDir: string) {
|
|||||||
return localPath;
|
return localPath;
|
||||||
return require.resolve(id, { paths: [rootDir] });
|
return require.resolve(id, { paths: [rootDir] });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fileIsModule(file: string): boolean {
|
|
||||||
if (file.endsWith('.mjs'))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
const folder = path.dirname(file);
|
|
||||||
return folderIsModule(folder);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function folderIsModule(folder: string): boolean {
|
|
||||||
const packageJsonPath = getPackageJsonPath(folder);
|
|
||||||
if (!packageJsonPath)
|
|
||||||
return false;
|
|
||||||
// Rely on `require` internal caching logic.
|
|
||||||
return require(packageJsonPath).type === 'module';
|
|
||||||
}
|
|
||||||
@ -17,7 +17,7 @@
|
|||||||
import type { TestBeginPayload, TestEndPayload, DonePayload, TestOutputPayload, StepBeginPayload, StepEndPayload, TeardownErrorsPayload, WatchTestResolvedPayload, RunPayload, SerializedLoaderData } from './ipc';
|
import type { TestBeginPayload, TestEndPayload, DonePayload, TestOutputPayload, StepBeginPayload, StepEndPayload, TeardownErrorsPayload, WatchTestResolvedPayload, RunPayload, SerializedLoaderData } from './ipc';
|
||||||
import type { TestResult, Reporter, TestStep, TestError } from '../types/testReporter';
|
import type { TestResult, Reporter, TestStep, TestError } from '../types/testReporter';
|
||||||
import type { Suite } from './test';
|
import type { Suite } from './test';
|
||||||
import type { Loader } from './loader';
|
import type { ConfigLoader } from './configLoader';
|
||||||
import type { ProcessExitData } from './processHost';
|
import type { ProcessExitData } from './processHost';
|
||||||
import { TestCase } from './test';
|
import { TestCase } from './test';
|
||||||
import { ManualPromise } from 'playwright-core/lib/utils';
|
import { ManualPromise } from 'playwright-core/lib/utils';
|
||||||
@ -52,13 +52,13 @@ export class Dispatcher {
|
|||||||
private _isStopped = false;
|
private _isStopped = false;
|
||||||
|
|
||||||
private _testById = new Map<string, TestData>();
|
private _testById = new Map<string, TestData>();
|
||||||
private _loader: Loader;
|
private _configLoader: ConfigLoader;
|
||||||
private _reporter: Reporter;
|
private _reporter: Reporter;
|
||||||
private _hasWorkerErrors = false;
|
private _hasWorkerErrors = false;
|
||||||
private _failureCount = 0;
|
private _failureCount = 0;
|
||||||
|
|
||||||
constructor(loader: Loader, testGroups: TestGroup[], reporter: Reporter) {
|
constructor(configLoader: ConfigLoader, testGroups: TestGroup[], reporter: Reporter) {
|
||||||
this._loader = loader;
|
this._configLoader = configLoader;
|
||||||
this._reporter = reporter;
|
this._reporter = reporter;
|
||||||
this._queue = testGroups;
|
this._queue = testGroups;
|
||||||
for (const group of testGroups) {
|
for (const group of testGroups) {
|
||||||
@ -108,7 +108,7 @@ export class Dispatcher {
|
|||||||
|
|
||||||
// 2. Start the worker if it is down.
|
// 2. Start the worker if it is down.
|
||||||
if (!worker) {
|
if (!worker) {
|
||||||
worker = this._createWorker(job, index, this._loader.serialize());
|
worker = this._createWorker(job, index, this._configLoader.serialize());
|
||||||
this._workerSlots[index].worker = worker;
|
this._workerSlots[index].worker = worker;
|
||||||
worker.on('exit', () => this._workerSlots[index].worker = undefined);
|
worker.on('exit', () => this._workerSlots[index].worker = undefined);
|
||||||
await worker.start();
|
await worker.start();
|
||||||
@ -152,7 +152,7 @@ export class Dispatcher {
|
|||||||
async run() {
|
async run() {
|
||||||
this._workerSlots = [];
|
this._workerSlots = [];
|
||||||
// 1. Allocate workers.
|
// 1. Allocate workers.
|
||||||
for (let i = 0; i < this._loader.fullConfig().workers; i++)
|
for (let i = 0; i < this._configLoader.fullConfig().workers; i++)
|
||||||
this._workerSlots.push({ busy: false });
|
this._workerSlots.push({ busy: false });
|
||||||
// 2. Schedule enough jobs.
|
// 2. Schedule enough jobs.
|
||||||
for (let i = 0; i < this._workerSlots.length; i++)
|
for (let i = 0; i < this._workerSlots.length; i++)
|
||||||
@ -439,7 +439,7 @@ export class Dispatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_createWorker(testGroup: TestGroup, parallelIndex: number, loaderData: SerializedLoaderData) {
|
_createWorker(testGroup: TestGroup, parallelIndex: number, loaderData: SerializedLoaderData) {
|
||||||
const worker = new WorkerHost(testGroup, parallelIndex, this._loader.fullConfig()._workerIsolation, loaderData);
|
const worker = new WorkerHost(testGroup, parallelIndex, this._configLoader.fullConfig()._workerIsolation, loaderData);
|
||||||
const handleOutput = (params: TestOutputPayload) => {
|
const handleOutput = (params: TestOutputPayload) => {
|
||||||
const chunk = chunkFromParams(params);
|
const chunk = chunkFromParams(params);
|
||||||
if (worker.didFail()) {
|
if (worker.didFail()) {
|
||||||
@ -480,7 +480,7 @@ export class Dispatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _hasReachedMaxFailures() {
|
private _hasReachedMaxFailures() {
|
||||||
const maxFailures = this._loader.fullConfig().maxFailures;
|
const maxFailures = this._configLoader.fullConfig().maxFailures;
|
||||||
return maxFailures > 0 && this._failureCount >= maxFailures;
|
return maxFailures > 0 && this._failureCount >= maxFailures;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -488,7 +488,7 @@ export class Dispatcher {
|
|||||||
if (result.status !== 'skipped' && result.status !== test.expectedStatus)
|
if (result.status !== 'skipped' && result.status !== test.expectedStatus)
|
||||||
++this._failureCount;
|
++this._failureCount;
|
||||||
this._reporter.onTestEnd?.(test, result);
|
this._reporter.onTestEnd?.(test, result);
|
||||||
const maxFailures = this._loader.fullConfig().maxFailures;
|
const maxFailures = this._configLoader.fullConfig().maxFailures;
|
||||||
if (maxFailures && this._failureCount === maxFailures)
|
if (maxFailures && this._failureCount === maxFailures)
|
||||||
this.stop().catch(e => {});
|
this.stop().catch(e => {});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import { promisify } from 'util';
|
|||||||
import type { FullResult, Reporter, TestError } from '../types/testReporter';
|
import type { FullResult, Reporter, TestError } from '../types/testReporter';
|
||||||
import type { TestGroup } from './dispatcher';
|
import type { TestGroup } from './dispatcher';
|
||||||
import { Dispatcher } from './dispatcher';
|
import { Dispatcher } from './dispatcher';
|
||||||
import { Loader } from './loader';
|
import { ConfigLoader } from './configLoader';
|
||||||
import type { TestRunnerPlugin } from './plugins';
|
import type { TestRunnerPlugin } from './plugins';
|
||||||
import { setRunnerToAddPluginsTo } from './plugins';
|
import { setRunnerToAddPluginsTo } from './plugins';
|
||||||
import { dockerPlugin } from './plugins/dockerPlugin';
|
import { dockerPlugin } from './plugins/dockerPlugin';
|
||||||
@ -45,6 +45,7 @@ import type { Config, FullConfigInternal, FullProjectInternal, ReporterInternal
|
|||||||
import { createFileMatcher, createFileMatcherFromFilters, createTitleMatcher, serializeError } from './util';
|
import { createFileMatcher, createFileMatcherFromFilters, createTitleMatcher, serializeError } from './util';
|
||||||
import type { Matcher, TestFileFilter } from './util';
|
import type { Matcher, TestFileFilter } from './util';
|
||||||
import { setFatalErrorSink } from './globals';
|
import { setFatalErrorSink } from './globals';
|
||||||
|
import { TestLoader } from './testLoader';
|
||||||
|
|
||||||
const removeFolderAsync = promisify(rimraf);
|
const removeFolderAsync = promisify(rimraf);
|
||||||
const readDirAsync = promisify(fs.readdir);
|
const readDirAsync = promisify(fs.readdir);
|
||||||
@ -79,13 +80,13 @@ export type ConfigCLIOverrides = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export class Runner {
|
export class Runner {
|
||||||
private _loader: Loader;
|
private _configLoader: ConfigLoader;
|
||||||
private _reporter!: ReporterInternal;
|
private _reporter!: ReporterInternal;
|
||||||
private _plugins: TestRunnerPlugin[] = [];
|
private _plugins: TestRunnerPlugin[] = [];
|
||||||
private _fatalErrors: TestError[] = [];
|
private _fatalErrors: TestError[] = [];
|
||||||
|
|
||||||
constructor(configCLIOverrides?: ConfigCLIOverrides) {
|
constructor(configCLIOverrides?: ConfigCLIOverrides) {
|
||||||
this._loader = new Loader(configCLIOverrides);
|
this._configLoader = new ConfigLoader(configCLIOverrides);
|
||||||
setRunnerToAddPluginsTo(this);
|
setRunnerToAddPluginsTo(this);
|
||||||
setFatalErrorSink(this._fatalErrors);
|
setFatalErrorSink(this._fatalErrors);
|
||||||
}
|
}
|
||||||
@ -95,11 +96,11 @@ export class Runner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async loadConfigFromResolvedFile(resolvedConfigFile: string): Promise<FullConfigInternal> {
|
async loadConfigFromResolvedFile(resolvedConfigFile: string): Promise<FullConfigInternal> {
|
||||||
return await this._loader.loadConfigFile(resolvedConfigFile);
|
return await this._configLoader.loadConfigFile(resolvedConfigFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadEmptyConfig(configFileOrDirectory: string): Promise<Config> {
|
loadEmptyConfig(configFileOrDirectory: string): Promise<Config> {
|
||||||
return this._loader.loadEmptyConfig(configFileOrDirectory);
|
return this._configLoader.loadEmptyConfig(configFileOrDirectory);
|
||||||
}
|
}
|
||||||
|
|
||||||
static resolveConfigFile(configFileOrDirectory: string): string | null {
|
static resolveConfigFile(configFileOrDirectory: string): string | null {
|
||||||
@ -144,17 +145,17 @@ export class Runner {
|
|||||||
html: HtmlReporter,
|
html: HtmlReporter,
|
||||||
};
|
};
|
||||||
const reporters: Reporter[] = [];
|
const reporters: Reporter[] = [];
|
||||||
for (const r of this._loader.fullConfig().reporter) {
|
for (const r of this._configLoader.fullConfig().reporter) {
|
||||||
const [name, arg] = r;
|
const [name, arg] = r;
|
||||||
if (name in defaultReporters) {
|
if (name in defaultReporters) {
|
||||||
reporters.push(new defaultReporters[name as keyof typeof defaultReporters](arg));
|
reporters.push(new defaultReporters[name as keyof typeof defaultReporters](arg));
|
||||||
} else {
|
} else {
|
||||||
const reporterConstructor = await this._loader.loadReporter(name);
|
const reporterConstructor = await this._configLoader.loadReporter(name);
|
||||||
reporters.push(new reporterConstructor(arg));
|
reporters.push(new reporterConstructor(arg));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (process.env.PW_TEST_REPORTER) {
|
if (process.env.PW_TEST_REPORTER) {
|
||||||
const reporterConstructor = await this._loader.loadReporter(process.env.PW_TEST_REPORTER);
|
const reporterConstructor = await this._configLoader.loadReporter(process.env.PW_TEST_REPORTER);
|
||||||
reporters.push(new reporterConstructor());
|
reporters.push(new reporterConstructor());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,7 +176,7 @@ export class Runner {
|
|||||||
|
|
||||||
async runAllTests(options: RunOptions): Promise<FullResult> {
|
async runAllTests(options: RunOptions): Promise<FullResult> {
|
||||||
this._reporter = await this._createReporter(!!options.listOnly);
|
this._reporter = await this._createReporter(!!options.listOnly);
|
||||||
const config = this._loader.fullConfig();
|
const config = this._configLoader.fullConfig();
|
||||||
const result = await raceAgainstTimeout(() => this._run(options), config.globalTimeout);
|
const result = await raceAgainstTimeout(() => this._run(options), config.globalTimeout);
|
||||||
let fullResult: FullResult;
|
let fullResult: FullResult;
|
||||||
if (result.timedOut) {
|
if (result.timedOut) {
|
||||||
@ -213,7 +214,7 @@ export class Runner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _collectProjects(projectNames?: string[]): FullProjectInternal[] {
|
private _collectProjects(projectNames?: string[]): FullProjectInternal[] {
|
||||||
const fullConfig = this._loader.fullConfig();
|
const fullConfig = this._configLoader.fullConfig();
|
||||||
if (!projectNames)
|
if (!projectNames)
|
||||||
return [...fullConfig.projects];
|
return [...fullConfig.projects];
|
||||||
const projectsToFind = new Set<string>();
|
const projectsToFind = new Set<string>();
|
||||||
@ -246,7 +247,7 @@ export class Runner {
|
|||||||
const fileToProjectName = new Map<string, string>();
|
const fileToProjectName = new Map<string, string>();
|
||||||
const commandLineFileMatcher = commandLineFileFilters.length ? createFileMatcherFromFilters(commandLineFileFilters) : () => true;
|
const commandLineFileMatcher = commandLineFileFilters.length ? createFileMatcherFromFilters(commandLineFileFilters) : () => true;
|
||||||
|
|
||||||
const config = this._loader.fullConfig();
|
const config = this._configLoader.fullConfig();
|
||||||
const globalSetupFiles = new Set<string>();
|
const globalSetupFiles = new Set<string>();
|
||||||
if (config._globalScripts) {
|
if (config._globalScripts) {
|
||||||
const allFiles = await collectFiles(config.rootDir, true);
|
const allFiles = await collectFiles(config.rootDir, true);
|
||||||
@ -295,7 +296,7 @@ export class Runner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _collectTestGroups(options: RunOptions): Promise<{ rootSuite: Suite, globalSetupGroups: TestGroup[], projectSetupGroups: TestGroup[], testGroups: TestGroup[] }> {
|
private async _collectTestGroups(options: RunOptions): Promise<{ rootSuite: Suite, globalSetupGroups: TestGroup[], projectSetupGroups: TestGroup[], testGroups: TestGroup[] }> {
|
||||||
const config = this._loader.fullConfig();
|
const config = this._configLoader.fullConfig();
|
||||||
const projects = this._collectProjects(options.projectFilter);
|
const projects = this._collectProjects(options.projectFilter);
|
||||||
const { filesByProject, setupFiles, globalSetupFiles } = await this._collectFiles(projects, options.testFileFilters);
|
const { filesByProject, setupFiles, globalSetupFiles } = await this._collectFiles(projects, options.testFileFilters);
|
||||||
|
|
||||||
@ -330,9 +331,10 @@ export class Runner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _createFilteredRootSuite(options: RunOptions, filesByProject: Map<FullProjectInternal, string[]>, doNotFilterFiles: Set<string>, shouldCloneTests: boolean, setupFiles: Set<string>, globalSetupFiles: Set<string>): Promise<{rootSuite: Suite, fatalErrors: TestError[]}> {
|
private async _createFilteredRootSuite(options: RunOptions, filesByProject: Map<FullProjectInternal, string[]>, doNotFilterFiles: Set<string>, shouldCloneTests: boolean, setupFiles: Set<string>, globalSetupFiles: Set<string>): Promise<{rootSuite: Suite, fatalErrors: TestError[]}> {
|
||||||
const config = this._loader.fullConfig();
|
const config = this._configLoader.fullConfig();
|
||||||
const fatalErrors: TestError[] = [];
|
const fatalErrors: TestError[] = [];
|
||||||
const allTestFiles = new Set<string>();
|
const allTestFiles = new Set<string>();
|
||||||
|
const testLoader = new TestLoader(config);
|
||||||
for (const files of filesByProject.values())
|
for (const files of filesByProject.values())
|
||||||
files.forEach(file => allTestFiles.add(file));
|
files.forEach(file => allTestFiles.add(file));
|
||||||
|
|
||||||
@ -344,7 +346,7 @@ export class Runner {
|
|||||||
type = 'globalSetup';
|
type = 'globalSetup';
|
||||||
else if (setupFiles.has(file))
|
else if (setupFiles.has(file))
|
||||||
type = 'projectSetup';
|
type = 'projectSetup';
|
||||||
const fileSuite = await this._loader.loadTestFile(file, 'runner', type);
|
const fileSuite = await testLoader.loadTestFile(file, 'runner', type);
|
||||||
if (fileSuite._loadError)
|
if (fileSuite._loadError)
|
||||||
fatalErrors.push(fileSuite._loadError);
|
fatalErrors.push(fileSuite._loadError);
|
||||||
// We have to clone only if there maybe subsequent calls of this method.
|
// We have to clone only if there maybe subsequent calls of this method.
|
||||||
@ -397,7 +399,7 @@ export class Runner {
|
|||||||
if (!fileSuite)
|
if (!fileSuite)
|
||||||
continue;
|
continue;
|
||||||
for (let repeatEachIndex = 0; repeatEachIndex < project.repeatEach; repeatEachIndex++) {
|
for (let repeatEachIndex = 0; repeatEachIndex < project.repeatEach; repeatEachIndex++) {
|
||||||
const builtSuite = this._loader.buildFileSuiteForProject(project, fileSuite, repeatEachIndex, titleMatcher);
|
const builtSuite = testLoader.buildFileSuiteForProject(project, fileSuite, repeatEachIndex, titleMatcher);
|
||||||
if (builtSuite)
|
if (builtSuite)
|
||||||
projectSuite._addSuite(builtSuite);
|
projectSuite._addSuite(builtSuite);
|
||||||
}
|
}
|
||||||
@ -407,7 +409,7 @@ export class Runner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _filterForCurrentShard(rootSuite: Suite, projectSetupGroups: TestGroup[], testGroups: TestGroup[]) {
|
private _filterForCurrentShard(rootSuite: Suite, projectSetupGroups: TestGroup[], testGroups: TestGroup[]) {
|
||||||
const shard = this._loader.fullConfig().shard;
|
const shard = this._configLoader.fullConfig().shard;
|
||||||
if (!shard)
|
if (!shard)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@ -471,7 +473,7 @@ export class Runner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _run(options: RunOptions): Promise<FullResult> {
|
private async _run(options: RunOptions): Promise<FullResult> {
|
||||||
const config = this._loader.fullConfig();
|
const config = this._configLoader.fullConfig();
|
||||||
// Each entry is an array of test groups that can be run concurrently. All
|
// Each entry is an array of test groups that can be run concurrently. All
|
||||||
// test groups from the previos entries must finish before entry starts.
|
// test groups from the previos entries must finish before entry starts.
|
||||||
const { rootSuite, globalSetupGroups, projectSetupGroups, testGroups } = await this._collectTestGroups(options);
|
const { rootSuite, globalSetupGroups, projectSetupGroups, testGroups } = await this._collectTestGroups(options);
|
||||||
@ -553,7 +555,7 @@ export class Runner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _dispatchToWorkers(stageGroups: TestGroup[]): Promise<'success'|'signal'|'workererror'> {
|
private async _dispatchToWorkers(stageGroups: TestGroup[]): Promise<'success'|'signal'|'workererror'> {
|
||||||
const dispatcher = new Dispatcher(this._loader, [...stageGroups], this._reporter);
|
const dispatcher = new Dispatcher(this._configLoader, [...stageGroups], this._reporter);
|
||||||
const sigintWatcher = new SigIntWatcher();
|
const sigintWatcher = new SigIntWatcher();
|
||||||
await Promise.race([dispatcher.run(), sigintWatcher.promise()]);
|
await Promise.race([dispatcher.run(), sigintWatcher.promise()]);
|
||||||
if (!sigintWatcher.hadSignal()) {
|
if (!sigintWatcher.hadSignal()) {
|
||||||
@ -587,7 +589,7 @@ export class Runner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _removeOutputDirs(options: RunOptions): Promise<boolean> {
|
private async _removeOutputDirs(options: RunOptions): Promise<boolean> {
|
||||||
const config = this._loader.fullConfig();
|
const config = this._configLoader.fullConfig();
|
||||||
const outputDirs = new Set<string>();
|
const outputDirs = new Set<string>();
|
||||||
for (const p of config.projects) {
|
for (const p of config.projects) {
|
||||||
if (!options.projectFilter || options.projectFilter.includes(p.name))
|
if (!options.projectFilter || options.projectFilter.includes(p.name))
|
||||||
@ -622,12 +624,12 @@ export class Runner {
|
|||||||
const tearDown = async () => {
|
const tearDown = async () => {
|
||||||
await this._runAndReportError(async () => {
|
await this._runAndReportError(async () => {
|
||||||
if (globalSetupResult && typeof globalSetupResult === 'function')
|
if (globalSetupResult && typeof globalSetupResult === 'function')
|
||||||
await globalSetupResult(this._loader.fullConfig());
|
await globalSetupResult(this._configLoader.fullConfig());
|
||||||
}, result);
|
}, result);
|
||||||
|
|
||||||
await this._runAndReportError(async () => {
|
await this._runAndReportError(async () => {
|
||||||
if (globalSetupResult && config.globalTeardown)
|
if (globalSetupResult && config.globalTeardown)
|
||||||
await (await this._loader.loadGlobalHook(config.globalTeardown))(this._loader.fullConfig());
|
await (await this._configLoader.loadGlobalHook(config.globalTeardown))(this._configLoader.fullConfig());
|
||||||
}, result);
|
}, result);
|
||||||
|
|
||||||
for (const plugin of pluginsThatWereSetUp.reverse()) {
|
for (const plugin of pluginsThatWereSetUp.reverse()) {
|
||||||
@ -659,9 +661,9 @@ export class Runner {
|
|||||||
// Then do global setup.
|
// Then do global setup.
|
||||||
if (!sigintWatcher.hadSignal()) {
|
if (!sigintWatcher.hadSignal()) {
|
||||||
if (config.globalSetup) {
|
if (config.globalSetup) {
|
||||||
const hook = await this._loader.loadGlobalHook(config.globalSetup);
|
const hook = await this._configLoader.loadGlobalHook(config.globalSetup);
|
||||||
await Promise.race([
|
await Promise.race([
|
||||||
Promise.resolve().then(() => hook(this._loader.fullConfig())).then((r: any) => globalSetupResult = r || '<noop>'),
|
Promise.resolve().then(() => hook(this._configLoader.fullConfig())).then((r: any) => globalSetupResult = r || '<noop>'),
|
||||||
sigintWatcher.promise(),
|
sigintWatcher.promise(),
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -19,7 +19,6 @@ import path from 'path';
|
|||||||
import { monotonicTime } from 'playwright-core/lib/utils';
|
import { monotonicTime } from 'playwright-core/lib/utils';
|
||||||
import type { TestInfoError, TestInfo, TestStatus } from '../types/test';
|
import type { TestInfoError, TestInfo, TestStatus } from '../types/test';
|
||||||
import type { StepBeginPayload, StepEndPayload, WorkerInitParams } from './ipc';
|
import type { StepBeginPayload, StepEndPayload, WorkerInitParams } from './ipc';
|
||||||
import type { Loader } from './loader';
|
|
||||||
import type { TestCase } from './test';
|
import type { TestCase } from './test';
|
||||||
import { TimeoutManager } from './timeoutManager';
|
import { TimeoutManager } from './timeoutManager';
|
||||||
import type { Annotation, FullConfigInternal, FullProjectInternal, TestStepInternal } from './types';
|
import type { Annotation, FullConfigInternal, FullProjectInternal, TestStepInternal } from './types';
|
||||||
@ -82,7 +81,7 @@ export class TestInfoImpl implements TestInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
loader: Loader,
|
config: FullConfigInternal,
|
||||||
project: FullProjectInternal,
|
project: FullProjectInternal,
|
||||||
workerParams: WorkerInitParams,
|
workerParams: WorkerInitParams,
|
||||||
test: TestCase,
|
test: TestCase,
|
||||||
@ -101,7 +100,7 @@ export class TestInfoImpl implements TestInfo {
|
|||||||
this.workerIndex = workerParams.workerIndex;
|
this.workerIndex = workerParams.workerIndex;
|
||||||
this.parallelIndex = workerParams.parallelIndex;
|
this.parallelIndex = workerParams.parallelIndex;
|
||||||
this.project = project;
|
this.project = project;
|
||||||
this.config = loader.fullConfig();
|
this.config = config;
|
||||||
this.title = test.title;
|
this.title = test.title;
|
||||||
this.titlePath = test.titlePath();
|
this.titlePath = test.titlePath();
|
||||||
this.file = test.location.file;
|
this.file = test.location.file;
|
||||||
|
|||||||
209
packages/playwright-test/src/testLoader.ts
Normal file
209
packages/playwright-test/src/testLoader.ts
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
/**
|
||||||
|
* 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 * as path from 'path';
|
||||||
|
import { calculateSha1 } from 'playwright-core/lib/utils';
|
||||||
|
import { FixturePool, isFixtureOption } from './fixtures';
|
||||||
|
import { setCurrentlyLoadingFileSuite } from './globals';
|
||||||
|
import type { WorkerIsolation } from './ipc';
|
||||||
|
import { Suite, type TestCase } from './test';
|
||||||
|
import type { TestTypeImpl } from './testType';
|
||||||
|
import { requireOrImport } from './transform';
|
||||||
|
import type { Fixtures, FixturesWithLocation, FullConfigInternal, FullProjectInternal } from './types';
|
||||||
|
import { serializeError } from './util';
|
||||||
|
|
||||||
|
export const defaultTimeout = 30000;
|
||||||
|
|
||||||
|
// To allow multiple loaders in the same process without clearing require cache,
|
||||||
|
// we make these maps global.
|
||||||
|
const cachedFileSuites = new Map<string, Suite>();
|
||||||
|
|
||||||
|
export class TestLoader {
|
||||||
|
private _projectSuiteBuilders = new Map<FullProjectInternal, ProjectSuiteBuilder>();
|
||||||
|
private _fullConfig: FullConfigInternal;
|
||||||
|
|
||||||
|
constructor(fullConfig: FullConfigInternal) {
|
||||||
|
this._fullConfig = fullConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadTestFile(file: string, environment: 'runner' | 'worker', phase: 'test' | 'projectSetup' | 'globalSetup') {
|
||||||
|
if (cachedFileSuites.has(file))
|
||||||
|
return cachedFileSuites.get(file)!;
|
||||||
|
const suite = new Suite(path.relative(this._fullConfig.rootDir, file) || path.basename(file), 'file');
|
||||||
|
suite._requireFile = file;
|
||||||
|
suite._phase = phase;
|
||||||
|
suite.location = { file, line: 0, column: 0 };
|
||||||
|
|
||||||
|
setCurrentlyLoadingFileSuite(suite);
|
||||||
|
try {
|
||||||
|
await requireOrImport(file);
|
||||||
|
cachedFileSuites.set(file, suite);
|
||||||
|
} catch (e) {
|
||||||
|
if (environment === 'worker')
|
||||||
|
throw e;
|
||||||
|
suite._loadError = serializeError(e);
|
||||||
|
} finally {
|
||||||
|
setCurrentlyLoadingFileSuite(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Test locations that we discover potentially have different file name.
|
||||||
|
// This could be due to either
|
||||||
|
// a) use of source maps or due to
|
||||||
|
// b) require of one file from another.
|
||||||
|
// Try fixing (a) w/o regressing (b).
|
||||||
|
|
||||||
|
const files = new Set<string>();
|
||||||
|
suite.allTests().map(t => files.add(t.location.file));
|
||||||
|
if (files.size === 1) {
|
||||||
|
// All tests point to one file.
|
||||||
|
const mappedFile = files.values().next().value;
|
||||||
|
if (suite.location.file !== mappedFile) {
|
||||||
|
// The file is different, check for a likely source map case.
|
||||||
|
if (path.extname(mappedFile) !== path.extname(suite.location.file))
|
||||||
|
suite.location.file = mappedFile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return suite;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildFileSuiteForProject(project: FullProjectInternal, suite: Suite, repeatEachIndex: number, filter: (test: TestCase) => boolean): Suite | undefined {
|
||||||
|
if (!this._projectSuiteBuilders.has(project))
|
||||||
|
this._projectSuiteBuilders.set(project, new ProjectSuiteBuilder(project));
|
||||||
|
const builder = this._projectSuiteBuilders.get(project)!;
|
||||||
|
return builder.cloneFileSuite(suite, 'isolate-pools', repeatEachIndex, filter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProjectSuiteBuilder {
|
||||||
|
private _project: FullProjectInternal;
|
||||||
|
private _testTypePools = new Map<TestTypeImpl, FixturePool>();
|
||||||
|
private _testPools = new Map<TestCase, FixturePool>();
|
||||||
|
|
||||||
|
constructor(project: FullProjectInternal) {
|
||||||
|
this._project = project;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _buildTestTypePool(testType: TestTypeImpl): FixturePool {
|
||||||
|
if (!this._testTypePools.has(testType)) {
|
||||||
|
const fixtures = this._applyConfigUseOptions(testType, this._project.use || {});
|
||||||
|
const pool = new FixturePool(fixtures);
|
||||||
|
this._testTypePools.set(testType, pool);
|
||||||
|
}
|
||||||
|
return this._testTypePools.get(testType)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: we can optimize this function by building the pool inline in cloneSuite
|
||||||
|
private _buildPool(test: TestCase): FixturePool {
|
||||||
|
if (!this._testPools.has(test)) {
|
||||||
|
let pool = this._buildTestTypePool(test._testType);
|
||||||
|
|
||||||
|
const parents: Suite[] = [];
|
||||||
|
for (let parent: Suite | undefined = test.parent; parent; parent = parent.parent)
|
||||||
|
parents.push(parent);
|
||||||
|
parents.reverse();
|
||||||
|
|
||||||
|
for (const parent of parents) {
|
||||||
|
if (parent._use.length)
|
||||||
|
pool = new FixturePool(parent._use, pool, parent._type === 'describe');
|
||||||
|
for (const hook of parent._hooks)
|
||||||
|
pool.validateFunction(hook.fn, hook.type + ' hook', hook.location);
|
||||||
|
for (const modifier of parent._modifiers)
|
||||||
|
pool.validateFunction(modifier.fn, modifier.type + ' modifier', modifier.location);
|
||||||
|
}
|
||||||
|
|
||||||
|
pool.validateFunction(test.fn, 'Test', test.location);
|
||||||
|
this._testPools.set(test, pool);
|
||||||
|
}
|
||||||
|
return this._testPools.get(test)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _cloneEntries(from: Suite, to: Suite, workerIsolation: WorkerIsolation, repeatEachIndex: number, filter: (test: TestCase) => boolean): boolean {
|
||||||
|
for (const entry of from._entries) {
|
||||||
|
if (entry instanceof Suite) {
|
||||||
|
const suite = entry._clone();
|
||||||
|
suite._fileId = to._fileId;
|
||||||
|
to._addSuite(suite);
|
||||||
|
// Ignore empty titles, similar to Suite.titlePath().
|
||||||
|
if (!this._cloneEntries(entry, suite, workerIsolation, repeatEachIndex, filter)) {
|
||||||
|
to._entries.pop();
|
||||||
|
to.suites.pop();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const test = entry._clone();
|
||||||
|
to._addTest(test);
|
||||||
|
test.retries = this._project.retries;
|
||||||
|
for (let parentSuite: Suite | undefined = to; parentSuite; parentSuite = parentSuite.parent) {
|
||||||
|
if (parentSuite._retries !== undefined) {
|
||||||
|
test.retries = parentSuite._retries;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const repeatEachIndexSuffix = repeatEachIndex ? ` (repeat:${repeatEachIndex})` : '';
|
||||||
|
// At the point of the query, suite is not yet attached to the project, so we only get file, describe and test titles.
|
||||||
|
const testIdExpression = `[project=${this._project._id}]${test.titlePath().join('\x1e')}${repeatEachIndexSuffix}`;
|
||||||
|
const testId = to._fileId + '-' + calculateSha1(testIdExpression).slice(0, 20);
|
||||||
|
test.id = testId;
|
||||||
|
test.repeatEachIndex = repeatEachIndex;
|
||||||
|
test._projectId = this._project._id;
|
||||||
|
if (!filter(test)) {
|
||||||
|
to._entries.pop();
|
||||||
|
to.tests.pop();
|
||||||
|
} else {
|
||||||
|
const pool = this._buildPool(entry);
|
||||||
|
if (this._project._fullConfig._workerIsolation === 'isolate-pools')
|
||||||
|
test._workerHash = `run${this._project._id}-${pool.digest}-repeat${repeatEachIndex}`;
|
||||||
|
else
|
||||||
|
test._workerHash = `run${this._project._id}-repeat${repeatEachIndex}`;
|
||||||
|
test._pool = pool;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!to._entries.length)
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
cloneFileSuite(suite: Suite, workerIsolation: WorkerIsolation, repeatEachIndex: number, filter: (test: TestCase) => boolean): Suite | undefined {
|
||||||
|
const result = suite._clone();
|
||||||
|
const relativeFile = path.relative(this._project.testDir, suite.location!.file).split(path.sep).join('/');
|
||||||
|
result._fileId = calculateSha1(relativeFile).slice(0, 20);
|
||||||
|
return this._cloneEntries(suite, result, workerIsolation, repeatEachIndex, filter) ? result : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _applyConfigUseOptions(testType: TestTypeImpl, configUse: Fixtures): FixturesWithLocation[] {
|
||||||
|
const configKeys = new Set(Object.keys(configUse));
|
||||||
|
if (!configKeys.size)
|
||||||
|
return testType.fixtures;
|
||||||
|
const result: FixturesWithLocation[] = [];
|
||||||
|
for (const f of testType.fixtures) {
|
||||||
|
result.push(f);
|
||||||
|
const optionsFromConfig: Fixtures = {};
|
||||||
|
for (const [key, value] of Object.entries(f.fixtures)) {
|
||||||
|
if (isFixtureOption(value) && configKeys.has(key))
|
||||||
|
(optionsFromConfig as any)[key] = [(configUse as any)[key], value[1]];
|
||||||
|
}
|
||||||
|
if (Object.entries(optionsFromConfig).length) {
|
||||||
|
// Add config options immediately after original option definition,
|
||||||
|
// so that any test.use() override it.
|
||||||
|
result.push({ fixtures: optionsFromConfig, location: { file: `project#${this._project._id}`, line: 1, column: 1 }, fromConfig: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -25,6 +25,7 @@ import type { TsConfigLoaderResult } from './third_party/tsconfig-loader';
|
|||||||
import { tsConfigLoader } from './third_party/tsconfig-loader';
|
import { tsConfigLoader } from './third_party/tsconfig-loader';
|
||||||
import Module from 'module';
|
import Module from 'module';
|
||||||
import type { BabelTransformFunction } from './babelBundle';
|
import type { BabelTransformFunction } from './babelBundle';
|
||||||
|
import { fileIsModule } from './util';
|
||||||
|
|
||||||
const version = 13;
|
const version = 13;
|
||||||
const cacheDir = process.env.PWTEST_CACHE_DIR || path.join(os.tmpdir(), 'playwright-transform-cache');
|
const cacheDir = process.env.PWTEST_CACHE_DIR || path.join(os.tmpdir(), 'playwright-transform-cache');
|
||||||
@ -215,6 +216,19 @@ export function transformHook(code: string, filename: string, moduleUrl?: string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function requireOrImport(file: string) {
|
||||||
|
const revertBabelRequire = installTransform();
|
||||||
|
const isModule = fileIsModule(file);
|
||||||
|
try {
|
||||||
|
const esmImport = () => eval(`import(${JSON.stringify(url.pathToFileURL(file))})`);
|
||||||
|
if (isModule)
|
||||||
|
return await esmImport();
|
||||||
|
return require(file);
|
||||||
|
} finally {
|
||||||
|
revertBabelRequire();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function installTransform(): () => void {
|
export function installTransform(): () => void {
|
||||||
let reverted = false;
|
let reverted = false;
|
||||||
|
|
||||||
|
|||||||
@ -296,4 +296,20 @@ export async function normalizeAndSaveAttachment(outputPath: string, name: strin
|
|||||||
const contentType = options.contentType ?? (typeof options.body === 'string' ? 'text/plain' : 'application/octet-stream');
|
const contentType = options.contentType ?? (typeof options.body === 'string' ? 'text/plain' : 'application/octet-stream');
|
||||||
return { name, contentType, body: typeof options.body === 'string' ? Buffer.from(options.body) : options.body };
|
return { name, contentType, body: typeof options.body === 'string' ? Buffer.from(options.body) : options.body };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fileIsModule(file: string): boolean {
|
||||||
|
if (file.endsWith('.mjs'))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
const folder = path.dirname(file);
|
||||||
|
return folderIsModule(folder);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function folderIsModule(folder: string): boolean {
|
||||||
|
const packageJsonPath = getPackageJsonPath(folder);
|
||||||
|
if (!packageJsonPath)
|
||||||
|
return false;
|
||||||
|
// Rely on `require` internal caching logic.
|
||||||
|
return require(packageJsonPath).type === 'module';
|
||||||
|
}
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import util from 'util';
|
|||||||
import { debugTest, formatLocation, relativeFilePath, serializeError } from './util';
|
import { debugTest, formatLocation, relativeFilePath, serializeError } from './util';
|
||||||
import type { TestBeginPayload, TestEndPayload, RunPayload, DonePayload, WorkerInitParams, TeardownErrorsPayload, WatchTestResolvedPayload } from './ipc';
|
import type { TestBeginPayload, TestEndPayload, RunPayload, DonePayload, WorkerInitParams, TeardownErrorsPayload, WatchTestResolvedPayload } from './ipc';
|
||||||
import { setCurrentTestInfo } from './globals';
|
import { setCurrentTestInfo } from './globals';
|
||||||
import { Loader } from './loader';
|
import { ConfigLoader } from './configLoader';
|
||||||
import type { Suite, TestCase } from './test';
|
import type { Suite, TestCase } from './test';
|
||||||
import type { Annotation, FullProjectInternal, TestInfoError } from './types';
|
import type { Annotation, FullProjectInternal, TestInfoError } from './types';
|
||||||
import { FixtureRunner } from './fixtures';
|
import { FixtureRunner } from './fixtures';
|
||||||
@ -28,12 +28,14 @@ import { TestInfoImpl } from './testInfo';
|
|||||||
import type { TimeSlot } from './timeoutManager';
|
import type { TimeSlot } from './timeoutManager';
|
||||||
import { TimeoutManager } from './timeoutManager';
|
import { TimeoutManager } from './timeoutManager';
|
||||||
import { ProcessRunner } from './process';
|
import { ProcessRunner } from './process';
|
||||||
|
import { TestLoader } from './testLoader';
|
||||||
|
|
||||||
const removeFolderAsync = util.promisify(rimraf);
|
const removeFolderAsync = util.promisify(rimraf);
|
||||||
|
|
||||||
export class WorkerRunner extends ProcessRunner {
|
export class WorkerRunner extends ProcessRunner {
|
||||||
private _params: WorkerInitParams;
|
private _params: WorkerInitParams;
|
||||||
private _loader!: Loader;
|
private _configLoader!: ConfigLoader;
|
||||||
|
private _testLoader!: TestLoader;
|
||||||
private _project!: FullProjectInternal;
|
private _project!: FullProjectInternal;
|
||||||
private _fixtureRunner: FixtureRunner;
|
private _fixtureRunner: FixtureRunner;
|
||||||
|
|
||||||
@ -164,15 +166,16 @@ export class WorkerRunner extends ProcessRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _loadIfNeeded() {
|
private async _loadIfNeeded() {
|
||||||
if (this._loader)
|
if (this._configLoader)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this._loader = await Loader.deserialize(this._params.loader);
|
this._configLoader = await ConfigLoader.deserialize(this._params.loader);
|
||||||
const globalProject = this._loader.fullConfig()._globalProject;
|
this._testLoader = new TestLoader(this._configLoader.fullConfig());
|
||||||
|
const globalProject = this._configLoader.fullConfig()._globalProject;
|
||||||
if (this._params.projectId === globalProject._id)
|
if (this._params.projectId === globalProject._id)
|
||||||
this._project = globalProject;
|
this._project = globalProject;
|
||||||
else
|
else
|
||||||
this._project = this._loader.fullConfig().projects.find(p => p._id === this._params.projectId)!;
|
this._project = this._configLoader.fullConfig().projects.find(p => p._id === this._params.projectId)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
async runTestGroup(runPayload: RunPayload) {
|
async runTestGroup(runPayload: RunPayload) {
|
||||||
@ -181,8 +184,8 @@ export class WorkerRunner extends ProcessRunner {
|
|||||||
let fatalUnknownTestIds;
|
let fatalUnknownTestIds;
|
||||||
try {
|
try {
|
||||||
await this._loadIfNeeded();
|
await this._loadIfNeeded();
|
||||||
const fileSuite = await this._loader.loadTestFile(runPayload.file, 'worker', runPayload.phase);
|
const fileSuite = await this._testLoader.loadTestFile(runPayload.file, 'worker', runPayload.phase);
|
||||||
const suite = this._loader.buildFileSuiteForProject(this._project, fileSuite, this._params.repeatEachIndex, test => {
|
const suite = this._testLoader.buildFileSuiteForProject(this._project, fileSuite, this._params.repeatEachIndex, test => {
|
||||||
if (runPayload.watchMode) {
|
if (runPayload.watchMode) {
|
||||||
const testResolvedPayload: WatchTestResolvedPayload = {
|
const testResolvedPayload: WatchTestResolvedPayload = {
|
||||||
testId: test.id,
|
testId: test.id,
|
||||||
@ -238,7 +241,7 @@ export class WorkerRunner extends ProcessRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _runTest(test: TestCase, retry: number, nextTest: TestCase | undefined) {
|
private async _runTest(test: TestCase, retry: number, nextTest: TestCase | undefined) {
|
||||||
const testInfo = new TestInfoImpl(this._loader, this._project, this._params, test, retry,
|
const testInfo = new TestInfoImpl(this._configLoader.fullConfig(), this._project, this._params, test, retry,
|
||||||
stepBeginPayload => this.dispatchEvent('stepBegin', stepBeginPayload),
|
stepBeginPayload => this.dispatchEvent('stepBegin', stepBeginPayload),
|
||||||
stepEndPayload => this.dispatchEvent('stepEnd', stepEndPayload));
|
stepEndPayload => this.dispatchEvent('stepEnd', stepEndPayload));
|
||||||
|
|
||||||
@ -484,8 +487,8 @@ export class WorkerRunner extends ProcessRunner {
|
|||||||
setCurrentTestInfo(null);
|
setCurrentTestInfo(null);
|
||||||
this.dispatchEvent('testEnd', buildTestEndPayload(testInfo));
|
this.dispatchEvent('testEnd', buildTestEndPayload(testInfo));
|
||||||
|
|
||||||
const preserveOutput = this._loader.fullConfig().preserveOutput === 'always' ||
|
const preserveOutput = this._configLoader.fullConfig().preserveOutput === 'always' ||
|
||||||
(this._loader.fullConfig().preserveOutput === 'failures-only' && testInfo._isFailure());
|
(this._configLoader.fullConfig().preserveOutput === 'failures-only' && testInfo._isFailure());
|
||||||
if (!preserveOutput)
|
if (!preserveOutput)
|
||||||
await removeFolderAsync(testInfo.outputDir).catch(e => {});
|
await removeFolderAsync(testInfo.outputDir).catch(e => {});
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user