Skip to content

Commit

Permalink
fix(isolated-test-runner): Support regexes (#146)
Browse files Browse the repository at this point in the history
* Add support for communicating regexes (and functions) from the stryker process to test runners running in child processes.
* Add dependency to 'serialize-javascript'. A minimalistic module for serializing regexes and functions in a json structure.
  • Loading branch information
nicojs authored and simondel committed Sep 9, 2016
1 parent 5597d68 commit 51b6903
Show file tree
Hide file tree
Showing 7 changed files with 62 additions and 19 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@
"lodash": "^3.10.1",
"log4js": "^0.6.33",
"mkdirp": "^0.5.1",
"node-glob": "^1.2.0"
"node-glob": "^1.2.0",
"serialize-javascript": "^1.3.0"
},
"devDependencies": {
"chai": "^3.4.1",
Expand Down
15 changes: 11 additions & 4 deletions src/isolated-runner/IsolatedTestRunnerAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import RunMessageBody from './RunMessageBody';
import ResultMessageBody from './ResultMessageBody';
import * as _ from 'lodash';
import * as log4js from 'log4js';
import {serialize} from '../utils/objectUtils';

const log = log4js.getLogger('IsolatedTestRunnerAdapter');
const MAX_WAIT_FOR_DISPOSE = 2000;
Expand Down Expand Up @@ -123,7 +124,13 @@ export default class TestRunnerChildProcessAdapter implements TestRunner {
runOptions: options
}
}
this.workerProcess.send(message);
this.send(message);
}

private send<T>(message: Message<T>){
// Serialize message before sending to preserve all javascript, including regexes and functions
// See https://github.com/stryker-mutator/stryker/issues/143
this.workerProcess.send(serialize(message));
}

private sendStartCommand() {
Expand All @@ -134,15 +141,15 @@ export default class TestRunnerChildProcessAdapter implements TestRunner {
runnerOptions: this.options
}
}
this.workerProcess.send(startMessage);
this.send(startMessage);
}

private sendInitCommand() {
this.workerProcess.send(this.emptyMessage(MessageType.Init));
this.send(this.emptyMessage(MessageType.Init));
}

private sendDisposeCommand() {
this.workerProcess.send(this.emptyMessage(MessageType.Dispose));
this.send(this.emptyMessage(MessageType.Dispose));
}

