From ec406b7099c3f140fb04b42d3d8a53da0d9a9812 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 1 Nov 2023 20:47:03 +0100 Subject: [PATCH 01/10] feat: implement provide/inject API to transfer data from the main thread --- packages/browser/src/client/main.ts | 1 + packages/vitest/src/api/setup.ts | 4 ++++ packages/vitest/src/api/types.ts | 3 ++- packages/vitest/src/integrations/vi.ts | 12 +++++++++++- packages/vitest/src/node/globalSetup.ts | 9 ++++++++- packages/vitest/src/node/pools/child.ts | 1 + packages/vitest/src/node/pools/threads.ts | 1 + packages/vitest/src/node/pools/vm-threads.ts | 1 + packages/vitest/src/node/workspace.ts | 13 +++++++++++-- packages/vitest/src/runtime/child.ts | 3 ++- packages/vitest/src/runtime/vm.ts | 3 ++- packages/vitest/src/runtime/worker.ts | 3 ++- packages/vitest/src/types/general.ts | 2 ++ packages/vitest/src/types/rpc.ts | 1 + packages/vitest/src/types/worker.ts | 1 + 15 files changed, 50 insertions(+), 8 deletions(-) diff --git a/packages/browser/src/client/main.ts b/packages/browser/src/client/main.ts index 841858af8735..778cff644e97 100644 --- a/packages/browser/src/client/main.ts +++ b/packages/browser/src/client/main.ts @@ -120,6 +120,7 @@ ws.addEventListener('open', async () => { environment: 0, prepare: 0, }, + providedContext: await client.rpc.getProvidedContext(), } // @ts-expect-error mocking vitest apis globalThis.__vitest_mocker__ = new VitestBrowserClientMocker() diff --git a/packages/vitest/src/api/setup.ts b/packages/vitest/src/api/setup.ts index 39a900b98e8c..724164c4df31 100644 --- a/packages/vitest/src/api/setup.ts +++ b/packages/vitest/src/api/setup.ts @@ -131,6 +131,10 @@ export function setup(vitestOrWorkspace: Vitest | WorkspaceProject, server?: Vit getCountOfFailedTests() { return ctx.state.getCountOfFailedTests() }, + // browser should have a separate RPC in the future, UI doesn't care for provided context + getProvidedContext() { + return 'ctx' in vitestOrWorkspace ? vitestOrWorkspace.getProvidedContext() : {} + }, }, { post: msg => ws.send(msg), diff --git a/packages/vitest/src/api/types.ts b/packages/vitest/src/api/types.ts index 0933448d2b14..e736c8ff53fe 100644 --- a/packages/vitest/src/api/types.ts +++ b/packages/vitest/src/api/types.ts @@ -1,6 +1,6 @@ import type { TransformResult } from 'vite' import type { CancelReason } from '@vitest/runner' -import type { AfterSuiteRunMeta, File, ModuleGraphData, Reporter, ResolvedConfig, SnapshotResult, TaskResultPack, UserConsoleLog } from '../types' +import type { AfterSuiteRunMeta, File, ModuleGraphData, ProvidedContext, Reporter, ResolvedConfig, SnapshotResult, TaskResultPack, UserConsoleLog } from '../types' export interface TransformResultWithSource extends TransformResult { source?: string @@ -30,6 +30,7 @@ export interface WebSocketHandlers { snapshotSaved(snapshot: SnapshotResult): void rerun(files: string[]): Promise updateSnapshot(file?: File): Promise + getProvidedContext(): ProvidedContext } export interface WebSocketEvents extends Pick { diff --git a/packages/vitest/src/integrations/vi.ts b/packages/vitest/src/integrations/vi.ts index fb4ce7a82f44..9b9167e9efaa 100644 --- a/packages/vitest/src/integrations/vi.ts +++ b/packages/vitest/src/integrations/vi.ts @@ -2,7 +2,7 @@ import type { FakeTimerInstallOpts } from '@sinonjs/fake-timers' import { assertTypes, createSimpleStackTrace } from '@vitest/utils' import { parseSingleStack } from '../utils/source-map' import type { VitestMocker } from '../runtime/mocker' -import type { ResolvedConfig, RuntimeConfig } from '../types' +import type { ProvidedContext, ResolvedConfig, RuntimeConfig } from '../types' import type { MockFactoryWithHelper } from '../types/mocker' import { getWorkerState } from '../utils/global' import { resetModules, waitForImportsToResolve } from '../utils/modules' @@ -333,6 +333,12 @@ export interface VitestUtils { * If config was changed with `vi.setConfig`, this will reset it to the original state. */ resetConfig(): void + + /** + * Access to injected context provided from the main thread. + * This usually returns a value provided by `globalSetup` or an external library. + */ + inject(key: T): ProvidedContext[T] } function createVitest(): VitestUtils { @@ -609,6 +615,10 @@ function createVitest(): VitestUtils { Object.assign(state.config, _config) } }, + + inject(key: T): ProvidedContext[T] { + return workerState.providedContext[key] + }, } return utils diff --git a/packages/vitest/src/node/globalSetup.ts b/packages/vitest/src/node/globalSetup.ts index 656e0db061f3..23a844203bbb 100644 --- a/packages/vitest/src/node/globalSetup.ts +++ b/packages/vitest/src/node/globalSetup.ts @@ -1,9 +1,16 @@ import { toArray } from '@vitest/utils' import type { ViteNodeRunner } from 'vite-node/client' +import type { ProvidedContext } from '../types/general' +import type { ResolvedConfig } from '../types/config' + +interface GlobalSetupUtilities { + config: ResolvedConfig + provide(key: T, value: ProvidedContext[T]): void +} export interface GlobalSetupFile { file: string - setup?: () => Promise | void + setup?: (utilities: GlobalSetupUtilities) => Promise | void teardown?: Function } diff --git a/packages/vitest/src/node/pools/child.ts b/packages/vitest/src/node/pools/child.ts index f1eddcd6dde5..67efd9cd9a52 100644 --- a/packages/vitest/src/node/pools/child.ts +++ b/packages/vitest/src/node/pools/child.ts @@ -104,6 +104,7 @@ export function createChildProcessPool(ctx: Vitest, { execArgv, env, forksPath } environment, workerId, projectName: project.getName(), + providedContext: project.getProvidedContext(), } try { await pool.run(data, { name, channel }) diff --git a/packages/vitest/src/node/pools/threads.ts b/packages/vitest/src/node/pools/threads.ts index 7b1b8040f54a..38588fd17090 100644 --- a/packages/vitest/src/node/pools/threads.ts +++ b/packages/vitest/src/node/pools/threads.ts @@ -92,6 +92,7 @@ export function createThreadsPool(ctx: Vitest, { execArgv, env, workerPath }: Po environment, workerId, projectName: project.getName(), + providedContext: project.getProvidedContext(), } try { await pool.run(data, { transferList: [workerPort], name }) diff --git a/packages/vitest/src/node/pools/vm-threads.ts b/packages/vitest/src/node/pools/vm-threads.ts index 5c8a298d4931..3f2d659dfa4b 100644 --- a/packages/vitest/src/node/pools/vm-threads.ts +++ b/packages/vitest/src/node/pools/vm-threads.ts @@ -97,6 +97,7 @@ export function createVmThreadsPool(ctx: Vitest, { execArgv, env, vmPath }: Pool environment, workerId, projectName: project.getName(), + providedContext: project.getProvidedContext(), } try { await pool.run(data, { transferList: [workerPort], name }) diff --git a/packages/vitest/src/node/workspace.ts b/packages/vitest/src/node/workspace.ts index 177eafbf1e5c..d7a821644f34 100644 --- a/packages/vitest/src/node/workspace.ts +++ b/packages/vitest/src/node/workspace.ts @@ -8,7 +8,7 @@ import { ViteNodeServer } from 'vite-node/server' import c from 'picocolors' import type { RawSourceMap } from 'vite-node' import { createBrowserServer } from '../integrations/browser/server' -import type { ArgumentsType, Reporter, ResolvedConfig, UserConfig, UserWorkspaceConfig, Vitest } from '../types' +import type { ArgumentsType, ProvidedContext, Reporter, ResolvedConfig, UserConfig, UserWorkspaceConfig, Vitest } from '../types' import { deepMerge } from '../utils' import type { Typechecker } from '../typecheck/typechecker' import type { BrowserProvider } from '../types/browser' @@ -71,6 +71,7 @@ export class WorkspaceProject { private _globalSetupInit = false private _globalSetups: GlobalSetupFile[] = [] + private _provided: ProvidedContext = {} constructor( public path: string | number, @@ -85,6 +86,14 @@ export class WorkspaceProject { return this.ctx.getCoreWorkspaceProject() === this } + provide = (key: string, value: unknown) => { + (this._provided as any)[key] = value + } + + getProvidedContext() { + return this._provided + } + async initializeGlobalSetup() { if (this._globalSetupInit) return @@ -95,7 +104,7 @@ export class WorkspaceProject { try { for (const globalSetupFile of this._globalSetups) { - const teardown = await globalSetupFile.setup?.() + const teardown = await globalSetupFile.setup?.({ provide: this.provide, config: this.config }) if (teardown == null || !!globalSetupFile.teardown) continue if (typeof teardown !== 'function') diff --git a/packages/vitest/src/runtime/child.ts b/packages/vitest/src/runtime/child.ts index 78cb84707b8d..70e1c1ed312b 100644 --- a/packages/vitest/src/runtime/child.ts +++ b/packages/vitest/src/runtime/child.ts @@ -14,7 +14,7 @@ import { createSafeRpc, rpcDone } from './rpc' import { setupInspect } from './inspector' async function init(ctx: ChildContext) { - const { config, workerId } = ctx + const { config, workerId, providedContext } = ctx process.env.VITEST_WORKER_ID = String(workerId) process.env.VITEST_POOL_ID = String(poolId) @@ -72,6 +72,7 @@ async function init(ctx: ChildContext) { prepare: performance.now(), }, rpc, + providedContext, isChildProcess: true, } diff --git a/packages/vitest/src/runtime/vm.ts b/packages/vitest/src/runtime/vm.ts index 15d30c982164..0214196cf7e4 100644 --- a/packages/vitest/src/runtime/vm.ts +++ b/packages/vitest/src/runtime/vm.ts @@ -19,7 +19,7 @@ const entryFile = pathToFileURL(resolve(distDir, 'entry-vm.js')).href export async function run(ctx: WorkerContext) { const moduleCache = new ModuleCacheMap() const mockMap = new Map() - const { config, port } = ctx + const { config, port, providedContext } = ctx let setCancel = (_reason: CancelReason) => {} const onCancel = new Promise((resolve) => { @@ -64,6 +64,7 @@ export async function run(ctx: WorkerContext) { prepare: performance.now(), }, rpc, + providedContext, } installSourcemapsSupport({ diff --git a/packages/vitest/src/runtime/worker.ts b/packages/vitest/src/runtime/worker.ts index 93f82ebfd776..a8d274ae0d60 100644 --- a/packages/vitest/src/runtime/worker.ts +++ b/packages/vitest/src/runtime/worker.ts @@ -17,7 +17,7 @@ async function init(ctx: WorkerContext) { if (isInitialized && isIsolatedThreads) throw new Error(`worker for ${ctx.files.join(',')} already initialized by ${getWorkerState().ctx.files.join(',')}. This is probably an internal bug of Vitest.`) - const { config, port, workerId } = ctx + const { config, port, workerId, providedContext } = ctx process.env.VITEST_WORKER_ID = String(workerId) process.env.VITEST_POOL_ID = String(poolId) @@ -58,6 +58,7 @@ async function init(ctx: WorkerContext) { prepare: performance.now(), }, rpc, + providedContext, } Object.defineProperty(globalThis, '__vitest_worker__', { diff --git a/packages/vitest/src/types/general.ts b/packages/vitest/src/types/general.ts index 776ac609adeb..dd62a7d18f5c 100644 --- a/packages/vitest/src/types/general.ts +++ b/packages/vitest/src/types/general.ts @@ -48,3 +48,5 @@ export interface ModuleGraphData { } export type OnServerRestartHandler = (reason?: string) => Promise | void + +export interface ProvidedContext {} diff --git a/packages/vitest/src/types/rpc.ts b/packages/vitest/src/types/rpc.ts index faad4cfe5107..d050f3928363 100644 --- a/packages/vitest/src/types/rpc.ts +++ b/packages/vitest/src/types/rpc.ts @@ -51,4 +51,5 @@ export interface ContextRPC { files: string[] invalidates?: string[] environment: ContextTestEnvironment + providedContext: Record } diff --git a/packages/vitest/src/types/worker.ts b/packages/vitest/src/types/worker.ts index 23e88a5eb21f..71ee864b68aa 100644 --- a/packages/vitest/src/types/worker.ts +++ b/packages/vitest/src/types/worker.ts @@ -33,6 +33,7 @@ export interface WorkerGlobalState { onCancel: Promise moduleCache: ModuleCacheMap mockMap: MockMap + providedContext: Record durations: { environment: number prepare: number From 7f029f2bbdc42f408795da7746269f15a1f140ff Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 1 Nov 2023 20:48:14 +0100 Subject: [PATCH 02/10] chore: clear provided --- packages/vitest/src/node/workspace.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/vitest/src/node/workspace.ts b/packages/vitest/src/node/workspace.ts index d7a821644f34..82bfc85a3e41 100644 --- a/packages/vitest/src/node/workspace.ts +++ b/packages/vitest/src/node/workspace.ts @@ -348,6 +348,7 @@ export class WorkspaceProject { this.typechecker?.stop(), this.browser?.close(), this.teardownGlobalSetup(), + () => this._provided = {}, ].filter(Boolean)) } return this.closingPromise From 0e2abea39611a1a93f42bfc676a3b51a3b6b728e Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 2 Nov 2023 10:39:30 +0100 Subject: [PATCH 03/10] fix: don't run global setup in parallel, validate "provide", merge provided with global --- packages/vitest/src/node/core.ts | 41 +++++++++++++++------------ packages/vitest/src/node/workspace.ts | 29 +++++++++++++------ 2 files changed, 43 insertions(+), 27 deletions(-) diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 0e445a7e99b6..3991b81f0bd2 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -438,9 +438,8 @@ export class Vitest { const coreProject = this.getCoreWorkspaceProject() if (!projects.has(coreProject)) projects.add(coreProject) - await Promise.all( - Array.from(projects).map(project => project.initializeGlobalSetup()), - ) + for await (const project of projects) + await project.initializeGlobalSetup() } async runFiles(paths: WorkspaceSpec[]) { @@ -750,22 +749,28 @@ export class Vitest { async close() { if (!this.closingPromise) { - const closePromises: unknown[] = this.projects.map(w => w.close().then(() => w.server = undefined as any)) - // close the core workspace server only once - // it's possible that it's not initialized at all because it's not running any tests - if (!this.projects.includes(this.coreWorkspaceProject)) - closePromises.push(this.coreWorkspaceProject.close().then(() => this.server = undefined as any)) - - if (this.pool) - closePromises.push(this.pool.close().then(() => this.pool = undefined)) - - closePromises.push(...this._onClose.map(fn => fn())) - - this.closingPromise = Promise.allSettled(closePromises).then((results) => { - results.filter(r => r.status === 'rejected').forEach((err) => { - this.logger.error('error during close', (err as PromiseRejectedResult).reason) + this.closingPromise = (async () => { + // do teardown before closing the server + for await (const project of [...this.projects].reverse()) + await project.teardownGlobalSetup() + + const closePromises: unknown[] = this.projects.map(w => w.close().then(() => w.server = undefined as any)) + // close the core workspace server only once + // it's possible that it's not initialized at all because it's not running any tests + if (!this.projects.includes(this.coreWorkspaceProject)) + closePromises.push(this.coreWorkspaceProject.close().then(() => this.server = undefined as any)) + + if (this.pool) + closePromises.push(this.pool.close().then(() => this.pool = undefined)) + + closePromises.push(...this._onClose.map(fn => fn())) + + return Promise.allSettled(closePromises).then((results) => { + results.filter(r => r.status === 'rejected').forEach((err) => { + this.logger.error('error during close', (err as PromiseRejectedResult).reason) + }) }) - }) + })() } return this.closingPromise } diff --git a/packages/vitest/src/node/workspace.ts b/packages/vitest/src/node/workspace.ts index 82bfc85a3e41..3ec4ab8231a5 100644 --- a/packages/vitest/src/node/workspace.ts +++ b/packages/vitest/src/node/workspace.ts @@ -69,8 +69,7 @@ export class WorkspaceProject { testFilesList: string[] = [] - private _globalSetupInit = false - private _globalSetups: GlobalSetupFile[] = [] + private _globalSetups: GlobalSetupFile[] | undefined private _provided: ProvidedContext = {} constructor( @@ -87,19 +86,32 @@ export class WorkspaceProject { } provide = (key: string, value: unknown) => { + try { + structuredClone(value) + } + catch (err) { + throw new Error(`Cannot provide value because it's not serializable.`, { + cause: err, + }) + } (this._provided as any)[key] = value } - getProvidedContext() { - return this._provided + getProvidedContext(): ProvidedContext { + if (this.isCore()) + return this._provided + // globalSetup can run even if core workspace is not part of the test run + // so we need to inherit its provided context + return { + ...this.ctx.getCoreWorkspaceProject().getProvidedContext(), + ...this._provided, + } } async initializeGlobalSetup() { - if (this._globalSetupInit) + if (this._globalSetups) return - this._globalSetupInit = true - this._globalSetups = await loadGlobalSetupFiles(this.runner, this.config.globalSetup) try { @@ -120,7 +132,7 @@ export class WorkspaceProject { } async teardownGlobalSetup() { - if (!this._globalSetupInit || !this._globalSetups.length) + if (!this._globalSetups) return for (const globalSetupFile of [...this._globalSetups].reverse()) { try { @@ -347,7 +359,6 @@ export class WorkspaceProject { this.server.close(), this.typechecker?.stop(), this.browser?.close(), - this.teardownGlobalSetup(), () => this._provided = {}, ].filter(Boolean)) } From 5e3ff320e835709deb6ffa1c5749974f757ba863 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 2 Nov 2023 10:52:23 +0100 Subject: [PATCH 04/10] test: add tests for global setup --- packages/vitest/src/node/config.ts | 6 ++++ packages/vitest/src/node/globalSetup.ts | 4 +-- packages/vitest/src/node/index.ts | 1 + packages/vitest/src/node/workspace.ts | 2 +- test/workspaces/globalTest.ts | 29 +++++++++++++++++-- .../space_3/global-provide.space-3-test.ts | 7 +++++ test/workspaces/space_3/localSetup.ts | 5 ++++ test/workspaces/space_3/vitest.config.ts | 1 + 8 files changed, 49 insertions(+), 6 deletions(-) create mode 100644 test/workspaces/space_3/global-provide.space-3-test.ts create mode 100644 test/workspaces/space_3/localSetup.ts diff --git a/packages/vitest/src/node/config.ts b/packages/vitest/src/node/config.ts index 8a5c4a47083a..8742526dd147 100644 --- a/packages/vitest/src/node/config.ts +++ b/packages/vitest/src/node/config.ts @@ -301,6 +301,12 @@ export function resolveConfig( ?? resolve(resolved.root, file), ), ) + resolved.globalSetup = toArray(resolved.globalSetup || []).map(file => + normalize( + resolveModule(file, { paths: [resolved.root] }) + ?? resolve(resolved.root, file), + ), + ) resolved.coverage.exclude.push(...resolved.setupFiles.map(file => `${resolved.coverage.allowExternal ? '**/' : ''}${relative(resolved.root, file)}`)) resolved.forceRerunTriggers = [ diff --git a/packages/vitest/src/node/globalSetup.ts b/packages/vitest/src/node/globalSetup.ts index 23a844203bbb..764fc0c87cbc 100644 --- a/packages/vitest/src/node/globalSetup.ts +++ b/packages/vitest/src/node/globalSetup.ts @@ -3,14 +3,14 @@ import type { ViteNodeRunner } from 'vite-node/client' import type { ProvidedContext } from '../types/general' import type { ResolvedConfig } from '../types/config' -interface GlobalSetupUtilities { +export interface GlobalSetupContext { config: ResolvedConfig provide(key: T, value: ProvidedContext[T]): void } export interface GlobalSetupFile { file: string - setup?: (utilities: GlobalSetupUtilities) => Promise | void + setup?: (utilities: GlobalSetupContext) => Promise | void teardown?: Function } diff --git a/packages/vitest/src/node/index.ts b/packages/vitest/src/node/index.ts index faf1941661fd..236915743837 100644 --- a/packages/vitest/src/node/index.ts +++ b/packages/vitest/src/node/index.ts @@ -5,6 +5,7 @@ export { VitestPlugin } from './plugins' export { startVitest } from './cli-api' export { registerConsoleShortcuts } from './stdin' export type { WorkspaceSpec } from './pool' +export type { GlobalSetupContext } from './globalSetup' export type { TestSequencer, TestSequencerConstructor } from './sequencers/types' export { BaseSequencer } from './sequencers/BaseSequencer' diff --git a/packages/vitest/src/node/workspace.ts b/packages/vitest/src/node/workspace.ts index 3ec4ab8231a5..dfca1ac477f0 100644 --- a/packages/vitest/src/node/workspace.ts +++ b/packages/vitest/src/node/workspace.ts @@ -90,7 +90,7 @@ export class WorkspaceProject { structuredClone(value) } catch (err) { - throw new Error(`Cannot provide value because it's not serializable.`, { + throw new Error(`Cannot provide "${key}" because it's not serializable.`, { cause: err, }) } diff --git a/test/workspaces/globalTest.ts b/test/workspaces/globalTest.ts index dccb99c98c08..f42631c2a19e 100644 --- a/test/workspaces/globalTest.ts +++ b/test/workspaces/globalTest.ts @@ -1,14 +1,37 @@ import { readFile } from 'node:fs/promises' import assert from 'node:assert/strict' +import type { GlobalSetupContext } from 'vitest/node' + +declare module 'vitest' { + interface ProvidedContext { + globalSetup: boolean + globalSetupOverriden: boolean + invalidValue: unknown + } +} + +export function setup({ provide }: GlobalSetupContext) { + provide('globalSetup', true) + provide('globalSetupOverriden', false) + try { + provide('invalidValue', () => {}) + throw new Error('Should throw') + } + catch (err: any) { + assert.equal(err.message, 'Cannot provide "invalidValue" because it\'s not serializable.') + assert.match(err.cause.message, /could not be cloned/) + assert.equal(err.cause.name, 'DataCloneError') + } +} export async function teardown() { const results = JSON.parse(await readFile('./results.json', 'utf-8')) try { assert.ok(results.success) - assert.equal(results.numTotalTestSuites, 9) - assert.equal(results.numTotalTests, 10) - assert.equal(results.numPassedTests, 10) + assert.equal(results.numTotalTestSuites, 11) + assert.equal(results.numTotalTests, 12) + assert.equal(results.numPassedTests, 12) const shared = results.testResults.filter((r: any) => r.name.includes('space_shared/test.spec.ts')) diff --git a/test/workspaces/space_3/global-provide.space-3-test.ts b/test/workspaces/space_3/global-provide.space-3-test.ts new file mode 100644 index 000000000000..5784e3765574 --- /dev/null +++ b/test/workspaces/space_3/global-provide.space-3-test.ts @@ -0,0 +1,7 @@ +import { expect, test, vi } from 'vitest' + +test('global setup provides data correctly', () => { + expect(vi.inject('globalSetup')).toBe(true) + expect(vi.inject('globalSetupOverriden')).toBe(true) + expect(vi.inject('invalidValue')).toBe(undefined) +}) diff --git a/test/workspaces/space_3/localSetup.ts b/test/workspaces/space_3/localSetup.ts new file mode 100644 index 000000000000..1e7eec44ca70 --- /dev/null +++ b/test/workspaces/space_3/localSetup.ts @@ -0,0 +1,5 @@ +import type { GlobalSetupContext } from 'vitest/node' + +export function setup({ provide }: GlobalSetupContext) { + provide('globalSetupOverriden', true) +} diff --git a/test/workspaces/space_3/vitest.config.ts b/test/workspaces/space_3/vitest.config.ts index 1270e126a7c1..55bdced7da5d 100644 --- a/test/workspaces/space_3/vitest.config.ts +++ b/test/workspaces/space_3/vitest.config.ts @@ -5,5 +5,6 @@ export default defineProject({ include: ['**/*.space-3-test.ts'], name: 'space_3', environment: 'node', + globalSetup: './localSetup.ts', }, }) From f2df92d0a7b9381cc3480eb60617c92f46aa7023 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 2 Nov 2023 11:00:53 +0100 Subject: [PATCH 05/10] chore: cleanup --- packages/vitest/src/node/workspace.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vitest/src/node/workspace.ts b/packages/vitest/src/node/workspace.ts index dfca1ac477f0..dda46727b8a0 100644 --- a/packages/vitest/src/node/workspace.ts +++ b/packages/vitest/src/node/workspace.ts @@ -70,7 +70,7 @@ export class WorkspaceProject { testFilesList: string[] = [] private _globalSetups: GlobalSetupFile[] | undefined - private _provided: ProvidedContext = {} + private _provided: ProvidedContext = {} as any constructor( public path: string | number, From d526f692e2006a2f911b8e77d802c8c6dcec74c2 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 2 Nov 2023 11:10:56 +0100 Subject: [PATCH 06/10] chore: move injected to a separate export --- packages/vitest/src/index.ts | 1 + packages/vitest/src/integrations/inject.ts | 11 ++++++ packages/vitest/src/integrations/vi.ts | 37 +++---------------- packages/vitest/src/runtime/execute.ts | 7 ++-- packages/vitest/src/utils/global.ts | 11 +++++- .../space_3/global-provide.space-3-test.ts | 8 ++-- 6 files changed, 36 insertions(+), 39 deletions(-) create mode 100644 packages/vitest/src/integrations/inject.ts diff --git a/packages/vitest/src/index.ts b/packages/vitest/src/index.ts index b7425e092cdb..9f5668d16649 100644 --- a/packages/vitest/src/index.ts +++ b/packages/vitest/src/index.ts @@ -15,6 +15,7 @@ export { runOnce, isFirstRun } from './integrations/run-once' export * from './integrations/chai' export * from './integrations/vi' export * from './integrations/utils' +export { inject } from './integrations/inject' export type { SnapshotEnvironment } from '@vitest/snapshot/environment' export * from './types' diff --git a/packages/vitest/src/integrations/inject.ts b/packages/vitest/src/integrations/inject.ts new file mode 100644 index 000000000000..ac23c73181b7 --- /dev/null +++ b/packages/vitest/src/integrations/inject.ts @@ -0,0 +1,11 @@ +import type { ProvidedContext } from '../types/general' +import { getWorkerState } from '../utils/global' + +/** + * Gives access to injected context provided from the main thread. + * This usually returns a value provided by `globalSetup` or an external library. + */ +export function inject(key: T): ProvidedContext[T] { + const workerState = getWorkerState() + return workerState.providedContext[key] +} diff --git a/packages/vitest/src/integrations/vi.ts b/packages/vitest/src/integrations/vi.ts index 9b9167e9efaa..46af9c23fc5e 100644 --- a/packages/vitest/src/integrations/vi.ts +++ b/packages/vitest/src/integrations/vi.ts @@ -2,7 +2,7 @@ import type { FakeTimerInstallOpts } from '@sinonjs/fake-timers' import { assertTypes, createSimpleStackTrace } from '@vitest/utils' import { parseSingleStack } from '../utils/source-map' import type { VitestMocker } from '../runtime/mocker' -import type { ProvidedContext, ResolvedConfig, RuntimeConfig } from '../types' +import type { ResolvedConfig, RuntimeConfig } from '../types' import type { MockFactoryWithHelper } from '../types/mocker' import { getWorkerState } from '../utils/global' import { resetModules, waitForImportsToResolve } from '../utils/modules' @@ -333,12 +333,6 @@ export interface VitestUtils { * If config was changed with `vi.setConfig`, this will reset it to the original state. */ resetConfig(): void - - /** - * Access to injected context provided from the main thread. - * This usually returns a value provided by `globalSetup` or an external library. - */ - inject(key: T): ProvidedContext[T] } function createVitest(): VitestUtils { @@ -359,15 +353,6 @@ function createVitest(): VitestUtils { const workerState = getWorkerState() - if (!workerState) { - const errorMsg = 'Vitest failed to access its internal state.' - + '\n\nOne of the following is possible:' - + '\n- "vitest" is imported directly without running "vitest" command' - + '\n- "vitest" is imported inside "globalSetup" (to fix this, use "setupFiles" instead, because "globalSetup" runs in a different context)' - + '\n- Otherwise, it might be a Vitest bug. Please report it to https://github.com/vitest-dev/vitest/issues\n' - throw new Error(errorMsg) - } - const _timers = new FakeTimers({ global: globalThis, config: workerState.config.fakeTimers, @@ -385,8 +370,6 @@ function createVitest(): VitestUtils { const utils: VitestUtils = { useFakeTimers(config?: FakeTimerInstallOpts) { - const workerState = getWorkerState() - if (workerState.isChildProcess) { if (config?.toFake?.includes('nextTick') || workerState.config?.fakeTimers?.toFake?.includes('nextTick')) { throw new Error( @@ -593,8 +576,7 @@ function createVitest(): VitestUtils { }, resetModules() { - const state = getWorkerState() - resetModules(state.moduleCache) + resetModules(workerState.moduleCache) return utils }, @@ -603,21 +585,14 @@ function createVitest(): VitestUtils { }, setConfig(config: RuntimeConfig) { - const state = getWorkerState() if (!_config) - _config = { ...state.config } - Object.assign(state.config, config) + _config = { ...workerState.config } + Object.assign(workerState.config, config) }, resetConfig() { - if (_config) { - const state = getWorkerState() - Object.assign(state.config, _config) - } - }, - - inject(key: T): ProvidedContext[T] { - return workerState.providedContext[key] + if (_config) + Object.assign(workerState.config, _config) }, } diff --git a/packages/vitest/src/runtime/execute.ts b/packages/vitest/src/runtime/execute.ts index d4085e2e2b1f..fbbbf6771649 100644 --- a/packages/vitest/src/runtime/execute.ts +++ b/packages/vitest/src/runtime/execute.ts @@ -8,7 +8,6 @@ import { processError } from '@vitest/utils/error' import type { MockMap } from '../types/mocker' import type { ResolvedConfig, ResolvedTestEnvironment, RuntimeRPC, WorkerGlobalState } from '../types' import { distDir } from '../paths' -import { getWorkerState } from '../utils/global' import { VitestMocker } from './mocker' import { ExternalModulesExecutor } from './external-executor' import { FileMap } from './vm/file-map' @@ -64,7 +63,8 @@ export interface ContextExecutorOptions { } export async function startVitestExecutor(options: ContextExecutorOptions) { - const state = () => getWorkerState() || options.state + // @ts-expect-error injected untyped global + const state = () => globalThis.__vitest_worker__ || options.state const rpc = () => state().rpc const processExit = process.exit @@ -203,7 +203,8 @@ export class VitestExecutor extends ViteNodeRunner { } get state() { - return getWorkerState() || this.options.state + // @ts-expect-error injected untyped global + return globalThis.__vitest_worker__ || this.options.state } shouldResolveId(id: string, _importee?: string | undefined): boolean { diff --git a/packages/vitest/src/utils/global.ts b/packages/vitest/src/utils/global.ts index ea7398b6eb1e..260a9751fc1b 100644 --- a/packages/vitest/src/utils/global.ts +++ b/packages/vitest/src/utils/global.ts @@ -2,7 +2,16 @@ import type { WorkerGlobalState } from '../types' export function getWorkerState(): WorkerGlobalState { // @ts-expect-error untyped global - return globalThis.__vitest_worker__ + const workerState = globalThis.__vitest_worker__ + if (!workerState) { + const errorMsg = 'Vitest failed to access its internal state.' + + '\n\nOne of the following is possible:' + + '\n- "vitest" is imported directly without running "vitest" command' + + '\n- "vitest" is imported inside "globalSetup" (to fix this, use "setupFiles" instead, because "globalSetup" runs in a different context)' + + '\n- Otherwise, it might be a Vitest bug. Please report it to https://github.com/vitest-dev/vitest/issues\n' + throw new Error(errorMsg) + } + return workerState } export function getCurrentEnvironment(): string { diff --git a/test/workspaces/space_3/global-provide.space-3-test.ts b/test/workspaces/space_3/global-provide.space-3-test.ts index 5784e3765574..4cf6b70dd48c 100644 --- a/test/workspaces/space_3/global-provide.space-3-test.ts +++ b/test/workspaces/space_3/global-provide.space-3-test.ts @@ -1,7 +1,7 @@ -import { expect, test, vi } from 'vitest' +import { expect, inject, test } from 'vitest' test('global setup provides data correctly', () => { - expect(vi.inject('globalSetup')).toBe(true) - expect(vi.inject('globalSetupOverriden')).toBe(true) - expect(vi.inject('invalidValue')).toBe(undefined) + expect(inject('globalSetup')).toBe(true) + expect(inject('globalSetupOverriden')).toBe(true) + expect(inject('invalidValue')).toBe(undefined) }) From 45b07ffdf5786757a5d603cd0cc126209c2bd8b4 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 2 Nov 2023 11:21:50 +0100 Subject: [PATCH 07/10] docs: add docs about inject/provide --- docs/config/index.md | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/docs/config/index.md b/docs/config/index.md index 273a519b3017..440eaa742638 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -957,7 +957,30 @@ Multiple globalSetup files are possible. setup and teardown are executed sequent ::: ::: warning -Beware that the global setup is running in a different global scope, so your tests don't have access to variables defined here. Also, since Vitest 1.0.0-beta, global setup runs only if there is at least one running test. This means that global setup might start running during watch mode after test file is changed, for example (the test file will wait for global setup to finish before running). +Since Vitest 1.0.0-beta, global setup runs only if there is at least one running test. This means that global setup might start running during watch mode after test file is changed (the test file will wait for global setup to finish before running). + +Beware that the global setup is running in a different global scope, so your tests don't have access to variables defined here. Hovewer, since 1.0.0 you can pass down serializable data to tests via `provide` method: + +```ts +// globalSetup.js +export default function setup({ provide }) { + provide('wsPort', 3000) +} +// example.test.js +import { inject } from 'vitest' + +inject('wsPort') === 3000 +``` + +If you are using TypeScript with `provide/inject` methods, you can extend `ProvidedContext` type to have type safe access: + +```ts +declare module 'vitest' { + export interface ProvidedContext { + wsPort: number + } +} +``` ::: From 963b8d79c4740c4fa92012ed81805bf7255fd9d5 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 2 Nov 2023 11:22:51 +0100 Subject: [PATCH 08/10] chore: cleanup --- packages/vitest/src/api/setup.ts | 2 +- packages/vitest/src/node/workspace.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vitest/src/api/setup.ts b/packages/vitest/src/api/setup.ts index 724164c4df31..a0bd2b9a0b20 100644 --- a/packages/vitest/src/api/setup.ts +++ b/packages/vitest/src/api/setup.ts @@ -133,7 +133,7 @@ export function setup(vitestOrWorkspace: Vitest | WorkspaceProject, server?: Vit }, // browser should have a separate RPC in the future, UI doesn't care for provided context getProvidedContext() { - return 'ctx' in vitestOrWorkspace ? vitestOrWorkspace.getProvidedContext() : {} + return 'ctx' in vitestOrWorkspace ? vitestOrWorkspace.getProvidedContext() : ({} as any) }, }, { diff --git a/packages/vitest/src/node/workspace.ts b/packages/vitest/src/node/workspace.ts index dda46727b8a0..614d84611981 100644 --- a/packages/vitest/src/node/workspace.ts +++ b/packages/vitest/src/node/workspace.ts @@ -359,7 +359,7 @@ export class WorkspaceProject { this.server.close(), this.typechecker?.stop(), this.browser?.close(), - () => this._provided = {}, + () => this._provided = ({} as any), ].filter(Boolean)) } return this.closingPromise From d01bebdd2869f50c03808056f07eb002f8776056 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 2 Nov 2023 11:31:16 +0100 Subject: [PATCH 09/10] docs: cleanup --- docs/config/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config/index.md b/docs/config/index.md index 440eaa742638..f05589c5bc14 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -972,7 +972,7 @@ import { inject } from 'vitest' inject('wsPort') === 3000 ``` -If you are using TypeScript with `provide/inject` methods, you can extend `ProvidedContext` type to have type safe access: +If you are using TypeScript, you can extend `ProvidedContext` type to have type safe access to `provide/inject` methods: ```ts declare module 'vitest' { From a5adad21b52d265abd5f3cde8f15a6c9ce6da522 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Sat, 4 Nov 2023 14:27:44 +0100 Subject: [PATCH 10/10] chore: cleanup --- packages/vitest/src/node/globalSetup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vitest/src/node/globalSetup.ts b/packages/vitest/src/node/globalSetup.ts index 764fc0c87cbc..04d5b0d3e2b1 100644 --- a/packages/vitest/src/node/globalSetup.ts +++ b/packages/vitest/src/node/globalSetup.ts @@ -10,7 +10,7 @@ export interface GlobalSetupContext { export interface GlobalSetupFile { file: string - setup?: (utilities: GlobalSetupContext) => Promise | void + setup?: (context: GlobalSetupContext) => Promise | void teardown?: Function }