diff --git a/packages/core/package.json b/packages/core/package.json index 13b77b32a4..6f0eaf9bfc 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -70,7 +70,7 @@ "progress": "~2.0.0", "rimraf": "~2.6.1", "rxjs": "~6.5.1", - "source-map": "~0.6.1", + "source-map": "~0.7.3", "surrial": "~1.0.0", "tree-kill": "~1.2.0", "tslib": "~1.9.3", diff --git a/packages/core/src/mutants/MutantTestMatcher.ts b/packages/core/src/mutants/MutantTestMatcher.ts index c351168ae7..96779e48ea 100644 --- a/packages/core/src/mutants/MutantTestMatcher.ts +++ b/packages/core/src/mutants/MutantTestMatcher.ts @@ -48,7 +48,7 @@ export class MutantTestMatcher { } } - public matchWithMutants(mutants: ReadonlyArray): ReadonlyArray { + public async matchWithMutants(mutants: ReadonlyArray): Promise> { const testableMutants = this.createTestableMutants(mutants); @@ -58,14 +58,14 @@ export class MutantTestMatcher { 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.selectAllTests(this.initialRunResult.runResult, TestSelectionResult.FailedButAlreadyReported)); } else { - testableMutants.forEach(testableMutant => this.enrichWithCoveredTests(testableMutant)); + await Promise.all(testableMutants.map(testableMutant => this.enrichWithCoveredTests(testableMutant))); } this.reporter.onAllMutantsMatchedWithTests(Object.freeze(testableMutants.map(this.mapMutantOnMatchedMutant))); return testableMutants; } - public enrichWithCoveredTests(testableMutant: TestableMutant) { - const transpiledLocation = this.initialRunResult.sourceMapper.transpiledLocationFor({ + public async enrichWithCoveredTests(testableMutant: TestableMutant) { + const transpiledLocation = await this.initialRunResult.sourceMapper.transpiledLocationFor({ fileName: testableMutant.mutant.fileName, location: testableMutant.location }); diff --git a/packages/core/src/transpiler/SourceMapper.ts b/packages/core/src/transpiler/SourceMapper.ts index c8fecfa7b6..6e72381654 100644 --- a/packages/core/src/transpiler/SourceMapper.ts +++ b/packages/core/src/transpiler/SourceMapper.ts @@ -36,7 +36,7 @@ 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 */ - public abstract transpiledLocationFor(originalLocation: MappedLocation): MappedLocation; + public abstract async transpiledLocationFor(originalLocation: MappedLocation): Promise; public abstract transpiledFileNameFor(originalFileName: string): string; @@ -69,11 +69,11 @@ export class TranspiledSourceMapper extends SourceMapper { /** * @inheritdoc */ - public transpiledLocationFor(originalLocation: MappedLocation): MappedLocation { + public async transpiledLocationFor(originalLocation: MappedLocation): Promise { const sourceMap = this.getSourceMap(originalLocation.fileName); const relativeSource = this.getRelativeSource(sourceMap, originalLocation); - const start = sourceMap.generatedPositionFor(originalLocation.location.start, relativeSource); - const end = sourceMap.generatedPositionFor(originalLocation.location.end, relativeSource); + const start = await sourceMap.generatedPositionFor(originalLocation.location.start, relativeSource); + const end = await sourceMap.generatedPositionFor(originalLocation.location.end, relativeSource); return { fileName: sourceMap.transpiledFile.name, location: { @@ -213,27 +213,31 @@ export class PassThroughSourceMapper extends SourceMapper { /** * @inheritdoc */ - public transpiledLocationFor(originalLocation: MappedLocation): MappedLocation { - return originalLocation; + public async transpiledLocationFor(originalLocation: MappedLocation): Promise { + return Promise.resolve(originalLocation); } } class SourceMap { - private readonly sourceMap: SourceMapConsumer; - constructor(public transpiledFile: File, public sourceMapFileName: string, rawSourceMap: RawSourceMap) { - this.sourceMap = new SourceMapConsumer(rawSourceMap); + private sourceMap: SourceMapConsumer | undefined; + constructor(public transpiledFile: File, public sourceMapFileName: string, private readonly rawSourceMap: RawSourceMap) { } - public generatedPositionFor(originalPosition: Position, relativeSource: string): Position { - const transpiledPosition = this.sourceMap.generatedPositionFor({ + public async generatedPositionFor(originalPosition: Position, relativeSource: string): Promise { + if (!this.sourceMap) { + this.sourceMap = await new SourceMapConsumer(this.rawSourceMap); + } + + const transpiledPosition = await this.sourceMap.generatedPositionFor({ bias: SourceMapConsumer.LEAST_UPPER_BOUND, column: originalPosition.column, line: originalPosition.line + 1, // SourceMapConsumer works 1-based source: relativeSource }); - return { - column: transpiledPosition.column, - line: transpiledPosition.line - 1 // Stryker works 0-based - }; + + return Promise.resolve({ + column: transpiledPosition.column || 0, + line: (transpiledPosition.line || 1) - 1 // Stryker works 0-based + }); } } diff --git a/packages/core/test/integration/source-mapper/SourceMapper.it.spec.ts b/packages/core/test/integration/source-mapper/SourceMapper.it.spec.ts index 9b9c953dbd..a651dd84e1 100644 --- a/packages/core/test/integration/source-mapper/SourceMapper.it.spec.ts +++ b/packages/core/test/integration/source-mapper/SourceMapper.it.spec.ts @@ -27,7 +27,7 @@ describe('Source mapper integration', () => { }); it('it should be able to map to transpiled location', async () => { - const actual = sut.transpiledLocationFor({ + const actual = await sut.transpiledLocationFor({ fileName: resolve('external-source-maps', 'ts', 'src', 'math.ts'), location: { end: { line: 7, column: 42 }, @@ -50,7 +50,7 @@ describe('Source mapper integration', () => { sut = new TranspiledSourceMapper(files); }); it('it should be able to map to transpiled location', async () => { - const actual = sut.transpiledLocationFor({ + const actual = await sut.transpiledLocationFor({ fileName: resolve('inline-source-maps', 'ts', 'src', 'math.ts'), location: { end: { line: 7, column: 42 }, diff --git a/packages/core/test/unit/SandboxPool.spec.ts b/packages/core/test/unit/SandboxPool.spec.ts index 65c30b84f7..1ab49691a6 100644 --- a/packages/core/test/unit/SandboxPool.spec.ts +++ b/packages/core/test/unit/SandboxPool.spec.ts @@ -61,7 +61,7 @@ describe(SandboxPool.name, () => { runResult: { tests: [], status: RunStatus.Complete }, sourceMapper: { transpiledFileNameFor: n => n, - transpiledLocationFor: n => n + transpiledLocationFor: n => Promise.resolve(n) } }; diff --git a/packages/core/test/unit/mutants/MutantTestMatcher.spec.ts b/packages/core/test/unit/mutants/MutantTestMatcher.spec.ts index 0143a0f8ef..86dfbbcfdc 100644 --- a/packages/core/test/unit/mutants/MutantTestMatcher.spec.ts +++ b/packages/core/test/unit/mutants/MutantTestMatcher.spec.ts @@ -92,9 +92,11 @@ describe(MutantTestMatcher.name, () => { describe('without code coverage info', () => { - it('should add both tests to the mutants and report failure', () => { - const result = sut.matchWithMutants(mutants); + it('should add both tests to the mutants and report failure', async () => { const expectedTestSelection = [{ id: 0, name: 'test one' }, { id: 1, name: 'test two' }]; + + const result = await sut.matchWithMutants(mutants); + expect(result[0].selectedTests).deep.eq(expectedTestSelection); expect(result[1].selectedTests).deep.eq(expectedTestSelection); expect(TestSelectionResult[result[0].testSelectionResult]).eq(TestSelectionResult[TestSelectionResult.FailedButAlreadyReported]); @@ -102,8 +104,9 @@ describe(MutantTestMatcher.name, () => { expect(testInjector.logger.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(mutants); + it('should have both mutants matched', async () => { + const result = await sut.matchWithMutants(mutants); + const matchedMutants: MatchedMutant[] = [ { fileName: result[0].fileName, @@ -122,6 +125,7 @@ describe(MutantTestMatcher.name, () => { timeSpentScopedTests: result[1].timeSpentScopedTests } ]; + expect(reporter.onAllMutantsMatchedWithTests).calledWith(Object.freeze(matchedMutants)); }); }); @@ -188,8 +192,9 @@ describe(MutantTestMatcher.name, () => { }; }); - it('should not have added the run results to the mutants', () => { - const result = sut.matchWithMutants(mutants); + it('should not have added the run results to the mutants', async () => { + const result = await sut.matchWithMutants(mutants); + expect(result[0].selectedTests).lengthOf(0); expect(result[1].selectedTests).lengthOf(0); }); @@ -226,12 +231,14 @@ describe(MutantTestMatcher.name, () => { sut = createSut(); }); - it('should have added the run results to the mutants', () => { - const result = sut.matchWithMutants(mutants); + it('should have added the run results to the mutants', async () => { const expectedTestSelectionFirstMutant: TestSelection[] = [ { id: 0, name: 'test one' }, { id: 1, name: 'test two' } ]; + + const result = await sut.matchWithMutants(mutants); + const expectedTestSelectionSecondMutant: TestSelection[] = [{ id: 0, name: 'test one' }]; expect(result[0].selectedTests).deep.eq(expectedTestSelectionFirstMutant); expect(result[1].selectedTests).deep.eq(expectedTestSelectionSecondMutant); @@ -248,12 +255,14 @@ describe(MutantTestMatcher.name, () => { sut = createSut(); }); - it('should select all test in the test run but not report the error yet', () => { - const result = sut.matchWithMutants(mutants); + it('should select all test in the test run but not report the error yet', async () => { const expectedTestSelection: TestSelection[] = [ { name: 'test one', id: 0 }, { name: 'test two', id: 1 } ]; + + const result = await sut.matchWithMutants(mutants); + expect(result[0].selectedTests).deep.eq(expectedTestSelection); expect(result[1].selectedTests).deep.eq(expectedTestSelection); expect(result[0].testSelectionResult).eq(TestSelectionResult.Failed); @@ -278,9 +287,11 @@ describe(MutantTestMatcher.name, () => { sut = createSut(); }); - it('should have added the run results to the mutant', () => { - const result = sut.matchWithMutants(mutants); + it('should have added the run results to the mutant', async () => { const expectedTestSelection = [{ id: 0, name: 'test one' }]; + + const result = await sut.matchWithMutants(mutants); + expect(result[0].selectedTests).deep.eq(expectedTestSelection); }); }); @@ -309,9 +320,11 @@ describe(MutantTestMatcher.name, () => { sut = createSut(); }); - it('should add all test results to the mutant that is covered by the baseline', () => { - const result = sut.matchWithMutants(mutants); + it('should add all test results to the mutant that is covered by the baseline', async () => { const expectedTestSelection = [{ id: 0, name: 'test one' }, { id: 1, name: 'test two' }]; + + const result = await sut.matchWithMutants(mutants); + expect(result[0].selectedTests).deep.eq(expectedTestSelection); expect(result[1].selectedTests).deep.eq(expectedTestSelection); }); @@ -320,7 +333,8 @@ describe(MutantTestMatcher.name, () => { }); describe('should not result in regression', () => { - it('should match up mutant for issue #151 (https://github.com/stryker-mutator/stryker/issues/151)', () => { + it('should match up mutant for issue #151 (https://github.com/stryker-mutator/stryker/issues/151)', async () => { + // Arrange const sourceFile = new SourceFile(new File('', '')); sourceFile.getLocation = () => ({ start: { line: 13, column: 38 }, end: { line: 24, column: 5 } }); const testableMutant = new TestableMutant('1', mutant({ @@ -338,7 +352,11 @@ describe(MutantTestMatcher.name, () => { timeSpentMs: 5 }); sut = createSut(); - sut.enrichWithCoveredTests(testableMutant); + + // Act + await sut.enrichWithCoveredTests(testableMutant); + + // Assert expect(testableMutant.selectedTests).deep.eq([{ id: 0, name: 'controllers SearchResultController should open a modal dialog with product details' @@ -354,11 +372,13 @@ describe(MutantTestMatcher.name, () => { sut = createSut(); }); - it('should match all mutants to all tests and log a warning when there is no coverage data', () => { + it('should match all mutants to all tests and log a warning when there is no coverage data', async () => { mutants.push(mutant({ fileName: 'fileWithMutantOne' }), mutant({ fileName: 'fileWithMutantTwo' })); initialRunResult.runResult.tests.push(testResult(), testResult()); - const result = sut.matchWithMutants(mutants); const expectedTestSelection: TestSelection[] = [{ id: 0, name: 'name' }, { id: 1, name: 'name' }]; + + const result = await sut.matchWithMutants(mutants); + expect(result[0].selectedTests).deep.eq(expectedTestSelection); expect(result[1].selectedTests).deep.eq(expectedTestSelection); expect(result[0].testSelectionResult).deep.eq(TestSelectionResult.FailedButAlreadyReported); @@ -380,12 +400,12 @@ describe(MutantTestMatcher.name, () => { }; }); - it('should retrieves source mapped location', () => { + it('should retrieves source mapped location', async () => { // Arrange mutants.push(mutant({ fileName: 'fileWithMutantOne', range: [4, 5] })); // Act - sut.matchWithMutants(mutants); + await sut.matchWithMutants(mutants); // Assert const expectedLocation: MappedLocation = { @@ -398,13 +418,13 @@ describe(MutantTestMatcher.name, () => { expect(initialRunResult.sourceMapper.transpiledLocationFor).calledWith(expectedLocation); }); - it('should match mutant to single test result', () => { + it('should match mutant to single test result', async () => { // Arrange mutants.push(mutant({ fileName: 'fileWithMutantOne', range: [4, 5] })); initialRunResult.runResult.tests.push(testResult({ name: 'test 1' }), testResult({ name: 'test 2' })); // Act - const result = sut.matchWithMutants(mutants); + const result = await sut.matchWithMutants(mutants); // Assert const expectedTestSelection: TestSelection[] = [{ @@ -428,11 +448,13 @@ describe(MutantTestMatcher.name, () => { sut = createSut(); }); - it('should match all mutants to all tests', () => { + it('should match all mutants to all tests', async () => { mutants.push(mutant({ fileName: 'fileWithMutantOne' }), mutant({ fileName: 'fileWithMutantTwo' })); initialRunResult.runResult.tests.push(testResult(), testResult()); - const result = sut.matchWithMutants(mutants); const expectedTestSelection = [{ id: 0, name: 'name' }, { id: 1, name: 'name' }]; + + const result = await sut.matchWithMutants(mutants); + expect(result[0].selectedTests).deep.eq(expectedTestSelection); expect(result[1].selectedTests).deep.eq(expectedTestSelection); }); diff --git a/packages/core/test/unit/transpiler/SourceMapper.spec.ts b/packages/core/test/unit/transpiler/SourceMapper.spec.ts index f76ecd05a5..7bd666a683 100644 --- a/packages/core/test/unit/transpiler/SourceMapper.spec.ts +++ b/packages/core/test/unit/transpiler/SourceMapper.spec.ts @@ -28,10 +28,10 @@ describe('SourceMapper', () => { // For some reason, `generatedPositionFor` is not defined on the `SourceMapConsumer` prototype // Define it here by hand sourceMapConsumerMock.generatedPositionFor = sinon.stub(); - sourceMapConsumerMock.generatedPositionFor.returns({ + sourceMapConsumerMock.generatedPositionFor.returns(Promise.resolve({ column: 2, line: 1 - }); + })); sinon.stub(sourceMapModule, 'SourceMapConsumer').returns(sourceMapConsumerMock); // Restore the static values, removed by the stub @@ -56,12 +56,12 @@ describe('SourceMapper', () => { sut = new PassThroughSourceMapper(); }); - it('should pass through the input on transpiledLocationFor', () => { + it('should pass through the input on transpiledLocationFor', async () => { const input: MappedLocation = { fileName: 'foo/bar.js', location: locationFactory() }; - expect(sut.transpiledLocationFor(input)).eq(input); + expect(await sut.transpiledLocationFor(input)).eq(input); }); }); @@ -73,7 +73,7 @@ describe('SourceMapper', () => { sut = new TranspiledSourceMapper(transpiledFiles); }); - it('should create SourceMapConsumers for files when transpiledLocationFor is called', () => { + it('should create SourceMapConsumer for a file when transpiledLocationFor is called', async () => { // Arrange const expectedMapFile1 = { sources: ['file1.ts'] }; const expectedMapFile2 = { sources: ['file2.ts'] }; @@ -82,63 +82,67 @@ describe('SourceMapper', () => { transpiledFiles.push(new File('file2.js', `// # sourceMappingURL=data:application/json;base64,${base64Encode(JSON.stringify(expectedMapFile2))}`)); // Act - sut.transpiledLocationFor(mappedLocation({ fileName: 'file1.ts' })); + await sut.transpiledLocationFor(mappedLocation({ fileName: 'file1.ts' })); // Assert expect(sourceMapModule.SourceMapConsumer).calledWithNew; expect(sourceMapModule.SourceMapConsumer).calledWith(expectedMapFile1); - expect(sourceMapModule.SourceMapConsumer).calledWith(expectedMapFile2); + expect(sourceMapModule.SourceMapConsumer).not.calledWith(expectedMapFile2); }); - it('should cache source maps for future use when `transpiledLocationFor` is called', () => { + it('should cache source maps for future use when `transpiledLocationFor` is called', async () => { // Arrange const expectedMapFile1 = { sources: ['file1.ts'] }; transpiledFiles.push(new File('file1.js', `// # sourceMappingURL=data:application/json;base64,${base64Encode(JSON.stringify(expectedMapFile1))}`)); // Act - sut.transpiledLocationFor(mappedLocation({ fileName: 'file1.ts' })); - sut.transpiledLocationFor(mappedLocation({ fileName: 'file1.ts' })); + await sut.transpiledLocationFor(mappedLocation({ fileName: 'file1.ts' })); + await 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 when the requested source map could not be found', async () => { + await expect(sut.transpiledLocationFor(mappedLocation({ fileName: 'foobar' }))).to.be.rejectedWith(SourceMapError, 'Source map not found for "foobar"' + ERROR_POSTFIX); }); - it('should throw an error if source map file is a binary file', () => { + it('should throw an error if source map file is a binary file', async () => { transpiledFiles.push(new File('file.js', '// # sourceMappingURL=file1.js.map')); transpiledFiles.push(new File('file1.js.map', Buffer.from(PNG_BASE64_ENCODED, 'base64'))); - expect(() => sut.transpiledLocationFor(mappedLocation({ fileName: 'foobar' }))) - .throws(SourceMapError, /^Source map file "file1.js.map" could not be parsed as json. Cannot analyse code coverage. Setting `coverageAnalysis: "off"` in your stryker.conf.js will prevent this error/); + + await expect(sut.transpiledLocationFor(mappedLocation({ fileName: 'foobar' }))) + .to.be.rejectedWith(SourceMapError, /^Source map file "file1.js.map" could not be parsed as json. Cannot analyse code coverage. Setting `coverageAnalysis: "off"` in your stryker.conf.js will prevent this error/); }); - it('should throw an error if source map data url is not supported', () => { + it('should throw an error if source map data url is not supported', async () => { const expectedMapFile1 = { sources: ['file1.ts'] }; transpiledFiles.push(new File('file1.js', `// # 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}`); + + await expect(sut.transpiledLocationFor(mappedLocation({ fileName: 'foobar' }))) + .to.be.rejectedWith(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', () => { + it('should throw an error if source map file cannot be found', async () => { transpiledFiles.push(new File('file1.js', '// # 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}`); + + await expect(sut.transpiledLocationFor(mappedLocation({ fileName: 'foobar' }))) + .to.be.rejectedWith(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', () => { + it('should throw an error if source map file url is not declared in a transpiled file', async () => { transpiledFiles.push(new File('file1.js', `// # sourceMapping%%%=file1.js.map`)); - expect(() => sut.transpiledLocationFor(mappedLocation({ fileName: 'foobar' }))) - .throws(SourceMapError, `Source map not found for "foobar"${ERROR_POSTFIX}`); + + await expect(sut.transpiledLocationFor(mappedLocation({ fileName: 'foobar' }))) + .to.be.rejectedWith(SourceMapError, `Source map not found for "foobar"${ERROR_POSTFIX}`); }); - it('should not throw an error if one of the files is a binary file', () => { + it('should not throw an error if one of the files is a binary file', async () => { const expectedMapFile1 = { sources: ['file1.ts'] }; transpiledFiles.push(new File('file1.js', `// # sourceMappingURL=data:application/json;base64,${base64Encode(JSON.stringify(expectedMapFile1))}`)); transpiledFiles.push(new File('foo.png', Buffer.from(PNG_BASE64_ENCODED, 'base64'))); - expect(sut.transpiledLocationFor(mappedLocation({ fileName: 'file1.ts' }))).deep.eq({ + + await expect(sut.transpiledLocationFor(mappedLocation({ fileName: 'file1.ts' }))).to.eventually.deep.eq({ fileName: 'file1.js', location: { end: {