vitest icon indicating copy to clipboard operation
vitest copied to clipboard

TypeError: Cannot read properties of undefined (reading 'endCol')

Open stramel opened this issue 1 year ago • 9 comments

Describe the bug

Appears to be a regression of a previous bug: https://github.com/vitest-dev/vitest/issues/5639 Related PR for previous bug: https://github.com/vitest-dev/vitest/pull/5642

When running coverage using @vitest/coverage-v8, with CJS files, it fails to properly report coverage.

As noted in the previous issue, adding a comment above the export makes the coverage work.

npm run cover

> cover
> vitest run --coverage


 RUN  v2.0.5 /Users/mstramel/PayPal/vitest-v8-coverage-repro
      Coverage enabled with v8

 ✓ test/basic.test.js (1)
   ✓ levels (1)
     ✓ should pass

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Unhandled Error ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
TypeError: Cannot read properties of undefined (reading 'endCol')
 ❯ range.sliceRange node_modules/@vitest/coverage-v8/dist/provider.js:423:55
 ❯ CovSource.offsetToOriginalRelative node_modules/@vitest/coverage-v8/dist/provider.js:1584:20
 ❯ V8ToIstanbul._maybeRemapStartColEndCol node_modules/@vitest/coverage-v8/dist/provider.js:1987:92
 ❯ node_modules/@vitest/coverage-v8/dist/provider.js:1896:60
 ❯ node_modules/@vitest/coverage-v8/dist/provider.js:1894:20
 ❯ V8ToIstanbul.applyCoverage node_modules/@vitest/coverage-v8/dist/provider.js:1893:12
 ❯ node_modules/@vitest/coverage-v8/dist/provider.js:2609:21




 ✓ test/basic.test.js (1)
   ✓ levels (1)
     ✓ should pass
close timed out after 10000ms
Tests closed successfully but something prevents Vite server from exiting
You can try to identify the cause by enabling "hanging-process" reporter. See https://vitest.dev/config/#reporters

Reproduction

Reproduction on GitHub because StackBlitz doesn't support @vitest/coverage-v8.

Minimal Reproduction: https://github.com/stramel/vitest-v8-coverage-repro/tree/main

Steps:

  • Run npm run cover
  • Observe error

Expected Result: Coverage should finish correctly without error Actual Result: Coverage doesn't finish and results in the error TypeError: Cannot read properties of undefined (reading 'endCol')

If you change src/basic.js to this

"use strict";

// FIX ISSUE

module.exports = {
  debug: 0,
  info: 1,
  warn: 2,
  error: 3,
  fatal: 4,
};

then run npm run cover it will work as expected.

System Info

`npx envinfo --system --npmPackages '{vitest,@vitest/*,vite,@vitejs/*}' --binaries --browsers`


  System:
    OS: macOS 14.5
    CPU: (12) arm64 Apple M2 Max
    Memory: 151.23 MB / 64.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 18.20.4 - ~/.volta/tools/image/node/18.20.4/bin/node
    Yarn: 1.22.19 - ~/.volta/bin/yarn
    npm: 10.8.2 - ~/.volta/tools/image/npm/10.8.2/bin/npm
  Browsers:
    Chrome: 127.0.6533.73
    Chrome Canary: 123.0.6270.0
    Edge: 127.0.2651.86
    Safari: 17.5
  npmPackages:
    @vitest/coverage-v8: latest => 2.0.5
    @vitest/ui: latest => 2.0.5
    vite: latest => 5.3.5
    vitest: latest => 2.0.5

Used Package Manager

npm

Validations

stramel avatar Aug 07 '24 16:08 stramel

What's this line? It's importing something that doesn't exist. Removing it makes the coverage work.

https://github.com/stramel/vitest-v8-coverage-repro/blob/0dea8002ec3583e2f244c8d5d10c9896b4ad1bff/test/basic.test.js#L2

AriPerkkio avatar Aug 07 '24 18:08 AriPerkkio

What's this line? It's importing something that doesn't exist. Removing it makes the coverage work.

stramel/vitest-v8-coverage-repro@0dea800/test/basic.test.js#L2

@AriPerkkio It's importing the package itself. It's equivalent to importing ../src/index.js

stramel avatar Aug 08 '24 04:08 stramel

Appears to be a regression of a previous bug: https://github.com/vitest-dev/vitest/issues/5639

I don't think this is regression. Has this ever worked?

  1. Calling import levels from "../src/basic"; ends up loading basic through Vite, and it's executed by vite-node.
{
  "scriptId": "468",
  "url": "file:///Users/x/vitest/test/coverage-test/fixtures/src/cjs-package/basic.js",
  "functions": [
    {
      "functionName": "",
      "ranges": [
        {
          "startOffset": 0,
          "endOffset": 1031, // <-- Notice that this doesn't match file's actual content -> It's executed by vite-node that adds wrapper code around it.
          "count": 1
        }
      ],
      "isBlockCoverage": true
    }
  ]
}
  1. Calling import test from ".."; loads "src/index.js" from package.json's "main" which isn't intercepted by Vite. The index.js will then require('basic') which is again not intercepted by Vite.
{
  "scriptId": "469",
  "url": "file:///Users/x/vitest/test/coverage-test/fixtures/src/cjs-package/basic.js",
  "functions": [
    {
      "functionName": "",
      "ranges": [
        {
          "startOffset": 0,
          "endOffset": 95, // <-- Notice, no vite-node's wrapper used. Only the file itself was executed
          "count": 1
        }
      ],
      "isBlockCoverage": true
    }
  ]
}

