[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

fix(coverage): include files based on `--project` filter (#7885)

authored by

Taylor Buchanan and committed by
GitHub
(Jun 26, 2025, 9:31 AM +0300) 761beeee 6d64a3f6

+166 -19
+49
test/coverage-test/test/workspace.project-filter.test.ts
··· 1 + import { expect } from 'vitest' 2 + import { readCoverageMap, runVitest, test } from '../utils' 3 + 4 + test('coverage files include all projects', async () => { 5 + await runVitest({ 6 + config: '../../configs/vitest.config.workspace.ts', 7 + coverage: { 8 + reporter: ['json', 'html'], 9 + include: ['**/src/**'], 10 + }, 11 + root: 'fixtures/workspaces/project', 12 + }) 13 + 14 + const coverageMap = await readCoverageMap('fixtures/workspaces/project/coverage/coverage-final.json') 15 + const files = coverageMap.files() 16 + 17 + // All files from workspace should be picked 18 + expect(files).toMatchInlineSnapshot(` 19 + [ 20 + "<process-cwd>/fixtures/workspaces/project/project1/src/id.ts", 21 + "<process-cwd>/fixtures/workspaces/project/project1/src/untested.ts", 22 + "<process-cwd>/fixtures/workspaces/project/project2/src/konst.ts", 23 + "<process-cwd>/fixtures/workspaces/project/project2/src/untested.ts", 24 + "<process-cwd>/fixtures/workspaces/project/shared/src/utils.ts", 25 + ] 26 + `) 27 + }) 28 + 29 + test('coverage files limited to specified project', async () => { 30 + await runVitest({ 31 + config: '../../configs/vitest.config.workspace.ts', 32 + coverage: { 33 + reporter: ['json', 'html'], 34 + include: ['**/src/**'], 35 + }, 36 + project: 'project2', 37 + root: 'fixtures/workspaces/project', 38 + }) 39 + 40 + const coverageMap = await readCoverageMap('fixtures/workspaces/project/coverage/coverage-final.json') 41 + const files = coverageMap.files() 42 + 43 + expect(files).toMatchInlineSnapshot(` 44 + [ 45 + "<process-cwd>/fixtures/workspaces/project/project2/src/konst.ts", 46 + "<process-cwd>/fixtures/workspaces/project/project2/src/untested.ts", 47 + ] 48 + `) 49 + })
+43 -19
packages/vitest/src/node/coverage.ts
··· 81 81 coverageFiles: CoverageFiles = new Map() 82 82 pendingPromises: Promise<void>[] = [] 83 83 coverageFilesDirectory!: string 84 + roots: string[] = [] 84 85 85 86 _initialize(ctx: Vitest): void { 86 87 this.ctx = ctx ··· 130 131 this.options.reportsDirectory, 131 132 tempDirectory, 132 133 ) 134 + 135 + // If --project filter is set pick only roots of resolved projects 136 + this.roots = ctx.config.project?.length 137 + ? [...new Set(ctx.projects.map(project => project.config.root))] 138 + : [ctx.config.root] 133 139 } 134 140 135 141 /** 136 142 * Check if file matches `coverage.include` but not `coverage.exclude` 137 143 */ 138 - isIncluded(_filename: string): boolean { 144 + isIncluded(_filename: string, root?: string): boolean { 145 + const roots = root ? [root] : this.roots 146 + 139 147 const filename = slash(_filename) 140 148 const cacheHit = this.globCache.get(filename) 141 149 ··· 144 152 } 145 153 146 154 // File outside project root with default allowExternal 147 - if (this.options.allowExternal === false && !filename.startsWith(this.ctx.config.root)) { 155 + if (this.options.allowExternal === false && roots.every(root => !filename.startsWith(root))) { 148 156 this.globCache.set(filename, false) 149 157 150 158 return false 151 159 } 152 160 153 - const options: pm.PicomatchOptions = { 154 - contains: true, 155 - dot: true, 156 - cwd: this.ctx.config.root, 157 - ignore: this.options.exclude, 158 - } 159 - 160 161 // By default `coverage.include` matches all files, except "coverage.exclude" 161 162 const glob = this.options.include || '**' 162 163 163 - const included = pm.isMatch(filename, glob, options) && existsSync(cleanUrl(filename)) 164 + let included = roots.some((root) => { 165 + const options: pm.PicomatchOptions = { 166 + contains: true, 167 + dot: true, 168 + cwd: root, 169 + ignore: this.options.exclude, 170 + } 171 + 172 + return pm.isMatch(filename, glob, options) 173 + }) 174 + 175 + included &&= existsSync(cleanUrl(filename)) 164 176 165 177 this.globCache.set(filename, included) 166 178 167 179 return included 168 180 } 169 181 170 - async getUntestedFiles(testedFiles: string[]): Promise<string[]> { 171 - if (this.options.include == null) { 172 - return [] 173 - } 174 - 175 - let includedFiles = await glob(this.options.include, { 176 - cwd: this.ctx.config.root, 182 + private async getUntestedFilesByRoot( 183 + testedFiles: string[], 184 + include: string[], 185 + root: string, 186 + ): Promise<string[]> { 187 + let includedFiles = await glob(include, { 188 + cwd: root, 177 189 ignore: [...this.options.exclude, ...testedFiles.map(file => slash(file))], 178 190 absolute: true, 179 191 dot: true, ··· 181 193 }) 182 194 183 195 // Run again through picomatch as tinyglobby's exclude pattern is different ({ "exclude": ["math"] } should ignore "src/math.ts") 184 - includedFiles = includedFiles.filter(file => this.isIncluded(file)) 196 + includedFiles = includedFiles.filter(file => this.isIncluded(file, root)) 185 197 186 198 if (this.ctx.config.changed) { 187 199 includedFiles = (this.ctx.config.related || []).filter(file => includedFiles.includes(file)) 188 200 } 189 201 190 - return includedFiles.map(file => slash(path.resolve(this.ctx.config.root, file))) 202 + return includedFiles.map(file => slash(path.resolve(root, file))) 203 + } 204 + 205 + async getUntestedFiles(testedFiles: string[]): Promise<string[]> { 206 + if (this.options.include == null) { 207 + return [] 208 + } 209 + 210 + const rootMapper = this.getUntestedFilesByRoot.bind(this, testedFiles, this.options.include) 211 + 212 + const matrix = await Promise.all(this.roots.map(rootMapper)) 213 + 214 + return matrix.flatMap(files => files) 191 215 } 192 216 193 217 createCoverageMap(): CoverageMap {
+26
test/coverage-test/fixtures/configs/vitest.config.workspace.ts
··· 1 + import { defineConfig } from "vitest/config"; 2 + 3 + export default defineConfig({ 4 + test: { 5 + projects: [ 6 + { 7 + test: { 8 + name: "project1", 9 + root: "fixtures/workspaces/project/project1", 10 + }, 11 + }, 12 + { 13 + test: { 14 + name: "project2", 15 + root: "fixtures/workspaces/project/project2", 16 + }, 17 + }, 18 + { 19 + test: { 20 + name: 'project-shared', 21 + root: 'fixtures/workspaces/project/shared', 22 + } 23 + } 24 + ] 25 + } 26 + });
+4
test/coverage-test/fixtures/workspaces/project/project1/src/id.ts
··· 1 + import { raise } from "../../shared/src/utils" 2 + 3 + export const id = <T>(value: T) => 4 + value ?? raise("Value cannot be undefined")
+3
test/coverage-test/fixtures/workspaces/project/project1/src/untested.ts
··· 1 + export function untested() { 2 + 3 + }
+6
test/coverage-test/fixtures/workspaces/project/project1/test/id.test.ts
··· 1 + import { expect, test } from 'vitest'; 2 + import { id } from '../src/id'; 3 + 4 + test('returns identity value', () => { 5 + expect(id(1)).toBe(1); 6 + })
+6
test/coverage-test/fixtures/workspaces/project/project2/src/konst.ts
··· 1 + import { raise } from "../../shared/src/utils" 2 + 3 + export const konst = <T>(value: T) => { 4 + value ??= raise("Value cannot be undefined") 5 + return () => value 6 + }
+3
test/coverage-test/fixtures/workspaces/project/project2/src/untested.ts
··· 1 + export function untested() { 2 + 3 + }
+8
test/coverage-test/fixtures/workspaces/project/project2/test/konst.test.ts
··· 1 + import { expect, test } from 'vitest' 2 + import { konst } from '../src/konst' 3 + 4 + test('returns function that returns constant value', () => { 5 + const fn = konst(1) 6 + 7 + expect(fn()).toBe(1); 8 + })
+11
test/coverage-test/fixtures/workspaces/project/shared/src/utils.ts
··· 1 + function raise(error: string): never 2 + function raise(error: Error): never 3 + function raise(error: Error | string): never { 4 + if (typeof error === 'string') { 5 + throw new Error(error) 6 + } else { 7 + throw error 8 + } 9 + } 10 + 11 + export { raise }
+7
test/coverage-test/fixtures/workspaces/project/shared/test/utils.test.ts
··· 1 + import { expect, test } from 'vitest' 2 + import { raise } from '../src/utils' 3 + 4 + test('raise throws error', () => { 5 + const message = 'Value cannot be undefined' 6 + expect(() => raise(message)).toThrowError(message) 7 + })