Skip to content

Commit

Permalink
feat(Child processes): Support process message polution (#572)
Browse files Browse the repository at this point in the history
Support transpilers which polute the process.on('message').
  • Loading branch information
nicojs authored and simondel committed Jan 12, 2018
1 parent ca389e5 commit dbe4d84
Show file tree
Hide file tree
Showing 2 changed files with 56 additions and 8 deletions.
27 changes: 25 additions & 2 deletions packages/stryker/src/child-proxy/ChildProcessProxyWorker.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { serialize, deserialize } from '../utils/objectUtils';
import { WorkerMessage, WorkerMessageKind, ParentMessage, autoStart } from './messageProtocol';
import { setGlobalLogLevel } from 'log4js';
import { setGlobalLogLevel, getLogger } from 'log4js';
import PluginLoader from '../PluginLoader';

export default class ChildProcessProxyWorker {

private log = getLogger(ChildProcessProxyWorker.name);

realSubject: any;

constructor() {
Expand All @@ -17,7 +20,8 @@ export default class ChildProcessProxyWorker {
}

private listenToParent() {
process.on('message', (serializedMessage: string) => {

const handler = (serializedMessage: string) => {
const message = deserialize(serializedMessage) as WorkerMessage;
switch (message.kind) {
case WorkerMessageKind.Init:
Expand All @@ -26,6 +30,7 @@ export default class ChildProcessProxyWorker {
const RealSubjectClass = require(message.requirePath).default;
this.realSubject = new RealSubjectClass(...message.constructorArgs);
this.send('init_done');
this.removeAnyAdditionalMessageListeners(handler);
break;
case WorkerMessageKind.Work:
const result = this.realSubject[message.methodName](...message.args);
Expand All @@ -35,6 +40,24 @@ export default class ChildProcessProxyWorker {
result
});
});
this.removeAnyAdditionalMessageListeners(handler);
break;
}
};
process.on('message', handler);
}

/**
* 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
*/
private removeAnyAdditionalMessageListeners(exceptListener: NodeJS.MessageListener) {
process.listeners('message').forEach(listener => {
if (listener !== exceptListener) {
this.log.debug('Removing an additional message listener, we don\'t want eavesdropping on our inter-process communication: %s', listener.toString());
process.removeListener('message', listener);
}
});
}
Expand Down
37 changes: 31 additions & 6 deletions packages/stryker/test/unit/child-proxy/ChildProcessWorkerSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,19 @@ describe('ChildProcessProxyWorker', () => {

let processOnStub: sinon.SinonStub;
let processSendStub: sinon.SinonStub;
let processListenersStub: sinon.SinonStub;
let setGlobalLogLevelStub: sinon.SinonStub;
let processRemoveListenerStub: sinon.SinonStub;
let pluginLoaderMock: Mock<PluginLoader>;
let originalProcessSend: undefined | ((message: any, sendHandle?: any) => void);
let originalProcessSend: undefined | NodeJS.MessageListener;
let processes: NodeJS.MessageListener[];

beforeEach(() => {
processes = [];
processOnStub = sandbox.stub(process, 'on');
processListenersStub = sandbox.stub(process, 'listeners');
processListenersStub.returns(processes);
processRemoveListenerStub = sandbox.stub(process, 'removeListener');
processSendStub = sandbox.stub();
// process.send is normally undefined
originalProcessSend = process.send;
Expand All @@ -38,36 +45,53 @@ describe('ChildProcessProxyWorker', () => {
describe('after init message', () => {

let sut: ChildProcessProxyWorker;
let initMessage: WorkerMessage;

beforeEach(() => {
sut = new ChildProcessProxyWorker();
const initMessage: WorkerMessage = {
initMessage = {
kind: WorkerMessageKind.Init,
logLevel: 'FooLevel',
constructorArgs: ['FooBarName'],
plugins: ['fooPlugin', 'barPlugin'],
requirePath: require.resolve('./HelloClass')
};
processOnStub.callArgWith(1, [serialize(initMessage)]);
});

it('should create the correct real instance', () => {
processOnStub.callArgWith(1, [serialize(initMessage)]);
expect(sut.realSubject).instanceOf(HelloClass);
const actual = sut.realSubject as HelloClass;
expect(actual.name).eq('FooBarName');
});

it('should send "init_done"', async () => {
processOnStub.callArgWith(1, [serialize(initMessage)]);
const expectedWorkerResponse: ParentMessage = 'init_done';
await tick(); // make sure promise is resolved
expect(processSendStub).calledWith(serialize(expectedWorkerResponse));
});

it('should remove any additional listeners', async () => {
// Arrange
function noop() { }
processes.push(noop);

// Act
processOnStub.callArgWith(1, [serialize(initMessage)]);
await tick(); // make sure promise is resolved

// Assert
expect(processRemoveListenerStub).calledWith('message', noop);
});

it('should set global log level', () => {
processOnStub.callArgWith(1, [serialize(initMessage)]);
expect(setGlobalLogLevelStub).calledWith('FooLevel');
});

it('should load plugins', () => {
processOnStub.callArgWith(1, [serialize(initMessage)]);
expect(pluginLoader.default).calledWithNew;
expect(pluginLoader.default).calledWith(['fooPlugin', 'barPlugin']);
expect(pluginLoaderMock.load).called;
Expand All @@ -77,6 +101,7 @@ describe('ChildProcessProxyWorker', () => {

async function actAndAssert(workerMessage: WorkMessage, expectedResult: WorkResult) {
// Act
processOnStub.callArgWith(1, [serialize(initMessage)]);
processOnStub.callArgWith(1, serialize(workerMessage));
await tick();
// Assert
Expand Down

0 comments on commit dbe4d84

Please sign in to comment.