I'm not sure if this can be fixed properly at all. Maybe we should finally just make the decision to completely ignore files that were not loaded via Vite.

You can fix this in your codebase by not relying on CommonJS when you are using Vite/Vitest.

AriPerkkio avatar Aug 08 '24 06:08 AriPerkkio

Also note that @vitest/coverage-istanbul might work out-of-the-box here. It does not include CommonJS files as it does the instrumentation using Vite plugins.

V8 coverage includes all files that Node executed so we don't really control that part.

AriPerkkio avatar Aug 08 '24 06:08 AriPerkkio

I think we could add a try-catch around here with warning logging when this kind of issues happen. It would then not fail whole report generation.

https://github.com/vitest-dev/vitest/blob/5d542035acb198bb7f74055e8aff26abf28dc2fb/packages/coverage-v8/src/provider.ts#L546

AriPerkkio avatar Aug 09 '24 14:08 AriPerkkio

I don't think this is regression. Has this ever worked?

I was basing this on your comment in the PR for the issue where you said:

This is an edge case that can happen if a file was loaded outside Vitest, e.g. using require()

Perhaps its semi-related but not quite the same issue / not a regression. I still find it a bit strange that adding the comment fixes the issue of the import.

On this note:

I'm not sure if this can be fixed properly at all. Maybe we should finally just make the decision to completely ignore files that were not loaded via Vite.

You can fix this in your codebase by not relying on CommonJS when you are using Vite/Vitest.

We're working on migrating to ESM and away from CJS currently. We just finished migrating to Vitest from a slew of other test runners (mocha, tap, tape, jest, etc) and our next step is starting to convert to ESM. We were hoping that we would be able to get all our coverage working through the v8 provider before moving to that step though.

I think we could add a try-catch around here with warning logging when this kind of issues happen. It would then not fail whole report generation.

I do think this would be a good idea as it would be helpful for people in similar cases.

stramel avatar Aug 11 '24 03:08 stramel

I still find it a bit strange that adding the comment fixes the issue of the import.

It may seem that it works, but it's still causing incorrect coverage results.

When import levels from "../src/basic"; is ran, we execute the basic.js in node:vm with following code:

'use strict';async (__vite_ssr_import__,__vite_ssr_dynamic_import__,__vite_ssr_exports__,__vite_ssr_exportAll__,__vite_ssr_import_meta__,require,exports,module,__filename,__dirname)=>{{"use strict";

module.exports = {
  debug: 0,
  info: 1,
  warn: 2,
  error: 3,
  fatal: 4,
};

//# sourceMappingSource=vite-node
//# sourceMappingURL=data:application/json;charset=utf-8;base64,...

}}

And when require('basic') is ran, NodeJS runs this code as:

"use strict";

module.exports = {
  debug: 0,
  info: 1,
  warn: 2,
  error: 3,
  fatal: 4,
};

Now when we get coverage results for this, they look like shown in https://github.com/vitest-dev/vitest/issues/6297#issuecomment-2275053107. In order to properly remap the coverage results back to source code, we need to apply offset in the beginning to get rid of the 'use strict';async (__vite_ssr_import__ ... wrapper code that vite-node adds. However now we are also applying it to the second case that wasn't run with this wrapper.

When the wrapper code is incorrectly assumed to be in the V8 coverage result, we simply read out-of-bounds of the original source file. When you add new comment (or any other code) there, it luckily adds enough characters so that this out-of-bounds read doesn't happen. But it's still incorrectly mapped so results are off.

I did some testing whether we could actually detect from the V8 results whether vite-node wrapper was there: https://github.com/vitest-dev/vitest/commit/9f571eec8424e2d048cafdd47bcc71d3a150161a . It could work fine but I'm not sure if we want to apply this for every single user. But even if we end up adding this logic later, I still think we should proceed with https://github.com/vitest-dev/vitest/pull/6318 as that error might happen in other cases as well.

AriPerkkio avatar Aug 11 '24 07:08 AriPerkkio

Ah, that makes a lot more sense! Thank you for the explanation! I agree that proceeding with the warning and not blocking coverage would be the best route

stramel avatar Aug 11 '24 18:08 stramel

Just confirmed that the fix worked by testing 2.1.0-beta.5. Thank you for the help!

stramel avatar Aug 13 '24 05:08 stramel

There's proper fix for this in https://github.com/vitest-dev/vitest/pull/7417. There we'll be able to actually identify whether file was executed by vite-node.

AriPerkkio avatar Feb 06 '25 15:02 AriPerkkio