diff --git a/.gitignore b/.gitignore index 7384cac5e241..5a6e9c76cbe4 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ dist .vite-node ltex* .DS_Store +.zed bench/test/*/*/ **/bench.json **/browser/browser.json @@ -35,4 +36,4 @@ test/browser/html/ test/core/html/ .vitest-attachments explainFiles.txt -.vitest-dump \ No newline at end of file +.vitest-dump diff --git a/packages/vitest/src/node/pool.ts b/packages/vitest/src/node/pool.ts index f48edac4f27e..41299a5ce1e5 100644 --- a/packages/vitest/src/node/pool.ts +++ b/packages/vitest/src/node/pool.ts @@ -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' @@ -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>() const projectExecArgvs = new WeakMap() @@ -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>) { + // 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. @@ -346,6 +346,43 @@ function groupSpecs(specs: TestSpecification[]) { // Type tests are run in a single group, per project const typechecks: Record = {} + const serializedEnvironmentOptions = new Map() + + 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] ||= [] @@ -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 @@ -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]) }) diff --git a/test/config/fixtures/pool/a.test.ts b/test/config/fixtures/pool/a.test.ts new file mode 100644 index 000000000000..33670f61a8b6 --- /dev/null +++ b/test/config/fixtures/pool/a.test.ts @@ -0,0 +1,3 @@ +import { test } from "vitest" + +test("a", () => { }) diff --git a/test/config/fixtures/pool/b.test.ts b/test/config/fixtures/pool/b.test.ts new file mode 100644 index 000000000000..0360ff9a3ab4 --- /dev/null +++ b/test/config/fixtures/pool/b.test.ts @@ -0,0 +1,3 @@ +import { test } from "vitest" + +test("b", () => { }) diff --git a/test/config/fixtures/pool/c.test.ts b/test/config/fixtures/pool/c.test.ts new file mode 100644 index 000000000000..9e331775656a --- /dev/null +++ b/test/config/fixtures/pool/c.test.ts @@ -0,0 +1,3 @@ +import { test } from "vitest" + +test("c", () => { }) diff --git a/test/config/fixtures/pool/print-testfiles.test.ts b/test/config/fixtures/pool/print-testfiles.test.ts new file mode 100644 index 000000000000..15b2d111c1c2 --- /dev/null +++ b/test/config/fixtures/pool/print-testfiles.test.ts @@ -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))) +}) diff --git a/test/config/test/pool.test.ts b/test/config/test/pool.test.ts index d8d228652081..af441aecbe07 100644 --- a/test/config/test/pool.test.ts +++ b/test/config/test/pool.test.ts @@ -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 () => { @@ -51,8 +52,39 @@ test('project level pool options overwrites top-level', async () => { expect(config.fileParallelism).toBe(false) }) -async function getConfig(options: Partial, cliOptions: Partial = {}) { - let config: SerializedConfig | undefined +test('isolated single worker pool receives single testfile at once', async () => { + const files = await getConfig({ + 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(` + [ + "/fixtures/pool/print-testfiles.test.ts", + ] + `) +}) + +test('non-isolated single worker pool receives all testfiles at once', async () => { + const files = await getConfig({ + 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(` + [ + "/fixtures/pool/a.test.ts", + "/fixtures/pool/b.test.ts", + "/fixtures/pool/c.test.ts", + "/fixtures/pool/print-testfiles.test.ts", + ] + `) +}) + +async function getConfig(options: Partial, cliOptions: Partial = {}): Promise { + let config: T | undefined await runVitest({ root: './fixtures/pool', @@ -66,3 +98,8 @@ async function getConfig(options: Partial, cliOptions: Partial') +}