From 5c689d9698f1b1dec681a31a12807bfe681b1975 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 2 Oct 2023 15:05:55 +0200 Subject: [PATCH 1/3] fix(vitest): don't initialize globalSetup if workspace doesn't run tests --- packages/vitest/src/node/core.ts | 6 ++ packages/vitest/src/node/globalSetup.ts | 38 ++++++++ .../vitest/src/node/plugins/globalSetup.ts | 90 ------------------- packages/vitest/src/node/plugins/index.ts | 2 - packages/vitest/src/node/plugins/workspace.ts | 2 - packages/vitest/src/node/workspace.ts | 52 +++++++++++ 6 files changed, 96 insertions(+), 94 deletions(-) create mode 100644 packages/vitest/src/node/globalSetup.ts delete mode 100644 packages/vitest/src/node/plugins/globalSetup.ts diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 05c654c8e9c3..a8a14bb10914 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -420,6 +420,10 @@ export class Vitest { return Array.from(projects).map(project => [project, file] as WorkspaceSpec) } + async initializeGlobalSetup(paths: WorkspaceSpec[]) { + await Promise.all(paths.map(async ([project]) => project.initializeGlobalSetup())) + } + async runFiles(paths: WorkspaceSpec[]) { const filepaths = paths.map(([, file]) => file) this.state.collectPaths(filepaths) @@ -440,6 +444,8 @@ export class Vitest { this.invalidates.clear() this.snapshot.clear() this.state.clearErrors() + await this.initializeGlobalSetup(paths) + try { await this.pool.runTests(paths, invalidates) } diff --git a/packages/vitest/src/node/globalSetup.ts b/packages/vitest/src/node/globalSetup.ts new file mode 100644 index 000000000000..165a89634fbf --- /dev/null +++ b/packages/vitest/src/node/globalSetup.ts @@ -0,0 +1,38 @@ +import { toArray } from '@vitest/utils' +import type { ViteNodeRunner } from 'vite-node/client' +import type { WorkspaceProject } from './workspace' + +export interface GlobalSetupFile { + file: string + setup?: () => Promise | void + teardown?: Function +} + +export async function loadGlobalSetupFiles(project: WorkspaceProject): Promise { + const globalSetupFiles = toArray(project.server.config.test?.globalSetup) + return Promise.all(globalSetupFiles.map(file => loadGlobalSetupFile(file, project.runner))) +} + +async function loadGlobalSetupFile(file: string, runner: ViteNodeRunner): Promise { + const m = await runner.executeFile(file) + for (const exp of ['default', 'setup', 'teardown']) { + if (m[exp] != null && typeof m[exp] !== 'function') + throw new Error(`invalid export in globalSetup file ${file}: ${exp} must be a function`) + } + if (m.default) { + return { + file, + setup: m.default, + } + } + else if (m.setup || m.teardown) { + return { + file, + setup: m.setup, + teardown: m.teardown, + } + } + else { + throw new Error(`invalid globalSetup file ${file}. Must export setup, teardown or have a default export`) + } +} diff --git a/packages/vitest/src/node/plugins/globalSetup.ts b/packages/vitest/src/node/plugins/globalSetup.ts deleted file mode 100644 index 7910590aff8c..000000000000 --- a/packages/vitest/src/node/plugins/globalSetup.ts +++ /dev/null @@ -1,90 +0,0 @@ -import type { Plugin } from 'vite' -import type { ViteNodeRunner } from 'vite-node/client' -import c from 'picocolors' -import { toArray } from '../../utils' -import { divider } from '../reporters/renderers/utils' -import type { Vitest } from '../core' -import type { Logger } from '../logger' - -interface GlobalSetupFile { - file: string - setup?: () => Promise | void - teardown?: Function -} - -type SetupInstance = Pick - -async function loadGlobalSetupFiles(project: SetupInstance): Promise { - const server = project.server - const runner = project.runner - const globalSetupFiles = toArray(server.config.test?.globalSetup) - return Promise.all(globalSetupFiles.map(file => loadGlobalSetupFile(file, runner))) -} - -async function loadGlobalSetupFile(file: string, runner: ViteNodeRunner): Promise { - const m = await runner.executeFile(file) - for (const exp of ['default', 'setup', 'teardown']) { - if (m[exp] != null && typeof m[exp] !== 'function') - throw new Error(`invalid export in globalSetup file ${file}: ${exp} must be a function`) - } - if (m.default) { - return { - file, - setup: m.default, - } - } - else if (m.setup || m.teardown) { - return { - file, - setup: m.setup, - teardown: m.teardown, - } - } - else { - throw new Error(`invalid globalSetup file ${file}. Must export setup, teardown or have a default export`) - } -} - -export function GlobalSetupPlugin(project: SetupInstance, logger: Logger): Plugin { - let globalSetupFiles: GlobalSetupFile[] - return { - name: 'vitest:global-setup-plugin', - enforce: 'pre', - - async buildStart() { - if (!project.server.config.test?.globalSetup) - return - - globalSetupFiles = await loadGlobalSetupFiles(project) - - try { - for (const globalSetupFile of globalSetupFiles) { - const teardown = await globalSetupFile.setup?.() - if (teardown == null || !!globalSetupFile.teardown) - continue - if (typeof teardown !== 'function') - throw new Error(`invalid return value in globalSetup file ${globalSetupFile.file}. Must return a function`) - globalSetupFile.teardown = teardown - } - } - catch (e) { - logger.error(`\n${c.red(divider(c.bold(c.inverse(' Error during global setup '))))}`) - await logger.printError(e) - process.exit(1) - } - }, - - async buildEnd() { - if (globalSetupFiles?.length) { - for (const globalSetupFile of globalSetupFiles.reverse()) { - try { - await globalSetupFile.teardown?.() - } - catch (error) { - logger.error(`error during global teardown of ${globalSetupFile.file}`, error) - } - } - } - }, - } -} diff --git a/packages/vitest/src/node/plugins/index.ts b/packages/vitest/src/node/plugins/index.ts index 2f30b17dc958..d7c8fc7f7047 100644 --- a/packages/vitest/src/node/plugins/index.ts +++ b/packages/vitest/src/node/plugins/index.ts @@ -8,7 +8,6 @@ import { resolveApiServerConfig } from '../config' import { Vitest } from '../core' import { generateScopedClassName } from '../../integrations/css/css-modules' import { SsrReplacerPlugin } from './ssrReplacer' -import { GlobalSetupPlugin } from './globalSetup' import { CSSEnablerPlugin } from './cssEnabler' import { CoverageTransform } from './coverageTransform' import { MocksPlugin } from './mocks' @@ -178,7 +177,6 @@ export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest('t }, }, SsrReplacerPlugin(), - GlobalSetupPlugin(ctx, ctx.logger), ...CSSEnablerPlugin(ctx), CoverageTransform(ctx), options.ui diff --git a/packages/vitest/src/node/plugins/workspace.ts b/packages/vitest/src/node/plugins/workspace.ts index cb4b016b85f6..119b7c453941 100644 --- a/packages/vitest/src/node/plugins/workspace.ts +++ b/packages/vitest/src/node/plugins/workspace.ts @@ -8,7 +8,6 @@ import type { ResolvedConfig, UserWorkspaceConfig } from '../../types' import { CoverageTransform } from './coverageTransform' import { CSSEnablerPlugin } from './cssEnabler' import { SsrReplacerPlugin } from './ssrReplacer' -import { GlobalSetupPlugin } from './globalSetup' import { MocksPlugin } from './mocks' import { deleteDefineConfig, hijackVitePluginInject, resolveFsAllow } from './utils' import { VitestResolver } from './vitestResolver' @@ -123,7 +122,6 @@ export function WorkspaceVitestPlugin(project: WorkspaceProject, options: Worksp SsrReplacerPlugin(), ...CSSEnablerPlugin(project), CoverageTransform(project.ctx), - GlobalSetupPlugin(project, project.ctx.logger), MocksPlugin(), VitestResolver(project.ctx), VitestOptimizer(), diff --git a/packages/vitest/src/node/workspace.ts b/packages/vitest/src/node/workspace.ts index e0e445ee2e18..947c4018776c 100644 --- a/packages/vitest/src/node/workspace.ts +++ b/packages/vitest/src/node/workspace.ts @@ -5,6 +5,7 @@ import { dirname, relative, resolve, toNamespacedPath } from 'pathe' import type { ViteDevServer, InlineConfig as ViteInlineConfig } from 'vite' import { ViteNodeRunner } from 'vite-node/client' import { ViteNodeServer } from 'vite-node/server' +import c from 'picocolors' import { createBrowserServer } from '../integrations/browser/server' import type { ArgumentsType, Reporter, ResolvedConfig, UserConfig, UserWorkspaceConfig, Vitest } from '../types' import { deepMerge, hasFailed } from '../utils' @@ -14,6 +15,9 @@ import { getBrowserProvider } from '../integrations/browser' import { isBrowserEnabled, resolveConfig } from './config' import { WorkspaceVitestPlugin } from './plugins/workspace' import { createViteServer } from './vite' +import type { GlobalSetupFile } from './globalSetup' +import { loadGlobalSetupFiles } from './globalSetup' +import { divider } from './reporters/renderers/utils' interface InitializeProjectOptions extends UserWorkspaceConfig { workspaceConfigPath: string @@ -64,6 +68,9 @@ export class WorkspaceProject { testFilesList: string[] = [] + private _globalSetupInit = false + private _globalSetups: GlobalSetupFile[] = [] + constructor( public path: string | number, public ctx: Vitest, @@ -77,6 +84,50 @@ export class WorkspaceProject { return this.ctx.getCoreWorkspaceProject() === this } + async initializeGlobalSetup() { + if (this._globalSetupInit) + return + + this._globalSetupInit = true + + this._globalSetups = await loadGlobalSetupFiles(this) + + try { + for (const globalSetupFile of this._globalSetups) { + const teardown = await globalSetupFile.setup?.() + if (teardown == null || !!globalSetupFile.teardown) + continue + if (typeof teardown !== 'function') + throw new Error(`invalid return value in globalSetup file ${globalSetupFile.file}. Must return a function`) + globalSetupFile.teardown = teardown + } + } + catch (e) { + this.logger.error(`\n${c.red(divider(c.bold(c.inverse(' Error during global setup '))))}`) + await this.logger.printError(e) + process.exit(1) + } + } + + async teardownGlobalSetup() { + if (!this._globalSetupInit || !this._globalSetups.length) + return + for (const globalSetupFile of this._globalSetups.reverse()) { + try { + await globalSetupFile.teardown?.() + } + catch (error) { + this.logger.error(`error during global teardown of ${globalSetupFile.file}`, error) + await this.logger.printError(error) + process.exitCode = 1 + } + } + } + + get logger() { + return this.ctx.logger + } + // it's possible that file path was imported with different queries (?raw, ?url, etc) getModulesByFilepath(file: string) { const set = this.server.moduleGraph.getModulesByFile(file) @@ -337,6 +388,7 @@ export class WorkspaceProject { this.server.close(), this.typechecker?.stop(), this.browser?.close(), + this.teardownGlobalSetup(), ].filter(Boolean)) } return this.closingPromise From dad3d73dbf1c40b3db3862e024193973e632d757 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 2 Oct 2023 15:35:03 +0200 Subject: [PATCH 2/3] test: fix failing test --- test/global-setup-fail/fixtures/example.test.ts | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 test/global-setup-fail/fixtures/example.test.ts diff --git a/test/global-setup-fail/fixtures/example.test.ts b/test/global-setup-fail/fixtures/example.test.ts new file mode 100644 index 000000000000..f71db97810eb --- /dev/null +++ b/test/global-setup-fail/fixtures/example.test.ts @@ -0,0 +1,5 @@ +import { expect, test } from 'vitest' + +test('example test', () => { + expect(1 + 1).toBe(2) +}) From e9617a468a949b7521daae27ea51cbda064955c5 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 2 Oct 2023 15:38:23 +0200 Subject: [PATCH 3/3] chore: cleanup --- docs/config/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/config/index.md b/docs/config/index.md index 9ff794c8c767..797d1d09c8d5 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -924,7 +924,7 @@ globalThis.resetBeforeEachTest = true - **Type:** `string | string[]` -Path to global setup files, relative to project root +Path to global setup files, relative to project root. A global setup file can either export named functions `setup` and `teardown` or a `default` function that returns a teardown function ([example](https://github.com/vitest-dev/vitest/blob/main/test/global-setup/vitest.config.ts)). @@ -933,7 +933,7 @@ Multiple globalSetup files are possible. setup and teardown are executed sequent ::: ::: warning -Beware that the global setup is run in a different global scope, so your tests don't have access to variables defined here. +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). :::