Skip to content

Commit c7f0c86

Browse files
authoredMar 19, 2023
feat: add an option to control Vitest pool with filepath (#3029)
1 parent 13005b3 commit c7f0c86

File tree

10 files changed

+198
-55
lines changed

10 files changed

+198
-55
lines changed
 

‎docs/config/index.md

+25
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,31 @@ export default defineConfig({
381381
})
382382
```
383383

384+
### poolMatchGlobs
385+
386+
- **Type:** `[string, 'threads' | 'child_process'][]`
387+
- **Default:** `[]`
388+
- **Version:** Since Vitest 0.29.4
389+
390+
Automatically assign pool in which tests will run based on globs. The first match will be used.
391+
392+
For example:
393+
394+
```ts
395+
import { defineConfig } from 'vitest/config'
396+
397+
export default defineConfig({
398+
test: {
399+
poolMatchGlobs: [
400+
// all tests in "worker-specific" directory will run inside a worker as if you enabled `--threads` for them,
401+
['**/tests/worker-specific/**', 'threads'],
402+
// all other tests will run based on "threads" option, if you didn't specify other globs
403+
// ...
404+
]
405+
}
406+
})
407+
```
408+
384409
### update
385410

386411
- **Type:** `boolean`

‎packages/vitest/src/node/pool.ts

+88-32
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { pathToFileURL } from 'node:url'
2+
import mm from 'micromatch'
23
import { resolve } from 'pathe'
34
import { distDir, rootDir } from '../constants'
5+
import type { VitestPool } from '../types'
46
import type { Vitest } from './core'
57
import { createChildProcessPool } from './pools/child'
68
import { createThreadsPool } from './pools/threads'
@@ -21,38 +23,92 @@ const loaderPath = pathToFileURL(resolve(distDir, './loader.js')).href
2123
const suppressLoaderWarningsPath = resolve(rootDir, './suppress-warnings.cjs')
2224

2325
export function createPool(ctx: Vitest): ProcessPool {
24-
const conditions = ctx.server.config.resolve.conditions?.flatMap(c => ['--conditions', c]) || []
25-
26-
// Instead of passing whole process.execArgv to the workers, pick allowed options.
27-
// Some options may crash worker, e.g. --prof, --title. nodejs/node#41103
28-
const execArgv = process.execArgv.filter(execArg =>
29-
execArg.startsWith('--cpu-prof') || execArg.startsWith('--heap-prof'),
30-
)
31-
32-
const options: PoolProcessOptions = {
33-
execArgv: ctx.config.deps.registerNodeLoader
34-
? [
35-
...execArgv,
36-
'--require',
37-
suppressLoaderWarningsPath,
38-
'--experimental-loader',
39-
loaderPath,
40-
]
41-
: [
42-
...execArgv,
43-
...conditions,
44-
],
45-
env: {
46-
TEST: 'true',
47-
VITEST: 'true',
48-
NODE_ENV: ctx.config.mode || 'test',
49-
VITEST_MODE: ctx.config.watch ? 'WATCH' : 'RUN',
50-
...process.env,
51-
...ctx.config.env,
52-
},
26+
const pools: Record<VitestPool, ProcessPool | null> = {
27+
child_process: null,
28+
threads: null,
29+
}
30+
31+
function getDefaultPoolName() {
32+
if (ctx.config.threads)
33+
return 'threads'
34+
return 'child_process'
35+
}
36+
37+
function getPoolName(file: string) {
38+
for (const [glob, pool] of ctx.config.poolMatchGlobs || []) {
39+
if (mm.isMatch(file, glob, { cwd: ctx.server.config.root }))
40+
return pool
41+
}
42+
return getDefaultPoolName()
43+
}
44+
45+
async function runTests(files: string[], invalidate?: string[]) {
46+
const conditions = ctx.server.config.resolve.conditions?.flatMap(c => ['--conditions', c]) || []
47+
48+
// Instead of passing whole process.execArgv to the workers, pick allowed options.
49+
// Some options may crash worker, e.g. --prof, --title. nodejs/node#41103
50+
const execArgv = process.execArgv.filter(execArg =>
51+
execArg.startsWith('--cpu-prof') || execArg.startsWith('--heap-prof'),
52+
)
53+
54+
const options: PoolProcessOptions = {
55+
execArgv: ctx.config.deps.registerNodeLoader
56+
? [
57+
...execArgv,
58+
'--require',
59+
suppressLoaderWarningsPath,
60+
'--experimental-loader',
61+
loaderPath,
62+
]
63+
: [
64+
...execArgv,
65+
...conditions,
66+
],
67+
env: {
68+
TEST: 'true',
69+
VITEST: 'true',
70+
NODE_ENV: ctx.config.mode || 'test',
71+
VITEST_MODE: ctx.config.watch ? 'WATCH' : 'RUN',
72+
...process.env,
73+
...ctx.config.env,
74+
},
75+
}
76+
77+
const filesByPool = {
78+
child_process: [] as string[],
79+
threads: [] as string[],
80+
browser: [] as string[],
81+
}
82+
83+
if (!ctx.config.poolMatchGlobs) {
84+
const name = getDefaultPoolName()
85+
filesByPool[name] = files
86+
}
87+
else {
88+
for (const file of files) {
89+
const pool = getPoolName(file)
90+
filesByPool[pool].push(file)
91+
}
92+
}
93+
94+
await Promise.all(Object.entries(filesByPool).map(([pool, files]) => {
95+
if (!files.length)
96+
return null
97+
98+
if (pool === 'threads') {
99+
pools.threads ??= createThreadsPool(ctx, options)
100+
return pools.threads.runTests(files, invalidate)
101+
}
102+
103+
pools.child_process ??= createChildProcessPool(ctx, options)
104+
return pools.child_process.runTests(files, invalidate)
105+
}))
53106
}
54107

55-
if (!ctx.config.threads)
56-
return createChildProcessPool(ctx, options)
57-
return createThreadsPool(ctx, options)
108+
return {
109+
runTests,
110+
async close() {
111+
await Promise.all(Object.values(pools).map(p => p?.close()))
112+
},
113+
}
58114
}

‎packages/vitest/src/node/pools/child.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { createMethodsRPC } from './rpc'
1515

1616
const childPath = fileURLToPath(pathToFileURL(resolve(distDir, './child.js')).href)
1717

18-
function setupChildProcessChannel(ctx: Vitest, fork: ChildProcess) {
18+
function setupChildProcessChannel(ctx: Vitest, fork: ChildProcess): void {
1919
createBirpc<{}, RuntimeRPC>(
2020
createMethodsRPC(ctx),
2121
{
@@ -31,16 +31,16 @@ function setupChildProcessChannel(ctx: Vitest, fork: ChildProcess) {
3131
)
3232
}
3333

34-
function stringifyRegex(input: RegExp | string): any {
34+
function stringifyRegex(input: RegExp | string): string {
3535
if (typeof input === 'string')
3636
return input
3737
return `$$vitest:${input.toString()}`
3838
}
3939

40-
function getTestConfig(ctx: Vitest) {
40+
function getTestConfig(ctx: Vitest): ResolvedConfig {
4141
const config = ctx.getSerializableConfig()
4242
// v8 serialize does not support regex
43-
return {
43+
return <ResolvedConfig>{
4444
...config,
4545
testNamePattern: config.testNamePattern
4646
? stringifyRegex(config.testNamePattern)
@@ -83,7 +83,7 @@ export function createChildProcessPool(ctx: Vitest, { execArgv, env }: PoolProce
8383
})
8484
}
8585

86-
async function runWithFiles(files: string[], invalidates: string[] = []) {
86+
async function runWithFiles(files: string[], invalidates: string[] = []): Promise<void> {
8787
ctx.state.clearFiles(files)
8888
const config = getTestConfig(ctx)
8989

@@ -119,6 +119,7 @@ export function createChildProcessPool(ctx: Vitest, { execArgv, env }: PoolProce
119119
if (!child.killed)
120120
child.kill()
121121
})
122+
children.clear()
122123
},
123124
}
124125
}

‎packages/vitest/src/types/config.ts

+16
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export type { SequenceHooks, SequenceSetupFiles } from '@vitest/runner'
1616
export type BuiltinEnvironment = 'node' | 'jsdom' | 'happy-dom' | 'edge-runtime'
1717
// Record is used, so user can get intellisense for builtin environments, but still allow custom environments
1818
export type VitestEnvironment = BuiltinEnvironment | (string & Record<never, never>)
19+
export type VitestPool = 'threads' | 'child_process'
1920
export type CSSModuleScopeStrategy = 'stable' | 'scoped' | 'non-scoped'
2021

2122
export type ApiConfig = Pick<CommonServerOptions, 'port' | 'strictPort' | 'host'>
@@ -162,6 +163,21 @@ export interface InlineConfig {
162163
*/
163164
environmentMatchGlobs?: [string, VitestEnvironment][]
164165

166+
/**
167+
* Automatically assign pool based on globs. The first match will be used.
168+
*
169+
* Format: [glob, pool-name]
170+
*
171+
* @default []
172+
* @example [
173+
* // all tests in "browser" directory will run in an actual browser
174+
* ['tests/browser/**', 'browser'],
175+
* // all other tests will run based on "threads" option, if you didn't specify other globs
176+
* // ...
177+
* ]
178+
*/
179+
poolMatchGlobs?: [string, VitestPool][]
180+
165181
/**
166182
* Update snapshot
167183
*

‎packages/vitest/src/utils/test-helpers.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export async function groupFilesByEnv(files: string[], config: ResolvedConfig) {
2525
// 2. Check for globals
2626
if (!env) {
2727
for (const [glob, target] of config.environmentMatchGlobs || []) {
28-
if (mm.isMatch(file, glob)) {
28+
if (mm.isMatch(file, glob, { cwd: config.root })) {
2929
env = target
3030
break
3131
}

‎pnpm-lock.yaml

+17-17
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎test/mixed-pools/package.json

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "@vitest/test-mixed-pools",
3+
"type": "module",
4+
"private": true,
5+
"scripts": {
6+
"test": "vitest",
7+
"coverage": "vitest run --coverage"
8+
},
9+
"devDependencies": {
10+
"vitest": "workspace:*"
11+
}
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { isMainThread, threadId } from 'node:worker_threads'
2+
import { expect, test } from 'vitest'
3+
4+
test('has access to child_process API', () => {
5+
expect(process.send).toBeDefined()
6+
})
7+
8+
test('doesn\'t have access to threads API', () => {
9+
expect(isMainThread).toBe(true)
10+
expect(threadId).toBe(0)
11+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { isMainThread, threadId } from 'node:worker_threads'
2+
import { expect, test } from 'vitest'
3+
4+
test('has access access to worker API', () => {
5+
expect(isMainThread).toBe(false)
6+
expect(threadId).toBeGreaterThan(0)
7+
})
8+
9+
test('doesn\'t have access access to child_process API', () => {
10+
expect(process.send).toBeUndefined()
11+
})

‎test/mixed-pools/vitest.config.ts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { defineConfig } from 'vite'
2+
3+
export default defineConfig({
4+
test: {
5+
include: ['test/*.test.ts'],
6+
poolMatchGlobs: [
7+
['**/test/*.child_process.test.ts', 'child_process'],
8+
['**/test/*.threads.test.ts', 'threads'],
9+
],
10+
},
11+
})

0 commit comments

Comments
 (0)
Please sign in to comment.