Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 30 additions & 13 deletions packages/vitest/src/node/pools/pool.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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
Comment thread
sheremet-va marked this conversation as resolved.
})
.finally(() => {
span.end()
})
.catch(error => span?.recordException(error))
.finally(() => span?.end())

const index = this.activeTasks.indexOf(activeTask)
if (index !== -1) {
Expand All @@ -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)),
)
Expand Down Expand Up @@ -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) {
Expand Down
3 changes: 2 additions & 1 deletion packages/vitest/src/node/pools/poolRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ enum RunnerState {
IDLE = 'idle',
STARTING = 'starting',
STARTED = 'started',
START_FAILURE = 'start_failure',
STOPPING = 'stopping',
STOPPED = 'stopped',
}
Expand Down Expand Up @@ -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
}
Expand Down
29 changes: 28 additions & 1 deletion test/config/test/pool.test.ts
Original file line number Diff line number Diff line change
@@ -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) => {
Expand Down Expand Up @@ -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<T = SerializedConfig>(options: Partial<TestUserConfig>, cliOptions: Partial<TestUserConfig> = {}): Promise<T> {
let config: T | undefined

Expand Down
Loading