Skip to content

Commit

Permalink
fix(dispose): clean up child processes in alternative flows (#1520)
Browse files Browse the repository at this point in the history
Make sure Stryker doesn't _hang indefinitely_ when it supposed to _exit prematurely_.
Before, there was a code path that didn't dispose all child processes.
Now using typed-injects [dispose functionality](https://github.com/nicojs/typed-inject/#-disposing-provided-stuff).
  • Loading branch information
nicojs committed Apr 24, 2019
1 parent 3c83bbe commit 31ee085
Show file tree
Hide file tree
Showing 14 changed files with 113 additions and 35 deletions.
5 changes: 5 additions & 0 deletions e2e/test/exit-prematurely/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"presets": [
"@babel/env"
]
}
5 changes: 5 additions & 0 deletions e2e/test/exit-prematurely/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions e2e/test/exit-prematurely/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "exit-prematurely",
"version": "1.0.0",
"private": true,
"description": "A module to test the alternative flow when Stryker should exit prematurely, see https://github.com/stryker-mutator/stryker/issues/1519",
"main": "index.js",
"scripts": {
"pretest": "rimraf \"reports\" stryker.log .stryker-tmp ",
"test": "stryker run",
"posttest": "mocha --require ts-node/register verify/*.ts"
},
"keywords": [],
"author": "",
"license": "ISC"
}
19 changes: 19 additions & 0 deletions e2e/test/exit-prematurely/stryker.conf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module.exports = function (config) {
config.set({
mutate: [
'src/*.js'
],
testFramework: 'mocha',
testRunner: 'mocha',
coverageAnalysis: 'off',
mutator: 'javascript',
transpilers: [
'babel'
],
timeoutMS: 60000,
reporters: ['clear-text', 'html', 'event-recorder'],
maxConcurrentTestRunners: 2,
logLevel: 'info',
fileLogLevel: 'info'
});
};
1 change: 1 addition & 0 deletions e2e/test/exit-prematurely/test/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// Idle
9 changes: 9 additions & 0 deletions e2e/test/exit-prematurely/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"declaration": false
},
"files": [
"verify/verify.ts"
]
}
19 changes: 19 additions & 0 deletions e2e/test/exit-prematurely/verify/verify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as fs from 'fs';
import { expect } from 'chai';

describe('Verify stryker has ran correctly', () => {

const strykerLog = fs.readFileSync('./stryker.log', 'utf8');

it('exit prematurely', async () => {
expect(strykerLog).contains('No tests were executed. Stryker will exit prematurely.');
});

it('should log about a mutant free world', async () => {
expect(strykerLog).contains('It\'s a mutant-free world, nothing to test');
});

it('should warn about the globbing expression resulting in no files', () => {
expect(strykerLog).contains('Globbing expression "src/*.js" did not result in any files.');
});
});
9 changes: 5 additions & 4 deletions packages/core/src/SandboxPool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ import { InitialTestRunResult } from './process/InitialTestExecutor';
import { Logger } from '@stryker-mutator/api/logging';
import TranspiledMutant from './TranspiledMutant';
import { MutantResult } from '@stryker-mutator/api/report';
import { Disposable } from 'typed-inject';

const MAX_CONCURRENT_INITIALIZING_SANDBOXES = 2;

