diff --git a/packages/stryker-api/.vscode/launch.json b/packages/stryker-api/.vscode/launch.json index aa76314b3e..832a8df801 100644 --- a/packages/stryker-api/.vscode/launch.json +++ b/packages/stryker-api/.vscode/launch.json @@ -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", @@ -12,7 +28,8 @@ "--timeout", "999999", "--colors", - "${workspaceRoot}/test/**/*.js" + "${workspaceRoot}/test/helpers/**/*.js", + "${workspaceRoot}/test/unit/**/*.js" ], "internalConsoleOptions": "openOnSessionStart" } diff --git a/packages/stryker-api/src/report/MatchedMutant.ts b/packages/stryker-api/src/report/MatchedMutant.ts index 97a022e12d..9ea3150dac 100644 --- a/packages/stryker-api/src/report/MatchedMutant.ts +++ b/packages/stryker-api/src/report/MatchedMutant.ts @@ -1,4 +1,5 @@ interface MatchedMutant { + readonly id: string; readonly mutatorName: string; readonly scopedTestIds: number[]; readonly timeSpentScopedTests: number; diff --git a/packages/stryker-api/src/report/MutantResult.ts b/packages/stryker-api/src/report/MutantResult.ts index be4ae8ed32..3968444d46 100644 --- a/packages/stryker-api/src/report/MutantResult.ts +++ b/packages/stryker-api/src/report/MutantResult.ts @@ -2,6 +2,7 @@ import MutantStatus from './MutantStatus'; import {Location, Range} from '../../core'; interface MutantResult { + id: string; sourceFilePath: string; mutatorName: string; status: MutantStatus; diff --git a/packages/stryker-api/src/transpile/FileLocation.ts b/packages/stryker-api/src/transpile/FileLocation.ts deleted file mode 100644 index 7b7191f03b..0000000000 --- a/packages/stryker-api/src/transpile/FileLocation.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Location } from '../../core'; - -export default interface FileLocation extends Location { - fileName: string; -} \ No newline at end of file diff --git a/packages/stryker-api/src/transpile/Transpiler.ts b/packages/stryker-api/src/transpile/Transpiler.ts index fa4770173c..eb8984b1f3 100644 --- a/packages/stryker-api/src/transpile/Transpiler.ts +++ b/packages/stryker-api/src/transpile/Transpiler.ts @@ -1,4 +1,3 @@ -import FileLocation from './FileLocation'; import TranspileResult from './TranspileResult'; import { File } from '../../core'; @@ -34,8 +33,4 @@ export default interface Transpiler { */ transpile(files: File[]): Promise; - /** - * Retrieve the location of a source location in the transpiled file. - */ - getMappedLocation(sourceFileLocation: FileLocation): FileLocation; } \ No newline at end of file diff --git a/packages/stryker-api/src/transpile/TranspilerOptions.ts b/packages/stryker-api/src/transpile/TranspilerOptions.ts index 5ab52c2a3b..4a7ee16b67 100644 --- a/packages/stryker-api/src/transpile/TranspilerOptions.ts +++ b/packages/stryker-api/src/transpile/TranspilerOptions.ts @@ -7,8 +7,8 @@ export default interface TranspilerOptions { config: Config; /** - * Indicates whether or not the source maps need to be kept. + * Indicates whether or not the source maps need to be produced as part of the transpiler output. * If false, the transpiler may optimize to not calculate source maps. */ - keepSourceMaps: boolean; + produceSourceMaps: boolean; } \ No newline at end of file diff --git a/packages/stryker-api/test/integration/install-module/install-module.ts b/packages/stryker-api/test/integration/install-module/install-module.ts index 9f9d7fff44..4ba46e24f3 100644 --- a/packages/stryker-api/test/integration/install-module/install-module.ts +++ b/packages/stryker-api/test/integration/install-module/install-module.ts @@ -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']); }); }); }); \ No newline at end of file diff --git a/packages/stryker-api/testResources/module/useCore.ts b/packages/stryker-api/testResources/module/useCore.ts index b6d5b16b30..66e40a14f3 100644 --- a/packages/stryker-api/testResources/module/useCore.ts +++ b/packages/stryker-api/testResources/module/useCore.ts @@ -33,7 +33,7 @@ const binaryFile = createFile({ mutated: false, included: false, transpiled: false, - content: Buffer.from('sdssdsd'), + content: Buffer.from('foobar'), kind: FileKind.Binary }); diff --git a/packages/stryker-api/testResources/module/useReport.ts b/packages/stryker-api/testResources/module/useReport.ts index bd13640297..e764e7498e 100644 --- a/packages/stryker-api/testResources/module/useReport.ts +++ b/packages/stryker-api/testResources/module/useReport.ts @@ -32,6 +32,7 @@ if (!(allReporter instanceof AllReporter)) { } let result: MutantResult = { + id: '13', sourceFilePath: 'string', mutatorName: 'string', status: MutantStatus.TimedOut, @@ -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)]); diff --git a/packages/stryker-api/testResources/module/useTranspile.ts b/packages/stryker-api/testResources/module/useTranspile.ts index d2c5c31115..d7bae7cea7 100644 --- a/packages/stryker-api/testResources/module/useTranspile.ts +++ b/packages/stryker-api/testResources/module/useTranspile.ts @@ -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 { @@ -8,22 +8,15 @@ class MyTranspiler implements Transpiler { transpile(files: File[]): Promise { 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 } }) - )); }); \ No newline at end of file diff --git a/packages/stryker-api/transpile.ts b/packages/stryker-api/transpile.ts index 779521ee96..4b3d939d1f 100644 --- a/packages/stryker-api/transpile.ts +++ b/packages/stryker-api/transpile.ts @@ -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'; diff --git a/packages/stryker-babel-transpiler/src/BabelTranspiler.ts b/packages/stryker-babel-transpiler/src/BabelTranspiler.ts index 8aa24e7118..d52045fcda 100644 --- a/packages/stryker-babel-transpiler/src/BabelTranspiler.ts +++ b/packages/stryker-babel-transpiler/src/BabelTranspiler.ts @@ -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'; @@ -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']; } @@ -78,10 +80,6 @@ class BabelTranspiler implements Transpiler { outputFiles }; } - - public getMappedLocation(): FileLocation { - throw new Error('Not implemented'); - } } export default BabelTranspiler; \ No newline at end of file diff --git a/packages/stryker-babel-transpiler/test/integration/BabelPluginProjectSpec.ts b/packages/stryker-babel-transpiler/test/integration/BabelPluginProjectSpec.ts index 464a7a4a1e..bb7d496d7b 100644 --- a/packages/stryker-babel-transpiler/test/integration/BabelPluginProjectSpec.ts +++ b/packages/stryker-babel-transpiler/test/integration/BabelPluginProjectSpec.ts @@ -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', () => { diff --git a/packages/stryker-babel-transpiler/test/integration/BabelPresetProjectSpec.ts b/packages/stryker-babel-transpiler/test/integration/BabelPresetProjectSpec.ts index 4c02a40b5e..daef483622 100644 --- a/packages/stryker-babel-transpiler/test/integration/BabelPresetProjectSpec.ts +++ b/packages/stryker-babel-transpiler/test/integration/BabelPresetProjectSpec.ts @@ -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', () => { diff --git a/packages/stryker-babel-transpiler/test/integration/BabelProjectSpec.ts b/packages/stryker-babel-transpiler/test/integration/BabelProjectSpec.ts index 548b81a813..a21bf382bd 100644 --- a/packages/stryker-babel-transpiler/test/integration/BabelProjectSpec.ts +++ b/packages/stryker-babel-transpiler/test/integration/BabelProjectSpec.ts @@ -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', () => { diff --git a/packages/stryker-babel-transpiler/test/unit/BabelTranspilerSpec.ts b/packages/stryker-babel-transpiler/test/unit/BabelTranspilerSpec.ts index ac51475dca..b2fe0dd18a 100644 --- a/packages/stryker-babel-transpiler/test/unit/BabelTranspilerSpec.ts +++ b/packages/stryker-babel-transpiler/test/unit/BabelTranspilerSpec.ts @@ -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'; @@ -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); }'), @@ -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'); - }); - }); }); \ No newline at end of file diff --git a/packages/stryker-html-reporter/test/helpers/producers.ts b/packages/stryker-html-reporter/test/helpers/producers.ts index d6bc8040a2..3ff02a8a4b 100644 --- a/packages/stryker-html-reporter/test/helpers/producers.ts +++ b/packages/stryker-html-reporter/test/helpers/producers.ts @@ -18,6 +18,7 @@ export const thresholds = factory(() => ({ export const mutantResult = factory(() => { const range: [number, number] = [24, 38]; return { + id: '1', sourceFilePath: 'src/test.js', mutatorName: 'Math', status: MutantStatus.Killed, diff --git a/packages/stryker-typescript/package.json b/packages/stryker-typescript/package.json index 01cf4b632d..9d01e2fa9a 100644 --- a/packages/stryker-typescript/package.json +++ b/packages/stryker-typescript/package.json @@ -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": { diff --git a/packages/stryker-typescript/src/TypescriptConfigEditor.ts b/packages/stryker-typescript/src/TypescriptConfigEditor.ts index 0affbd4b92..8d5f4bd004 100644 --- a/packages/stryker-typescript/src/TypescriptConfigEditor.ts +++ b/packages/stryker-typescript/src/TypescriptConfigEditor.ts @@ -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 @@ -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)); diff --git a/packages/stryker-typescript/src/TypescriptTranspiler.ts b/packages/stryker-typescript/src/TypescriptTranspiler.ts index 2224496b26..198fd65e73 100644 --- a/packages/stryker-typescript/src/TypescriptTranspiler.ts +++ b/packages/stryker-typescript/src/TypescriptTranspiler.ts @@ -1,21 +1,21 @@ +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 { @@ -23,37 +23,38 @@ export default class TypescriptTranspiler implements Transpiler { .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 isSingleOutput = false; + 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 (!isSingleOutput) { + const emitOutput = this.languageService.emit(file); + isSingleOutput = emitOutput.singleResult; + return emitOutput.outputFiles; + } else { + return []; + } } else { - return file; + // File is not a typescript file + return [file]; } - })); + }); return this.createSuccessResult(resultFiles); } } diff --git a/packages/stryker-typescript/src/helpers/tsHelpers.ts b/packages/stryker-typescript/src/helpers/tsHelpers.ts index 3a7b8d4cdb..0a249db5e9 100644 --- a/packages/stryker-typescript/src/helpers/tsHelpers.ts +++ b/packages/stryker-typescript/src/helpers/tsHelpers.ts @@ -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]; } @@ -78,6 +86,14 @@ export function isTypescriptFile(file: File) { tsExtensions().some(extension => file.name.endsWith(extension)); } +export function isJavaScriptFile(file: ts.OutputFile) { + return file.name.endsWith('.js') || file.name.endsWith('.jsx'); +} + +export function isMapFile(file: ts.OutputFile) { + return file.name.endsWith('.map'); +} + /** * Determines whether or not given file is a typescript header file (*.d.ts) */ diff --git a/packages/stryker-typescript/src/transpiler/OutputFile.ts b/packages/stryker-typescript/src/transpiler/OutputFile.ts deleted file mode 100644 index c1d3d7ebc8..0000000000 --- a/packages/stryker-typescript/src/transpiler/OutputFile.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Location, Position } from 'stryker-api/core'; -import { SourceMapConsumer } from 'source-map'; -import { FileLocation } from 'stryker-api/transpile'; - -export default class OutputFile { - - constructor(public name: string, public content: string, public sourceMapContent: string) { - } - - getMappedLocation(sourceFileLocation: FileLocation): Location | null { - const generatedStart = this.mappedPositionFor(sourceFileLocation.fileName, sourceFileLocation.start, SourceMapConsumer.GREATEST_LOWER_BOUND); - const generatedEnd = this.mappedPositionFor(sourceFileLocation.fileName, sourceFileLocation.end, SourceMapConsumer.LEAST_UPPER_BOUND); - if (generatedStart.column && generatedStart.line && generatedEnd.column && generatedEnd.line) { - return { - start: { - line: generatedStart.line, - column: generatedStart.column - }, - end: { - line: generatedEnd.line, - column: generatedEnd.column - } - }; - } else { - return null; - } - } - - private mappedPositionFor(sourceFileName: string, position: Position, bias: number) { - return this.sourceMap.generatedPositionFor({ - source: sourceFileName, - line: position.line, - column: position.column, - bias - }); - } - - private _sourceMap: SourceMapConsumer; - public get sourceMap(): SourceMapConsumer { - if (!this._sourceMap) { - const rawSourceMap = JSON.parse(this.sourceMapContent) as sourceMap.RawSourceMap; - this._sourceMap = new SourceMapConsumer(rawSourceMap); - } - return this._sourceMap; - } - -} diff --git a/packages/stryker-typescript/src/transpiler/TranspilingLanguageService.ts b/packages/stryker-typescript/src/transpiler/TranspilingLanguageService.ts index 24634d26f5..84c25db528 100644 --- a/packages/stryker-typescript/src/transpiler/TranspilingLanguageService.ts +++ b/packages/stryker-typescript/src/transpiler/TranspilingLanguageService.ts @@ -5,25 +5,27 @@ import * as ts from 'typescript'; import { Logger, getLogger } from 'log4js'; import flatMap = require('lodash.flatmap'); import { TextFile, FileDescriptor, FileKind } from 'stryker-api/core'; -import { FileLocation } from 'stryker-api/transpile'; import ScriptFile from './ScriptFile'; -import OutputFile from './OutputFile'; +import { normalizeFileFromTypescript, isJavaScriptFile, isMapFile } from '../helpers/tsHelpers'; const libRegex = /^lib\.(?:\w|\.)*\.?d\.ts$/; +export interface EmitOutput { + singleResult: boolean; + outputFiles: TextFile[]; +} + export default class TranspilingLanguageService { private languageService: ts.LanguageService; private compilerOptions: ts.CompilerOptions; private readonly files: ts.MapLike; - private readonly outputFiles: ts.MapLike; private logger: Logger; private readonly diagnosticsFormatter: ts.FormatDiagnosticsHost; - constructor(compilerOptions: Readonly, private rootFiles: TextFile[], private projectDirectory: string, private keepSourceMaps: boolean) { + constructor(compilerOptions: Readonly, rootFiles: TextFile[], private projectDirectory: string, private produceSourceMaps: boolean) { this.logger = getLogger(TranspilingLanguageService.name); this.files = Object.create(null); - this.outputFiles = Object.create(null); this.compilerOptions = this.adaptCompilerOptions(compilerOptions); rootFiles.forEach(file => this.files[file.name] = new ScriptFile(file.name, file.content)); const host = this.createLanguageServiceHost(); @@ -43,7 +45,7 @@ export default class TranspilingLanguageService { */ private adaptCompilerOptions(source: ts.CompilerOptions) { const compilerOptions = Object.assign({}, source); - compilerOptions.sourceMap = this.keepSourceMaps; + compilerOptions.sourceMap = this.produceSourceMaps; compilerOptions.inlineSourceMap = false; compilerOptions.declaration = false; return compilerOptions; @@ -64,66 +66,35 @@ export default class TranspilingLanguageService { } /** - * Get the output text for given source files - * @param sourceFiles Emit output files based on given source files + * Get the output text file for given source file + * @param sourceFile Emit output file based on this source file * @return Map Returns a map of source file names with their output files. * If all output files are bundled together, only returns the output file once using the first file as key */ - emit(sourceFiles: FileDescriptor[] = this.rootFiles): ts.MapLike { - if (this.compilerOptions.outFile) { - // If it is a single out file, just transpile one file as it is all bundled together anyway. - const outputFile = this.mapToOutput(sourceFiles[0]); - // All output is bundled together. Configure this output file for all root files. - this.rootFiles.forEach(rootFile => this.outputFiles[rootFile.name] = outputFile); - - return { - [sourceFiles[0].name]: { - name: outputFile.name, - content: outputFile.content, - mutated: sourceFiles[0].mutated, - kind: FileKind.Text, - transpiled: true, // Override transpiled. If a next transpiler comes along, definitely pick up this file. - included: true // Override included, as it should be included when there is only one output file - } - }; - } else { - return sourceFiles.reduce((fileMap, sourceFile) => { - const outputFile = this.mapToOutput(sourceFile); - this.outputFiles[sourceFile.name] = outputFile; - const textOutput: TextFile = { - name: outputFile.name, - content: outputFile.content, - mutated: sourceFile.mutated, - included: sourceFile.included, - transpiled: sourceFile.transpiled, + emit(fileDescriptor: FileDescriptor): EmitOutput { + const emittedFiles = this.languageService.getEmitOutput(fileDescriptor.name).outputFiles; + const jsFile = emittedFiles.find(isJavaScriptFile); + const mapFile = emittedFiles.find(isMapFile); + if (jsFile) { + const outputFiles: TextFile[] = [{ + content: jsFile.text, + name: normalizeFileFromTypescript(jsFile.name), + included: fileDescriptor.included, + kind: FileKind.Text, + mutated: fileDescriptor.mutated, + transpiled: fileDescriptor.transpiled + }]; + if (mapFile) { + outputFiles.push({ + content: mapFile.text, + name: normalizeFileFromTypescript(mapFile.name), + included: false, kind: FileKind.Text, - }; - fileMap[sourceFile.name] = textOutput; - return fileMap; - }, Object.create(null)); - } - } - - getMappedLocationFor(sourceFileLocation: FileLocation): FileLocation | null { - const outputFile = this.outputFiles[sourceFileLocation.fileName]; - if (outputFile) { - const location = this.outputFiles[sourceFileLocation.fileName] - .getMappedLocation(sourceFileLocation); - if (location) { - const targetFileLocation = location as FileLocation; - targetFileLocation.fileName = outputFile.name; - return targetFileLocation; + mutated: false, + transpiled: false + }); } - } - return null; - } - - private mapToOutput(fileDescriptor: FileDescriptor) { - const outputFiles = this.languageService.getEmitOutput(fileDescriptor.name).outputFiles; - const mapFile = outputFiles.find(file => file.name.endsWith('.js.map')); - const jsFile = outputFiles.find(file => file.name.endsWith('.js')); - if (jsFile) { - return new OutputFile(jsFile.name, jsFile.text, mapFile ? mapFile.text : ''); + return { singleResult: !!this.compilerOptions.outFile, outputFiles }; } else { throw new Error(`Emit error! Could not emit file ${fileDescriptor.name}`); } diff --git a/packages/stryker-typescript/test/integration/ownDogFoodSpec.ts b/packages/stryker-typescript/test/integration/ownDogFoodSpec.ts index 02af51a51e..e1ef54f361 100644 --- a/packages/stryker-typescript/test/integration/ownDogFoodSpec.ts +++ b/packages/stryker-typescript/test/integration/ownDogFoodSpec.ts @@ -33,7 +33,7 @@ describe('stryker-typescript', function () { }); it('should be able to transpile itself', async () => { - const transpiler = new TypescriptTranspiler({ config, keepSourceMaps: true }); + const transpiler = new TypescriptTranspiler({ config, produceSourceMaps: true }); const transpileResult = await transpiler.transpile(inputFiles); expect(transpileResult.error).to.be.null; const outputFiles = transpileResult.outputFiles; @@ -41,7 +41,7 @@ describe('stryker-typescript', function () { }); it('should result in an error if a variable is declared as any and noImplicitAny = true', async () => { - const transpiler = new TypescriptTranspiler({ config, keepSourceMaps: true }); + const transpiler = new TypescriptTranspiler({ config, produceSourceMaps: true }); inputFiles[0].content += 'function foo(bar) { return bar; } '; const transpileResult = await transpiler.transpile(inputFiles); expect(transpileResult.error).contains('error TS7006: Parameter \'bar\' implicitly has an \'any\' type'); @@ -51,7 +51,7 @@ describe('stryker-typescript', function () { 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 transpiler = new TypescriptTranspiler({ config, produceSourceMaps: true }); const transpileResult = await transpiler.transpile(inputFiles); expect(transpileResult.error).null; }); diff --git a/packages/stryker-typescript/test/integration/sampleSpec.ts b/packages/stryker-typescript/test/integration/sampleSpec.ts index 145d91cf22..2e0658d8a9 100644 --- a/packages/stryker-typescript/test/integration/sampleSpec.ts +++ b/packages/stryker-typescript/test/integration/sampleSpec.ts @@ -45,18 +45,31 @@ describe('Sample integration', function () { }); it('should be able to transpile source code', async () => { - const transpiler = new TypescriptTranspiler({ config, keepSourceMaps: true }); + const transpiler = new TypescriptTranspiler({ config, produceSourceMaps: false }); 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', async () => { + it('should be able to produce source maps', async () => { + const transpiler = new TypescriptTranspiler({ config, produceSourceMaps: true }); + const transpileResult = await transpiler.transpile(inputFiles); + const outputFiles = transpileResult.outputFiles; + expect(outputFiles).lengthOf(4); + const mapFiles = outputFiles.filter(file => file.name.endsWith('.map')); + expect(mapFiles).lengthOf(2); + expect(mapFiles.map(file => file.name)).deep.eq([ + path.resolve(__dirname, '..', '..', 'testResources', 'sampleProject', 'math.js.map'), + path.resolve(__dirname, '..', '..', 'testResources', 'sampleProject', 'useMath.js.map') + ]); + }); + + it('should be able to transpile mutated code', async () => { // Transpile mutants const mutator = new TypescriptMutator(config); const mutants = mutator.mutate(inputFiles); - const transpiler = new TypescriptTranspiler({ config, keepSourceMaps: true }); + const transpiler = new TypescriptTranspiler({ config, produceSourceMaps: false }); transpiler.transpile(inputFiles); const mathDotTS = inputFiles.filter(file => file.name.endsWith('math.ts'))[0]; const [firstBinaryMutant, stringSubtractMutant] = mutants.filter(m => m.mutatorName === 'BinaryExpression'); diff --git a/packages/stryker-typescript/test/integration/useHeaderFile.ts b/packages/stryker-typescript/test/integration/useHeaderFile.ts index 7435c56063..c2d64e92af 100644 --- a/packages/stryker-typescript/test/integration/useHeaderFile.ts +++ b/packages/stryker-typescript/test/integration/useHeaderFile.ts @@ -35,7 +35,7 @@ describe('Use header file integration', function () { }); it('should be able to transpile source code', async () => { - const transpiler = new TypescriptTranspiler({ config, keepSourceMaps: true }); + const transpiler = new TypescriptTranspiler({ config, produceSourceMaps: false }); const transpileResult = await transpiler.transpile(inputFiles); expect(transpileResult.error).to.be.null; const outputFiles = transpileResult.outputFiles; diff --git a/packages/stryker-typescript/test/unit/TypescriptTranspilerSpec.ts b/packages/stryker-typescript/test/unit/TypescriptTranspilerSpec.ts index e65720c91b..aeee22c644 100644 --- a/packages/stryker-typescript/test/unit/TypescriptTranspilerSpec.ts +++ b/packages/stryker-typescript/test/unit/TypescriptTranspilerSpec.ts @@ -5,7 +5,7 @@ import { Mock, mock, textFile, binaryFile } from '../helpers/producers'; import TypescriptTranspiler from '../../src/TypescriptTranspiler'; import { Config } from 'stryker-api/config'; import { TextFile } from 'stryker-api/core'; - +import { EmitOutput } from '../../src/transpiler/TranspilingLanguageService'; describe('TypescriptTranspiler', () => { @@ -22,42 +22,34 @@ describe('TypescriptTranspiler', () => { it('set global log level', () => { config.logLevel = 'foobar'; - sut = new TypescriptTranspiler({ config, keepSourceMaps: true }); + sut = new TypescriptTranspiler({ config, produceSourceMaps: true }); expect(log4js.setGlobalLogLevel).calledWith('foobar'); }); describe('transpile', () => { let singleFileOutputEnabled: boolean; - function makeOutputFile(file: TextFile) { + function makeOutputFile(file: TextFile): EmitOutput { const copy = Object.assign({}, file); - if (file.name.endsWith('.ts') || file.name.endsWith('.js')) { + if (singleFileOutputEnabled) { + const singleFileOutput: TextFile = Object.assign({}, textFile({ + name: 'allOutput.js', + content: 'single output', + })); + return { singleResult: singleFileOutputEnabled, outputFiles: [singleFileOutput] }; + } else if (file.name.endsWith('.ts') || file.name.endsWith('.js')) { copy.name = copy.name.replace('.ts', '.js'); - return copy; + return { singleResult: singleFileOutputEnabled, outputFiles: [copy] }; } else { throw new Error(`Could not transpile "${file.name}"`); } } - function makeOutput(sourceFiles: TextFile[]) { - if (singleFileOutputEnabled) { - const singleFileOutput = Object.assign({}, sourceFiles[0], { name: 'allOutput.js' }); - return { - [sourceFiles[0].name]: singleFileOutput - }; - } else { - return sourceFiles.reduce((map, sourceFile) => { - map[sourceFile.name] = makeOutputFile(sourceFile); - return map; - }, Object.create(null)); - } - } - beforeEach(() => { singleFileOutputEnabled = false; languageService.getSemanticDiagnostics.returns([]); // no errors by default - languageService.emit.callsFake(makeOutput); - sut = new TypescriptTranspiler({ config, keepSourceMaps: true }); + languageService.emit.callsFake(makeOutputFile); + sut = new TypescriptTranspiler({ config, produceSourceMaps: true }); }); it('should transpile given files', async () => { @@ -103,7 +95,7 @@ describe('TypescriptTranspiler', () => { expect(output.error).eq(null); expect(output.outputFiles).deep.eq([ textFile({ name: 'file1.ts', transpiled: false }), - textFile({ name: 'allOutput.js', transpiled: true }), + textFile({ name: 'allOutput.js', content: 'single output', transpiled: true }), binaryFile({ name: 'file3.bin' }), textFile({ name: 'file5.ts', transpiled: false }) ]); @@ -119,10 +111,8 @@ describe('TypescriptTranspiler', () => { textFile({ name: 'file6.js', transpiled: true }) // OK, transpiled JS file ]; sut.transpile(input); - expect(languageService.emit).calledWith([ - textFile({ name: 'file1.ts', transpiled: true }), - textFile({ name: 'file6.js', transpiled: true }) - ]); + expect(languageService.emit).calledWith(textFile({ name: 'file1.ts', transpiled: true })); + expect(languageService.emit).calledWith(textFile({ name: 'file6.js', transpiled: true })); }); it('should return errors when there are diagnostic messages', async () => { diff --git a/packages/stryker/package.json b/packages/stryker/package.json index a315b204cd..23838435d7 100644 --- a/packages/stryker/package.json +++ b/packages/stryker/package.json @@ -6,7 +6,7 @@ "typings": "src/Stryker.d.ts", "scripts": { "start": "tsc -w", - "prebuild": "rimraf \"+(test|src)/**/*+(.d.ts|.js|.map)\" .nyc_output reports coverage testResources/module/node_modules/stryker", + "prebuild": "rimraf \"+(test|src)/**/*+(.d.ts|.js|.map)\" .nyc_output reports coverage", "build": "tsc -p .", "postbuild": "tslint -p tsconfig.json", "test": "nyc --check-coverage --reporter=html --report-dir=reports/coverage --lines 80 --functions 80 --branches 75 mocha \"test/**/*.js\"", @@ -69,6 +69,7 @@ "rimraf": "^2.6.1", "rxjs": "^5.4.3", "serialize-javascript": "^1.3.0", + "source-map": "^0.6.1", "tslib": "^1.5.0", "typed-rest-client": "^0.10.0" }, diff --git a/packages/stryker/src/ConfigValidator.ts b/packages/stryker/src/ConfigValidator.ts index c42f91690a..1ee920f065 100644 --- a/packages/stryker/src/ConfigValidator.ts +++ b/packages/stryker/src/ConfigValidator.ts @@ -23,7 +23,7 @@ export default class ConfigValidator { this.validateIsStringArray('reporter', this.strykerConfig.reporter); this.validateIsStringArray('transpilers', this.strykerConfig.transpilers); this.validateCoverageAnalysis(); - this.downgradeCoverageAnalysisIfNeeded(); + this.validateCoverageAnalysisWithRespectToTranspilers(); this.crashIfNeeded(); } @@ -78,10 +78,13 @@ export default class ConfigValidator { } } - private downgradeCoverageAnalysisIfNeeded() { - if (this.strykerConfig.transpilers.length && this.strykerConfig.coverageAnalysis !== 'off') { - this.log.info('Disabled coverage analysis for this run (off). Coverage analysis using transpilers is not supported yet.'); - this.strykerConfig.coverageAnalysis = 'off'; + private validateCoverageAnalysisWithRespectToTranspilers() { + if (Array.isArray(this.strykerConfig.transpilers) && + this.strykerConfig.transpilers.length > 1 && + this.strykerConfig.coverageAnalysis !== 'off') { + this.invalidate(`Value "${this.strykerConfig.coverageAnalysis}" for \`coverageAnalysis\` is invalid with multiple transpilers (configured transpilers: ${ + this.strykerConfig.transpilers.join(', ') + }). Please report this to the Stryker team if you whish this feature to be implemented`); } } diff --git a/packages/stryker/src/MutantTestMatcher.ts b/packages/stryker/src/MutantTestMatcher.ts index e80b44ace5..1c9936df73 100644 --- a/packages/stryker/src/MutantTestMatcher.ts +++ b/packages/stryker/src/MutantTestMatcher.ts @@ -1,33 +1,43 @@ -import { getLogger } from 'log4js'; import * as _ from 'lodash'; +import { getLogger } from 'log4js'; import { RunResult, CoverageCollection, StatementMap, CoveragePerTestResult, CoverageResult } from 'stryker-api/test_runner'; -import { Location, StrykerOptions, File, TextFile } from 'stryker-api/core'; +import { StrykerOptions, File, TextFile } from 'stryker-api/core'; import { MatchedMutant } from 'stryker-api/report'; import { Mutant } from 'stryker-api/mutant'; -import TestableMutant from './TestableMutant'; +import TestableMutant, { TestSelectionResult } from './TestableMutant'; import StrictReporter from './reporters/StrictReporter'; import { CoverageMapsByFile, CoverageMaps } from './transpiler/CoverageInstrumenterTranspiler'; import { filterEmpty } from './utils/objectUtils'; import SourceFile from './SourceFile'; +import SourceMapper from './transpiler/SourceMapper'; +import LocationHelper from './utils/LocationHelper'; -enum StatementLocationKind { +enum StatementIndexKind { function, statement } /** - * Represents a location inside the coverage data of a file + * Represents a statement index inside the coverage maps of a file * Either the function map, or statement map */ -interface StatementLocation { - kind: StatementLocationKind; +interface StatementIndex { + kind: StatementIndexKind; index: string; } export default class MutantTestMatcher { private readonly log = getLogger(MutantTestMatcher.name); - constructor(private mutants: Mutant[], private files: File[], private initialRunResult: RunResult, private coveragePerFile: CoverageMapsByFile, private options: StrykerOptions, private reporter: StrictReporter) { + + constructor( + private mutants: Mutant[], + private files: File[], + private initialRunResult: RunResult, + private sourceMapper: SourceMapper, + private coveragePerFile: CoverageMapsByFile, + private options: StrykerOptions, + private reporter: StrictReporter) { } private get baseline(): CoverageCollection | null { @@ -43,10 +53,10 @@ export default class MutantTestMatcher { const testableMutants = this.createTestableMutants(); if (this.options.coverageAnalysis === 'off') { - testableMutants.forEach(mutant => mutant.addAllTestResults(this.initialRunResult)); + testableMutants.forEach(mutant => mutant.selectAllTests(this.initialRunResult, TestSelectionResult.Success)); } else if (!this.initialRunResult.coverage) { this.log.warn('No coverage result found, even though coverageAnalysis is "%s". Assuming that all tests cover each mutant. This might have a big impact on the performance.', this.options.coverageAnalysis); - testableMutants.forEach(mutant => mutant.addAllTestResults(this.initialRunResult)); + testableMutants.forEach(mutant => mutant.selectAllTests(this.initialRunResult, TestSelectionResult.FailedButAlreadyReporter)); } else { testableMutants.forEach(testableMutant => this.enrichWithCoveredTests(testableMutant)); } @@ -55,45 +65,52 @@ export default class MutantTestMatcher { } enrichWithCoveredTests(testableMutant: TestableMutant) { - const fileCoverage = this.coveragePerFile[testableMutant.mutant.fileName]; - const smallestCoveringIndicator = this.findMatchingCoveringIndicator(testableMutant, fileCoverage); - if (smallestCoveringIndicator) { - if (this.isCoveredByBaseline(testableMutant.mutant.fileName, smallestCoveringIndicator)) { - testableMutant.addAllTestResults(this.initialRunResult); + const transpiledLocation = this.sourceMapper.transpiledLocationFor({ + fileName: testableMutant.mutant.fileName, + location: testableMutant.location + }); + const fileCoverage = this.coveragePerFile[transpiledLocation.fileName]; + const statementIndex = this.findMatchingStatement(new LocationHelper(transpiledLocation.location), fileCoverage); + if (statementIndex) { + if (this.isCoveredByBaseline(transpiledLocation.fileName, statementIndex)) { + testableMutant.selectAllTests(this.initialRunResult, TestSelectionResult.Success); } else { this.initialRunResult.tests.forEach((testResult, id) => { - if (this.isCoveredByTest(id, testableMutant.mutant.fileName, smallestCoveringIndicator)) { - testableMutant.addTestResult(id, testResult); + if (this.isCoveredByTest(id, transpiledLocation.fileName, statementIndex)) { + testableMutant.selectTest(testResult, id); } }); } } else { - this.log.warn('Cannot find statement for mutant %s in statement map for file. Assuming that all tests cover this mutant. This might have a big impact on the performance.', this.stringify(testableMutant)); - testableMutant.addAllTestResults(this.initialRunResult); + // Could not find a statement corresponding to this mutant + // This can happen when for example mutating a TypeScript interface + // It should result in an early result during mutation testing + // Lets delay error reporting for now + testableMutant.selectAllTests(this.initialRunResult, TestSelectionResult.Failed); } } - private isCoveredByBaseline(filename: string, coveredCodeIndicator: StatementLocation): boolean { + private isCoveredByBaseline(fileName: string, statementIndex: StatementIndex): boolean { if (this.baseline) { - const coverageResult = this.baseline[filename]; - return this.isCoveredByCoverageCollection(coverageResult, coveredCodeIndicator); + const coverageResult = this.baseline[fileName]; + return this.isCoveredByCoverageCollection(coverageResult, statementIndex); } else { return false; } } - private isCoveredByTest(testId: number, filename: string, coveredCodeIndicator: StatementLocation): boolean { + private isCoveredByTest(testId: number, fileName: string, statementIndex: StatementIndex): boolean { const coverageCollection = this.findCoverageCollectionForTest(testId); - const coveredFile = coverageCollection && coverageCollection[filename]; - return this.isCoveredByCoverageCollection(coveredFile, coveredCodeIndicator); + const coveredFile = coverageCollection && coverageCollection[fileName]; + return this.isCoveredByCoverageCollection(coveredFile, statementIndex); } - private isCoveredByCoverageCollection(coveredFile: CoverageResult | null, coveredCodeIndicator: StatementLocation): boolean { + private isCoveredByCoverageCollection(coveredFile: CoverageResult | null, statementIndex: StatementIndex): boolean { if (coveredFile) { - if (coveredCodeIndicator.kind === StatementLocationKind.statement) { - return coveredFile.s[coveredCodeIndicator.index] > 0; + if (statementIndex.kind === StatementIndexKind.statement) { + return coveredFile.s[statementIndex.index] > 0; } else { - return coveredFile.f[coveredCodeIndicator.index] > 0; + return coveredFile.f[statementIndex.index] > 0; } } else { return false; @@ -102,10 +119,10 @@ export default class MutantTestMatcher { private createTestableMutants(): TestableMutant[] { const sourceFiles = this.files.filter(file => file.mutated).map(file => new SourceFile(file as TextFile)); - return filterEmpty(this.mutants.map(mutant => { + return filterEmpty(this.mutants.map((mutant, index) => { const sourceFile = sourceFiles.find(file => file.name === mutant.fileName); if (sourceFile) { - return new TestableMutant(mutant, sourceFile); + return new TestableMutant(index.toString(), mutant, sourceFile); } else { this.log.error(`Mutant "${mutant.mutatorName}${mutant.replacement}" is corrupt, because cannot find a text file with name ${mutant.fileName}. List of source files: \n\t${sourceFiles.map(s => s.name).join('\n\t')}`); return null; @@ -121,6 +138,7 @@ export default class MutantTestMatcher { */ private mapMutantOnMatchedMutant(testableMutant: TestableMutant): MatchedMutant { const matchedMutant = _.cloneDeep({ + id: testableMutant.id, mutatorName: testableMutant.mutant.mutatorName, scopedTestIds: testableMutant.selectedTests.map(testSelection => testSelection.id), timeSpentScopedTests: testableMutant.timeSpentScopedTests, @@ -130,18 +148,18 @@ export default class MutantTestMatcher { return Object.freeze(matchedMutant); } - private findMatchingCoveringIndicator(mutant: TestableMutant, fileCoverage: CoverageMaps): StatementLocation | null { - const statementIndex = this.findMatchingStatement(mutant, fileCoverage.statementMap); + private findMatchingStatement(location: LocationHelper, fileCoverage: CoverageMaps): StatementIndex | null { + const statementIndex = this.findMatchingStatementInMap(location, fileCoverage.statementMap); if (statementIndex) { return { - kind: StatementLocationKind.statement, + kind: StatementIndexKind.statement, index: statementIndex }; } else { - const functionIndex = this.findMatchingStatement(mutant, fileCoverage.fnMap); + const functionIndex = this.findMatchingStatementInMap(location, fileCoverage.fnMap); if (functionIndex) { return { - kind: StatementLocationKind.function, + kind: StatementIndexKind.function, index: functionIndex }; } else { @@ -151,55 +169,29 @@ export default class MutantTestMatcher { } /** - * Finds the smallest statement that covers a mutant. - * @param mutant The mutant. - * @param statementMap of the covering file. - * @returns The index of the smallest statement surrounding the mutant, or null if not found. + * Finds the smallest statement that covers a location + * @param needle The location to find. + * @param haystack the statement map or function map to search in. + * @returns The index of the smallest statement surrounding the location, or null if not found. */ - private findMatchingStatement(mutant: TestableMutant, statementMap: StatementMap): string | null { - let smallestStatement: string | null = null; - if (statementMap) { - Object.keys(statementMap).forEach(statementId => { - let location = statementMap[statementId]; - - if (this.locationCoversMutant(mutant.location, location) && (!smallestStatement || this.isSmallerArea(statementMap[smallestStatement], location))) { - smallestStatement = statementId; + private findMatchingStatementInMap(needle: LocationHelper, haystack: StatementMap): string | null { + let smallestStatement: { index: string | null, location: LocationHelper } = { + index: null, + location: LocationHelper.MAX_VALUE + }; + if (haystack) { + Object.keys(haystack).forEach(statementId => { + const statementLocation = haystack[statementId]; + + if (needle.isCoveredBy(statementLocation) && smallestStatement.location.isSmallerArea(statementLocation)) { + smallestStatement = { + index: statementId, + location: new LocationHelper(statementLocation) + }; } }); } - return smallestStatement; - } - - /** - * Indicates whether the second location is smaller than the first location. - * @param first The area which may cover a bigger area than the second location. - * @param second The area which may cover a smaller area than the first location. - * @returns true if the second location covers a smaller area than the first. - */ - private isSmallerArea(first: Location, second: Location): boolean { - let firstLocationHasSmallerArea = false; - let lineDifference = (first.end.line - first.start.line) - (second.end.line - second.start.line); - let coversLessLines = lineDifference > 0; - let coversLessColumns = lineDifference === 0 && (second.start.column - first.start.column) + (first.end.column - second.end.column) > 0; - if (coversLessLines || coversLessColumns) { - firstLocationHasSmallerArea = true; - } - return firstLocationHasSmallerArea; - } - - /** - * Indicates whether a location covers a mutant. - * @param mutantLocation The location of the mutant. - * @param statementLocation The location of the statement. - * @returns true if the location covers the mutant. - */ - private locationCoversMutant(mutantLocation: Location, statementLocation: Location): boolean { - let mutantIsAfterStart = mutantLocation.start.line > statementLocation.start.line || - (mutantLocation.start.line === statementLocation.start.line && mutantLocation.start.column >= statementLocation.start.column); - let mutantIsBeforeEnd = mutantLocation.end.line < statementLocation.end.line || - (mutantLocation.end.line === statementLocation.end.line && mutantLocation.end.column <= statementLocation.end.column); - - return mutantIsAfterStart && mutantIsBeforeEnd; + return smallestStatement.index; } private findCoverageCollectionForTest(testId: number): CoverageCollection | null { @@ -217,8 +209,4 @@ export default class MutantTestMatcher { private isCoveragePerTestResult(coverage: CoverageCollection | CoveragePerTestResult | undefined): coverage is CoveragePerTestResult { return this.options.coverageAnalysis === 'perTest'; } - - private stringify(mutant: TestableMutant) { - return `${mutant.mutant.mutatorName}: (${mutant.replacement}) file://${mutant.fileName}:${mutant.location.start.line + 1}:${mutant.location.start.column}`; - } } \ No newline at end of file diff --git a/packages/stryker/src/Sandbox.ts b/packages/stryker/src/Sandbox.ts index 9f151aa8a9..372748f822 100644 --- a/packages/stryker/src/Sandbox.ts +++ b/packages/stryker/src/Sandbox.ts @@ -11,7 +11,7 @@ import ResilientTestRunnerFactory from './isolated-runner/ResilientTestRunnerFac import IsolatedRunnerOptions from './isolated-runner/IsolatedRunnerOptions'; import { TempFolder } from './utils/TempFolder'; import * as fileUtils from './utils/fileUtils'; -import TestableMutant from './TestableMutant'; +import TestableMutant, { TestSelectionResult } from './TestableMutant'; import TranspiledMutant from './TranspiledMutant'; interface FileMap { @@ -65,6 +65,9 @@ export default class Sandbox { public async runMutant(transpiledMutant: TranspiledMutant): Promise { const mutantFiles = transpiledMutant.transpileResult.outputFiles; + if (transpiledMutant.mutant.testSelectionResult === TestSelectionResult.Failed) { + this.log.warn(`Failed find coverage data for this mutant, running all tests. This might have an impact on performance: ${transpiledMutant.mutant.toString()}`); + } await Promise.all(mutantFiles.map(mutatedFile => this.writeFileInSandbox(mutatedFile)).concat(this.filterTests(transpiledMutant.mutant))); const runResult = await this.run(this.calculateTimeout(transpiledMutant.mutant)); await this.reset(mutantFiles); diff --git a/packages/stryker/src/Stryker.ts b/packages/stryker/src/Stryker.ts index 3d9721be0f..573544089a 100644 --- a/packages/stryker/src/Stryker.ts +++ b/packages/stryker/src/Stryker.ts @@ -1,3 +1,4 @@ +import 'source-map-support/register'; import { Config, ConfigEditorFactory } from 'stryker-api/config'; import { StrykerOptions, File } from 'stryker-api/core'; import { MutantResult } from 'stryker-api/report'; @@ -18,6 +19,7 @@ import StrictReporter from './reporters/StrictReporter'; import MutatorFacade from './MutatorFacade'; import InitialTestExecutor, { InitialTestRunResult } from './process/InitialTestExecutor'; import MutationTestExecutor from './process/MutationTestExecutor'; +import SourceMapper from './transpiler/SourceMapper'; export default class Stryker { @@ -49,7 +51,7 @@ export default class Stryker { this.timer.reset(); const inputFiles = await new InputFileResolver(this.config.mutate, this.config.files, this.reporter).resolve(); TempFolder.instance().initialize(); - const initialTestRunProcess = this.createInitialTestRunner(inputFiles); + const initialTestRunProcess = this.createInitialTestRunProcess(inputFiles); const initialTestRunResult = await initialTestRunProcess.run(); const testableMutants = await this.mutate(inputFiles, initialTestRunResult); if (initialTestRunResult.runResult.tests.length && testableMutants.length) { @@ -77,6 +79,7 @@ export default class Stryker { mutants, inputFiles, initialTestRunResult.runResult, + SourceMapper.create(initialTestRunResult.transpiledFiles, this.config), initialTestRunResult.coverageMaps, this.config, this.reporter); @@ -123,7 +126,7 @@ export default class Stryker { return new MutationTestExecutor(this.config, inputFiles, this.testFramework, this.reporter); } - private createInitialTestRunner(inputFiles: File[]) { + private createInitialTestRunProcess(inputFiles: File[]) { return new InitialTestExecutor(this.config, inputFiles, this.testFramework, this.timer); } diff --git a/packages/stryker/src/TestableMutant.ts b/packages/stryker/src/TestableMutant.ts index adda101e12..70c4dd358a 100644 --- a/packages/stryker/src/TestableMutant.ts +++ b/packages/stryker/src/TestableMutant.ts @@ -6,6 +6,11 @@ import { MutantStatus, MutantResult } from 'stryker-api/report'; import { freezeRecursively } from './utils/objectUtils'; import { TestSelection } from 'stryker-api/test_framework'; +export enum TestSelectionResult { + Failed, + FailedButAlreadyReporter, + Success +} export default class TestableMutant { @@ -13,6 +18,7 @@ export default class TestableMutant { public specsRan: string[] = []; private _timeSpentScopedTests = 0; private _location: Location; + testSelectionResult = TestSelectionResult.Success; get selectedTests(): TestSelection[] { return this._selectedTests; @@ -59,16 +65,17 @@ export default class TestableMutant { return this.sourceFile.content; } - public addAllTestResults(runResult: RunResult) { - runResult.tests.forEach((testResult, id) => this.addTestResult(id, testResult)); + public selectAllTests(runResult: RunResult, testSelectionResult: TestSelectionResult) { + this.testSelectionResult = testSelectionResult; + runResult.tests.forEach((testResult, id) => this.selectTest(testResult, id)); } - public addTestResult(index: number, testResult: TestResult) { + public selectTest(testResult: TestResult, index: number) { this._selectedTests.push({ id: index, name: testResult.name }); this._timeSpentScopedTests += testResult.timeSpentMs; } - constructor(public mutant: Mutant, public sourceFile: SourceFile) { + constructor(public readonly id: string, public mutant: Mutant, public sourceFile: SourceFile) { } public get originalLines() { @@ -95,6 +102,7 @@ export default class TestableMutant { public result(status: MutantStatus, testsRan: string[]): MutantResult { return freezeRecursively({ + id: this.id, sourceFilePath: this.fileName, mutatorName: this.mutatorName, status, @@ -107,4 +115,8 @@ export default class TestableMutant { }); } + toString() { + return `${this.mutant.mutatorName}: (${this.replacement}) file://${this.fileName}:${this.location.start.line + 1}:${this.location.start.column}`; + } + } \ No newline at end of file diff --git a/packages/stryker/src/TranspiledMutant.ts b/packages/stryker/src/TranspiledMutant.ts index 4e3aa0cd53..0d96b31069 100644 --- a/packages/stryker/src/TranspiledMutant.ts +++ b/packages/stryker/src/TranspiledMutant.ts @@ -1,6 +1,31 @@ import TestableMutant from './TestableMutant'; import { TranspileResult } from 'stryker-api/transpile'; +import { File, TextFile } from 'stryker-api/core'; export default class TranspiledMutant { - constructor(public mutant: TestableMutant, public transpileResult: TranspileResult) { } + + /** + * Creates a transpiled mutant + * @param mutant The mutant which is just transpiled + * @param transpileResult The transpile result of the mutant + * @param changedAnyTranspiledFiles Indicated whether or not this mutant changed the transpiled output files. This is not always the case, for example: mutating a TS interface + */ + constructor(public mutant: TestableMutant, public transpileResult: TranspileResult, public changedAnyTranspiledFiles: boolean) { } + + static create(mutant: TestableMutant, transpileResult: TranspileResult, unMutatedFiles: File[]) { + return new TranspiledMutant(mutant, transpileResult, someFilesChanged()); + + function someFilesChanged(): boolean { + return transpileResult.outputFiles.some(file => fileChanged(file)); + } + + function fileChanged(file: File) { + if (unMutatedFiles) { + const unMutatedFile = unMutatedFiles.find(f => f.name === file.name); + return !unMutatedFile || (unMutatedFile as TextFile).content !== (file as TextFile).content; + } else { + return true; + } + } + } } \ No newline at end of file diff --git a/packages/stryker/src/process/InitialTestExecutor.ts b/packages/stryker/src/process/InitialTestExecutor.ts index 04221b71ea..72b18a8234 100644 --- a/packages/stryker/src/process/InitialTestExecutor.ts +++ b/packages/stryker/src/process/InitialTestExecutor.ts @@ -51,7 +51,11 @@ export default class InitialTestExecutor { const sandbox = await Sandbox.create(this.options, 0, transpileResult.outputFiles, this.testFramework); const runResult = await sandbox.run(INITIAL_RUN_TIMEOUT); await sandbox.dispose(); - return { runResult, transpiledFiles: transpileResult.outputFiles, coverageMaps: coverageInstrumenterTranspiler.fileCoveragePerFile }; + return { + runResult, + transpiledFiles: transpileResult.outputFiles, + coverageMaps: coverageInstrumenterTranspiler.fileCoverageMaps + }; } } @@ -97,7 +101,11 @@ export default class InitialTestExecutor { * which is used to instrument for code coverage when needed. */ private createTranspilerFacade(coverageInstrumenterTranspiler: CoverageInstrumenterTranspiler): Transpiler { - const transpilerSettings: TranspilerOptions = { config: this.options, keepSourceMaps: true }; + // Let the transpiler produce source maps only if coverage analysis is enabled + const transpilerSettings: TranspilerOptions = { + config: this.options, + produceSourceMaps: this.options.coverageAnalysis !== 'off' + }; return new TranspilerFacade(transpilerSettings, { name: CoverageInstrumenterTranspiler.name, transpiler: coverageInstrumenterTranspiler @@ -105,7 +113,7 @@ export default class InitialTestExecutor { } private createCoverageInstrumenterTranspiler() { - return new CoverageInstrumenterTranspiler({ keepSourceMaps: true, config: this.options }, this.testFramework); + return new CoverageInstrumenterTranspiler({ produceSourceMaps: true, config: this.options }, this.testFramework); } private logTranspileResult(transpileResult: TranspileResult) { diff --git a/packages/stryker/src/process/MutationTestExecutor.ts b/packages/stryker/src/process/MutationTestExecutor.ts index 1a75269bb7..ee7a0f3cec 100644 --- a/packages/stryker/src/process/MutationTestExecutor.ts +++ b/packages/stryker/src/process/MutationTestExecutor.ts @@ -13,7 +13,6 @@ import SandboxPool from '../SandboxPool'; export default class MutationTestExecutor { - constructor(private config: Config, private inputFiles: File[], private testFramework: TestFramework | null, private reporter: StrictReporter) { } @@ -46,6 +45,7 @@ export default class MutationTestExecutor { return transpiledMutants .zip(recycled.merge(sandboxes), createTuple) + .map(earlyResult) .flatMap(runInSandbox) .do(recycle) .map(({ result }) => result) @@ -57,13 +57,25 @@ export default class MutationTestExecutor { } } -function runInSandbox([transpiledMutant, sandbox]: [TranspiledMutant, Sandbox]): Promise<{ sandbox: Sandbox, result: MutantResult }> { +function earlyResult([transpiledMutant, sandbox]: [TranspiledMutant, Sandbox]): [TranspiledMutant, Sandbox, MutantResult | null] { if (transpiledMutant.transpileResult.error) { const result = transpiledMutant.mutant.result(MutantStatus.TranspileError, []); - return Promise.resolve({ sandbox, result }); + return [transpiledMutant, sandbox, result]; } else if (!transpiledMutant.mutant.selectedTests.length) { const result = transpiledMutant.mutant.result(MutantStatus.NoCoverage, []); - return Promise.resolve({ sandbox, result }); + return [transpiledMutant, sandbox, result]; + } else if (!transpiledMutant.changedAnyTranspiledFiles) { + const result = transpiledMutant.mutant.result(MutantStatus.Survived, []); + return [transpiledMutant, sandbox, result]; + } else { + // No early result possible, need to run in the sandbox later + return [transpiledMutant, sandbox, null]; + } +} + +function runInSandbox([transpiledMutant, sandbox, earlyResult]: [TranspiledMutant, Sandbox, MutantResult | null]): Promise<{ sandbox: Sandbox, result: MutantResult }> { + if (earlyResult) { + return Promise.resolve({ sandbox, result: earlyResult }); } else { return sandbox.runMutant(transpiledMutant) .then(runResult => ({ sandbox, result: collectMutantResult(transpiledMutant.mutant, runResult) })); @@ -107,6 +119,7 @@ function reportResult(reporter: StrictReporter) { reporter.onMutantTested(mutantResult); }; } + function reportAll(reporter: StrictReporter) { return (mutantResults: MutantResult[]) => { reporter.onAllMutantsTested(mutantResults); diff --git a/packages/stryker/src/reporters/ProgressKeeper.ts b/packages/stryker/src/reporters/ProgressKeeper.ts index 1ad2e8b7be..051693d834 100644 --- a/packages/stryker/src/reporters/ProgressKeeper.ts +++ b/packages/stryker/src/reporters/ProgressKeeper.ts @@ -1,4 +1,5 @@ -import { MatchedMutant, Reporter, MutantResult, MutantStatus } from 'stryker-api/report'; +import { MatchedMutant, Reporter, MutantResult } from 'stryker-api/report'; +import { MutantStatus } from 'stryker-api/report'; abstract class ProgressKeeper implements Reporter { @@ -8,19 +9,19 @@ abstract class ProgressKeeper implements Reporter { total: 0 }; + private mutantIdsWithoutCoverage: string[]; + onAllMutantsMatchedWithTests(matchedMutants: ReadonlyArray): void { - this.progress.total = matchedMutants.filter(m => m.scopedTestIds.length > 0).length; + this.mutantIdsWithoutCoverage = matchedMutants.filter(m => m.scopedTestIds.length === 0).map(m => m.id); + this.progress.total = matchedMutants.length - this.mutantIdsWithoutCoverage.length; } onMutantTested(result: MutantResult): void { - this.progress.tested++; - switch (result.status) { - case MutantStatus.NoCoverage: - this.progress.tested--; // correct for not tested, because no coverage - break; - case MutantStatus.Survived: - this.progress.survived++; - break; + if (!this.mutantIdsWithoutCoverage.some(id => result.id === id)) { + this.progress.tested++; + } + if (result.status === MutantStatus.Survived) { + this.progress.survived++; } } } diff --git a/packages/stryker/src/transpiler/CoverageInstrumenterTranspiler.ts b/packages/stryker/src/transpiler/CoverageInstrumenterTranspiler.ts index e618417d3b..e99f6527a9 100644 --- a/packages/stryker/src/transpiler/CoverageInstrumenterTranspiler.ts +++ b/packages/stryker/src/transpiler/CoverageInstrumenterTranspiler.ts @@ -1,4 +1,4 @@ -import { Transpiler, TranspileResult, FileLocation, TranspilerOptions } from 'stryker-api/transpile'; +import { Transpiler, TranspileResult, TranspilerOptions } from 'stryker-api/transpile'; import { File, FileKind, TextFile } from 'stryker-api/core'; import { createInstrumenter, Instrumenter } from 'istanbul-lib-instrument'; import { errorToString, wrapInClosure } from '../utils/objectUtils'; @@ -20,7 +20,7 @@ export interface CoverageMapsByFile { export default class CoverageInstrumenterTranspiler implements Transpiler { private instrumenter: Instrumenter; - public fileCoveragePerFile: CoverageMapsByFile = Object.create(null); + public fileCoverageMaps: CoverageMapsByFile = Object.create(null); private log: Logger; constructor(private settings: TranspilerOptions, private testFramework: TestFramework | null) { @@ -40,10 +40,6 @@ export default class CoverageInstrumenterTranspiler implements Transpiler { } } - public getMappedLocation(sourceFileLocation: FileLocation): FileLocation { - return sourceFileLocation; - } - /** * Coverage variable *must* have the name '__coverage__'. Only that variable * is reported back to the TestRunner process when using one of the karma @@ -95,7 +91,7 @@ export default class CoverageInstrumenterTranspiler implements Transpiler { try { const content = this.instrumenter.instrumentSync(sourceFile.content, sourceFile.name); const fileCoverage = this.patchRanges(this.instrumenter.lastFileCoverage()); - this.fileCoveragePerFile[sourceFile.name] = this.retrieveCoverageMaps(fileCoverage); + this.fileCoverageMaps[sourceFile.name] = this.retrieveCoverageMaps(fileCoverage); return { mutated: sourceFile.mutated, included: sourceFile.included, @@ -119,7 +115,7 @@ export default class CoverageInstrumenterTranspiler implements Transpiler { } private addCollectCoverageFileIfNeeded(result: TranspileResult): TranspileResult { - if (Object.keys(this.fileCoveragePerFile).length && this.settings.config.coverageAnalysis === 'perTest') { + if (Object.keys(this.fileCoverageMaps).length && this.settings.config.coverageAnalysis === 'perTest') { if (this.testFramework) { // Add piece of javascript to collect coverage per test results const content = this.coveragePerTestFileContent(this.testFramework); diff --git a/packages/stryker/src/transpiler/MutantTranspiler.ts b/packages/stryker/src/transpiler/MutantTranspiler.ts index 72a286475d..f4121cc771 100644 --- a/packages/stryker/src/transpiler/MutantTranspiler.ts +++ b/packages/stryker/src/transpiler/MutantTranspiler.ts @@ -5,7 +5,7 @@ import TestableMutant from '../TestableMutant'; import { File, TextFile, FileKind } from 'stryker-api/core'; import SourceFile from '../SourceFile'; import ChildProcessProxy, { ChildProxy } from '../child-proxy/ChildProcessProxy'; -import { TranspileResult, FileLocation } from 'stryker-api/transpile'; +import { TranspileResult, TranspilerOptions } from 'stryker-api/transpile'; import TranspiledMutant from '../TranspiledMutant'; export default class MutantTranspiler { @@ -13,6 +13,7 @@ export default class MutantTranspiler { private transpilerChildProcess: ChildProcessProxy | undefined; private proxy: ChildProxy; private currentMutatedFile: SourceFile; + private unMutatedFiles: File[]; /** * Creates the mutant transpiler in a child process if one is defined. @@ -20,7 +21,7 @@ export default class MutantTranspiler { * @param config The Stryker config */ constructor(config: Config) { - const transpilerOptions = { config, keepSourceMaps: false }; + const transpilerOptions: TranspilerOptions = { config, produceSourceMaps: false }; if (config.transpilers.length) { this.transpilerChildProcess = ChildProcessProxy.create( require.resolve('./TranspilerFacade'), @@ -31,20 +32,15 @@ export default class MutantTranspiler { ); this.proxy = this.transpilerChildProcess.proxy; } else { - let transpiler = new TranspilerFacade(transpilerOptions); - this.proxy = { - transpile(files: File[]) { - return Promise.resolve(transpiler.transpile(files)); - }, - getMappedLocation(sourceFileLocation: FileLocation) { - return Promise.resolve(transpiler.getMappedLocation(sourceFileLocation)); - } - }; + this.proxy = new TranspilerFacade(transpilerOptions); } } initialize(files: File[]): Promise { - return this.proxy.transpile(files); + return this.proxy.transpile(files).then((transpileResult: TranspileResult) => { + this.unMutatedFiles = transpileResult.outputFiles; + return transpileResult; + }); } transpileMutants(allMutants: TestableMutant[]): Observable { @@ -54,7 +50,7 @@ export default class MutantTranspiler { const mutant = mutants.shift(); if (mutant) { this.transpileMutant(mutant) - .then(transpileResult => observer.next({ mutant, transpileResult })) + .then(transpileResult => observer.next(TranspiledMutant.create(mutant, transpileResult, this.unMutatedFiles))) .then(nextMutant) .catch(error => observer.error(error)); } else { diff --git a/packages/stryker/src/transpiler/SourceMapper.ts b/packages/stryker/src/transpiler/SourceMapper.ts new file mode 100644 index 0000000000..4c729ecba6 --- /dev/null +++ b/packages/stryker/src/transpiler/SourceMapper.ts @@ -0,0 +1,223 @@ +import * as path from 'path'; +import { SourceMapConsumer, RawSourceMap } from 'source-map'; +import { File, FileKind, TextFile, Location, Position } from 'stryker-api/core'; +import { Config } from 'stryker-api/config'; +import { base64Decode } from '../utils/objectUtils'; + +const SOURCE_MAP_URL_REGEX = /\/\/\s*#\s*sourceMappingURL=(.*)/g; + + +// This file contains source mapping logic. +// It reads transpiled output files (*.js) and scans it for comments like these: sourceMappingURL=*.js.map +// If it finds it, it will use mozilla's source-map to implement the `transpiledLocationFor` method. + + +export interface MappedLocation { + fileName: string; + location: Location; +} + +export class SourceMapError extends Error { + constructor(message: string) { + super(`${message}. Cannot analyse code coverage. Setting \`coverageAnalysis: "off"\` in your stryker.conf.js will prevent this error, but forces Stryker to run each test for each mutant.`); + Error.captureStackTrace(this, SourceMapError); + // TS recommendation: https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work + Object.setPrototypeOf(this, SourceMapError.prototype); + } +} + +/** + * Represents an object that can calculated a transpiled location for a given original location + * It is implemented with the [composite pattern](https://en.wikipedia.org/wiki/Composite_pattern) + * Use the `create` method to retrieve a specific `SourceMapper` implementation + */ +export default abstract class SourceMapper { + /** + * Calculated a transpiled location for a given original location + * @param originalLocation The original location to be converted to a transpiled location + */ + abstract transpiledLocationFor(originalLocation: MappedLocation): MappedLocation; + + static create(transpiledFiles: File[], config: Config): SourceMapper { + if (config.transpilers.length && config.coverageAnalysis !== 'off') { + return new TranspiledSourceMapper(transpiledFiles); + } else { + return new PassThroughSourceMapper(); + } + } +} + +export class TranspiledSourceMapper extends SourceMapper { + + private sourceMaps: SourceMapBySource; + + constructor(private transpiledFiles: File[]) { + super(); + } + + /** + * @inheritdoc + */ + transpiledLocationFor(originalLocation: MappedLocation): MappedLocation { + const sourceMap = this.getSourceMap(originalLocation.fileName); + if (!sourceMap) { + throw new SourceMapError(`Source map not found for "${originalLocation.fileName}"`); + } else { + const relativeSource = this.getRelativeSource(sourceMap, originalLocation); + const start = sourceMap.generatedPositionFor(originalLocation.location.start, relativeSource); + const end = sourceMap.generatedPositionFor(originalLocation.location.end, relativeSource); + return { + fileName: sourceMap.transpiledFile.name, + location: { + start, + end + } + }; + } + } + + private getRelativeSource(from: SourceMap, to: MappedLocation) { + return path.relative(path.dirname(from.sourceMapFileName), to.fileName) + .replace(/\\/g, '/'); + } + + /** + * Gets the source map for given file + */ + private getSourceMap(sourceFileName: string): SourceMap | undefined { + if (!this.sourceMaps) { + this.sourceMaps = this.createSourceMaps(); + } + return this.sourceMaps[path.resolve(sourceFileName)]; + } + + /** + * Creates all source maps for lazy loading purposes + */ + private createSourceMaps(): SourceMapBySource { + const sourceMaps: SourceMapBySource = Object.create(null); + this.transpiledFiles.forEach(transpiledFile => { + if (transpiledFile.mutated && transpiledFile.kind === FileKind.Text) { + const sourceMapFile = this.getSourceMapForFile(transpiledFile); + const rawSourceMap: RawSourceMap = JSON.parse(sourceMapFile.content); + const sourceMap = new SourceMap(transpiledFile, sourceMapFile.name, rawSourceMap); + rawSourceMap.sources.forEach(source => { + const sourceFileName = path.resolve(path.dirname(sourceMapFile.name), source); + sourceMaps[sourceFileName] = sourceMap; + }); + } + }); + return sourceMaps; + } + + private getSourceMapForFile(transpiledFile: TextFile) { + const sourceMappingUrl = this.getSourceMapUrl(transpiledFile); + const sourceMapFile = this.getSourceMapFileFromUrl(sourceMappingUrl, transpiledFile); + return sourceMapFile; + } + + /** + * Gets the source map file from a url. + * @param sourceMapUrl The source map url. Can be a data url (data:application/json;base64,ABC...), or an actual file url + * @param transpiledFile The transpiled file for which the data url is + */ + private getSourceMapFileFromUrl(sourceMapUrl: string, transpiledFile: File): TextFile { + const sourceMapFile = this.isInlineUrl(sourceMapUrl) ? + this.getInlineSourceMap(sourceMapUrl, transpiledFile) : this.getExternalSourceMap(sourceMapUrl, transpiledFile); + if (sourceMapFile.kind === FileKind.Text) { + return sourceMapFile; + } else { + throw new SourceMapError(`Source map file "${sourceMapFile.name}" has the wrong file kind. "${FileKind[sourceMapFile.kind]}" instead of "${FileKind[FileKind.Text]}"`); + } + } + + private isInlineUrl(sourceMapUrl: string) { + return sourceMapUrl.startsWith('data:'); + } + + /** + * Gets the source map from a data url + */ + private getInlineSourceMap(sourceMapUrl: string, transpiledFile: File): TextFile { + const supportedDataPrefix = 'data:application/json;base64,'; + if (sourceMapUrl.startsWith(supportedDataPrefix)) { + const content = base64Decode(sourceMapUrl.substr(supportedDataPrefix.length)); + return { + name: transpiledFile.name, + content, + kind: FileKind.Text, + included: false, + mutated: false, + transpiled: false + }; + } else { + throw new SourceMapError(`Source map file for "${transpiledFile.name}" cannot be read. Data url "${sourceMapUrl.substr(0, sourceMapUrl.lastIndexOf(','))}" found, where "${supportedDataPrefix.substr(0, supportedDataPrefix.length - 1)}" was expected`); + } + } + + /** + * Gets the source map from a file + */ + private getExternalSourceMap(sourceMapUrl: string, transpiledFile: File) { + const sourceMapFileName = path.resolve(path.dirname(transpiledFile.name), sourceMapUrl); + const sourceMapFile = this.transpiledFiles.find(file => path.resolve(file.name) === sourceMapFileName); + if (sourceMapFile) { + return sourceMapFile; + } else { + throw new SourceMapError(`Source map file "${sourceMapUrl}" (referenced by "${transpiledFile.name}") cannot be found in list of transpiled files`); + } + } + + /** + * Gets the source map url from a transpiled file (the last comment with sourceMappingURL= ...) + */ + private getSourceMapUrl(transpiledFile: TextFile): string { + SOURCE_MAP_URL_REGEX.lastIndex = 0; + let currentMatch: RegExpExecArray | null; + let lastMatch: RegExpExecArray | null = null; + // Retrieve the final sourceMappingURL comment in the file + while (currentMatch = SOURCE_MAP_URL_REGEX.exec(transpiledFile.content)) { + lastMatch = currentMatch; + } + if (lastMatch) { + return lastMatch[1]; + } else { + throw new SourceMapError(`No source map reference found in transpiled file "${transpiledFile.name}"`); + } + } +} + + +export class PassThroughSourceMapper extends SourceMapper { + + /** + * @inheritdoc + */ + transpiledLocationFor(originalLocation: MappedLocation): MappedLocation { + return originalLocation; + } +} + + +class SourceMap { + private sourceMap: SourceMapConsumer; + constructor(public transpiledFile: TextFile, public sourceMapFileName: string, rawSourceMap: RawSourceMap) { + this.sourceMap = new SourceMapConsumer(rawSourceMap); + } + generatedPositionFor(originalPosition: Position, relativeSource: string): Position { + const transpiledPosition = this.sourceMap.generatedPositionFor({ + bias: SourceMapConsumer.LEAST_UPPER_BOUND, + column: originalPosition.column, + line: originalPosition.line + 1, // SourceMapConsumer works 1-based + source: relativeSource + }); + return { + line: transpiledPosition.line - 1, // Stryker works 0-based + column: transpiledPosition.column + }; + } +} + +interface SourceMapBySource { + [sourceFileName: string]: SourceMap; +} diff --git a/packages/stryker/src/transpiler/TranspilerFacade.ts b/packages/stryker/src/transpiler/TranspilerFacade.ts index 925f0f451c..0bfd598249 100644 --- a/packages/stryker/src/transpiler/TranspilerFacade.ts +++ b/packages/stryker/src/transpiler/TranspilerFacade.ts @@ -1,5 +1,5 @@ import { File } from 'stryker-api/core'; -import { Transpiler, FileLocation, TranspileResult, TranspilerOptions, TranspilerFactory } from 'stryker-api/transpile'; +import { Transpiler, TranspileResult, TranspilerOptions, TranspilerFactory } from 'stryker-api/transpile'; class NamedTranspiler { constructor(public name: string, public transpiler: Transpiler) { } @@ -21,22 +21,6 @@ export default class TranspilerFacade implements Transpiler { return this.performTranspileChain(this.createPassThruTranspileResult(files)); } - public getMappedLocation(sourceFileLocation: FileLocation): FileLocation { - return this.performMappedLocationChain(sourceFileLocation); - } - - private performMappedLocationChain( - sourceFileLocation: FileLocation, - remainingChain: NamedTranspiler[] = this.innerTranspilers.slice() - ): FileLocation { - const next = remainingChain.shift(); - if (next) { - return this.performMappedLocationChain(next.transpiler.getMappedLocation(sourceFileLocation), remainingChain); - } else { - return sourceFileLocation; - } - } - private async performTranspileChain( currentResult: TranspileResult, remainingChain: NamedTranspiler[] = this.innerTranspilers.slice() diff --git a/packages/stryker/src/utils/LocationHelper.ts b/packages/stryker/src/utils/LocationHelper.ts new file mode 100644 index 0000000000..535643a0e6 --- /dev/null +++ b/packages/stryker/src/utils/LocationHelper.ts @@ -0,0 +1,41 @@ +import { Location } from 'stryker-api/core'; + +export default class LocationHelper { + + static MAX_VALUE = new LocationHelper(Object.freeze({ + start: Object.freeze({ column: 0, line: -1 }), + end: Object.freeze({ column: Number.POSITIVE_INFINITY, line: Number.POSITIVE_INFINITY }) + })); + + constructor(private loc: Location) { } + + /** + * Indicates whether the current location is covered by an other location. + * @param maybeWrapper The location that is questioned to be wrapping this location. + * @returns true if this location is covered by given location, otherwise false + */ + isCoveredBy(maybeWrapper: Location): boolean { + let isAfterStart = this.loc.start.line > maybeWrapper.start.line || + (this.loc.start.line === maybeWrapper.start.line && this.loc.start.column >= maybeWrapper.start.column); + let isBeforeEnd = this.loc.end.line < maybeWrapper.end.line || + (this.loc.end.line === maybeWrapper.end.line && this.loc.end.column <= maybeWrapper.end.column); + return isAfterStart && isBeforeEnd; + } + + + /** + * Indicates whether the given location is smaller than this location. + * @param maybeSmaller The area which is questioned to cover a smaller area than this location. + * @returns true if the given location covers a smaller area than this one. + */ + isSmallerArea(maybeSmaller: Location) { + let firstLocationHasSmallerArea = false; + let lineDifference = (this.loc.end.line - this.loc.start.line) - (maybeSmaller.end.line - maybeSmaller.start.line); + let coversLessLines = lineDifference > 0; + let coversLessColumns = lineDifference === 0 && (maybeSmaller.start.column - this.loc.start.column) + (this.loc.end.column - maybeSmaller.end.column) > 0; + if (coversLessLines || coversLessColumns) { + firstLocationHasSmallerArea = true; + } + return firstLocationHasSmallerArea; + } +} \ No newline at end of file diff --git a/packages/stryker/src/utils/objectUtils.ts b/packages/stryker/src/utils/objectUtils.ts index 44f53d1096..b8f11e5958 100644 --- a/packages/stryker/src/utils/objectUtils.ts +++ b/packages/stryker/src/utils/objectUtils.ts @@ -77,4 +77,8 @@ export function wrapInClosure(codeFragment: string) { */ export function setExitCode(n: number) { process.exitCode = n; +} + +export function base64Decode(base64EncodedString: string) { + return Buffer.from(base64EncodedString, 'base64').toString('utf8'); } \ No newline at end of file diff --git a/packages/stryker/stryker.conf.js b/packages/stryker/stryker.conf.js index e4a18f4ed2..34acc6a369 100644 --- a/packages/stryker/stryker.conf.js +++ b/packages/stryker/stryker.conf.js @@ -1,7 +1,7 @@ module.exports = function (config) { var typescript = true; - var es6 = false; + var es6 = true; if (typescript) { config.set({ @@ -10,10 +10,11 @@ module.exports = function (config) { '!test/integration/**/*.ts', '!src/**/*.ts', { pattern: 'src/**/*.ts', included: false, mutated: true }, + '!src/**/*.d.ts', { pattern: 'node_modules/stryker-api/*.js', included: false, mutated: false, transpiled: false }, { pattern: 'node_modules/stryker-api/src/**/*.js', included: false, mutated: false, transpiled: false } ], - coverageAnalysis: 'off', + coverageAnalysis: 'perTest', tsconfigFile: 'tsconfig.json', mutator: 'typescript', transpilers: [ @@ -37,6 +38,7 @@ module.exports = function (config) { config.set({ testFramework: 'mocha', testRunner: 'mocha', + maxConcurrentTestRunners: 5, reporter: ['progress', 'html', 'clear-text', 'event-recorder'], thresholds: { high: 80, diff --git a/packages/stryker/test/helpers/producers.ts b/packages/stryker/test/helpers/producers.ts index 5e9d5b0e16..8ea78b5f57 100644 --- a/packages/stryker/test/helpers/producers.ts +++ b/packages/stryker/test/helpers/producers.ts @@ -1,6 +1,6 @@ import { TestResult, TestStatus, RunResult, RunStatus } from 'stryker-api/test_runner'; import { Mutant } from 'stryker-api/mutant'; -import { FileLocation, TranspileResult } from 'stryker-api/transpile'; +import { TranspileResult } from 'stryker-api/transpile'; import { Config } from 'stryker-api/config'; import * as sinon from 'sinon'; import { TestFramework, TestSelection } from 'stryker-api/test_framework'; @@ -12,6 +12,7 @@ import TranspiledMutant from '../../src/TranspiledMutant'; import { Logger } from 'log4js'; import { FileCoverageData } from 'istanbul-lib-coverage'; import { CoverageMaps } from '../../src/transpiler/CoverageInstrumenterTranspiler'; +import { MappedLocation } from '../../src/transpiler/SourceMapper'; export type Mock = { [P in keyof T]: sinon.SinonStub; @@ -53,6 +54,7 @@ function factoryMethod(defaultsFactory: () => T) { export const location = factoryMethod(() => ({ start: { line: 0, column: 0 }, end: { line: 0, column: 0 } })); export const mutantResult = factoryMethod(() => ({ + id: '256', location: location(), mutatedLines: '', mutatorName: '', @@ -107,10 +109,10 @@ export const textFile = factory({ kind: FileKind.Text }); -export const fileLocation = factory({ - fileName: 'fileName', - start: { line: 0, column: 0 }, end: { line: 0, column: 0 } -}); +export const mappedLocation = factoryMethod(() => ({ + fileName: 'file.js', + location: location() +})); export const coverageMaps = factoryMethod(() => ({ statementMap: {}, @@ -204,12 +206,13 @@ export const ALL_REPORTER_EVENTS: Array = ['onSourceFileRead', 'onAllSourceFilesRead', 'onAllMutantsMatchedWithTests', 'onMutantTested', 'onAllMutantsTested', 'onScoreCalculated', 'wrapUp']; -export function matchedMutant(numberOfTests: number): MatchedMutant { +export function matchedMutant(numberOfTests: number, mutantId = numberOfTests.toString()): MatchedMutant { let scopedTestIds: number[] = []; for (let i = 0; i < numberOfTests; i++) { scopedTestIds.push(1); } return { + id: mutantId, mutatorName: '', scopedTestIds: scopedTestIds, timeSpentScopedTests: 0, @@ -225,7 +228,7 @@ export const transpileResult = factoryMethod(() => ({ export const sourceFile = () => new SourceFile(textFile()); -export const testableMutant = (fileName = 'file') => new TestableMutant(mutant({ +export const testableMutant = (fileName = 'file') => new TestableMutant('1337', mutant({ range: [12, 13], replacement: '-', fileName @@ -234,4 +237,4 @@ export const testableMutant = (fileName = 'file') => new TestableMutant(mutant({ )); export const transpiledMutant = (fileName = 'file') => - new TranspiledMutant(testableMutant(fileName), transpileResult()); \ No newline at end of file + new TranspiledMutant(testableMutant(fileName), transpileResult(), true); \ No newline at end of file diff --git a/packages/stryker/test/integration/source-mapper/SourceMapperIT.ts b/packages/stryker/test/integration/source-mapper/SourceMapperIT.ts new file mode 100644 index 0000000000..559a4edcf8 --- /dev/null +++ b/packages/stryker/test/integration/source-mapper/SourceMapperIT.ts @@ -0,0 +1,81 @@ +import * as fs from 'mz/fs'; +import * as path from 'path'; +import { File } from 'stryker-api/core'; +import { textFile } from '../../helpers/producers'; +import { TranspiledSourceMapper } from '../../../src/transpiler/SourceMapper'; +import { expect } from 'chai'; + +function resolve(...filePart: string[]) { + return path.resolve(__dirname, '..', '..', '..', 'testResources', 'source-mapper', ...filePart); +} + +function readFiles(...files: string[]): Promise { + return Promise.all(files + .map(relative => resolve(relative)) + .map(fileName => fs.readFile(fileName, 'utf8').then(content => { + const isMapFile = path.extname(fileName) === '.map'; + return textFile({ + content, + name: fileName, + transpiled: !isMapFile, + mutated: !isMapFile + }); + }))); +} + +describe('Source mapper integration', () => { + + let sut: TranspiledSourceMapper; + + describe('with external source maps', () => { + beforeEach(async () => { + const files = await readFiles( + path.join('external-source-maps', 'js', 'math.js'), + path.join('external-source-maps', 'js', 'math.js.map')); + sut = new TranspiledSourceMapper(files); + }); + + it('it should be able to map to transpiled location', async () => { + const actual = sut.transpiledLocationFor({ + fileName: resolve('external-source-maps', 'ts', 'src', 'math.ts'), + location: { + start: { line: 7, column: 8 }, + end: { line: 7, column: 42 } + } + }); + expect(actual).deep.eq({ + fileName: resolve('external-source-maps', 'js', 'math.js'), + location: { + start: { line: 15, column: 10 }, + end: { line: 16, column: 0 } + } + }); + }); + }); + + describe('with inline source maps', () => { + beforeEach(async () => { + const files = await readFiles(path.join('inline-source-maps', 'js', 'math.js')); + sut = new TranspiledSourceMapper(files); + }); + it('it should be able to map to transpiled location', async () => { + const actual = sut.transpiledLocationFor({ + fileName: resolve('inline-source-maps', 'ts', 'src', 'math.ts'), + location: { + start: { line: 7, column: 8 }, + end: { line: 7, column: 42 } + } + }); + expect(actual).deep.eq({ + fileName: resolve('inline-source-maps', 'js', 'math.js'), + location: { + start: { line: 15, column: 10 }, + end: { line: 16, column: 0 } + } + }); + }); + }); + + +}); + diff --git a/packages/stryker/test/unit/ConfigValidatorSpec.ts b/packages/stryker/test/unit/ConfigValidatorSpec.ts index 366684e978..9d66542d63 100644 --- a/packages/stryker/test/unit/ConfigValidatorSpec.ts +++ b/packages/stryker/test/unit/ConfigValidatorSpec.ts @@ -67,13 +67,16 @@ describe('ConfigValidator', () => { }); }); - it('should downgrade coverageAnalysis when transpilers are specified (for now)', () => { + it('should be invalid with coverageAnalysis when 2 transpilers are specified (for now)', () => { config.transpilers.push('a transpiler'); + config.transpilers.push('a second transpiler'); config.coverageAnalysis = 'all'; sut = new ConfigValidator(config, testFramework()); sut.validate(); - expect(log.info).calledWith('Disabled coverage analysis for this run (off). Coverage analysis using transpilers is not supported yet.'); - expect(config.coverageAnalysis).eq('off'); + expect(log.fatal).calledWith('Value "all" for `coverageAnalysis` is invalid with multiple transpilers' + + ' (configured transpilers: a transpiler, a second transpiler). Please report this to the Stryker team' + + ' if you whish this feature to be implemented'); + expect(exitStub).calledWith(1); }); it('should be invalid with invalid logLevel', () => { diff --git a/packages/stryker/test/unit/MutantTestMatcherSpec.ts b/packages/stryker/test/unit/MutantTestMatcherSpec.ts index 351cf920ea..a0be4b9c6a 100644 --- a/packages/stryker/test/unit/MutantTestMatcherSpec.ts +++ b/packages/stryker/test/unit/MutantTestMatcherSpec.ts @@ -1,5 +1,6 @@ import { Logger } from 'log4js'; import { Mutant } from 'stryker-api/mutant'; +import { TestSelection } from 'stryker-api/test_framework'; import { textFile, testResult } from './../helpers/producers'; import { expect } from 'chai'; import { RunResult, TestResult, RunStatus, TestStatus, CoverageCollection, CoveragePerTestResult } from 'stryker-api/test_runner'; @@ -8,10 +9,11 @@ import { MatchedMutant } from 'stryker-api/report'; import MutantTestMatcher from '../../src/MutantTestMatcher'; import currentLogMock from '../helpers/log4jsMock'; import { file, mutant, Mock, mock } from '../helpers/producers'; -import TestableMutant from '../../src/TestableMutant'; +import TestableMutant, { TestSelectionResult } from '../../src/TestableMutant'; import SourceFile from '../../src/SourceFile'; import BroadcastReporter from '../../src/reporters/BroadcastReporter'; import { CoverageMapsByFile } from '../../src/transpiler/CoverageInstrumenterTranspiler'; +import { PassThroughSourceMapper, MappedLocation } from '../../src/transpiler/SourceMapper'; describe('MutantTestMatcher', () => { @@ -23,6 +25,7 @@ describe('MutantTestMatcher', () => { let strykerOptions: StrykerOptions; let reporter: Mock; let files: File[]; + let sourceMapper: PassThroughSourceMapper; beforeEach(() => { log = currentLogMock(); @@ -38,7 +41,16 @@ describe('MutantTestMatcher', () => { name: 'fileWithMutantTwo', content: '\n\n\n\n\n\n\n\n\n\n' })]; - sut = new MutantTestMatcher(mutants, files, runResult, fileCoverageDictionary, strykerOptions, reporter); + sourceMapper = new PassThroughSourceMapper(); + sandbox.spy(sourceMapper, 'transpiledLocationFor'); + sut = new MutantTestMatcher( + mutants, + files, + runResult, + sourceMapper, + fileCoverageDictionary, + strykerOptions, + reporter); }); describe('with coverageAnalysis: "perTest"', () => { @@ -85,17 +97,21 @@ describe('MutantTestMatcher', () => { describe('without code coverage info', () => { - it('should add both tests to the mutants', () => { + it('should add both tests to the mutants and report failure', () => { const result = sut.matchWithMutants(); const expectedTestSelection = [{ id: 0, name: 'test one' }, { id: 1, name: 'test two' }]; expect(result[0].selectedTests).deep.eq(expectedTestSelection); expect(result[1].selectedTests).deep.eq(expectedTestSelection); + expect(result[0].testSelectionResult).eq(TestSelectionResult.FailedButAlreadyReporter); + expect(result[1].testSelectionResult).eq(TestSelectionResult.FailedButAlreadyReporter); + expect(log.warn).calledWith('No coverage result found, even though coverageAnalysis is "%s". Assuming that all tests cover each mutant. This might have a big impact on the performance.', 'perTest'); }); it('should have both mutants matched', () => { const result = sut.matchWithMutants(); const matchedMutants: MatchedMutant[] = [ { + id: '0', mutatorName: result[0].mutatorName, scopedTestIds: result[0].selectedTests.map(test => test.id), timeSpentScopedTests: result[0].timeSpentScopedTests, @@ -103,6 +119,7 @@ describe('MutantTestMatcher', () => { replacement: result[0].replacement }, { + id: '1', mutatorName: result[1].mutatorName, scopedTestIds: result[1].selectedTests.map(test => test.id), timeSpentScopedTests: result[1].timeSpentScopedTests, @@ -192,7 +209,7 @@ describe('MutantTestMatcher', () => { }; fileCoverageDictionary['fileWithMutantTwo'] = { statementMap: { - '1': { start: { line: 10, column: 0 }, end: { line: 10, column: 0 } } + '1': { start: { line: 0, column: 0 }, end: { line: 10, column: 0 } } }, fnMap: {} }; @@ -212,9 +229,36 @@ describe('MutantTestMatcher', () => { it('should have added the run results to the mutants', () => { const result = sut.matchWithMutants(); - const expectedTestSelection = [{ id: 0, name: 'test one' }, { id: 1, name: 'test two' }]; + const expectedTestSelectionFirstMutant: TestSelection[] = [ + { id: 0, name: 'test one' }, + { id: 1, name: 'test two' } + ]; + const expectedTestSelectionSecondMutant: TestSelection[] = [{ id: 0, name: 'test one' }]; + expect(result[0].selectedTests).deep.eq(expectedTestSelectionFirstMutant); + expect(result[1].selectedTests).deep.eq(expectedTestSelectionSecondMutant); + expect(result[0].testSelectionResult).deep.eq(TestSelectionResult.Success); + expect(result[1].testSelectionResult).deep.eq(TestSelectionResult.Success); + }); + }); + + describe('without matching statements or functions', () => { + beforeEach(() => { + fileCoverageDictionary['fileWithMutantOne'] = { statementMap: {}, fnMap: {} }; + fileCoverageDictionary['fileWithMutantTwo'] = { statementMap: {}, fnMap: {} }; + runResult.coverage = { baseline: {}, deviations: {} }; + }); + + it('should select all test in the test run but not report the error yet', () => { + const result = sut.matchWithMutants(); + const expectedTestSelection: TestSelection[] = [ + { name: 'test one', id: 0 }, + { name: 'test two', id: 1 } + ]; expect(result[0].selectedTests).deep.eq(expectedTestSelection); expect(result[1].selectedTests).deep.eq(expectedTestSelection); + expect(result[0].testSelectionResult).eq(TestSelectionResult.Failed); + expect(result[1].testSelectionResult).eq(TestSelectionResult.Failed); + expect(log.warn).not.called; }); }); @@ -275,7 +319,7 @@ describe('MutantTestMatcher', () => { it('should match up mutant for issue #151 (https://github.com/stryker-mutator/stryker/issues/151)', () => { const sourceFile = new SourceFile(textFile()); sourceFile.getLocation = () => ({ 'start': { 'line': 13, 'column': 38 }, 'end': { 'line': 24, 'column': 5 } }); - const testableMutant = new TestableMutant(mutant({ + const testableMutant = new TestableMutant('1', mutant({ fileName: 'juice-shop\\app\\js\\controllers\\SearchResultController.js' }), sourceFile); @@ -306,11 +350,66 @@ describe('MutantTestMatcher', () => { mutants.push(mutant({ fileName: 'fileWithMutantOne' }), mutant({ fileName: 'fileWithMutantTwo' })); runResult.tests.push(testResult(), testResult()); const result = sut.matchWithMutants(); - const expectedTestSelection = [{ id: 0, name: 'name' }, { id: 1, name: 'name' }]; + const expectedTestSelection: TestSelection[] = [{ id: 0, name: 'name' }, { id: 1, name: 'name' }]; expect(result[0].selectedTests).deep.eq(expectedTestSelection); expect(result[1].selectedTests).deep.eq(expectedTestSelection); + expect(result[0].testSelectionResult).deep.eq(TestSelectionResult.FailedButAlreadyReporter); + expect(result[1].testSelectionResult).deep.eq(TestSelectionResult.FailedButAlreadyReporter); expect(log.warn).to.have.been.calledWith('No coverage result found, even though coverageAnalysis is "%s". Assuming that all tests cover each mutant. This might have a big impact on the performance.', 'all'); }); + + describe('when there is coverage data', () => { + + beforeEach(() => { + runResult.coverage = { + fileWithMutantOne: { s: { '0': 1 }, f: {} } + }; + fileCoverageDictionary['fileWithMutantOne'] = { + statementMap: { + '0': { start: { line: 0, column: 0 }, end: { line: 6, column: 0 } } + }, fnMap: {} + }; + }); + + it('should retrieves source mapped location', () => { + // Arrange + mutants.push(mutant({ fileName: 'fileWithMutantOne', range: [4, 5] })); + + // Act + sut.matchWithMutants(); + + // Assert + const expectedLocation: MappedLocation = { + location: { + start: { line: 4, column: 0 }, + end: { line: 4, column: 1 } + }, + fileName: 'fileWithMutantOne' + }; + expect(sourceMapper.transpiledLocationFor).calledWith(expectedLocation); + }); + + it('should match mutant to single test result', () => { + // Arrange + mutants.push(mutant({ fileName: 'fileWithMutantOne', range: [4, 5] })); + runResult.tests.push(testResult({ name: 'test 1' }), testResult({ name: 'test 2' })); + + // Act + const result = sut.matchWithMutants(); + + // Assert + const expectedTestSelection: TestSelection[] = [{ + id: 0, + name: 'test 1' + }, { + id: 1, + name: 'test 2' + }]; + expect(result).lengthOf(1); + expect(result[0].selectedTests).deep.eq(expectedTestSelection); + }); + }); + }); describe('with coverageAnalysis: "off"', () => { diff --git a/packages/stryker/test/unit/SandboxSpec.ts b/packages/stryker/test/unit/SandboxSpec.ts index 2281b24ff0..b29d751aa4 100644 --- a/packages/stryker/test/unit/SandboxSpec.ts +++ b/packages/stryker/test/unit/SandboxSpec.ts @@ -1,3 +1,4 @@ +import { Logger } from 'log4js'; import { Mutant } from 'stryker-api/mutant'; import { Config } from 'stryker-api/config'; import * as sinon from 'sinon'; @@ -11,12 +12,13 @@ import Sandbox from '../../src/Sandbox'; import { TempFolder } from '../../src/utils/TempFolder'; import ResilientTestRunnerFactory from '../../src/isolated-runner/ResilientTestRunnerFactory'; import IsolatedRunnerOptions from '../../src/isolated-runner/IsolatedRunnerOptions'; -import TestableMutant from '../../src/TestableMutant'; -import { mutant as createMutant, testResult, textFile, fileDescriptor, webFile, transpileResult } from '../helpers/producers'; +import TestableMutant, { TestSelectionResult } from '../../src/TestableMutant'; +import { mutant as createMutant, testResult, textFile, fileDescriptor, webFile, transpileResult, Mock } from '../helpers/producers'; import SourceFile from '../../src/SourceFile'; import '../helpers/globals'; import TranspiledMutant from '../../src/TranspiledMutant'; import * as fileUtils from '../../src/utils/fileUtils'; +import currentLogMock from '../helpers/log4jsMock'; describe('Sandbox', () => { let sut: Sandbox; @@ -32,6 +34,7 @@ describe('Sandbox', () => { let expectedTargetFileToMutate: string; let expectedTestFrameworkHooksFile: string; let fileSystemStub: sinon.SinonStub; + let log: Mock; beforeEach(() => { options = { port: 43, timeoutFactor: 23, timeoutMs: 1000, testRunner: 'sandboxUnitTestRunner' } as any; @@ -55,6 +58,7 @@ describe('Sandbox', () => { fileSystemStub.resolves(); sandbox.stub(mkdirp, 'sync').returns(''); sandbox.stub(ResilientTestRunnerFactory, 'create').returns(testRunner); + log = currentLogMock(); }); it('should copy input files when created', async () => { @@ -113,45 +117,58 @@ describe('Sandbox', () => { mutant = createMutant({ fileName: expectedFileToMutate.name, replacement: 'mutated', range: [0, 8] }); const testableMutant = new TestableMutant( + '1', mutant, new SourceFile(textFile({ content: 'original code' }))); - testableMutant.addTestResult(1, testResult({ timeSpentMs: 10 })); - testableMutant.addTestResult(2, testResult({ timeSpentMs: 2 })); + testableMutant.selectTest(testResult({ timeSpentMs: 10 }), 1); + testableMutant.selectTest(testResult({ timeSpentMs: 2 }), 2); transpiledMutant = new TranspiledMutant(testableMutant, { error: null, outputFiles: [textFile({ name: expectedFileToMutate.name, content: 'mutated code' })] - }); + }, true); testFrameworkStub.filter.returns(testFilterCodeFragment); }); - describe('when mutant has scopedTestIds', () => { + it('should save the mutant to disk', async () => { + await sut.runMutant(transpiledMutant); + expect(fileUtils.writeFile).to.have.been.calledWith(expectedTargetFileToMutate, 'mutated code'); + expect(log.warn).not.called; + }); - beforeEach(() => { - return sut.runMutant(transpiledMutant); - }); + it('should nog log a warning if test selection was failed but already reported', async () => { + transpiledMutant.mutant.testSelectionResult = TestSelectionResult.FailedButAlreadyReporter; + await sut.runMutant(transpiledMutant); + expect(log.warn).not.called; + }); - it('should save the mutant to disk', () => { - expect(fileUtils.writeFile).to.have.been.calledWith(expectedTargetFileToMutate, 'mutated code'); - }); + it('should log a warning if tests could not have been selected', async () => { + transpiledMutant.mutant.testSelectionResult = TestSelectionResult.Failed; + await sut.runMutant(transpiledMutant); + const expectedLogMessage = `Failed find coverage data for this mutant, running all tests. This might have an impact on performance: ${transpiledMutant.mutant.toString()}`; + expect(log.warn).calledWith(expectedLogMessage); + }); - it('should filter the scoped tests', () => { - expect(testFrameworkStub.filter).to.have.been.calledWith(transpiledMutant.mutant.selectedTests); - }); + it('should filter the scoped tests', async () => { + await sut.runMutant(transpiledMutant); + expect(testFrameworkStub.filter).to.have.been.calledWith(transpiledMutant.mutant.selectedTests); + }); - it('should write the filter code fragment to hooks file', () => { - expect(fileUtils.writeFile).calledWith(expectedTestFrameworkHooksFile, wrapInClosure(testFilterCodeFragment)); - }); + it('should write the filter code fragment to hooks file', async () => { + await sut.runMutant(transpiledMutant); + expect(fileUtils.writeFile).calledWith(expectedTestFrameworkHooksFile, wrapInClosure(testFilterCodeFragment)); + }); - it('should have ran testRunner with correct timeout', () => { - expect(testRunner.run).calledWith({ timeout: 12 * 23 + 1000 }); - }); + it('should have ran testRunner with correct timeout', async () => { + await sut.runMutant(transpiledMutant); + expect(testRunner.run).calledWith({ timeout: 12 * 23 + 1000 }); + }); - it('should have reset the source file', () => { - let timesCalled = fileSystemStub.getCalls().length - 1; - let lastCall = fileSystemStub.getCall(timesCalled); + it('should have reset the source file', async () => { + await sut.runMutant(transpiledMutant); - expect(lastCall.args).to.deep.equal([expectedTargetFileToMutate, 'original code']); - }); + let timesCalled = fileSystemStub.getCalls().length - 1; + let lastCall = fileSystemStub.getCall(timesCalled); + expect(lastCall.args).to.deep.equal([expectedTargetFileToMutate, 'original code']); }); }); }); @@ -179,8 +196,8 @@ describe('Sandbox', () => { describe('when runMutant()', () => { beforeEach(() => { - const mutant = new TestableMutant(createMutant(), new SourceFile(textFile())); - return sut.runMutant(new TranspiledMutant(mutant, transpileResult({ outputFiles: [textFile({ name: expectedTargetFileToMutate })] }))); + const mutant = new TestableMutant('2', createMutant(), new SourceFile(textFile())); + return sut.runMutant(new TranspiledMutant(mutant, transpileResult({ outputFiles: [textFile({ name: expectedTargetFileToMutate })] }), true)); }); it('should not filter any tests', () => { diff --git a/packages/stryker/test/unit/TestableMutantSpec.ts b/packages/stryker/test/unit/TestableMutantSpec.ts index 919ae951e9..60df5498a8 100644 --- a/packages/stryker/test/unit/TestableMutantSpec.ts +++ b/packages/stryker/test/unit/TestableMutantSpec.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; import { Mutant } from 'stryker-api/mutant'; -import TestableMutant from '../../src/TestableMutant'; +import TestableMutant, { TestSelectionResult } from '../../src/TestableMutant'; import { mutant, textFile, runResult, testResult } from './../helpers/producers'; import SourceFile from '../../src/SourceFile'; import { TextFile } from 'stryker-api/core'; @@ -14,10 +14,11 @@ describe('TestableMutant', () => { beforeEach(() => { innerMutant = mutant(); innerTextFile = textFile(); - sut = new TestableMutant(innerMutant, new SourceFile(innerTextFile)); + sut = new TestableMutant('3', innerMutant, new SourceFile(innerTextFile)); }); it('should pass properties from mutant and source code', () => { + expect(sut.id).eq('3'); expect(sut.fileName).eq(innerMutant.fileName); expect(sut.range).eq(innerMutant.range); expect(sut.mutatorName).eq(innerMutant.mutatorName); @@ -25,10 +26,11 @@ describe('TestableMutant', () => { expect(sut.originalCode).eq(innerTextFile.content); }); - it('should reflect timeSpentScopedTests and scopedTestIds', () => { - sut.addAllTestResults(runResult({ tests: [testResult({ name: 'spec1', timeSpentMs: 12 }), testResult({ name: 'spec2', timeSpentMs: 42 })] })); + it('should reflect timeSpentScopedTests, scopedTestIds and TestSelectionResult', () => { + sut.selectAllTests(runResult({ tests: [testResult({ name: 'spec1', timeSpentMs: 12 }), testResult({ name: 'spec2', timeSpentMs: 42 })] }), TestSelectionResult.FailedButAlreadyReporter); expect(sut.timeSpentScopedTests).eq(54); expect(sut.selectedTests).deep.eq([{ id: 0, name: 'spec1' }, { id: 1, name: 'spec2' }]); + expect(sut.testSelectionResult).eq(TestSelectionResult.FailedButAlreadyReporter); }); it('should calculate position using sourceFile', () => { diff --git a/packages/stryker/test/unit/process/InitialTestExecutorSpec.ts b/packages/stryker/test/unit/process/InitialTestExecutorSpec.ts index 844c710649..9c11596b76 100644 --- a/packages/stryker/test/unit/process/InitialTestExecutorSpec.ts +++ b/packages/stryker/test/unit/process/InitialTestExecutorSpec.ts @@ -73,6 +73,27 @@ describe('InitialTestExecutor run', () => { expect(StrykerSandbox.create).calledWith(options, 0, transpileResultMock.outputFiles, testFrameworkMock); }); + it('should create the transpiler with produceSourceMaps = true when coverage analysis is enabled', async () => { + options.coverageAnalysis = 'all'; + await sut.run(); + const expectedTranspilerOptions: TranspilerOptions = { + produceSourceMaps: true, + config: options + }; + expect(transpilerFacade.default).calledWithNew; + expect(transpilerFacade.default).calledWith(expectedTranspilerOptions); + }); + + it('should create the transpiler with produceSourceMaps = false when coverage analysis is "off"', async () => { + options.coverageAnalysis = 'off'; + await sut.run(); + const expectedTranspilerOptions: TranspilerOptions = { + produceSourceMaps: false, + config: options + }; + expect(transpilerFacade.default).calledWith(expectedTranspilerOptions); + }); + it('should initialize, run and dispose the sandbox', async () => { await sut.run(); expect(strykerSandboxMock.run).to.have.been.calledWith(60 * 1000 * 5); @@ -81,7 +102,7 @@ describe('InitialTestExecutor run', () => { it('should pass through the result', async () => { const coverageData = coverageMaps(); - coverageInstrumenterTranspilerMock.fileCoveragePerFile = { someFile: coverageData } as any; + coverageInstrumenterTranspilerMock.fileCoverageMaps = { someFile: coverageData } as any; const expectedResult: InitialTestRunResult = { runResult: expectedRunResult, transpiledFiles: transpileResultMock.outputFiles, @@ -139,7 +160,7 @@ describe('InitialTestExecutor run', () => { await sut.run(); const expectedSettings: TranspilerOptions = { config: options, - keepSourceMaps: true + produceSourceMaps: true }; expect(coverageInstrumenterTranspiler.default).calledWithNew; expect(coverageInstrumenterTranspiler.default).calledWith(expectedSettings, testFrameworkMock); diff --git a/packages/stryker/test/unit/process/MutationTestExecutorSpec.ts b/packages/stryker/test/unit/process/MutationTestExecutorSpec.ts index f1507eec38..5b2c374325 100644 --- a/packages/stryker/test/unit/process/MutationTestExecutorSpec.ts +++ b/packages/stryker/test/unit/process/MutationTestExecutorSpec.ts @@ -21,7 +21,7 @@ const createTranspiledMutants = (...n: number[]) => { return n.map(n => { const mutant = transpiledMutant(`mutant_${n}`); if (n) { - mutant.mutant.addTestResult(n, testResult()); + mutant.mutant.selectTest(testResult(), n); } return mutant; }); @@ -89,17 +89,18 @@ describe('MutationTestExecutor', () => { let secondSandbox: Mock; beforeEach(() => { - transpiledMutants = createTranspiledMutants(0, 1, 2, 3, 4, 5); + transpiledMutants = createTranspiledMutants(0, 1, 2, 3, 4, 5, 6); transpiledMutants[1].transpileResult.error = 'Error! Cannot negate a string (or something)'; + transpiledMutants[6].changedAnyTranspiledFiles = false; firstSandbox = mock(Sandbox); secondSandbox = mock(Sandbox); mutantTranspilerMock.transpileMutants.returns(Observable.of(...transpiledMutants)); - sandboxPoolMock.streamSandboxes.returns(Observable.of(...[firstSandbox, secondSandbox])); + sandboxPoolMock.streamSandboxes.returns(Observable.of(firstSandbox, secondSandbox)); sut = new MutantTestExecutor(config(), inputFiles, testFrameworkMock, reporter); - // The uncovered and transpile errors should not be run in a sandbox + // The uncovered, transpile error and changedAnyTranspiledFiles = false should not be ran in a sandbox // Mock first sandbox to return first success, then failed firstSandbox.runMutant .withArgs(transpiledMutants[2]).resolves({ status: RunStatus.Complete, tests: [{ name: 'test1', status: TestStatus.Success }, { name: 'skipped', status: TestStatus.Skipped }] }) @@ -119,13 +120,14 @@ describe('MutationTestExecutor', () => { it('should have reported onMutantTested on all mutants', async () => { const actualResults = await sut.run(mutants); - expect(reporter.onMutantTested).to.have.callCount(6); + expect(reporter.onMutantTested).to.have.callCount(7); expect(reporter.onMutantTested).to.have.been.calledWith(actualResults[0]); expect(reporter.onMutantTested).to.have.been.calledWith(actualResults[1]); expect(reporter.onMutantTested).to.have.been.calledWith(actualResults[2]); expect(reporter.onMutantTested).to.have.been.calledWith(actualResults[3]); expect(reporter.onMutantTested).to.have.been.calledWith(actualResults[4]); expect(reporter.onMutantTested).to.have.been.calledWith(actualResults[5]); + expect(reporter.onMutantTested).to.have.been.calledWith(actualResults[6]); }); it('should have reported onAllMutantsTested', async () => { @@ -136,13 +138,14 @@ describe('MutationTestExecutor', () => { it('should eventually resolve the correct mutant results', async () => { const actualResults = await sut.run(mutants); const actualResultsSorted = _.sortBy(actualResults, r => r.sourceFilePath); - expect(actualResults.length).to.be.eq(6); + expect(actualResults.length).to.be.eq(7); expect(actualResultsSorted[0].status).to.be.eq(MutantStatus.NoCoverage); expect(actualResultsSorted[1].status).to.be.eq(MutantStatus.TranspileError); expect(actualResultsSorted[2].status).to.be.eq(MutantStatus.Survived); expect(actualResultsSorted[3].status).to.be.eq(MutantStatus.TimedOut); expect(actualResultsSorted[4].status).to.be.eq(MutantStatus.Killed); expect(actualResultsSorted[5].status).to.be.eq(MutantStatus.RuntimeError); + expect(actualResultsSorted[6].status).to.be.eq(MutantStatus.Survived); }); }); }); \ No newline at end of file diff --git a/packages/stryker/test/unit/reporters/ClearTextReporterSpec.ts b/packages/stryker/test/unit/reporters/ClearTextReporterSpec.ts index 1c3901d8e5..fb2919c536 100644 --- a/packages/stryker/test/unit/reporters/ClearTextReporterSpec.ts +++ b/packages/stryker/test/unit/reporters/ClearTextReporterSpec.ts @@ -5,7 +5,7 @@ import * as chalk from 'chalk'; import * as _ from 'lodash'; import { MutantStatus, MutantResult } from 'stryker-api/report'; import ClearTextReporter from '../../../src/reporters/ClearTextReporter'; -import { scoreResult, mutationScoreThresholds, config } from '../../helpers/producers'; +import { scoreResult, mutationScoreThresholds, config, mutantResult } from '../../helpers/producers'; describe('ClearTextReporter', () => { let sut: ClearTextReporter; @@ -227,7 +227,7 @@ describe('ClearTextReporter', () => { function mutantResults(...status: MutantStatus[]): MutantResult[] { return status.map(status => { - const result: MutantResult = { + const result: MutantResult = mutantResult({ location: { start: { line: 1, column: 2 }, end: { line: 3, column: 4 } }, range: [0, 0], mutatedLines: 'mutated line', @@ -237,7 +237,7 @@ describe('ClearTextReporter', () => { sourceFilePath: '', testsRan: ['a test', 'a second test', 'a third test'], status: status - }; + }); return result; }); } diff --git a/packages/stryker/test/unit/reporters/DotsReporterSpec.ts b/packages/stryker/test/unit/reporters/DotsReporterSpec.ts index 2ef6b4f03f..f576e43e82 100644 --- a/packages/stryker/test/unit/reporters/DotsReporterSpec.ts +++ b/packages/stryker/test/unit/reporters/DotsReporterSpec.ts @@ -4,6 +4,7 @@ import { MutantStatus, MutantResult } from 'stryker-api/report'; import { expect } from 'chai'; import * as chalk from 'chalk'; import * as os from 'os'; +import * as producers from '../../helpers/producers'; describe('DotsReporter', () => { @@ -18,7 +19,7 @@ describe('DotsReporter', () => { describe('onMutantTested()', () => { - describe('when status is KILLED', () => { + describe('when status is Killed', () => { beforeEach(() => { sut.onMutantTested(mutantResult(MutantStatus.Killed)); @@ -29,7 +30,7 @@ describe('DotsReporter', () => { }); }); - describe('when status is TIMEDOUT', () => { + describe('when status is TimedOut', () => { beforeEach(() => { sut.onMutantTested(mutantResult(MutantStatus.TimedOut)); @@ -40,7 +41,7 @@ describe('DotsReporter', () => { }); }); - describe('when status is SURVIVED', () => { + describe('when status is Survived', () => { beforeEach(() => { sut.onMutantTested(mutantResult(MutantStatus.Survived)); @@ -64,7 +65,7 @@ describe('DotsReporter', () => { }); function mutantResult(status: MutantStatus): MutantResult { - return { + return producers.mutantResult({ location: { start: { line: 0, column: 0 }, end: { line: 0, column: 0 } }, mutatedLines: '', mutatorName: '', @@ -74,7 +75,7 @@ describe('DotsReporter', () => { testsRan: [''], status: status, range: [0, 0] - }; + }); } }); diff --git a/packages/stryker/test/unit/reporters/ProgressReporterSpec.ts b/packages/stryker/test/unit/reporters/ProgressReporterSpec.ts index a3226de870..ca8a106854 100644 --- a/packages/stryker/test/unit/reporters/ProgressReporterSpec.ts +++ b/packages/stryker/test/unit/reporters/ProgressReporterSpec.ts @@ -55,33 +55,27 @@ describe('ProgressReporter', () => { let progressBarTickTokens: any; beforeEach(() => { - matchedMutants = [matchedMutant(1), matchedMutant(4), matchedMutant(2)]; - + matchedMutants = [matchedMutant(0), matchedMutant(1), matchedMutant(4), matchedMutant(2)]; sut.onAllMutantsMatchedWithTests(matchedMutants); }); - describe('when status is not "Survived"', () => { - - beforeEach(() => { - sut.onMutantTested(mutantResult({ status: MutantStatus.Killed })); - }); - - it('should tick the ProgressBar with 1 tested mutant, 0 survived', () => { - progressBarTickTokens = { total: 3, tested: 1, survived: 0 }; - expect(progressBar.tick).to.have.been.calledWithMatch(progressBarTickTokens); - }); + it('should tick the ProgressBar with 1 tested mutant, 0 survived when status is not "Survived"', () => { + sut.onMutantTested(mutantResult({ status: MutantStatus.Killed })); + progressBarTickTokens = { total: 3, tested: 1, survived: 0 }; + expect(progressBar.tick).to.have.been.calledWithMatch(progressBarTickTokens); }); - describe('when status is "Survived"', () => { - - beforeEach(() => { - sut.onMutantTested(mutantResult({ status: MutantStatus.Survived })); - }); + it('should not tick the ProgressBar if the result was for a mutant that wasn\'t matched to any tests', () => { + // mutant 0 isn't matched to any tests + sut.onMutantTested(mutantResult({ id: '0', status: MutantStatus.TranspileError })); + progressBarTickTokens = { total: 3, tested: 0, survived: 0 }; + expect(progressBar.tick).to.not.have.been.called; + }); - it('should tick the ProgressBar with 1 survived mutant', () => { - progressBarTickTokens = { total: 3, tested: 1, survived: 1 }; - expect(progressBar.tick).to.have.been.calledWithMatch(progressBarTickTokens); - }); + it('should tick the ProgressBar with 1 survived mutant when status is "Survived"', () => { + sut.onMutantTested(mutantResult({ status: MutantStatus.Survived })); + progressBarTickTokens = { total: 3, tested: 1, survived: 1 }; + expect(progressBar.tick).to.have.been.calledWithMatch(progressBarTickTokens); }); }); }); diff --git a/packages/stryker/test/unit/transpiler/CoverageInstrumenterTranspilerSpec.ts b/packages/stryker/test/unit/transpiler/CoverageInstrumenterTranspilerSpec.ts index 7101a36bc1..bd86b32456 100644 --- a/packages/stryker/test/unit/transpiler/CoverageInstrumenterTranspilerSpec.ts +++ b/packages/stryker/test/unit/transpiler/CoverageInstrumenterTranspilerSpec.ts @@ -13,7 +13,7 @@ describe('CoverageInstrumenterTranspiler', () => { }); it('should not instrument any code when coverage analysis is off', async () => { - sut = new CoverageInstrumenterTranspiler({ config, keepSourceMaps: false }, null); + sut = new CoverageInstrumenterTranspiler({ config, produceSourceMaps: false }, null); config.coverageAnalysis = 'off'; const input = [textFile({ mutated: true }), binaryFile({ mutated: true }), webFile({ mutated: true })]; const output = await sut.transpile(input); @@ -25,7 +25,7 @@ describe('CoverageInstrumenterTranspiler', () => { beforeEach(() => { config.coverageAnalysis = 'all'; - sut = new CoverageInstrumenterTranspiler({ config, keepSourceMaps: false }, null); + sut = new CoverageInstrumenterTranspiler({ config, produceSourceMaps: false }, null); }); it('should instrument code of mutated files', async () => { @@ -50,11 +50,11 @@ describe('CoverageInstrumenterTranspiler', () => { textFile({ name: 'foobar.js', mutated: true, content: 'console.log("foobar");' }) ]; sut.transpile(input); - expect(sut.fileCoveragePerFile['something.js'].statementMap).deep.eq({}); - expect(sut.fileCoveragePerFile['something.js'].fnMap[0]).deep.eq({ start: { line: 0, column: 22 }, end: { line: 0, column: 24 } }); - expect(sut.fileCoveragePerFile['something.js'].fnMap[1]).undefined; - expect(sut.fileCoveragePerFile['foobar.js'].statementMap).deep.eq({ '0': { start: { line: 0, column: 0 }, end: { line: 0, column: 22 } } }); - expect(sut.fileCoveragePerFile['foobar.js'].fnMap).deep.eq({}); + expect(sut.fileCoverageMaps['something.js'].statementMap).deep.eq({}); + expect(sut.fileCoverageMaps['something.js'].fnMap[0]).deep.eq({ start: { line: 0, column: 22 }, end: { line: 0, column: 24 } }); + expect(sut.fileCoverageMaps['something.js'].fnMap[1]).undefined; + expect(sut.fileCoverageMaps['foobar.js'].statementMap).deep.eq({ '0': { start: { line: 0, column: 0 }, end: { line: 0, column: 22 } } }); + expect(sut.fileCoverageMaps['foobar.js'].fnMap).deep.eq({}); }); it('should fill error message and not transpile input when the file contains a parse error', async () => { @@ -69,7 +69,7 @@ describe('CoverageInstrumenterTranspiler', () => { beforeEach(() => { config.coverageAnalysis = 'perTest'; - sut = new CoverageInstrumenterTranspiler({ config, keepSourceMaps: false }, testFramework()); + sut = new CoverageInstrumenterTranspiler({ config, produceSourceMaps: false }, testFramework()); input = [textFile({ mutated: true, content: 'function something() {}' })]; }); @@ -93,7 +93,7 @@ describe('CoverageInstrumenterTranspiler', () => { 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); + sut = new CoverageInstrumenterTranspiler({ config, produceSourceMaps: true }, null); 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.'); }); diff --git a/packages/stryker/test/unit/transpiler/MutantTranspilerSpec.ts b/packages/stryker/test/unit/transpiler/MutantTranspilerSpec.ts index 02d9013207..9165edc758 100644 --- a/packages/stryker/test/unit/transpiler/MutantTranspilerSpec.ts +++ b/packages/stryker/test/unit/transpiler/MutantTranspilerSpec.ts @@ -5,6 +5,7 @@ import TranspilerFacade, * as transpilerFacade from '../../../src/transpiler/Tra import { Mock, mock, transpileResult, config, textFile, webFile, testableMutant } from '../../helpers/producers'; import { TranspileResult } from 'stryker-api/transpile'; import '../../helpers/globals'; +import TranspiledMutant from '../../../src/TranspiledMutant'; describe('MutantTranspiler', () => { let sut: MutantTranspiler; @@ -35,7 +36,7 @@ describe('MutantTranspiler', () => { 'someLogLevel', ['plugin1'], TranspilerFacade, - { config: expectedConfig, keepSourceMaps: false }); + { config: expectedConfig, produceSourceMaps: false }); }); describe('initialize', () => { @@ -68,10 +69,31 @@ describe('MutantTranspiler', () => { const actualResult = await sut.transpileMutants(mutants) .toArray() .toPromise(); - expect(actualResult).deep.eq([ - { mutant: mutants[0], transpileResult: transpileResultOne }, - { mutant: mutants[1], transpileResult: transpileResultTwo } - ]); + const expected: TranspiledMutant[] = [ + { mutant: mutants[0], transpileResult: transpileResultOne, changedAnyTranspiledFiles: true }, + { mutant: mutants[1], transpileResult: transpileResultTwo, changedAnyTranspiledFiles: true } + ]; + expect(actualResult).deep.eq(expected); + }); + + it('should set set the changedAnyTranspiledFiles boolean to false if transpiled output did not change', async () => { + // Arrange + transpilerFacadeMock.transpile.reset(); + transpilerFacadeMock.transpile.resolves(transpileResultOne); + const mutants = [testableMutant()]; + const files = [textFile()]; + await sut.initialize(files); + + // Act + const actual = await sut.transpileMutants(mutants) + .toArray() + .toPromise(); + + // Assert + const expected: TranspiledMutant[] = [ + { mutant: mutants[0], transpileResult: transpileResultOne, changedAnyTranspiledFiles: false } + ]; + expect(actual).deep.eq(expected); }); it('should transpile mutants one by one in sequence', async () => { diff --git a/packages/stryker/test/unit/transpiler/SourceMapperSpec.ts b/packages/stryker/test/unit/transpiler/SourceMapperSpec.ts new file mode 100644 index 0000000000..2f31e8d1e0 --- /dev/null +++ b/packages/stryker/test/unit/transpiler/SourceMapperSpec.ts @@ -0,0 +1,152 @@ +import { expect } from 'chai'; +import * as sourceMapModule from 'source-map'; +import { Config } from 'stryker-api/config'; +import { File } from 'stryker-api/core'; +import SourceMapper, { PassThroughSourceMapper, TranspiledSourceMapper, MappedLocation, SourceMapError } from '../../../src/transpiler/SourceMapper'; +import { Mock, mock, config as configFactory, location as locationFactory, textFile, mappedLocation, binaryFile } from '../../helpers/producers'; + +const GREATEST_LOWER_BOUND = sourceMapModule.SourceMapConsumer.GREATEST_LOWER_BOUND; +const LEAST_UPPER_BOUND = sourceMapModule.SourceMapConsumer.LEAST_UPPER_BOUND; + +function base64Encode(input: string) { + return Buffer.from(input).toString('base64'); +} + +const ERROR_POSTFIX = '. Cannot analyse code coverage. Setting `coverageAnalysis: "off"` in your stryker.conf.js will prevent this error, but forces Stryker to run each test for each mutant.'; + +describe('SourceMapper', () => { + let sut: SourceMapper; + let sourceMapConsumerMock: Mock; + let config: Config; + + beforeEach(() => { + config = configFactory(); + sourceMapConsumerMock = mock(sourceMapModule.SourceMapConsumer); + + // For some reason, `generatedPositionFor` is not defined on the `SourceMapConsumer` prototype + // Define it here by hand + sourceMapConsumerMock.generatedPositionFor = sandbox.stub(); + sourceMapConsumerMock.generatedPositionFor.returns({ + line: 1, + column: 2 + }); + sandbox.stub(sourceMapModule, 'SourceMapConsumer').returns(sourceMapConsumerMock); + + // Restore the static values, removed by the stub + sourceMapModule.SourceMapConsumer.LEAST_UPPER_BOUND = LEAST_UPPER_BOUND; + sourceMapModule.SourceMapConsumer.GREATEST_LOWER_BOUND = GREATEST_LOWER_BOUND; + }); + + describe('create', () => { + it('should create a PassThrough source mapper when no transpiler was configured', () => { + config.transpilers = []; + expect(SourceMapper.create([], config)).instanceOf(PassThroughSourceMapper); + }); + it('should create a Transpiled source mapper when a transpiler was configured', () => { + config.transpilers = ['a transpiler']; + expect(SourceMapper.create([], config)).instanceOf(TranspiledSourceMapper); + }); + }); + + describe('PassThrough', () => { + beforeEach(() => { + sut = new PassThroughSourceMapper(); + }); + + it('should pass through the input on transpiledLocationFor', () => { + const input: MappedLocation = { + fileName: 'foo/bar.js', + location: locationFactory() + }; + expect(sut.transpiledLocationFor(input)).eq(input); + }); + }); + + describe('Transpiled', () => { + let transpiledFiles: File[]; + + beforeEach(() => { + transpiledFiles = []; + sut = new TranspiledSourceMapper(transpiledFiles); + }); + + it('should create SourceMapConsumers for mutated text files when transpiledLocationFor is called', () => { + // Arrange + const expectedMapFile1 = { sources: ['file1.ts'] }; + const expectedMapFile2 = { sources: ['file2.ts'] }; + transpiledFiles.push(textFile({ + name: 'file1.js', mutated: true, content: `// # sourceMappingURL=file1.js.map` + })); + transpiledFiles.push(textFile({ + name: 'file1.js.map', mutated: false, content: JSON.stringify(expectedMapFile1) + })); + transpiledFiles.push(textFile({ + name: 'file2.js', mutated: true, content: `// # sourceMappingURL=data:application/json;base64,${base64Encode(JSON.stringify(expectedMapFile2))}` + })); + + // Act + sut.transpiledLocationFor(mappedLocation({ fileName: 'file1.ts' })); + + // Assert + expect(sourceMapModule.SourceMapConsumer).calledWithNew; + expect(sourceMapModule.SourceMapConsumer).calledWith(expectedMapFile1); + expect(sourceMapModule.SourceMapConsumer).calledWith(expectedMapFile2); + }); + + it('should cache source maps for future use when `transpiledLocationFor` is called', () => { + // Arrange + const expectedMapFile1 = { sources: ['file1.ts'] }; + transpiledFiles.push(textFile({ + name: 'file1.js', mutated: true, content: `// # sourceMappingURL=data:application/json;base64,${base64Encode(JSON.stringify(expectedMapFile1))}` + })); + + // Act + sut.transpiledLocationFor(mappedLocation({ fileName: 'file1.ts' })); + sut.transpiledLocationFor(mappedLocation({ fileName: 'file1.ts' })); + + // Assert + expect(sourceMapModule.SourceMapConsumer).calledOnce; + }); + + it('should throw an error when the requested source map could not be found', () => { + expect(() => sut.transpiledLocationFor(mappedLocation({ fileName: 'foobar' }))) + .throws(SourceMapError, 'Source map not found for "foobar"' + ERROR_POSTFIX); + }); + + it('should throw an error if source map file is a binary file', () => { + transpiledFiles.push(textFile({ + name: 'file1.js', mutated: true, content: '// # sourceMappingURL=file1.js.map' + })); + transpiledFiles.push(binaryFile({ + name: 'file1.js.map' + })); + expect(() => sut.transpiledLocationFor(mappedLocation({ fileName: 'foobar' }))) + .throws(SourceMapError, `Source map file "file1.js.map" has the wrong file kind. "Binary" instead of "Text"${ERROR_POSTFIX}`); + }); + + it('should throw an error if source map data url is not supported', () => { + const expectedMapFile1 = { sources: ['file1.ts'] }; + transpiledFiles.push(textFile({ + name: 'file1.js', mutated: true, content: `// # sourceMappingURL=data:application/xml;base64,${base64Encode(JSON.stringify(expectedMapFile1))}` + })); + expect(() => sut.transpiledLocationFor(mappedLocation({ fileName: 'foobar' }))) + .throws(SourceMapError, `Source map file for "file1.js" cannot be read. Data url "data:application/xml;base64" found, where "data:application/json;base64" was expected${ERROR_POSTFIX}`); + }); + + it('should throw an error if source map file cannot be found', () => { + transpiledFiles.push(textFile({ + name: 'file1.js', mutated: true, content: `// # sourceMappingURL=file1.js.map` + })); + expect(() => sut.transpiledLocationFor(mappedLocation({ fileName: 'foobar' }))) + .throws(SourceMapError, `Source map file "file1.js.map" (referenced by "file1.js") cannot be found in list of transpiled files${ERROR_POSTFIX}`); + }); + + it('should throw an error if source map file url is not declared in a transpiled file', () => { + transpiledFiles.push(textFile({ + name: 'file1.js', mutated: true, content: `// # sourceMapping%%%=file1.js.map` + })); + expect(() => sut.transpiledLocationFor(mappedLocation({ fileName: 'foobar' }))) + .throws(SourceMapError, `No source map reference found in transpiled file "file1.js"${ERROR_POSTFIX}`); + }); + }); +}); \ No newline at end of file diff --git a/packages/stryker/test/unit/transpiler/TranspilerFacadeSpec.ts b/packages/stryker/test/unit/transpiler/TranspilerFacadeSpec.ts index 3351c58d50..f75a17f351 100644 --- a/packages/stryker/test/unit/transpiler/TranspilerFacadeSpec.ts +++ b/packages/stryker/test/unit/transpiler/TranspilerFacadeSpec.ts @@ -1,8 +1,8 @@ import { expect } from 'chai'; import { Config } from 'stryker-api/config'; import TranspilerFacade from '../../../src/transpiler/TranspilerFacade'; -import { Transpiler, TranspilerFactory, TranspileResult, FileLocation } from 'stryker-api/transpile'; -import { file, fileLocation, mock, Mock, transpileResult } from '../../helpers/producers'; +import { Transpiler, TranspilerFactory, TranspileResult } from 'stryker-api/transpile'; +import { file, mock, Mock, transpileResult } from '../../helpers/producers'; describe('TranspilerFacade', () => { let createStub: sinon.SinonStub; @@ -15,7 +15,7 @@ describe('TranspilerFacade', () => { describe('when there are no transpilers', () => { beforeEach(() => { - sut = new TranspilerFacade({ config: new Config(), keepSourceMaps: true }); + sut = new TranspilerFacade({ config: new Config(), produceSourceMaps: true }); }); it('should return input when `transpile` is called', async () => { @@ -25,13 +25,6 @@ describe('TranspilerFacade', () => { expect(result.error).is.null; expect(result.outputFiles).eq(input); }); - - it('should return input when `getMappedLocation` is called', () => { - const input = fileLocation({ fileName: 'input' }); - const result = sut.getMappedLocation(input); - expect(createStub).not.called; - expect(result).eq(input); - }); }); describe('with 2 transpilers', () => { @@ -40,8 +33,6 @@ describe('TranspilerFacade', () => { let transpilerTwo: Mock; let resultOne: TranspileResult; let resultTwo: TranspileResult; - let locationOne: FileLocation; - let locationTwo: FileLocation; let config: Config; beforeEach(() => { @@ -51,26 +42,22 @@ describe('TranspilerFacade', () => { transpilerTwo = mock(TranspilerFacade); resultOne = transpileResult({ outputFiles: [file({ name: 'result-1' })] }); resultTwo = transpileResult({ outputFiles: [file({ name: 'result-2' })] }); - locationOne = fileLocation({ fileName: 'location-1' }); - locationTwo = fileLocation({ fileName: 'location-2' }); createStub .withArgs('transpiler-one').returns(transpilerOne) .withArgs('transpiler-two').returns(transpilerTwo); transpilerOne.transpile.returns(resultOne); - transpilerOne.getMappedLocation.returns(locationOne); transpilerTwo.transpile.returns(resultTwo); - transpilerTwo.getMappedLocation.returns(locationTwo); }); it('should create two transpilers', () => { - sut = new TranspilerFacade({ config, keepSourceMaps: true }); + sut = new TranspilerFacade({ config, produceSourceMaps: true }); expect(createStub).calledTwice; expect(createStub).calledWith('transpiler-one'); expect(createStub).calledWith('transpiler-two'); }); it('should chain the transpilers when `transpile` is called', async () => { - sut = new TranspilerFacade({ config, keepSourceMaps: true }); + sut = new TranspilerFacade({ config, produceSourceMaps: true }); const input = [file({ name: 'input' })]; const result = await sut.transpile(input); expect(result).eq(resultTwo); @@ -84,7 +71,7 @@ describe('TranspilerFacade', () => { additionalTranspiler.transpile.returns(expectedResult); const input = [file({ name: 'input' })]; sut = new TranspilerFacade( - { config, keepSourceMaps: true }, + { config, produceSourceMaps: true }, { name: 'someTranspiler', transpiler: additionalTranspiler } ); const output = await sut.transpile(input); @@ -94,7 +81,7 @@ describe('TranspilerFacade', () => { it('should stop chaining if an error occurs during `transpile`', async () => { - sut = new TranspilerFacade({ config, keepSourceMaps: true }); + sut = new TranspilerFacade({ config, produceSourceMaps: true }); const input = [file({ name: 'input' })]; resultOne.error = 'an error'; const result = await sut.transpile(input); @@ -103,14 +90,6 @@ describe('TranspilerFacade', () => { expect(transpilerTwo.transpile).not.called; }); - it('should chain the transpilers when `getMappedLocation` is called', () => { - sut = new TranspilerFacade({ config, keepSourceMaps: true }); - const input = fileLocation({ fileName: 'input' }); - const result = sut.getMappedLocation(input); - expect(result).eq(locationTwo); - expect(transpilerOne.getMappedLocation).calledWith(input); - expect(transpilerTwo.getMappedLocation).calledWith(locationOne); - }); }); }); diff --git a/packages/stryker/testResources/source-mapper/.gitignore b/packages/stryker/testResources/source-mapper/.gitignore new file mode 100644 index 0000000000..2481dd5411 --- /dev/null +++ b/packages/stryker/testResources/source-mapper/.gitignore @@ -0,0 +1,2 @@ +# Include .js.map files in this directory. They are used in SourceMapperIT. +!*.js.map \ No newline at end of file diff --git a/packages/stryker/testResources/source-mapper/external-source-maps/js/math.js b/packages/stryker/testResources/source-mapper/external-source-maps/js/math.js new file mode 100644 index 0000000000..4cc382b0e2 --- /dev/null +++ b/packages/stryker/testResources/source-mapper/external-source-maps/js/math.js @@ -0,0 +1,19 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +function max() { + var numbers = []; + for (var _i = 0; _i < arguments.length; _i++) { + numbers[_i] = arguments[_i]; + } + return Math.max.apply(Math, numbers); +} +exports.max = max; +function total() { + var numbers = []; + for (var _i = 0; _i < arguments.length; _i++) { + numbers[_i] = arguments[_i]; + } + return numbers.reduce(function (a, b) { return a + b; }); +} +exports.total = total; +//# sourceMappingURL=math.js.map \ No newline at end of file diff --git a/packages/stryker/testResources/source-mapper/external-source-maps/js/math.js.map b/packages/stryker/testResources/source-mapper/external-source-maps/js/math.js.map new file mode 100644 index 0000000000..19221091e7 --- /dev/null +++ b/packages/stryker/testResources/source-mapper/external-source-maps/js/math.js.map @@ -0,0 +1 @@ +{"version":3,"file":"math.js","sourceRoot":"","sources":["../ts/src/math.ts"],"names":[],"mappings":";;AAEA;IAAoB,iBAAoB;SAApB,UAAoB,EAApB,qBAAoB,EAApB,IAAoB;QAApB,4BAAoB;;IACtC,MAAM,CAAC,IAAI,CAAC,GAAG,OAAR,IAAI,EAAQ,OAAO,EAAE;AAC9B,CAAC;AAFD,kBAEC;AAED;IAAsB,iBAAoB;SAApB,UAAoB,EAApB,qBAAoB,EAApB,IAAoB;QAApB,4BAAoB;;IACxC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,UAAC,CAAC,EAAE,CAAC,IAAK,OAAA,CAAC,GAAG,CAAC,EAAL,CAAK,CAAC,CAAC;AACzC,CAAC;AAFD,sBAEC"} \ No newline at end of file diff --git a/packages/stryker/testResources/source-mapper/external-source-maps/ts/src/math.ts b/packages/stryker/testResources/source-mapper/external-source-maps/ts/src/math.ts new file mode 100644 index 0000000000..37c99e67bd --- /dev/null +++ b/packages/stryker/testResources/source-mapper/external-source-maps/ts/src/math.ts @@ -0,0 +1,9 @@ + + +export function max(...numbers: number[]) { + return Math.max(...numbers); +} + +export function total(...numbers: number[]) { + return numbers.reduce((a, b) => a + b); +} \ No newline at end of file diff --git a/packages/stryker/testResources/source-mapper/external-source-maps/tsconfig.json b/packages/stryker/testResources/source-mapper/external-source-maps/tsconfig.json new file mode 100644 index 0000000000..e20087354a --- /dev/null +++ b/packages/stryker/testResources/source-mapper/external-source-maps/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "strict": true, + "outDir": "js", + "sourceMap": true + } +} \ No newline at end of file diff --git a/packages/stryker/testResources/source-mapper/inline-source-maps/js/math.js b/packages/stryker/testResources/source-mapper/inline-source-maps/js/math.js new file mode 100644 index 0000000000..b9e8a89a47 --- /dev/null +++ b/packages/stryker/testResources/source-mapper/inline-source-maps/js/math.js @@ -0,0 +1,19 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +function max() { + var numbers = []; + for (var _i = 0; _i < arguments.length; _i++) { + numbers[_i] = arguments[_i]; + } + return Math.max.apply(Math, numbers); +} +exports.max = max; +function total() { + var numbers = []; + for (var _i = 0; _i < arguments.length; _i++) { + numbers[_i] = arguments[_i]; + } + return numbers.reduce(function (a, b) { return a + b; }); +} +exports.total = total; +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWF0aC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL3NyYy9tYXRoLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7O0FBRUE7SUFBb0IsaUJBQW9CO1NBQXBCLFVBQW9CLEVBQXBCLHFCQUFvQixFQUFwQixJQUFvQjtRQUFwQiw0QkFBb0I7O0lBQ3RDLE1BQU0sQ0FBQyxJQUFJLENBQUMsR0FBRyxPQUFSLElBQUksRUFBUSxPQUFPLEVBQUU7QUFDOUIsQ0FBQztBQUZELGtCQUVDO0FBRUQ7SUFBc0IsaUJBQW9CO1NBQXBCLFVBQW9CLEVBQXBCLHFCQUFvQixFQUFwQixJQUFvQjtRQUFwQiw0QkFBb0I7O0lBQ3hDLE1BQU0sQ0FBQyxPQUFPLENBQUMsTUFBTSxDQUFDLFVBQUMsQ0FBQyxFQUFFLENBQUMsSUFBSyxPQUFBLENBQUMsR0FBRyxDQUFDLEVBQUwsQ0FBSyxDQUFDLENBQUM7QUFDekMsQ0FBQztBQUZELHNCQUVDIn0= \ No newline at end of file diff --git a/packages/stryker/testResources/source-mapper/inline-source-maps/ts/src/math.ts b/packages/stryker/testResources/source-mapper/inline-source-maps/ts/src/math.ts new file mode 100644 index 0000000000..37c99e67bd --- /dev/null +++ b/packages/stryker/testResources/source-mapper/inline-source-maps/ts/src/math.ts @@ -0,0 +1,9 @@ + + +export function max(...numbers: number[]) { + return Math.max(...numbers); +} + +export function total(...numbers: number[]) { + return numbers.reduce((a, b) => a + b); +} \ No newline at end of file diff --git a/packages/stryker/testResources/source-mapper/inline-source-maps/tsconfig.json b/packages/stryker/testResources/source-mapper/inline-source-maps/tsconfig.json new file mode 100644 index 0000000000..1d65a35c9e --- /dev/null +++ b/packages/stryker/testResources/source-mapper/inline-source-maps/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "strict": true, + "outDir": "js", + "sourceMap": true, + "inlineSourceMap": true + } +} \ No newline at end of file