diff --git a/packages/vitest/src/node/pools/pool.ts b/packages/vitest/src/node/pools/pool.ts index 7c13499176e4..eeccdc8aa825 100644 --- a/packages/vitest/src/node/pools/pool.ts +++ b/packages/vitest/src/node/pools/pool.ts @@ -1,3 +1,4 @@ +import type { Span } from '@opentelemetry/api' import type { ContextTestEnvironment } from '../../types/worker' import type { Logger } from '../logger' import type { StateManager } from '../state' @@ -118,21 +119,27 @@ export class Pool { WORKER_START_TIMEOUT, ) - await runner.start({ workerId: task.context.workerId }).finally(() => clearTimeout(id)) + await runner.start({ workerId: task.context.workerId }) + .catch(error => + resolver.reject( + new Error(`[vitest-pool]: Failed to start ${task.worker} worker for test files ${formatFiles(task)}.`, { cause: error }), + ), + ) + .finally(() => clearTimeout(id)) } - const span = runner.startTracesSpan(`vitest.worker.${method}`) - // Start running the test in the worker - runner.request(method, task.context) + let span: Span | undefined + + if (!resolver.isRejected) { + span = runner.startTracesSpan(`vitest.worker.${method}`) + + // Start running the test in the worker + runner.request(method, task.context) + } await resolver.promise - .catch((error) => { - span.recordException(error) - throw error - }) - .finally(() => { - span.end() - }) + .catch(error => span?.recordException(error)) + .finally(() => span?.end()) const index = this.activeTasks.indexOf(activeTask) if (index !== -1) { @@ -158,7 +165,7 @@ export class Pool { ) this.exitPromises.push( - runner.stop() + runner.stop({ force: resolver.isRejected }) .then(() => clearTimeout(id)) .catch(error => this.logger.error(`[vitest-pool]: Failed to terminate ${task.worker} worker for test files ${formatFiles(task)}.`, error)), ) @@ -281,7 +288,17 @@ function withResolvers() { reject = rej }) - return { resolve, reject, promise } + const resolver = { + promise, + resolve, + reject: (reason: unknown) => { + resolver.isRejected = true + reject(reason) + }, + isRejected: false, + } + + return resolver } function formatFiles(task: PoolTask) { diff --git a/packages/vitest/src/node/pools/poolRunner.ts b/packages/vitest/src/node/pools/poolRunner.ts index da40c7a14468..26613932da1e 100644 --- a/packages/vitest/src/node/pools/poolRunner.ts +++ b/packages/vitest/src/node/pools/poolRunner.ts @@ -15,6 +15,7 @@ enum RunnerState { IDLE = 'idle', STARTING = 'starting', STARTED = 'started', + START_FAILURE = 'start_failure', STOPPING = 'stopping', STOPPED = 'stopped', } @@ -231,7 +232,7 @@ export class PoolRunner { this._state = RunnerState.STARTED } catch (error: any) { - this._state = RunnerState.IDLE + this._state = RunnerState.START_FAILURE startSpan?.recordException(error) throw error } diff --git a/test/config/test/pool.test.ts b/test/config/test/pool.test.ts index 749d53d8f564..f3f69dd04d2c 100644 --- a/test/config/test/pool.test.ts +++ b/test/config/test/pool.test.ts @@ -1,7 +1,7 @@ import type { SerializedConfig } from 'vitest' import type { TestUserConfig } from 'vitest/node' import { normalize } from 'pathe' -import { assert, describe, expect, test } from 'vitest' +import { assert, describe, expect, test, vi } from 'vitest' import { runVitest, StableTestFileOrderSorter } from '../../test-utils' describe.each(['forks', 'threads', 'vmThreads', 'vmForks'])('%s', async (pool) => { @@ -102,6 +102,33 @@ test('non-isolated happy-dom worker pool receives all testfiles at once', async `) }) +test('worker start failure should not hang', async () => { + const stop = vi.fn() + + const { stdout, stderr } = await runVitest({ + root: './fixtures/pool', + include: ['a.test.ts'], + pool: { + name: 'pool-with-crashing-workers', + // @ts-expect-error -- intentional + createPoolWorker: () => ({ + start: () => Promise.reject(new Error('Mock')), + stop, + on() {}, + off() {}, + send() {}, + }), + }, + }) + + expect(stderr).toContain('Error: [vitest-pool]: Failed to start pool-with-crashing-workers worker for test files') + expect(stderr).toContain('a.test.ts') + expect(stderr).toContain('Caused by: Error: Mock') + expect(stdout).toContain('Errors 1 error') + + expect(stop).toHaveBeenCalled() +}) + async function getConfig(options: Partial, cliOptions: Partial = {}): Promise { let config: T | undefined