Skip to content

Commit

Permalink
feat(sandbox): Change cwd in Sandboxes (#187)
Browse files Browse the repository at this point in the history
* feat(sandbox): Change cwd in `Sandbox`es

Change the current working directory for all isolated sandboxes. This is needed to remove unwanted side effects during test runs. For example RequireJS wasn't able to find files in combination with karma.
* Add new interface `IsolatedRunnerOptions`, which extends `RunnerOptions` with the addition of `SandboxWorkingFolder`
* Change working directory as soon as the IsoltatedTestRunnerAdapterWorker has loaded the plugins as they can be relative to the parent working directory.

* fix(typescript): Set back typescript version
  • Loading branch information
nicojs authored and simondel committed Dec 15, 2016
1 parent 3a34fe1 commit 28e1e5d
Show file tree
Hide file tree
Showing 10 changed files with 82 additions and 29 deletions.
8 changes: 5 additions & 3 deletions src/Sandbox.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import * as path from 'path';
import * as log4js from 'log4js';
import * as _ from 'lodash';
import { RunnerOptions, RunResult, StatementMap } from 'stryker-api/test_runner';
import { RunResult, StatementMap } from 'stryker-api/test_runner';
import { InputFile, StrykerOptions } from 'stryker-api/core';
import { TestFramework } from 'stryker-api/test_framework';
import { wrapInClosure } from './utils/objectUtils';
import IsolatedTestRunnerAdapterFactory from './isolated-runner/IsolatedTestRunnerAdapterFactory';
import IsolatedTestRunnerAdapter from './isolated-runner/IsolatedTestRunnerAdapter';
import IsolatedRunnerOptions from './isolated-runner/IsolatedRunnerOptions';
import StrykerTempFolder from './utils/StrykerTempFolder';
import Mutant from './Mutant';
import CoverageInstrumenter from './coverage/CoverageInstrumenter';
Expand Down Expand Up @@ -76,10 +77,11 @@ export default class Sandbox {
private initializeTestRunner(): void | Promise<any> {
let files = this.files.map(originalFile => <InputFile>_.assign(_.cloneDeep(originalFile), { path: this.fileMap[originalFile.path] }));
files.unshift({ path: this.testHooksFile, mutated: false, included: true });
let settings: RunnerOptions = {
let settings: IsolatedRunnerOptions = {
files,
strykerOptions: this.options,
port: this.options.port + this.index
port: this.options.port + this.index,
sandboxWorkingFolder: this.workingFolder
};
log.debug(`Creating test runner %s using settings {port: %s}`, this.index, settings.port);
this.testRunner = IsolatedTestRunnerAdapterFactory.create(settings);
Expand Down
7 changes: 7 additions & 0 deletions src/isolated-runner/IsolatedRunnerOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { RunnerOptions } from 'stryker-api/test_runner';

interface IsolatedRunnerOptions extends RunnerOptions {
sandboxWorkingFolder: string;
}

export default IsolatedRunnerOptions;
15 changes: 8 additions & 7 deletions src/isolated-runner/IsolatedTestRunnerAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { TestRunner, RunResult, RunOptions, RunnerOptions, TestResult, RunStatus } from 'stryker-api/test_runner';
import { StrykerOptions } from 'stryker-api/core';
import { fork, ChildProcess } from 'child_process';
import { AdapterMessage, WorkerMessage } from './MessageProtocol';
import * as _ from 'lodash';
import * as log4js from 'log4js';
import { EventEmitter } from 'events';
import * as log4js from 'log4js';
import * as _ from 'lodash';
import { fork, ChildProcess } from 'child_process';
import { TestRunner, RunResult, RunOptions, TestResult, RunStatus } from 'stryker-api/test_runner';
import { StrykerOptions } from 'stryker-api/core';
import { serialize } from '../utils/objectUtils';
import { AdapterMessage, WorkerMessage } from './MessageProtocol';
import IsolatedRunnerOptions from './IsolatedRunnerOptions';

const log = log4js.getLogger('IsolatedTestRunnerAdapter');
const MAX_WAIT_FOR_DISPOSE = 2000;
Expand All @@ -27,7 +28,7 @@ export default class TestRunnerChildProcessAdapter extends EventEmitter implemen
private currentRunStartedTimestamp: Date;
private isDisposing: boolean;

constructor(private realTestRunnerName: string, private options: RunnerOptions) {
constructor(private realTestRunnerName: string, private options: IsolatedRunnerOptions) {
super();
this.startWorker();
}
Expand Down
7 changes: 5 additions & 2 deletions src/isolated-runner/IsolatedTestRunnerAdapterFactory.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { TestRunnerFactory, TestRunner, RunnerOptions } from 'stryker-api/test_runner';
import { TestRunnerFactory, TestRunner } from 'stryker-api/test_runner';
import IsolatedTestRunnerAdapter from './IsolatedTestRunnerAdapter';
import IsolatedRunnerOptions from './IsolatedRunnerOptions';



export default {
create(settings: RunnerOptions): IsolatedTestRunnerAdapter {
create(settings: IsolatedRunnerOptions): IsolatedTestRunnerAdapter {
return new IsolatedTestRunnerAdapter(settings.strykerOptions.testRunner, settings);
}
};
4 changes: 3 additions & 1 deletion src/isolated-runner/IsolatedTestRunnerAdapterWorker.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AdapterMessage, RunMessage, StartMessage, ResultMessage, EmptyWorkerMessage, WorkerMessage } from './MessageProtocol';
import { RunnerOptions, TestRunner, RunStatus, TestRunnerFactory, RunResult } from 'stryker-api/test_runner';
import { TestRunner, RunStatus, TestRunnerFactory, RunResult } from 'stryker-api/test_runner';
import PluginLoader from '../PluginLoader';
import * as log4js from 'log4js';
import { isPromise, deserialize } from '../utils/objectUtils';
Expand Down Expand Up @@ -42,6 +42,8 @@ class IsolatedTestRunnerAdapterWorker {

start(message: StartMessage) {
this.loadPlugins(message.runnerOptions.strykerOptions.plugins);
log.debug(`Changing current working directory for this process to ${message.runnerOptions.sandboxWorkingFolder}`);
process.chdir(message.runnerOptions.sandboxWorkingFolder);
this.underlyingTestRunner = TestRunnerFactory.instance().create(message.runnerName, message.runnerOptions);
}

Expand Down
3 changes: 2 additions & 1 deletion src/isolated-runner/MessageProtocol.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { RunResult, RunnerOptions } from 'stryker-api/test_runner';
import { RunOptions } from 'stryker-api/test_runner';
import IsolatedRunnerOptions from './IsolatedRunnerOptions';

export type AdapterMessage = RunMessage | StartMessage | EmptyAdapterMessage;
export type WorkerMessage = ResultMessage | EmptyWorkerMessage;
Expand All @@ -17,7 +18,7 @@ export interface RunMessage {
export interface StartMessage {
kind: 'start';
runnerName: string;
runnerOptions: RunnerOptions;
runnerOptions: IsolatedRunnerOptions;
}

export interface EmptyAdapterMessage {
Expand Down
28 changes: 22 additions & 6 deletions test/integration/isolated-runner/IsolatedTestRunnerAdapterSpec.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,33 @@
import TestRunnerChildProcessAdapter from '../../../src/isolated-runner/IsolatedTestRunnerAdapter';
import { TestRunnerFactory, TestRunner, RunOptions, RunResult, TestStatus, RunnerOptions, RunStatus } from 'stryker-api/test_runner';
import { StrykerOptions } from 'stryker-api/core';
import * as path from 'path';
import { expect } from 'chai';
import logger from '../../helpers/log4jsMock';
import { TestRunnerFactory, TestRunner, RunOptions, RunResult, TestStatus, RunStatus } from 'stryker-api/test_runner';
import { StrykerOptions } from 'stryker-api/core';
import TestRunnerChildProcessAdapter from '../../../src/isolated-runner/IsolatedTestRunnerAdapter';
import IsolatedRunnerOptions from '../../../src/isolated-runner/IsolatedRunnerOptions';

describe('TestRunnerChildProcessAdapter', function () {

this.timeout(10000);

let sut: TestRunnerChildProcessAdapter;
let options: RunnerOptions = {
let options: IsolatedRunnerOptions = {
strykerOptions: {
plugins: [
'../../test/integration/isolated-runner/DirectResolvedTestRunner',
'../../test/integration/isolated-runner/NeverResolvedTestRunner',
'../../test/integration/isolated-runner/SlowInitAndDisposeTestRunner',
'../../test/integration/isolated-runner/CoverageReportingTestRunner',
'../../test/integration/isolated-runner/ErroredTestRunner',
'../../test/integration/isolated-runner/VerifyWorkingFolderTestRunner',
'../../test/integration/isolated-runner/DiscoverRegexTestRunner'],
testRunner: 'karma',
testFramework: 'jasmine',
port: null,
'someRegex': /someRegex/
},
files: [],
port: null
port: null,
sandboxWorkingFolder: path.resolve('./test/integration/isolated-runner')
};

describe('when sending a regex in the options', () => {
Expand Down Expand Up @@ -108,4 +111,17 @@ describe('TestRunnerChildProcessAdapter', function () {
after(() => sut.dispose());
});

describe('when test runner verifies the current working folder', () => {
before(() => {
sut = new TestRunnerChildProcessAdapter('verify-working-folder', options);
return sut.init();
});

it('should run and resolve', () => sut.run({ timeout: 4000 })
.then(result => {
if (result.errorMessages && result.errorMessages.length) {
expect.fail(null, null, result.errorMessages[0]);
}
}));
});
});
16 changes: 16 additions & 0 deletions test/integration/isolated-runner/VerifyWorkingFolderTestRunner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { EventEmitter } from 'events';
import { RunResult, RunStatus, RunOptions, TestRunner, TestRunnerFactory } from 'stryker-api/test_runner';
class VerifyWorkingFolderTestRunner extends EventEmitter implements TestRunner {

runResult: RunResult = { status: RunStatus.Complete, tests: [] };

run(options: RunOptions) {
if (process.cwd() === __dirname) {
return Promise.resolve(this.runResult);
} else {
return Promise.reject(new Error(`Expected ${process.cwd()} to be ${__dirname}`));
}
}
}

TestRunnerFactory.instance().register('verify-working-folder', VerifyWorkingFolderTestRunner);
17 changes: 10 additions & 7 deletions test/unit/SandboxSpec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { expect } from 'chai';
import * as sinon from 'sinon';
import * as path from 'path';
import * as os from 'os';
import { expect } from 'chai';
import { StrykerOptions, InputFile } from 'stryker-api/core';
import { RunnerOptions, RunResult } from 'stryker-api/test_runner';
import { RunResult } from 'stryker-api/test_runner';
import { wrapInClosure } from '../../src/utils/objectUtils';
import CoverageInstrumenter from '../../src/coverage/CoverageInstrumenter';
import Sandbox from '../../src/Sandbox';
import StrykerTempFolder from '../../src/utils/StrykerTempFolder';
import { wrapInClosure } from '../../src/utils/objectUtils';
import IsolatedTestRunnerAdapterFactory from '../../src/isolated-runner/IsolatedTestRunnerAdapterFactory';
import IsolatedRunnerOptions from '../../src/isolated-runner/IsolatedRunnerOptions';

describe('Sandbox', () => {
let sut: Sandbox;
Expand Down Expand Up @@ -87,14 +88,15 @@ describe('Sandbox', () => {
.and.calledWith(files[1].path, path.join(workingFolder, 'file2')));

it('should have created the isolated test runner inc framework hook', () => {
const expectedSettings: RunnerOptions = {
const expectedSettings: IsolatedRunnerOptions = {
files: [
{ path: expectedTestFrameworkHooksFile, mutated: false, included: true },
{ path: expectedTargetFileToMutate, mutated: true, included: true },
{ path: path.join(workingFolder, 'file2'), mutated: false, included: false }
],
port: 46,
strykerOptions: options
strykerOptions: options,
sandboxWorkingFolder: workingFolder
};
expect(IsolatedTestRunnerAdapterFactory.create).to.have.been.calledWith(expectedSettings);
});
Expand Down Expand Up @@ -149,14 +151,15 @@ describe('Sandbox', () => {
beforeEach(() => sut.initialize());

it('should have created the isolated test runner', () => {
const expectedSettings: RunnerOptions = {
const expectedSettings: IsolatedRunnerOptions = {
files: [
{ path: path.join(workingFolder, '___testHooksForStryker.js'), mutated: false, included: true },
{ path: path.join(workingFolder, 'file1'), mutated: true, included: true },
{ path: path.join(workingFolder, 'file2'), mutated: false, included: false }
],
port: 46,
strykerOptions: options
strykerOptions: options,
sandboxWorkingFolder: workingFolder
};
expect(IsolatedTestRunnerAdapterFactory.create).to.have.been.calledWith(expectedSettings);
});
Expand Down
6 changes: 4 additions & 2 deletions test/unit/isolated-runner/IsolatedTestRunnerAdapterSpec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as child_process from 'child_process';
import * as sinon from 'sinon';
import { RunnerOptions, RunOptions, RunResult, RunStatus } from 'stryker-api/test_runner';
import { RunOptions, RunResult, RunStatus } from 'stryker-api/test_runner';
import IsolatedTestRunnerAdapter from '../../../src/isolated-runner/IsolatedTestRunnerAdapter';
import IsolatedRunnerOptions from '../../../src/isolated-runner/IsolatedRunnerOptions';
import { WorkerMessage, AdapterMessage, RunMessage, ResultMessage } from '../../../src/isolated-runner/MessageProtocol';
import { serialize } from '../../../src/utils/objectUtils';
import { expect } from 'chai';
Expand All @@ -13,12 +14,13 @@ describe('IsolatedTestRunnerAdapter', () => {
let sinonSandbox: sinon.SinonSandbox;
let clock: sinon.SinonFakeTimers;
let fakeChildProcess: any;
let runnerOptions: RunnerOptions;
let runnerOptions: IsolatedRunnerOptions;

beforeEach(() => {
runnerOptions = {
port: 42,
files: [],
sandboxWorkingFolder: 'a working directory',
strykerOptions: null
};
sinonSandbox = sinon.sandbox.create();
Expand Down

0 comments on commit 28e1e5d

Please sign in to comment.