Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(coverage analysis): Support transpiled code #559

Merged
merged 10 commits into from
Feb 3, 2018
19 changes: 18 additions & 1 deletion packages/stryker-api/.vscode/launch.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Integration tests",
"program": "${workspaceFolder}/../../node_modules/mocha/bin/_mocha",
"args": [
"-u",
"tdd",
"--timeout",
"999999",
"--colors",
"${workspaceFolder}/test/helpers/**/*.js",
"${workspaceFolder}/test/integration/**/*.js"
],
"internalConsoleOptions": "openOnSessionStart"
},
{
"type": "node",
"request": "launch",
Expand All @@ -12,7 +28,8 @@
"--timeout",
"999999",
"--colors",
"${workspaceRoot}/test/**/*.js"
"${workspaceRoot}/test/helpers/**/*.js",
"${workspaceRoot}/test/unit/**/*.js"
],
"internalConsoleOptions": "openOnSessionStart"
}
Expand Down
1 change: 1 addition & 0 deletions packages/stryker-api/src/report/MatchedMutant.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
interface MatchedMutant {
readonly id: string;
readonly mutatorName: string;
readonly scopedTestIds: number[];
readonly timeSpentScopedTests: number;
Expand Down
1 change: 1 addition & 0 deletions packages/stryker-api/src/report/MutantResult.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import MutantStatus from './MutantStatus';
import {Location, Range} from '../../core';

interface MutantResult {
id: string;
sourceFilePath: string;
mutatorName: string;
status: MutantStatus;
Expand Down
5 changes: 0 additions & 5 deletions packages/stryker-api/src/transpile/FileLocation.ts

This file was deleted.

5 changes: 0 additions & 5 deletions packages/stryker-api/src/transpile/Transpiler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import FileLocation from './FileLocation';
import TranspileResult from './TranspileResult';
import { File } from '../../core';

Expand Down Expand Up @@ -34,8 +33,4 @@ export default interface Transpiler {
*/
transpile(files: File[]): Promise<TranspileResult>;

/**
* Retrieve the location of a source location in the transpiled file.
*/
getMappedLocation(sourceFileLocation: FileLocation): FileLocation;
}
2 changes: 1 addition & 1 deletion packages/stryker-api/src/transpile/TranspilerOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ export default interface TranspilerOptions {
* Indicates whether or not the source maps need to be kept.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment seems to be out of date

* If false, the transpiler may optimize to not calculate source maps.
*/
keepSourceMaps: boolean;
produceSourceMaps: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ describe('we have a module using stryker', function () {
arrangeActAndAssertModule('mutant', ['mutatorName: \'foo\'']);
arrangeActAndAssertModule('report', ['empty', 'all', 'status: 3', 'originalLines: \'string\'', 'Mutant status runtime error: RuntimeError', 'transpile error: TranspileError']);
arrangeActAndAssertModule('test_runner', ['MyTestRunner']);
arrangeActAndAssertModule('transpile', ['my-file', 'foo']);
arrangeActAndAssertModule('transpile', ['foo', 'bar']);
});
});
});
2 changes: 1 addition & 1 deletion packages/stryker-api/testResources/module/useCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const binaryFile = createFile({
mutated: false,
included: false,
transpiled: false,
content: Buffer.from('sdssdsd'),
content: Buffer.from('foobar'),
kind: FileKind.Binary
});

Expand Down
12 changes: 7 additions & 5 deletions packages/stryker-api/testResources/module/useReport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ if (!(allReporter instanceof AllReporter)) {
}

