chore: prepare to load scripts in subprocess (#20174)
This commit is contained in:
parent
9ba5a1be38
commit
020dcd89fa
|
|
@ -111,7 +111,7 @@ export class Dispatcher {
|
||||||
worker = this._createWorker(job, index, this._loader.serialize());
|
worker = this._createWorker(job, index, this._loader.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.init();
|
await worker.start();
|
||||||
if (this._isStopped) // Check stopped signal after async hop.
|
if (this._isStopped) // Check stopped signal after async hop.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,9 +35,9 @@ export type WorkerIsolation =
|
||||||
|
|
||||||
|
|
||||||
export type ProcessInitParams = {
|
export type ProcessInitParams = {
|
||||||
workerIndex?: number;
|
|
||||||
stdoutParams: TtyParams;
|
stdoutParams: TtyParams;
|
||||||
stderrParams: TtyParams;
|
stderrParams: TtyParams;
|
||||||
|
processName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WorkerInitParams = {
|
export type WorkerInitParams = {
|
||||||
|
|
|
||||||
|
|
@ -37,9 +37,7 @@ export type ProtocolResponse = {
|
||||||
|
|
||||||
export class ProcessRunner {
|
export class ProcessRunner {
|
||||||
appendProcessTeardownDiagnostics(error: TestInfoError) { }
|
appendProcessTeardownDiagnostics(error: TestInfoError) { }
|
||||||
unhandledError(reason: any) { }
|
async gracefullyClose(): Promise<void> { }
|
||||||
async cleanup(): Promise<void> { }
|
|
||||||
async stop(): Promise<void> { }
|
|
||||||
|
|
||||||
protected dispatchEvent(method: string, params: any) {
|
protected dispatchEvent(method: string, params: any) {
|
||||||
const response: ProtocolResponse = { method, params };
|
const response: ProtocolResponse = { method, params };
|
||||||
|
|
@ -74,29 +72,18 @@ process.on('SIGINT', () => {});
|
||||||
process.on('SIGTERM', () => {});
|
process.on('SIGTERM', () => {});
|
||||||
|
|
||||||
let processRunner: ProcessRunner;
|
let processRunner: ProcessRunner;
|
||||||
let workerIndex: number | undefined;
|
let processName: string | undefined;
|
||||||
|
|
||||||
process.on('unhandledRejection', (reason, promise) => {
|
|
||||||
if (processRunner)
|
|
||||||
processRunner.unhandledError(reason);
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on('uncaughtException', error => {
|
|
||||||
if (processRunner)
|
|
||||||
processRunner.unhandledError(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on('message', async message => {
|
process.on('message', async message => {
|
||||||
if (message.method === 'init') {
|
if (message.method === '__init__') {
|
||||||
const initParams = message.params as ProcessInitParams;
|
const { processParams, runnerParams, runnerScript } = message.params as { processParams: ProcessInitParams, runnerParams: any, runnerScript: string };
|
||||||
workerIndex = initParams.workerIndex;
|
setTtyParams(process.stdout, processParams.stdoutParams);
|
||||||
initConsoleParameters(initParams);
|
setTtyParams(process.stderr, processParams.stderrParams);
|
||||||
startProfiling();
|
startProfiling();
|
||||||
const { create } = require(process.env.PW_PROCESS_RUNNER_SCRIPT!);
|
const { create } = require(runnerScript);
|
||||||
processRunner = create(initParams) as ProcessRunner;
|
processRunner = create(runnerParams) as ProcessRunner;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (message.method === 'stop') {
|
if (message.method === '__stop__') {
|
||||||
await gracefullyCloseAndExit();
|
await gracefullyCloseAndExit();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -121,12 +108,10 @@ async function gracefullyCloseAndExit() {
|
||||||
setTimeout(() => process.exit(0), 30000);
|
setTimeout(() => process.exit(0), 30000);
|
||||||
// Meanwhile, try to gracefully shutdown.
|
// Meanwhile, try to gracefully shutdown.
|
||||||
try {
|
try {
|
||||||
if (processRunner) {
|
if (processRunner)
|
||||||
await processRunner.stop();
|
await processRunner.gracefullyClose();
|
||||||
await processRunner.cleanup();
|
if (processName)
|
||||||
}
|
await stopProfiling(processName);
|
||||||
if (workerIndex !== undefined)
|
|
||||||
await stopProfiling(workerIndex);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
try {
|
try {
|
||||||
const error = serializeError(e);
|
const error = serializeError(e);
|
||||||
|
|
@ -155,12 +140,6 @@ function chunkToParams(chunk: Buffer | string): { text?: string, buffer?: strin
|
||||||
return { text: chunk };
|
return { text: chunk };
|
||||||
}
|
}
|
||||||
|
|
||||||
function initConsoleParameters(initParams: ProcessInitParams) {
|
|
||||||
// Make sure the output supports colors.
|
|
||||||
setTtyParams(process.stdout, initParams.stdoutParams);
|
|
||||||
setTtyParams(process.stderr, initParams.stderrParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setTtyParams(stream: WriteStream, params: TtyParams) {
|
function setTtyParams(stream: WriteStream, params: TtyParams) {
|
||||||
stream.isTTY = true;
|
stream.isTTY = true;
|
||||||
if (params.rows)
|
if (params.rows)
|
||||||
|
|
|
||||||
|
|
@ -33,13 +33,15 @@ export class ProcessHost<InitParams> extends EventEmitter {
|
||||||
private _runnerScript: string;
|
private _runnerScript: string;
|
||||||
private _lastMessageId = 0;
|
private _lastMessageId = 0;
|
||||||
private _callbacks = new Map<number, { resolve: (result: any) => void, reject: (error: Error) => void }>();
|
private _callbacks = new Map<number, { resolve: (result: any) => void, reject: (error: Error) => void }>();
|
||||||
|
private _processName: string;
|
||||||
|
|
||||||
constructor(runnerScript: string) {
|
constructor(runnerScript: string, processName: string) {
|
||||||
super();
|
super();
|
||||||
this._runnerScript = runnerScript;
|
this._runnerScript = runnerScript;
|
||||||
|
this._processName = processName;
|
||||||
}
|
}
|
||||||
|
|
||||||
async doInit(params: InitParams) {
|
protected async startRunner(runnerParams: InitParams) {
|
||||||
this.process = child_process.fork(require.resolve('./process'), {
|
this.process = child_process.fork(require.resolve('./process'), {
|
||||||
detached: false,
|
detached: false,
|
||||||
env: {
|
env: {
|
||||||
|
|
@ -90,9 +92,16 @@ export class ProcessHost<InitParams> extends EventEmitter {
|
||||||
columns: process.stderr.columns,
|
columns: process.stderr.columns,
|
||||||
colorDepth: process.stderr.getColorDepth?.() || 8
|
colorDepth: process.stderr.getColorDepth?.() || 8
|
||||||
},
|
},
|
||||||
|
processName: this._processName
|
||||||
};
|
};
|
||||||
|
|
||||||
this.send({ method: 'init', params: { ...processParams, ...params } });
|
this.send({
|
||||||
|
method: '__init__', params: {
|
||||||
|
processParams,
|
||||||
|
runnerScript: this._runnerScript,
|
||||||
|
runnerParams
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected sendMessage(message: { method: string, params?: any }) {
|
protected sendMessage(message: { method: string, params?: any }) {
|
||||||
|
|
@ -116,7 +125,7 @@ export class ProcessHost<InitParams> extends EventEmitter {
|
||||||
if (this.didExit)
|
if (this.didExit)
|
||||||
return;
|
return;
|
||||||
if (!this._didSendStop) {
|
if (!this._didSendStop) {
|
||||||
this.send({ method: 'stop' });
|
this.send({ method: '__stop__' });
|
||||||
this._didSendStop = true;
|
this._didSendStop = true;
|
||||||
}
|
}
|
||||||
await new Promise(f => this.once('exit', f));
|
await new Promise(f => this.once('exit', f));
|
||||||
|
|
|
||||||
|
|
@ -34,14 +34,14 @@ export async function startProfiling() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function stopProfiling(workerIndex: number | undefined) {
|
export async function stopProfiling(processName: string | undefined) {
|
||||||
if (!profileDir)
|
if (!profileDir)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
await new Promise<void>(f => session.post('Profiler.stop', (err, { profile }) => {
|
await new Promise<void>(f => session.post('Profiler.stop', (err, { profile }) => {
|
||||||
if (!err) {
|
if (!err) {
|
||||||
fs.mkdirSync(profileDir, { recursive: true });
|
fs.mkdirSync(profileDir, { recursive: true });
|
||||||
fs.writeFileSync(path.join(profileDir, workerIndex === undefined ? 'runner.json' : 'worker' + workerIndex + '.json'), JSON.stringify(profile));
|
fs.writeFileSync(path.join(profileDir, (processName || 'runner') + '.json'), JSON.stringify(profile));
|
||||||
}
|
}
|
||||||
f();
|
f();
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
|
|
@ -25,15 +25,16 @@ export class WorkerHost extends ProcessHost<WorkerInitParams> {
|
||||||
readonly workerIndex: number;
|
readonly workerIndex: number;
|
||||||
private _hash: string;
|
private _hash: string;
|
||||||
currentTestId: string | null = null;
|
currentTestId: string | null = null;
|
||||||
private _initParams: WorkerInitParams;
|
private _params: WorkerInitParams;
|
||||||
|
|
||||||
constructor(testGroup: TestGroup, parallelIndex: number, workerIsolation: WorkerIsolation, loader: SerializedLoaderData) {
|
constructor(testGroup: TestGroup, parallelIndex: number, workerIsolation: WorkerIsolation, loader: SerializedLoaderData) {
|
||||||
super(require.resolve('./workerRunner.js'));
|
const workerIndex = lastWorkerIndex++;
|
||||||
this.workerIndex = lastWorkerIndex++;
|
super(require.resolve('./workerRunner.js'), `worker-${workerIndex}`);
|
||||||
|
this.workerIndex = workerIndex;
|
||||||
this.parallelIndex = parallelIndex;
|
this.parallelIndex = parallelIndex;
|
||||||
this._hash = testGroup.workerHash;
|
this._hash = testGroup.workerHash;
|
||||||
|
|
||||||
this._initParams = {
|
this._params = {
|
||||||
workerIsolation,
|
workerIsolation,
|
||||||
workerIndex: this.workerIndex,
|
workerIndex: this.workerIndex,
|
||||||
parallelIndex,
|
parallelIndex,
|
||||||
|
|
@ -43,8 +44,8 @@ export class WorkerHost extends ProcessHost<WorkerInitParams> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async start() {
|
||||||
await this.doInit(this._initParams);
|
await this.startRunner(this._params);
|
||||||
}
|
}
|
||||||
|
|
||||||
runTestGroup(runPayload: RunPayload) {
|
runTestGroup(runPayload: RunPayload) {
|
||||||
|
|
|
||||||
|
|
@ -68,9 +68,12 @@ export class WorkerRunner extends ProcessRunner {
|
||||||
// Resolve this promise, so worker does not stall waiting for the non-existent run to finish,
|
// Resolve this promise, so worker does not stall waiting for the non-existent run to finish,
|
||||||
// when it was sopped before running any test group.
|
// when it was sopped before running any test group.
|
||||||
this._runFinished.resolve();
|
this._runFinished.resolve();
|
||||||
|
|
||||||
|
process.on('unhandledRejection', reason => this.unhandledError(reason));
|
||||||
|
process.on('uncaughtException', error => this.unhandledError(error));
|
||||||
}
|
}
|
||||||
|
|
||||||
override stop(): Promise<void> {
|
private _stop(): Promise<void> {
|
||||||
if (!this._isStopped) {
|
if (!this._isStopped) {
|
||||||
this._isStopped = true;
|
this._isStopped = true;
|
||||||
|
|
||||||
|
|
@ -83,7 +86,9 @@ export class WorkerRunner extends ProcessRunner {
|
||||||
return this._runFinished;
|
return this._runFinished;
|
||||||
}
|
}
|
||||||
|
|
||||||
override async cleanup() {
|
override async gracefullyClose() {
|
||||||
|
await this._stop();
|
||||||
|
|
||||||
// We have to load the project to get the right deadline below.
|
// We have to load the project to get the right deadline below.
|
||||||
await this._loadIfNeeded();
|
await this._loadIfNeeded();
|
||||||
await this._teardownScopes();
|
await this._teardownScopes();
|
||||||
|
|
@ -133,7 +138,7 @@ export class WorkerRunner extends ProcessRunner {
|
||||||
this._fatalErrors.push(timeoutError);
|
this._fatalErrors.push(timeoutError);
|
||||||
}
|
}
|
||||||
|
|
||||||
override unhandledError(error: Error | any) {
|
unhandledError(error: Error | any) {
|
||||||
// Usually, we do not differentiate between errors in the control flow
|
// Usually, we do not differentiate between errors in the control flow
|
||||||
// and unhandled errors - both lead to the test failing. This is good for regular tests,
|
// and unhandled errors - both lead to the test failing. This is good for regular tests,
|
||||||
// so that you can, e.g. expect() from inside an event handler. The test fails,
|
// so that you can, e.g. expect() from inside an event handler. The test fails,
|
||||||
|
|
@ -155,7 +160,7 @@ export class WorkerRunner extends ProcessRunner {
|
||||||
if (!this._fatalErrors.length)
|
if (!this._fatalErrors.length)
|
||||||
this._fatalErrors.push(serializeError(error));
|
this._fatalErrors.push(serializeError(error));
|
||||||
}
|
}
|
||||||
this.stop();
|
this._stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _loadIfNeeded() {
|
private async _loadIfNeeded() {
|
||||||
|
|
@ -208,7 +213,7 @@ export class WorkerRunner extends ProcessRunner {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fatalUnknownTestIds = runPayload.entries.map(e => e.testId);
|
fatalUnknownTestIds = runPayload.entries.map(e => e.testId);
|
||||||
this.stop();
|
this._stop();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// In theory, we should run above code without any errors.
|
// In theory, we should run above code without any errors.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue