-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
fix: update coverage.include and coverage.exclude #9426
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
base: main
Are you sure you want to change the base?
fix: update coverage.include and coverage.exclude #9426
Conversation
✅ Deploy Preview for vitest-dev ready!Built without sensitive environment variables
To edit notification comments on pull requests, go to your Netlify project configuration. |
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.
Could you check the failing tests and add new tests to test/coverage-test to validate this fix? Or maybe use the test/coverage-test/test/include-exclude.test.ts.
@AriPerkkio Sure, I apologize for not doing it from the beginning... I'll work on it. 😅 |
| return false | ||
| } | ||
| for (const projectRoot of roots) { | ||
| const relativePath = slash(relative(projectRoot, 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.
Doesn't this actually change how defined globs are interpreted when using --project filter?
- When it's not used, globs are relative to
root✅ - When it's used, globs are relative to project's
root❌
├── package.json
├── vitest.config.ts
└── packages
├── one
│ ├── src
│ │ └── math.ts
│ └── test
│ └── math.test.ts
└── two
├── src
│ └── get-user.ts
└── test
└── get-user.test.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
coverage: {
include: ["./packages/**/*.ts"],
},
projects: [
{
test: {
name: "One",
root: "./packages/one",
},
},
{
test: {
name: "Two",
root: "./packages/two",
},
},
],
},
});> vitest --run --coverage
RUN v4.0.16 /x/examples/packages/vitest-example
Coverage enabled with v8
✓ Two test/get-user.test.ts (1 test) 1ms
✓ One test/math.test.ts (1 test) 1ms
Test Files 2 passed (2)
Tests 2 passed (2)
Start at 10:12:31
Duration 115ms (transform 32ms, setup 0ms, import 43ms, tests 2ms, environment 0ms)
% Coverage report from v8
--------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------|---------|----------|---------|---------|-------------------
All files | 40 | 100 | 40 | 40 |
one/src | 25 | 100 | 25 | 25 |
math.ts | 25 | 100 | 25 | 25 | 6-14
two/src | 100 | 100 | 100 | 100 |
get-user.ts | 100 | 100 | 100 | 100 |
--------------|---------|----------|---------|---------|-------------------
This should show coverage for packages/one, but it's missing:
> vitest --run --coverage --project one
RUN v4.0.16 /x/examples/packages/vitest-example
Coverage enabled with v8
✓ One test/math.test.ts (1 test) 1ms
✓ sum 0ms
Test Files 1 passed (1)
Tests 1 passed (1)
Start at 10:12:50
Duration 99ms (transform 14ms, setup 0ms, import 19ms, tests 1ms, environment 0ms)
% Coverage report from v8
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 0 | 0 | 0 | 0 |
----------|---------|----------|---------|---------|-------------------
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.
Okay, I will evaluate these cases.
packages/vitest/src/node/coverage.ts
Outdated
| const matchTarget = isExternal ? filename : relativePath | ||
|
|
||
| this.globCache.set(filename, included) | ||
| if (excludes.length && pm.isMatch(matchTarget, excludes, { dot: true })) { |
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.
excludes is always defined array.
| if (excludes.length && pm.isMatch(matchTarget, excludes, { dot: true })) { | |
| if (pm.isMatch(matchTarget, excludes, { dot: true })) { |
| continue | ||
| } | ||
|
|
||
| if (pm.isMatch(matchTarget, includeGlobs, { dot: true, contains: true })) { |
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.
Why do we need 2x pm.isMatch calls, instead of doing just one with passing ignore: options.exclude? Any examples that would fail without 2x calls?
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.
You’re right that we call pm.isMatch twice, but they serve different purposes and need distinct options:
First call (excludes, { dot: true }): early-out for any path matching coverage.exclude, regardless of how includes are written. This prevents excluded files from slipping through when an include pattern is broad.
Second call (includeGlobs, { dot: true, contains: true }): only after passing exclusion do we check if the file qualifies for coverage.include. The contains: true is important for patterns like "src/utils" that should match nested paths (e.g., src/utils/math.ts), which a strict match would miss.
Dropping either call breaks behavior: without the exclude check, excluded files (e.g., __mocks__) get counted; without the include check (with contains: true), legitimate files under broad includes are skipped.
| include: [normalizeURL(import.meta.url)], | ||
| coverage: { | ||
| reporter: 'json', | ||
| exclude: ['./utils.ts'], | ||
| exclude: ['./utils.ts', '**/test/**'], | ||
| }, | ||
| }) | ||
|
|
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.
Looks like you needed to add **/test/** here to avoid test/coverage-test/test/bundled-sources.test.ts being showed up here. I think that's actually bug.
Debugging this case further shows that isIncluded is falsely returning true for isIncluded('/x/vitest/test/coverage-test/test/bundled-sources.test.ts') even when this.options.excludes contains following:
[
'./utils.ts',
'/x/vitest/test/coverage-test/test/bundled-sources.test.ts',
'/x/vitest/test/coverage-test/fixtures/configs/vitest.config.ts',
'vitest.config.ts',
'vitest.config.mts',
'vitest.config.cts',
'vitest.config.js',
'vitest.config.mjs',
'vitest.config.cjs',
'vite.config.ts',
'vite.config.mts',
'vite.config.cts',
'vite.config.js',
'vite.config.mjs',
'vite.config.cjs',
'**/virtual:*',
'**/__x00__*',
'**/node_modules/**'
]| include: [normalizeURL(import.meta.url)], | |
| coverage: { | |
| reporter: 'json', | |
| exclude: ['./utils.ts'], | |
| exclude: ['./utils.ts', '**/test/**'], | |
| }, | |
| }) | |
| exclude: ['./utils.ts'], |
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.
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.
Don't remove include and coverage, just change the coverage.exclude back to exclude: ['./utils.ts'].
dab0aa3 to
c98cb54
Compare

Description
Fixes
coverage.include/coverage.excludeglob matching so it behaves liketest.include/test.exclude(relative to the project root), addressing Vitest v4 regressions reported in #9395.In v4, matching was done against the full (absolute) filename with
picomatchusingcontains: trueandignore, which made patterns such as./*.tsor**/foo/**unexpectedly match parts of the absolute path (including the CWD), causing everything to be excluded.This change:
./*.tswork as expected./.../foo/...), by using relative paths for in-project files and only using the absolute filename for truly external files.allowExternal: falsesemantics by skipping external files instead of incorrectly failing matches for everything.fix #9395
Please don't delete this checklist! Before submitting the PR, please make sure you do the following:
pnpm-lock.yamlunless you introduce a new test example.Tests
pnpm test:ci.Documentation
pnpm run docscommand.Changesets
feat:,fix:,perf:,docs:, orchore:.