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

Map code coverage from transformers #2290

Merged
merged 10 commits into from
Mar 3, 2017
Merged

Conversation

jwbay
Copy link
Contributor

@jwbay jwbay commented Dec 11, 2016

Summary
With this PR transformers can supply source maps to Jest and see code coverage mapped against the original sources in all reports without any further processing.

Example transformer - https://github.com/jwbay/jestcoveragetest/blob/master/preprocessor.js#L30-L33

The support for threading source maps through like this was only recently added to the istanbul-lib-x packages.

Test plan
jest

There is a test repo here that I've been using to verify behavior. Screenshots are from that. https://github.com/jwbay/jestcoveragetest

Before:

image

After:

image

It's not completely perfect but I think it's the closest we can get for now -- I think the trailing braces are an issue with fidelity of the supplied source maps. It seems inline with the results of remap-istanbul, which is basically the gold standard.

TODO:

cc @kulshekhar, @kwonoj, and @Igmat who have been working with coverage remapping in ts-jest. If you all have any test cases it would be good to see if this handles them.

@kwonoj
Copy link
Contributor

kwonoj commented Dec 11, 2016

One of the suffering point of external code coverage mapping is jest does own caching mechanism and does not invoke preprocessor if a file has not changed. Is this change resolves those issue? Just looking at glimpse it relies on existing preprocessor pipeline.

@jwbay
Copy link
Contributor Author

jwbay commented Dec 11, 2016

Short answer, yes it resolves that. This approach does rely on the source map having the original sources inlined into the sourcesText property. (fixed) The source map supplied to the instrumenter is attached to the coverage object by istanbul and written out alongside the covered code, which is what Jest caches.

Copy link
Contributor

@aaronabramov aaronabramov left a comment

Choose a reason for hiding this comment

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

that is actually pretty awesome.
It seems like it affects the coverage as well (not only line mapping)

screen shot 2016-12-12 at 7 43 22 pm

screen shot 2016-12-12 at 7 43 14 pm

is that expected?