export class SandboxPool {
export class SandboxPool implements Disposable {

private readonly allSandboxes: Promise<Sandbox>[] = [];
private readonly overheadTimeMS: number;
Expand Down Expand Up @@ -84,11 +85,11 @@ export class SandboxPool {
}

private readonly registerSandbox = async (promisedSandbox: Promise<Sandbox>): Promise<Sandbox> => {
this.allSandboxes.push(promisedSandbox);
return promisedSandbox;
this.allSandboxes.push(promisedSandbox);
return promisedSandbox;
}

public async disposeAll() {
public async dispose() {
const sandboxes = await Promise.all(this.allSandboxes);
return Promise.all(sandboxes.map(sandbox => sandbox.dispose()));
}
Expand Down
25 changes: 16 additions & 9 deletions packages/core/src/Stryker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,16 +79,23 @@ export default class Stryker {
const testableMutants = await mutationTestProcessInjector
.injectClass(MutantTestMatcher)
.matchWithMutants(mutator.mutate(inputFiles.filesToMutate));
if (initialRunResult.runResult.tests.length && testableMutants.length) {
const mutationTestExecutor = mutationTestProcessInjector.injectClass(MutationTestExecutor);
const mutantResults = await mutationTestExecutor.run(testableMutants);
await this.reportScore(mutantResults, inputFileInjector);
await TempFolder.instance().clean();
await this.logDone();
try {
if (initialRunResult.runResult.tests.length && testableMutants.length) {
const mutationTestExecutor = mutationTestProcessInjector
.injectClass(MutationTestExecutor);
const mutantResults = await mutationTestExecutor.run(testableMutants);
await this.reportScore(mutantResults, inputFileInjector);
await TempFolder.instance().clean();
await this.logDone();
return mutantResults;
} else {
this.logRemark();
}
} finally {
// `injector.dispose` calls `dispose` on all created instances
// Namely the `SandboxPool` and the `ChildProcessProxy` instances
mutationTestProcessInjector.dispose();
await LogConfigurator.shutdown();
return mutantResults;
} else {
this.logRemark();
}
}
return Promise.resolve([]);
Expand Down
3 changes: 0 additions & 3 deletions packages/core/src/process/MutationTestExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,6 @@ export class MutationTestExecutor {
tap(this.reportAll)
).toPromise();

// TODO: Let typed inject dispose of sandbox pool
await this.sandboxPool.disposeAll();
await this.mutantTranspileScheduler.dispose();
return results;
}

Expand Down
6 changes: 2 additions & 4 deletions packages/core/src/transpiler/MutantTranspileScheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { BehaviorSubject } from 'rxjs';

const INITIAL_CONCURRENCY = 100;

export class MutantTranspileScheduler {
export class MutantTranspileScheduler implements Disposable {

private currentMutatedFile: SourceFile;
private readonly concurrencyTicket$ = new BehaviorSubject<number>(INITIAL_CONCURRENCY);
Expand Down Expand Up @@ -44,12 +44,10 @@ export class MutantTranspileScheduler {
}

/**
* Dispose all
* Dispose
*/
public dispose() {
this.concurrencyTicket$.complete();
// TODO: Let typed-inject dispose this one
this.transpiler.dispose();
}

private createTranspiledMutant(mutant: TestableMutant, transpileResult: TranspileResult) {
Expand Down
8 changes: 4 additions & 4 deletions packages/core/test/unit/SandboxPool.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,18 +178,18 @@ describe(SandboxPool.name, () => {
await expect(actRunMutants()).rejectedWith(expectedError);
});
});
describe('disposeAll', () => {
describe('dispose', () => {
it('should have disposed all sandboxes', async () => {
sut = createSut();
await actRunMutants();
await sut.disposeAll();
await sut.dispose();
expect(firstSandbox.dispose).called;
expect(secondSandbox.dispose).called;
});

it('should not do anything if no sandboxes were created', async () => {
sut = createSut();
await sut.disposeAll();
await sut.dispose();
expect(firstSandbox.dispose).not.called;
expect(secondSandbox.dispose).not.called;
});
Expand All @@ -211,7 +211,7 @@ describe(SandboxPool.name, () => {
const runPromise = sut.runMutants(from(inputMutants)).toPromise();
task.resolve(firstSandbox as unknown as Sandbox);
await runPromise;
const disposePromise = sut.disposeAll();
const disposePromise = sut.dispose();
task2.resolve(secondSandbox as unknown as Sandbox);
await disposePromise;
expect(secondSandbox.dispose).called;
Expand Down
12 changes: 12 additions & 0 deletions packages/core/test/unit/Stryker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,11 @@ describe(Stryker.name, () => {
await sut.runMutationTest();
expect(logMock.info).to.have.been.calledWith('Trouble figuring out what went wrong? Try `npx stryker run --fileLogLevel trace --logLevel debug` to get some more info.');
});

it('should dispose the injector', async () => {
await sut.runMutationTest();
expect(injectorMock.dispose).called;
});
});

describe('happy flow', () => {
Expand Down Expand Up @@ -263,10 +268,17 @@ describe(Stryker.name, () => {
expect(reporterMock.wrapUp).to.have.been.called;
});

it('should dispose the injector', async () => {
sut = new Stryker({});
await sut.runMutationTest();
expect(injectorMock.dispose).called;
});

it('should shutdown the log4js server', async () => {
sut = new Stryker({});
await sut.runMutationTest();
expect(shutdownLoggingStub).called;
expect(shutdownLoggingStub).calledAfter(injectorMock.dispose);
});

it('should create the transpiler with produceSourceMaps = true when coverage analysis is enabled', async () => {
Expand Down
12 changes: 1 addition & 11 deletions packages/core/test/unit/process/MutationTestExecutor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ describe(MutationTestExecutor.name, () => {
sandboxPoolMock = mock(SandboxPool);
mutantTranspileSchedulerMock = mock(MutantTranspileScheduler);
mutantTranspileSchedulerMock.scheduleNext = sinon.stub();
sandboxPoolMock.disposeAll.resolves();
sandboxPoolMock.dispose.resolves();
reporter = mock(BroadcastReporter);
inputFiles = new InputFileCollection([new File('input.ts', '')], []);
mutants = [testableMutant()];
Expand All @@ -56,16 +56,6 @@ describe(MutationTestExecutor.name, () => {

describe('run', () => {

it('should dispose all sandboxes afterwards', async () => {
await sut.run(mutants);
expect(sandboxPoolMock.disposeAll).called;
});

it('should dispose the mutantTranspiler', async () => {
await sut.run(mutants);
expect(mutantTranspileSchedulerMock.dispose).called;
});

it('should have ran the mutants in the sandbox pool', async () => {
await sut.run(mutants);
expect(mutantTranspileSchedulerMock.scheduleTranspileMutants).calledWith(mutants);
Expand Down

0 comments on commit 31ee085

Please sign in to comment.