let result: MutantResult = {
id: '13',
sourceFilePath: 'string',
mutatorName: 'string',
status: MutantStatus.TimedOut,
Expand All @@ -48,11 +49,12 @@ console.log(`Mutant status runtime error: ${MutantStatus[MutantStatus.RuntimeErr
console.log(`Mutant status transpile error: ${MutantStatus[MutantStatus.TranspileError]}`);

const matchedMutant: MatchedMutant = {
mutatorName: '',
scopedTestIds: [52],
timeSpentScopedTests: 52,
fileName: 'string',
replacement: 'string'
id: '13',
mutatorName: '',
scopedTestIds: [52],
timeSpentScopedTests: 52,
fileName: 'string',
replacement: 'string'
};

allReporter.onAllMutantsMatchedWithTests([Object.freeze(matchedMutant)]);
Expand Down
13 changes: 3 additions & 10 deletions packages/stryker-api/testResources/module/useTranspile.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Config } from 'stryker-api/config';
import { Transpiler, FileLocation, TranspileResult, TranspilerFactory, TranspilerOptions } from 'stryker-api/transpile';
import { Transpiler, TranspileResult, TranspilerFactory, TranspilerOptions } from 'stryker-api/transpile';
import { TextFile, File, FileKind } from 'stryker-api/core';

class MyTranspiler implements Transpiler {
Expand All @@ -8,22 +8,15 @@ class MyTranspiler implements Transpiler {

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],
outputFiles: [{ name: 'foo', content: 'bar', kind: FileKind.Text, mutated: this.transpilerOptions.produceSourceMaps, included: false, transpiled: true } as File],
error: null
});
}
getMappedLocation(sourceFileLocation: FileLocation): FileLocation {
return sourceFileLocation;
}
}

TranspilerFactory.instance().register('my-transpiler', MyTranspiler);
const transpiler = TranspilerFactory.instance().create('my-transpiler', { keepSourceMaps: true, config: new Config() });
const transpiler = TranspilerFactory.instance().create('my-transpiler', { produceSourceMaps: true, config: new Config() });

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 } })
));
});
1 change: 0 additions & 1 deletion packages/stryker-api/transpile.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export { default as FileLocation } from './src/transpile/FileLocation';
export { default as Transpiler } from './src/transpile/Transpiler';
export { default as TranspileResult } from './src/transpile/TranspileResult';
export { default as TranspilerFactory } from './src/transpile/TranspilerFactory';
Expand Down
10 changes: 4 additions & 6 deletions packages/stryker-babel-transpiler/src/BabelTranspiler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Transpiler, TranspilerOptions, TranspileResult, FileLocation } from 'stryker-api/transpile';
import { Transpiler, TranspilerOptions, TranspileResult } from 'stryker-api/transpile';
import { File, TextFile, FileKind } from 'stryker-api/core';
import * as babel from 'babel-core';
import * as path from 'path';
Expand All @@ -11,7 +11,9 @@ class BabelTranspiler implements Transpiler {

public constructor(options: TranspilerOptions) {
this.babelConfig = new BabelConfigReader().readConfig(options.config);

if (options.produceSourceMaps) {
throw new Error(`Invalid \`coverageAnalysis\` "${options.config.coverageAnalysis}" is not supported by the stryker-babel-transpiler. Not able to produce source maps yet. Please set it to "off".`);
}
this.knownExtensions = ['.js', '.jsx'];
}

Expand Down Expand Up @@ -78,10 +80,6 @@ class BabelTranspiler implements Transpiler {
outputFiles
};
}

public getMappedLocation(): FileLocation {
throw new Error('Not implemented');
}
}

export default BabelTranspiler;
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe('BabelPluginProject', () => {
babelConfig = ProjectLoader.loadBabelRc(projectDir);
config = new Config();
config.set({ babelConfig });
babelTranspiler = new BabelTranspiler({ config, keepSourceMaps: false });
babelTranspiler = new BabelTranspiler({ config, produceSourceMaps: false });
});