private handleResultMessage(message: Message<ResultMessageBody>) {
Expand Down
5 changes: 3 additions & 2 deletions src/isolated-runner/IsolatedTestRunnerAdapterWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import RunMessageBody from './RunMessageBody';
import ResultMessageBody from './ResultMessageBody';
import PluginLoader from '../PluginLoader';
import * as log4js from 'log4js';
import {isPromise} from '../utils/objectUtils';
import {isPromise, deserialize} from '../utils/objectUtils';

const log = log4js.getLogger('TestRunnerChildProcessAdapterWorker');

Expand All @@ -18,7 +18,8 @@ class TestRunnerChildProcessAdapterWorker {
}

listenToMessages() {
process.on('message', (message: Message<any>) => {
process.on('message', (serializedMessage: string) => {
let message: Message<any> = deserialize(serializedMessage);
switch (message.type) {
case MessageType.Start:
this.start(message.body);
Expand Down
10 changes: 9 additions & 1 deletion src/utils/objectUtils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import * as _ from 'lodash';

// Don't use JSON.parse, as it does not allow for regexes or functions, etc
export var serialize: (obj: any) => string = require('serialize-javascript');

export function freezeRecursively(target: { [customConfig: string]: any }) {
Object.freeze(target);
Object.keys(target).forEach(key => {
Expand All @@ -11,4 +14,9 @@ export function freezeRecursively(target: { [customConfig: string]: any }) {

export function isPromise(input: void | Promise<any>): input is Promise<any> {
return input && typeof (<any>input)['then'] === 'function';
}
}

export function deserialize(serializedJavascript: String): any {
// Don't use JSON.parse, as it does not allow for regexes or functions, etc
return eval(`(${serializedJavascript})`);
}
18 changes: 18 additions & 0 deletions test/integration/isolated-runner/DiscoverRegexTestRunner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { TestRunnerFactory, RunnerOptions, TestRunner, RunOptions, RunResult, TestResult } from 'stryker-api/test_runner';
import { isRegExp } from 'util';

class DiscoverRegexTestRunner implements TestRunner {

constructor(private runnerOptions: RunnerOptions) {
}

run(options: RunOptions) {
if (isRegExp(this.runnerOptions.strykerOptions['someRegex'])) {
return Promise.resolve({ result: TestResult.Complete, testNames: []});
} else {
return Promise.resolve({ result: TestResult.Error, testNames: [], errorMessages: ['No regex found in runnerOptions.strykerOptions.someRegex'] });
}
}
}

TestRunnerFactory.instance().register('discover-regex', DiscoverRegexTestRunner);
25 changes: 16 additions & 9 deletions test/integration/isolated-runner/IsolatedTestRunnerAdapterSpec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import TestRunnerChildProcessAdapter from '../../../src/isolated-runner/IsolatedTestRunnerAdapter';
import {TestRunnerFactory, TestRunner, RunOptions, RunResult, TestResult, RunnerOptions} from 'stryker-api/test_runner';
import {StrykerOptions} from 'stryker-api/core';
import * as chai from 'chai';
import * as chaiAsPromised from 'chai-as-promised';
chai.use(chaiAsPromised);
let expect = chai.expect;
import * as log4js from 'log4js';
import {expect} from 'chai';
import logger from '../../helpers/log4jsMock';

describe('TestRunnerChildProcessAdapter', function () {

Expand All @@ -17,22 +14,32 @@ describe('TestRunnerChildProcessAdapter', function () {
plugins: [
'../../test/integration/isolated-runner/DirectResolvedTestRunner',
'../../test/integration/isolated-runner/NeverResolvedTestRunner',
'../../test/integration/isolated-runner/SlowInitAndDisposeTestRunner'],
'../../test/integration/isolated-runner/SlowInitAndDisposeTestRunner',
'../../test/integration/isolated-runner/DiscoverRegexTestRunner'],
testRunner: 'karma',
testFramework: 'jasmine',
port: null
port: null,
'someRegex': /someRegex/
},
files: [],
port: null,
port: null
};

describe('when sending a regex in the options', () => {
before(() => sut = new TestRunnerChildProcessAdapter('discover-regex', options));

it('correctly receive the regex on the other end',
() => expect(sut.run({timeout: 4000})).to.eventually.have.property('result', TestResult.Complete));

});

describe('when test runner behind responds quickly', () => {
before(() => {
sut = new TestRunnerChildProcessAdapter('direct-resolved', options);
});

it('should run and resolve', () =>
expect(sut.run({ timeout: 4000 })).to.eventually.satisfy((result: RunResult) => result.result === TestResult.Complete));
expect(sut.run({ timeout: 4000 })).to.eventually.have.property('result', TestResult.Complete));

});

Expand Down
5 changes: 3 additions & 2 deletions test/unit/isolated-runner/IsolatedTestRunnerAdapterSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Message, {MessageType} from '../../../src/isolated-runner/Message';
import ResultMessageBody from '../../../src/isolated-runner/ResultMessageBody';
import RunMessageBody from '../../../src/isolated-runner/RunMessageBody';
import StartMessageBody from '../../../src/isolated-runner/StartMessageBody';
import {serialize} from '../../../src/utils/objectUtils';
import {expect} from 'chai';
import * as path from 'path';
import * as _ from 'lodash';
Expand Down Expand Up @@ -61,7 +62,7 @@ describe('IsolatedTestRunnerAdapter', () => {
type: MessageType.Run,
body: { runOptions }
}
expect(fakeChildProcess.send).to.have.been.calledWith(expectedMessage)
expect(fakeChildProcess.send).to.have.been.calledWith(serialize(expectedMessage));
});

describe('and a timeout occurred', () => {
Expand All @@ -70,7 +71,7 @@ describe('IsolatedTestRunnerAdapter', () => {
clock.tick(2100);
});

it('should send `dispose` to worker process', () => expect(fakeChildProcess.send).to.have.been.calledWith({ type: MessageType.Dispose }));
it('should send `dispose` to worker process', () => expect(fakeChildProcess.send).to.have.been.calledWith(serialize({ type: MessageType.Dispose })));

let actAssertTimeout = () => {
it('should kill the child process and start a new one', () => {
Expand Down

0 comments on commit 51b6903

Please sign in to comment.