Skip to content

Commit

Permalink
feat: allow hooks to be executed in a stack or list (#2294)
Browse files Browse the repository at this point in the history
* feat: allow hooks to be executed in a stack or list

* chore: update test
  • Loading branch information
sheremet-va committed Nov 8, 2022
1 parent f0b048b commit c386bce
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 11 deletions.
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', () => {
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])
})
})

0 comments on commit c386bce

Please sign in to comment.