@stryker-mutator/core
Version:
The extendable JavaScript mutation testing framework
146 lines • 7.01 kB
JavaScript
import path from 'path';
import { fileURLToPath } from 'url';
import { errorToString } from '@stryker-mutator/util';
import log4js from 'log4js';
import { createInjector } from 'typed-inject';
import { commonTokens } from '@stryker-mutator/api/plugin';
import { LogConfigurator } from '../logging/index.js';
import { deserialize, serialize } from '../utils/string-utils.js';
import { coreTokens, provideLogger, PluginCreator } from '../di/index.js';
import { PluginLoader } from '../di/plugin-loader.js';
import { ParentMessageKind, WorkerMessageKind } from './message-protocol.js';
export class ChildProcessProxyWorker {
constructor(injectorFactory) {
this.injectorFactory = injectorFactory;
// Make sure to bind the methods in order to ensure the `this` pointer
this.handleMessage = this.handleMessage.bind(this);
// Start listening before sending the spawned message
process.on('message', this.handleMessage);
this.send({ kind: ParentMessageKind.Ready });
}
send(value) {
if (process.send) {
const str = serialize(value);
process.send(str);
}
}
handleMessage(serializedMessage) {
const message = deserialize(String(serializedMessage));
switch (message.kind) {
case WorkerMessageKind.Init:
// eslint-disable-next-line @typescript-eslint/no-floating-promises -- No handle needed, handleInit has try catch
this.handleInit(message);
this.removeAnyAdditionalMessageListeners(this.handleMessage);
break;
case WorkerMessageKind.Call:
// eslint-disable-next-line @typescript-eslint/no-floating-promises -- No handle needed, handleCall has try catch
this.handleCall(message);
this.removeAnyAdditionalMessageListeners(this.handleMessage);
break;
case WorkerMessageKind.Dispose:
const sendCompleted = () => {
this.send({ kind: ParentMessageKind.DisposeCompleted });
};
LogConfigurator.shutdown().then(sendCompleted).catch(sendCompleted);
break;
}
}
async handleInit(message) {
try {
LogConfigurator.configureChildProcess(message.loggingContext);
this.log = log4js.getLogger(ChildProcessProxyWorker.name);
this.handlePromiseRejections();
// Load plugins in the child process
const pluginInjector = provideLogger(this.injectorFactory())
.provideValue(commonTokens.options, message.options)
.provideValue(commonTokens.fileDescriptions, message.fileDescriptions);
const pluginLoader = pluginInjector.injectClass(PluginLoader);
const { pluginsByKind } = await pluginLoader.load(message.pluginModulePaths);
const injector = pluginInjector
.provideValue(coreTokens.pluginsByKind, pluginsByKind)
.provideClass(coreTokens.pluginCreator, PluginCreator);
const childModule = await import(message.modulePath);
const RealSubjectClass = childModule[message.namedExport];
const workingDir = path.resolve(message.workingDirectory);
if (process.cwd() !== workingDir) {
this.log.debug(`Changing current working directory for this process to ${workingDir}`);
process.chdir(workingDir);
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
this.realSubject = injector.injectClass(RealSubjectClass);
this.send({ kind: ParentMessageKind.Initialized });
}
catch (err) {
this.send({
error: errorToString(err),
kind: ParentMessageKind.InitError,
});
}
}
async handleCall(message) {
try {
const result = await this.doCall(message);
this.send({
correlationId: message.correlationId,
kind: ParentMessageKind.CallResult,
result,
});
}
catch (err) {
this.send({
correlationId: message.correlationId,
error: errorToString(err),
kind: ParentMessageKind.CallRejection,
});
}
}
doCall(message) {
if (typeof this.realSubject[message.methodName] === 'function') {
return this.realSubject[message.methodName](...message.args);
}
else {
return this.realSubject[message.methodName];
}
}
/**
* Remove any addition message listeners that might me eavesdropping.
* the @ngtools/webpack plugin listens to messages and throws an error whenever it could not handle a message
* @see https://github.com/angular/angular-cli/blob/f776d3cf7982b64734c57fe4407434e9f4ec09f7/packages/%40ngtools/webpack/src/type_checker.ts#L79
* @param exceptListener The listener that should remain
*/
removeAnyAdditionalMessageListeners(exceptListener) {
process.listeners('message').forEach((listener) => {
var _a;
if (listener !== exceptListener) {
(_a = this.log) === null || _a === void 0 ? void 0 : _a.debug("Removing an additional message listener, we don't want eavesdropping on our inter-process communication: %s", listener.toString());
process.removeListener('message', listener);
}
});
}
/**
* During mutation testing, it's to be expected that promise rejections are not handled synchronously anymore (or not at all)
* Let's handle those events so future versions of node don't crash
* See issue 350: https://github.com/stryker-mutator/stryker-js/issues/350
*/
handlePromiseRejections() {
const unhandledRejections = [];
process.on('unhandledRejection', (reason, promise) => {
var _a;
const unhandledPromiseId = unhandledRejections.push(promise);
(_a = this.log) === null || _a === void 0 ? void 0 : _a.debug(`UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: ${unhandledPromiseId}): ${reason}`);
});
process.on('rejectionHandled', (promise) => {
var _a;
const unhandledPromiseId = unhandledRejections.indexOf(promise) + 1;
(_a = this.log) === null || _a === void 0 ? void 0 : _a.debug(`PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: ${unhandledPromiseId})`);
});
}
}
// Prevent side effects for merely importing the file
// Only actually start the child worker when it is requested
// Stryker disable all
if (fileURLToPath(import.meta.url) === process.argv[1]) {
new ChildProcessProxyWorker(createInjector);
}
// Stryker restore all
//# sourceMappingURL=child-process-proxy-worker.js.map