if (shouldTransform(filename, config)) {
if (transform.canInstrument && typeof transform.getEmptyCoverage === 'function') {
return transform.getEmptyCoverage(content, filename, config, {
Copy link
Contributor

Choose a reason for hiding this comment

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

does that delegate getting the empty coverage object to a transformer if it implements it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, exactly. Not sure how important visual accuracy is for uncovered code, but it can look pretty busted without this, so you'd probably get a couple issues now and then. Also prevents the branch/etc count from jumping up/down depending on whether the file executed or not.

scripts/build.js Outdated
@@ -69,7 +69,7 @@ function buildFile(file, silent) {
const relativeToSrcPath = path.relative(packageSrcPath, file);
const destPath = path.resolve(packageBuildPath, relativeToSrcPath);

spawnSync('mkdir', ['-p', path.dirname(destPath)]);
mkdir.sync(path.dirname(destPath));
Copy link
Contributor

Choose a reason for hiding this comment

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

did it fail on win or something? :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes 😅 Since mkdirp was already in jest's dependencies I figured it wouldn't hurt.

@@ -59,6 +62,7 @@ class CoverageReporter extends BaseReporter {
runnerContext: RunnerContext,
) {
this._addUntestedFiles(config, runnerContext);
const { map, sourceFinder } = this._sourceMapStore.transformCoverage(this._coverageMap);
Copy link
Contributor

Choose a reason for hiding this comment

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

which part is actually reading the source maps? and what happens if source maps are not available?

Copy link
Member

Choose a reason for hiding this comment

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

nit: code style, this should be {map, sourceFinder}.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@DmitriiAbramov istanbul-lib-source-maps has code that plucks the source map from the coverage object as part of this transformCoverage call. If no source map is tacked onto the object, it's not mapped.

@cpojer Will fix shortly!

@@ -75,7 +79,7 @@ class CoverageReporter extends BaseReporter {
}

reporter.addAll(coverageReporters);
reporter.write(this._coverageMap);
reporter.write(map, { sourceFinder });
Copy link
Member

Choose a reason for hiding this comment

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

code style too.

@@ -342,3 +367,4 @@ module.exports = (

module.exports.EVAL_RESULT_VARIABLE = EVAL_RESULT_VARIABLE;
module.exports.transformSource = transformSource;
module.exports.generateEmptyCoverage = generateEmptyCoverage;
Copy link
Member

Choose a reason for hiding this comment

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

please add an empty line at the end of this file.

@cpojer
Copy link
Member

cpojer commented Dec 13, 2016

oh wow, this is indeed cool! That will make ts-jest a lot nicer I'm sure.

@jwbay
Copy link
Contributor Author

jwbay commented Dec 13, 2016

@DmitriiAbramov I can't seem to reply directly to your question with the images, but yes, it's expected. The branch/statement and sometimes function count can be different in the original source vs. the transformed/covered code if a compiler injects new branches/statements/etc. When this is reconciled during the remapping, the percentages are changed as a result. Whether that's correct behavior seems to be an open question with lots of opinions :)

@aaronabramov
Copy link
Contributor

and what about the performance? is it much slower to do the mapping?

@jwbay
Copy link
Contributor Author

jwbay commented Dec 14, 2016

I'll do some performance testing. Hopefully it's not a huge hit, but I'm sure it's not free. It's going to be doing this extra work per file with coverage - https://github.com/istanbuljs/istanbul-lib-source-maps/blob/master/lib/transformer.js#L84.

Also, I found a way to make this work with babel that should make the transformers do far less work. They won't need to take over instrumentation to see remapped coverage and we won't need to add getEmptyCoverage to the transformer API. It needs this PR merged and a different kind of transformer API change. Inline source maps will just work and we could limit this to transformers that support them, but I'd like to make transformer.process return either a content string as it does today, or an object like { code: string, sourceMap: Object }.

@cpojer
Copy link
Member

cpojer commented Dec 14, 2016

This will only affect performance when doing jest --no-cache --coverage, right? Can you take a few samples before and after and give us some numbers? This may make sense on the Jest repo itself.

@jwbay
Copy link
Contributor Author

jwbay commented Dec 16, 2016

@cpojer It will affect performance on every run that does --coverage if source maps are present. Cache doesn't come into play unless we're talking about cost of generating source maps, which depends on the transformer.

Here are some perf numbers. I opted for synthetic tests to have more control and test project sizes. Each file has 60 statements, 50 branches, 20 functions, and 50 lines. Each row is an average of 5 runs. 'Coverage time' is time spent in the onRunComplete method of CoverageReporter.

Without mapped coverage (master branch)

Test file count Total time Coverage time
10 3.00s 0.23s
100 7.95s 0.47s
1000 55.42s 2.90s

With mapped coverage and source maps

Test file count Total time Coverage time
10 3.08s 0.28s
100 8.67s 0.76s
1000 57.18s 4.43s

With mapped coverage, but no source maps supplied

Test file count Total time Coverage time
1000 55.61s 2.86s

@jwbay
Copy link
Contributor Author

jwbay commented Dec 16, 2016

Another concern is memory. If the source map has its source content inlined, you could end up with the source code text of your entire project held in memory by the main Jest process. This could be bad 😅 I'll look at whether it's possible to remap on the fly and clean up after.

@aaronabramov
Copy link
Contributor

@jwbay i don't think it's possible to clean up on the fly, because you don't know if there's going to be more coverage coming for a specific file until the end of the test suite. But we definitely need to make sure we only collect source map for files we intend to cover and that we provide an option to disable mapping (at fb souce maps will be gigabytes of data 😄 )

@jwbay
Copy link
Contributor Author

jwbay commented Dec 17, 2016

I meant more cleaning up the source map than the coverage. When a single test result comes in with a source map attached, I think we might able to remap there and delete the source map, so we're only ever merging mapped coverage and the source map doesn't live past its usefulness. This would hurt runtime if a file is executed multiple times across tests, though. You could be mapping against the same source map multiple times instead of once at the end of the run.

Completely turning off source maps is going to be difficult with this PR in its current form. If source maps are inlined with the content, they'll get picked up by Babel and attached to coverage by the plugin. I'll add some kind of flag to prevent that behavior since it's pretty memory intensive to not have an off switch.

@codecov-io
Copy link

codecov-io commented Dec 17, 2016

Codecov Report

Merging #2290 into master will increase coverage by 0.06%.
The diff coverage is 80.43%.

@@            Coverage Diff             @@
##           master    #2290      +/-   ##
==========================================
+ Coverage   67.12%   67.18%   +0.06%     
==========================================
  Files         142      143       +1     
  Lines        5126     5163      +37     
  Branches        0        3       +3     
==========================================
+ Hits         3441     3469      +28     
- Misses       1685     1693       +8     
- Partials        0        1       +1
Impacted Files Coverage Δ
packages/jest-config/src/validConfig.js 100% <ø> (ø)
packages/jest-config/src/normalize.js 85.51% <ø> (ø)
packages/jest-config/src/defaults.js 100% <ø> (ø)
packages/jest-config/src/setFromArgv.js 0% <ø> (ø)
packages/jest-runtime/src/transform.js 89.33% <100%> (+1.45%)
...ackages/jest-cli/src/reporters/CoverageReporter.js 61.11% <57.14%> (-3.41%)
integration_tests/coverage-remapping/covered.ts 85.71% <85.71%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 8c17c0f...ad11e77. Read the comment docs.

@aaronabramov
Copy link
Contributor

i'm super excited about this RP, i'll try to test in on the internal FB repo and see how much memory it eats

@jwbay
Copy link
Contributor Author

jwbay commented Dec 29, 2016

This is ready for review. The babel plugin update is released as an RC at the moment. Once that's promoted all the boxes will be ticked.

@jwbay jwbay changed the title [WIP] Map code coverage from transformers Map code coverage from transformers Dec 29, 2016
@jwbay jwbay force-pushed the map-coverage branch 2 times, most recently from d7964c2 to 78c0a62 Compare February 13, 2017 04:40
@jwbay
Copy link
Contributor Author

jwbay commented Feb 13, 2017

@DmitriiAbramov You might get a kick out of this -- turns out the test lockups I assumed were caused by memory pressure were actually caused by the same issue you found. In my case it was a vendored library that ended up being a 3MB string after instrumentation. It was a coincidence that a couple of the largest dependency graphs in the app happened to hit that file.

I switched the implementation to store source maps on disk anyway, though. It's much safer performance wise. The cost is now concentrated as CPU time at the end of the run instead of spread out as memory during the run (and costing CPU time at the end anyway). A project with 3K files takes about 1500ms to remap, and a project with a couple hundred files takes 300ms.

Copy link
Contributor

@aaronabramov aaronabramov left a comment

Choose a reason for hiding this comment

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

@jwbay have you tried mapping coverage for babel transforms?

i know it's not very useful, since babel-jest can handle transformation with the need to remap anything, but i was wondering if we should at least add a test case for it.

@cpojer what do you think?

const sourceMapFilePath = cacheFilePath + '.map';
writeCacheFile(sourceMapFilePath, sourceMapContent);
result +=
`\n;global.__coverage__['${escapePathForJavaScript(filename)}']` +
Copy link
Contributor

Choose a reason for hiding this comment

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

isn't __coverage__ name configurable from istanbul? we can potentially break coverage collection.
i also think that this wrapping logic deserves to be a separate function and inputSourceMapPath property name should be extracted into a separate constant, since it's something we introduce to istanbul coverage object

Copy link
Contributor

Choose a reason for hiding this comment

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

@DmitriiAbramov well in practice, it can pretty much be assumed that __covearge__ is the variable (I don't know that I've seen many deviations), this variable can be configured:

https://github.com/istanbuljs/istanbul-lib-instrument/blob/b08e4f55d60a43d542add9b09b3bf52a1d88ce6f/src/instrumenter.js#L13

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, the more I look at this the less I like it. To do this correctly, we could try and update the istanbul-lib-x packages to either overload the inputSourceMap property to accept a path to a source map as well as a source map proper, or add a new inputSourceMapPath property. Either one would be fairly straightforward, it would just need to touch two or three packages. @bcoe what do you think?

statementMap: { [statementId: number]: any },
branchMap: { [branchId: number]: any },
inputSourceMap?: Object,
inputSourceMapPath?: string,
Copy link
Contributor

Choose a reason for hiding this comment

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

is this istanbul shape, or are we adding sourcemap properties to it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

inputSourceMap is official. inputSourceMapPath is not (yet).

@@ -11,6 +11,11 @@

import type {Config, Path} from 'types/Config';

export type TransformedSource = {|
content: string,
sourceMap: ?Object | string,
Copy link
Contributor

Choose a reason for hiding this comment

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

is that the way typescript compiler returns it by default?
if not, should we rename it to babel's defaults?
i think it was {code, map} shape

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, typescript returns something different. I was trying not to play favorites with compilers too much. I'll change to babel's shape.

@cpojer
Copy link
Member

cpojer commented Feb 14, 2017

I'd prefer to hold off until after Jest 19 with this one.

@jwbay
Copy link
Contributor Author

jwbay commented Feb 15, 2017

@DmitriiAbramov I haven't really tried to remap babel output for the reason you mentioned. I'll see if I can add a test.

# Conflicts:
#	packages/jest-cli/package.json
#	packages/jest-config/src/setFromArgv.js
#	packages/jest-runtime/package.json
#	types/TestResult.js
# Conflicts:
#	packages/jest-cli/package.json
@jwbay
Copy link
Contributor Author

jwbay commented Feb 24, 2017

@DmitriiAbramov ecbde20 switches the approach from source map info hitching a ride on the global coverage object to being more of a first class citizen in jest-runtime. It required some function signature changes, but disconnecting source maps from coverage a bit has some benefits. It sets Jest up for remapping stacktraces for errors, and jest-editor-support might be able to do some cool stuff with source maps exposed. Also, we can now map coverage for untested files, which doesn't seem too important but will probably save Jest a few github issues now and then.

@jwbay
Copy link
Contributor Author

jwbay commented Feb 24, 2017

As far as compiler support, it doesn’t look good for babel. I tested four compilers: babel, typescript, buble, and coffeescript. The testbed is updated with the transformers I used.

  • It seems Babel is doing something with its source maps that the source-map package doesn’t like. It ends up returning a lot of wrong/invalid positions. Sometimes it gets things right, but not often. I tried retainLines to help it along but it didn’t do much.
  • Typescript is pretty solid. It’s not perfect, but I’ve not seen things go horribly wrong.
  • Buble is also solid, which is impressive since it apparently does string transformations instead of an AST transform.
  • Coffeescript is basically all over the place.

I debugged a bit and it looks like istanbul-lib-source-maps isn’t doing anything weird or wrong. It seems to come down to the source-map package returning odd source positions for source maps from some compilers. Given all this, I’ve added a disclaimer of sorts to the docs for the flag.

@jwbay jwbay changed the title [WIP] Map code coverage from transformers Map code coverage from transformers Feb 24, 2017
@bookman25
Copy link
Contributor

Just ran into a good use case for this. By having jest provide the source map information, it would allow jest-editor-support to leverage that info for generating code coverage overlays.

@aaronabramov
Copy link
Contributor

i tested it locally and it works really nice!
no luck with www, but it's because of our monstrous transform pipeline (i don't think it's a blocker since it'll not affect www and we won't be able to use it right now anyway)

@aaronabramov aaronabramov merged commit c6d2b08 into jestjs:master Mar 3, 2017
@aaronabramov
Copy link
Contributor

@jwbay thank you so much for working on it! and sorry it took so long :)
This is going to be a game changer for TS projects!

@jwbay jwbay deleted the map-coverage branch March 9, 2017 23:29
skovhus pushed a commit to skovhus/jest that referenced this pull request Apr 29, 2017
* source map support for coverage

* be less dangerous

only enforce files in src/ having @flow since everything else may not be run through babel

* lazy require for istanbul-lib-source-maps

* add documentation for mapCoverage config option

* store source maps on disk instead of in worker process memory

* add mapCoverage to CLI, switch off by default

* move source map info from coverage object to jest-runtime

* tweak docs, fix a test
tushardhole pushed a commit to tushardhole/jest that referenced this pull request Aug 21, 2017
* source map support for coverage

* be less dangerous

only enforce files in src/ having @flow since everything else may not be run through babel

* lazy require for istanbul-lib-source-maps

* add documentation for mapCoverage config option

* store source maps on disk instead of in worker process memory

* add mapCoverage to CLI, switch off by default

* move source map info from coverage object to jest-runtime

* tweak docs, fix a test
@github-actions
Copy link

This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.
Please note this issue tracker is not a help forum. We recommend using StackOverflow or our discord channel for questions.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators May 13, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants