Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(coverage analysis): Support transpiled code #559

Merged
merged 10 commits into from
Feb 3, 2018

Conversation

nicojs
Copy link
Member

@nicojs nicojs commented Dec 24, 2017

Support coverage analysis while having one transpiler enabled. Remove the old "keepSourceMaps" boolean on transpiler options in favor of "produceSourceMaps". This allows for the source map resolving logic to be moved to stryker itself, removing the need to implement it in every transpiler plugin.

TODO:

  • Enable source map for the stryker-babel-transpiler Throw an error if the stryker-babel-transpiler is asked to produce source maps (for now)
  • Add unit tests to the SourceMapper class
  • Add integration test for SourceMapping
  • Find a way to remove the warnings if a mutant has no javascript representation. For example: mutating a TypeScript interface. Use this file as an example: "/src/isolated-runner/MessageProtocol.ts"
  • Refactor the MutantTestMatcher class. (too complex)
  • Write an awesome blog article (PR here: docs(blog): Add blog about typescript coverage analysis stryker-mutator.github.io#53)

Done:

Stryker API

  • Rename keepSourceMaps -> produceSourceMaps on TranspilerOptions
  • Remove getMappedLocation method from Transpiler
  • Remove FileLocation interface from the transpiler api (not needed anymore)
  • Add an id field to MutantResult and MatchedMutant. This is used in the progress reporter for now to identify mutants without test coverage.

Stryker

  • Add dependency to 'source-map'.
  • Add class SourceMapper responsible for mapping original code to transpiled code.
  • Use SourceMapper in the MutantTestMatcher in order to locate a mutant in the js output file.
  • Use the mutant id in the progress reporter to identify mutants without test coverage. Using MutantResult.NoCoverage for this is not enough anymore as mutants that are not matched to a test can now also have the status TranspileError.
  • MutationTestExecutor: Report a mutant as survived early if a mutant results in the same transpiled code (i.e. mutating a TypeScript interface)
  • MutantTestMatcher: Delay reporting a warning if the statement could not be found in the statement map. Instead mark the mutant as TestSelectionResult.Failed and later log the warning if it is ran in the Sandbox. Don't report if an early result prevented the mutant from being tested.

Stryker TypeScript

  • Remove old source-mapping logic from stryker-typescript, it was never used
  • Support produceSourceMaps transpiler option

Stryker babel transpiler

  • throw error when asked to produce source maps (for now)

First attempt implement coverage analysis while having one transpiler enabled. Removed the old "keepSourceMaps" boolean on transpiler options in favor of "produceSourceMaps". This allows for the source map resolving logic to be moved to stryker itself.

*Stryker API*

* Rename `keepSourceMaps` -> `produceSourceMaps` on `TranspilerOptions`
* Remove `getMappedLocation` method from `Transpiler`
* Remove `FileLocation` interface from the transpiler api (not needed anymore)
* Add an id` field to `MutantResult` and `MatchedMutant`. This is used in the progress reporter for now to identify mutants without test coverage.

*Stryker*

* Add dependency to 'source-map'.
* Add class `SourceMapper` responsible for mapping original code to transpiled code.
* Use `SourceMapper` in the `MutantTestMatcher` in order to locate a mutant in the js output file.
* Use the mutant id in the progress reporter to identify mutants without test coverage. Using `MutantResult.NoCoverage` for this is not enough anymore as mutants that are not matched to a test can now also have the status `TranspileError`.

*Stryker TypeScript*

* Remove old source-mapping logic from stryker-typescript, it was never used
@nicojs
Copy link
Member Author

nicojs commented Dec 24, 2017

@Archcry could you add babel source map production to this branch? You can use stryker-typescript as an example.

@nicojs nicojs changed the title WIP: feat(coverage analysis): Implement coverage analysis with one WIP: feat(coverage analysis): Support transliled code Dec 27, 2017
@nicojs nicojs changed the title WIP: feat(coverage analysis): Support transliled code WIP: feat(coverage analysis): Support transpiled code Dec 27, 2017
@nicojs
Copy link
Member Author

nicojs commented Dec 27, 2017

Find a way to remove the warnings if a mutant has no javascript representation. For example: mutating a TypeScript interface. Use this file as an example: "/src/isolated-runner/MessageProtocol.ts"

@simondel what do you think about this solution:

Delay printing the warning until after the mutant is transpiled. If the transpiled code results in the same code is the same as without the mutant (for example: mutating a typescript interface), we don't even run any tests, as we know for sure they will pass. We just mark the mutant as survived, never printing a warning.

EDIT: This is what i have implemented in my latest commit.

* Don't run mutant in a sandbox when the transpiled result is the same as the not mutated transpiled result
* When the tests for a mutant could not be selected because the statement or function could not be found in the maps, delay reporting the warning untill we know for sure a mutant does not have an early result.
@nicojs
Copy link
Member Author

nicojs commented Dec 28, 2017

Running Stryker on Stryker now takes 6 minutes on my machine (max resources allocated). Saving about 10 minutes.

Ran 41.88 tests per mutant on average.
[2017-12-28 07:26:48.679] [INFO] Stryker - Done in 6 minutes 4 seconds.

@nicojs nicojs changed the title WIP: feat(coverage analysis): Support transpiled code feat(coverage analysis): Support transpiled code Dec 28, 2017
Copy link
Member

@simondel simondel left a comment

Choose a reason for hiding this comment

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

Here is part one of the feedback. I mainly find the SourceMapper to be quite confusing

@@ -10,5 +10,5 @@ export default interface TranspilerOptions {
* Indicates whether or not the source maps need to be kept.
Copy link
Member

Choose a reason for hiding this comment

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

This comment seems to be out of date

const resultFiles = filterNotEmpty(allFiles.map(file => {
if (file.transpiled && isTypescriptFile(file)) {
return outputFiles[file.name];
let moreOutput = true;
Copy link
Member

Choose a reason for hiding this comment

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

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

transpiled: sourceFile.transpiled,
emit(fileDescriptor: FileDescriptor): EmitOutput {
const emittedFiles = this.languageService.getEmitOutput(fileDescriptor.name).outputFiles;
const jsFile = emittedFiles.find(file => file.name.endsWith('.js'));
Copy link
Member

Choose a reason for hiding this comment

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

Is there a chance that this outputs '.jsx' files?

@@ -133,4 +123,8 @@ describe('TypescriptTranspiler', () => {
expect(result.outputFiles).lengthOf(0);
});
});

describe('transpile with source maps', () => {
Copy link
Member

Choose a reason for hiding this comment

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

Is this supposed to be empty?

Copy link
Member Author

Choose a reason for hiding this comment

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

euhm... 😒

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(id, testResult));
Copy link
Member

Choose a reason for hiding this comment

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

It would be cleaner if we would swap the params around so they are consistent. Right now we swap the params around when we call selectTest

@@ -65,6 +65,9 @@ export default class Sandbox {

public async runMutant(transpiledMutant: TranspiledMutant): Promise<RunResult> {
const mutantFiles = transpiledMutant.transpileResult.outputFiles;
if (transpiledMutant.mutant.testSelectionResult === TestSelectionResult.Failed) {
Copy link
Member

Choose a reason for hiding this comment

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

Can't we check this in the MutantTestMatcher?

Copy link
Member

Choose a reason for hiding this comment

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

And if we move this check to the MutantTestMatcher, we may be able to remove the TestSelectionResult all together.

Copy link
Member Author

Choose a reason for hiding this comment

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

No. I added it in this PR. See my comment here: #559 (comment)

If you have a better idea, please let me know :/

SOURCE_MAP_URL_REGEX.lastIndex = 0;
let currentMatch: RegExpExecArray | null;
let lastMatch: RegExpExecArray | null = null;
while (currentMatch = SOURCE_MAP_URL_REGEX.exec(transpiledFile.content)) {
Copy link
Member

Choose a reason for hiding this comment

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

What does this do?

Copy link
Member Author

Choose a reason for hiding this comment

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

it retrieves the final sourceMappingURL comment in a given file. I've added a comment to it 👍

}

private getSourceMapForFile(transpiledFile: TextFile) {
const sourceMappingUrl = this.getSourceMapUrl(transpiledFile);
Copy link
Member

Choose a reason for hiding this comment

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

Is it an URL that we get?

Copy link
Member Author

@nicojs nicojs Jan 19, 2018

Choose a reason for hiding this comment

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

Yes. It is either a data url (data:application/json;base64,ABC...) or an external url (main.js.map).

I've renamed some methods:
getSourceMapFromTranspiledFiles -> getExternalSourceMap
getSourceMapFromDataUrl -> getInlineSourceMap

Is that more clear?

}
}

export class TranspiledSourceMapper extends SourceMapper {
Copy link
Member

Choose a reason for hiding this comment

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

This class is quite difficult to read for me. I have the feeling that I get sent all over the place and I have no idea why the code does what it does.

Copy link
Member Author

Choose a reason for hiding this comment

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

I've refactored a bunch of code and made it more object-oriented by adding a SourceMap class (instead of a SourceMapInfo interface). Hope this is a bit clearer.

@@ -55,45 +65,52 @@ export default class MutantTestMatcher {
}

enrichWithCoveredTests(testableMutant: TestableMutant) {
Copy link
Member

Choose a reason for hiding this comment

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

This is only used privately (except for when testing)

Copy link
Member Author

@nicojs nicojs Jan 19, 2018

Choose a reason for hiding this comment

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

Good catch. Yes, it is public as a convenience for unit testing. Ideally I don't like to make these methods public, but it already was the case and I didn't feel like refactoring that part as well.

@nicojs
Copy link
Member Author

nicojs commented Jan 19, 2018

@simondel Thanks for your feedback! I think I've commented on/fixed all remarks. Could you please take another look.

@simondel simondel merged commit 7c351ad into master Feb 3, 2018
@simondel simondel deleted the feat/transpiler-coverage-analysis branch February 3, 2018 20:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants