[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: create environment once per worker with `isolate: false` (#8915)

authored by

Vladimir and committed by
GitHub
(Nov 3, 2025, 5:20 PM +0100) c9078a26 9d2b4d50

+268 -215
-1
packages/ui/client/shim.d.ts
··· 9 9 10 10 declare interface Error { 11 11 VITEST_TEST_NAME?: string 12 - VITEST_AFTER_ENV_TEARDOWN?: boolean 13 12 VITEST_TEST_PATH?: string 14 13 }
+30
test/cli/test/shared-env.test.ts
··· 1 + import { expect, test } from 'vitest' 2 + import { runInlineTests, StableTestFileOrderSorter } from '../../test-utils' 3 + 4 + test.each([ 5 + 1, 6 + 2, 7 + ])('the environment is shared between tests with maxWorkers: %s', async (maxWorkers) => { 8 + const testCode = ` 9 + test('document is the same', () => { 10 + expect(__vitest_worker__.ctx.config.isolate).toBe(false) 11 + expect(__vitest_worker__.ctx.config.maxWorkers).toBe(${maxWorkers}) 12 + expect(globalThis.__document ??= document).toBe(document) 13 + }) 14 + ` 15 + const { stderr } = await runInlineTests({ 16 + '1.test.js': testCode, 17 + '2.test.js': testCode, 18 + '3.test.js': testCode, 19 + '4.test.js': testCode, 20 + 'vitest.config.js': { 21 + test: { 22 + environment: 'happy-dom', 23 + globals: true, 24 + isolate: false, 25 + }, 26 + }, 27 + }, { sequence: { sequencer: StableTestFileOrderSorter }, maxWorkers }) 28 + 29 + expect(stderr).toBe('') 30 + })
+8 -5
packages/browser/src/client/client.ts
··· 20 20 location.protocol === 'https:' ? 'wss:' : 'ws:' 21 21 }//${HOST}/__vitest_browser_api__?type=${PAGE_TYPE}&rpcId=${RPC_ID}&sessionId=${getBrowserState().sessionId}&projectName=${getBrowserState().config.name || ''}&method=${METHOD}&token=${(window as any).VITEST_API_TOKEN || '0'}` 22 22 23 - let setCancel = (_: CancelReason) => {} 24 - export const onCancel: Promise<CancelReason> = new Promise((resolve) => { 25 - setCancel = resolve 26 - }) 23 + const onCancelCallbacks: ((reason: CancelReason) => void)[] = [] 24 + 25 + export function onCancel(callback: (reason: CancelReason) => void): void { 26 + onCancelCallbacks.push(callback) 27 + } 27 28 28 29 export interface VitestBrowserClient { 29 30 rpc: BrowserRPC ··· 74 75 75 76 ctx.rpc = createBirpc<WebSocketBrowserHandlers, WebSocketBrowserEvents>( 76 77 { 77 - onCancel: setCancel, 78 + async onCancel(reason) { 79 + await Promise.all(onCancelCallbacks.map(fn => fn(reason))) 80 + }, 78 81 async createTesters(options) { 79 82 const orchestrator = await waitForOrchestrator() 80 83 return orchestrator.createTesters(options)
+1 -4
packages/vitest/src/node/pool.ts
··· 147 147 148 148 taskGroup.push({ 149 149 context: { 150 - pool, 151 - config: project.serializedConfig, 152 150 files: specs.map(spec => ({ filepath: spec.moduleId, testLocations: spec.testLines })), 153 151 invalidates, 154 - environment, 155 - projectName: project.name, 156 152 providedContext: project.getProvidedContext(), 157 153 workerId: workerId++, 158 154 }, 155 + environment, 159 156 project, 160 157 env, 161 158 execArgv,
-11
packages/vitest/src/node/printError.ts
··· 219 219 220 220 const testPath = (e as any).VITEST_TEST_PATH 221 221 const testName = (e as any).VITEST_TEST_NAME 222 - const afterEnvTeardown = (e as any).VITEST_AFTER_ENV_TEARDOWN 223 222 // testName has testPath inside 224 223 if (testPath) { 225 224 logger.error( ··· 238 237 )}". It might mean one of the following:` 239 238 + '\n- The error was thrown, while Vitest was running this test.' 240 239 + '\n- If the error occurred after the test had been completed, this was the last documented test before it was thrown.', 241 - ), 242 - ) 243 - } 244 - if (afterEnvTeardown) { 245 - logger.error( 246 - c.red( 247 - 'This error was caught after test environment was torn down. Make sure to cancel any running tasks before test finishes:' 248 - + '\n- cancel timeouts using clearTimeout and clearInterval' 249 - + '\n- wait for promises to resolve using the await keyword', 250 240 ), 251 241 ) 252 242 } ··· 292 282 'columnNumber', 293 283 'VITEST_TEST_NAME', 294 284 'VITEST_TEST_PATH', 295 - 'VITEST_AFTER_ENV_TEARDOWN', 296 285 '__vitest_rollup_error__', 297 286 ...Object.getOwnPropertyNames(Error.prototype), 298 287 ...Object.getOwnPropertyNames(Object.prototype),
+10 -21
packages/vitest/src/runtime/rpc.ts
··· 61 61 return Promise.all(awaitable) 62 62 } 63 63 64 - let previousRpc: undefined | WorkerRPC 64 + const onCancelCallbacks: ((reason: CancelReason) => void)[] = [] 65 + 66 + export function onCancel(callback: (reason: CancelReason) => void): void { 67 + onCancelCallbacks.push(callback) 68 + } 65 69 66 70 export function createRuntimeRpc( 67 71 options: Pick< 68 72 BirpcOptions<RuntimeRPC>, 69 73 'on' | 'post' | 'serialize' | 'deserialize' 70 74 >, 71 - ): { rpc: WorkerRPC; onCancel: Promise<CancelReason> } { 72 - if (previousRpc) { 73 - previousRpc.$close() 74 - previousRpc = undefined 75 - } 76 - 77 - let setCancel = (_reason: CancelReason) => {} 78 - const onCancel = new Promise<CancelReason>((resolve) => { 79 - setCancel = resolve 80 - }) 81 - 82 - const rpc = createSafeRpc( 75 + ): WorkerRPC { 76 + return createSafeRpc( 83 77 createBirpc<RuntimeRPC, RunnerRPC>( 84 78 { 85 - onCancel: setCancel, 79 + async onCancel(reason) { 80 + await Promise.all(onCancelCallbacks.map(fn => fn(reason))) 81 + }, 86 82 }, 87 83 { 88 84 eventNames: [ ··· 95 91 }, 96 92 ), 97 93 ) 98 - 99 - previousRpc = rpc 100 - 101 - return { 102 - rpc, 103 - onCancel, 104 - } 105 94 } 106 95 107 96 export function createSafeRpc(rpc: WorkerRPC): WorkerRPC {
+35 -44
packages/vitest/src/runtime/runBaseTests.ts
··· 1 1 import type { FileSpecification } from '@vitest/runner' 2 - import type { ResolvedTestEnvironment } from '../types/environment' 2 + import type { Environment } from '../types/environment' 3 3 import type { SerializedConfig } from './config' 4 4 import type { VitestModuleRunner } from './moduleRunner/moduleRunner' 5 5 import { performance } from 'node:perf_hooks' 6 6 import { collectTests, startTests } from '@vitest/runner' 7 - import { setupChaiConfig } from '../integrations/chai/config' 8 7 import { 9 8 startCoverageInsideWorker, 10 9 stopCoverageInsideWorker, 11 10 } from '../integrations/coverage' 11 + import { resolveSnapshotEnvironment } from '../integrations/snapshot/environments/resolveSnapshotEnvironment' 12 12 import { vi } from '../integrations/vi' 13 13 import { closeInspector } from './inspector' 14 14 import { resolveTestRunner } from './runners' 15 - import { setupGlobalEnv, withEnv } from './setup-node' 15 + import { setupGlobalEnv } from './setup-node' 16 16 import { getWorkerState, resetModules } from './utils' 17 17 18 18 // browser shouldn't call this! ··· 20 20 method: 'run' | 'collect', 21 21 files: FileSpecification[], 22 22 config: SerializedConfig, 23 - environment: ResolvedTestEnvironment, 24 23 moduleRunner: VitestModuleRunner, 24 + environment: Environment, 25 25 ): Promise<void> { 26 26 const workerState = getWorkerState() 27 27 28 - await setupGlobalEnv(config, environment, moduleRunner) 29 - await startCoverageInsideWorker(config.coverage, moduleRunner, { isolate: config.isolate }) 28 + const [testRunner] = await Promise.all([ 29 + resolveTestRunner(config, moduleRunner), 30 + setupGlobalEnv(config, environment), 31 + startCoverageInsideWorker(config.coverage, moduleRunner, { isolate: config.isolate }), 32 + (async () => { 33 + if (!workerState.config.snapshotOptions.snapshotEnvironment) { 34 + workerState.config.snapshotOptions.snapshotEnvironment 35 + = await resolveSnapshotEnvironment(config, moduleRunner) 36 + } 37 + })(), 38 + ]) 30 39 31 - if (config.chaiConfig) { 32 - setupChaiConfig(config.chaiConfig) 33 - } 34 - 35 - const runner = await resolveTestRunner(config, moduleRunner) 36 - 37 - workerState.onCancel.then((reason) => { 40 + workerState.onCancel((reason) => { 38 41 closeInspector(config) 39 - runner.cancel?.(reason) 42 + testRunner.cancel?.(reason) 40 43 }) 41 44 42 45 workerState.durations.prepare = performance.now() - workerState.durations.prepare 43 - workerState.durations.environment = performance.now() 44 46 45 - await withEnv( 46 - environment, 47 - environment.options || config.environmentOptions || {}, 48 - async () => { 49 - workerState.durations.environment 50 - = performance.now() - workerState.durations.environment 47 + for (const file of files) { 48 + if (config.isolate) { 49 + moduleRunner.mocker.reset() 50 + resetModules(workerState.evaluatedModules, true) 51 + } 51 52 52 - for (const file of files) { 53 - if (config.isolate) { 54 - moduleRunner.mocker.reset() 55 - resetModules(workerState.evaluatedModules, true) 56 - } 53 + workerState.filepath = file.filepath 57 54 58 - workerState.filepath = file.filepath 55 + if (method === 'run') { 56 + await startTests([file], testRunner) 57 + } 58 + else { 59 + await collectTests([file], testRunner) 60 + } 59 61 60 - if (method === 'run') { 61 - await startTests([file], runner) 62 - } 63 - else { 64 - await collectTests([file], runner) 65 - } 62 + // reset after tests, because user might call `vi.setConfig` in setupFile 63 + vi.resetConfig() 64 + // mocks should not affect different files 65 + vi.restoreAllMocks() 66 + } 66 67 67 - // reset after tests, because user might call `vi.setConfig` in setupFile 68 - vi.resetConfig() 69 - // mocks should not affect different files 70 - vi.restoreAllMocks() 71 - } 72 - 73 - await stopCoverageInsideWorker(config.coverage, moduleRunner, { isolate: config.isolate }) 74 - }, 75 - ) 76 - 77 - workerState.environmentTeardownRun = true 68 + await stopCoverageInsideWorker(config.coverage, moduleRunner, { isolate: config.isolate }) 78 69 }
+1 -1
packages/vitest/src/runtime/runVmTests.ts
··· 78 78 79 79 runner.getWorkerContext = undefined 80 80 81 - workerState.onCancel.then((reason) => { 81 + workerState.onCancel((reason) => { 82 82 closeInspector(config) 83 83 runner.cancel?.(reason) 84 84 })
+5 -37
packages/vitest/src/runtime/setup-node.ts
··· 1 - import type { ResolvedTestEnvironment } from '../types/environment' 1 + import type { Environment } from '../types/environment' 2 2 import type { SerializedConfig } from './config' 3 - import type { VitestModuleRunner } from './moduleRunner/moduleRunner' 4 3 import { createRequire } from 'node:module' 5 4 import timers from 'node:timers' 6 5 import timersPromises from 'node:timers/promises' 7 6 import util from 'node:util' 8 7 import { KNOWN_ASSET_TYPES } from '@vitest/utils/constants' 9 - import { getSafeTimers } from '@vitest/utils/timers' 10 - import { expect } from '../integrations/chai' 11 - import { resolveSnapshotEnvironment } from '../integrations/snapshot/environments/resolveSnapshotEnvironment' 12 8 import * as VitestIndex from '../public/index' 13 9 import { setupCommonEnv } from './setup-common' 14 - import { getWorkerState } from './utils' 15 10 16 11 // this should only be used in Node 17 12 let globalSetup = false 18 13 export async function setupGlobalEnv( 19 14 config: SerializedConfig, 20 - { environment }: ResolvedTestEnvironment, 21 - moduleRunner: VitestModuleRunner, 15 + environment: Environment, 22 16 ): Promise<void> { 23 17 await setupCommonEnv(config) 24 18 ··· 27 21 enumerable: false, 28 22 }) 29 23 30 - const state = getWorkerState() 31 - 32 - if (!state.config.snapshotOptions.snapshotEnvironment) { 33 - state.config.snapshotOptions.snapshotEnvironment 34 - = await resolveSnapshotEnvironment(config, moduleRunner) 35 - } 24 + VitestIndex.expect.setState({ 25 + environment: environment.name, 26 + }) 36 27 37 28 if (globalSetup) { 38 29 return ··· 82 73 const { createCustomConsole } = await import('./console') 83 74 84 75 globalThis.console = createCustomConsole() 85 - } 86 - 87 - export async function withEnv( 88 - { environment }: ResolvedTestEnvironment, 89 - options: Record<string, any>, 90 - fn: () => Promise<void>, 91 - ): Promise<void> { 92 - // @ts-expect-error untyped global 93 - globalThis.__vitest_environment__ = environment.name 94 - expect.setState({ 95 - environment: environment.name, 96 - }) 97 - const env = await environment.setup(globalThis, options) 98 - try { 99 - await fn() 100 - } 101 - finally { 102 - // Run possible setTimeouts, e.g. the onces used by ConsoleLogSpy 103 - const { setTimeout } = getSafeTimers() 104 - await new Promise(resolve => setTimeout(resolve)) 105 - 106 - await env.teardown(globalThis) 107 - } 108 76 }
+9 -15
packages/vitest/src/runtime/worker.ts
··· 1 - import type { ModuleRunner } from 'vite/module-runner' 2 1 import type { ContextRPC, WorkerGlobalState } from '../types/worker' 3 2 import type { VitestWorker } from './workers/types' 4 3 import { createStackString, parseStacktrace } from '@vitest/utils/source-map' 5 - import { loadEnvironment } from '../integrations/env/loader' 6 4 import { setupInspect } from './inspector' 7 5 import { VitestEvaluatedModules } from './moduleRunner/evaluatedModules' 8 - import { createRuntimeRpc, rpcDone } from './rpc' 6 + import { onCancel, rpcDone } from './rpc' 9 7 10 8 const resolvingModules = new Set<string>() 11 9 const globalListeners = new Set<() => unknown>() ··· 15 13 16 14 const cleanups: (() => void | Promise<void>)[] = [setupInspect(ctx)] 17 15 18 - let environmentLoader: ModuleRunner | undefined 19 - 20 16 // RPC is used to communicate between worker (be it a thread worker or child process or a custom implementation) and the main thread 21 - const { rpc, onCancel } = createRuntimeRpc(worker) 17 + const rpc = ctx.rpc 22 18 23 19 try { 24 20 // do not close the RPC channel so that we can get the error messages sent to the main thread ··· 28 24 })) 29 25 }) 30 26 31 - const beforeEnvironmentTime = performance.now() 32 - const { environment, loader } = await loadEnvironment(ctx, rpc) 33 - environmentLoader = loader 34 - 35 27 const state = { 36 28 ctx, 37 29 // here we create a new one, workers can reassign this if they need to keep it non-isolated ··· 39 31 resolvingModules, 40 32 moduleExecutionInfo: new Map(), 41 33 config: ctx.config, 42 - onCancel, 43 - environment, 34 + // this is set later by vm or base 35 + environment: null!, 44 36 durations: { 45 - environment: beforeEnvironmentTime, 37 + environment: 0, 46 38 prepare: prepareStart, 47 39 }, 48 40 rpc, 41 + onCancel, 49 42 onCleanup: listener => globalListeners.add(listener), 50 43 providedContext: ctx.providedContext, 51 44 onFilterStackTrace(stack) { ··· 67 60 finally { 68 61 await rpcDone().catch(() => {}) 69 62 await Promise.all(cleanups.map(fn => fn())).catch(() => {}) 70 - await environmentLoader?.close() 71 63 } 72 64 } 73 65 ··· 83 75 await Promise.all([...globalListeners].map(l => l())) 84 76 } 85 77 78 + const env = process.env 79 + 86 80 function createImportMetaEnvProxy(): WorkerGlobalState['metaEnv'] { 87 81 // packages/vitest/src/node/plugins/index.ts:146 88 82 const booleanKeys = ['DEV', 'PROD', 'SSR'] 89 - return new Proxy(process.env, { 83 + return new Proxy(env, { 90 84 get(_, key) { 91 85 if (typeof key !== 'string') { 92 86 return undefined
+29 -7
packages/vitest/src/types/worker.ts
··· 12 12 options: Record<string, any> | null 13 13 } 14 14 15 + export interface WorkerTestEnvironment { 16 + name: string 17 + options: Record<string, any> | null 18 + } 19 + 15 20 export type TestExecutionMethod = 'run' | 'collect' 16 21 17 - export interface ContextRPC { 18 - pool: string 19 - config: SerializedConfig 20 - projectName: string 22 + export interface WorkerExecuteContext { 21 23 files: FileSpecification[] 22 - environment: ContextTestEnvironment 23 24 providedContext: Record<string, any> 24 25 invalidates?: string[] 25 26 26 27 /** Exposed to test runner as `VITEST_WORKER_ID`. Value is unique per each isolated worker. */ 27 28 workerId: number 29 + } 30 + 31 + export interface ContextRPC { 32 + pool: string 33 + config: SerializedConfig 34 + projectName: string 35 + environment: WorkerTestEnvironment 36 + rpc: WorkerRPC 37 + files: FileSpecification[] 38 + providedContext: Record<string, any> 39 + invalidates?: string[] 40 + 41 + /** Exposed to test runner as `VITEST_WORKER_ID`. Value is unique per each isolated worker. */ 42 + workerId: number 43 + } 44 + 45 + export interface WorkerSetupContext { 46 + environment: WorkerTestEnvironment 47 + pool: string 48 + config: SerializedConfig 49 + projectName: string 50 + rpc: WorkerRPC 28 51 } 29 52 30 53 export interface WorkerGlobalState { ··· 42 65 SSR: boolean 43 66 } 44 67 environment: Environment 45 - environmentTeardownRun?: boolean 46 - onCancel: Promise<CancelReason> 47 68 evaluatedModules: EvaluatedModules 48 69 resolvingModules: Set<string> 49 70 moduleExecutionInfo: Map<string, any> 71 + onCancel: (listener: (reason: CancelReason) => unknown) => void 50 72 onCleanup: (listener: () => unknown) => void 51 73 providedContext: Record<string, any> 52 74 durations: {
+1 -4
test/cli/test/__snapshots__/fails.test.ts.snap
··· 123 123 Error: Hook timed out in 16ms." 124 124 `; 125 125 126 - exports[`should fail unhandled.test.ts 1`] = ` 127 - "Error: some error 128 - Error: Uncaught [Error: some error]" 129 - `; 126 + exports[`should fail unhandled.test.ts 1`] = `"Error: some error"`; 130 127 131 128 exports[`should fail unhandled-suite.test.ts 1`] = `"Unknown Error: promise error"`;
+1 -1
packages/browser/src/client/tester/runner.ts
··· 312 312 }) 313 313 cachedRunner = runner 314 314 315 - onCancel.then((reason) => { 315 + onCancel((reason) => { 316 316 runner.cancel?.(reason) 317 317 }) 318 318
+1
packages/browser/src/client/tester/state.ts
··· 8 8 9 9 const state: WorkerGlobalState = { 10 10 ctx: { 11 + rpc: null as any, 11 12 pool: 'browser', 12 13 workerId: 1, 13 14 config,
+1 -1
packages/browser/src/client/tester/tester.ts
··· 103 103 104 104 state.metaEnv = import.meta.env 105 105 state.onCancel = onCancel 106 + state.ctx.rpc = rpc as any 106 107 state.rpc = rpc as any 107 108 108 109 const interceptor = createModuleMockerInterceptor() ··· 253 254 await rpc.wdioSwitchContext('parent') 254 255 .catch(error => unhandledError(error, 'Cleanup Error')) 255 256 } 256 - state.environmentTeardownRun = true 257 257 await stopCoverageInsideWorker(config.coverage, moduleRunner, { isolate: config.browser.isolate }).catch((error) => { 258 258 return unhandledError(error, 'Coverage Error') 259 259 })
-11
packages/ui/client/components/dashboard/ErrorEntry.vue
··· 30 30 </li> 31 31 </ul> 32 32 </div> 33 - <div v-if="error.VITEST_AFTER_ENV_TEARDOWN" text="sm" font-thin> 34 - This error was caught after test environment was torn down. Make sure to cancel any running tasks before test finishes:<br> 35 - <ul> 36 - <li> 37 - Cancel timeouts using clearTimeout and clearInterval. 38 - </li> 39 - <li> 40 - Wait for promises to resolve using the await keyword. 41 - </li> 42 - </ul> 43 - </div> 44 33 </template>
+1
packages/vitest/src/integrations/env/jsdom.ts
··· 7 7 let userErrorListenerCount = 0 8 8 function throwUnhandlerError(e: ErrorEvent) { 9 9 if (userErrorListenerCount === 0 && e.error != null) { 10 + e.preventDefault() 10 11 process.emit('uncaughtException', e.error) 11 12 } 12 13 }
+3 -4
packages/vitest/src/integrations/env/loader.ts
··· 1 1 import type { BuiltinEnvironment, VitestEnvironment } from '../../node/types/config' 2 2 import type { Environment } from '../../types/environment' 3 - import type { ContextRPC, WorkerRPC } from '../../types/worker' 3 + import type { WorkerRPC } from '../../types/worker' 4 4 import { readFileSync } from 'node:fs' 5 5 import { isBuiltin } from 'node:module' 6 6 import { pathToFileURL } from 'node:url' ··· 54 54 } 55 55 56 56 export async function loadEnvironment( 57 - ctx: ContextRPC, 57 + name: string, 58 + root: string, 58 59 rpc: WorkerRPC, 59 60 ): Promise<{ environment: Environment; loader?: ModuleRunner }> { 60 - const name = ctx.environment.name 61 61 if (isBuiltinEnvironment(name)) { 62 62 return { environment: environments[name] } 63 63 } 64 - const root = ctx.config.root 65 64 const loader = await createEnvironmentLoader(root, rpc) 66 65 const packageId 67 66 = name[0] === '.' || name[0] === '/'
+2 -2
packages/vitest/src/node/pools/pool.ts
··· 208 208 distPath: this.options.distPath, 209 209 project: task.project, 210 210 method, 211 - environment: task.context.environment.name, 211 + environment: task.environment, 212 212 env: task.env, 213 213 execArgv: task.execArgv, 214 214 } ··· 280 280 return ( 281 281 runner.worker.name === task.worker 282 282 && runner.project === task.project 283 - && runner.environment === task.context.environment.name 283 + && runner.environment.name === task.environment.name 284 284 && (!runner.worker.canReuse || runner.worker.canReuse(task)) 285 285 ) 286 286 }
+17 -3
packages/vitest/src/node/pools/poolRunner.ts
··· 1 1 import type { DeferPromise } from '@vitest/utils/helpers' 2 2 import type { BirpcReturn } from 'birpc' 3 3 import type { RunnerRPC, RuntimeRPC } from '../../types/rpc' 4 + import type { ContextTestEnvironment } from '../../types/worker' 4 5 import type { TestProject } from '../project' 5 6 import type { PoolOptions, PoolWorker, WorkerRequest, WorkerResponse } from './types' 6 7 import { EventEmitter } from 'node:events' ··· 25 26 public poolId: number | undefined = undefined 26 27 27 28 public readonly project: TestProject 28 - public readonly environment: string 29 + public readonly environment: ContextTestEnvironment 29 30 30 31 private _state: RunnerState = RunnerState.IDLE 31 32 private _operationLock: DeferPromise<void> | null = null ··· 108 109 __vitest_worker_request__: true, 109 110 options: { 110 111 reportMemory: this.worker.reportMemory ?? false, 112 + }, 113 + context: { 114 + environment: { 115 + name: this.environment.name, 116 + options: this.environment.options, 117 + }, 118 + config: this.project.serializedConfig, 119 + pool: this.worker.name, 111 120 }, 112 121 }) 113 122 ··· 232 241 } 233 242 234 243 private waitForStart() { 235 - return new Promise<void>((resolve) => { 244 + return new Promise<void>((resolve, reject) => { 236 245 const onStart = (message: WorkerResponse) => { 237 246 if (message.type === 'started') { 238 247 this.off('message', onStart) 239 - resolve() 248 + if (message.error) { 249 + reject(message.error) 250 + } 251 + else { 252 + resolve() 253 + } 240 254 } 241 255 } 242 256
+17 -7
packages/vitest/src/node/pools/types.ts
··· 1 - import type { ContextRPC } from '../../types/worker' 1 + import type { ContextTestEnvironment, WorkerExecuteContext, WorkerTestEnvironment } from '../../types/worker' 2 2 import type { TestProject } from '../project' 3 + import type { SerializedConfig } from '../types/config' 3 4 4 5 export interface PoolRunnerInitializer { 5 6 readonly name: string ··· 11 12 project: TestProject 12 13 method: 'run' | 'collect' 13 14 cacheFs?: boolean 14 - environment: string 15 + environment: ContextTestEnvironment 15 16 execArgv: string[] 16 17 env: Partial<NodeJS.ProcessEnv> 17 18 } ··· 51 52 * so modifying it once will modify it for every task. 52 53 */ 53 54 execArgv: string[] 54 - context: ContextRPC 55 + context: WorkerExecuteContext 56 + environment: ContextTestEnvironment 55 57 memoryLimit: number | null 56 58 } 57 59 58 60 export type WorkerRequest 59 61 = { __vitest_worker_request__: true } & ( 60 - | { type: 'start'; options: { reportMemory: boolean } } 62 + | { 63 + type: 'start' 64 + options: { reportMemory: boolean } 65 + context: { 66 + environment: WorkerTestEnvironment 67 + config: SerializedConfig 68 + pool: string 69 + } 70 + } 61 71 | { type: 'stop' } 62 - | { type: 'run'; context: ContextRPC; poolId: number } 63 - | { type: 'collect'; context: ContextRPC; poolId: number } 72 + | { type: 'run'; context: WorkerExecuteContext; poolId: number } 73 + | { type: 'collect'; context: WorkerExecuteContext; poolId: number } 64 74 | { type: 'cancel' } 65 75 ) 66 76 67 77 export type WorkerResponse 68 78 = { __vitest_worker_response__: true } & ( 69 - | { type: 'started' } 79 + | { type: 'started'; error?: unknown } 70 80 | { type: 'stopped'; error?: unknown } 71 81 | { type: 'testfileFinished'; usedMemory?: number; error?: unknown } 72 82 )
-1
packages/vitest/src/runtime/moduleRunner/errorCatcher.ts
··· 29 29 if (worker.filepath) { 30 30 error.VITEST_TEST_PATH = worker.filepath 31 31 } 32 - error.VITEST_AFTER_ENV_TEARDOWN = worker.environmentTeardownRun 33 32 } 34 33 state().rpc.onUnhandledError(error, type) 35 34 }
+38 -11
packages/vitest/src/runtime/workers/base.ts
··· 1 - import type { WorkerGlobalState } from '../../types/worker' 1 + import type { Environment } from '../../types/environment' 2 + import type { WorkerGlobalState, WorkerSetupContext } from '../../types/worker' 2 3 import type { VitestModuleRunner } from '../moduleRunner/moduleRunner' 3 4 import type { ContextModuleRunnerOptions } from '../moduleRunner/startModuleRunner' 4 5 import { runInThisContext } from 'node:vm' 5 6 import * as spyModule from '@vitest/spy' 7 + import { setupChaiConfig } from '../../integrations/chai/config' 8 + import { loadEnvironment } from '../../integrations/env/loader' 6 9 import { VitestEvaluatedModules } from '../moduleRunner/evaluatedModules' 7 10 import { createNodeImportMeta } from '../moduleRunner/moduleRunner' 8 11 import { startVitestModuleRunner } from '../moduleRunner/startModuleRunner' ··· 23 26 return _moduleRunner 24 27 } 25 28 29 + let _currentEnvironment!: Environment 30 + let _environmentTime: number 31 + 32 + export async function setupEnvironment(context: WorkerSetupContext): Promise<() => Promise<void>> { 33 + const startTime = performance.now() 34 + const { 35 + environment: { name: environmentName, options: environmentOptions }, 36 + rpc, 37 + config, 38 + } = context 39 + 40 + const { environment, loader } = await loadEnvironment(environmentName, config.root, rpc) 41 + _currentEnvironment = environment 42 + const env = await environment.setup(globalThis, environmentOptions || config.environmentOptions || {}) 43 + 44 + _environmentTime = performance.now() - startTime 45 + 46 + if (config.chaiConfig) { 47 + setupChaiConfig(config.chaiConfig) 48 + } 49 + 50 + return async () => { 51 + await env.teardown(globalThis) 52 + await loader?.close() 53 + } 54 + } 55 + 26 56 /** @experimental */ 27 57 export async function runBaseTests(method: 'run' | 'collect', state: WorkerGlobalState): Promise<void> { 28 58 const { ctx } = state 59 + state.environment = _currentEnvironment 60 + state.durations.environment = _environmentTime 29 61 // state has new context, but we want to reuse existing ones 30 62 state.evaluatedModules = evaluatedModules 31 63 state.moduleExecutionInfo = moduleExecutionInfo ··· 41 73 }) 42 74 } 43 75 ctx.files.forEach((i) => { 44 - const filepath = typeof i === 'string' ? i : i.filepath 76 + const filepath = i.filepath 45 77 const modules = state.evaluatedModules.fileToModulesMap.get(filepath) || [] 46 78 modules.forEach((module) => { 47 79 state.evaluatedModules.invalidateModule(module) 48 80 }) 49 81 }) 50 82 51 - const executor = startModuleRunner({ 83 + const moduleRunner = startModuleRunner({ 52 84 state, 53 85 evaluatedModules: state.evaluatedModules, 54 86 spyModule, 55 87 createImportMeta: createNodeImportMeta, 56 88 }) 57 - const fileSpecs = ctx.files.map(f => 58 - typeof f === 'string' 59 - ? { filepath: f, testLocations: undefined } 60 - : f, 61 - ) 62 89 // we could load @vite/env, but it would take ~8ms, while this takes ~0,02ms 63 90 if (ctx.config.serializedDefines) { 64 91 try { ··· 74 101 75 102 await run( 76 103 method, 77 - fileSpecs, 104 + ctx.files, 78 105 ctx.config, 79 - { environment: state.environment, options: ctx.environment.options }, 80 - executor, 106 + moduleRunner, 107 + _currentEnvironment, 81 108 ) 82 109 }
+2 -2
packages/vitest/src/runtime/workers/forks.ts
··· 1 - import { runBaseTests } from './base' 1 + import { runBaseTests, setupEnvironment } from './base' 2 2 import workerInit from './init-forks' 3 3 4 - workerInit({ runTests: runBaseTests }) 4 + workerInit({ runTests: runBaseTests, setup: setupEnvironment })
+3 -1
packages/vitest/src/runtime/workers/init-forks.ts
··· 1 1 import type { ResolvedConfig, SerializedConfig } from '../../node/types/config' 2 - import type { WorkerGlobalState } from '../../types/worker' 2 + import type { WorkerGlobalState, WorkerSetupContext } from '../../types/worker' 3 3 import v8 from 'node:v8' 4 4 import { init } from './init' 5 5 ··· 29 29 30 30 export default function workerInit(options: { 31 31 runTests: (method: 'run' | 'collect', state: WorkerGlobalState) => Promise<void> 32 + setup?: (context: WorkerSetupContext) => Promise<() => Promise<unknown>> 32 33 }): void { 33 34 const { runTests } = options 34 35 ··· 41 42 deserialize: v => v8.deserialize(Buffer.from(v)), 42 43 runTests: state => executeTests('run', state), 43 44 collectTests: state => executeTests('collect', state), 45 + setup: options.setup, 44 46 }) 45 47 46 48 async function executeTests(method: 'run' | 'collect', state: WorkerGlobalState) {
+3 -1
packages/vitest/src/runtime/workers/init-threads.ts
··· 1 - import type { WorkerGlobalState } from '../../types/worker' 1 + import type { WorkerGlobalState, WorkerSetupContext } from '../../types/worker' 2 2 import { isMainThread, parentPort } from 'node:worker_threads' 3 3 import { init } from './init' 4 4 ··· 8 8 9 9 export default function workerInit(options: { 10 10 runTests: (method: 'run' | 'collect', state: WorkerGlobalState) => Promise<void> 11 + setup?: (context: WorkerSetupContext) => Promise<() => Promise<unknown>> 11 12 }): void { 12 13 const { runTests } = options 13 14 ··· 18 19 teardown: () => parentPort!.removeAllListeners('message'), 19 20 runTests: async state => runTests('run', state), 20 21 collectTests: async state => runTests('collect', state), 22 + setup: options.setup, 21 23 }) 22 24 }
+36 -6
packages/vitest/src/runtime/workers/init.ts
··· 1 1 import type { WorkerRequest, WorkerResponse } from '../../node/pools/types' 2 + import type { WorkerSetupContext } from '../../types/worker' 2 3 import type { VitestWorker } from './types' 3 4 import { serializeError } from '@vitest/utils/error' 5 + import { createRuntimeRpc } from '../rpc' 4 6 import * as entrypoint from '../worker' 5 7 6 8 interface Options extends VitestWorker { ··· 17 19 18 20 let runPromise: Promise<unknown> | undefined 19 21 let isRunning = false 22 + let workerTeardown: (() => Promise<unknown>) | undefined 23 + let setupContext!: WorkerSetupContext 20 24 21 25 function send(response: WorkerResponse) { 22 26 worker.post(worker.serialize ? worker.serialize(response) : response) ··· 34 38 switch (message.type) { 35 39 case 'start': { 36 40 reportMemory = message.options.reportMemory 37 - send({ type: 'started', __vitest_worker_response__ }) 41 + 42 + const { environment, config, pool } = message.context 43 + try { 44 + const rpc = createRuntimeRpc(worker) 45 + setupContext = { 46 + environment, 47 + config, 48 + pool, 49 + rpc, 50 + projectName: config.name || '', 51 + } 52 + workerTeardown = await worker.setup?.(setupContext) 53 + 54 + send({ type: 'started', __vitest_worker_response__ }) 55 + } 56 + catch (error) { 57 + send({ type: 'started', __vitest_worker_response__, error: serializeError(error) }) 58 + } 38 59 39 60 break 40 61 } ··· 66 87 isRunning = true 67 88 68 89 try { 69 - runPromise = entrypoint.run(message.context, worker) 90 + runPromise = entrypoint.run({ ...setupContext, ...message.context }, worker) 70 91 .catch(error => serializeError(error)) 71 92 const error = await runPromise 72 93 ··· 112 133 isRunning = true 113 134 114 135 try { 115 - runPromise = entrypoint.collect(message.context, worker) 136 + runPromise = entrypoint.collect({ ...setupContext, ...message.context }, worker) 116 137 .catch(error => serializeError(error)) 117 138 const error = await runPromise 118 139 ··· 133 154 134 155 case 'stop': { 135 156 await runPromise 136 - const error = await entrypoint.teardown() 137 - .catch(error => serializeError(error)) 138 157 139 - send({ type: 'stopped', error, __vitest_worker_response__ }) 158 + try { 159 + const error = await entrypoint.teardown() 160 + .catch(error => serializeError(error)) 161 + 162 + await workerTeardown?.() 163 + 164 + send({ type: 'stopped', error, __vitest_worker_response__ }) 165 + } 166 + catch (error) { 167 + send({ type: 'stopped', error: serializeError(error), __vitest_worker_response__ }) 168 + } 169 + 140 170 worker.teardown?.() 141 171 142 172 break
+2 -2
packages/vitest/src/runtime/workers/threads.ts
··· 1 - import { runBaseTests } from './base' 1 + import { runBaseTests, setupEnvironment } from './base' 2 2 import workerInit from './init-threads' 3 3 4 - workerInit({ runTests: runBaseTests }) 4 + workerInit({ runTests: runBaseTests, setup: setupEnvironment })
+3 -1
packages/vitest/src/runtime/workers/types.ts
··· 1 1 import type { Awaitable } from '@vitest/utils' 2 2 import type { BirpcOptions } from 'birpc' 3 3 import type { RuntimeRPC } from '../../types/rpc' 4 - import type { WorkerGlobalState } from '../../types/worker' 4 + import type { WorkerGlobalState, WorkerSetupContext } from '../../types/worker' 5 5 6 6 type WorkerRpcOptions = Pick< 7 7 BirpcOptions<RuntimeRPC>, ··· 11 11 export interface VitestWorker extends WorkerRpcOptions { 12 12 runTests: (state: WorkerGlobalState) => Awaitable<unknown> 13 13 collectTests: (state: WorkerGlobalState) => Awaitable<unknown> 14 + 15 + setup?: (context: WorkerSetupContext) => Promise<() => Promise<unknown>> 14 16 }
+7 -3
packages/vitest/src/runtime/workers/vm.ts
··· 3 3 import { pathToFileURL } from 'node:url' 4 4 import { isContext, runInContext } from 'node:vm' 5 5 import { resolve } from 'pathe' 6 + import { loadEnvironment } from '../../integrations/env/loader' 6 7 import { distDir } from '../../paths' 7 8 import { createCustomConsole } from '../console' 8 9 import { ExternalModulesExecutor } from '../external-executor' ··· 18 19 const packageCache = new Map<string, string>() 19 20 20 21 export async function runVmTests(method: 'run' | 'collect', state: WorkerGlobalState): Promise<void> { 21 - const { environment, ctx, rpc } = state 22 + const { ctx, rpc } = state 23 + 24 + const beforeEnvironmentTime = performance.now() 25 + const { environment } = await loadEnvironment(ctx.environment.name, ctx.config.root, rpc) 26 + state.environment = environment 22 27 23 28 if (!environment.setupVM) { 24 29 const envName = ctx.environment.name ··· 34 39 ctx.environment.options || ctx.config.environmentOptions || {}, 35 40 ) 36 41 37 - state.durations.environment = performance.now() - state.durations.environment 42 + state.durations.environment = performance.now() - beforeEnvironmentTime 38 43 39 44 process.env.VITEST_VM_POOL = '1' 40 45 ··· 126 131 } 127 132 finally { 128 133 await vm.teardown?.() 129 - state.environmentTeardownRun = true 130 134 } 131 135 }
+2 -8
packages/vitest/src/node/pools/workers/forksWorker.ts
··· 40 40 } 41 41 42 42 send(message: WorkerRequest): void { 43 - if ('context' in message) { 44 - message = { 45 - ...message, 46 - context: { 47 - ...message.context, 48 - config: wrapSerializableConfig(message.context.config), 49 - }, 50 - } 43 + if ('context' in message && 'config' in message.context) { 44 + message.context.config = wrapSerializableConfig(message.context.config) 51 45 } 52 46 53 47 this.fork.send(v8.serialize(message))