Skip to content

Commit

Permalink
fix: don't await an empty timeout after every test (#7281)
Browse files Browse the repository at this point in the history
  • Loading branch information
sheremet-va authored Jan 17, 2025
1 parent 450de3b commit ef1aa89
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 16 deletions.
64 changes: 51 additions & 13 deletions packages/runner/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import type {
import { getSafeTimers } from '@vitest/utils'
import { PendingError } from './errors'

const now = Date.now

export const collectorContext: RuntimeContext = {
tasks: [],
currentSuite: null,
Expand Down Expand Up @@ -41,19 +43,55 @@ export function withTimeout<T extends (...args: any[]) => any>(

// this function name is used to filter error in test/cli/test/fails.test.ts
return (function runWithTimeout(...args: T extends (...args: infer A) => any ? A : never) {
return Promise.race([
new Promise((resolve, reject) => {
const timer = setTimeout(() => {
clearTimeout(timer)
reject(new Error(makeTimeoutMsg(isHook, timeout)))
}, timeout)
// `unref` might not exist in browser
timer.unref?.()
}),
Promise.resolve(fn(...args)).then((result) => {
return new Promise(resolve => setTimeout(resolve, 0, result))
}),
]) as Awaitable<void>
const startTime = now()
return new Promise((resolve_, reject_) => {
const timer = setTimeout(() => {
clearTimeout(timer)
reject(new Error(makeTimeoutMsg(isHook, timeout)))
}, timeout)
// `unref` might not exist in browser
timer.unref?.()

function resolve(result: unknown) {
clearTimeout(timer)
resolve_(result)
}

function reject(error: unknown) {
clearTimeout(timer)
reject_(error)
}

// sync test/hook will be caught by try/catch
try {
const result = fn(...args) as PromiseLike<unknown>
// the result is a thenable, we don't wrap this in Promise.resolve
// to avoid creating new promises
if (typeof result === 'object' && result != null && typeof result.then === 'function') {
result.then(
(result) => {
// if sync test/hook took too long, setTimeout won't be triggered,
// but we still need to fail the test, see
// https://github.com/vitest-dev/vitest/issues/2920
if (now() - startTime >= timeout) {
reject(new Error(makeTimeoutMsg(isHook, timeout)))
}
else {
resolve(result)
}
},
reject,
)
}
else {
resolve(result)
}
}
// user sync test/hook throws an error
catch (error) {
reject(error)
}
})
}) as T
}

Expand Down
10 changes: 8 additions & 2 deletions test/cli/test/__snapshots__/fails.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,15 @@ exports[`should fail async-assertion.test.ts 1`] = `
AssertionError: expected 'xx' to be 'yy' // Object.is equality"
`;

exports[`should fail concurrent-suite-deadlock.test.ts 1`] = `"Error: Test timed out in 500ms."`;
exports[`should fail concurrent-suite-deadlock.test.ts 1`] = `
"Error: Test timed out in 500ms.
Error: Test timed out in 500ms."
`;

exports[`should fail concurrent-test-deadlock.test.ts 1`] = `"Error: Test timed out in 500ms."`;
exports[`should fail concurrent-test-deadlock.test.ts 1`] = `
"Error: Test timed out in 500ms.
Error: Test timed out in 500ms."
`;

exports[`should fail each-timeout.test.ts 1`] = `"Error: Test timed out in 10ms."`;

Expand Down
2 changes: 1 addition & 1 deletion test/cli/test/fails.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ it.each(files)('should fail %s', async (file) => {
const msg = String(stderr)
.split(/\n/g)
.reverse()
.filter(i => i.includes('Error: ') && !i.includes('Command failed') && !i.includes('stackStr') && !i.includes('at runTest') && !i.includes('at runWithTimeout'))
.filter(i => i.includes('Error: ') && !i.includes('Command failed') && !i.includes('stackStr') && !i.includes('at runTest') && !i.includes('at runWithTimeout') && !i.includes('file:'))
.map(i => i.trim().replace(root, '<rootDir>'),
)
.join('\n')
Expand Down

0 comments on commit ef1aa89

Please sign in to comment.