question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Incorrect branch coverage when loaders used

See original GitHub issue

c8 shows uncovered lines, but the whole file is covered.

image

Version: output of node -v 16.9.0 Platform: output of uname -a Darwin

  • Version: latest
  • Platform: mac os

Repository https://github.com/coderaiser/c8-reproduce

When I’m using loaders to mock imports the coverage I see is broken.

Code:

import {
    readFile,
} from 'fs/promises';

import {
    execSync,
} from 'child_process';

export default (a, b, c) => {
    if (a)
        return readFile();

      if (c)
          return execSync();

      return 'd';
};

Tests:

import {createMockImport} from 'mock-import';
import {
    test,
    stub,
} from 'supertape';

const {mockImport, reImport, stopAll} = createMockImport(import.meta.url);

test('changelog: a', async (t) => {
    mockImport('fs/promises', {
        readFile: stub().returns('a'),
    });
    const fn = await reImport('./changelog.js');
    stopAll();

    t.equal(fn.default(1), 'a');
});

test('changelog: c', async (t) => {
    mockImport('child_process', {
        execSync: stub().returns('c'),
    });

    const fn = await reImport('./changelog.js');
    stopAll();

    t.equal(fn.default(0, 0, 1), 'c');
});

test('changelog: d', async (t) => {
    const fn = await import('./changelog.js?count=4');

    t.equal(fn.default(0, 0, 0, 1), 'd');
});

What mock-import does is converts source to:

const {
    readFile: readFile
} = global.__mockImportCache.get('fs/promises');

import {
    execSync,
} from 'child_process';

export default (a, b, c) => {
    if (a)
        return readFile();
     
      if (c)
          return execSync();
      
      return 'd';
};

And imports it as ./changelog.js?count=1 on first test, then on second test:

import {
    readFile
} from 'fs/promises'

const {
    execSync,
} = global.__mockImportCache.get('fs/promises');

export default (a, b, c) => {
    if (a)
        return readFile();
     
      if (c)
          return execSync();
      
      return 'd';
};

File imported as ./changelog.js?count=2, and then on third assertion code isn’t changed.

Issue Analytics

  • State:open
  • Created 2 years ago
  • Comments:14 (14 by maintainers)

github_iconTop GitHub Comments

2reactions
bcoecommented, Jan 3, 2022

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?).

1reaction
bcoecommented, Jan 2, 2022

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:

Screen Shot 2022-01-02 at 3 28 34 PM

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?

Read more comments on GitHub >

github_iconTop Results From Across the Web

Test coverage: import statements not covered - Stack Overflow
The problem is, I have uncovered import statements saying else path not taken" and "branch not covered". The uncovered imports vary across ...
Read more >
What Is Branch Coverage & What Does It Really Tell You?
What's it used for? Branch coverage is a metric that indicates whether all branches in a codebase are exercised by tests. A "branch"...
Read more >
Test coverage reporting — nose2 0.12.0 documentation
The coverage and mp plugins may be used in conjunction to enable multiprocess testing with coverage reporting. Special instructions: Due to the way...
Read more >
Problem with coverage test . IntelliJ Idea.
Looks like your TEMP directory path contains spaces, so IDEA tries to save a temporary file used by coverage to some place which...
Read more >
Configuring Jest
The bail config option can be used here to have Jest stop running tests after n ... If the file path matches any...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found