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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ dist
.vite-node
ltex*
.DS_Store
.zed
bench/test/*/*/
**/bench.json
**/browser/browser.json
Expand All @@ -35,4 +36,4 @@ test/browser/html/
test/core/html/
.vitest-attachments
explainFiles.txt
.vitest-dump
.vitest-dump
55 changes: 51 additions & 4 deletions packages/vitest/src/node/pool.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Awaitable } from '@vitest/utils'
import type { ContextTestEnvironment } from '../types/worker'
import type { Vitest } from './core'
import type { PoolTask } from './pools/types'
import type { TestProject } from './project'
Expand Down Expand Up @@ -87,7 +88,7 @@ export function createPool(ctx: Vitest): ProcessPool {

const sorted = await sequencer.sort(specs)
const environments = await getSpecificationsEnvironments(specs)
const groups = groupSpecs(sorted)
const groups = groupSpecs(sorted, environments)

const projectEnvs = new WeakMap<TestProject, Partial<NodeJS.ProcessEnv>>()
const projectExecArgvs = new WeakMap<TestProject, string[]>()
Expand Down Expand Up @@ -330,9 +331,8 @@ function getMemoryLimit(config: ResolvedConfig, pool: string) {
return null
}

function groupSpecs(specs: TestSpecification[]) {
// Test files are passed to test runner one at a time, except Typechecker.
// TODO: Should non-isolated test files be passed to test runner all at once?
function groupSpecs(specs: TestSpecification[], environments: Awaited<ReturnType<typeof getSpecificationsEnvironments>>) {
// Test files are passed to test runner one at a time, except for Typechecker or when "--maxWorker=1 --no-isolate"
type SpecsForRunner = TestSpecification[]

// Tests in a single group are executed with `maxWorkers` parallelism.
Expand All @@ -346,6 +346,43 @@ function groupSpecs(specs: TestSpecification[]) {
// Type tests are run in a single group, per project
const typechecks: Record<string, TestSpecification[]> = {}

const serializedEnvironmentOptions = new Map<ContextTestEnvironment, string>()

function getSerializedOptions(env: ContextTestEnvironment) {
const options = serializedEnvironmentOptions.get(env)

if (options) {
return options
}

const serialized = JSON.stringify(env.options)
serializedEnvironmentOptions.set(env, serialized)
return serialized
}

function isEqualEnvironments(a: TestSpecification, b: TestSpecification) {
const aEnv = environments.get(a)
const bEnv = environments.get(b)

if (!aEnv && !bEnv) {
return true
}

if (!aEnv || !bEnv || aEnv.name !== bEnv.name) {
return false
}

if (!aEnv.options && !bEnv.options) {
return true
}

if (!aEnv.options || !bEnv.options) {
return false
}

return getSerializedOptions(aEnv) === getSerializedOptions(bEnv)
}

specs.forEach((spec) => {
if (spec.pool === 'typescript') {
typechecks[spec.project.name] ||= []
Expand All @@ -361,6 +398,7 @@ function groupSpecs(specs: TestSpecification[]) {
}

const maxWorkers = resolveMaxWorkers(spec.project)
const isolate = spec.project.config.isolate
groups[order] ||= { specs: [], maxWorkers }

// Multiple projects with different maxWorkers but same groupId
Expand All @@ -370,6 +408,15 @@ function groupSpecs(specs: TestSpecification[]) {
throw new Error(`Projects "${last}" and "${spec.project.name}" have different 'maxWorkers' but same 'sequence.groupId'.\nProvide unique 'sequence.groupId' for them.`)
}

// Non-isolated single worker can receive all files at once
if (isolate === false && maxWorkers === 1) {
const previous = groups[order].specs[0]?.[0]

if (previous && previous.project.name === spec.project.name && isEqualEnvironments(spec, previous)) {
return groups[order].specs[0].push(spec)
}
}

groups[order].specs.push([spec])
})

Expand Down
3 changes: 3 additions & 0 deletions test/config/fixtures/pool/a.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { test } from "vitest"

test("a", () => { })
3 changes: 3 additions & 0 deletions test/config/fixtures/pool/b.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { test } from "vitest"

test("b", () => { })
3 changes: 3 additions & 0 deletions test/config/fixtures/pool/c.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { test } from "vitest"

test("c", () => { })
6 changes: 6 additions & 0 deletions test/config/fixtures/pool/print-testfiles.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { test } from 'vitest'

test('print config', () => {
// @ts-expect-error -- internal
console.log(JSON.stringify(globalThis.__vitest_worker__.ctx.files.map(file => file.filepath)))
})
43 changes: 40 additions & 3 deletions test/config/test/pool.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { SerializedConfig } from 'vitest'
import type { TestUserConfig } from 'vitest/node'
import { normalize } from 'pathe'
import { assert, describe, expect, test } from 'vitest'
import { runVitest } from '../../test-utils'
import { runVitest, StableTestFileOrderSorter } from '../../test-utils'

describe.each(['forks', 'threads', 'vmThreads', 'vmForks'])('%s', async (pool) => {
test('resolves top-level pool', async () => {
Expand Down Expand Up @@ -51,8 +52,39 @@ test('project level pool options overwrites top-level', async () => {
expect(config.fileParallelism).toBe(false)
})

async function getConfig(options: Partial<TestUserConfig>, cliOptions: Partial<TestUserConfig> = {}) {
let config: SerializedConfig | undefined
test('isolated single worker pool receives single testfile at once', async () => {
const files = await getConfig<string[]>({
maxWorkers: 1,
isolate: true,
sequence: { sequencer: StableTestFileOrderSorter },
}, { include: ['print-testfiles.test.ts', 'a.test.ts', 'b.test.ts', 'c.test.ts'] })

expect(files.map(normalizeFilename)).toMatchInlineSnapshot(`
[
"<process-cwd>/fixtures/pool/print-testfiles.test.ts",
]
`)
})

test('non-isolated single worker pool receives all testfiles at once', async () => {
const files = await getConfig<string[]>({
maxWorkers: 1,
isolate: false,
sequence: { sequencer: StableTestFileOrderSorter },
}, { include: ['print-testfiles.test.ts', 'a.test.ts', 'b.test.ts', 'c.test.ts'] })

expect(files.map(normalizeFilename)).toMatchInlineSnapshot(`
[
"<process-cwd>/fixtures/pool/a.test.ts",
"<process-cwd>/fixtures/pool/b.test.ts",
"<process-cwd>/fixtures/pool/c.test.ts",
"<process-cwd>/fixtures/pool/print-testfiles.test.ts",
]
`)
})

async function getConfig<T = SerializedConfig>(options: Partial<TestUserConfig>, cliOptions: Partial<TestUserConfig> = {}): Promise<T> {
let config: T | undefined

await runVitest({
root: './fixtures/pool',
Expand All @@ -66,3 +98,8 @@ async function getConfig(options: Partial<TestUserConfig>, cliOptions: Partial<T
assert(config)
return config
}

function normalizeFilename(filename: string) {
return normalize(filename)
.replace(normalize(process.cwd()), '<process-cwd>')
}
Loading