diff --git a/docs/config/index.md b/docs/config/index.md index 0c58ae163520..0ed0d3e9984e 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -381,6 +381,31 @@ export default defineConfig({ }) ``` +### poolMatchGlobs + +- **Type:** `[string, 'threads' | 'child_process'][]` +- **Default:** `[]` +- **Version:** Since Vitest 0.29.4 + +Automatically assign pool in which tests will run based on globs. The first match will be used. + +For example: + +```ts +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + poolMatchGlobs: [ + // all tests in "worker-specific" directory will run inside a worker as if you enabled `--threads` for them, + ['**/tests/worker-specific/**', 'threads'], + // all other tests will run based on "threads" option, if you didn't specify other globs + // ... + ] + } +}) +``` + ### update - **Type:** `boolean` diff --git a/packages/vitest/src/node/pool.ts b/packages/vitest/src/node/pool.ts index c849f05f0112..5b5cf234fbdb 100644 --- a/packages/vitest/src/node/pool.ts +++ b/packages/vitest/src/node/pool.ts @@ -1,6 +1,8 @@ import { pathToFileURL } from 'node:url' +import mm from 'micromatch' import { resolve } from 'pathe' import { distDir, rootDir } from '../constants' +import type { VitestPool } from '../types' import type { Vitest } from './core' import { createChildProcessPool } from './pools/child' import { createThreadsPool } from './pools/threads' @@ -21,38 +23,92 @@ const loaderPath = pathToFileURL(resolve(distDir, './loader.js')).href const suppressLoaderWarningsPath = resolve(rootDir, './suppress-warnings.cjs') export function createPool(ctx: Vitest): ProcessPool { - const conditions = ctx.server.config.resolve.conditions?.flatMap(c => ['--conditions', c]) || [] - - // Instead of passing whole process.execArgv to the workers, pick allowed options. - // Some options may crash worker, e.g. --prof, --title. nodejs/node#41103 - const execArgv = process.execArgv.filter(execArg => - execArg.startsWith('--cpu-prof') || execArg.startsWith('--heap-prof'), - ) - - const options: PoolProcessOptions = { - execArgv: ctx.config.deps.registerNodeLoader - ? [ - ...execArgv, - '--require', - suppressLoaderWarningsPath, - '--experimental-loader', - loaderPath, - ] - : [ - ...execArgv, - ...conditions, - ], - env: { - TEST: 'true', - VITEST: 'true', - NODE_ENV: ctx.config.mode || 'test', - VITEST_MODE: ctx.config.watch ? 'WATCH' : 'RUN', - ...process.env, - ...ctx.config.env, - }, + const pools: Record = { + child_process: null, + threads: null, + } + + function getDefaultPoolName() { + if (ctx.config.threads) + return 'threads' + return 'child_process' + } + + function getPoolName(file: string) { + for (const [glob, pool] of ctx.config.poolMatchGlobs || []) { + if (mm.isMatch(file, glob, { cwd: ctx.server.config.root })) + return pool + } + return getDefaultPoolName() + } + + async function runTests(files: string[], invalidate?: string[]) { + const conditions = ctx.server.config.resolve.conditions?.flatMap(c => ['--conditions', c]) || [] + + // Instead of passing whole process.execArgv to the workers, pick allowed options. + // Some options may crash worker, e.g. --prof, --title. nodejs/node#41103 + const execArgv = process.execArgv.filter(execArg => + execArg.startsWith('--cpu-prof') || execArg.startsWith('--heap-prof'), + ) + + const options: PoolProcessOptions = { + execArgv: ctx.config.deps.registerNodeLoader + ? [ + ...execArgv, + '--require', + suppressLoaderWarningsPath, + '--experimental-loader', + loaderPath, + ] + : [ + ...execArgv, + ...conditions, + ], + env: { + TEST: 'true', + VITEST: 'true', + NODE_ENV: ctx.config.mode || 'test', + VITEST_MODE: ctx.config.watch ? 'WATCH' : 'RUN', + ...process.env, + ...ctx.config.env, + }, + } + + const filesByPool = { + child_process: [] as string[], + threads: [] as string[], + browser: [] as string[], + } + + if (!ctx.config.poolMatchGlobs) { + const name = getDefaultPoolName() + filesByPool[name] = files + } + else { + for (const file of files) { + const pool = getPoolName(file) + filesByPool[pool].push(file) + } + } + + await Promise.all(Object.entries(filesByPool).map(([pool, files]) => { + if (!files.length) + return null + + if (pool === 'threads') { + pools.threads ??= createThreadsPool(ctx, options) + return pools.threads.runTests(files, invalidate) + } + + pools.child_process ??= createChildProcessPool(ctx, options) + return pools.child_process.runTests(files, invalidate) + })) } - if (!ctx.config.threads) - return createChildProcessPool(ctx, options) - return createThreadsPool(ctx, options) + return { + runTests, + async close() { + await Promise.all(Object.values(pools).map(p => p?.close())) + }, + } } diff --git a/packages/vitest/src/node/pools/child.ts b/packages/vitest/src/node/pools/child.ts index 3e074454b83a..6f51193192c5 100644 --- a/packages/vitest/src/node/pools/child.ts +++ b/packages/vitest/src/node/pools/child.ts @@ -15,7 +15,7 @@ import { createMethodsRPC } from './rpc' const childPath = fileURLToPath(pathToFileURL(resolve(distDir, './child.js')).href) -function setupChildProcessChannel(ctx: Vitest, fork: ChildProcess) { +function setupChildProcessChannel(ctx: Vitest, fork: ChildProcess): void { createBirpc<{}, RuntimeRPC>( createMethodsRPC(ctx), { @@ -31,16 +31,16 @@ function setupChildProcessChannel(ctx: Vitest, fork: ChildProcess) { ) } -function stringifyRegex(input: RegExp | string): any { +function stringifyRegex(input: RegExp | string): string { if (typeof input === 'string') return input return `$$vitest:${input.toString()}` } -function getTestConfig(ctx: Vitest) { +function getTestConfig(ctx: Vitest): ResolvedConfig { const config = ctx.getSerializableConfig() // v8 serialize does not support regex - return { + return { ...config, testNamePattern: config.testNamePattern ? stringifyRegex(config.testNamePattern) @@ -83,7 +83,7 @@ export function createChildProcessPool(ctx: Vitest, { execArgv, env }: PoolProce }) } - async function runWithFiles(files: string[], invalidates: string[] = []) { + async function runWithFiles(files: string[], invalidates: string[] = []): Promise { ctx.state.clearFiles(files) const config = getTestConfig(ctx) @@ -119,6 +119,7 @@ export function createChildProcessPool(ctx: Vitest, { execArgv, env }: PoolProce if (!child.killed) child.kill() }) + children.clear() }, } } diff --git a/packages/vitest/src/types/config.ts b/packages/vitest/src/types/config.ts index 2deb42366892..277b209a9b5c 100644 --- a/packages/vitest/src/types/config.ts +++ b/packages/vitest/src/types/config.ts @@ -16,6 +16,7 @@ export type { SequenceHooks, SequenceSetupFiles } from '@vitest/runner' export type BuiltinEnvironment = 'node' | 'jsdom' | 'happy-dom' | 'edge-runtime' // Record is used, so user can get intellisense for builtin environments, but still allow custom environments export type VitestEnvironment = BuiltinEnvironment | (string & Record) +export type VitestPool = 'threads' | 'child_process' export type CSSModuleScopeStrategy = 'stable' | 'scoped' | 'non-scoped' export type ApiConfig = Pick @@ -162,6 +163,21 @@ export interface InlineConfig { */ environmentMatchGlobs?: [string, VitestEnvironment][] + /** + * Automatically assign pool based on globs. The first match will be used. + * + * Format: [glob, pool-name] + * + * @default [] + * @example [ + * // all tests in "browser" directory will run in an actual browser + * ['tests/browser/**', 'browser'], + * // all other tests will run based on "threads" option, if you didn't specify other globs + * // ... + * ] + */ + poolMatchGlobs?: [string, VitestPool][] + /** * Update snapshot * diff --git a/packages/vitest/src/utils/test-helpers.ts b/packages/vitest/src/utils/test-helpers.ts index 28a484d49c15..392cc34a7bcd 100644 --- a/packages/vitest/src/utils/test-helpers.ts +++ b/packages/vitest/src/utils/test-helpers.ts @@ -25,7 +25,7 @@ export async function groupFilesByEnv(files: string[], config: ResolvedConfig) { // 2. Check for globals if (!env) { for (const [glob, target] of config.environmentMatchGlobs || []) { - if (mm.isMatch(file, glob)) { + if (mm.isMatch(file, glob, { cwd: config.root })) { env = target break } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index acbc525ec1a0..03704e2c0777 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1158,6 +1158,12 @@ importers: devDependencies: vitest: link:../../packages/vitest + test/mixed-pools: + specifiers: + vitest: workspace:* + devDependencies: + vitest: link:../../packages/vitest + test/path-resolution: specifiers: '@edge-runtime/vm': 1.1.0-beta.26 @@ -1729,7 +1735,7 @@ packages: resolution: {integrity: sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.20.7 + '@babel/types': 7.21.3 dev: true /@babel/helper-builder-binary-assignment-operator-visitor/7.18.9: @@ -1979,7 +1985,6 @@ packages: dependencies: '@babel/template': 7.20.7 '@babel/types': 7.21.3 - dev: true /@babel/helper-hoist-variables/7.18.6: resolution: {integrity: sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==} @@ -2005,7 +2010,7 @@ packages: resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.20.7 + '@babel/types': 7.21.3 /@babel/helper-module-transforms/7.18.9: resolution: {integrity: sha512-KYNqY0ICwfv19b31XzvmI/mfcylOzbLtowkw+mfvGPAQ3kfCnMLYbED3YecL5tPd8nAYFQFAd6JHp2LxZk/J1g==} @@ -2042,7 +2047,7 @@ packages: resolution: {integrity: sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.20.7 + '@babel/types': 7.21.3 dev: true /@babel/helper-plugin-utils/7.10.4: @@ -2181,7 +2186,7 @@ packages: engines: {node: '>=6.0.0'} hasBin: true dependencies: - '@babel/types': 7.20.7 + '@babel/types': 7.21.3 /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/7.18.6_@babel+core@7.18.13: resolution: {integrity: sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==} @@ -4081,7 +4086,7 @@ packages: '@babel/plugin-transform-unicode-escapes': 7.18.10_@babel+core@7.18.13 '@babel/plugin-transform-unicode-regex': 7.18.6_@babel+core@7.18.13 '@babel/preset-modules': 0.1.5_@babel+core@7.18.13 - '@babel/types': 7.20.7 + '@babel/types': 7.21.3 babel-plugin-polyfill-corejs2: 0.3.2_@babel+core@7.18.13 babel-plugin-polyfill-corejs3: 0.5.3_@babel+core@7.18.13 babel-plugin-polyfill-regenerator: 0.4.0_@babel+core@7.18.13 @@ -4167,7 +4172,7 @@ packages: '@babel/plugin-transform-unicode-escapes': 7.18.10_@babel+core@7.20.12 '@babel/plugin-transform-unicode-regex': 7.18.6_@babel+core@7.20.12 '@babel/preset-modules': 0.1.5_@babel+core@7.20.12 - '@babel/types': 7.20.7 + '@babel/types': 7.21.3 babel-plugin-polyfill-corejs2: 0.3.2_@babel+core@7.20.12 babel-plugin-polyfill-corejs3: 0.5.3_@babel+core@7.20.12 babel-plugin-polyfill-regenerator: 0.4.0_@babel+core@7.20.12 @@ -4307,7 +4312,7 @@ packages: dependencies: '@babel/code-frame': 7.18.6 '@babel/parser': 7.20.7 - '@babel/types': 7.20.7 + '@babel/types': 7.21.3 /@babel/traverse/7.18.13: resolution: {integrity: sha512-N6kt9X1jRMLPxxxPYWi7tgvJRH/rtoU+dbKAPDM44RFHiMH8igdsaSBgFeskhSl/kLWLDUvIh1RXCrTmg0/zvA==} @@ -4333,11 +4338,11 @@ packages: '@babel/code-frame': 7.18.6 '@babel/generator': 7.20.7 '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-function-name': 7.19.0 + '@babel/helper-function-name': 7.21.0 '@babel/helper-hoist-variables': 7.18.6 '@babel/helper-split-export-declaration': 7.18.6 '@babel/parser': 7.20.7 - '@babel/types': 7.20.7 + '@babel/types': 7.21.3 debug: 4.3.4 globals: 11.12.0 transitivePeerDependencies: @@ -8424,7 +8429,7 @@ packages: '@babel/plugin-syntax-jsx': 7.18.6_@babel+core@7.20.12 '@babel/template': 7.20.7 '@babel/traverse': 7.20.12 - '@babel/types': 7.20.7 + '@babel/types': 7.21.3 '@vue/babel-helper-vue-transform-on': 1.0.2 camelcase: 6.3.0 html-tags: 3.2.0 @@ -17904,11 +17909,6 @@ packages: resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} dev: true - /punycode/2.1.1: - resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==} - engines: {node: '>=6'} - dev: true - /punycode/2.3.0: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} engines: {node: '>=6'} @@ -20261,7 +20261,7 @@ packages: engines: {node: '>=6'} dependencies: psl: 1.9.0 - punycode: 2.1.1 + punycode: 2.3.0 universalify: 0.2.0 url-parse: 1.5.10 dev: true diff --git a/test/mixed-pools/package.json b/test/mixed-pools/package.json new file mode 100644 index 000000000000..f492c216d7c6 --- /dev/null +++ b/test/mixed-pools/package.json @@ -0,0 +1,12 @@ +{ + "name": "@vitest/test-mixed-pools", + "type": "module", + "private": true, + "scripts": { + "test": "vitest", + "coverage": "vitest run --coverage" + }, + "devDependencies": { + "vitest": "workspace:*" + } +} diff --git a/test/mixed-pools/test/child-specific.child_process.test.ts b/test/mixed-pools/test/child-specific.child_process.test.ts new file mode 100644 index 000000000000..b0d5056ce8ee --- /dev/null +++ b/test/mixed-pools/test/child-specific.child_process.test.ts @@ -0,0 +1,11 @@ +import { isMainThread, threadId } from 'node:worker_threads' +import { expect, test } from 'vitest' + +test('has access to child_process API', () => { + expect(process.send).toBeDefined() +}) + +test('doesn\'t have access to threads API', () => { + expect(isMainThread).toBe(true) + expect(threadId).toBe(0) +}) diff --git a/test/mixed-pools/test/threads-specific.threads.test.ts b/test/mixed-pools/test/threads-specific.threads.test.ts new file mode 100644 index 000000000000..19e9209f2e18 --- /dev/null +++ b/test/mixed-pools/test/threads-specific.threads.test.ts @@ -0,0 +1,11 @@ +import { isMainThread, threadId } from 'node:worker_threads' +import { expect, test } from 'vitest' + +test('has access access to worker API', () => { + expect(isMainThread).toBe(false) + expect(threadId).toBeGreaterThan(0) +}) + +test('doesn\'t have access access to child_process API', () => { + expect(process.send).toBeUndefined() +}) diff --git a/test/mixed-pools/vitest.config.ts b/test/mixed-pools/vitest.config.ts new file mode 100644 index 000000000000..01ef30192205 --- /dev/null +++ b/test/mixed-pools/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vite' + +export default defineConfig({ + test: { + include: ['test/*.test.ts'], + poolMatchGlobs: [ + ['**/test/*.child_process.test.ts', 'child_process'], + ['**/test/*.threads.test.ts', 'threads'], + ], + }, +})