Skip to content

Commit 268a19e

Browse files
authored
fix(runner): show stacktrace on hook timeout error (#7502)
1 parent ff42bcb commit 268a19e

File tree

5 files changed

+256
-14
lines changed

5 files changed

+256
-14
lines changed

packages/runner/src/context.ts

+16-6
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export function withTimeout<T extends (...args: any[]) => any>(
3434
fn: T,
3535
timeout: number,
3636
isHook = false,
37+
stackTraceError?: Error,
3738
): T {
3839
if (timeout <= 0 || timeout === Number.POSITIVE_INFINITY) {
3940
return fn
@@ -47,18 +48,22 @@ export function withTimeout<T extends (...args: any[]) => any>(
4748
return new Promise((resolve_, reject_) => {
4849
const timer = setTimeout(() => {
4950
clearTimeout(timer)
50-
reject(new Error(makeTimeoutMsg(isHook, timeout)))
51+
rejectTimeoutError()
5152
}, timeout)
5253
// `unref` might not exist in browser
5354
timer.unref?.()
5455

56+
function rejectTimeoutError() {
57+
reject_(makeTimeoutError(isHook, timeout, stackTraceError))
58+
}
59+
5560
function resolve(result: unknown) {
5661
clearTimeout(timer)
5762
// if test/hook took too long in microtask, setTimeout won't be triggered,
5863
// but we still need to fail the test, see
5964
// https://github.com/vitest-dev/vitest/issues/2920
6065
if (now() - startTime >= timeout) {
61-
reject_(new Error(makeTimeoutMsg(isHook, timeout)))
66+
rejectTimeoutError()
6267
return
6368
}
6469
resolve_(result)
@@ -108,26 +113,31 @@ export function createTestContext(
108113
context.onTestFailed = (handler, timeout) => {
109114
test.onFailed ||= []
110115
test.onFailed.push(
111-
withTimeout(handler, timeout ?? runner.config.hookTimeout, true),
116+
withTimeout(handler, timeout ?? runner.config.hookTimeout, true, new Error('STACK_TRACE_ERROR')),
112117
)
113118
}
114119

115120
context.onTestFinished = (handler, timeout) => {
116121
test.onFinished ||= []
117122
test.onFinished.push(
118-
withTimeout(handler, timeout ?? runner.config.hookTimeout, true),
123+
withTimeout(handler, timeout ?? runner.config.hookTimeout, true, new Error('STACK_TRACE_ERROR')),
119124
)
120125
}
121126

122127
return runner.extendTaskContext?.(context) || context
123128
}
124129

125-
function makeTimeoutMsg(isHook: boolean, timeout: number) {
126-
return `${
130+
function makeTimeoutError(isHook: boolean, timeout: number, stackTraceError?: Error) {
131+
const message = `${
127132
isHook ? 'Hook' : 'Test'
128133
} timed out in ${timeout}ms.\nIf this is a long-running ${
129134
isHook ? 'hook' : 'test'
130135
}, pass a timeout value as the last argument or configure it globally with "${
131136
isHook ? 'hookTimeout' : 'testTimeout'
132137
}".`
138+
const error = new Error(message)
139+
if (stackTraceError?.stack) {
140+
error.stack = stackTraceError.stack.replace(error.message, stackTraceError.message)
141+
}
142+
return error
133143
}

packages/runner/src/hooks.ts

+54-8
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,19 @@ function getDefaultHookTimeout() {
1919
}
2020

2121
const CLEANUP_TIMEOUT_KEY = Symbol.for('VITEST_CLEANUP_TIMEOUT')
22+
const CLEANUP_STACK_TRACE_KEY = Symbol.for('VITEST_CLEANUP_STACK_TRACE')
2223

2324
export function getBeforeHookCleanupCallback(hook: Function, result: any): Function | undefined {
2425
if (typeof result === 'function') {
2526
const timeout
2627
= CLEANUP_TIMEOUT_KEY in hook && typeof hook[CLEANUP_TIMEOUT_KEY] === 'number'
2728
? hook[CLEANUP_TIMEOUT_KEY]
2829
: getDefaultHookTimeout()
29-
return withTimeout(result, timeout, true)
30+
const stackTraceError
31+
= CLEANUP_STACK_TRACE_KEY in hook && hook[CLEANUP_STACK_TRACE_KEY] instanceof Error
32+
? hook[CLEANUP_STACK_TRACE_KEY]
33+
: undefined
34+
return withTimeout(result, timeout, true, stackTraceError)
3035
}
3136
}
3237

@@ -52,9 +57,21 @@ export function beforeAll(
5257
timeout: number = getDefaultHookTimeout(),
5358
): void {
5459
assertTypes(fn, '"beforeAll" callback', ['function'])
60+
const stackTraceError = new Error('STACK_TRACE_ERROR')
5561
return getCurrentSuite().on(
5662
'beforeAll',
57-
Object.assign(withTimeout(fn, timeout, true), { [CLEANUP_TIMEOUT_KEY]: timeout }),
63+
Object.assign(
64+
withTimeout(
65+
fn,
66+
timeout,
67+
true,
68+
stackTraceError,
69+
),
70+
{
71+
[CLEANUP_TIMEOUT_KEY]: timeout,
72+
[CLEANUP_STACK_TRACE_KEY]: stackTraceError,
73+
},
74+
),
5875
)
5976
}
6077

@@ -79,7 +96,12 @@ export function afterAll(fn: AfterAllListener, timeout?: number): void {
7996
assertTypes(fn, '"afterAll" callback', ['function'])
8097
return getCurrentSuite().on(
8198
'afterAll',
82-
withTimeout(fn, timeout ?? getDefaultHookTimeout(), true),
99+
withTimeout(
100+
fn,
101+
timeout ?? getDefaultHookTimeout(),
102+
true,
103+
new Error('STACK_TRACE_ERROR'),
104+
),
83105
)
84106
}
85107

@@ -105,11 +127,20 @@ export function beforeEach<ExtraContext = object>(
105127
timeout: number = getDefaultHookTimeout(),
106128
): void {
107129
assertTypes(fn, '"beforeEach" callback', ['function'])
130+
const stackTraceError = new Error('STACK_TRACE_ERROR')
108131
return getCurrentSuite<ExtraContext>().on(
109132
'beforeEach',
110133
Object.assign(
111-
withTimeout(withFixtures(fn), timeout ?? getDefaultHookTimeout(), true),
112-
{ [CLEANUP_TIMEOUT_KEY]: timeout },
134+
withTimeout(
135+
withFixtures(fn),
136+
timeout ?? getDefaultHookTimeout(),
137+
true,
138+
stackTraceError,
139+
),
140+
{
141+
[CLEANUP_TIMEOUT_KEY]: timeout,
142+
[CLEANUP_STACK_TRACE_KEY]: stackTraceError,
143+
},
113144
),
114145
)
115146
}
@@ -138,7 +169,12 @@ export function afterEach<ExtraContext = object>(
138169
assertTypes(fn, '"afterEach" callback', ['function'])
139170
return getCurrentSuite<ExtraContext>().on(
140171
'afterEach',
141-
withTimeout(withFixtures(fn), timeout ?? getDefaultHookTimeout(), true),
172+
withTimeout(
173+
withFixtures(fn),
174+
timeout ?? getDefaultHookTimeout(),
175+
true,
176+
new Error('STACK_TRACE_ERROR'),
177+
),
142178
)
143179
}
144180

@@ -165,7 +201,12 @@ export const onTestFailed: TaskHook<OnTestFailedHandler> = createTestHook(
165201
(test, handler, timeout) => {
166202
test.onFailed ||= []
167203
test.onFailed.push(
168-
withTimeout(handler, timeout ?? getDefaultHookTimeout(), true),
204+
withTimeout(
205+
handler,
206+
timeout ?? getDefaultHookTimeout(),
207+
true,
208+
new Error('STACK_TRACE_ERROR'),
209+
),
169210
)
170211
},
171212
)
@@ -198,7 +239,12 @@ export const onTestFinished: TaskHook<OnTestFinishedHandler> = createTestHook(
198239
(test, handler, timeout) => {
199240
test.onFinished ||= []
200241
test.onFinished.push(
201-
withTimeout(handler, timeout ?? getDefaultHookTimeout(), true),
242+
withTimeout(
243+
handler,
244+
timeout ?? getDefaultHookTimeout(),
245+
true,
246+
new Error('STACK_TRACE_ERROR'),
247+
),
202248
)
203249
},
204250
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { describe, it, beforeAll, beforeEach, afterAll, afterEach } from "vitest"
2+
3+
describe('beforeAll', () => {
4+
beforeAll(() => new Promise(() => {}), 10)
5+
6+
it('ok', () => {})
7+
})
8+
9+
describe('beforeEach', () => {
10+
beforeEach(() => new Promise(() => {}), 20)
11+
12+
it('ok', () => {})
13+
})
14+
15+
describe('afterAll', () => {
16+
afterAll(() => new Promise(() => {}), 30)
17+
18+
it('ok', () => {})
19+
})
20+
21+
describe('afterEach', () => {
22+
afterEach(() => new Promise(() => {}), 40)
23+
24+
it('ok', () => {})
25+
})
26+
27+
describe('cleanup-beforeAll', () => {
28+
beforeAll(() => () => new Promise(() => {}), 50)
29+
30+
it('ok', () => {})
31+
})
32+
33+
describe('cleanup-beforeEach', () => {
34+
beforeEach(() => () => new Promise(() => {}), 60)
35+
36+
it('ok', () => {})
37+
})
38+
39+
describe('onFailed', () => {
40+
it('fail', (ctx) => {
41+
ctx.onTestFailed(() => new Promise(() => {}), 70)
42+
throw new Error('fail')
43+
})
44+
})
45+
46+
describe('onFinished', () => {
47+
it('ok', (ctx) => {
48+
ctx.onTestFinished(() => new Promise(() => {}), 80)
49+
})
50+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`timeout error with stack trace 1`] = `
4+
"
5+
⎯⎯⎯⎯⎯⎯ Failed Suites 3 ⎯⎯⎯⎯⎯⎯⎯
6+
7+
FAIL basic.test.ts > beforeAll
8+
Error: Hook timed out in 10ms.
9+
If this is a long-running hook, pass a timeout value as the last argument or configure it globally with "hookTimeout".
10+
❯ basic.test.ts:4:3
11+
2|
12+
3| describe('beforeAll', () => {
13+
4| beforeAll(() => new Promise(() => {}), 10)
14+
| ^
15+
5|
16+
6| it('ok', () => {})
17+
18+
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/9]⎯
19+
20+
FAIL basic.test.ts > afterAll
21+
Error: Hook timed out in 30ms.
22+
If this is a long-running hook, pass a timeout value as the last argument or configure it globally with "hookTimeout".
23+
basic.test.ts:16:3
24+
14|
25+
15| describe('afterAll', () => {
26+
16| afterAll(() => new Promise(() => {}), 30)
27+
| ^
28+
17|
29+
18| it('ok', () => {})
30+
31+
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/9]⎯
32+
33+
FAIL basic.test.ts > cleanup-beforeAll
34+
Error: Hook timed out in 50ms.
35+
If this is a long-running hook, pass a timeout value as the last argument or configure it globally with "hookTimeout".
36+
basic.test.ts:28:3
37+
26|
38+
27| describe('cleanup-beforeAll', () => {
39+
28| beforeAll(() => () => new Promise(() => {}), 50)
40+
| ^
41+
29|
42+
30| it('ok', () => {})
43+
44+
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[3/9]⎯
45+
46+
47+
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 5 ⎯⎯⎯⎯⎯⎯⎯
48+
49+
FAIL basic.test.ts > beforeEach > ok
50+
Error: Hook timed out in 20ms.
51+
If this is a long-running hook, pass a timeout value as the last argument or configure it globally with "hookTimeout".
52+
basic.test.ts:10:3
53+
8|
54+
9| describe('beforeEach', () => {
55+
10| beforeEach(() => new Promise(() => {}), 20)
56+
| ^
57+
11|
58+
12| it('ok', () => {})
59+
60+
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[4/9]⎯
61+
62+
FAIL basic.test.ts > afterEach > ok
63+
Error: Hook timed out in 40ms.
64+
If this is a long-running hook, pass a timeout value as the last argument or configure it globally with "hookTimeout".
65+
basic.test.ts:22:3
66+
20|
67+
21| describe('afterEach', () => {
68+
22| afterEach(() => new Promise(() => {}), 40)
69+
| ^
70+
23|
71+
24| it('ok', () => {})
72+
73+
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[5/9]⎯
74+
75+
FAIL basic.test.ts > cleanup-beforeEach > ok
76+
Error: Hook timed out in 60ms.
77+
If this is a long-running hook, pass a timeout value as the last argument or configure it globally with "hookTimeout".
78+
basic.test.ts:34:3
79+
32|
80+
33| describe('cleanup-beforeEach', () => {
81+
34| beforeEach(() => () => new Promise(() => {}), 60)
82+
| ^
83+
35|
84+
36| it('ok', () => {})
85+
86+
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[6/9]⎯
87+
88+
FAIL basic.test.ts > onFailed > fail
89+
Error: fail
90+
basic.test.ts:42:11
91+
40| it('fail', (ctx) => {
92+
41| ctx.onTestFailed(() => new Promise(() => {}), 70)
93+
42| throw new Error('fail')
94+
| ^
95+
43| })
96+
44| })
97+
98+
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[7/9]⎯
99+
100+
FAIL basic.test.ts > onFailed > fail
101+
Error: Hook timed out in 70ms.
102+
If this is a long-running hook, pass a timeout value as the last argument or configure it globally with "hookTimeout".
103+
basic.test.ts:41:9
104+
39| describe('onFailed', () => {
105+
40| it('fail', (ctx) => {
106+
41| ctx.onTestFailed(() => new Promise(() => {}), 70)
107+
| ^
108+
42| throw new Error('fail')
109+
43| })
110+
111+
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[8/9]⎯
112+
113+
FAIL basic.test.ts > onFinished > ok
114+
Error: Hook timed out in 80ms.
115+
If this is a long-running hook, pass a timeout value as the last argument or configure it globally with "hookTimeout".
116+
basic.test.ts:48:9
117+
46| describe('onFinished', () => {
118+
47| it('ok', (ctx) => {
119+
48| ctx.onTestFinished(() => new Promise(() => {}), 80)
120+
| ^
121+
49| })
122+
50| })
123+
124+
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[9/9]⎯
125+
126+
"
127+
`;

test/config/test/hook-timeout.test.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { expect, test } from 'vitest'
2+
import { runVitest } from '../../test-utils'
3+
4+
test('timeout error with stack trace', async () => {
5+
const { stderr } = await runVitest({
6+
root: './fixtures/hook-timeout',
7+
})
8+
expect(stderr).toMatchSnapshot()
9+
})

0 commit comments

Comments
 (0)