diff --git a/src/index.ts b/src/index.ts index b95718294..83e4e8d97 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,8 @@ import type { MatchersObject } from './integrations/chai/types' import type { UserOptions } from './types' export * from './types' -export * from './runtime/suite' +export { suite, test, describe, it } from './runtime/suite' +export * from './runtime/hooks' export * from './integrations/chai' export * from './integrations/sinon' diff --git a/src/node/init.ts b/src/node/init.ts index 9ba4e060f..44b8c8b08 100644 --- a/src/node/init.ts +++ b/src/node/init.ts @@ -78,6 +78,9 @@ export async function initViteServer(options: CliOptions = {}) { if (process.env.VITEST_MIN_THREADS) resolved.minThreads = parseInt(process.env.VITEST_MIN_THREADS) + resolved.setupFiles = Array.from(resolved.setupFiles || []) + .map(i => resolve(root, i)) + return { server, config: resolved, diff --git a/src/runtime/collect.ts b/src/runtime/collect.ts index 5ed7cbccb..7e872ab8b 100644 --- a/src/runtime/collect.ts +++ b/src/runtime/collect.ts @@ -1,14 +1,15 @@ import { basename } from 'path' import { performance } from 'perf_hooks' import { nanoid } from 'nanoid/non-secure' -import type { File, Suite, Test } from '../types' +import type { ResolvedConfig, File, Suite, Test } from '../types' import { interpretOnlyMode } from '../utils' import { clearContext, createSuiteHooks, defaultSuite } from './suite' -import { context } from './context' import { setHooks } from './map' import { processError } from './error' +import { context } from './context' +import { runSetupFiles } from './setup' -export async function collectTests(paths: string[]) { +export async function collectTests(paths: string[], config: ResolvedConfig) { const files: File[] = [] for (const filepath of paths) { @@ -26,6 +27,7 @@ export async function collectTests(paths: string[]) { clearContext() try { + await runSetupFiles(config) await import(filepath) for (const c of [defaultSuite, ...context.tasks]) { diff --git a/src/runtime/context.ts b/src/runtime/context.ts index 5f9d9822b..8b6dc67db 100644 --- a/src/runtime/context.ts +++ b/src/runtime/context.ts @@ -1,6 +1,41 @@ -import type { GlobalContext } from '../types' +import type { Awaitable, RuntimeContext, SuiteCollector } from '../types' -export const context: GlobalContext = { +export const context: RuntimeContext = { tasks: [], currentSuite: null, } + +export function collectTask(task: SuiteCollector) { + context.currentSuite?.tasks.push(task) +} + +export async function runWithSuite(suite: SuiteCollector, fn: (() => Awaitable)) { + const prev = context.currentSuite + context.currentSuite = suite + await fn() + context.currentSuite = prev +} + +export function getDefaultTestTimeout() { + return process.__vitest_worker__?.config?.testTimeout ?? 5000 +} + +export function getDefaultHookTimeout() { + return process.__vitest_worker__?.config?.hookTimeout ?? 5000 +} + +export function withTimeout any)>(fn: T, _timeout?: number): T { + const timeout = _timeout ?? getDefaultTestTimeout() + if (timeout <= 0 || timeout === Infinity) + return fn + + return ((...args: (T extends ((...args: infer A) => any) ? A : never)) => { + return Promise.race([fn(...args), new Promise((resolve, reject) => { + const timer = setTimeout(() => { + clearTimeout(timer) + reject(new Error(`Test timed out in ${timeout}ms.`)) + }, timeout) + timer.unref() + })]) as Awaitable + }) as T +} diff --git a/src/runtime/entry.ts b/src/runtime/entry.ts index e1858e3f7..caad79c17 100644 --- a/src/runtime/entry.ts +++ b/src/runtime/entry.ts @@ -1,11 +1,11 @@ import type { ResolvedConfig } from '../types' -import { setupGlobalEnv, withEnv } from './env' +import { setupGlobalEnv, withEnv } from './setup' import { startTests } from './run' export async function run(files: string[], config: ResolvedConfig): Promise { await setupGlobalEnv(config) await withEnv(config.environment, async() => { - await startTests(files) + await startTests(files, config) }) } diff --git a/src/runtime/hooks.ts b/src/runtime/hooks.ts new file mode 100644 index 000000000..f106ed28f --- /dev/null +++ b/src/runtime/hooks.ts @@ -0,0 +1,9 @@ +import type { SuiteHooks } from '../types' +import { getDefaultHookTimeout, withTimeout } from './context' +import { getCurrentSuite } from './suite' + +// suite hooks +export const beforeAll = (fn: SuiteHooks['beforeAll'][0], timeout?: number) => getCurrentSuite().on('beforeAll', withTimeout(fn, timeout ?? getDefaultHookTimeout())) +export const afterAll = (fn: SuiteHooks['afterAll'][0], timeout?: number) => getCurrentSuite().on('afterAll', withTimeout(fn, timeout ?? getDefaultHookTimeout())) +export const beforeEach = (fn: SuiteHooks['beforeEach'][0], timeout?: number) => getCurrentSuite().on('beforeEach', withTimeout(fn, timeout ?? getDefaultHookTimeout())) +export const afterEach = (fn: SuiteHooks['afterEach'][0], timeout?: number) => getCurrentSuite().on('afterEach', withTimeout(fn, timeout ?? getDefaultHookTimeout())) diff --git a/src/runtime/run.ts b/src/runtime/run.ts index 9d549bb86..40e83b8b7 100644 --- a/src/runtime/run.ts +++ b/src/runtime/run.ts @@ -1,6 +1,6 @@ import { performance } from 'perf_hooks' import type { HookListener } from 'vitest' -import type { Test, Suite, SuiteHooks, Task } from '../types' +import type { ResolvedConfig, Test, Suite, SuiteHooks, Task } from '../types' import { getSnapshotClient } from '../integrations/snapshot/chai' import { hasFailed, hasTests, partitionSuiteChildren } from '../utils' import { getFn, getHooks } from './map' @@ -8,7 +8,7 @@ import { rpc, send } from './rpc' import { collectTests } from './collect' import { processError } from './error' -async function callHook(suite: Suite, name: T, args: SuiteHooks[T][0] extends HookListener ? A : never) { +export async function callSuiteHook(suite: Suite, name: T, args: SuiteHooks[T][0] extends HookListener ? A : never) { await Promise.all(getHooks(suite)[name].map(fn => fn(...(args as any)))) } @@ -31,7 +31,7 @@ export async function runTest(test: Test) { process.__vitest_worker__.current = test try { - await callHook(test.suite, 'beforeEach', [test, test.suite]) + await callSuiteHook(test.suite, 'beforeEach', [test, test.suite]) await getFn(test)() test.result.state = 'pass' } @@ -40,7 +40,7 @@ export async function runTest(test: Test) { test.result.error = processError(e) } try { - await callHook(test.suite, 'afterEach', [test, test.suite]) + await callSuiteHook(test.suite, 'afterEach', [test, test.suite]) } catch (e) { test.result.state = 'fail' @@ -75,7 +75,7 @@ export async function runSuite(suite: Suite) { } else { try { - await callHook(suite, 'beforeAll', [suite]) + await callSuiteHook(suite, 'beforeAll', [suite]) for (const tasksGroup of partitionSuiteChildren(suite)) { const computeMode = tasksGroup[0].computeMode @@ -88,7 +88,7 @@ export async function runSuite(suite: Suite) { } } - await callHook(suite, 'afterAll', [suite]) + await callSuiteHook(suite, 'afterAll', [suite]) } catch (e) { suite.result.state = 'fail' @@ -124,8 +124,8 @@ export async function runSuites(suites: Suite[]) { await runSuite(suite) } -export async function startTests(paths: string[]) { - const files = await collectTests(paths) +export async function startTests(paths: string[], config: ResolvedConfig) { + const files = await collectTests(paths, config) send('onCollected', files) diff --git a/src/runtime/env.ts b/src/runtime/setup.ts similarity index 82% rename from src/runtime/env.ts rename to src/runtime/setup.ts index 7d377da5a..60c59acd0 100644 --- a/src/runtime/env.ts +++ b/src/runtime/setup.ts @@ -3,6 +3,7 @@ import { Writable } from 'stream' import { environments } from '../env' import { setupChai } from '../integrations/chai/setup' import type { ResolvedConfig } from '../types' +import { toArray } from '../utils' import { send } from './rpc' let globalSetup = false @@ -59,3 +60,13 @@ export async function withEnv(name: ResolvedConfig['environment'], fn: () => Pro await env.teardown(globalThis) } } + +export async function runSetupFiles(config: ResolvedConfig) { + const files = toArray(config.setupFiles) + await Promise.all( + files.map(async(file) => { + process.__vitest_worker__.moduleCache.delete(file) + await import(file) + }), + ) +} diff --git a/src/runtime/suite.ts b/src/runtime/suite.ts index a72c3f0fb..34e5a3abe 100644 --- a/src/runtime/suite.ts +++ b/src/runtime/suite.ts @@ -1,24 +1,22 @@ import { nanoid } from 'nanoid/non-secure' -import type { SuiteHooks, Test, SuiteCollector, TestCollector, RunMode, ComputeMode, TestFactory, TestFunction, File, Suite, Awaitable, ResolvedConfig, RpcCall, RpcSend } from '../types' -import { context } from './context' +import type { SuiteHooks, Test, SuiteCollector, TestCollector, RunMode, ComputeMode, TestFactory, TestFunction, File, Suite, ResolvedConfig, RpcCall, RpcSend, ModuleCache } from '../types' +import { collectTask, context, runWithSuite, withTimeout } from './context' import { getHooks, setFn, setHooks } from './map' export const suite = createSuite() export const defaultSuite = suite('') -function getCurrentSuite() { +export function clearContext() { + context.tasks.length = 0 + defaultSuite.clear() + context.currentSuite = defaultSuite +} + +export function getCurrentSuite() { return context.currentSuite || defaultSuite } -const getDefaultTestTimeout = () => { - return process.__vitest_worker__?.config?.testTimeout ?? 5000 -} - -const getDefaultHookTimeout = () => { - return process.__vitest_worker__?.config?.hookTimeout ?? 5000 -} - export function createSuiteHooks() { return { beforeAll: [], @@ -84,12 +82,8 @@ function createSuiteCollector(name: string, factory: TestFactory = () => { }, mo async function collect(file?: File) { factoryQueue.length = 0 - if (factory) { - const prev = context.currentSuite - context.currentSuite = collector - await factory(test) - context.currentSuite = prev - } + if (factory) + await runWithSuite(collector, () => factory(test)) const allChildren = await Promise.all( [...factoryQueue, ...tasks] @@ -108,7 +102,7 @@ function createSuiteCollector(name: string, factory: TestFactory = () => { }, mo return suite } - context.currentSuite?.tasks.push(collector) + collectTask(collector) return collector } @@ -144,7 +138,6 @@ function createTestCollector(collectTest: (name: string, fn: TestFunction, mode: } // apis - export const test = (function() { function test(name: string, fn: TestFunction, timeout?: number) { return getCurrentSuite().test(name, fn, timeout) @@ -215,35 +208,6 @@ function createSuite() { export const describe = suite export const it = test -// hooks -export const beforeAll = (fn: SuiteHooks['beforeAll'][0], timeout?: number) => getCurrentSuite().on('beforeAll', withTimeout(fn, timeout ?? getDefaultHookTimeout())) -export const afterAll = (fn: SuiteHooks['afterAll'][0], timeout?: number) => getCurrentSuite().on('afterAll', withTimeout(fn, timeout ?? getDefaultHookTimeout())) -export const beforeEach = (fn: SuiteHooks['beforeEach'][0], timeout?: number) => getCurrentSuite().on('beforeEach', withTimeout(fn, timeout ?? getDefaultHookTimeout())) -export const afterEach = (fn: SuiteHooks['afterEach'][0], timeout?: number) => getCurrentSuite().on('afterEach', withTimeout(fn, timeout ?? getDefaultHookTimeout())) - -// utils -export function clearContext() { - context.tasks.length = 0 - defaultSuite.clear() - context.currentSuite = defaultSuite -} - -function withTimeout any)>(fn: T, _timeout?: number): T { - const timeout = _timeout ?? getDefaultTestTimeout() - if (timeout <= 0 || timeout === Infinity) - return fn - - return ((...args: (T extends ((...args: infer A) => any) ? A : never)) => { - return Promise.race([fn(...args), new Promise((resolve, reject) => { - const timer = setTimeout(() => { - clearTimeout(timer) - reject(new Error(`Test timed out in ${timeout}ms.`)) - }, timeout) - timer.unref() - })]) as Awaitable - }) as T -} - declare global { namespace NodeJS { interface Process { @@ -252,6 +216,7 @@ declare global { rpc: RpcCall send: RpcSend current?: Test + moduleCache: Map } } } diff --git a/src/runtime/worker.ts b/src/runtime/worker.ts index 427354a87..98d0a88bc 100644 --- a/src/runtime/worker.ts +++ b/src/runtime/worker.ts @@ -1,12 +1,11 @@ import { resolve } from 'path' import { nanoid } from 'nanoid/non-secure' -import type { WorkerContext, ResolvedConfig } from '../types' +import type { WorkerContext, ResolvedConfig, ModuleCache } from '../types' import { distDir } from '../constants' -import type { ExecuteOptions } from '../node/execute' import { executeInViteNode } from '../node/execute' let _run: (files: string[], config: ResolvedConfig) => Promise -const moduleCache: ExecuteOptions['moduleCache'] = new Map() +const moduleCache: Map = new Map() export async function init(ctx: WorkerContext) { if (_run) @@ -38,6 +37,7 @@ export default async function run(ctx: WorkerContext) { const rpcPromiseMap = new Map any); reject: (...args: any) => any }>() process.__vitest_worker__ = { + moduleCache, config, rpc: (method, ...args) => { return new Promise((resolve, reject) => { diff --git a/src/types/options.ts b/src/types/options.ts index 600c4f37a..85279258f 100644 --- a/src/types/options.ts +++ b/src/types/options.ts @@ -120,11 +120,15 @@ export interface UserOptions { /** * Silent mode - * TODO: implement this * * @default false */ silent?: boolean + + /** + * Path to setup files + */ + setupFiles?: string | string[] } export interface CliOptions extends UserOptions { diff --git a/src/types/tasks.ts b/src/types/tasks.ts index b22446dd0..91a35327e 100644 --- a/src/types/tasks.ts +++ b/src/types/tasks.ts @@ -91,7 +91,7 @@ export interface SuiteCollector { export type TestFactory = (test: (name: string, fn: TestFunction) => void) => Awaitable -export interface GlobalContext { +export interface RuntimeContext { tasks: (SuiteCollector | Test)[] currentSuite: SuiteCollector | null } diff --git a/test/core/test/setup.ts b/test/core/test/setup.ts new file mode 100644 index 000000000..4db67978f --- /dev/null +++ b/test/core/test/setup.ts @@ -0,0 +1,5 @@ +import { beforeEach } from 'vitest' + +beforeEach(() => { + // console.log(`hi ${s.name}`) +}) diff --git a/test/core/vitest.config.ts b/test/core/vitest.config.ts index 67b95ffa5..6864af400 100644 --- a/test/core/vitest.config.ts +++ b/test/core/vitest.config.ts @@ -4,5 +4,8 @@ export default defineConfig({ test: { testTimeout: 1000, // threads: false, + setupFiles: [ + './test/setup.ts', + ], }, })