Skip to content

Commit

Permalink
feat(transpiler api): Async transpiler plugin support (#433)
Browse files Browse the repository at this point in the history
Make the `Transpiler` interface an async interface. This is useful to support transpilers with a strictly async api (i.e. webpack)
  • Loading branch information
Sander Koenders authored and nicojs committed Oct 24, 2017
1 parent b9231fe commit 794e587
Show file tree
Hide file tree
Showing 12 changed files with 63 additions and 63 deletions.
2 changes: 1 addition & 1 deletion packages/stryker-api/src/transpile/Transpiler.ts
Expand Up @@ -32,7 +32,7 @@ export default interface Transpiler {
*
* @returns an error message (if transpiling failed) or the output files to be used in the next transpiler
*/
transpile(files: File[]): TranspileResult;
transpile(files: File[]): Promise<TranspileResult>;

/**
* Retrieve the location of a source location in the transpiled file.
Expand Down
19 changes: 10 additions & 9 deletions packages/stryker-api/testResources/module/useTranspile.ts
Expand Up @@ -6,11 +6,11 @@ class MyTranspiler implements Transpiler {

constructor(private transpilerOptions: TranspilerOptions) { }

transpile(files: File[]): TranspileResult {
return {
outputFiles: [{ name: 'foo', content: 'string', kind: FileKind.Text, mutated: this.transpilerOptions.keepSourceMaps, included: false, transpiled: true }],
transpile(files: File[]): Promise<TranspileResult> {
return Promise.resolve({
outputFiles: [{ name: 'foo', content: 'string', kind: FileKind.Text, mutated: this.transpilerOptions.keepSourceMaps, included: false, transpiled: true } as File],
error: null
};
});
}
getMappedLocation(sourceFileLocation: FileLocation): FileLocation {
return sourceFileLocation;
Expand All @@ -20,9 +20,10 @@ class MyTranspiler implements Transpiler {
TranspilerFactory.instance().register('my-transpiler', MyTranspiler);
const transpiler = TranspilerFactory.instance().create('my-transpiler', { keepSourceMaps: true, config: new Config() });

const transpileResult = transpiler.transpile([{ kind: FileKind.Text, content: '', name: '', mutated: true, included: false, transpiled: true }]);
console.log(JSON.stringify(transpileResult));
transpiler.transpile([{ kind: FileKind.Text, content: '', name: '', mutated: true, included: false, transpiled: true }]).then((transpileResult) => {
console.log(JSON.stringify(transpileResult));

console.log(JSON.stringify(
transpiler.getMappedLocation({ fileName: 'my-file', start: { line: 1, column: 2 }, end: { line: 3, column: 4 } })
));
console.log(JSON.stringify(
transpiler.getMappedLocation({ fileName: 'my-file', start: { line: 1, column: 2 }, end: { line: 3, column: 4 } })
));
});
4 changes: 2 additions & 2 deletions packages/stryker-typescript/src/TypescriptTranspiler.ts
Expand Up @@ -17,15 +17,15 @@ export default class TypescriptTranspiler implements Transpiler {
this.keepSourceMaps = options.keepSourceMaps;
}

transpile(files: File[]): TranspileResult {
transpile(files: File[]): Promise<TranspileResult> {
const typescriptFiles = filterOutTypescriptFiles(files);
if (!this.languageService) {
this.languageService = new TranspilingLanguageService(
getCompilerOptions(this.config), typescriptFiles, getProjectDirectory(this.config), this.keepSourceMaps);
} else {
this.languageService.replace(typescriptFiles);
}
return this.transpileAndResult(typescriptFiles, files);
return Promise.resolve(this.transpileAndResult(typescriptFiles, files));
}

getMappedLocation(sourceFileLocation: FileLocation): FileLocation {
Expand Down
12 changes: 6 additions & 6 deletions packages/stryker-typescript/test/integration/ownDogFoodSpec.ts
Expand Up @@ -32,27 +32,27 @@ describe('stryker-typescript', function () {
setGlobalLogLevel('trace');
});

it('should be able to transpile itself', () => {
it('should be able to transpile itself', async () => {
const transpiler = new TypescriptTranspiler({ config, keepSourceMaps: true });
const transpileResult = transpiler.transpile(inputFiles);
const transpileResult = await transpiler.transpile(inputFiles);
expect(transpileResult.error).to.be.null;
const outputFiles = transpileResult.outputFiles;
expect(outputFiles.length).greaterThan(10);
});

it('should result in an error if a variable is declared as any and noImplicitAny = true', () => {
it('should result in an error if a variable is declared as any and noImplicitAny = true', async () => {
const transpiler = new TypescriptTranspiler({ config, keepSourceMaps: true });
inputFiles[0].content += 'function foo(bar) { return bar; } ';
const transpileResult = transpiler.transpile(inputFiles);
const transpileResult = await transpiler.transpile(inputFiles);
expect(transpileResult.error).contains('error TS7006: Parameter \'bar\' implicitly has an \'any\' type');
expect(transpileResult.outputFiles.length).eq(0);
});

it('should not result in an error if a variable is declared as any and noImplicitAny = false', () => {
it('should not result in an error if a variable is declared as any and noImplicitAny = false', async () => {
config['tsconfig'].noImplicitAny = false;
inputFiles[0].content += 'const shouldResultInError = 3';
const transpiler = new TypescriptTranspiler({ config, keepSourceMaps: true });
const transpileResult = transpiler.transpile(inputFiles);
const transpileResult = await transpiler.transpile(inputFiles);
expect(transpileResult.error).null;
});
});
10 changes: 5 additions & 5 deletions packages/stryker-typescript/test/integration/sampleSpec.ts
Expand Up @@ -44,24 +44,24 @@ describe('Sample integration', function () {
expect(mutants.length).to.eq(4);
});

it('should be able to transpile source code', () => {
it('should be able to transpile source code', async () => {
const transpiler = new TypescriptTranspiler({ config, keepSourceMaps: true });
const transpileResult = transpiler.transpile(inputFiles);
const transpileResult = await transpiler.transpile(inputFiles);
expect(transpileResult.error).to.be.null;
const outputFiles = transpileResult.outputFiles;
expect(outputFiles.length).to.eq(2);
});

it('should be able to mutate transpiled code', () => {
it('should be able to mutate transpiled code', async () => {
// Transpile mutants
const mutator = new TypescriptMutator(config);
const mutants = mutator.mutate(inputFiles);
const transpiler = new TypescriptTranspiler({ config, keepSourceMaps: true });
transpiler.transpile(inputFiles);
const mathDotTS = inputFiles.filter(file => file.name.endsWith('math.ts'))[0];
const [firstBinaryMutant, stringSubtractMutant] = mutants.filter(m => m.mutatorName === 'BinaryExpression');
const correctResult = transpiler.transpile([mutateFile(mathDotTS, firstBinaryMutant)]);
const errorResult = transpiler.transpile([mutateFile(mathDotTS, stringSubtractMutant)]);
const correctResult = await transpiler.transpile([mutateFile(mathDotTS, firstBinaryMutant)]);
const errorResult = await transpiler.transpile([mutateFile(mathDotTS, stringSubtractMutant)]);
expect(correctResult.error).null;
expect(correctResult.outputFiles).lengthOf(1);
expect(path.resolve(correctResult.outputFiles[0].name)).eq(path.resolve(path.dirname(mathDotTS.name), 'math.js'));
Expand Down
Expand Up @@ -60,8 +60,8 @@ describe('TypescriptTranspiler', () => {
sut = new TypescriptTranspiler({ config, keepSourceMaps: true });
});

it('should transpile given files', () => {
const result = sut.transpile([
it('should transpile given files', async () => {
const result = await sut.transpile([
textFile({ name: 'foo.ts', transpiled: true }),
textFile({ name: 'bar.ts', transpiled: true })
]);
Expand All @@ -72,15 +72,15 @@ describe('TypescriptTranspiler', () => {
]);
});

it('should keep file order', () => {
it('should keep file order', async () => {
const input = [
textFile({ name: 'file1.js', transpiled: false }),
textFile({ name: 'file2.ts', transpiled: true }),
binaryFile({ name: 'file3.bin', transpiled: true }),
textFile({ name: 'file4.ts', transpiled: true }),
textFile({ name: 'file5.d.ts', transpiled: true })
];
const result = sut.transpile(input);
const result = await sut.transpile(input);
expect(result.error).eq(null);
expect(result.outputFiles).deep.eq([
textFile({ name: 'file1.js', transpiled: false }),
Expand All @@ -91,7 +91,7 @@ describe('TypescriptTranspiler', () => {
]);
});

it('should keep order if single output result file', () => {
it('should keep order if single output result file', async () => {
singleFileOutputEnabled = true;
const input = [
textFile({ name: 'file1.ts', transpiled: false }),
Expand All @@ -100,7 +100,7 @@ describe('TypescriptTranspiler', () => {
textFile({ name: 'file4.ts', transpiled: true }),
textFile({ name: 'file5.ts', transpiled: false })
];
const output = sut.transpile(input);
const output = await sut.transpile(input);
expect(output.error).eq(null);
expect(output.outputFiles).deep.eq([
textFile({ name: 'file1.ts', transpiled: false }),
Expand All @@ -126,10 +126,10 @@ describe('TypescriptTranspiler', () => {
]);
});

it('should return errors when there are diagnostic messages', () => {
it('should return errors when there are diagnostic messages', async () => {
languageService.getSemanticDiagnostics.returns('foobar');
const input = [textFile({ name: 'file1.ts' }), textFile({ name: 'file2.ts' })];
const result = sut.transpile(input);
const result = await sut.transpile(input);
expect(result.error).eq('foobar');
expect(result.outputFiles).lengthOf(0);
});
Expand Down
Empty file modified packages/stryker/bin/stryker 100644 → 100755
Empty file.
5 changes: 2 additions & 3 deletions packages/stryker/src/process/InitialTestExecutor.ts
Expand Up @@ -43,7 +43,7 @@ export default class InitialTestExecutor {
private async initialRunInSandbox(): Promise<InitialTestRunResult> {
const coverageInstrumenterTranspiler = this.createCoverageInstrumenterTranspiler();
const transpilerFacade = this.createTranspilerFacade(coverageInstrumenterTranspiler);
const transpileResult = transpilerFacade.transpile(this.files);
const transpileResult = await transpilerFacade.transpile(this.files);
if (transpileResult.error) {
throw new Error(`Could not transpile input files: ${transpileResult.error}`);
} else {
Expand Down Expand Up @@ -98,11 +98,10 @@ export default class InitialTestExecutor {
*/
private createTranspilerFacade(coverageInstrumenterTranspiler: CoverageInstrumenterTranspiler): Transpiler {
const transpilerSettings: TranspilerOptions = { config: this.options, keepSourceMaps: true };
const transpiler = new TranspilerFacade(transpilerSettings, {
return new TranspilerFacade(transpilerSettings, {
name: CoverageInstrumenterTranspiler.name,
transpiler: coverageInstrumenterTranspiler
});
return transpiler;
}

private createCoverageInstrumenterTranspiler() {
Expand Down
Expand Up @@ -24,19 +24,19 @@ export default class CoverageInstrumenterTranspiler implements Transpiler {
this.log = getLogger(CoverageInstrumenterTranspiler.name);
}

transpile(files: File[]): TranspileResult {
public transpile(files: File[]): Promise<TranspileResult> {
try {
const result: TranspileResult = {
outputFiles: files.map(file => this.instrumentFileIfNeeded(file)),
error: null
};
return this.addCollectCoverageFileIfNeeded(result);
return Promise.resolve(this.addCollectCoverageFileIfNeeded(result));
} catch (error) {
return this.errorResult(errorToString(error));
return Promise.resolve(this.errorResult(errorToString(error)));
}
}

getMappedLocation(sourceFileLocation: FileLocation): FileLocation {
public getMappedLocation(sourceFileLocation: FileLocation): FileLocation {
return sourceFileLocation;
}

Expand Down
10 changes: 5 additions & 5 deletions packages/stryker/src/transpiler/TranspilerFacade.ts
Expand Up @@ -17,11 +17,11 @@ export default class TranspilerFacade implements Transpiler {
}
}

transpile(files: File[]): TranspileResult {
public transpile(files: File[]): Promise<TranspileResult> {
return this.performTranspileChain(this.createPassThruTranspileResult(files));
}

getMappedLocation(sourceFileLocation: FileLocation): FileLocation {
public getMappedLocation(sourceFileLocation: FileLocation): FileLocation {
return this.performMappedLocationChain(sourceFileLocation);
}

Expand All @@ -37,13 +37,13 @@ export default class TranspilerFacade implements Transpiler {
}
}

private performTranspileChain(
private async performTranspileChain(
currentResult: TranspileResult,
remainingChain: NamedTranspiler[] = this.innerTranspilers.slice()
): TranspileResult {
): Promise<TranspileResult> {
const next = remainingChain.shift();
if (next) {
const nextResult = next.transpiler.transpile(currentResult.outputFiles);
const nextResult = await next.transpiler.transpile(currentResult.outputFiles);
if (nextResult.error) {
nextResult.error = `Execute ${next.name}: ${nextResult.error}`;
return nextResult;
Expand Down
Expand Up @@ -12,11 +12,11 @@ describe('CoverageInstrumenterTranspiler', () => {
config = new Config();
});

it('should not instrument any code when coverage analysis is off', () => {
it('should not instrument any code when coverage analysis is off', async () => {
sut = new CoverageInstrumenterTranspiler({ config, keepSourceMaps: false }, null);
config.coverageAnalysis = 'off';
const input = [textFile({ mutated: true }), binaryFile({ mutated: true }), webFile({ mutated: true })];
const output = sut.transpile(input);
const output = await sut.transpile(input);
expect(output.error).null;
expect(output.outputFiles).deep.eq(input);
});
Expand All @@ -28,14 +28,14 @@ describe('CoverageInstrumenterTranspiler', () => {
sut = new CoverageInstrumenterTranspiler({ config, keepSourceMaps: false }, null);
});

it('should instrument code of mutated files', () => {
it('should instrument code of mutated files', async () => {
const input = [
textFile({ mutated: true, content: 'function something() {}' }),
binaryFile({ mutated: true }),
webFile({ mutated: true }),
textFile({ mutated: false })
];
const output = sut.transpile(input);
const output = await sut.transpile(input);
expect(output.error).null;
expect(output.outputFiles[1]).eq(output.outputFiles[1]);
expect(output.outputFiles[2]).eq(output.outputFiles[2]);
Expand All @@ -56,9 +56,9 @@ describe('CoverageInstrumenterTranspiler', () => {
});
});

it('should fill error message and not transpile input when the file contains a parse error', () => {
it('should fill error message and not transpile input when the file contains a parse error', async () => {
const invalidJavascriptFile = textFile({ name: 'invalid/file.js', content: 'function something {}', mutated: true });
const output = sut.transpile([invalidJavascriptFile]);
const output = await sut.transpile([invalidJavascriptFile]);
expect(output.error).contains('Could not instrument "invalid/file.js" for code coverage. Error: Line 1: Unexpected token {');
});
});
Expand All @@ -72,15 +72,15 @@ describe('CoverageInstrumenterTranspiler', () => {
input = [textFile({ mutated: true, content: 'function something() {}' })];
});

it('should use the coverage variable "__strykerCoverageCurrentTest__"', () => {
const output = sut.transpile(input);
it('should use the coverage variable "__strykerCoverageCurrentTest__"', async () => {
const output = await sut.transpile(input);
expect(output.error).null;
const instrumentedContent = (output.outputFiles[1] as TextFile).content;
expect(instrumentedContent).to.contain('__strykerCoverageCurrentTest__').and.contain('.f[\'1\']++');
});

it('should also add a collectCoveragePerTest file', () => {
const output = sut.transpile(input);
it('should also add a collectCoveragePerTest file', async () => {
const output = await sut.transpile(input);
expect(output.error).null;
expect(output.outputFiles).lengthOf(2);
const actualContent = (output.outputFiles[0] as TextFile).content;
Expand All @@ -90,10 +90,10 @@ describe('CoverageInstrumenterTranspiler', () => {
});
});

it('should result in an error if coverage analysis is "perTest" and there is no testFramework', () => {
it('should result in an error if coverage analysis is "perTest" and there is no testFramework', async () => {
config.coverageAnalysis = 'perTest';
sut = new CoverageInstrumenterTranspiler({ config, keepSourceMaps: true }, null);
const output = sut.transpile([textFile({ content: 'a + b' })]);
const output = await sut.transpile([textFile({ content: 'a + b' })]);
expect(output.error).eq('Cannot measure coverage results per test, there is no testFramework and thus no way of executing code right before and after each test.');
});
});
16 changes: 8 additions & 8 deletions packages/stryker/test/unit/transpiler/TranspilerFacadeSpec.ts
Expand Up @@ -18,9 +18,9 @@ describe('TranspilerFacade', () => {
sut = new TranspilerFacade({ config: new Config(), keepSourceMaps: true });
});

it('should return input when `transpile` is called', () => {
it('should return input when `transpile` is called', async () => {
const input = [file({ name: 'input' })];
const result = sut.transpile(input);
const result = await sut.transpile(input);
expect(createStub).not.called;
expect(result.error).is.null;
expect(result.outputFiles).eq(input);
Expand Down Expand Up @@ -69,16 +69,16 @@ describe('TranspilerFacade', () => {
expect(createStub).calledWith('transpiler-two');
});

it('should chain the transpilers when `transpile` is called', () => {
it('should chain the transpilers when `transpile` is called', async () => {
sut = new TranspilerFacade({ config, keepSourceMaps: true });
const input = [file({ name: 'input' })];
const result = sut.transpile(input);
const result = await sut.transpile(input);
expect(result).eq(resultTwo);
expect(transpilerOne.transpile).calledWith(input);
expect(transpilerTwo.transpile).calledWith(resultOne.outputFiles);
});

it('should chain an additional transpiler when requested', () => {
it('should chain an additional transpiler when requested', async () => {
const additionalTranspiler = mock(TranspilerFacade);
const expectedResult = transpileResult({ outputFiles: [file({ name: 'result-3' })] });
additionalTranspiler.transpile.returns(expectedResult);
Expand All @@ -87,17 +87,17 @@ describe('TranspilerFacade', () => {
{ config, keepSourceMaps: true },
{ name: 'someTranspiler', transpiler: additionalTranspiler }
);
const output = sut.transpile(input);
const output = await sut.transpile(input);
expect(output).eq(expectedResult);
expect(additionalTranspiler.transpile).calledWith(resultTwo.outputFiles);
});


it('should stop chaining if an error occurs during `transpile`', () => {
it('should stop chaining if an error occurs during `transpile`', async () => {
sut = new TranspilerFacade({ config, keepSourceMaps: true });
const input = [file({ name: 'input' })];
resultOne.error = 'an error';
const result = sut.transpile(input);
const result = await sut.transpile(input);
expect(result).eq(resultOne);
expect(transpilerOne.transpile).calledWith(input);
expect(transpilerTwo.transpile).not.called;
Expand Down

0 comments on commit 794e587

Please sign in to comment.