-
-
Notifications
You must be signed in to change notification settings - Fork 6.5k
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
Conversation
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. |
Short answer, yes it resolves that. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
||
if (shouldTransform(filename, config)) { | ||
if (transform.canInstrument && typeof transform.getEmptyCoverage === 'function') { | ||
return transform.getEmptyCoverage(content, filename, config, { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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? :)
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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}
.
There was a problem hiding this comment.
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 }); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
oh wow, this is indeed cool! That will make ts-jest a lot nicer I'm sure. |
@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 :) |
and what about the performance? is it much slower to do the mapping? |
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 |
This will only affect performance when doing |
@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)
With mapped coverage and source maps
With mapped coverage, but no source maps supplied
|
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. |
@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 😄 ) |
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 Report
@@ 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
Continue to review full report at Codecov.
|
7f68b71
to
8320519
Compare
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 |
eee261e
to
59d3f14
Compare
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. |
d7964c2
to
78c0a62
Compare
@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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const sourceMapFilePath = cacheFilePath + '.map'; | ||
writeCacheFile(sourceMapFilePath, sourceMapContent); | ||
result += | ||
`\n;global.__coverage__['${escapePathForJavaScript(filename)}']` + |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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:
There was a problem hiding this comment.
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?
types/TestResult.js
Outdated
statementMap: { [statementId: number]: any }, | ||
branchMap: { [branchId: number]: any }, | ||
inputSourceMap?: Object, | ||
inputSourceMapPath?: string, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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).
types/Transform.js
Outdated
@@ -11,6 +11,11 @@ | |||
|
|||
import type {Config, Path} from 'types/Config'; | |||
|
|||
export type TransformedSource = {| | |||
content: string, | |||
sourceMap: ?Object | string, |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
I'd prefer to hold off until after Jest 19 with this one. |
@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
@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. |
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.
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. |
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. |
i tested it locally and it works really nice! |
@jwbay thank you so much for working on it! and sorry it took so long :) |
* 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
* 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
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. |
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:
After:
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.