Skip to content

Commit 6c5a447

Browse files
authored
fix: run hook cleanups when a later hook fails (#11605)
1 parent 9697b38 commit 6c5a447

3 files changed

Lines changed: 348 additions & 14 deletions

File tree

‎packages/vitest/src/runtime/runner/run.ts‎

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -178,15 +178,15 @@ async function callSuiteHook<T extends keyof SuiteHooks>(
178178
name: T,
179179
runner: VitestRunner,
180180
args: SuiteHooks[T][0] extends (...args: infer A) => Awaitable<any> ? A : never,
181-
): Promise<unknown[]> {
181+
cleanups: unknown[] = [],
182+
): Promise<void> {
182183
const sequence = runner.config.sequence.hooks
183184

184-
const callbacks: unknown[] = []
185185
// stop at file level
186186
const parentSuite: Suite | null = 'filepath' in suite ? null : suite.suite || suite.file
187187

188188
if (name === 'beforeEach' && parentSuite) {
189-
callbacks.push(...(await callSuiteHook(parentSuite, currentTask, name, runner, args)))
189+
await callSuiteHook(parentSuite, currentTask, name, runner, args, cleanups)
190190
}
191191

192192
const hooks = getSuiteHooks(suite, name, sequence)
@@ -206,10 +206,25 @@ async function callSuiteHook<T extends keyof SuiteHooks>(
206206
}
207207

208208
if (sequence === 'parallel') {
209-
callbacks.push(...(await Promise.all(hooks.map((hook) => runHook(hook)))))
209+
const results = await Promise.allSettled(hooks.map((hook) => runHook(hook)))
210+
// hooks can reject with the same `signal.reason`
211+
const errors = new Set<unknown>()
212+
for (const result of results) {
213+
if (result.status === 'fulfilled') {
214+
cleanups.push(result.value)
215+
} else {
216+
errors.add(result.reason)
217+
}
218+
}
219+
if (errors.size === 1) {
220+
throw [...errors][0]
221+
}
222+
if (errors.size > 1) {
223+
throw new AggregateError([...errors], `Multiple "${name}" hooks failed.`)
224+
}
210225
} else {
211226
for (const hook of hooks) {
212-
callbacks.push(await runHook(hook))
227+
cleanups.push(await runHook(hook))
213228
}
214229
}
215230

@@ -218,10 +233,8 @@ async function callSuiteHook<T extends keyof SuiteHooks>(
218233
}
219234

220235
if (name === 'afterEach' && parentSuite) {
221-
callbacks.push(...(await callSuiteHook(parentSuite, currentTask, name, runner, args)))
236+
await callSuiteHook(parentSuite, currentTask, name, runner, args)
222237
}
223-
224-
return callbacks
225238
}
226239

227240
function getAroundEachHooks(suite: Suite): AroundEachListener[] {
@@ -603,7 +616,7 @@ async function runTest(test: Test, runner: VitestRunner): Promise<void> {
603616
const retry = getRetryCount(test.retry)
604617
for (let retryCount = 0; retryCount <= retry; retryCount++) {
605618
const attemptErrorsStart = test.result.errors?.length ?? 0
606-
let beforeEachCleanups: unknown[] = []
619+
const beforeEachCleanups: unknown[] = []
607620
// fixtureCheckpoint is passed by callAroundEachHooks - it represents the count
608621
// of fixture cleanup functions AFTER all aroundEach fixtures have been resolved
609622
// but BEFORE the test runs. This allows us to clean up only fixtures created
@@ -617,8 +630,15 @@ async function runTest(test: Test, runner: VitestRunner): Promise<void> {
617630

618631
test.result!.repeatCount = repeatCount
619632

620-
beforeEachCleanups = await $('test.beforeEach', () =>
621-
callSuiteHook(suite, test, 'beforeEach', runner, [test.context, suite]),
633+
await $('test.beforeEach', () =>
634+
callSuiteHook(
635+
suite,
636+
test,
637+
'beforeEach',
638+
runner,
639+
[test.context, suite],
640+
beforeEachCleanups,
641+
),
622642
)
623643

624644
if (runner.runTask) {
@@ -840,7 +860,7 @@ async function runSuite(suite: Suite, runner: VitestRunner): Promise<void> {
840860

841861
updateTask('suite-prepare', suite, runner)
842862

843-
let beforeAllCleanups: unknown[] = []
863+
const beforeAllCleanups: unknown[] = []
844864

845865
if (suite.mode === 'skip') {
846866
suite.result.state = 'skip'
@@ -859,8 +879,8 @@ async function runSuite(suite: Suite, runner: VitestRunner): Promise<void> {
859879
try {
860880
// beforeAll
861881
try {
862-
beforeAllCleanups = await $('suite.beforeAll', () =>
863-
callSuiteHook(suite, suite, 'beforeAll', runner, [suite]),
882+
await $('suite.beforeAll', () =>
883+
callSuiteHook(suite, suite, 'beforeAll', runner, [suite], beforeAllCleanups),
864884
)
865885
} catch (e) {
866886
failTask(suite.result!, e, runner.config._diffOptions)

‎test/e2e/test/concurrent.test.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,8 @@ test('beforeEach deadlocks with insufficient maxConcurrency', async () => {
440440
"wrapper": {
441441
"t": [
442442
"Hook timed out in 500ms.
443+
If this is a long-running hook, pass a timeout value as the last argument or configure it globally with "hookTimeout".",
444+
"Hook timed out in 500ms.
443445
If this is a long-running hook, pass a timeout value as the last argument or configure it globally with "hookTimeout".",
444446
],
445447
},
@@ -522,6 +524,8 @@ test('afterEach deadlocks with insufficient maxConcurrency', async () => {
522524
"wrapper": {
523525
"t": [
524526
"Hook timed out in 500ms.
527+
If this is a long-running hook, pass a timeout value as the last argument or configure it globally with "hookTimeout".",
528+
"Hook timed out in 500ms.
525529
If this is a long-running hook, pass a timeout value as the last argument or configure it globally with "hookTimeout".",
526530
],
527531
},

0 commit comments

Comments
 (0)