[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

fix(pool): prevent test run hang on worker crash (#10543)

Co-authored-by: Ari Perkkiö <ari.perkkio@gmail.com>

authored by

Jattioui Ismail
Ari Perkkiö
and committed by
GitHub
(Jun 10, 2026, 1:56 PM +0300) 4087802b 13b78d98

+116 -6
+71
test/e2e/test/pool-worker-exit.test.ts
··· 1 + import { runVitest, StableTestFileOrderSorter } from '#test-utils' 2 + import { resolve } from 'pathe' 3 + import { expect, test } from 'vitest' 4 + import { readCoverageMap } from '../../coverage-test/utils' 5 + 6 + test('worker death on a shared runner does not skip coverage finalization', async () => { 7 + const root = './fixtures/pool-worker-exit' 8 + 9 + const { buildTree } = await runVitest({ 10 + root, 11 + pool: 'forks', 12 + 13 + // Disable isolation to make sure crashed worker doesn't hang whole test run 14 + isolate: false, 15 + maxWorkers: 2, 16 + 17 + sequence: { sequencer: StableTestFileOrderSorter }, 18 + include: [ 19 + '1-first.test.ts', 20 + '2-crash.test.ts', 21 + '3-crash.test.ts', 22 + '4-third.test.ts', 23 + ], 24 + 25 + reporters: 'default', 26 + coverage: { 27 + enabled: true, 28 + provider: 'v8', 29 + reporter: ['json'], 30 + reportOnFailure: true, 31 + }, 32 + }) 33 + 34 + expect(buildTree(t => ({ state: t.result().state }))).toMatchInlineSnapshot(` 35 + { 36 + "1-first.test.ts": { 37 + "first test exercises src so it should appear in coverage": { 38 + "state": "passed", 39 + }, 40 + }, 41 + "2-crash.test.ts": { 42 + "the worker dies before sending testfileFinished": { 43 + "state": "pending", 44 + }, 45 + }, 46 + "3-crash.test.ts": { 47 + "the worker dies before sending testfileFinished": { 48 + "state": "pending", 49 + }, 50 + }, 51 + "4-third.test.ts": { 52 + "third test": { 53 + "state": "passed", 54 + }, 55 + }, 56 + } 57 + `) 58 + 59 + // Crashing worker should not interfere with other test-run, coverage should be reported: 60 + const coverageMap = await readCoverageMap(resolve(root, 'coverage/coverage-final.json')) 61 + const fileCoverage = coverageMap.fileCoverageFor('<process-cwd>/fixtures/pool-worker-exit/src.ts') 62 + 63 + expect(fileCoverage.toSummary().functions).toMatchInlineSnapshot(` 64 + { 65 + "covered": 1, 66 + "pct": 50, 67 + "skipped": 0, 68 + "total": 2, 69 + } 70 + `) 71 + })
+6
test/e2e/fixtures/pool-worker-exit/1-first.test.ts
··· 1 + import { expect, test } from 'vitest' 2 + import { covered } from './src' 3 + 4 + test('first test exercises src so it should appear in coverage', () => { 5 + expect(covered()).toBe(42) 6 + })
+8
test/e2e/fixtures/pool-worker-exit/2-crash.test.ts
··· 1 + import { test } from 'vitest' 2 + 3 + test('the worker dies before sending testfileFinished', async () => { 4 + // SIGKILL the worker process so it can't send testfileFinished back to main. 5 + // Pre-fix this caused pool.run() to hang forever instead of rejecting. 6 + queueMicrotask(() => process.kill(process.pid, 'SIGKILL')) 7 + await new Promise(() => {}) 8 + })
+8
test/e2e/fixtures/pool-worker-exit/3-crash.test.ts
··· 1 + import { test } from 'vitest' 2 + 3 + test('the worker dies before sending testfileFinished', async () => { 4 + // SIGKILL the worker process so it can't send testfileFinished back to main. 5 + // Pre-fix this caused pool.run() to hang forever instead of rejecting. 6 + queueMicrotask(() => process.kill(process.pid, 'SIGKILL')) 7 + await new Promise(() => {}) 8 + })
+6
test/e2e/fixtures/pool-worker-exit/4-third.test.ts
··· 1 + import { expect, test } from 'vitest' 2 + import { covered } from './src.js' 3 + 4 + test('third test', () => { 5 + expect(covered()).toBe(42) 6 + })
+7
test/e2e/fixtures/pool-worker-exit/src.ts
··· 1 + export function covered() { 2 + return 42 3 + } 4 + 5 + export function uncovered() { 6 + return 42 7 + }
+9 -6
packages/vitest/src/node/pools/pool.ts
··· 101 101 } 102 102 103 103 runner.off('message', onFinished) 104 + runner.off('error', onTaskError) 104 105 resolver.resolve() 105 106 } 106 107 } 107 108 109 + function onTaskError(error: unknown) { 110 + runner.off('message', onFinished) 111 + runner.off('error', onTaskError) 112 + resolver.reject(new Error(`[vitest-pool]: Worker ${task.worker} emitted error.`, { cause: error })) 113 + } 114 + 108 115 runner.on('message', onFinished) 116 + runner.on('error', onTaskError) 109 117 110 118 if (!runner.isStarted) { 111 - runner.on('error', (error) => { 112 - resolver.reject( 113 - new Error(`[vitest-pool]: Worker ${task.worker} emitted error.`, { cause: error }), 114 - ) 115 - }) 116 - 117 119 const id = setTimeout( 118 120 () => resolver.reject(new Error(`[vitest-pool]: Timeout starting ${task.worker} runner.`)), 119 121 WORKER_START_TIMEOUT, ··· 148 150 149 151 if ( 150 152 !task.isolate 153 + && !runner.isTerminated 151 154 && !isMemoryLimitReached 152 155 && this.queue[0]?.task.isolate === false 153 156 && isEqualRunner(runner, this.queue[0].task)
+1
packages/vitest/src/node/pools/poolRunner.ts
··· 368 368 369 369 private emitUnexpectedExit = (): void => { 370 370 const error = new Error(`Worker exited unexpectedly during ${this._state} state`) 371 + this._state = RunnerState.STOPPED 371 372 372 373 this._eventEmitter.emit('error', error) 373 374 }