it('should have project files', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe('BabelPresetProject', () => {
expectedResultFiles = ProjectLoader.removeEOL(ProjectLoader.getFiles(path.join(projectDir, 'expectedResult')));
babelConfig = ProjectLoader.loadBabelRc(projectDir);
config.set({ babelConfig });
babelTranspiler = new BabelTranspiler({ config, keepSourceMaps: false });
babelTranspiler = new BabelTranspiler({ config, produceSourceMaps: false });
});

it('should have project files', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe('BabelProject', () => {
babelConfig = ProjectLoader.loadBabelRc(projectDir);
config = new Config();
config.set({ babelConfig });
babelTranspiler = new BabelTranspiler({ config, keepSourceMaps: false });
babelTranspiler = new BabelTranspiler({ config, produceSourceMaps: false });
});

it('should have project files', () => {
Expand Down
20 changes: 2 additions & 18 deletions packages/stryker-babel-transpiler/test/unit/BabelTranspilerSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import BabelTranspiler from '../../src/BabelTranspiler';
import { expect, assert } from 'chai';
import { File } from 'stryker-api/core';
import { Transpiler } from 'stryker-api/transpile';
import { Position, FileKind } from 'stryker-api/core';
import { FileKind } from 'stryker-api/core';
import { Config } from 'stryker-api/config';
import { createFile } from '../helpers/producers';
import * as sinon from 'sinon';
Expand All @@ -25,7 +25,7 @@ describe('BabelTranspiler', () => {
};
});

babelTranspiler = new BabelTranspiler({ config: new Config, keepSourceMaps: false });
babelTranspiler = new BabelTranspiler({ config: new Config, produceSourceMaps: false });

files = [
createFile('main.js', 'const main = () => { sum(2); divide(2); }'),
Expand Down Expand Up @@ -96,20 +96,4 @@ describe('BabelTranspiler', () => {
});
});

describe('getMappedLocation', () => {
it('should throw a not implemented error', () => {
const position: Position = {
line: 0,
column: 0
};

const fileLocation: { fileName: string, start: Position, end: Position } = {
fileName: 'test',
start: position,
end: position
};

expect(() => babelTranspiler.getMappedLocation(fileLocation)).to.throw(Error, 'Not implemented');
});
});
});
1 change: 1 addition & 0 deletions packages/stryker-html-reporter/test/helpers/producers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const thresholds = factory<MutationScoreThresholds>(() => ({
export const mutantResult = factory<MutantResult>(() => {
const range: [number, number] = [24, 38];
return {
id: '1',
sourceFilePath: 'src/test.js',
mutatorName: 'Math',
status: MutantStatus.Killed,
Expand Down
1 change: 0 additions & 1 deletion packages/stryker-typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
"lodash.flatmap": "^4.5.0",
"log4js": "^1.1.1",
"semver": "^5.4.1",
"source-map": "^0.5.6",
"tslib": "^1.5.0"
},
"devDependencies": {
Expand Down
6 changes: 3 additions & 3 deletions packages/stryker-typescript/src/TypescriptConfigEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as ts from 'typescript';
import { getLogger, setGlobalLogLevel } from 'log4js';
import { ConfigEditor, Config } from 'stryker-api/config';
import { CONFIG_KEY_FILE, CONFIG_KEY_OPTIONS } from './helpers/keys';
import { normalizeForTypescript } from './helpers/tsHelpers';
import { normalizeFileForTypescript } from './helpers/tsHelpers';

// Override some compiler options that have to do with code quality. When mutating, we're not interested in the resulting code quality
// See https://github.com/stryker-mutator/stryker/issues/391 for more info
Expand Down Expand Up @@ -47,9 +47,9 @@ export default class TypescriptConfigEditor implements ConfigEditor {
}

private readTypescriptConfig(tsconfigFileName: string, host: ts.ParseConfigHost) {
const configFileBase = normalizeForTypescript(path.dirname(tsconfigFileName));
const configFileBase = normalizeFileForTypescript(path.dirname(tsconfigFileName));
const configFileText = fs.readFileSync(tsconfigFileName, 'utf8');
const tsconfigFileNameNormalizedForTypeScript = normalizeForTypescript(tsconfigFileName);
const tsconfigFileNameNormalizedForTypeScript = normalizeFileForTypescript(tsconfigFileName);
const parseResult = ts.parseConfigFileTextToJson(tsconfigFileNameNormalizedForTypeScript, configFileText);
if (parseResult.error) {
const error = ts.formatDiagnostics([parseResult.error], this.diagnosticsHost(configFileBase));
Expand Down
45 changes: 23 additions & 22 deletions packages/stryker-typescript/src/TypescriptTranspiler.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,60 @@
import flatMap = require('lodash.flatmap');
import { Config } from 'stryker-api/config';
import { Transpiler, TranspileResult, TranspilerOptions, FileLocation } from 'stryker-api/transpile';
import { Transpiler, TranspileResult, TranspilerOptions } from 'stryker-api/transpile';
import { File } from 'stryker-api/core';
import { filterTypescriptFiles, getCompilerOptions, getProjectDirectory, filterNotEmpty, isHeaderFile, guardTypescriptVersion, isTypescriptFile } from './helpers/tsHelpers';
import { filterTypescriptFiles, getCompilerOptions, getProjectDirectory, isHeaderFile, guardTypescriptVersion, isTypescriptFile } from './helpers/tsHelpers';
import TranspilingLanguageService from './transpiler/TranspilingLanguageService';
import { setGlobalLogLevel } from 'log4js';

export default class TypescriptTranspiler implements Transpiler {
private languageService: TranspilingLanguageService;
private readonly next: Transpiler;
private readonly config: Config;
private readonly keepSourceMaps: boolean;
private readonly produceSourceMaps: boolean;

constructor(options: TranspilerOptions) {
guardTypescriptVersion();
setGlobalLogLevel(options.config.logLevel);
this.config = options.config;
this.keepSourceMaps = options.keepSourceMaps;
this.produceSourceMaps = options.produceSourceMaps;
}

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

getMappedLocation(sourceFileLocation: FileLocation): FileLocation {
const outputLocation = this.languageService.getMappedLocationFor(sourceFileLocation);
if (outputLocation) {
return this.next.getMappedLocation(outputLocation);
} else {
throw new Error(`Could not find mapped location for ${sourceFileLocation.fileName}:${sourceFileLocation.start.line}:${sourceFileLocation.start.column}`);
}
}

private transpileAndResult(typescriptFiles: File[], allFiles: File[]) {
const error = this.languageService.getSemanticDiagnostics(typescriptFiles.map(file => file.name));
if (error.length) {
return this.createErrorResult(error);
} else {
const implementationFiles = typescriptFiles.filter(file => !isHeaderFile(file));
const outputFiles = this.languageService.emit(implementationFiles);
// Keep original order of the files
const resultFiles = filterNotEmpty(allFiles.map(file => {
if (file.transpiled && isTypescriptFile(file)) {
return outputFiles[file.name];
let moreOutput = true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps isSingleOutput = false would be nicer. (Note that this turns the implementation around and you will also have to reverse the if (moreOutput) check and you have to swap the way you set the variable after emitting.

const resultFiles: File[] = flatMap(allFiles, file => {
if (isHeaderFile(file)) {
// Header files are not compiled to output
return [];
} else if (file.transpiled && isTypescriptFile(file)) {
// File is a typescript file. Only emit if more output is expected.
if (moreOutput) {
const emitOutput = this.languageService.emit(file);
moreOutput = !emitOutput.singleResult;
return emitOutput.outputFiles;
} else {
return [];
}
} else {
return file;
// File is not a typescript file
return [file];
}
}));
});
return this.createSuccessResult(resultFiles);
}
}
Expand Down
10 changes: 9 additions & 1 deletion packages/stryker-typescript/src/helpers/tsHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,18 @@ export function getTSConfig(strykerConfig: Config): ts.CompilerOptions | undefin
* For some reason, typescript on windows doesn't like back slashes
* @param fileName The file name to be normalized
*/
export function normalizeForTypescript(fileName: string) {
export function normalizeFileForTypescript(fileName: string) {
return fileName.replace(/\\/g, '/');
}

/**
* For some reason, typescript on windows doesn't like back slashes
* @param fileName The file name to be normalized
*/
export function normalizeFileFromTypescript(fileName: string) {
return fileName.replace(/\//g, path.sep);
}

export function getCompilerOptions(config: Config) {
return config[CONFIG_KEY_OPTIONS];
}
Expand Down
Loading