You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@coderaiser thanks for the bug report. I'm not super familiar with the loader API, so not sure where this bug might be cropping up -- it could be as simple as coverage reports not being output by Node.js in one edge case (which would be relatively easy to fix).
If the problem is missing lines of coverage in v8 itself, this tends to be more difficult to fix.
If you beat me to the punch, and are excited to dig into this, where I would start looking would be in the raw v8 output:
Just landed ability to generate sourcemap according to file url received from loader. Since mock-import does some changes, sourcemap should help to identify correct locations of code parts.
And it works good in the browser. The question is: does c8 supports instrumented code?
I see that there is an option --exclude-after-remap but I don't see any changes in coverage information.
Could you please help me to get source map working?
@bcoe Unfortunately it shows full coverage every time, so it’s useless. Could you please tell me is it possible to make c8 read source maps?
Source file is changed inside of loader, and it has different name(with ?mock-import=n) to have ability to import file again with mocked import. It works very good, but sometimes (like in this example) shoes bad lines and columns positions, and shows that some code is uncovered, but it is covered.
But they are needed to import file again and transform it's content with mocked modules. Is it possible to cut them after we merged reports related to all files with prefix?
Hey @coderaiser, can you figure out a minimal failing test case? that demonstrates how truncating the query string causes this bug?
I think we could probably figure out an approach to keep the suffix longer, until the source map lookup occurs, if that's the root of the problem (I think I'm not yet fully understanding, what do the entries for sourcemaps look like In the cache, they include the query string?)
Hey @coderaiser, can you figure out a minimal failing test case? that demonstrates how truncating the query string causes this bug?
Here is repository and test, to get things working run:
git clone https://github.com/coderaiser/c8-reproduce
cd c8-reproduce
npm i
npm run coverage
You can use npm run debug for debugging.
And you will see that coverage information is not full:
changelog.js | 100 | 20 | 100 | 100 | 11-14
Here is PR with partial support of sourcemaps, it keeps file URLS as they are, but the problem still exist in https://github.com/bcoe/c8/blob/v7.11.0/lib/report.js#L116, as it works with v8 coverage offsets which are different, I saw that v8-to-instanbul parses this differences according to sourcemaps but I'm not quit understand how it works. It returns Infinity for line and column in some cases.
I dug into the problem a bit more, the root of the problem is essentially that the three changelog.js files are being treated as the same file, due to the similar name, but in practice their in memory representation is three different files (it seems like their transpiled wrappers differ, and the source map at the end of the file also differs, leading to discrepancies in the byte length).
Here's an example of why this would cause trouble merging:
Note that in mock-import-1 an mock-import-2, the default method is the range 144 - 271, but in mock-import-3 it falls in the position 109 - 236, this breaks the logic that's applied to merge the v8 format output together.
To address this problem, I think you would need to:
treat these files as distinct files, rather than applying the merging logic.
run each individual file through v8-to-istanbul with its own SourceMap.
extend v8-to-istanbul with logic to merge reports post-hoc, rather than merging the v8 format output.
There's a question of whether the Istanbul format output will merge cleanly, I haven't tried this before.
I hacked together an approach that merges three coverage reports at the end, rather than merging the v8 output at the start, it kind of works:
However, it has trouble merging branches, this isn't unexpected because SourceMaps provide sparse data, so trying to combine two source maps can be like comparing apples and oranges.
A better way to fix your problem, I think, would be to figure out where the 30 byte discrepancy is happening in loader-3, vs., loader-2 and loader-1. If you can get these byte ranges to correlate, c8 will start working -- my guess is a header is being injected by 1/2 but is not being injected for 3 ... perhaps a different function wrapper?
it goes to v8toIstanbul and then get's to load where we get originalRawSource which is accessible from source map, and it is the same for all three sourcemaps:
in practice their in memory representation is three different files (it seems like their transpiled wrappers differ, and the source map at the end of the file also differs, leading to discrepancies in the byte length).
This is amazing! Would be great if you add this to c8 :)
Yes I'd be open to adding this functionality, the only issue is it does draw attention to the fact that merging multiple istanbul reports is a bit buggy.
The function and line coverage should be pretty accurate, I think, but as demonstrated by the yellow blocks in the report I shared, branch coverage is a bit off.
All I did was stop dropping the ? suffix:
diff --git a/lib/report.js b/lib/report.js
index d3c8806..41edabf 100644
--- a/lib/report.js+++ b/lib/report.js@@ -117,6 +117,7 @@ class Report {
map.merge(converter.toIstanbul())
}
} catch (err) {
+ console.info(err)
debuglog(`file: ${v8ScriptCov.url} error: ${err.stack}`)
}
}
@@ -143,7 +144,12 @@ class Report {
*/
_getSourceMap (v8ScriptCov) {
const sources = {}
- const sourceMapAndLineLengths = this.sourceMapCache[pathToFileURL(v8ScriptCov.url).href]+ let suffix = '';+ const match = v8ScriptCov.url.match(/(?<query>\?.*)$/)+ if (match) {+ suffix = match.groups.query;+ }+ const sourceMapAndLineLengths = this.sourceMapCache['file://' + v8ScriptCov.url]
if (sourceMapAndLineLengths) {
// See: https://github.com/nodejs/node/pull/34305
if (!sourceMapAndLineLengths.data) return
@@ -279,15 +285,22 @@ class Report {
}
if (/^file:\/\//.test(v8ScriptCov.url)) {
try {
- v8ScriptCov.url = fileURLToPath(v8ScriptCov.url)- fileIndex.add(v8ScriptCov.url)+ let suffix = '';+ const match = v8ScriptCov.url.match(/(?<query>\?.*)$/)+ if (match) {+ suffix = match.groups.query;+ }+ const normalized = fileURLToPath(v8ScriptCov.url)+ v8ScriptCov.url = normalized + suffix;+ fileIndex.add(normalized)
} catch (err) {
debuglog(`${err.stack}`)
continue
}
}
if ((!this.omitRelative || isAbsolute(v8ScriptCov.url))) {
- if (this.excludeAfterRemap || this.exclude.shouldInstrument(v8ScriptCov.url)) {+ const url = v8ScriptCov.url.split('?')[0]+ if (this.excludeAfterRemap || this.exclude.shouldInstrument(url)) {
result.push(v8ScriptCov)
}
}
@@ -307,7 +320,12 @@ class Report {
_normalizeSourceMapCache (v8SourceMapCache) {
const cache = {}
for (const fileURL of Object.keys(v8SourceMapCache)) {
- cache[pathToFileURL(fileURLToPath(fileURL)).href] = v8SourceMapCache[fileURL]+ let suffix = '';+ const match = fileURL.match(/(?<query>\?.*)$/)+ if (match) {+ suffix = match.groups.query;+ }+ cache[pathToFileURL(fileURLToPath(fileURL)).href + suffix] = v8SourceMapCache[fileURL]
}
return cache
}
The approach should be fleshed out more with tests, and using a helper rather than repeated code -- also it fails if no source is found right now, since v8-to-istanbul will try to load coverage.js?=foo.bar from disk (this should probably be fixed in v8-to-istanbul, with it dropping the suffix perhaps there?).
I think we're running into this issue (or something very similar) trying to run tests with TypeScript / ESM / tsx as well: it seems to incorrectly report import statements as branches:
Maintainer of several test frameworks here. Since query strings are really the only way to invalidate imports when running tests in esm, this is really not great.
Would really appreciate if someone could pick this up again! Cheers
I think we're running into this issue (or something very similar) trying to run tests with TypeScript / ESM / tsx as well: it seems to incorrectly report import statements as branches:
This is happening to me as well, but only on dynamic imports.
Activity
bcoe commentedon Sep 9, 2021
@coderaiser thanks for the bug report. I'm not super familiar with the loader API, so not sure where this bug might be cropping up -- it could be as simple as coverage reports not being output by Node.js in one edge case (which would be relatively easy to fix).
If the problem is missing lines of coverage in v8 itself, this tends to be more difficult to fix.
If you beat me to the punch, and are excited to dig into this, where I would start looking would be in the raw v8 output:
These files contain the raw output from v8 (which is written to disk by Node.js on exit):
☝️ my guess is that we'll see output that's slightly off startOffets/endOffset values for code using the loaders.
feature(mock-import) add support of sourcemaps (bcoe/c8#325)
coderaiser commentedon Dec 31, 2021
Just landed ability to generate
sourcemapaccording to fileurlreceived from loader. Sincemock-importdoes some changes,sourcemapshould help to identify correct locations of code parts.Generated file looks this way:
And it works good in the browser. The question is: does
c8supports instrumented code?I see that there is an option
--exclude-after-remapbut I don't see any changes in coverage information.Could you please help me to get source map working?
coderaiser commentedon Dec 31, 2021
Looks like istrumenting with
babel-plugin-istanbulhelped to solve the problem on a half! No it works full coverage all the time 😄 .Happy holidays 🎉 !
bcoe commentedon Dec 31, 2021
@coderaiser If I understand correctly, you're using
babel-plugin-istanbulrather thanc8, sounds like it's working for you and I can close this?coderaiser commentedon Dec 31, 2021
@bcoe Unfortunately it shows full coverage every time, so it’s useless. Could you please tell me is it possible to make
c8read source maps?Source file is changed inside of loader, and it has different name(with
?mock-import=n) to have ability to import file again with mocked import. It works very good, but sometimes (like in this example) shoes bad lines and columns positions, and shows that some code is uncovered, but it is covered.coderaiser commentedon Jan 1, 2022
Looks like it can be related to
_normalizeMapCacheand_normalizeProcessCovin/lib/report.js. Both uses fileURLToPath which is cut down?mock-import-count=1suffixes.But they are needed to
importfile again and transform it's content with mocked modules. Is it possible to cut them after we merged reports related to all files with prefix?coderaiser commentedon Jan 1, 2022
Looks like this is
1:manycase fromv8-to-instanbulfunction namedload:1source code;manysource maps;We should merge all changes that we find using different source maps (for each test when
importismocked).bcoe commentedon Jan 1, 2022
Hey @coderaiser, can you figure out a minimal failing test case? that demonstrates how truncating the query string causes this bug?
I think we could probably figure out an approach to keep the suffix longer, until the source map lookup occurs, if that's the root of the problem (I think I'm not yet fully understanding, what do the entries for sourcemaps look like In the cache, they include the query string?)
feature: report: ability to use dynamic source maps for `?count=x` im…
coderaiser commentedon Jan 2, 2022
Hey @bcoe, OK, right now I have 3 tests.
Sourcemap doesn't includes
query string, it includes path of a file on disk.When
readFilemocked:Here is version
Base64encoded:{ "version": 3, "sources": [ "file:///Users/coderaiser/c8-reproduce/lib/changelog.js" ], "names": [], "mappings": ";;iCAEO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;;AAEnB,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE;IACH,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACZ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;;AAEtB,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE;IACxB,CAAC,EAAE,CAAC,CAAC;QACD,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;;MAEnB,CAAC,EAAE,CAAC,CAAC;UACD,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;;MAErB,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAChB,CAAC", "file": "file:///Users/coderaiser/c8-reproduce/lib/changelog.js.map", "sourcesContent": [ "import {\\n readFile,\\n} from 'fs/promises';\\n\\nimport {\\n execSync,\\n} from 'child_process';\\n\\nexport default (a, b, c) => {\\n if (a)\\n return readFile();\\n \\n if (c)\\n return execSync();\\n \\n return 'd';\\n};\\n\\n" ] }When
execSyncmocked:When nothing mocked:
Here is repository and test, to get things working run:
git clone https://github.com/coderaiser/c8-reproduce cd c8-reproduce npm i npm run coverageYou can use
npm run debugfor debugging.And you will see that coverage information is not full:
Here is
PRwith partial support ofsourcemaps, it keeps fileURLSas they are, but the problem still exist in https://github.com/bcoe/c8/blob/v7.11.0/lib/report.js#L116, as it works withv8coverage offsets which are different, I saw thatv8-to-instanbulparses this differences according tosourcemapsbut I'm not quit understand how it works. It returnsInfinityforlineandcolumnin some cases.#345.
coderaiser commentedon Jan 2, 2022
Here is offsets I receive in
v8ProcessCov:{ "result": [ { "scriptId": "0", "url": "file:///Users/coderaiser/c8-reproduce/lib/changelog.js?mock-import-count=1", "functions": [ { "functionName": "", "ranges": [ { "startOffset": 0, "endOffset": 1824, "count": 1 } ], "isBlockCoverage": true }, { "functionName": "default", "ranges": [ { "startOffset": 144, "endOffset": 271, "count": 1 }, { "startOffset": 196, "endOffset": 270, "count": 0 } ], "isBlockCoverage": true } ] }, { "scriptId": "1", "url": "file:///Users/coderaiser/c8-reproduce/lib/changelog.js?mock-import-count=2", "functions": [ { "functionName": "", "ranges": [ { "startOffset": 0, "endOffset": 1820, "count": 1 } ], "isBlockCoverage": true }, { "functionName": "default", "ranges": [ { "startOffset": 144, "endOffset": 271, "count": 1 }, { "startOffset": 178, "endOffset": 196, "count": 0 }, { "startOffset": 244, "endOffset": 270, "count": 0 } ], "isBlockCoverage": true } ] }, { "scriptId": "2", "url": "file:///Users/coderaiser/c8-reproduce/lib/changelog.js?mock-import-count=3", "functions": [ { "functionName": "", "ranges": [ { "startOffset": 0, "endOffset": 1929, "count": 1 } ], "isBlockCoverage": true }, { "functionName": "default", "ranges": [ { "startOffset": 109, "endOffset": 236, "count": 1 }, { "startOffset": 143, "endOffset": 161, "count": 0 }, { "startOffset": 191, "endOffset": 209, "count": 0 } ], "isBlockCoverage": true } ] }] }And they are passed to applyCoverage from
v8-to-instanbul.bcoe commentedon Jan 2, 2022
I dug into the problem a bit more, the root of the problem is essentially that the three
changelog.jsfiles are being treated as the same file, due to the similar name, but in practice their in memory representation is three different files (it seems like their transpiled wrappers differ, and the source map at the end of the file also differs, leading to discrepancies in the byte length).Here's an example of why this would cause trouble merging:
changelog.js?mock-import-3:vs.,
changelog.js?mock-import-2:changelog.js?mock-import-1:Note that in
mock-import-1anmock-import-2, thedefaultmethod is the range144 - 271, but inmock-import-3it falls in the position109 - 236, this breaks the logic that's applied to merge the v8 format output together.To address this problem, I think you would need to:
v8-to-istanbulwith its own SourceMap.v8-to-istanbulwith logic to merge reports post-hoc, rather than merging the v8 format output.There's a question of whether the Istanbul format output will merge cleanly, I haven't tried this before.
bcoe commentedon Jan 2, 2022
I hacked together an approach that merges three coverage reports at the end, rather than merging the
v8output at the start, it kind of works:However, it has trouble merging branches, this isn't unexpected because SourceMaps provide sparse data, so trying to combine two source maps can be like comparing apples and oranges.
A better way to fix your problem, I think, would be to figure out where the 30 byte discrepancy is happening in
loader-3, vs.,loader-2andloader-1. If you can get these byte ranges to correlate, c8 will start working -- my guess is a header is being injected by 1/2 but is not being injected for3... perhaps a different function wrapper?coderaiser commentedon Jan 2, 2022
I don't quite understand, right now each files is converted:
c8/lib/report.js
Lines 93 to 102 in eaf8456
it goes to
v8toIstanbuland then get's toloadwhere we getoriginalRawSourcewhich is accessible fromsource map, and it is the same for all threesourcemaps:Then for some reason
this.pathpassed tonew CovSourceinsources.js, but it never used 🤷♂️Then
sourceTranspiledmade fromrawSource:And than saves value of
this.sourceTranspiled:That's right, this is 3 different files:
Second:
And third:
3-rd file is the same as original, but it still has sourcemap added by
recast. If I don't generatesourcemapfor third file result is the same:changelog.js | 100 | 42.85 | 100 | 100 | 11-14
This is amazing! Would be great if you add this to
c8:).bcoe commentedon Jan 3, 2022
Yes I'd be open to adding this functionality, the only issue is it does draw attention to the fact that merging multiple istanbul reports is a bit buggy.
The function and line coverage should be pretty accurate, I think, but as demonstrated by the yellow blocks in the report I shared, branch coverage is a bit off.
All I did was stop dropping the
?suffix:The approach should be fleshed out more with tests, and using a helper rather than repeated code -- also it fails if no source is found right now, since
v8-to-istanbulwill try to loadcoverage.js?=foo.barfrom disk (this should probably be fixed in v8-to-istanbul, with it dropping the suffix perhaps there?).alecgibson commentedon Jun 9, 2023
I think we're running into this issue (or something very similar) trying to run tests with TypeScript / ESM /
tsxas well: it seems to incorrectly reportimportstatements as branches:iambumblehead commentedon Nov 13, 2023
It would be outstanding if c8 would differentiate moduleIds requested with query string to provide accurate coverage results.
bcoe commentedon Nov 21, 2023
@iambumblehead agreed. Would happily accept a patch for this, or even a failing test.
iambumblehead commentedon Nov 21, 2023
@bcoe #501
simlu commentedon Dec 21, 2023
Maintainer of several test frameworks here. Since query strings are really the only way to invalidate imports when running tests in esm, this is really not great.
Would really appreciate if someone could pick this up again! Cheers
feature: report: ability to use dynamic source maps for `?count=x` im…
electrovir commentedon Jul 28, 2024
This is happening to me as well, but only on dynamic imports.