Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow hooks to be executed in a stack or list #2294

Merged
merged 2 commits into from Nov 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 12 additions & 1 deletion docs/config/index.md
Expand Up @@ -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.

Expand Down Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion packages/vitest/src/node/config.ts
Expand Up @@ -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,
Expand Down
34 changes: 26 additions & 8 deletions 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'
Expand Down Expand Up @@ -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<T extends keyof SuiteHooks>(
suite: Suite,
currentTask: Task,
Expand All @@ -47,9 +54,20 @@ export async function callSuiteHook<T extends keyof SuiteHooks>(
}

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) {
Expand Down Expand Up @@ -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()
}))
}
Expand Down Expand Up @@ -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,
Expand All @@ -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)()
Expand Down
12 changes: 11 additions & 1 deletion packages/vitest/src/types/config.ts
Expand Up @@ -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<never, never>)
export type CSSModuleScopeStrategy = 'stable' | 'scoped' | 'non-scoped'
export type SequenceHooks = 'stack' | 'list' | 'parallel'

export type ApiConfig = Pick<CommonServerOptions, 'port' | 'strictPort' | 'host'>

Expand Down Expand Up @@ -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
}

/**
Expand Down Expand Up @@ -552,6 +561,7 @@ export interface ResolvedConfig extends Omit<Required<UserConfig>, 'config' | 'f

sequence: {
sequencer: TestSequencerConstructor
hooks: SequenceHooks
shuffle?: boolean
seed?: number
}
Expand All @@ -569,4 +579,4 @@ export type RuntimeConfig = Pick<
| 'restoreMocks'
| 'fakeTimers'
| 'maxConcurrency'
>
> & { sequence?: { hooks?: SequenceHooks } }
47 changes: 47 additions & 0 deletions 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])
})
})
47 changes: 47 additions & 0 deletions 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])
})
})
43 changes: 43 additions & 0 deletions test/core/test/hooks-stack.test.ts
@@ -0,0 +1,43 @@
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', () => {
sheremet-va marked this conversation as resolved.
Show resolved Hide resolved
callHook('beforeAll', 1)
callHook('afterAll', 4)

callHook('beforeAll', 2)
// will wait for it
vitest.afterAll(async () => {
await Promise.resolve()
hookOrder.push(5)
})

callHook('beforeEach', 7)
callHook('afterEach', 10)

callHook('beforeEach', 8)
callHook('afterEach', 11)

test('before hooks pushed in order', () => {
expect(hookOrder).toEqual([1, 2, 7, 8])
})
})

describe('previous suite run all hooks', () => {
test('after all hooks run in reverse order', () => {
expect(hookOrder).toEqual([1, 2, 7, 8, 11, 10, 5, 4])
})
})