mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(deps): inherit environment changes from dependencies (#21771)
This commit is contained in:
parent
6efb383433
commit
2a2d6c25a4
@ -123,6 +123,8 @@ export type TeardownErrorsPayload = {
|
||||
fatalErrors: TestInfoError[];
|
||||
};
|
||||
|
||||
export type EnvProducedPayload = [string, string | null][];
|
||||
|
||||
export function serializeConfig(config: FullConfigInternal): SerializedConfig {
|
||||
const result: SerializedConfig = {
|
||||
configFile: config.configFile,
|
||||
|
@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import type { WriteStream } from 'tty';
|
||||
import type { ProcessInitParams, TtyParams } from './ipc';
|
||||
import type { EnvProducedPayload, ProcessInitParams, TtyParams } from './ipc';
|
||||
import { startProfiling, stopProfiling } from 'playwright-core/lib/utils';
|
||||
import type { TestInfoError } from './types';
|
||||
import { serializeError } from '../util';
|
||||
@ -53,6 +53,8 @@ process.on('SIGTERM', () => {});
|
||||
|
||||
let processRunner: ProcessRunner;
|
||||
let processName: string;
|
||||
const startingEnv = { ...process.env };
|
||||
|
||||
process.on('message', async message => {
|
||||
if (message.method === '__init__') {
|
||||
const { processParams, runnerParams, runnerScript } = message.params as { processParams: ProcessInitParams, runnerParams: any, runnerScript: string };
|
||||
@ -65,6 +67,9 @@ process.on('message', async message => {
|
||||
return;
|
||||
}
|
||||
if (message.method === '__stop__') {
|
||||
const keys = new Set([...Object.keys(process.env), ...Object.keys(startingEnv)]);
|
||||
const producedEnv: EnvProducedPayload = [...keys].filter(key => startingEnv[key] !== process.env[key]).map(key => [key, process.env[key] ?? null]);
|
||||
sendMessageToParent({ method: '__env_produced__', params: producedEnv });
|
||||
await gracefullyCloseAndExit();
|
||||
return;
|
||||
}
|
||||
|
@ -35,6 +35,8 @@ type TestData = {
|
||||
resultByWorkerIndex: Map<number, TestResultData>;
|
||||
};
|
||||
|
||||
export type EnvByProjectId = Map<string, Record<string, string | undefined>>;
|
||||
|
||||
export class Dispatcher {
|
||||
private _workerSlots: { busy: boolean, worker?: WorkerHost }[] = [];
|
||||
private _queue: TestGroup[] = [];
|
||||
@ -48,6 +50,9 @@ export class Dispatcher {
|
||||
private _hasWorkerErrors = false;
|
||||
private _failureCount = 0;
|
||||
|
||||
private _extraEnvByProjectId: EnvByProjectId = new Map();
|
||||
private _producedEnvByProjectId: EnvByProjectId = new Map();
|
||||
|
||||
constructor(config: FullConfigInternal, reporter: Reporter) {
|
||||
this._config = config;
|
||||
this._reporter = reporter;
|
||||
@ -164,7 +169,8 @@ export class Dispatcher {
|
||||
return workersWithSameHash > this._queuedOrRunningHashCount.get(worker.hash())!;
|
||||
}
|
||||
|
||||
async run(testGroups: TestGroup[]) {
|
||||
async run(testGroups: TestGroup[], extraEnvByProjectId: EnvByProjectId) {
|
||||
this._extraEnvByProjectId = extraEnvByProjectId;
|
||||
this._queue = testGroups;
|
||||
for (const group of testGroups) {
|
||||
this._queuedOrRunningHashCount.set(group.workerHash, 1 + (this._queuedOrRunningHashCount.get(group.workerHash) || 0));
|
||||
@ -452,7 +458,7 @@ export class Dispatcher {
|
||||
}
|
||||
|
||||
_createWorker(testGroup: TestGroup, parallelIndex: number, loaderData: SerializedConfig) {
|
||||
const worker = new WorkerHost(testGroup, parallelIndex, loaderData);
|
||||
const worker = new WorkerHost(testGroup, parallelIndex, loaderData, this._extraEnvByProjectId.get(testGroup.projectId) || {});
|
||||
const handleOutput = (params: TestOutputPayload) => {
|
||||
const chunk = chunkFromParams(params);
|
||||
if (worker.didFail()) {
|
||||
@ -481,9 +487,17 @@ export class Dispatcher {
|
||||
for (const error of params.fatalErrors)
|
||||
this._reporter.onError?.(error);
|
||||
});
|
||||
worker.on('exit', () => {
|
||||
const producedEnv = this._producedEnvByProjectId.get(testGroup.projectId) || {};
|
||||
this._producedEnvByProjectId.set(testGroup.projectId, { ...producedEnv, ...worker.producedEnv() });
|
||||
});
|
||||
return worker;
|
||||
}
|
||||
|
||||
producedEnvByProjectId() {
|
||||
return this._producedEnvByProjectId;
|
||||
}
|
||||
|
||||
async stop() {
|
||||
if (this._isStopped)
|
||||
return;
|
||||
|
@ -46,8 +46,8 @@ export class OutOfProcessLoaderHost {
|
||||
private _processHost: ProcessHost;
|
||||
|
||||
constructor(config: FullConfigInternal) {
|
||||
this._processHost = new ProcessHost(require.resolve('../loader/loaderMain.js'), 'loader');
|
||||
this._startPromise = this._processHost.startRunner(serializeConfig(config), true, {});
|
||||
this._processHost = new ProcessHost(require.resolve('../loader/loaderMain.js'), 'loader', {});
|
||||
this._startPromise = this._processHost.startRunner(serializeConfig(config), true);
|
||||
}
|
||||
|
||||
async loadTestFile(file: string, testErrors: TestError[]): Promise<Suite> {
|
||||
|
@ -17,7 +17,7 @@
|
||||
import child_process from 'child_process';
|
||||
import { EventEmitter } from 'events';
|
||||
import { debug } from 'playwright-core/lib/utilsBundle';
|
||||
import type { ProcessInitParams } from '../common/ipc';
|
||||
import type { EnvProducedPayload, ProcessInitParams } from '../common/ipc';
|
||||
import type { ProtocolResponse } from '../common/process';
|
||||
|
||||
export type ProcessExitData = {
|
||||
@ -35,17 +35,20 @@ export class ProcessHost extends EventEmitter {
|
||||
private _lastMessageId = 0;
|
||||
private _callbacks = new Map<number, { resolve: (result: any) => void, reject: (error: Error) => void }>();
|
||||
private _processName: string;
|
||||
private _producedEnv: Record<string, string | undefined> = {};
|
||||
private _extraEnv: Record<string, string | undefined>;
|
||||
|
||||
constructor(runnerScript: string, processName: string) {
|
||||
constructor(runnerScript: string, processName: string, env: Record<string, string | undefined>) {
|
||||
super();
|
||||
this._runnerScript = runnerScript;
|
||||
this._processName = processName;
|
||||
this._extraEnv = env;
|
||||
}
|
||||
|
||||
async startRunner(runnerParams: any, inheritStdio: boolean, env: NodeJS.ProcessEnv) {
|
||||
async startRunner(runnerParams: any, inheritStdio: boolean) {
|
||||
this.process = child_process.fork(require.resolve('../common/process'), {
|
||||
detached: false,
|
||||
env: { ...process.env, ...env },
|
||||
env: { ...process.env, ...this._extraEnv },
|
||||
stdio: inheritStdio ? ['ignore', 'inherit', 'inherit', 'ipc'] : ['ignore', 'ignore', process.env.PW_RUNNER_DEBUG ? 'inherit' : 'ignore', 'ipc'],
|
||||
});
|
||||
this.process.on('exit', (code, signal) => {
|
||||
@ -56,7 +59,10 @@ export class ProcessHost extends EventEmitter {
|
||||
this.process.on('message', (message: any) => {
|
||||
if (debug.enabled('pw:test:protocol'))
|
||||
debug('pw:test:protocol')('◀ RECV ' + JSON.stringify(message));
|
||||
if (message.method === '__dispatch__') {
|
||||
if (message.method === '__env_produced__') {
|
||||
const producedEnv: EnvProducedPayload = message.params;
|
||||
this._producedEnv = Object.fromEntries(producedEnv.map(e => [e[0], e[1] ?? undefined]));
|
||||
} else if (message.method === '__dispatch__') {
|
||||
const { id, error, method, params, result } = message.params as ProtocolResponse;
|
||||
if (id && this._callbacks.has(id)) {
|
||||
const { resolve, reject } = this._callbacks.get(id)!;
|
||||
@ -139,6 +145,10 @@ export class ProcessHost extends EventEmitter {
|
||||
return this._didSendStop;
|
||||
}
|
||||
|
||||
producedEnv() {
|
||||
return this._producedEnv;
|
||||
}
|
||||
|
||||
private send(message: { method: string, params?: any }) {
|
||||
if (debug.enabled('pw:test:protocol'))
|
||||
debug('pw:test:protocol')('SEND ► ' + JSON.stringify(message));
|
||||
|
@ -18,7 +18,7 @@ import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { promisify } from 'util';
|
||||
import { debug, rimraf } from 'playwright-core/lib/utilsBundle';
|
||||
import { Dispatcher } from './dispatcher';
|
||||
import { Dispatcher, type EnvByProjectId } from './dispatcher';
|
||||
import type { TestRunnerPluginRegistration } from '../plugins';
|
||||
import type { Multiplexer } from '../reporters/multiplexer';
|
||||
import { createTestGroups, type TestGroup } from '../runner/testGroups';
|
||||
@ -217,6 +217,7 @@ function createRunTestsTask(): Task<TaskRunnerState> {
|
||||
return async context => {
|
||||
const { phases } = context;
|
||||
const successfulProjects = new Set<FullProjectInternal>();
|
||||
const extraEnvByProjectId: EnvByProjectId = new Map();
|
||||
|
||||
for (const { dispatcher, projects } of phases) {
|
||||
// Each phase contains dispatcher and a set of test groups.
|
||||
@ -224,6 +225,12 @@ function createRunTestsTask(): Task<TaskRunnerState> {
|
||||
// that depend on the projects that failed previously.
|
||||
const phaseTestGroups: TestGroup[] = [];
|
||||
for (const { project, testGroups } of projects) {
|
||||
// Inherit extra enviroment variables from dependencies.
|
||||
let extraEnv: Record<string, string | undefined> = {};
|
||||
for (const dep of project._internal.deps)
|
||||
extraEnv = { ...extraEnv, ...extraEnvByProjectId.get(dep._internal.id) };
|
||||
extraEnvByProjectId.set(project._internal.id, extraEnv);
|
||||
|
||||
const hasFailedDeps = project._internal.deps.some(p => !successfulProjects.has(p));
|
||||
if (!hasFailedDeps) {
|
||||
phaseTestGroups.push(...testGroups);
|
||||
@ -236,8 +243,12 @@ function createRunTestsTask(): Task<TaskRunnerState> {
|
||||
}
|
||||
|
||||
if (phaseTestGroups.length) {
|
||||
await dispatcher!.run(phaseTestGroups);
|
||||
await dispatcher!.run(phaseTestGroups, extraEnvByProjectId);
|
||||
await dispatcher.stop();
|
||||
for (const [projectId, envProduced] of dispatcher.producedEnvByProjectId()) {
|
||||
const extraEnv = extraEnvByProjectId.get(projectId) || {};
|
||||
extraEnvByProjectId.set(projectId, { ...extraEnv, ...envProduced });
|
||||
}
|
||||
}
|
||||
|
||||
// If the worker broke, fail everything, we have no way of knowing which
|
||||
|
@ -27,9 +27,13 @@ export class WorkerHost extends ProcessHost {
|
||||
currentTestId: string | null = null;
|
||||
private _params: WorkerInitParams;
|
||||
|
||||
constructor(testGroup: TestGroup, parallelIndex: number, config: SerializedConfig) {
|
||||
constructor(testGroup: TestGroup, parallelIndex: number, config: SerializedConfig, extraEnv: Record<string, string | undefined>) {
|
||||
const workerIndex = lastWorkerIndex++;
|
||||
super(require.resolve('../worker/workerMain.js'), `worker-${workerIndex}`);
|
||||
super(require.resolve('../worker/workerMain.js'), `worker-${workerIndex}`, {
|
||||
...extraEnv,
|
||||
FORCE_COLOR: '1',
|
||||
DEBUG_COLORS: '1',
|
||||
});
|
||||
this.workerIndex = workerIndex;
|
||||
this.parallelIndex = parallelIndex;
|
||||
this._hash = testGroup.workerHash;
|
||||
@ -44,10 +48,7 @@ export class WorkerHost extends ProcessHost {
|
||||
}
|
||||
|
||||
async start() {
|
||||
await this.startRunner(this._params, false, {
|
||||
FORCE_COLOR: '1',
|
||||
DEBUG_COLORS: '1',
|
||||
});
|
||||
await this.startRunner(this._params, false);
|
||||
}
|
||||
|
||||
runTestGroup(runPayload: RunPayload) {
|
||||
|
@ -38,6 +38,50 @@ test('should run projects with dependencies', async ({ runInlineTest }) => {
|
||||
expect(result.outputLines).toEqual(['A', 'B', 'C']);
|
||||
});
|
||||
|
||||
test('should inherit env changes from dependencies', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'playwright.config.ts': `
|
||||
module.exports = { projects: [
|
||||
{ name: 'A', testMatch: '**/a.spec.ts' },
|
||||
{ name: 'B', testMatch: '**/b.spec.ts' },
|
||||
{ name: 'C', testMatch: '**/c.spec.ts', dependencies: ['A'] },
|
||||
{ name: 'D', testMatch: '**/d.spec.ts', dependencies: ['B'] },
|
||||
] };
|
||||
`,
|
||||
'a.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('pass', async ({}, testInfo) => {
|
||||
process.env.SET_IN_A = 'valuea';
|
||||
delete process.env.SET_OUTSIDE;
|
||||
console.log('\\n%%A');
|
||||
});
|
||||
`,
|
||||
'b.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('pass', async ({}, testInfo) => {
|
||||
process.env.SET_IN_B = 'valueb';
|
||||
console.log('\\n%%B');
|
||||
});
|
||||
`,
|
||||
'c.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('pass', async ({}, testInfo) => {
|
||||
console.log('\\n%%C-' + process.env.SET_IN_A + '-' + process.env.SET_IN_B + '-' + process.env.SET_OUTSIDE);
|
||||
});
|
||||
`,
|
||||
'd.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('pass', async ({}, testInfo) => {
|
||||
console.log('\\n%%D-' + process.env.SET_IN_A + '-' + process.env.SET_IN_B + '-' + process.env.SET_OUTSIDE);
|
||||
});
|
||||
`,
|
||||
}, {}, { SET_OUTSIDE: 'outside' });
|
||||
expect(result.passed).toBe(4);
|
||||
expect(result.failed).toBe(0);
|
||||
expect(result.skipped).toBe(0);
|
||||
expect(result.outputLines.sort()).toEqual(['A', 'B', 'C-valuea-undefined-undefined', 'D-undefined-valueb-outside']);
|
||||
});
|
||||
|
||||
test('should not run projects with dependencies when --no-deps is passed', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'playwright.config.ts': `
|
||||
|
Loading…
x
Reference in New Issue
Block a user