diff --git a/packages/playwright-test/src/dispatcher.ts b/packages/playwright-test/src/dispatcher.ts index ba00822d68..0971be701b 100644 --- a/packages/playwright-test/src/dispatcher.ts +++ b/packages/playwright-test/src/dispatcher.ts @@ -17,7 +17,7 @@ import child_process from 'child_process'; import path from 'path'; 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 { Suite } from './test'; import type { Loader } from './loader'; @@ -428,7 +428,7 @@ export class Dispatcher { } _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 chunk = chunkFromParams(params); if (worker.didFail()) { @@ -496,12 +496,14 @@ class Worker extends EventEmitter { private _didFail = false; private didExit = false; private _ready: Promise; + workerIsolation: WorkerIsolation; - constructor(hash: string, parallelIndex: number) { + constructor(hash: string, parallelIndex: number, workerIsolation: WorkerIsolation) { super(); this.workerIndex = lastWorkerIndex++; this._hash = hash; this.parallelIndex = parallelIndex; + this.workerIsolation = workerIsolation; this.process = child_process.fork(path.join(__dirname, 'worker.js'), { detached: false, @@ -534,6 +536,7 @@ class Worker extends EventEmitter { async init(testGroup: TestGroup, loaderData: SerializedLoaderData) { await this._ready; const params: WorkerInitParams = { + workerIsolation: this.workerIsolation, workerIndex: this.workerIndex, parallelIndex: this.parallelIndex, repeatEachIndex: testGroup.repeatEachIndex, diff --git a/packages/playwright-test/src/ipc.ts b/packages/playwright-test/src/ipc.ts index 152611eb98..65677a2525 100644 --- a/packages/playwright-test/src/ipc.ts +++ b/packages/playwright-test/src/ipc.ts @@ -30,7 +30,13 @@ export type TtyParams = { 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 = { + workerIsolation: WorkerIsolation; workerIndex: number; parallelIndex: number; repeatEachIndex: number; diff --git a/packages/playwright-test/src/loader.ts b/packages/playwright-test/src/loader.ts index 368f9daa43..f0476361ee 100644 --- a/packages/playwright-test/src/loader.ts +++ b/packages/playwright-test/src/loader.ts @@ -19,7 +19,7 @@ import type { Config, Project, ReporterDescription, FullProjectInternal, FullCon import { getPackageJsonPath, mergeObjects, errorWithFile } from './util'; import { setCurrentlyLoadingFileSuite } from './globals'; import { Suite, type TestCase } from './test'; -import type { SerializedLoaderData } from './ipc'; +import type { SerializedLoaderData, WorkerIsolation } from './ipc'; import * as path from 'path'; import * as url from 'url'; import * as fs from 'fs'; @@ -228,7 +228,7 @@ export class Loader { if (!this._projectSuiteBuilders.has(project)) this._projectSuiteBuilders.set(project, new ProjectSuiteBuilder(project)); const builder = this._projectSuiteBuilders.get(project)!; - return builder.cloneFileSuite(suite, repeatEachIndex, filter); + return builder.cloneFileSuite(suite, 'isolate-pools', repeatEachIndex, filter); } serialize(): SerializedLoaderData { @@ -371,14 +371,14 @@ class ProjectSuiteBuilder { 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) { 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, repeatEachIndex, filter)) { + if (!this._cloneEntries(entry, suite, workerIsolation, repeatEachIndex, filter)) { to._entries.pop(); to.suites.pop(); } @@ -398,7 +398,10 @@ class ProjectSuiteBuilder { to.tests.pop(); } else { 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; } } @@ -408,11 +411,11 @@ class ProjectSuiteBuilder { 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 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, repeatEachIndex, filter) ? result : undefined; + return this._cloneEntries(suite, result, workerIsolation, repeatEachIndex, filter) ? result : undefined; } private _applyConfigUseOptions(testType: TestTypeImpl, configUse: Fixtures): FixturesWithLocation[] { @@ -647,6 +650,7 @@ export const baseFullConfig: FullConfigInternal = { _globalOutputDir: path.resolve(process.cwd()), _configDir: '', _testGroupsCount: 0, + _workerIsolation: 'isolate-pools', }; function resolveReporters(reporters: Config['reporter'], rootDir: string): ReporterDescription[]|undefined { diff --git a/packages/playwright-test/src/types.ts b/packages/playwright-test/src/types.ts index f2a6d745f7..a9addb2b80 100644 --- a/packages/playwright-test/src/types.ts +++ b/packages/playwright-test/src/types.ts @@ -16,6 +16,7 @@ import type { Fixtures, TestError, Project } from '../types/test'; import type { Location } from '../types/testReporter'; +import type { WorkerIsolation } 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 FullConfigInternal extends FullConfigPublic { _globalOutputDir: string; _configDir: string; _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. */ diff --git a/packages/playwright-test/src/workerRunner.ts b/packages/playwright-test/src/workerRunner.ts index cbb7019baf..fd3bf13486 100644 --- a/packages/playwright-test/src/workerRunner.ts +++ b/packages/playwright-test/src/workerRunner.ts @@ -278,7 +278,11 @@ export class WorkerRunner extends EventEmitter { }; 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!); }