[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): correct coverage when `isolate: false` is used (#6957)

authored by

Ari Perkkiö and committed by
GitHub
(Dec 9, 2024, 1:41 PM +0200) 426ce6d8 fa754094

+193 -51
+2 -2
packages/browser/src/client/tester/tester.ts
··· 122 122 try { 123 123 await Promise.all([ 124 124 setupCommonEnv(config), 125 - startCoverageInsideWorker(config.coverage, executor), 125 + startCoverageInsideWorker(config.coverage, executor, { isolate: config.browser.isolate }), 126 126 (async () => { 127 127 const VitestIndex = await import('vitest') 128 128 Object.defineProperty(window, '__vitest_index__', { ··· 160 160 }, 'Cleanup Error') 161 161 } 162 162 state.environmentTeardownRun = true 163 - await stopCoverageInsideWorker(config.coverage, executor).catch((error) => { 163 + await stopCoverageInsideWorker(config.coverage, executor, { isolate: config.browser.isolate }).catch((error) => { 164 164 client.rpc.onUnhandledError({ 165 165 name: error.name, 166 166 message: error.message,
+41 -25
packages/coverage-istanbul/src/index.ts
··· 1 + import type { CoverageMapData } from 'istanbul-lib-coverage' 2 + import type { CoverageProviderModule } from 'vitest/node' 1 3 import type { IstanbulCoverageProvider } from './provider' 2 4 import { COVERAGE_STORE_KEY } from './constants' 3 5 4 - export async function getProvider(): Promise<IstanbulCoverageProvider> { 5 - // to not bundle the provider 6 - const providerPath = './provider.js' 7 - const { IstanbulCoverageProvider } = (await import( 8 - /* @vite-ignore */ 9 - providerPath 10 - )) as typeof import('./provider') 11 - return new IstanbulCoverageProvider() 12 - } 13 - 14 - export function takeCoverage(): any { 15 - // @ts-expect-error -- untyped global 16 - const coverage = globalThis[COVERAGE_STORE_KEY] 6 + export default { 7 + takeCoverage() { 8 + // @ts-expect-error -- untyped global 9 + return globalThis[COVERAGE_STORE_KEY] 10 + }, 17 11 18 12 // Reset coverage map to prevent duplicate results if this is called twice in row 19 - // @ts-expect-error -- untyped global 20 - globalThis[COVERAGE_STORE_KEY] = {} 13 + startCoverage() { 14 + // @ts-expect-error -- untyped global 15 + const coverageMap = globalThis[COVERAGE_STORE_KEY] as CoverageMapData 21 16 22 - return coverage 23 - } 17 + // When isolated, there are no previous results 18 + if (!coverageMap) { 19 + return 20 + } 24 21 25 - const _default: { 26 - getProvider: () => Promise<IstanbulCoverageProvider> 27 - takeCoverage: () => any 28 - } = { 29 - getProvider, 30 - takeCoverage, 31 - } 22 + for (const filename in coverageMap) { 23 + const branches = coverageMap[filename].b 24 + 25 + for (const key in branches) { 26 + branches[key] = branches[key].map(() => 0) 27 + } 28 + 29 + for (const metric of ['f', 's'] as const) { 30 + const entry = coverageMap[filename][metric] 31 + 32 + for (const key in entry) { 33 + entry[key] = 0 34 + } 35 + } 36 + } 37 + }, 38 + 39 + async getProvider(): Promise<IstanbulCoverageProvider> { 40 + // to not bundle the provider 41 + const providerPath = './provider.js' 42 + const { IstanbulCoverageProvider } = (await import( 43 + /* @vite-ignore */ 44 + providerPath 45 + )) as typeof import('./provider') 32 46 33 - export default _default 47 + return new IstanbulCoverageProvider() 48 + }, 49 + } satisfies CoverageProviderModule
+11 -4
packages/coverage-v8/src/browser.ts
··· 1 + import type { CoverageProviderModule } from 'vitest/node' 1 2 import type { V8CoverageProvider } from './provider' 2 3 import { cdp } from '@vitest/browser/context' 3 4 import { loadProvider } from './load-provider' 4 5 5 6 const session = cdp() 7 + let enabled = false 6 8 7 9 type ScriptCoverage = Awaited<ReturnType<typeof session.send<'Profiler.takePreciseCoverage'>>> 8 10 9 11 export default { 10 12 async startCoverage() { 13 + if (enabled) { 14 + return 15 + } 16 + 17 + enabled = true 18 + 11 19 await session.send('Profiler.enable') 12 20 await session.send('Profiler.startPreciseCoverage', { 13 21 callCount: true, ··· 32 40 return { result } 33 41 }, 34 42 35 - async stopCoverage() { 36 - await session.send('Profiler.stopPreciseCoverage') 37 - await session.send('Profiler.disable') 43 + stopCoverage() { 44 + // Browser mode should not stop coverage as same V8 instance is shared between tests 38 45 }, 39 46 40 47 async getProvider(): Promise<V8CoverageProvider> { 41 48 return loadProvider() 42 49 }, 43 - } 50 + } satisfies CoverageProviderModule 44 51 45 52 function filterResult(coverage: ScriptCoverage['result'][number]): boolean { 46 53 if (!coverage.url.startsWith(window.location.origin)) {
+15 -3
packages/coverage-v8/src/index.ts
··· 1 + import type { CoverageProviderModule } from 'vitest/node' 1 2 import type { V8CoverageProvider } from './provider' 2 3 import inspector, { type Profiler } from 'node:inspector' 3 4 import { provider } from 'std-env' 4 5 import { loadProvider } from './load-provider' 5 6 6 7 const session = new inspector.Session() 8 + let enabled = false 7 9 8 10 export default { 9 - startCoverage(): void { 11 + startCoverage({ isolate }) { 12 + if (isolate === false && enabled) { 13 + return 14 + } 15 + 16 + enabled = true 17 + 10 18 session.connect() 11 19 session.post('Profiler.enable') 12 20 session.post('Profiler.startPreciseCoverage', { ··· 34 42 }) 35 43 }, 36 44 37 - stopCoverage(): void { 45 + stopCoverage({ isolate }) { 46 + if (isolate === false) { 47 + return 48 + } 49 + 38 50 session.post('Profiler.stopPreciseCoverage') 39 51 session.post('Profiler.disable') 40 52 session.disconnect() ··· 43 55 async getProvider(): Promise<V8CoverageProvider> { 44 56 return loadProvider() 45 57 }, 46 - } 58 + } satisfies CoverageProviderModule 47 59 48 60 function filterResult(coverage: Profiler.ScriptCoverage): boolean { 49 61 if (!coverage.url.startsWith('file://')) {
+4 -2
packages/vitest/src/integrations/coverage.ts
··· 79 79 export async function startCoverageInsideWorker( 80 80 options: SerializedCoverageConfig | undefined, 81 81 loader: Loader, 82 + runtimeOptions: { isolate: boolean }, 82 83 ) { 83 84 const coverageModule = await resolveCoverageProviderModule(options, loader) 84 85 85 86 if (coverageModule) { 86 - return coverageModule.startCoverage?.() 87 + return coverageModule.startCoverage?.(runtimeOptions) 87 88 } 88 89 89 90 return null ··· 105 106 export async function stopCoverageInsideWorker( 106 107 options: SerializedCoverageConfig | undefined, 107 108 loader: Loader, 109 + runtimeOptions: { isolate: boolean }, 108 110 ) { 109 111 const coverageModule = await resolveCoverageProviderModule(options, loader) 110 112 111 113 if (coverageModule) { 112 - return coverageModule.stopCoverage?.() 114 + return coverageModule.stopCoverage?.(runtimeOptions) 113 115 } 114 116 115 117 return null
+2 -2
packages/vitest/src/node/types/coverage.ts
··· 66 66 /** 67 67 * Executed before tests are run in the worker thread. 68 68 */ 69 - startCoverage?: () => unknown | Promise<unknown> 69 + startCoverage?: (runtimeOptions: { isolate: boolean }) => unknown | Promise<unknown> 70 70 71 71 /** 72 72 * Executed on after each run in the worker thread. Possible to return a payload passed to the provider ··· 76 76 /** 77 77 * Executed after all tests have been run in the worker thread. 78 78 */ 79 - stopCoverage?: () => unknown | Promise<unknown> 79 + stopCoverage?: (runtimeOptions: { isolate: boolean }) => unknown | Promise<unknown> 80 80 } 81 81 82 82 export type CoverageReporter = keyof ReportOptions | (string & {})
+7 -10
packages/vitest/src/runtime/runBaseTests.ts
··· 25 25 ): Promise<void> { 26 26 const workerState = getWorkerState() 27 27 28 + const isIsolatedThreads = config.pool === 'threads' && (config.poolOptions?.threads?.isolate ?? true) 29 + const isIsolatedForks = config.pool === 'forks' && (config.poolOptions?.forks?.isolate ?? true) 30 + const isolate = isIsolatedThreads || isIsolatedForks 31 + 28 32 await setupGlobalEnv(config, environment, executor) 29 - await startCoverageInsideWorker(config.coverage, executor) 33 + await startCoverageInsideWorker(config.coverage, executor, { isolate }) 30 34 31 35 if (config.chaiConfig) { 32 36 setupChaiConfig(config.chaiConfig) ··· 50 54 = performance.now() - workerState.durations.environment 51 55 52 56 for (const file of files) { 53 - const isIsolatedThreads 54 - = config.pool === 'threads' 55 - && (config.poolOptions?.threads?.isolate ?? true) 56 - const isIsolatedForks 57 - = config.pool === 'forks' 58 - && (config.poolOptions?.forks?.isolate ?? true) 59 - 60 - if (isIsolatedThreads || isIsolatedForks) { 57 + if (isolate) { 61 58 executor.mocker.reset() 62 59 resetModules(workerState.moduleCache, true) 63 60 } ··· 77 74 vi.restoreAllMocks() 78 75 } 79 76 80 - await stopCoverageInsideWorker(config.coverage, executor) 77 + await stopCoverageInsideWorker(config.coverage, executor, { isolate }) 81 78 }, 82 79 ) 83 80
+2 -2
packages/vitest/src/runtime/runVmTests.ts
··· 62 62 getSourceMap: source => workerState.moduleCache.getSourceMap(source), 63 63 }) 64 64 65 - await startCoverageInsideWorker(config.coverage, executor) 65 + await startCoverageInsideWorker(config.coverage, executor, { isolate: false }) 66 66 67 67 if (config.chaiConfig) { 68 68 setupChaiConfig(config.chaiConfig) ··· 101 101 vi.restoreAllMocks() 102 102 } 103 103 104 - await stopCoverageInsideWorker(config.coverage, executor) 104 + await stopCoverageInsideWorker(config.coverage, executor, { isolate: false }) 105 105 } 106 106 107 107 function resolveCss(mod: NodeJS.Module) {
+6
test/coverage-test/fixtures/setup.isolation.ts
··· 1 + import { beforeAll } from "vitest"; 2 + import { branch } from "./src/branch"; 3 + 4 + beforeAll(() => { 5 + branch(1); 6 + });
+7
test/coverage-test/fixtures/src/branch.ts
··· 1 + export const branch = async (a: number) => { 2 + if (a === 15) { 3 + return true; 4 + } 5 + 6 + return false; 7 + };
+9
test/coverage-test/fixtures/test/isolation-1-fixture.test.ts
··· 1 + import { test } from "vitest"; 2 + import { multiply, remainder, subtract, sum } from "../src/math"; 3 + 4 + test("Should run function sucessfully", async () => { 5 + sum(1, 1); 6 + subtract(1,2) 7 + multiply(3,4) 8 + remainder(6,7) 9 + });
+10
test/coverage-test/fixtures/test/isolation-2-fixture.test.ts
··· 1 + import { test } from "vitest"; 2 + import { branch } from "../src/branch"; 3 + 4 + test("cover some lines", async () => { 5 + branch(15); 6 + }); 7 + 8 + test("cover lines", async () => { 9 + branch(2); 10 + });
+70
test/coverage-test/test/isolation.test.ts
··· 1 + import type { WorkspaceSpec } from 'vitest/node' 2 + import { expect, test } from 'vitest' 3 + import { readCoverageMap, runVitest } from '../utils' 4 + 5 + const pools = ['forks'] 6 + 7 + if (!process.env.COVERAGE_BROWSER) { 8 + pools.push('threads') 9 + 10 + const [major] = process.version.slice(1).split('.').map(num => Number(num)) 11 + 12 + if (major < 22) { 13 + pools.push('vmForks', 'vmThreads') 14 + } 15 + } 16 + 17 + for (const isolate of [true, false]) { 18 + for (const pool of pools) { 19 + test(`{ isolate: ${isolate}, pool: "${pool}" }`, async () => { 20 + await runVitest({ 21 + include: ['fixtures/test/isolation-*'], 22 + setupFiles: ['fixtures/setup.isolation.ts'], 23 + sequence: { sequencer: Sorter }, 24 + 25 + pool, 26 + isolate, 27 + fileParallelism: false, 28 + 29 + coverage: { 30 + all: false, 31 + reporter: ['json', 'html'], 32 + }, 33 + 34 + // @ts-expect-error -- merged in runVitest 35 + browser: { 36 + isolate, 37 + }, 38 + }) 39 + 40 + const coverageMap = await readCoverageMap() 41 + 42 + const branches = coverageMap.fileCoverageFor('<process-cwd>/fixtures/src/branch.ts') 43 + expect(branches.toSummary().lines.pct).toBe(100) 44 + expect(branches.toSummary().statements.pct).toBe(100) 45 + expect(branches.toSummary().functions.pct).toBe(100) 46 + expect(branches.toSummary().branches.pct).toBe(100) 47 + 48 + const math = coverageMap.fileCoverageFor('<process-cwd>/fixtures/src/math.ts') 49 + expect(math.toSummary().lines.pct).toBe(100) 50 + expect(math.toSummary().statements.pct).toBe(100) 51 + expect(math.toSummary().functions.pct).toBe(100) 52 + expect(math.toSummary().branches.pct).toBe(100) 53 + }) 54 + } 55 + } 56 + 57 + class Sorter { 58 + sort(files: WorkspaceSpec[]) { 59 + return files.sort((a) => { 60 + if (a.moduleId.includes('isolation-1')) { 61 + return -1 62 + } 63 + return 1 64 + }) 65 + } 66 + 67 + shard(files: WorkspaceSpec[]) { 68 + return files 69 + } 70 + }
+1
test/coverage-test/utils.ts
··· 51 51 headless: true, 52 52 name: 'chromium', 53 53 provider: 'playwright', 54 + ...config.browser, 54 55 }, 55 56 }) 56 57
+4 -1
test/coverage-test/vitest.config.ts
··· 1 1 import { defineConfig } from 'vitest/config' 2 2 3 3 export default defineConfig({ 4 + server: { 5 + watch: null, 6 + }, 4 7 test: { 5 - reporters: 'basic', 8 + reporters: 'verbose', 6 9 isolate: false, 7 10 poolOptions: { 8 11 threads: {
+2
test/coverage-test/vitest.workspace.custom.ts
··· 66 66 BROWSER_TESTS, 67 67 68 68 // Other non-provider-specific tests that should be run on browser mode as well 69 + '**/isolation.test.ts', 69 70 '**/include-exclude.test.ts', 70 71 '**/allow-external.test.ts', 71 72 '**/ignore-hints.test.ts', ··· 90 91 BROWSER_TESTS, 91 92 92 93 // Other non-provider-specific tests that should be run on browser mode as well 94 + '**/isolation.test.ts', 93 95 '**/include-exclude.test.ts', 94 96 '**/allow-external.test.ts', 95 97 '**/ignore-hints.test.ts',