chore: experimental project isolation mode (#16081)
This commit is contained in:
parent
d4c63b21e7
commit
de147fafba
|
|
@ -17,7 +17,7 @@
|
||||||
import child_process from 'child_process';
|
import child_process from 'child_process';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import type { RunPayload, TestBeginPayload, TestEndPayload, DonePayload, TestOutputPayload, WorkerInitParams, StepBeginPayload, StepEndPayload, SerializedLoaderData, TeardownErrorsPayload, TestServerTestResolvedPayload } from './ipc';
|
import type { RunPayload, TestBeginPayload, TestEndPayload, DonePayload, TestOutputPayload, WorkerInitParams, StepBeginPayload, StepEndPayload, SerializedLoaderData, TeardownErrorsPayload, TestServerTestResolvedPayload, WorkerIsolation } 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 { Loader } from './loader';
|
||||||
|
|
@ -428,7 +428,7 @@ export class Dispatcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
_createWorker(hash: string, parallelIndex: number) {
|
_createWorker(hash: string, parallelIndex: number) {
|
||||||
const worker = new Worker(hash, parallelIndex);
|
const worker = new Worker(hash, parallelIndex, this._loader.fullConfig()._workerIsolation);
|
||||||
const handleOutput = (params: TestOutputPayload) => {
|
const handleOutput = (params: TestOutputPayload) => {
|
||||||
const chunk = chunkFromParams(params);
|
const chunk = chunkFromParams(params);
|
||||||
if (worker.didFail()) {
|
if (worker.didFail()) {
|
||||||
|
|
@ -496,12 +496,14 @@ class Worker extends EventEmitter {
|
||||||
private _didFail = false;
|
private _didFail = false;
|
||||||
private didExit = false;
|
private didExit = false;
|
||||||
private _ready: Promise<void>;
|
private _ready: Promise<void>;
|
||||||
|
workerIsolation: WorkerIsolation;
|
||||||
|
|
||||||
constructor(hash: string, parallelIndex: number) {
|
constructor(hash: string, parallelIndex: number, workerIsolation: WorkerIsolation) {
|
||||||
super();
|
super();
|
||||||
this.workerIndex = lastWorkerIndex++;
|
this.workerIndex = lastWorkerIndex++;
|
||||||
this._hash = hash;
|
this._hash = hash;
|
||||||
this.parallelIndex = parallelIndex;
|
this.parallelIndex = parallelIndex;
|
||||||
|
this.workerIsolation = workerIsolation;
|
||||||
|
|
||||||
this.process = child_process.fork(path.join(__dirname, 'worker.js'), {
|
this.process = child_process.fork(path.join(__dirname, 'worker.js'), {
|
||||||
detached: false,
|
detached: false,
|
||||||
|
|
@ -534,6 +536,7 @@ class Worker extends EventEmitter {
|
||||||
async init(testGroup: TestGroup, loaderData: SerializedLoaderData) {
|
async init(testGroup: TestGroup, loaderData: SerializedLoaderData) {
|
||||||
await this._ready;
|
await this._ready;
|
||||||
const params: WorkerInitParams = {
|
const params: WorkerInitParams = {
|
||||||
|
workerIsolation: this.workerIsolation,
|
||||||
workerIndex: this.workerIndex,
|
workerIndex: this.workerIndex,
|
||||||
parallelIndex: this.parallelIndex,
|
parallelIndex: this.parallelIndex,
|
||||||
repeatEachIndex: testGroup.repeatEachIndex,
|
repeatEachIndex: testGroup.repeatEachIndex,
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,13 @@ export type TtyParams = {
|
||||||
colorDepth: number;
|
colorDepth: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type WorkerIsolation =
|
||||||
|
'isolate-projects' | // create new worker for new project type
|
||||||
|
'isolate-pools'; // create new worker for new worker fixture pool digest
|
||||||
|
|
||||||
|
|
||||||
export type WorkerInitParams = {
|
export type WorkerInitParams = {
|
||||||
|
workerIsolation: WorkerIsolation;
|
||||||
workerIndex: number;
|
workerIndex: number;
|
||||||
parallelIndex: number;
|
parallelIndex: number;
|
||||||
repeatEachIndex: number;
|
repeatEachIndex: number;
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import type { Config, Project, ReporterDescription, FullProjectInternal, FullCon
|
||||||
import { getPackageJsonPath, mergeObjects, errorWithFile } from './util';
|
import { getPackageJsonPath, mergeObjects, errorWithFile } from './util';
|
||||||
import { setCurrentlyLoadingFileSuite } from './globals';
|
import { setCurrentlyLoadingFileSuite } from './globals';
|
||||||
import { Suite, type TestCase } from './test';
|
import { Suite, type TestCase } from './test';
|
||||||
import type { SerializedLoaderData } from './ipc';
|
import type { SerializedLoaderData, WorkerIsolation } from './ipc';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as url from 'url';
|
import * as url from 'url';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
|
|
@ -228,7 +228,7 @@ export class Loader {
|
||||||
if (!this._projectSuiteBuilders.has(project))
|
if (!this._projectSuiteBuilders.has(project))
|
||||||
this._projectSuiteBuilders.set(project, new ProjectSuiteBuilder(project));
|
this._projectSuiteBuilders.set(project, new ProjectSuiteBuilder(project));
|
||||||
const builder = this._projectSuiteBuilders.get(project)!;
|
const builder = this._projectSuiteBuilders.get(project)!;
|
||||||
return builder.cloneFileSuite(suite, repeatEachIndex, filter);
|
return builder.cloneFileSuite(suite, 'isolate-pools', repeatEachIndex, filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
serialize(): SerializedLoaderData {
|
serialize(): SerializedLoaderData {
|
||||||
|
|
@ -371,14 +371,14 @@ class ProjectSuiteBuilder {
|
||||||
return this._testPools.get(test)!;
|
return this._testPools.get(test)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _cloneEntries(from: Suite, to: Suite, repeatEachIndex: number, filter: (test: TestCase) => boolean): boolean {
|
private _cloneEntries(from: Suite, to: Suite, workerIsolation: WorkerIsolation, repeatEachIndex: number, filter: (test: TestCase) => boolean): boolean {
|
||||||
for (const entry of from._entries) {
|
for (const entry of from._entries) {
|
||||||
if (entry instanceof Suite) {
|
if (entry instanceof Suite) {
|
||||||
const suite = entry._clone();
|
const suite = entry._clone();
|
||||||
suite._fileId = to._fileId;
|
suite._fileId = to._fileId;
|
||||||
to._addSuite(suite);
|
to._addSuite(suite);
|
||||||
// Ignore empty titles, similar to Suite.titlePath().
|
// Ignore empty titles, similar to Suite.titlePath().
|
||||||
if (!this._cloneEntries(entry, suite, repeatEachIndex, filter)) {
|
if (!this._cloneEntries(entry, suite, workerIsolation, repeatEachIndex, filter)) {
|
||||||
to._entries.pop();
|
to._entries.pop();
|
||||||
to.suites.pop();
|
to.suites.pop();
|
||||||
}
|
}
|
||||||
|
|
@ -398,7 +398,10 @@ class ProjectSuiteBuilder {
|
||||||
to.tests.pop();
|
to.tests.pop();
|
||||||
} else {
|
} else {
|
||||||
const pool = this._buildPool(entry);
|
const pool = this._buildPool(entry);
|
||||||
test._workerHash = `run${this._project._id}-${pool.digest}-repeat${repeatEachIndex}`;
|
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;
|
test._pool = pool;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -408,11 +411,11 @@ class ProjectSuiteBuilder {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
cloneFileSuite(suite: Suite, repeatEachIndex: number, filter: (test: TestCase) => boolean): Suite | undefined {
|
cloneFileSuite(suite: Suite, workerIsolation: WorkerIsolation, repeatEachIndex: number, filter: (test: TestCase) => boolean): Suite | undefined {
|
||||||
const result = suite._clone();
|
const result = suite._clone();
|
||||||
const relativeFile = path.relative(this._project.testDir, suite.location!.file).split(path.sep).join('/');
|
const relativeFile = path.relative(this._project.testDir, suite.location!.file).split(path.sep).join('/');
|
||||||
result._fileId = calculateSha1(relativeFile).slice(0, 20);
|
result._fileId = calculateSha1(relativeFile).slice(0, 20);
|
||||||
return this._cloneEntries(suite, result, repeatEachIndex, filter) ? result : undefined;
|
return this._cloneEntries(suite, result, workerIsolation, repeatEachIndex, filter) ? result : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _applyConfigUseOptions(testType: TestTypeImpl, configUse: Fixtures): FixturesWithLocation[] {
|
private _applyConfigUseOptions(testType: TestTypeImpl, configUse: Fixtures): FixturesWithLocation[] {
|
||||||
|
|
@ -647,6 +650,7 @@ export const baseFullConfig: FullConfigInternal = {
|
||||||
_globalOutputDir: path.resolve(process.cwd()),
|
_globalOutputDir: path.resolve(process.cwd()),
|
||||||
_configDir: '',
|
_configDir: '',
|
||||||
_testGroupsCount: 0,
|
_testGroupsCount: 0,
|
||||||
|
_workerIsolation: 'isolate-pools',
|
||||||
};
|
};
|
||||||
|
|
||||||
function resolveReporters(reporters: Config['reporter'], rootDir: string): ReporterDescription[]|undefined {
|
function resolveReporters(reporters: Config['reporter'], rootDir: string): ReporterDescription[]|undefined {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
import type { Fixtures, TestError, Project } from '../types/test';
|
import type { Fixtures, TestError, Project } from '../types/test';
|
||||||
import type { Location } from '../types/testReporter';
|
import type { Location } from '../types/testReporter';
|
||||||
|
import type { WorkerIsolation } from './ipc';
|
||||||
import type { FullConfig as FullConfigPublic, FullProject as FullProjectPublic } from './types';
|
import type { FullConfig as FullConfigPublic, FullProject as FullProjectPublic } from './types';
|
||||||
export * from '../types/test';
|
export * from '../types/test';
|
||||||
export type { Location } from '../types/testReporter';
|
export type { Location } from '../types/testReporter';
|
||||||
|
|
@ -44,6 +45,7 @@ export interface FullConfigInternal extends FullConfigPublic {
|
||||||
_globalOutputDir: string;
|
_globalOutputDir: string;
|
||||||
_configDir: string;
|
_configDir: string;
|
||||||
_testGroupsCount: number;
|
_testGroupsCount: number;
|
||||||
|
_workerIsolation: WorkerIsolation;
|
||||||
/**
|
/**
|
||||||
* If populated, this should also be the first/only entry in _webServers. Legacy singleton `webServer` as well as those provided via an array in the user-facing playwright.config.{ts,js} will be in `_webServers`. The legacy field (`webServer`) field additionally stores the backwards-compatible singleton `webServer` since it had been showing up in globalSetup to the user.
|
* If populated, this should also be the first/only entry in _webServers. Legacy singleton `webServer` as well as those provided via an array in the user-facing playwright.config.{ts,js} will be in `_webServers`. The legacy field (`webServer`) field additionally stores the backwards-compatible singleton `webServer` since it had been showing up in globalSetup to the user.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -278,7 +278,11 @@ export class WorkerRunner extends EventEmitter {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!this._isStopped) {
|
if (!this._isStopped) {
|
||||||
// Update the fixture pool - it may differ between tests, but only in test-scoped fixtures.
|
// Update the fixture pool - it may differ between tests.
|
||||||
|
// - In case of isolate-pools worker isolation, only test-scoped fixtures may differ.
|
||||||
|
// - In case of isolate-projects, worker fixtures can differ too, tear down worker fixture scope if they differ.
|
||||||
|
if (this._params.workerIsolation === 'isolate-projects' && this._fixtureRunner.pool && this._fixtureRunner.pool.digest !== test._pool!.digest)
|
||||||
|
await this._teardownScopes();
|
||||||
this._fixtureRunner.setPool(test._pool!);
|
this._fixtureRunner.setPool(test._pool!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue