From 0030fc2ad4cc059e01cf092dad3774f222007bf9 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 8 Nov 2022 14:00:01 +0100 Subject: [PATCH] feat: allow hooks to be executed in a stack or list --- docs/config/index.md | 13 +++++++- packages/vitest/src/node/config.ts | 3 +- packages/vitest/src/runtime/run.ts | 34 ++++++++++++++----- packages/vitest/src/types/config.ts | 12 ++++++- test/core/test/hooks-list.test.ts | 47 +++++++++++++++++++++++++++ test/core/test/hooks-parallel.test.ts | 47 +++++++++++++++++++++++++++ test/core/test/hooks-stack.test.ts | 47 +++++++++++++++++++++++++++ 7 files changed, 192 insertions(+), 11 deletions(-) create mode 100644 test/core/test/hooks-list.test.ts create mode 100644 test/core/test/hooks-parallel.test.ts create mode 100644 test/core/test/hooks-stack.test.ts diff --git a/docs/config/index.md b/docs/config/index.md index 473ac1d51af7..f02cdc6d7475 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -821,7 +821,7 @@ Path to cache directory. ### sequence -- **Type**: `{ sequencer?, shuffle?, seed? }` +- **Type**: `{ sequencer?, shuffle?, seed?, hooks? }` Options for how tests should be sorted. @@ -850,6 +850,17 @@ Vitest usually uses cache to sort tests, so long running tests start earlier - t Sets the randomization seed, if tests are running in random order. +#### sequence.hooks + +- **Type**: `'stack' | 'list' | 'parallel'` +- **Default**: `'parallel'` + +Changes the order in which hooks are executed. + +- `stack` will order "after" hooks in reverse order, "before" hooks will run in the order they were defined +- `list` will order all hooks in the order they are defined +- `parallel` will run hooks in a single group in parallel (hooks in parent suites will still run before the current suite's hooks) + ### typecheck Options for configuring [typechecking](/guide/testing-types) test environment. diff --git a/packages/vitest/src/node/config.ts b/packages/vitest/src/node/config.ts index c12c7ad29dcb..cc57f4153a37 100644 --- a/packages/vitest/src/node/config.ts +++ b/packages/vitest/src/node/config.ts @@ -221,13 +221,14 @@ export function resolveConfig( if (resolved.cache) resolved.cache.dir = VitestCache.resolveCacheDir(resolved.root, resolved.cache.dir) + resolved.sequence ??= {} as any if (!resolved.sequence?.sequencer) { - resolved.sequence ??= {} as any // CLI flag has higher priority resolved.sequence.sequencer = resolved.sequence.shuffle ? RandomSequencer : BaseSequencer } + resolved.sequence.hooks ??= 'parallel' resolved.typecheck = { ...configDefaults.typecheck, diff --git a/packages/vitest/src/runtime/run.ts b/packages/vitest/src/runtime/run.ts index cec0882ca7e2..799f7b48a551 100644 --- a/packages/vitest/src/runtime/run.ts +++ b/packages/vitest/src/runtime/run.ts @@ -1,8 +1,8 @@ import { performance } from 'perf_hooks' import limit from 'p-limit' -import type { BenchTask, Benchmark, BenchmarkResult, File, HookCleanupCallback, HookListener, ResolvedConfig, Suite, SuiteHooks, Task, TaskResult, TaskState, Test } from '../types' +import type { BenchTask, Benchmark, BenchmarkResult, File, HookCleanupCallback, HookListener, ResolvedConfig, SequenceHooks, Suite, SuiteHooks, Task, TaskResult, TaskState, Test } from '../types' import { vi } from '../integrations/vi' -import { assertTypes, clearTimeout, createDefer, getFullName, getWorkerState, hasFailed, hasTests, isBrowser, isNode, isRunningInBenchmark, partitionSuiteChildren, setTimeout, shuffle } from '../utils' +import { clearTimeout, createDefer, getFullName, getWorkerState, hasFailed, hasTests, isBrowser, isNode, isRunningInBenchmark, partitionSuiteChildren, setTimeout, shuffle } from '../utils' import { getState, setState } from '../integrations/chai/jest-expect' import { GLOBAL_EXPECT } from '../integrations/chai/constants' import { takeCoverageInsideWorker } from '../integrations/coverage' @@ -33,6 +33,13 @@ function updateSuiteHookState(suite: Task, name: keyof SuiteHooks, state: TaskSt } } +function getSuiteHooks(suite: Suite, name: keyof SuiteHooks, sequence: SequenceHooks) { + const hooks = getHooks(suite)[name] + if (sequence === 'stack' && (name === 'afterAll' || name === 'afterEach')) + return hooks.slice().reverse() + return hooks +} + export async function callSuiteHook( suite: Suite, currentTask: Task, @@ -47,9 +54,20 @@ export async function callSuiteHook( } updateSuiteHookState(currentTask, name, 'run') - callbacks.push( - ...await Promise.all(getHooks(suite)[name].map(fn => fn(...(args as any)))), - ) + + const state = getWorkerState() + const sequence = state.config.sequence.hooks + + const hooks = getSuiteHooks(suite, name, sequence) + + if (sequence === 'parallel') { + callbacks.push(...await Promise.all(hooks.map(fn => fn(...args as any)))) + } + else { + for (const hook of hooks) + callbacks.push(await hook(...args as any)) + } + updateSuiteHookState(currentTask, name, 'pass') if (name === 'afterEach' && suite.suite) { @@ -87,9 +105,8 @@ async function sendTasksUpdate() { const callCleanupHooks = async (cleanups: HookCleanupCallback[]) => { await Promise.all(cleanups.map(async (fn) => { - if (!fn) + if (typeof fn !== 'function') return - assertTypes(fn, 'hook teardown', ['function']) await fn() })) } @@ -131,7 +148,6 @@ export async function runTest(test: Test) { for (let retryCount = 0; retryCount < retry; retryCount++) { let beforeEachCleanups: HookCleanupCallback[] = [] try { - beforeEachCleanups = await callSuiteHook(test.suite, test, 'beforeEach', [test.context, test.suite]) setState({ assertionCalls: 0, isExpectingAssertions: false, @@ -142,6 +158,8 @@ export async function runTest(test: Test) { currentTestName: getFullName(test), }, (globalThis as any)[GLOBAL_EXPECT]) + beforeEachCleanups = await callSuiteHook(test.suite, test, 'beforeEach', [test.context, test.suite]) + test.result.retryCount = retryCount await getFn(test)() diff --git a/packages/vitest/src/types/config.ts b/packages/vitest/src/types/config.ts index 256061aaea1b..05c8595f24da 100644 --- a/packages/vitest/src/types/config.ts +++ b/packages/vitest/src/types/config.ts @@ -14,6 +14,7 @@ export type BuiltinEnvironment = 'node' | 'jsdom' | 'happy-dom' | 'edge-runtime' // Record is used, so user can get intellisense for builtin environments, but still allow custom environments export type VitestEnvironment = BuiltinEnvironment | (string & Record) export type CSSModuleScopeStrategy = 'stable' | 'scoped' | 'non-scoped' +export type SequenceHooks = 'stack' | 'list' | 'parallel' export type ApiConfig = Pick @@ -430,6 +431,14 @@ export interface InlineConfig { * @default Date.now() */ seed?: number + /** + * Defines how hooks should be ordered + * - `stack` will order "after" hooks in reverse order, "before" hooks will run sequentially + * - `list` will order hooks in the order they are defined + * - `parallel` will run hooks in a single group in parallel + * @default 'parallel' + */ + hooks?: SequenceHooks } /** @@ -552,6 +561,7 @@ export interface ResolvedConfig extends Omit, 'config' | 'f sequence: { sequencer: TestSequencerConstructor + hooks: SequenceHooks shuffle?: boolean seed?: number } @@ -569,4 +579,4 @@ export type RuntimeConfig = Pick< | 'restoreMocks' | 'fakeTimers' | 'maxConcurrency' -> +> & { sequence?: { hooks?: SequenceHooks } } diff --git a/test/core/test/hooks-list.test.ts b/test/core/test/hooks-list.test.ts new file mode 100644 index 000000000000..d503336d033d --- /dev/null +++ b/test/core/test/hooks-list.test.ts @@ -0,0 +1,47 @@ +import * as vitest from 'vitest' +import { describe, expect, test, vi } from 'vitest' + +vi.setConfig({ + sequence: { + hooks: 'list', + }, +}) + +const hookOrder: number[] = [] +function callHook(hook: 'beforeAll' | 'beforeEach' | 'afterAll' | 'afterEach', order: number) { + vitest[hook](() => { + hookOrder.push(order) + }) +} + +describe('hooks are called as list', () => { + callHook('beforeAll', 1) + callHook('beforeAll', 2) + callHook('beforeAll', 3) + + callHook('afterAll', 4) + // will wait for it + vitest.afterAll(async () => { + await Promise.resolve() + hookOrder.push(5) + }) + callHook('afterAll', 6) + + callHook('beforeEach', 7) + callHook('beforeEach', 8) + callHook('beforeEach', 9) + + callHook('afterEach', 10) + callHook('afterEach', 11) + callHook('afterEach', 12) + + test('before hooks pushed in order', () => { + expect(hookOrder).toEqual([1, 2, 3, 7, 8, 9]) + }) +}) + +describe('previous suite run all hooks', () => { + test('after all hooks run in defined order', () => { + expect(hookOrder).toEqual([1, 2, 3, 7, 8, 9, 10, 11, 12, 4, 5, 6]) + }) +}) diff --git a/test/core/test/hooks-parallel.test.ts b/test/core/test/hooks-parallel.test.ts new file mode 100644 index 000000000000..f688ee9b0244 --- /dev/null +++ b/test/core/test/hooks-parallel.test.ts @@ -0,0 +1,47 @@ +import * as vitest from 'vitest' +import { describe, expect, test, vi } from 'vitest' + +vi.setConfig({ + sequence: { + hooks: 'parallel', + }, +}) + +const hookOrder: number[] = [] +function callHook(hook: 'beforeAll' | 'beforeEach' | 'afterAll' | 'afterEach', order: number) { + vitest[hook](() => { + hookOrder.push(order) + }) +} + +describe('hooks are called in parallel', () => { + callHook('beforeAll', 1) + callHook('beforeAll', 2) + callHook('beforeAll', 3) + + callHook('afterAll', 4) + // will always be last + vitest.afterAll(async () => { + await Promise.resolve() + hookOrder.push(5) + }) + callHook('afterAll', 6) + + callHook('beforeEach', 7) + callHook('beforeEach', 8) + callHook('beforeEach', 9) + + callHook('afterEach', 10) + callHook('afterEach', 11) + callHook('afterEach', 12) + + test('before hooks pushed in order', () => { + expect(hookOrder).toEqual([1, 2, 3, 7, 8, 9]) + }) +}) + +describe('previous suite run all hooks', () => { + test('after all hooks run in defined order', () => { + expect(hookOrder).toEqual([1, 2, 3, 7, 8, 9, 10, 11, 12, 4, 6, 5]) + }) +}) diff --git a/test/core/test/hooks-stack.test.ts b/test/core/test/hooks-stack.test.ts new file mode 100644 index 000000000000..7f6f7f972393 --- /dev/null +++ b/test/core/test/hooks-stack.test.ts @@ -0,0 +1,47 @@ +import * as vitest from 'vitest' +import { describe, expect, test, vi } from 'vitest' + +vi.setConfig({ + sequence: { + hooks: 'stack', + }, +}) + +const hookOrder: number[] = [] +function callHook(hook: 'beforeAll' | 'beforeEach' | 'afterAll' | 'afterEach', order: number) { + vitest[hook](() => { + hookOrder.push(order) + }) +} + +describe('hooks are called sequentially', () => { + callHook('beforeAll', 1) + callHook('beforeAll', 2) + callHook('beforeAll', 3) + + callHook('afterAll', 4) + // will wait for it + vitest.afterAll(async () => { + await Promise.resolve() + hookOrder.push(5) + }) + callHook('afterAll', 6) + + callHook('beforeEach', 7) + callHook('beforeEach', 8) + callHook('beforeEach', 9) + + callHook('afterEach', 10) + callHook('afterEach', 11) + callHook('afterEach', 12) + + test('before hooks pushed in order', () => { + expect(hookOrder).toEqual([1, 2, 3, 7, 8, 9]) + }) +}) + +describe('previous suite run all hooks', () => { + test('after all hooks run in reverse order', () => { + expect(hookOrder).toEqual([1, 2, 3, 7, 8, 9, 12, 11, 10, 6, 5, 4]) + }) +})