@stryker-mutator/core
Version:
The extendable JavaScript mutation testing framework
228 lines • 10.4 kB
JavaScript
import path from 'path';
import { URL } from 'url';
import { commonTokens, PluginKind } from '@stryker-mutator/api/plugin';
import { factory, testInjector } from '@stryker-mutator/test-helpers';
import { expect } from 'chai';
import sinon from 'sinon';
import { Task } from '@stryker-mutator/util';
import { ChildProcessProxyWorker } from '../../../src/child-proxy/child-process-proxy-worker.js';
import { ParentMessageKind, WorkerMessageKind, } from '../../../src/child-proxy/message-protocol.js';
import { LogConfigurator } from '../../../src/logging/index.js';
import { serialize } from '../../../src/utils/string-utils.js';
import { currentLogMock } from '../../helpers/log-mock.js';
import { coreTokens, PluginLoader } from '../../../src/di/index.js';
import { HelloClass } from './hello-class.js';
const LOGGING_CONTEXT = Object.freeze({ port: 4200, level: "fatal" /* LogLevel.Fatal */ });
describe(ChildProcessProxyWorker.name, () => {
let processOnStub;
let processSendStub;
let processListenersStub;
let configureChildProcessStub;
let processRemoveListenerStub;
let injectorMock;
let pluginLoaderMock;
let processChdirStub;
let createInjectorStub;
let logMock;
let originalProcessSend;
let processes;
let loadedPlugins;
let pluginModulePaths;
const workingDir = 'working dir';
beforeEach(() => {
injectorMock = factory.injector();
createInjectorStub = sinon.stub();
createInjectorStub.returns(injectorMock);
pluginLoaderMock = sinon.createStubInstance(PluginLoader);
injectorMock.injectClass.withArgs(PluginLoader).returns(pluginLoaderMock).withArgs(HelloClass).returns(new HelloClass(testInjector.options));
processes = [];
logMock = currentLogMock();
processOnStub = sinon.stub(process, 'on');
processListenersStub = sinon.stub(process, 'listeners');
processListenersStub.returns(processes);
processRemoveListenerStub = sinon.stub(process, 'removeListener');
processSendStub = sinon.stub();
pluginModulePaths = ['plugin', 'paths'];
loadedPlugins = {
pluginModulePaths,
pluginsByKind: new Map([
[PluginKind.Reporter, [{ kind: PluginKind.Reporter, name: 'rep', factory: factory.reporter }]],
]),
schemaContributions: [],
};
pluginLoaderMock.load.resolves(loadedPlugins);
// process.send is normally undefined
originalProcessSend = process.send;
process.send = processSendStub;
processChdirStub = sinon.stub(process, 'chdir');
configureChildProcessStub = sinon.stub(LogConfigurator, 'configureChildProcess');
});
afterEach(() => {
process.send = originalProcessSend;
});
it('should listen to parent process', () => {
new ChildProcessProxyWorker(createInjectorStub);
expect(processOnStub).calledWith('message');
});
describe('after init message', () => {
let sut;
let initMessage;
beforeEach(() => {
sut = new ChildProcessProxyWorker(createInjectorStub);
const options = factory.strykerOptions({ testRunner: 'fooBar' });
initMessage = {
kind: WorkerMessageKind.Init,
loggingContext: LOGGING_CONTEXT,
options,
fileDescriptions: { 'foo.js': { mutate: true } },
pluginModulePaths,
namedExport: HelloClass.name,
modulePath: new URL('./hello-class.js', import.meta.url).toString(),
workingDirectory: workingDir,
};
});
it('should create the correct real instance', async () => {
await processOnMessage(initMessage);
expect(sut.realSubject).instanceOf(HelloClass);
sinon.assert.calledWithExactly(injectorMock.provideValue, commonTokens.options, initMessage.options);
sinon.assert.calledWithExactly(injectorMock.provideValue, coreTokens.pluginsByKind, loadedPlugins.pluginsByKind);
});
it('should change the current working directory', async () => {
await processOnMessage(initMessage);
const fullWorkingDir = path.resolve(workingDir);
expect(logMock.debug).calledWith(`Changing current working directory for this process to ${fullWorkingDir}`);
expect(processChdirStub).calledWith(fullWorkingDir);
});
it("should not change the current working directory if it didn't change", async () => {
initMessage.workingDirectory = process.cwd();
await processOnMessage(initMessage);
expect(logMock.debug).not.called;
expect(processChdirStub).not.called;
});
it('should send "init_done"', async () => {
await processOnMessage(initMessage);
const expectedWorkerResponse = { kind: ParentMessageKind.Initialized };
expect(processSendStub).calledWith(serialize(expectedWorkerResponse));
});
it('should remove any additional listeners', async () => {
// Arrange
function noop() {
//noop
}
processes.push(noop);
// Act
await processOnMessage(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(configureChildProcessStub).calledWith(LOGGING_CONTEXT);
});
it('should handle unhandledRejection events', async () => {
await processOnMessage(initMessage);
const error = new Error('foobar');
processOnStub.withArgs('unhandledRejection').callArgWith(1, error);
expect(logMock.debug).calledWith(`UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): ${error}`);
});
it('should handle rejectionHandled events', async () => {
await processOnMessage(initMessage);
processOnStub.withArgs('rejectionHandled').callArgWith(1);
expect(logMock.debug).calledWith('PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 0)');
});
describe('on worker message', () => {
async function actAndAssert(workerMessage, expectedResult) {
// Act
await processOnMessage(initMessage);
await processOnMessage(workerMessage);
// Assert
expect(processSendStub).calledWith(serialize(expectedResult));
}
async function actAndAssertRejection(workerMessage, expectedError) {
// Act
await processOnMessage(initMessage);
await processOnMessage(workerMessage);
// Assert
expect(processSendStub).calledWithMatch(`"correlationId":${workerMessage.correlationId.toString()}`);
expect(processSendStub).calledWithMatch(`"kind":${ParentMessageKind.CallRejection.toString()}`);
expect(processSendStub).calledWithMatch(`"error":"Error: ${expectedError}`);
}
it('should send the result', async () => {
// Arrange
const workerMessage = {
args: [],
correlationId: 32,
kind: WorkerMessageKind.Call,
methodName: 'sayHello',
};
const expectedResult = {
correlationId: 32,
kind: ParentMessageKind.CallResult,
result: 'hello from HelloClass',
};
await actAndAssert(workerMessage, expectedResult);
});
it('should send a rejection', async () => {
// Arrange
const workerMessage = {
args: [],
correlationId: 32,
kind: WorkerMessageKind.Call,
methodName: 'reject',
};
await actAndAssertRejection(workerMessage, 'Rejected');
});
it('should send a thrown synchronous error as rejection', async () => {
// Arrange
const workerMessage = {
args: ['foo bar'],
correlationId: 32,
kind: WorkerMessageKind.Call,
methodName: 'throw',
};
await actAndAssertRejection(workerMessage, 'foo bar');
});
it('should use correct arguments', async () => {
// Arrange
const workerMessage = {
args: ['foo', 'bar', 'chair'],
correlationId: 32,
kind: WorkerMessageKind.Call,
methodName: 'say',
};
const expectedResult = {
correlationId: 32,
kind: ParentMessageKind.CallResult,
result: 'hello foo and bar and chair',
};
await actAndAssert(workerMessage, expectedResult);
});
it('should work with promises from real class', async () => {
// Arrange
const workerMessage = {
args: [],
correlationId: 32,
kind: WorkerMessageKind.Call,
methodName: 'sayDelayed',
};
const expectedResult = {
correlationId: 32,
kind: ParentMessageKind.CallResult,
result: 'delayed hello from HelloClass',
};
await actAndAssert(workerMessage, expectedResult);
});
});
});
function processOnMessage(message) {
const task = new Task();
processSendStub.callsFake(task.resolve);
processOnStub.withArgs('message').callArgWith(1, [serialize(message)]);
return task.promise;
}
});
function tick() {
return new Promise((res) => setTimeout(res, 0));
}
//# sourceMappingURL=child-process-worker.spec.js.map