feat!: rewrite pools without tinypool (#8705)

Co-authored-by: Vladimir Sheremet <[email protected]>
This commit is contained in:
Ari Perkkiö
2025-10-21 19:12:54 +02:00
committed by GitHub
co-authored by Vladimir Sheremet
parent 1238067a4a
commit fbddc8024f
117 changed files with 2457 additions and 3113 deletions
+1 -1
View File
@@ -149,7 +149,7 @@ Avoid deps that has large transitive dependencies that results in bloated size c
If there are libraries that are needed and don't comply with our size
requirements, a fork can be tried to reduce its size while we work with them to
upstream our changes (see [tinypool](https://github.com/tinylibs/tinypool) for example)
upstream our changes.
### Think before adding yet another option
-1
View File
@@ -41,7 +41,6 @@ Next generation testing framework powered by Vite.
- [JSDOM](https://github.com/jsdom/jsdom) and [happy-dom](https://github.com/capricorn86/happy-dom) for DOM and browser API mocking
- [Browser Mode](https://vitest.dev/guide/browser/) for running component tests in the browser
- Components testing ([Vue](https://github.com/vitest-tests/browser-examples/tree/main/examples/vue), [React](https://github.com/vitest-tests/browser-examples/tree/main/examples/react), [Svelte](https://github.com/vitest-tests/browser-examples/tree/main/examples/svelte), [Lit](./examples/lit), [Marko](https://github.com/marko-js/examples/tree/master/examples/library-ts))
- Workers multi-threading via [Tinypool](https://github.com/tinylibs/tinypool) (a lightweight fork of [Piscina](https://github.com/piscinajs/piscina))
- Benchmarking support with [Tinybench](https://github.com/tinylibs/tinybench)
- [Projects](https://vitest.dev/guide/projects) support
- [expect-type](https://github.com/mmkal/expect-type) for type-level testing
@@ -10,7 +10,6 @@
<ListItem>Component testing for Vue, React, Svelte, Lit, Marko and more</ListItem>
<ListItem>Out-of-the-box TypeScript / JSX support</ListItem>
<ListItem>ESM first, top level await</ListItem>
<ListItem>Workers multi-threading via <a target="_blank" href="https://github.com/tinylibs/tinypool" rel="noopener noreferrer">Tinypool</a></ListItem>
<ListItem>Benchmarking support with <a target="_blank" href="https://github.com/tinylibs/tinybench" rel="noopener noreferrer">Tinybench</a></ListItem>
<ListItem>Filtering, timeouts, concurrent for suite and tests</ListItem>
<ListItem><a href="/guide/projects">Projects</a> support</ListItem>
+5 -1
View File
@@ -52,7 +52,7 @@ export default ({ mode }: { mode: string }) => {
['link', { rel: 'icon', href: '/favicon.ico', sizes: '48x48' }],
['link', { rel: 'icon', href: '/logo.svg', sizes: 'any', type: 'image/svg+xml' }],
['meta', { name: 'author', content: `${teamMembers.map(c => c.name).join(', ')} and ${vitestName} contributors` }],
['meta', { name: 'keywords', content: 'vitest, vite, test, coverage, snapshot, react, vue, preact, svelte, solid, lit, marko, ruby, cypress, puppeteer, jsdom, happy-dom, test-runner, jest, typescript, esm, tinypool, tinyspy, node' }],
['meta', { name: 'keywords', content: 'vitest, vite, test, coverage, snapshot, react, vue, preact, svelte, solid, lit, marko, ruby, cypress, puppeteer, jsdom, happy-dom, test-runner, jest, typescript, esm, tinyspy, node' }],
['meta', { property: 'og:title', content: vitestName }],
['meta', { property: 'og:description', content: vitestDescription }],
['meta', { property: 'og:url', content: ogUrl }],
@@ -628,6 +628,10 @@ function guide(): DefaultTheme.SidebarItem[] {
},
],
},
{
text: 'Recipes',
link: '/guide/recipes',
},
]
}
+90 -39
View File
@@ -1,10 +1,10 @@
# Custom Pool
::: warning
This is an advanced and very low-level API. If you just want to [run tests](/guide/), you probably don't need this. It is primarily used by library authors.
This is an advanced, experimental and very low-level API. If you just want to [run tests](/guide/), you probably don't need this. It is primarily used by library authors.
:::
Vitest runs tests in pools. By default, there are several pools:
Vitest runs tests in a pool. By default, there are several pool runners:
- `threads` to run tests using `node:worker_threads` (isolation is provided with a new worker context)
- `forks` to run tests using `node:child_process` (isolation is provided with a new `child_process.fork` process)
@@ -12,21 +12,24 @@ Vitest runs tests in pools. By default, there are several pools:
- `browser` to run tests using browser providers
- `typescript` to run typechecking on tests
You can provide your own pool by specifying a file path:
::: tip
See [`vitest-pool-example`](https://www.npmjs.com/package/vitest-pool-example) for example of a custom pool runner implementation.
:::
## Usage
You can provide your own pool runner by a function that returns `PoolRunnerInitializer`.
```ts [vitest.config.ts]
import { defineConfig } from 'vitest/config'
import customPool from './my-custom-pool.ts'
export default defineConfig({
test: {
// will run every file with a custom pool by default
pool: './my-custom-pool.ts',
// you can provide options using `poolOptions` object
poolOptions: {
myCustomPool: {
customProperty: true,
},
},
pool: customPool({
customProperty: true,
})
},
})
```
@@ -34,6 +37,8 @@ export default defineConfig({
If you need to run tests in different pools, use the [`projects`](/guide/projects) feature:
```ts [vitest.config.ts]
import customPool from './my-custom-pool.ts'
export default defineConfig({
test: {
projects: [
@@ -43,6 +48,14 @@ export default defineConfig({
pool: 'threads',
},
},
{
extends: true,
test: {
pool: customPool({
customProperty: true,
})
}
}
],
},
})
@@ -50,47 +63,85 @@ export default defineConfig({
## API
The file specified in `pool` option should export a function (can be async) that accepts `Vitest` interface as its first option. This function needs to return an object matching `ProcessPool` interface:
The `pool` option accepts a `PoolRunnerInitializer` that can be used for custom pool runners. The `name` property should indicate name of the custom pool runner. It should be identical with your worker's `name` property.
```ts
import type { ProcessPool, TestSpecification } from 'vitest/node'
```ts [my-custom-pool.ts]
import type { PoolRunnerInitializer } from 'vitest/node'
export interface ProcessPool {
name: string
runTests: (files: TestSpecification[], invalidates?: string[]) => Promise<void>
collectTests: (files: TestSpecification[], invalidates?: string[]) => Promise<void>
close?: () => Promise<void>
export function customPool(customOptions: CustomOptions): PoolRunnerInitializer {
return {
name: 'custom-pool',
createPoolWorker: options => new CustomPoolWorker(options, customOptions),
}
}
```
The function is called only once (unless the server config was updated), and it's generally a good idea to initialize everything you need for tests inside that function and reuse it when `runTests` is called.
In your `CustomPoolWorker` you need to define all required methods:
Vitest calls `runTest` when new tests are scheduled to run. It will not call it if `files` is empty. The first argument is an array of [TestSpecifications](/advanced/api/test-specification). Files are sorted using [`sequencer`](/config/#sequence-sequencer) before `runTests` is called. It's possible (but unlikely) to have the same file twice, but it will always have a different project - this is implemented via [`projects`](/guide/projects) configuration.
```ts [my-custom-pool.ts]
import type { PoolOptions, PoolWorker, WorkerRequest } from 'vitest/node'
Vitest will wait until `runTests` is executed before finishing a run (i.e., it will emit [`onTestRunEnd`](/advanced/reporters) only after `runTests` is resolved).
class CustomPoolWorker implements PoolWorker {
name = 'custom-pool'
private customOptions: CustomOptions
If you are using a custom pool, you will have to provide test files and their results yourself - you can reference [`vitest.state`](https://github.com/vitest-dev/vitest/blob/main/packages/vitest/src/node/state.ts) for that (most important are `collectFiles` and `updateTasks`). Vitest uses `startTests` function from `@vitest/runner` package to do that.
constructor(options: PoolOptions, customOptions: CustomOptions) {
this.customOptions = customOptions
}
Vitest will call `collectTests` if `vitest.collect` is called or `vitest list` is invoked via a CLI command. It works the same way as `runTests`, but you don't have to run test callbacks, only report their tasks by calling `vitest.state.collectFiles(files)`.
send(message: WorkerRequest): void {
// Provide way to send your worker a message
}
To communicate between different processes, you can create methods object using `createMethodsRPC` from `vitest/node`, and use any form of communication that you prefer. For example, to use WebSockets with `birpc` you can write something like this:
on(event: string, callback: (arg: any) => void): void {
// Provide way to listen to your workers events, e.g. message, error, exit
}
```ts
import { createBirpc } from 'birpc'
import { parse, stringify } from 'flatted'
import { createMethodsRPC, TestProject } from 'vitest/node'
off(event: string, callback: (arg: any) => void): void {
// Provide way to unsubscribe `on` listeners
}
function createRpc(project: TestProject, wss: WebSocketServer) {
return createBirpc(
createMethodsRPC(project),
{
post: msg => wss.send(msg),
on: fn => wss.on('message', fn),
serialize: stringify,
deserialize: parse,
},
)
async start() {
// do something when the worker is started
}
async stop() {
// cleanup the state
}
deserialize(data) {
return data
}
}
```
You can see a simple example of a pool made from scratch that doesn't run tests but marks them as collected in [pool/custom-pool.ts](https://github.com/vitest-dev/vitest/blob/main/test/cli/fixtures/custom-pool/pool/custom-pool.ts).
Your `CustomPoolRunner` will be controlling how your custom test runner worker life cycles and communication channel works. For example, your `CustomPoolRunner` could launch a `node:worker_threads` `Worker`, and provide communication via `Worker.postMessage` and `parentPort`.
In your worker file, you can import helper utilities from `vitest/worker`:
```ts [my-worker.ts]
import { init, runBaseTests } from 'vitest/worker'
init({
post: (response) => {
// Provide way to send this message to CustomPoolRunner's onWorker as message event
},
on: (callback) => {
// Provide a way to listen CustomPoolRunner's "postMessage" calls
},
off: (callback) => {
// Optional, provide a way to remove listeners added by "on" calls
},
teardown: () => {
// Optional, provide a way to teardown worker, e.g. unsubscribe all the `on` listeners
},
serialize: (value) => {
// Optional, provide custom serializer for `post` calls
},
deserialize: (value) => {
// Optional, provide custom deserializer for `on` callbacks
},
runTests: state => runBaseTests('run', state),
collectTests: state => runBaseTests('collect', state),
})
```
+18 -219
View File
@@ -674,7 +674,7 @@ Custom [reporters](/guide/reporters) for output. Reporters can be [a Reporter in
Write test results to a file when the `--reporter=json`, `--reporter=html` or `--reporter=junit` option is also specified.
By providing an object instead of a string you can define individual outputs when using multiple reporters.
### pool<NonProjectOption /> {#pool}
### pool
- **Type:** `'threads' | 'forks' | 'vmThreads' | 'vmForks'`
- **Default:** `'forks'`
@@ -682,19 +682,19 @@ By providing an object instead of a string you can define individual outputs whe
Pool used to run tests in.
#### threads<NonProjectOption />
#### threads
Enable multi-threading using [tinypool](https://github.com/tinylibs/tinypool) (a lightweight fork of [Piscina](https://github.com/piscinajs/piscina)). When using threads you are unable to use process related APIs such as `process.chdir()`. Some libraries written in native languages, such as Prisma, `bcrypt` and `canvas`, have problems when running in multiple threads and run into segfaults. In these cases it is advised to use `forks` pool instead.
Enable multi-threading. When using threads you are unable to use process related APIs such as `process.chdir()`. Some libraries written in native languages, such as Prisma, `bcrypt` and `canvas`, have problems when running in multiple threads and run into segfaults. In these cases it is advised to use `forks` pool instead.
#### forks<NonProjectOption />
#### forks
Similar as `threads` pool but uses `child_process` instead of `worker_threads` via [tinypool](https://github.com/tinylibs/tinypool). Communication between tests and main process is not as fast as with `threads` pool. Process related APIs such as `process.chdir()` are available in `forks` pool.
Similar as `threads` pool but uses `child_process` instead of `worker_threads`. Communication between tests and main process is not as fast as with `threads` pool. Process related APIs such as `process.chdir()` are available in `forks` pool.
#### vmThreads<NonProjectOption />
#### vmThreads
Run tests using [VM context](https://nodejs.org/api/vm.html) (inside a sandboxed environment) in a `threads` pool.
This makes tests run faster, but the VM module is unstable when running [ESM code](https://github.com/nodejs/node/issues/37648). Your tests will [leak memory](https://github.com/nodejs/node/issues/33439) - to battle that, consider manually editing [`poolOptions.vmThreads.memoryLimit`](#pooloptions-vmthreads-memorylimit) value.
This makes tests run faster, but the VM module is unstable when running [ESM code](https://github.com/nodejs/node/issues/37648). Your tests will [leak memory](https://github.com/nodejs/node/issues/33439) - to battle that, consider manually editing [`vmMemoryLimit`](#vmMemorylimit) value.
::: warning
Running code in a sandbox has some advantages (faster tests), but also comes with a number of disadvantages.
@@ -716,166 +716,28 @@ catch (err) {
Please, be aware of these issues when using this option. Vitest team cannot fix any of the issues on our side.
:::
#### vmForks<NonProjectOption />
#### vmForks
Similar as `vmThreads` pool but uses `child_process` instead of `worker_threads` via [tinypool](https://github.com/tinylibs/tinypool). Communication between tests and the main process is not as fast as with `vmThreads` pool. Process related APIs such as `process.chdir()` are available in `vmForks` pool. Please be aware that this pool has the same pitfalls listed in `vmThreads`.
Similar as `vmThreads` pool but uses `child_process` instead of `worker_threads`. Communication between tests and the main process is not as fast as with `vmThreads` pool. Process related APIs such as `process.chdir()` are available in `vmForks` pool. Please be aware that this pool has the same pitfalls listed in `vmThreads`.
### poolOptions<NonProjectOption /> {#pooloptions}
- **Type:** `Record<'threads' | 'forks' | 'vmThreads' | 'vmForks', {}>`
- **Default:** `{}`
#### poolOptions.threads
Options for `threads` pool.
```ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
poolOptions: {
threads: {
// Threads related options here
}
}
}
})
```
##### poolOptions.threads.maxThreads<NonProjectOption />
- **Type:** `number | string`
- **Default:** _available CPUs_
Maximum number or percentage of threads. You can also use `VITEST_MAX_THREADS` environment variable.
##### poolOptions.threads.singleThread
- **Type:** `boolean`
- **Default:** `false`
Run all tests with the same environment inside a single worker thread. This will disable built-in module isolation (your source code or [inlined](#server-deps-inline) code will still be reevaluated for each test), but can improve test performance.
:::warning
Even though this option will force tests to run one after another, this option is different from Jest's `--runInBand`. Vitest uses workers not only for running tests in parallel, but also to provide isolation. By disabling this option, your tests will run sequentially, but in the same global context, so you must provide isolation yourself.
This might cause all sorts of issues, if you are relying on global state (frontend frameworks usually do) or your code relies on environment to be defined separately for each test. But can be a speed boost for your tests (up to 3 times faster), that don't necessarily rely on global state or can easily bypass that.
:::
##### poolOptions.threads.useAtomics<NonProjectOption />
- **Type:** `boolean`
- **Default:** `false`
Use Atomics to synchronize threads.
This can improve performance in some cases, but might cause segfault in older Node versions.
##### poolOptions.threads.isolate
- **Type:** `boolean`
- **Default:** `true`
Isolate environment for each test file.
##### poolOptions.threads.execArgv<NonProjectOption />
### execArgv
- **Type:** `string[]`
- **Default:** `[]`
Pass additional arguments to `node` in the threads. See [Command-line API | Node.js](https://nodejs.org/docs/latest/api/cli.html) for more information.
Pass additional arguments to `node` in the runner worker. See [Command-line API | Node.js](https://nodejs.org/docs/latest/api/cli.html) for more information.
:::warning
Be careful when using, it as some options may crash worker, e.g. --prof, --title. See https://github.com/nodejs/node/issues/41103.
:::
#### poolOptions.forks
Options for `forks` pool.
```ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
poolOptions: {
forks: {
// Forks related options here
}
}
}
})
```
##### poolOptions.forks.maxForks<NonProjectOption />
- **Type:** `number | string`
- **Default:** _available CPUs_
Maximum number or percentage of forks. You can also use `VITEST_MAX_FORKS` environment variable.
##### poolOptions.forks.isolate
- **Type:** `boolean`
- **Default:** `true`
Isolate environment for each test file.
##### poolOptions.forks.singleFork
- **Type:** `boolean`
- **Default:** `false`
Run all tests with the same environment inside a single child process. This will disable built-in module isolation (your source code or [inlined](#server-deps-inline) code will still be reevaluated for each test), but can improve test performance.
:::warning
Even though this option will force tests to run one after another, this option is different from Jest's `--runInBand`. Vitest uses child processes not only for running tests in parallel, but also to provide isolation. By disabling this option, your tests will run sequentially, but in the same global context, so you must provide isolation yourself.
This might cause all sorts of issues, if you are relying on global state (frontend frameworks usually do) or your code relies on environment to be defined separately for each test. But can be a speed boost for your tests (up to 3 times faster), that don't necessarily rely on global state or can easily bypass that.
:::
##### poolOptions.forks.execArgv<NonProjectOption />
- **Type:** `string[]`
- **Default:** `[]`
Pass additional arguments to `node` process in the child processes. See [Command-line API | Node.js](https://nodejs.org/docs/latest/api/cli.html) for more information.
:::warning
Be careful when using, it as some options may crash worker, e.g. --prof, --title. See https://github.com/nodejs/node/issues/41103.
:::
#### poolOptions.vmThreads
Options for `vmThreads` pool.
```ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
poolOptions: {
vmThreads: {
// VM threads related options here
}
}
}
})
```
##### poolOptions.vmThreads.maxThreads<NonProjectOption />
- **Type:** `number | string`
- **Default:** _available CPUs_
Maximum number or percentage of threads. You can also use `VITEST_MAX_THREADS` environment variable.
##### poolOptions.vmThreads.memoryLimit<NonProjectOption />
### vmMemoryLimit
- **Type:** `string | number`
- **Default:** `1 / CPU Cores`
This option affects only `vmForks` and `vmThreads` pools.
Specifies the memory limit for workers before they are recycled. This value heavily depends on your environment, so it's better to specify it manually instead of relying on the default.
::: tip
@@ -900,70 +762,7 @@ The limit can be specified in a number of different ways and whatever the result
Percentage based memory limit [does not work on Linux CircleCI](https://github.com/jestjs/jest/issues/11956#issuecomment-1212925677) workers due to incorrect system memory being reported.
:::
##### poolOptions.vmThreads.useAtomics<NonProjectOption />
- **Type:** `boolean`
- **Default:** `false`
Use Atomics to synchronize threads.
This can improve performance in some cases, but might cause segfault in older Node versions.
##### poolOptions.vmThreads.execArgv<NonProjectOption />
- **Type:** `string[]`
- **Default:** `[]`
Pass additional arguments to `node` process in the VM context. See [Command-line API | Node.js](https://nodejs.org/docs/latest/api/cli.html) for more information.
:::warning
Be careful when using, it as some options may crash worker, e.g. --prof, --title. See https://github.com/nodejs/node/issues/41103.
:::
#### poolOptions.vmForks<NonProjectOption />
Options for `vmForks` pool.
```ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
poolOptions: {
vmForks: {
// VM forks related options here
}
}
}
})
```
##### poolOptions.vmForks.maxForks<NonProjectOption />
- **Type:** `number | string`
- **Default:** _available CPUs_
Maximum number or percentage of forks. You can also use `VITEST_MAX_FORKS` environment variable.
##### poolOptions.vmForks.memoryLimit<NonProjectOption />
- **Type:** `string | number`
- **Default:** `1 / CPU Cores`
Specifies the memory limit for workers before they are recycled. This value heavily depends on your environment, so it's better to specify it manually instead of relying on the default. How the value is calculated is described in [`poolOptions.vmThreads.memoryLimit`](#pooloptions-vmthreads-memorylimit)
##### poolOptions.vmForks.execArgv<NonProjectOption />
- **Type:** `string[]`
- **Default:** `[]`
Pass additional arguments to `node` process in the VM context. See [Command-line API | Node.js](https://nodejs.org/docs/latest/api/cli.html) for more information.
:::warning
Be careful when using, it as some options may crash worker, e.g. --prof, --title. See https://github.com/nodejs/node/issues/41103.
:::
### fileParallelism<NonProjectOption /> {#fileparallelism}
### fileParallelism
- **Type:** `boolean`
- **Default:** `true`
@@ -975,11 +774,11 @@ Should all test files run in parallel. Setting this to `false` will override `ma
This option doesn't affect tests running in the same file. If you want to run those in parallel, use `concurrent` option on [describe](/api/#describe-concurrent) or via [a config](#sequence-concurrent).
:::
### maxWorkers<NonProjectOption /> {#maxworkers}
### maxWorkers
- **Type:** `number | string`
Maximum number or percentage of workers to run tests in. `poolOptions.{threads,vmThreads}.maxThreads`/`poolOptions.forks.maxForks` has higher priority.
Maximum number or percentage of workers to run tests in.
### testTimeout
@@ -2342,7 +2141,7 @@ Run tests in an isolated environment. This option has no effect on `vmThreads` a
Disabling this option might [improve performance](/guide/improving-performance) if your code doesn't rely on side effects (which is usually true for projects with `node` environment).
::: tip
You can disable isolation for specific pools by using [`poolOptions`](#pooloptions) property.
You can disable isolation for specific test files by using Vitest workspaces and disabling isolation per project.
:::
### includeTaskLocation {#includeTaskLocation}
+8 -106
View File
@@ -390,117 +390,19 @@ Enable trace view mode. Supported: "on", "off", "on-first-retry", "on-all-retrie
Specify pool, if not running in the browser (default: `forks`)
### poolOptions.threads.isolate
### execArgv
- **CLI:** `--poolOptions.threads.isolate`
- **Config:** [poolOptions.threads.isolate](/config/#pooloptions-threads-isolate)
- **CLI:** `--execArgv <option>`
- **Config:** [execArgv](/config/#execargv)
Isolate tests in threads pool (default: `true`)
Pass additional arguments to `node` process when spawning `worker_threads` or `child_process`.
### poolOptions.threads.singleThread
### vmMemoryLimit
- **CLI:** `--poolOptions.threads.singleThread`
- **Config:** [poolOptions.threads.singleThread](/config/#pooloptions-threads-singlethread)
- **CLI:** `--vmMemoryLimit <limit>`
- **Config:** [vmMemoryLimit](/config/#vmmemorylimit)
Run tests inside a single thread (default: `false`)
### poolOptions.threads.maxThreads
- **CLI:** `--poolOptions.threads.maxThreads <workers>`
- **Config:** [poolOptions.threads.maxThreads](/config/#pooloptions-threads-maxthreads)
Maximum number or percentage of threads to run tests in
### poolOptions.threads.useAtomics
- **CLI:** `--poolOptions.threads.useAtomics`
- **Config:** [poolOptions.threads.useAtomics](/config/#pooloptions-threads-useatomics)
Use Atomics to synchronize threads. This can improve performance in some cases, but might cause segfault in older Node versions (default: `false`)
### poolOptions.vmThreads.isolate
- **CLI:** `--poolOptions.vmThreads.isolate`
- **Config:** [poolOptions.vmThreads.isolate](/config/#pooloptions-vmthreads-isolate)
Isolate tests in threads pool (default: `true`)
### poolOptions.vmThreads.singleThread
- **CLI:** `--poolOptions.vmThreads.singleThread`
- **Config:** [poolOptions.vmThreads.singleThread](/config/#pooloptions-vmthreads-singlethread)
Run tests inside a single thread (default: `false`)
### poolOptions.vmThreads.maxThreads
- **CLI:** `--poolOptions.vmThreads.maxThreads <workers>`
- **Config:** [poolOptions.vmThreads.maxThreads](/config/#pooloptions-vmthreads-maxthreads)
Maximum number or percentage of threads to run tests in
### poolOptions.vmThreads.useAtomics
- **CLI:** `--poolOptions.vmThreads.useAtomics`
- **Config:** [poolOptions.vmThreads.useAtomics](/config/#pooloptions-vmthreads-useatomics)
Use Atomics to synchronize threads. This can improve performance in some cases, but might cause segfault in older Node versions (default: `false`)
### poolOptions.vmThreads.memoryLimit
- **CLI:** `--poolOptions.vmThreads.memoryLimit <limit>`
- **Config:** [poolOptions.vmThreads.memoryLimit](/config/#pooloptions-vmthreads-memorylimit)
Memory limit for VM threads pool. If you see memory leaks, try to tinker this value.
### poolOptions.forks.isolate
- **CLI:** `--poolOptions.forks.isolate`
- **Config:** [poolOptions.forks.isolate](/config/#pooloptions-forks-isolate)
Isolate tests in forks pool (default: `true`)
### poolOptions.forks.singleFork
- **CLI:** `--poolOptions.forks.singleFork`
- **Config:** [poolOptions.forks.singleFork](/config/#pooloptions-forks-singlefork)
Run tests inside a single child_process (default: `false`)
### poolOptions.forks.maxForks
- **CLI:** `--poolOptions.forks.maxForks <workers>`
- **Config:** [poolOptions.forks.maxForks](/config/#pooloptions-forks-maxforks)
Maximum number or percentage of processes to run tests in
### poolOptions.vmForks.isolate
- **CLI:** `--poolOptions.vmForks.isolate`
- **Config:** [poolOptions.vmForks.isolate](/config/#pooloptions-vmforks-isolate)
Isolate tests in forks pool (default: `true`)
### poolOptions.vmForks.singleFork
- **CLI:** `--poolOptions.vmForks.singleFork`
- **Config:** [poolOptions.vmForks.singleFork](/config/#pooloptions-vmforks-singlefork)
Run tests inside a single child_process (default: `false`)
### poolOptions.vmForks.maxForks
- **CLI:** `--poolOptions.vmForks.maxForks <workers>`
- **Config:** [poolOptions.vmForks.maxForks](/config/#pooloptions-vmforks-maxforks)
Maximum number or percentage of processes to run tests in
### poolOptions.vmForks.memoryLimit
- **CLI:** `--poolOptions.vmForks.memoryLimit <limit>`
- **Config:** [poolOptions.vmForks.memoryLimit](/config/#pooloptions-vmforks-memorylimit)
Memory limit for VM forks pool. If you see memory leaks, try to tinker this value.
Memory limit for VM pools. If you see memory leaks, try to tinker this value.
### fileParallelism
+2 -12
View File
@@ -120,22 +120,12 @@ Vitest also supports debugging tests without IDEs. However this requires that te
```sh
# To run in a single worker
vitest --inspect-brk --pool threads --poolOptions.threads.singleThread
# To run in a single child process
vitest --inspect-brk --pool forks --poolOptions.forks.singleFork
vitest --inspect-brk --no-file-parallelism
# To run in browser mode
vitest --inspect-brk --browser --no-file-parallelism
```
If you are using Vitest 1.1 or higher, you can also just provide `--no-file-parallelism` flag:
```sh
# If pool is unknown
vitest --inspect-brk --no-file-parallelism
```
Once Vitest starts it will stop execution and wait for you to open developer tools that can connect to [Node.js inspector](https://nodejs.org/en/docs/guides/debugging-getting-started/). You can use Chrome DevTools for this by opening `chrome://inspect` on browser.
In watch mode you can keep the debugger open during test re-runs by using the `--poolOptions.threads.isolate false` options.
In watch mode you can keep the debugger open during test re-runs by using the `--isolate false` options.
+2 -3
View File
@@ -34,9 +34,8 @@ Out-of-the-box ES Module / TypeScript / JSX support / PostCSS
## Threads
By default Vitest runs test files in [multiple processes](/guide/parallelism) using [`node:child_process`](https://nodejs.org/api/child_process.html) via [Tinypool](https://github.com/tinylibs/tinypool) (a lightweight fork of [Piscina](https://github.com/piscinajs/piscina)), allowing tests to run simultaneously. If you want to speed up your test suite even further, consider enabling `--pool=threads` to run tests using [`node:worker_threads`](https://nodejs.org/api/worker_threads.html) (beware that some packages might not work with this setup).
To run tests in a single thread or process, see [`poolOptions`](/config/#pooloptions).
By default Vitest runs test files in [multiple processes](/guide/parallelism) using [`node:child_process`](https://nodejs.org/api/child_process.html), allowing tests to run simultaneously. If you want to speed up your test suite even further, consider enabling `--pool=threads` to run tests using [`node:worker_threads`](https://nodejs.org/api/worker_threads.html) (beware that some packages might not work with this setup).
To run tests in a single thread or process, see [`fileParallelism`](/config/#fileParallelism).
Vitest also isolates each file's environment so env mutations in one file don't affect others. Isolation can be disabled by passing `--no-isolate` to the CLI (trading correctness for run performance).
+28 -11
View File
@@ -20,17 +20,34 @@ import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
isolate: false,
// you can also disable isolation only for specific pools
poolOptions: {
forks: {
isolate: false,
},
},
},
})
```
:::
You can also disable isolation for specific files only by using `projects`:
```ts [vitest.config.js]
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
projects: [
{
name: 'Isolated',
isolate: true, // (default value)
exclude: ['**.non-isolated.test.ts'],
},
{
name: 'Non-isolated',
isolate: false,
include: ['**.non-isolated.test.ts'],
}
]
},
})
```
:::tip
If you are using `vmThreads` pool, you cannot disable isolation. Use `threads` pool instead to improve your tests performance.
:::
@@ -179,11 +196,11 @@ To reduce the load from main thread's Vite server you can use test sharding. The
```sh
# Example for splitting tests on 32 CPU to 4 shards.
# As each process needs 1 main thread, there's 7 threads for test runners (1+7)*4 = 32
# Use VITEST_MAX_THREADS or VITEST_MAX_FORKS depending on the pool:
VITEST_MAX_THREADS=7 vitest run --reporter=blob --shard=1/4 & \
VITEST_MAX_THREADS=7 vitest run --reporter=blob --shard=2/4 & \
VITEST_MAX_THREADS=7 vitest run --reporter=blob --shard=3/4 & \
VITEST_MAX_THREADS=7 vitest run --reporter=blob --shard=4/4 & \
# Use VITEST_MAX_WORKERS:
VITEST_MAX_WORKERS=7 vitest run --reporter=blob --shard=1/4 & \
VITEST_MAX_WORKERS=7 vitest run --reporter=blob --shard=2/4 & \
VITEST_MAX_WORKERS=7 vitest run --reporter=blob --shard=3/4 & \
VITEST_MAX_WORKERS=7 vitest run --reporter=blob --shard=4/4 & \
wait # https://man7.org/linux/man-pages/man2/waitpid.2.html
vitest run --merge-reports
+101 -1
View File
@@ -271,6 +271,106 @@ const { getElementError } = utils // [!code ++]
Both `@vitest/browser/context` and `@vitest/browser/utils` work at runtime during the transition period, but they will be removed in a future release.
:::
### Pool Rework
Vitest has used [`tinypool`](https://github.com/tinylibs/tinypool) for orchestrating how test files are run in the test runner workers. Tinypool has controlled how complex tasks like parallelism, isolation and IPC communication works internally. However we've found that Tinypool has some flaws that are slowing down development of Vitest. In Vitest v4 we've completely removed Tinypool and rewritten how pools work without new dependencies. Read more about reasoning from [feat!: rewrite pools without tinypool #8705
](https://github.com/vitest-dev/vitest/pull/8705).
New pool architecture allows Vitest to simplify many previously complex configuration options:
- `maxThreads` and `maxForks` are now `maxWorkers`.
- Environment variables `VITEST_MAX_THREADS` and `VITEST_MAX_FORKS` are now `VITEST_MAX_WORKERS`.
- `singleThread` and `singleFork` are now `maxWorkers: 1`.
- `poolOptions` is removed. All previous `poolOptions` are now top-level options. The `memoryLimit` of VM pools is renamed to `vmMemoryLimit`.
- `threads.useAtomics` is removed. If you have a use case for this, feel free to open a new feature request.
- Custom pool interface has been rewritten, see [Custom Pool](/advanced/pool.html#custom-pool)
```ts
export default defineConfig({
test: {
poolOptions: { // [!code --]
forks: { // [!code --]
execArgv: ['--expose-gc'], // [!code --]
isolate: false, // [!code --]
singleFork: true, // [!code --]
}, // [!code --]
vmThreads: { // [!code --]
memoryLimit: '300Mb' // [!code --]
}, // [!code --]
}, // [!code --]
execArgv: ['--expose-gc'], // [!code ++]
isolate: false, // [!code ++]
maxWorkers: 1, // [!code ++]
memoryLimit: '300Mb', // [!code ++]
}
})
```
Previously it was not possible to specify some pool related options per project when using [Vitest Projects](/guide/projects). With the new architecture this is no longer a blocker.
::: code-group
```ts [Isolation per project]
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
projects: [
{
// Non-isolated unit tests
name: 'Unit tests',
isolate: false,
exclude: ['**.integration.test.ts'],
},
{
// Isolated integration tests
name: 'Integration tests',
include: ['**.integration.test.ts'],
},
],
},
})
```
```ts [Parallel & Sequential projects]
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
projects: [
{
name: 'Parallel',
exclude: ['**.sequantial.test.ts'],
},
{
name: 'Sequential',
include: ['**.sequantial.test.ts'],
fileParallelism: false,
},
],
},
})
```
```ts [Node CLI options per project]
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
projects: [
{
name: 'Production env',
execArgv: ['--env-file=.env.prod']
},
{
name: 'Staging env',
execArgv: ['--env-file=.env.staging']
},
],
},
})
```
:::
See [Recipes](/guide/recipes) for more examples.
### Reporter Updates
Reporter APIs `onCollected`, `onSpecsCollected`, `onPathsCollected`, `onTaskUpdate` and `onFinished` were removed. See [`Reporters API`](/advanced/api/reporters) for new alternatives. The new APIs were introduced in Vitest `v3.0.0`.
@@ -424,7 +524,7 @@ Vitest's `test` names are joined with a `>` symbol to make it easier to distingu
### Envs
Just like Jest, Vitest sets `NODE_ENV` to `test`, if it wasn't set before. Vitest also has a counterpart for `JEST_WORKER_ID` called `VITEST_POOL_ID` (always less than or equal to `maxThreads`), so if you rely on it, don't forget to rename it. Vitest also exposes `VITEST_WORKER_ID` which is a unique ID of a running worker - this number is not affected by `maxThreads`, and will increase with each created worker.
Just like Jest, Vitest sets `NODE_ENV` to `test`, if it wasn't set before. Vitest also has a counterpart for `JEST_WORKER_ID` called `VITEST_POOL_ID` (always less than or equal to `maxWorkers`), so if you rely on it, don't forget to rename it. Vitest also exposes `VITEST_WORKER_ID` which is a unique ID of a running worker - this number is not affected by `maxWorkers`, and will increase with each created worker.
### Replace property
+1 -1
View File
@@ -12,7 +12,7 @@ By default, Vitest runs _test files_ in parallel. Depending on the specified `po
- `forks` (the default) and `vmForks` run tests in different [child processes](https://nodejs.org/api/child_process.html)
- `threads` and `vmThreads` run tests in different [worker threads](https://nodejs.org/api/worker_threads.html)
Both "child processes" and "worker threads" are refered to as "workers". You can configure the number of running workers with [`maxWorkers`](/config/#maxworkers) option. Or more granually with [`poolOptions`](/config/#pooloptions) configuration.
Both "child processes" and "worker threads" are refered to as "workers". You can configure the number of running workers with [`maxWorkers`](/config/#maxworkers) option.
If you have a lot of tests, it is usually faster to run them in parallel, but it also depends on the project, the environment and [isolation](/config/#isolate) state. To disable file parallelisation, you can set [`fileParallelism`](/config/#fileparallelism) to `false`. To learn more about possible performance improvements, read the [Performance Guide](/guide/improving-performance).
+9 -40
View File
@@ -34,54 +34,23 @@ In cases where your test execution time is high, you can generate a profile of t
The `--prof` option does not work with `pool: 'threads'` due to `node:worker_threads` limitations.
:::
To pass these options to Vitest's test runner, define `poolOptions.<pool>.execArgv` in your Vitest configuration:
To pass these options to Vitest's test runner, define `execArgv` in your Vitest configuration:
::: code-group
```ts [Forks]
```ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
pool: 'forks',
poolOptions: {
forks: {
execArgv: [
'--cpu-prof',
'--cpu-prof-dir=test-runner-profile',
'--heap-prof',
'--heap-prof-dir=test-runner-profile'
],
// To generate a single profile
singleFork: true,
},
},
fileParallelism: false,
execArgv: [
'--cpu-prof',
'--cpu-prof-dir=test-runner-profile',
'--heap-prof',
'--heap-prof-dir=test-runner-profile'
],
},
})
```
```ts [Threads]
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
pool: 'threads',
poolOptions: {
threads: {
execArgv: [
'--cpu-prof',
'--cpu-prof-dir=test-runner-profile',
'--heap-prof',
'--heap-prof-dir=test-runner-profile'
],
// To generate a single profile
singleThread: true,
},
},
},
})
```
:::
After the tests have run there should be a `test-runner-profile/*.cpuprofile` and `test-runner-profile/*.heapprofile` files generated. See [Inspecting profiling records](#inspecting-profiling-records) for instructions how to analyze these files.
+55
View File
@@ -0,0 +1,55 @@
---
title: Recipes | Guide
---
# Recipes
## Disabling isolation for specific test files only
You can speed up your test run by disabling isolation for specific set of files by specifying `isolate` per `projects` entries:
```ts [vitest.config.ts]
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
projects: [
{
// Non-isolated unit tests
name: 'Unit tests',
isolate: false,
exclude: ['**.integration.test.ts'],
},
{
// Isolated integration tests
name: 'Integration tests',
include: ['**.integration.test.ts'],
},
],
},
})
```
## Parallel and Sequential test files
You can split test files into parallel and sequential groups by using `projects` option:
```ts [vitest.config.ts]
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
projects: [
{
name: 'Parallel',
exclude: ['**.sequantial.test.ts'],
},
{
name: 'Sequential',
include: ['**.sequantial.test.ts'],
fileParallelism: false,
},
],
},
})
```
+1 -1
View File
@@ -409,7 +409,7 @@ const test = baseTest.extend({
The `worker` scope will run the fixture once per worker. The number of running workers depends on various factors. By default, every file runs in a separate worker, so `file` and `worker` scopes work the same way.
However, if you disable [isolation](/config/#isolate), then the number of workers is limited by the [`maxWorkers`](/config/#maxworkers) or [`poolOptions`](/config/#pooloptions) configuration.
However, if you disable [isolation](/config/#isolate), then the number of workers is limited by the [`maxWorkers`](/config/#maxworkers) configuration.
Note that specifying `scope: 'worker'` when running tests in `vmThreads` or `vmForks` will work the same way as `scope: 'file'`. This limitation exists because every test file has its own VM context, so if Vitest were to initiate it once, one context could leak to another and create many reference inconsistencies (instances of the same class would reference different constructors, for example).
+10 -32
View File
@@ -5,39 +5,17 @@ export default defineConfig({
watch: false,
globalSetup: './global-setup.ts',
// Switch between forks|threads
pool: 'forks',
// Generate a single profile
fileParallelism: false,
poolOptions: {
threads: {
execArgv: [
// https://nodejs.org/api/cli.html#--cpu-prof
'--cpu-prof',
'--cpu-prof-dir=threads-profile',
execArgv: [
// https://nodejs.org/api/cli.html#--cpu-prof
'--cpu-prof',
'--cpu-prof-dir=vitest-profile',
// https://nodejs.org/api/cli.html#--heap-prof
'--heap-prof',
'--heap-prof-dir=threads-profile',
],
// Generate a single profile
singleThread: true,
},
forks: {
execArgv: [
// https://nodejs.org/api/cli.html#--cpu-prof
'--cpu-prof',
'--cpu-prof-dir=forks-profile',
// https://nodejs.org/api/cli.html#--heap-prof
'--heap-prof',
'--heap-prof-dir=forks-profile',
],
// Generate a single profile
singleFork: true,
},
},
// https://nodejs.org/api/cli.html#--heap-prof
'--heap-prof',
'--heap-prof-dir=vitest-profile',
],
},
})
+2 -2
View File
@@ -1,4 +1,4 @@
import type { CancelReason } from '@vitest/runner'
import type { CancelReason, FileSpecification } from '@vitest/runner'
import { getBrowserState } from './utils'
export interface IframeViewportEvent {
@@ -27,7 +27,7 @@ export interface GlobalChannelTestRunCanceledEvent {
export interface IframeExecuteEvent {
event: 'execute'
method: 'run' | 'collect'
files: string[]
files: FileSpecification[]
iframeId: string
context: string
}
+5 -2
View File
@@ -1,4 +1,5 @@
import type { GlobalChannelIncomingEvent, IframeChannelIncomingEvent, IframeChannelOutgoingEvent, IframeViewportDoneEvent, IframeViewportFailEvent } from '@vitest/browser/client'
import type { FileSpecification } from '@vitest/runner'
import type { BrowserTesterOptions, SerializedConfig } from 'vitest'
import { channel, client, globalChannel } from '@vitest/browser/client'
import { generateFileHash } from '@vitest/runner/utils'
@@ -134,13 +135,15 @@ export class IframeOrchestrator {
private async runIsolatedTestInIframe(
container: HTMLDivElement,
file: string,
spec: FileSpecification,
options: BrowserTesterOptions,
startTime: number,
) {
const config = getConfig()
const { width, height } = config.browser.viewport
const file = spec.filepath
if (this.iframes.has(file)) {
this.iframes.get(file)!.remove()
this.iframes.delete(file)
@@ -151,7 +154,7 @@ export class IframeOrchestrator {
// running tests after the "prepare" event
await sendEventToIframe({
event: 'execute',
files: [file],
files: [spec],
method: options.method,
iframeId: file,
context: options.providedContext,
+6 -5
View File
@@ -1,4 +1,5 @@
import type { BrowserRPC, IframeChannelEvent } from '@vitest/browser/client'
import type { FileSpecification } from '@vitest/runner'
import { channel, client, onCancel } from '@vitest/browser/client'
import { parse } from 'flatted'
import { page, server, userEvent } from 'vitest/browser'
@@ -155,7 +156,7 @@ let preparedData:
| Awaited<ReturnType<typeof prepareTestEnvironment>>
| undefined
async function executeTests(method: 'run' | 'collect', files: string[]) {
async function executeTests(method: 'run' | 'collect', files: FileSpecification[]) {
if (!preparedData) {
throw new Error(`Data was not properly initialized. This is a bug in Vitest. Please, open a new issue with reproduction.`)
}
@@ -168,17 +169,17 @@ async function executeTests(method: 'run' | 'collect', files: string[]) {
runner.setMethod(method)
const version = url.searchParams.get('browserv') || ''
files.forEach((filename) => {
const currentVersion = browserHashMap.get(filename)
files.forEach(({ filepath }) => {
const currentVersion = browserHashMap.get(filepath)
if (!currentVersion || currentVersion[1] !== version) {
browserHashMap.set(filename, version)
browserHashMap.set(filepath, version)
}
})
debug?.('prepare time', state.durations.prepare, 'ms')
for (const file of files) {
state.filepath = file
state.filepath = file.filepath
if (method === 'run') {
await startTests([file], runner)
-1
View File
@@ -23,7 +23,6 @@ const stackIgnorePatterns: (string | RegExp)[] = [
'/vite-node/dist/',
'/vite-node/src/',
'/node_modules/chai/',
'/node_modules/tinypool/',
'/node_modules/tinyspy/',
'/vite/dist/node/module-runner',
'/rolldown-vite/dist/node/module-runner',
+4 -1
View File
@@ -104,6 +104,10 @@
"./mocker": {
"types": "./dist/mocker.d.ts",
"default": "./dist/mocker.js"
},
"./worker": {
"types": "./worker.d.ts",
"default": "./dist/worker.js"
}
},
"main": "./dist/index.js",
@@ -186,7 +190,6 @@
"tinybench": "^2.9.0",
"tinyexec": "^0.3.2",
"tinyglobby": "catalog:",
"tinypool": "^2.0.0",
"tinyrainbow": "catalog:",
"vite": "^6.0.0 || ^7.0.0",
"why-is-node-running": "^2.3.0"
+8 -2
View File
@@ -29,12 +29,16 @@ const entries = {
'spy': 'src/integrations/spy.ts',
'coverage': 'src/public/coverage.ts',
'reporters': 'src/public/reporters.ts',
'worker': 'src/public/worker.ts',
'module-runner': 'src/public/module-runner.ts',
'module-evaluator': 'src/runtime/moduleRunner/moduleEvaluator.ts',
// for performance reasons we bundle them separately so we don't import everything at once
'worker-vm': 'src/runtime/worker-vm.ts',
'worker-base': 'src/runtime/worker-base.ts',
// 'worker': 'src/runtime/worker.ts',
'workers/forks': 'src/runtime/workers/forks.ts',
'workers/threads': 'src/runtime/workers/threads.ts',
'workers/vmThreads': 'src/runtime/workers/vmThreads.ts',
'workers/vmForks': 'src/runtime/workers/vmForks.ts',
'workers/runVmTests': 'src/runtime/runVmTests.ts',
@@ -53,6 +57,7 @@ const dtsEntries = {
'reporters': 'src/public/reporters.ts',
'mocker': 'src/public/mocker.ts',
'snapshot': 'src/public/snapshot.ts',
'worker': 'src/public/worker.ts',
'module-evaluator': 'src/runtime/moduleRunner/moduleEvaluator.ts',
}
@@ -68,6 +73,7 @@ const external = [
'node:vm',
'node:http',
'node:console',
'node:events',
'inspector',
'vitest/optional-types.js',
'vitest/browser',
-2
View File
@@ -58,7 +58,6 @@ export const configDefaults: Readonly<{
watch: boolean
globals: boolean
environment: 'node'
pool: 'forks'
clearMocks: boolean
restoreMocks: boolean
mockReset: boolean
@@ -96,7 +95,6 @@ export const configDefaults: Readonly<{
watch: !isCI && process.stdin.isTTY,
globals: false,
environment: 'node' as const,
pool: 'forks' as const,
clearMocks: false,
restoreMocks: false,
mockReset: false,
+8 -79
View File
@@ -1,10 +1,4 @@
import type { ApiConfig } from '../types/config'
import type {
ForksOptions,
ThreadsOptions,
VmOptions,
WorkerContextOptions,
} from '../types/pool-options'
import type { CliOptions } from './cli-api'
import { defaultBrowserPort, defaultPort } from '../../constants'
import { ReportersMap } from '../reporters'
@@ -55,38 +49,6 @@ const apiConfig: (port: number) => CLIOptions<ApiConfig> = (port: number) => ({
middlewareMode: null,
})
const poolThreadsCommands: CLIOptions<ThreadsOptions & WorkerContextOptions> = {
isolate: {
description: 'Isolate tests in threads pool (default: `true`)',
},
singleThread: {
description: 'Run tests inside a single thread (default: `false`)',
},
maxThreads: {
description: 'Maximum number or percentage of threads to run tests in',
argument: '<workers>',
},
useAtomics: {
description:
'Use Atomics to synchronize threads. This can improve performance in some cases, but might cause segfault in older Node versions (default: `false`)',
},
execArgv: null,
}
const poolForksCommands: CLIOptions<ForksOptions & WorkerContextOptions> = {
isolate: {
description: 'Isolate tests in forks pool (default: `true`)',
},
singleFork: {
description: 'Run tests inside a single child_process (default: `false`)',
},
maxForks: {
description: 'Maximum number or percentage of processes to run tests in',
argument: '<workers>',
},
execArgv: null,
}
function watermarkTransform(value: unknown) {
if (typeof value === 'string') {
return value.split(',').map(Number)
@@ -443,47 +405,14 @@ export const cliOptionsConfig: VitestCLIOptions = {
argument: '<pool>',
subcommands: null, // don't support custom objects
},
poolOptions: {
description: 'Specify pool options',
argument: '<options>',
// we use casting here because TypeScript (for some reason) makes this into CLIOption<unknown>
// even when using casting, these types fail if the new option is added which is good
subcommands: {
threads: {
description: 'Specify threads pool options',
argument: '<options>',
subcommands: poolThreadsCommands,
} as CLIOption<ThreadsOptions & WorkerContextOptions>,
vmThreads: {
description: 'Specify VM threads pool options',
argument: '<options>',
subcommands: {
...poolThreadsCommands,
memoryLimit: {
description:
'Memory limit for VM threads pool. If you see memory leaks, try to tinker this value.',
argument: '<limit>',
},
},
} as CLIOption<ThreadsOptions & VmOptions>,
forks: {
description: 'Specify forks pool options',
argument: '<options>',
subcommands: poolForksCommands,
} as CLIOption<ForksOptions & WorkerContextOptions>,
vmForks: {
description: 'Specify VM forks pool options',
argument: '<options>',
subcommands: {
...poolForksCommands,
memoryLimit: {
description:
'Memory limit for VM forks pool. If you see memory leaks, try to tinker this value.',
argument: '<limit>',
},
},
} as CLIOption<ForksOptions & VmOptions>,
},
execArgv: {
description: 'Pass additional arguments to `node` process when spawning `worker_threads` or `child_process`.',
argument: '<option>',
},
vmMemoryLimit: {
description:
'Memory limit for VM pools. If you see memory leaks, try to tinker this value.',
argument: '<limit>',
},
fileParallelism: {
description:
@@ -8,7 +8,6 @@ import type {
UserConfig,
} from '../types/config'
import type { BaseCoverageOptions, CoverageReporterWithOptions } from '../types/coverage'
import type { BuiltinPool, ForksOptions, PoolOptions, ThreadsOptions } from '../types/pool-options'
import crypto from 'node:crypto'
import { slash, toArray } from '@vitest/utils/helpers'
import { resolveModule } from 'local-pkg'
@@ -24,7 +23,6 @@ import {
import { benchmarkConfigDefaults, configDefaults } from '../../defaults'
import { isCI, stdProvider } from '../../utils/env'
import { getWorkersCountByPercentage } from '../../utils/workers'
import { builtinPools } from '../pool'
import { BaseSequencer } from '../sequencers/BaseSequencer'
import { RandomSequencer } from '../sequencers/RandomSequencer'
@@ -143,6 +141,13 @@ export function resolveConfig(
mode,
} as any as ResolvedConfig
if (options.pool && typeof options.pool !== 'string') {
resolved.pool = options.pool.name
resolved.poolRunner = options.pool
}
resolved.pool ??= 'forks'
resolved.project = toArray(resolved.project)
resolved.provide ??= {}
@@ -222,16 +227,10 @@ export function resolveConfig(
}
if (resolved.inspect || resolved.inspectBrk) {
const isSingleThread
= resolved.pool === 'threads'
&& resolved.poolOptions?.threads?.singleThread
const isSingleFork
= resolved.pool === 'forks' && resolved.poolOptions?.forks?.singleFork
if (resolved.fileParallelism && !isSingleThread && !isSingleFork) {
if (resolved.fileParallelism) {
const inspectOption = `--inspect${resolved.inspectBrk ? '-brk' : ''}`
throw new Error(
`You cannot use ${inspectOption} without "--no-file-parallelism", "poolOptions.threads.singleThread" or "poolOptions.forks.singleFork"`,
`You cannot use ${inspectOption} without "--no-file-parallelism"`,
)
}
}
@@ -499,60 +498,19 @@ export function resolveConfig(
delete (resolved as any).resolveSnapshotPath
}
resolved.execArgv ??= []
resolved.pool ??= 'threads'
if (process.env.VITEST_MAX_THREADS) {
resolved.poolOptions = {
...resolved.poolOptions,
threads: {
...resolved.poolOptions?.threads,
maxThreads: Number.parseInt(process.env.VITEST_MAX_THREADS),
},
vmThreads: {
...resolved.poolOptions?.vmThreads,
maxThreads: Number.parseInt(process.env.VITEST_MAX_THREADS),
},
}
if (
resolved.pool === 'vmForks'
|| resolved.pool === 'vmThreads'
|| resolved.pool === 'typescript'
) {
resolved.isolate = false
}
if (process.env.VITEST_MAX_FORKS) {
resolved.poolOptions = {
...resolved.poolOptions,
forks: {
...resolved.poolOptions?.forks,
maxForks: Number.parseInt(process.env.VITEST_MAX_FORKS),
},
vmForks: {
...resolved.poolOptions?.vmForks,
maxForks: Number.parseInt(process.env.VITEST_MAX_FORKS),
},
}
}
const poolThreadsOptions = [
['threads', 'maxThreads'],
['vmThreads', 'maxThreads'],
] as const satisfies [keyof PoolOptions, keyof ThreadsOptions][]
for (const [poolOptionKey, workerOptionKey] of poolThreadsOptions) {
if (resolved.poolOptions?.[poolOptionKey]?.[workerOptionKey]) {
resolved.poolOptions[poolOptionKey]![workerOptionKey] = resolveInlineWorkerOption(resolved.poolOptions[poolOptionKey]![workerOptionKey]!)
}
}
const poolForksOptions = [
['forks', 'maxForks'],
['vmForks', 'maxForks'],
] as const satisfies [keyof PoolOptions, keyof ForksOptions][]
for (const [poolOptionKey, workerOptionKey] of poolForksOptions) {
if (resolved.poolOptions?.[poolOptionKey]?.[workerOptionKey]) {
resolved.poolOptions[poolOptionKey]![workerOptionKey] = resolveInlineWorkerOption(resolved.poolOptions[poolOptionKey]![workerOptionKey]!)
}
}
if (!builtinPools.includes(resolved.pool as BuiltinPool)) {
resolved.pool = resolvePath(resolved.pool, resolved.root)
if (process.env.VITEST_MAX_WORKERS) {
resolved.maxWorkers = Number.parseInt(process.env.VITEST_MAX_WORKERS)
}
if (mode === 'benchmark') {
@@ -846,8 +804,8 @@ export function resolveConfig(
)
}
resolved.testTimeout ??= resolved.browser.enabled ? 15000 : 5000
resolved.hookTimeout ??= resolved.browser.enabled ? 30000 : 10000
resolved.testTimeout ??= resolved.browser.enabled ? 30_000 : 5_000
resolved.hookTimeout ??= resolved.browser.enabled ? 30_000 : 10_000
return resolved
}
@@ -5,16 +5,14 @@ export function serializeConfig(project: TestProject): SerializedConfig {
const { config, globalConfig } = project
const viteConfig = project._vite?.config
const optimizer = config.deps?.optimizer || {}
const poolOptions = config.poolOptions
// Resolve from server.config to avoid comparing against default value
const isolate = viteConfig?.test?.isolate
return {
// TODO: remove functions from environmentOptions
environmentOptions: config.environmentOptions,
mode: config.mode,
isolate: config.isolate,
fileParallelism: config.fileParallelism,
maxWorkers: config.maxWorkers,
base: config.base,
logHeapUsage: config.logHeapUsage,
runner: config.runner,
@@ -63,42 +61,6 @@ export function serializeConfig(project: TestProject): SerializedConfig {
}
})(config.coverage),
fakeTimers: config.fakeTimers,
poolOptions: {
forks: {
singleFork:
poolOptions?.forks?.singleFork
?? globalConfig.poolOptions?.forks?.singleFork
?? false,
isolate:
poolOptions?.forks?.isolate
?? isolate
?? globalConfig.poolOptions?.forks?.isolate
?? true,
},
threads: {
singleThread:
poolOptions?.threads?.singleThread
?? globalConfig.poolOptions?.threads?.singleThread
?? false,
isolate:
poolOptions?.threads?.isolate
?? isolate
?? globalConfig.poolOptions?.threads?.isolate
?? true,
},
vmThreads: {
singleThread:
poolOptions?.vmThreads?.singleThread
?? globalConfig.poolOptions?.vmThreads?.singleThread
?? false,
},
vmForks: {
singleFork:
poolOptions?.vmForks?.singleFork
?? globalConfig.poolOptions?.vmForks?.singleFork
?? false,
},
},
deps: {
web: config.deps.web || {},
optimizer: Object.entries(optimizer).reduce((acc, [name, option]) => {
-1
View File
@@ -1185,7 +1185,6 @@ export class Vitest {
setTimeout(() => {
this.report('onProcessTimeout').then(() => {
console.warn(`close timed out after ${this.config.teardownTimeout}ms`)
this.state.getProcessTimeoutCauses().forEach(cause => console.warn(cause))
if (!this.pool) {
const runningServers = [this._vite, ...this.projects.map(p => p._vite)].filter(Boolean).length
+2 -1
View File
@@ -318,7 +318,8 @@ export class Logger {
process.exitCode = exitCode !== undefined ? (128 + exitCode) : Number(signal)
}
process.exit()
// Timeout to flush stderr
setTimeout(() => process.exit(), 1)
}
process.once('SIGINT', onExit)
-16
View File
@@ -113,22 +113,6 @@ export async function VitestPlugin(
},
},
test: {
poolOptions: {
threads: {
isolate:
options.poolOptions?.threads?.isolate
?? options.isolate
?? testConfig.poolOptions?.threads?.isolate
?? viteConfig.test?.isolate,
},
forks: {
isolate:
options.poolOptions?.forks?.isolate
?? options.isolate
?? testConfig.poolOptions?.forks?.isolate
?? viteConfig.test?.isolate,
},
},
root: testConfig.root ?? viteConfig.test?.root,
deps: testConfig.deps ?? viteConfig.test?.deps,
},
+278 -183
View File
@@ -1,28 +1,26 @@
import type { Awaitable } from '@vitest/utils'
import type { Vitest } from './core'
import type { PoolTask } from './pools/types'
import type { TestProject } from './project'
import type { TestSpecification } from './spec'
import type { BuiltinPool, Pool } from './types/pool-options'
import type { BuiltinPool, ResolvedConfig } from './types/config'
import * as nodeos from 'node:os'
import { isatty } from 'node:tty'
import { resolve } from 'pathe'
import { version as viteVersion } from 'vite'
import { rootDir } from '../paths'
import { isWindows } from '../utils/env'
import { getWorkerMemoryLimit, stringToBytes } from '../utils/memory-limit'
import { groupFilesByEnv } from '../utils/test-helpers'
import { createBrowserPool } from './pools/browser'
import { createForksPool } from './pools/forks'
import { createThreadsPool } from './pools/threads'
import { createTypecheckPool } from './pools/typecheck'
import { createVmForksPool } from './pools/vmForks'
import { createVmThreadsPool } from './pools/vmThreads'
import { Pool } from './pools/pool'
const suppressWarningsPath = resolve(rootDir, './suppress-warnings.cjs')
export type RunWithFiles = (
type RunWithFiles = (
files: TestSpecification[],
invalidates?: string[]
) => Awaitable<void>
type LocalPool = Exclude<Pool, 'browser'>
) => Promise<void>
export interface ProcessPool {
name: string
@@ -45,30 +43,161 @@ export const builtinPools: BuiltinPool[] = [
'typescript',
]
function getDefaultPoolName(project: TestProject): Pool {
export function getFilePoolName(project: TestProject): ResolvedConfig['pool'] {
if (project.config.browser.enabled) {
return 'browser'
}
return project.config.pool
}
export function getFilePoolName(project: TestProject): Pool {
return getDefaultPoolName(project)
}
export function createPool(ctx: Vitest): ProcessPool {
const pools: Record<Pool, ProcessPool | null> = {
forks: null,
threads: null,
browser: null,
vmThreads: null,
vmForks: null,
typescript: null,
const pool = new Pool({
distPath: ctx.distPath,
teardownTimeout: ctx.config.teardownTimeout,
state: ctx.state,
}, ctx.logger)
const options = resolveOptions(ctx)
const Sequencer = ctx.config.sequence.sequencer
const sequencer = new Sequencer(ctx)
let browserPool: ProcessPool | undefined
async function executeTests(method: 'run' | 'collect', specs: TestSpecification[], invalidates?: string[]): Promise<void> {
ctx.onCancel(() => pool.cancel())
if (ctx.config.shard) {
if (!ctx.config.passWithNoTests && ctx.config.shard.count > specs.length) {
throw new Error(
'--shard <count> must be a smaller than count of test files. '
+ `Resolved ${specs.length} test files for --shard=${ctx.config.shard.index}/${ctx.config.shard.count}.`,
)
}
specs = await sequencer.shard(Array.from(specs))
}
const taskGroups: {
tasks: PoolTask[]
maxWorkers: number
// browser pool has a more complex logic, so we keep it separately for now
browserSpecs: TestSpecification[]
}[] = []
let workerId = 0
const sorted = await sequencer.sort(specs)
const groups = groupSpecs(sorted)
for (const group of groups) {
if (!group) {
continue
}
const taskGroup: PoolTask[] = []
const browserSpecs: TestSpecification[] = []
taskGroups.push({
tasks: taskGroup,
maxWorkers: group.maxWorkers,
browserSpecs,
})
for (const specs of group.specs) {
const { project, pool } = specs[0]
if (pool === 'browser') {
browserSpecs.push(...specs)
continue
}
const byEnv = await groupFilesByEnv(specs)
const env = Object.values(byEnv)[0][0]
taskGroup.push({
context: {
pool,
config: project.serializedConfig,
files: specs.map(spec => ({ filepath: spec.moduleId, testLocations: spec.testLines })),
invalidates,
environment: env.environment,
projectName: project.name,
providedContext: project.getProvidedContext(),
workerId: workerId++,
},
project,
env: options.env,
execArgv: [...options.execArgv, ...project.config.execArgv],
worker: pool,
isolate: project.config.isolate,
memoryLimit: getMemoryLimit(ctx.config, pool) ?? null,
})
}
}
const results: PromiseSettledResult<void>[] = []
for (const { tasks, browserSpecs, maxWorkers } of taskGroups) {
pool.setMaxWorkers(maxWorkers)
const promises = tasks.map(async (task) => {
if (ctx.isCancelling) {
return ctx.state.cancelFiles(task.context.files, task.project)
}
try {
await pool.run(task, method)
}
catch (error) {
// Intentionally cancelled
if (ctx.isCancelling && error instanceof Error && error.message === 'Cancelled') {
ctx.state.cancelFiles(task.context.files, task.project)
}
else {
throw error
}
}
})
if (browserSpecs.length) {
browserPool ??= createBrowserPool(ctx)
if (method === 'collect') {
promises.push(browserPool.collectTests(browserSpecs))
}
else {
promises.push(browserPool.runTests(browserSpecs))
}
}
const groupResults = await Promise.allSettled(promises)
results.push(...groupResults)
}
const errors = results
.filter(result => result.status === 'rejected')
.map(result => result.reason)
if (errors.length > 0) {
throw new AggregateError(
errors,
'Errors occurred while running tests. For more information, see serialized error.',
)
}
}
return {
name: 'default',
runTests: (files, invalidates) => executeTests('run', files, invalidates),
collectTests: (files, invalidates) => executeTests('collect', files, invalidates),
async close() {
await Promise.all([pool.close(), browserPool?.close?.()])
},
}
}
function resolveOptions(ctx: Vitest) {
// in addition to resolve.conditions Vite also adds production/development,
// see: https://github.com/vitejs/vite/blob/af2aa09575229462635b7cbb6d248ca853057ba2/packages/vite/src/node/plugins/resolve.ts#L1056-L1080
const viteMajor = Number(viteVersion.split('.')[0])
const potentialConditions = new Set(viteMajor >= 6
? (ctx.vite.config.ssr.resolve?.conditions ?? [])
: [
@@ -76,6 +205,7 @@ export function createPool(ctx: Vitest): ProcessPool {
'development',
...ctx.vite.config.resolve.conditions,
])
const conditions = [...potentialConditions]
.filter((condition) => {
if (condition === 'production') {
@@ -103,167 +233,132 @@ export function createPool(ctx: Vitest): ProcessPool {
|| execArg.startsWith('--diagnostic-dir'),
)
async function executeTests(method: 'runTests' | 'collectTests', files: TestSpecification[], invalidate?: string[]) {
const options: PoolProcessOptions = {
execArgv: [
...execArgv,
...conditions,
'--experimental-import-meta-resolve',
'--require',
suppressWarningsPath,
],
env: {
TEST: 'true',
VITEST: 'true',
NODE_ENV: process.env.NODE_ENV || 'test',
VITEST_MODE: ctx.config.watch ? 'WATCH' : 'RUN',
FORCE_TTY: isatty(1) ? 'true' : '',
...process.env,
...ctx.config.env,
},
}
// env are case-insensitive on Windows, but spawned processes don't support it
if (isWindows) {
for (const name in options.env) {
options.env[name.toUpperCase()] = options.env[name]
}
}
const poolConcurrentPromises = new Map<string, Promise<ProcessPool>>()
const customPools = new Map<string, ProcessPool>()
async function resolveCustomPool(filepath: string) {
if (customPools.has(filepath)) {
return customPools.get(filepath)!
}
const pool = await ctx.runner.import(filepath)
if (typeof pool.default !== 'function') {
throw new TypeError(
`Custom pool "${filepath}" must export a function as default export`,
)
}
const poolInstance = await pool.default(ctx, options)
if (typeof poolInstance?.name !== 'string') {
throw new TypeError(
`Custom pool "${filepath}" should return an object with "name" property`,
)
}
if (typeof poolInstance?.[method] !== 'function') {
throw new TypeError(
`Custom pool "${filepath}" should return an object with "${method}" method`,
)
}
customPools.set(filepath, poolInstance)
return poolInstance as ProcessPool
}
function getConcurrentPool(pool: string, fn: () => Promise<ProcessPool>) {
if (poolConcurrentPromises.has(pool)) {
return poolConcurrentPromises.get(pool)!
}
const promise = fn().finally(() => {
poolConcurrentPromises.delete(pool)
})
poolConcurrentPromises.set(pool, promise)
return promise
}
function getCustomPool(pool: string) {
return getConcurrentPool(pool, () => resolveCustomPool(pool))
}
const groupedSpecifications: Record<string, TestSpecification[]> = {}
const groups = new Set<number>()
const factories: Record<LocalPool, (specs: TestSpecification[]) => ProcessPool> = {
vmThreads: specs => createVmThreadsPool(ctx, options, specs),
vmForks: specs => createVmForksPool(ctx, options, specs),
threads: specs => createThreadsPool(ctx, options, specs),
forks: specs => createForksPool(ctx, options, specs),
typescript: () => createTypecheckPool(ctx),
browser: () => createBrowserPool(ctx),
}
for (const spec of files) {
const group = spec.project.config.sequence.groupOrder ?? 0
groups.add(group)
groupedSpecifications[group] ??= []
groupedSpecifications[group].push(spec)
}
const Sequencer = ctx.config.sequence.sequencer
const sequencer = new Sequencer(ctx)
async function sortSpecs(specs: TestSpecification[]) {
if (ctx.config.shard) {
if (!ctx.config.passWithNoTests && ctx.config.shard.count > specs.length) {
throw new Error(
'--shard <count> must be a smaller than count of test files. '
+ `Resolved ${specs.length} test files for --shard=${ctx.config.shard.index}/${ctx.config.shard.count}.`,
)
}
specs = await sequencer.shard(specs)
}
return sequencer.sort(specs)
}
const sortedGroups = Array.from(groups).sort()
for (const group of sortedGroups) {
const specifications = groupedSpecifications[group]
if (!specifications?.length) {
continue
}
const filesByPool: Record<LocalPool, TestSpecification[]> = {
forks: [],
threads: [],
vmThreads: [],
vmForks: [],
typescript: [],
}
specifications.forEach((specification) => {
const pool = specification.pool
filesByPool[pool] ??= []
filesByPool[pool].push(specification)
})
await Promise.all(
Object.entries(filesByPool).map(async (entry) => {
const [pool, files] = entry as [Pool, TestSpecification[]]
if (!files.length) {
return null
}
const specs = await sortSpecs(files)
if (pool in factories) {
const factory = factories[pool]
pools[pool] ??= factory(specs)
return pools[pool]![method](specs, invalidate)
}
const poolHandler = await getCustomPool(pool)
pools[poolHandler.name] ??= poolHandler
return poolHandler[method](specs, invalidate)
}),
)
}
}
return {
name: 'default',
runTests: (files, invalidates) => executeTests('runTests', files, invalidates),
collectTests: (files, invalidates) => executeTests('collectTests', files, invalidates),
async close() {
await Promise.all(Object.values(pools).map(p => p?.close?.()))
const options: PoolProcessOptions = {
execArgv: [
...execArgv,
...conditions,
'--experimental-import-meta-resolve',
'--require',
suppressWarningsPath,
],
env: {
TEST: 'true',
VITEST: 'true',
NODE_ENV: process.env.NODE_ENV || 'test',
VITEST_MODE: ctx.config.watch ? 'WATCH' : 'RUN',
FORCE_TTY: isatty(1) ? 'true' : '',
...process.env,
...ctx.config.env,
},
}
// env are case-insensitive on Windows, but spawned processes don't support it
if (isWindows) {
for (const name in options.env) {
options.env[name.toUpperCase()] = options.env[name]
}
}
return options
}
function resolveMaxWorkers(project: TestProject) {
if (project.config.maxWorkers) {
return project.config.maxWorkers
}
if (project.vitest.config.maxWorkers) {
return project.vitest.config.maxWorkers
}
const numCpus = typeof nodeos.availableParallelism === 'function'
? nodeos.availableParallelism()
: nodeos.cpus().length
if (project.vitest.config.watch) {
return Math.max(Math.floor(numCpus / 2), 1)
}
return Math.max(numCpus - 1, 1)
}
function getMemoryLimit(config: ResolvedConfig, pool: string) {
if (pool !== 'vmForks' && pool !== 'vmThreads') {
return null
}
const memory = nodeos.totalmem()
const limit = getWorkerMemoryLimit(config)
if (typeof memory === 'number') {
return stringToBytes(limit, config.watch ? memory / 2 : memory)
}
// If totalmem is not supported we cannot resolve percentage based values like 0.5, "50%"
if (
(typeof limit === 'number' && limit > 1)
|| (typeof limit === 'string' && limit.at(-1) !== '%')
) {
return stringToBytes(limit)
}
// just ignore "memoryLimit" value because we cannot detect memory limit
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?
type SpecsForRunner = TestSpecification[]
// Tests in a single group are executed with `maxWorkers` parallelism.
// Next group starts running after previous finishes - allows real sequential tests.
interface Groups { specs: SpecsForRunner[]; maxWorkers: number }
const groups: Groups[] = []
// Files without file parallelism but without explicit sequence.groupOrder
const sequential: Groups = { specs: [], maxWorkers: 1 }
// Type tests are run in a single group, per project
const typechecks: Record<string, TestSpecification[]> = {}
specs.forEach((spec) => {
if (spec.pool === 'typescript') {
typechecks[spec.project.name] ||= []
typechecks[spec.project.name].push(spec)
return
}
const order = spec.project.config.sequence.groupOrder
// Files that have disabled parallelism and default groupId are set into their own group
if (order === 0 && spec.project.config.fileParallelism === false) {
return sequential.specs.push([spec])
}
const maxWorkers = resolveMaxWorkers(spec.project)
groups[order] ||= { specs: [], maxWorkers }
// Multiple projects with different maxWorkers but same groupId
if (groups[order].maxWorkers !== maxWorkers) {
const last = groups[order].specs.at(-1)?.at(-1)?.project.name
throw new Error(`Projects "${last}" and "${spec.project.name}" have different 'maxWorkers' but same 'sequence.groupId'.\nProvide unique 'sequence.groupId' for them.`)
}
groups[order].specs.push([spec])
})
for (const project in typechecks) {
const order = Math.max(0, ...groups.keys()) + 1
groups[order] ||= { specs: [], maxWorkers: resolveMaxWorkers(typechecks[project][0].project) }
groups[order].specs.push(typechecks[project])
}
if (sequential.specs.length) {
groups.push(sequential)
}
return groups
}
+16 -9
View File
@@ -1,3 +1,4 @@
import type { FileSpecification } from '@vitest/runner'
import type { DeferPromise } from '@vitest/utils/helpers'
import type { Vitest } from '../core'
import type { ProcessPool } from '../pool'
@@ -20,9 +21,12 @@ export function createBrowserPool(vitest: Vitest): ProcessPool {
? nodeos.availableParallelism()
: nodeos.cpus().length
// if there are more than ~12 threads (optimistically), the main thread chokes
// https://github.com/vitest-dev/vitest/issues/7871
const maxThreadsCount = Math.min(12, numCpus - 1)
const threadsCount = vitest.config.watch
? Math.max(Math.floor(numCpus / 2), 1)
: Math.max(numCpus - 1, 1)
? Math.max(Math.floor(maxThreadsCount / 2), 1)
: Math.max(maxThreadsCount, 1)
const projectPools = new WeakMap<TestProject, BrowserPool>()
@@ -55,10 +59,13 @@ export function createBrowserPool(vitest: Vitest): ProcessPool {
}
const runWorkspaceTests = async (method: 'run' | 'collect', specs: TestSpecification[]) => {
const groupedFiles = new Map<TestProject, string[]>()
for (const { project, moduleId } of specs) {
const groupedFiles = new Map<TestProject, FileSpecification[]>()
for (const { project, moduleId, testLines } of specs) {
const files = groupedFiles.get(project) || []
files.push(moduleId)
files.push({
filepath: moduleId,
testLocations: testLines,
})
groupedFiles.set(project, files)
}
@@ -81,7 +88,7 @@ export function createBrowserPool(vitest: Vitest): ProcessPool {
debug?.('provider is ready for %s project', project.name)
const pool = ensurePool(project)
vitest.state.clearFiles(project, files)
vitest.state.clearFiles(project, files.map(f => f.filepath))
providers.add(project.browser!.provider)
return {
@@ -163,7 +170,7 @@ function escapePathToRegexp(path: string): string {
}
class BrowserPool {
private _queue: string[] = []
private _queue: FileSpecification[] = []
private _promise: DeferPromise<void> | undefined
private _providedContext: string | undefined
@@ -191,7 +198,7 @@ class BrowserPool {
return this.project.browser!.state.orchestrators
}
async runTests(method: 'run' | 'collect', files: string[]): Promise<void> {
async runTests(method: 'run' | 'collect', files: FileSpecification[]): Promise<void> {
this._promise ??= createDefer<void>()
if (!files.length) {
@@ -311,7 +318,7 @@ class BrowserPool {
const orchestrator = this.getOrchestrator(sessionId)
debug?.('[%s] run test %s', sessionId, file)
this.setBreakpoint(sessionId, file).then(() => {
this.setBreakpoint(sessionId, file.filepath).then(() => {
// this starts running tests inside the orchestrator
orchestrator.createTesters(
{
-307
View File
@@ -1,307 +0,0 @@
import type { FileSpecification } from '@vitest/runner'
import type { TinypoolChannel, Options as TinypoolOptions } from 'tinypool'
import type { RunnerRPC, RuntimeRPC } from '../../types/rpc'
import type { ContextRPC, ContextTestEnvironment } from '../../types/worker'
import type { Vitest } from '../core'
import type { PoolProcessOptions, ProcessPool, RunWithFiles } from '../pool'
import type { TestProject } from '../project'
import type { TestSpecification } from '../spec'
import type { SerializedConfig } from '../types/config'
import EventEmitter from 'node:events'
import * as nodeos from 'node:os'
import { resolve } from 'node:path'
import v8 from 'node:v8'
import { createBirpc } from 'birpc'
import { Tinypool } from 'tinypool'
import { groupBy } from '../../utils/base'
import { wrapSerializableConfig } from '../../utils/config-helpers'
import { envsOrder, groupFilesByEnv } from '../../utils/test-helpers'
import { createMethodsRPC } from './rpc'
function createChildProcessChannel(project: TestProject, collect = false) {
const emitter = new EventEmitter()
const events = { message: 'message', response: 'response' }
const rpc = createBirpc<RunnerRPC, RuntimeRPC>(createMethodsRPC(project, { cacheFs: true, collect }), {
eventNames: ['onCancel'],
serialize: v8.serialize,
deserialize: (v) => {
try {
return v8.deserialize(Buffer.from(v))
}
catch (error) {
let stringified = ''
try {
stringified = `\nReceived value: ${JSON.stringify(v)}`
}
catch {}
throw new Error(`[vitest-pool]: Unexpected call to process.send(). Make sure your test cases are not interfering with process's channel.${stringified}`, { cause: error })
}
},
post(v) {
emitter.emit(events.message, v)
},
on(fn) {
emitter.on(events.response, fn)
},
timeout: -1,
})
project.vitest.onCancel(reason => rpc.onCancel(reason))
const channel: TinypoolChannel = {
onMessage: callback => emitter.on(events.message, callback),
postMessage: message => emitter.emit(events.response, message),
onClose: () => {
emitter.removeAllListeners()
rpc.$close(new Error('[vitest-pool]: Pending methods while closing rpc'))
},
}
return channel
}
export function createForksPool(
vitest: Vitest,
{ execArgv, env }: PoolProcessOptions,
specifications: TestSpecification[],
): ProcessPool {
const numCpus
= typeof nodeos.availableParallelism === 'function'
? nodeos.availableParallelism()
: nodeos.cpus().length
const threadsCount = vitest.config.watch
? Math.max(Math.floor(numCpus / 2), 1)
: Math.max(numCpus - 1, 1)
const recommendedCount = vitest.config.watch
? threadsCount
: Math.min(threadsCount, specifications.length)
const poolOptions = vitest.config.poolOptions?.forks ?? {}
const maxThreads
= poolOptions.maxForks ?? vitest.config.maxWorkers ?? recommendedCount
const minThreads = vitest.config.watch
? Math.min(recommendedCount, maxThreads)
// avoid recreating threads when tests are finished
: 0
const options: TinypoolOptions = {
runtime: 'child_process',
filename: resolve(vitest.distPath, 'worker-base.js'),
teardown: 'teardown',
maxThreads,
minThreads,
env,
execArgv: [...(poolOptions.execArgv ?? []), ...execArgv],
terminateTimeout: vitest.config.teardownTimeout,
concurrentTasksPerWorker: 1,
}
const isolated = poolOptions.isolate ?? true
if (isolated) {
options.isolateWorkers = true
}
if (poolOptions.singleFork || !vitest.config.fileParallelism) {
options.maxThreads = 1
options.minThreads = 1
}
const pool = new Tinypool(options)
const runWithFiles = (name: string): RunWithFiles => {
let id = 0
async function runFiles(
project: TestProject,
config: SerializedConfig,
files: FileSpecification[],
environment: ContextTestEnvironment,
invalidates: string[] = [],
) {
const paths = files.map(f => f.filepath)
vitest.state.clearFiles(project, paths)
const channel = createChildProcessChannel(project, name === 'collect')
const workerId = ++id
const data: ContextRPC = {
pool: 'forks',
config,
files,
invalidates,
environment,
workerId,
projectName: project.name,
providedContext: project.getProvidedContext(),
}
try {
await pool.run(data, { name, channel })
}
catch (error) {
// Worker got stuck and won't terminate - this may cause process to hang
if (
error instanceof Error
&& /Failed to terminate worker/.test(error.message)
) {
vitest.state.addProcessTimeoutCause(
`Failed to terminate worker while running ${paths.join(', ')}.`,
)
}
// Intentionally cancelled
else if (
vitest.isCancelling
&& error instanceof Error
&& /The task has been cancelled/.test(error.message)
) {
vitest.state.cancelFiles(paths, project)
}
else {
throw error
}
}
}
return async (specs, invalidates) => {
// Cancel pending tasks from pool when possible
vitest.onCancel(() => pool.cancelPendingTasks())
const configs = new WeakMap<TestProject, SerializedConfig>()
const getConfig = (project: TestProject): SerializedConfig => {
if (configs.has(project)) {
return configs.get(project)!
}
const _config = project.serializedConfig
const config = wrapSerializableConfig(_config)
configs.set(project, config)
return config
}
const singleFork = specs.filter(
spec => spec.project.config.poolOptions?.forks?.singleFork,
)
const multipleForks = specs.filter(
spec => !spec.project.config.poolOptions?.forks?.singleFork,
)
if (multipleForks.length) {
const filesByEnv = await groupFilesByEnv(multipleForks)
const files = Object.values(filesByEnv).flat()
const results: PromiseSettledResult<void>[] = []
if (isolated) {
results.push(
...(await Promise.allSettled(
files.map(({ file, environment, project }) =>
runFiles(
project,
getConfig(project),
[file],
environment,
invalidates,
),
),
)),
)
}
else {
// When isolation is disabled, we still need to isolate environments and workspace projects from each other.
// Tasks are still running parallel but environments are isolated between tasks.
const grouped = groupBy(
files,
({ project, environment }) =>
project.name
+ environment.name
+ JSON.stringify(environment.options),
)
for (const group of Object.values(grouped)) {
// Push all files to pool's queue
results.push(
...(await Promise.allSettled(
group.map(({ file, environment, project }) =>
runFiles(
project,
getConfig(project),
[file],
environment,
invalidates,
),
),
)),
)
// Once all tasks are running or finished, recycle worker for isolation.
// On-going workers will run in the previous environment.
await new Promise<void>(resolve =>
pool.queueSize === 0 ? resolve() : pool.once('drain', resolve),
)
await pool.recycleWorkers()
}
}
const errors = results
.filter((r): r is PromiseRejectedResult => r.status === 'rejected')
.map(r => r.reason)
if (errors.length > 0) {
throw new AggregateError(
errors,
'Errors occurred while running tests. For more information, see serialized error.',
)
}
}
if (singleFork.length) {
const filesByEnv = await groupFilesByEnv(singleFork)
const envs = envsOrder.concat(
Object.keys(filesByEnv).filter(env => !envsOrder.includes(env)),
)
for (const env of envs) {
const files = filesByEnv[env]
if (!files?.length) {
continue
}
const filesByOptions = groupBy(
files,
({ project, environment }) =>
project.name + JSON.stringify(environment.options),
)
for (const files of Object.values(filesByOptions)) {
// Always run environments isolated between each other
await pool.recycleWorkers()
const filenames = files.map(f => f.file)
await runFiles(
files[0].project,
getConfig(files[0].project),
filenames,
files[0].environment,
invalidates,
)
}
}
}
}
}
return {
name: 'forks',
runTests: runWithFiles('run'),
collectTests: runWithFiles('collect'),
close: () => pool.destroy(),
}
}
+297
View File
@@ -0,0 +1,297 @@
import type { Logger } from '../logger'
import type { StateManager } from '../state'
import type { PoolOptions, PoolTask, WorkerResponse } from './types'
import { PoolRunner } from './poolRunner'
import { ForksPoolWorker } from './workers/forksWorker'
import { ThreadsPoolWorker } from './workers/threadsWorker'
import { TypecheckPoolWorker } from './workers/typecheckWorker'
import { VmForksPoolWorker } from './workers/vmForksWorker'
import { VmThreadsPoolWorker } from './workers/vmThreadsWorker'
const WORKER_START_TIMEOUT = 5_000
interface Options {
distPath: string
teardownTimeout: number
state: StateManager
}
interface QueuedTask {
task: PoolTask
resolver: ReturnType<typeof withResolvers>
method: 'run' | 'collect'
}
interface ActiveTask extends QueuedTask {
cancelTask: () => Promise<void>
}
export class Pool {
private maxWorkers: number = 0
private workerIds = new Map<number, boolean>()
private queue: QueuedTask[] = []
private activeTasks: ActiveTask[] = []
private sharedRunners: PoolRunner[] = []
private exitPromises: Promise<void>[] = []
private _isCancelling: boolean = false
constructor(private options: Options, private logger: Logger) {}
setMaxWorkers(maxWorkers: number): void {
this.maxWorkers = maxWorkers
this.workerIds = new Map(
Array.from({ length: maxWorkers }).fill(0).map((_, i) => [i + 1, true]),
)
}
async run(task: PoolTask, method: 'run' | 'collect'): Promise<void> {
// Prevent new tasks from being queued during cancellation
if (this._isCancelling) {
throw new Error('[vitest-pool]: Cannot run tasks while pool is cancelling')
}
// Every runner related failure should make this promise reject so that it's picked by pool.
// This resolver is used to make the error handling in recursive queue easier.
const testFinish = withResolvers()
this.queue.push({ task, resolver: testFinish, method })
void this.schedule()
await testFinish.promise
}
private async schedule(): Promise<void> {
if (this.queue.length === 0 || this.activeTasks.length >= this.maxWorkers) {
return
}
const { task, resolver, method } = this.queue.shift()!
try {
let isMemoryLimitReached = false
const runner = this.getPoolRunner(task, method)
const activeTask = { task, resolver, method, cancelTask }
this.activeTasks.push(activeTask)
runner.on('error', (error) => {
resolver.reject(
new Error(`[vitest-pool]: Worker ${task.worker} emitted error.`, { cause: error }),
)
})
async function cancelTask() {
await runner.stop()
resolver.reject(new Error('Cancelled'))
}
const onFinished = (message: WorkerResponse) => {
if (message?.__vitest_worker_response__ && message.type === 'testfileFinished') {
if (task.memoryLimit && message.usedMemory) {
isMemoryLimitReached = message.usedMemory >= task.memoryLimit
}
if (message.error) {
this.options.state.catchError(message.error, 'Test Run Error')
}
runner.off('message', onFinished)
resolver.resolve()
}
}
runner.on('message', onFinished)
if (!runner.isStarted) {
const id = setTimeout(
() => resolver.reject(new Error(`[vitest-pool]: Timeout starting ${task.worker} runner.`)),
WORKER_START_TIMEOUT,
)
await runner.start().finally(() => clearTimeout(id))
}
const poolId = runner.poolId ?? this.getWorkerId()
runner.poolId = poolId
// Start running the test in the worker
runner.postMessage({
__vitest_worker_request__: true,
type: method,
context: task.context,
poolId,
})
await resolver.promise
const index = this.activeTasks.indexOf(activeTask)
if (index !== -1) {
this.activeTasks.splice(index, 1)
}
if (
!task.isolate
&& !isMemoryLimitReached
&& this.queue[0]?.task.isolate === false
&& isEqualRunner(runner, this.queue[0].task)
) {
this.sharedRunners.push(runner)
return this.schedule()
}
// Runner terminations are started but not awaited until the end of full run.
// Runner termination can also already start from task cancellation.
if (!runner.isTerminated) {
const id = setTimeout(
() => this.logger.error(`[vitest-pool]: Timeout terminating ${task.worker} worker for test files ${formatFiles(task)}.`),
this.options.teardownTimeout,
)
this.exitPromises.push(
runner.stop()
.then(() => clearTimeout(id))
.catch(error => this.logger.error(`[vitest-pool]: Failed to terminate ${task.worker} worker for test files ${formatFiles(task)}.`, error)),
)
}
this.freeWorkerId(poolId)
}
// This is mostly to avoid zombie workers when/if Vitest internals run into errors
catch (error) {
return resolver.reject(error)
}
return this.schedule()
}
async cancel(): Promise<void> {
// Set flag to prevent new tasks from being queued
this._isCancelling = true
const pendingTasks = this.queue.splice(0)
if (pendingTasks.length) {
const error = new Error('Cancelled')
pendingTasks.forEach(task => task.resolver.reject(error))
}
const activeTasks = this.activeTasks.splice(0)
await Promise.all(activeTasks.map(task => task.cancelTask()))
const sharedRunners = this.sharedRunners.splice(0)
await Promise.all(sharedRunners.map(runner => runner.stop()))
await Promise.all(this.exitPromises.splice(0))
this.workerIds.forEach((_, id) => this.freeWorkerId(id))
// Reset flag after cancellation completes
this._isCancelling = false
}
async close(): Promise<void> {
await this.cancel()
}
private getPoolRunner(task: PoolTask, method: 'run' | 'collect'): PoolRunner {
if (task.isolate === false) {
const index = this.sharedRunners.findIndex(runner => isEqualRunner(runner, task))
if (index !== -1) {
return this.sharedRunners.splice(index, 1)[0]
}
}
const options: PoolOptions = {
distPath: this.options.distPath,
project: task.project,
method,
environment: task.context.environment.name,
env: task.env,
execArgv: task.execArgv,
}
switch (task.worker) {
case 'forks':
return new PoolRunner(options, new ForksPoolWorker(options))
case 'vmForks':
return new PoolRunner(options, new VmForksPoolWorker(options))
case 'threads':
return new PoolRunner(options, new ThreadsPoolWorker(options))
case 'vmThreads':
return new PoolRunner(options, new VmThreadsPoolWorker(options))
case 'typescript':
return new PoolRunner(options, new TypecheckPoolWorker(options))
}
const customPool = task.project.config.poolRunner
if (customPool != null && customPool.name === task.worker) {
return new PoolRunner(options, customPool.createPoolWorker(options))
}
throw new Error(`Runner ${task.worker} is not supported. Test files: ${formatFiles(task)}.`)
}
private getWorkerId() {
let workerId = 0
this.workerIds.forEach((state, id) => {
if (state && !workerId) {
workerId = id
this.workerIds.set(id, false)
}
})
return workerId
}
private freeWorkerId(id: number) {
this.workerIds.set(id, true)
}
}
function withResolvers() {
let resolve = () => {}
let reject = (_error: unknown) => {}
const promise = new Promise<void>((res, rej) => {
resolve = res
reject = rej
})
return { resolve, reject, promise }
}
function formatFiles(task: PoolTask) {
return task.context.files.map(file => file.filepath).join(', ')
}
function isEqualRunner(runner: PoolRunner, task: PoolTask) {
if (task.isolate) {
throw new Error('Isolated tasks should not share runners')
}
return (
runner.worker.name === task.worker
&& runner.project === task.project
&& runner.environment === task.context.environment.name
&& runner.worker.execArgv.every((arg, index) => task.execArgv[index] === arg)
&& isEnvEqual(runner.worker.env, task.env)
)
}
function isEnvEqual(a: PoolOptions['env'], b: PoolTask['env']) {
const keys = Object.keys(a)
if (keys.length !== Object.keys(b).length) {
return false
}
return keys.every(key => a[key] === b[key])
}
@@ -0,0 +1,274 @@
import type { DeferPromise } from '@vitest/utils/helpers'
import type { BirpcReturn } from 'birpc'
import type { RunnerRPC, RuntimeRPC } from '../../types/rpc'
import type { TestProject } from '../project'
import type { PoolOptions, PoolWorker, WorkerRequest, WorkerResponse } from './types'
import { EventEmitter } from 'node:events'
import { createDefer } from '@vitest/utils/helpers'
import { createBirpc } from 'birpc'
import { createMethodsRPC } from './rpc'
enum RunnerState {
IDLE = 'idle',
STARTING = 'starting',
STARTED = 'started',
STOPPING = 'stopping',
STOPPED = 'stopped',
}
const START_TIMEOUT = 10_000
const STOP_TIMEOUT = 10_000
/** @experimental */
export class PoolRunner {
/** Exposed to test runner as `VITEST_POOL_ID`. Value is between 1-`maxWorkers`. */
public poolId: number | undefined = undefined
public readonly project: TestProject
public readonly environment: string
private _state: RunnerState = RunnerState.IDLE
private _operationLock: DeferPromise<void> | null = null
private _eventEmitter: EventEmitter<{
message: [WorkerResponse]
error: [Error]
rpc: [unknown]
}> = new EventEmitter()
private _rpc: BirpcReturn<RunnerRPC, RuntimeRPC>
public get isTerminated(): boolean {
return this._state === RunnerState.STOPPED
}
public get isStarted(): boolean {
return this._state === RunnerState.STARTED
}
constructor(options: PoolOptions, public worker: PoolWorker) {
this.project = options.project
this.environment = options.environment
this._rpc = createBirpc<RunnerRPC, RuntimeRPC>(
createMethodsRPC(this.project, {
collect: options.method === 'collect',
cacheFs: worker.cacheFs,
}),
{
eventNames: ['onCancel'],
post: request => this.postMessage(request),
on: callback => this._eventEmitter.on('rpc', callback),
timeout: -1,
},
)
this.project.vitest.onCancel(reason => this._rpc.onCancel(reason))
}
postMessage(message: WorkerRequest): void {
// Only send messages when runner is active (not fully stopped)
// Allow sending during STOPPING state for the 'stop' message itself
if (this._state !== RunnerState.STOPPED) {
return this.worker.send(message)
}
}
async start(): Promise<void> {
// Wait for any ongoing operation to complete
if (this._operationLock) {
await this._operationLock
}
// If already started or starting, return early
if (this._state === RunnerState.STARTED || this._state === RunnerState.STARTING) {
return
}
// If stopped, cannot restart
if (this._state === RunnerState.STOPPED) {
throw new Error('[vitest-pool-runner]: Cannot start a stopped runner')
}
// Create operation lock to prevent concurrent start/stop
this._operationLock = createDefer()
try {
this._state = RunnerState.STARTING
await this.worker.start()
// Attach event listeners AFTER starting worker to avoid issues
// if worker.start() fails
this.worker.on('error', this.emitWorkerError)
this.worker.on('exit', this.emitUnexpectedExit)
this.worker.on('message', this.emitWorkerMessage)
// Wait for 'started' message with timeout
const startPromise = this.withTimeout(this.waitForStart(), START_TIMEOUT)
this.postMessage({
type: 'start',
__vitest_worker_request__: true,
options: {
reportMemory: this.worker.reportMemory ?? false,
},
})
await startPromise
this._state = RunnerState.STARTED
}
catch (error) {
this._state = RunnerState.IDLE
throw error
}
finally {
this._operationLock.resolve()
this._operationLock = null
}
}
async stop(): Promise<void> {
// Wait for any ongoing operation to complete
if (this._operationLock) {
await this._operationLock
}
// If already stopped or stopping, return early
if (this._state === RunnerState.STOPPED || this._state === RunnerState.STOPPING) {
return
}
// If never started, just mark as stopped
if (this._state === RunnerState.IDLE) {
this._state = RunnerState.STOPPED
return
}
// Create operation lock to prevent concurrent start/stop
this._operationLock = createDefer()
try {
this._state = RunnerState.STOPPING
// Remove exit listener early to avoid "unexpected exit" errors during shutdown
this.worker.off('exit', this.emitUnexpectedExit)
// Wait for 'stopped' message with timeout
await this.withTimeout(
new Promise<void>((resolve) => {
const onStop = (response: WorkerResponse) => {
if (response.type === 'stopped') {
if (response.error) {
this.project.vitest.state.catchError(
response.error,
'Teardown Error',
)
}
resolve()
this.off('message', onStop)
}
}
this.on('message', onStop)
this.postMessage({ type: 'stop', __vitest_worker_request__: true })
}),
STOP_TIMEOUT,
)
// Clean up internal event emitter and RPC
// This implicitly cleans up all message listeners
this._eventEmitter.removeAllListeners()
this._rpc.$close(new Error('[vitest-pool-runner]: Pending methods while closing rpc'))
// Stop the worker process (this sets _fork/_thread to undefined)
// Worker's event listeners (error, message) are implicitly removed when worker terminates
await this.worker.stop()
this._state = RunnerState.STOPPED
}
catch (error) {
// Ensure we transition to stopped state even on error
this._state = RunnerState.STOPPED
throw error
}
finally {
this._operationLock.resolve()
this._operationLock = null
}
}
on(event: 'message', callback: (message: WorkerResponse) => void): void
on(event: 'error', callback: (error: Error) => void): void
on(event: 'message' | 'error', callback: (arg: any) => void): void {
this._eventEmitter.on(event, callback)
}
off(event: 'message', callback: (message: WorkerResponse) => void): void
off(event: 'error', callback: (error: Error) => void): void
off(event: 'message' | 'error', callback: (arg: any) => void): void {
this._eventEmitter.off(event, callback)
}
private emitWorkerError = (maybeError: unknown): void => {
const error = maybeError instanceof Error ? maybeError : new Error(String(maybeError))
this._eventEmitter.emit('error', error)
}
private emitWorkerMessage = (response: WorkerResponse | { m: string; __vitest_worker_response__: false }): void => {
try {
const message = this.worker.deserialize(response) as WorkerResponse
if (typeof message === 'object' && message != null && message.__vitest_worker_response__) {
this._eventEmitter.emit('message', message)
}
else {
this._eventEmitter.emit('rpc', message)
}
}
catch (error) {
this._eventEmitter.emit('error', error as Error)
}
}
private emitUnexpectedExit = (): void => {
const error = new Error('Worker exited unexpectedly')
this._eventEmitter.emit('error', error)
}
private waitForStart() {
return new Promise<void>((resolve) => {
const onStart = (message: WorkerResponse) => {
if (message.type === 'started') {
this.off('message', onStart)
resolve()
}
}
this.on('message', onStart)
})
}
private withTimeout(promise: Promise<unknown>, timeout: number) {
return new Promise<unknown>((resolve_, reject_) => {
const timer = setTimeout(
() => reject(new Error('[vitest-pool-runner]: Timeout waiting for worker to respond')),
timeout,
)
function resolve(value: unknown) {
clearTimeout(timer)
resolve_(value)
}
function reject(error: Error) {
clearTimeout(timer)
reject_(error)
}
promise.then(resolve, reject)
})
}
}
-292
View File
@@ -1,292 +0,0 @@
import type { FileSpecification } from '@vitest/runner/types/runner'
import type { Options as TinypoolOptions } from 'tinypool'
import type { RunnerRPC, RuntimeRPC } from '../../types/rpc'
import type { ContextTestEnvironment } from '../../types/worker'
import type { Vitest } from '../core'
import type { PoolProcessOptions, ProcessPool, RunWithFiles } from '../pool'
import type { TestProject } from '../project'
import type { TestSpecification } from '../spec'
import type { SerializedConfig } from '../types/config'
import type { WorkerContext } from '../types/worker'
import * as nodeos from 'node:os'
import { resolve } from 'node:path'
import { MessageChannel } from 'node:worker_threads'
import { createBirpc } from 'birpc'
import Tinypool from 'tinypool'
import { groupBy } from '../../utils/base'
import { envsOrder, groupFilesByEnv } from '../../utils/test-helpers'
import { createMethodsRPC } from './rpc'
function createWorkerChannel(project: TestProject, collect: boolean) {
const channel = new MessageChannel()
const port = channel.port2
const workerPort = channel.port1
const rpc = createBirpc<RunnerRPC, RuntimeRPC>(createMethodsRPC(project, { collect }), {
eventNames: ['onCancel'],
post(v) {
port.postMessage(v)
},
on(fn) {
port.on('message', fn)
},
timeout: -1,
})
project.vitest.onCancel(reason => rpc.onCancel(reason))
const onClose = () => {
port.close()
workerPort.close()
rpc.$close(new Error('[vitest-pool]: Pending methods while closing rpc'))
}
return { workerPort, port, onClose }
}
export function createThreadsPool(
vitest: Vitest,
{ execArgv, env }: PoolProcessOptions,
specifications: TestSpecification[],
): ProcessPool {
const numCpus
= typeof nodeos.availableParallelism === 'function'
? nodeos.availableParallelism()
: nodeos.cpus().length
const threadsCount = vitest.config.watch
? Math.max(Math.floor(numCpus / 2), 1)
: Math.max(numCpus - 1, 1)
const recommendedCount = vitest.config.watch
? threadsCount
: Math.min(threadsCount, specifications.length)
const poolOptions = vitest.config.poolOptions?.threads ?? {}
const maxThreads
= poolOptions.maxThreads ?? vitest.config.maxWorkers ?? recommendedCount
const minThreads = vitest.config.watch
? Math.min(recommendedCount, maxThreads)
// avoid recreating forks when tests are finished
: 0
const options: TinypoolOptions = {
filename: resolve(vitest.distPath, 'worker-base.js'),
teardown: 'teardown',
// TODO: investigate further
// It seems atomics introduced V8 Fatal Error https://github.com/vitest-dev/vitest/issues/1191
useAtomics: poolOptions.useAtomics ?? false,
maxThreads,
minThreads,
env,
execArgv: [...(poolOptions.execArgv ?? []), ...execArgv],
terminateTimeout: vitest.config.teardownTimeout,
concurrentTasksPerWorker: 1,
}
const isolated = poolOptions.isolate ?? true
if (isolated) {
options.isolateWorkers = true
}
if (poolOptions.singleThread || !vitest.config.fileParallelism) {
options.maxThreads = 1
options.minThreads = 1
}
const pool = new Tinypool(options)
const runWithFiles = (name: string): RunWithFiles => {
let id = 0
async function runFiles(
project: TestProject,
config: SerializedConfig,
files: FileSpecification[],
environment: ContextTestEnvironment,
invalidates: string[] = [],
) {
const paths = files.map(f => f.filepath)
vitest.state.clearFiles(project, paths)
const { workerPort, onClose } = createWorkerChannel(project, name === 'collect')
const workerId = ++id
const data: WorkerContext = {
pool: 'threads',
port: workerPort,
config,
files,
invalidates,
environment,
workerId,
projectName: project.name,
providedContext: project.getProvidedContext(),
}
try {
await pool.run(data, { transferList: [workerPort], name, channel: { onClose } })
}
catch (error) {
// Worker got stuck and won't terminate - this may cause process to hang
if (
error instanceof Error
&& /Failed to terminate worker/.test(error.message)
) {
vitest.state.addProcessTimeoutCause(
`Failed to terminate worker while running ${paths.join(
', ',
)}. \nSee https://vitest.dev/guide/common-errors.html#failed-to-terminate-worker for troubleshooting.`,
)
}
// Intentionally cancelled
else if (
vitest.isCancelling
&& error instanceof Error
&& /The task has been cancelled/.test(error.message)
) {
vitest.state.cancelFiles(paths, project)
}
else {
throw error
}
}
}
return async (specs, invalidates) => {
// Cancel pending tasks from pool when possible
vitest.onCancel(() => pool.cancelPendingTasks())
const configs = new WeakMap<TestProject, SerializedConfig>()
const getConfig = (project: TestProject): SerializedConfig => {
if (configs.has(project)) {
return configs.get(project)!
}
const config = project.serializedConfig
configs.set(project, config)
return config
}
const singleThreads = specs.filter(
spec => spec.project.config.poolOptions?.threads?.singleThread,
)
const multipleThreads = specs.filter(
spec => !spec.project.config.poolOptions?.threads?.singleThread,
)
if (multipleThreads.length) {
const filesByEnv = await groupFilesByEnv(multipleThreads)
const files = Object.values(filesByEnv).flat()
const results: PromiseSettledResult<void>[] = []
if (isolated) {
results.push(
...(await Promise.allSettled(
files.map(({ file, environment, project }) =>
runFiles(
project,
getConfig(project),
[file],
environment,
invalidates,
),
),
)),
)
}
else {
// When isolation is disabled, we still need to isolate environments and workspace projects from each other.
// Tasks are still running parallel but environments are isolated between tasks.
const grouped = groupBy(
files,
({ project, environment }) =>
project.name
+ environment.name
+ JSON.stringify(environment.options),
)
for (const group of Object.values(grouped)) {
// Push all files to pool's queue
results.push(
...(await Promise.allSettled(
group.map(({ file, environment, project }) =>
runFiles(
project,
getConfig(project),
[file],
environment,
invalidates,
),
),
)),
)
// Once all tasks are running or finished, recycle worker for isolation.
// On-going workers will run in the previous environment.
await new Promise<void>(resolve =>
pool.queueSize === 0 ? resolve() : pool.once('drain', resolve),
)
await pool.recycleWorkers()
}
}
const errors = results
.filter((r): r is PromiseRejectedResult => r.status === 'rejected')
.map(r => r.reason)
if (errors.length > 0) {
throw new AggregateError(
errors,
'Errors occurred while running tests. For more information, see serialized error.',
)
}
}
if (singleThreads.length) {
const filesByEnv = await groupFilesByEnv(singleThreads)
const envs = envsOrder.concat(
Object.keys(filesByEnv).filter(env => !envsOrder.includes(env)),
)
for (const env of envs) {
const files = filesByEnv[env]
if (!files?.length) {
continue
}
const filesByOptions = groupBy(
files,
({ project, environment }) =>
project.name + JSON.stringify(environment.options),
)
for (const files of Object.values(filesByOptions)) {
// Always run environments isolated between each other
await pool.recycleWorkers()
const filenames = files.map(f => f.file)
await runFiles(
files[0].project,
getConfig(files[0].project),
filenames,
files[0].environment,
invalidates,
)
}
}
}
}
}
return {
name: 'threads',
runTests: runWithFiles('run'),
collectTests: runWithFiles('collect'),
close: () => pool.destroy(),
}
}
-183
View File
@@ -1,183 +0,0 @@
import type { DeferPromise } from '@vitest/utils'
import type { TypecheckResults } from '../../typecheck/typechecker'
import type { Vitest } from '../core'
import type { ProcessPool } from '../pool'
import type { TestProject } from '../project'
import type { TestSpecification } from '../spec'
import type { TestRunEndReason } from '../types/reporter'
import { hasFailed } from '@vitest/runner/utils'
import { createDefer } from '@vitest/utils/helpers'
import { Typechecker } from '../../typecheck/typechecker'
import { groupBy } from '../../utils/base'
export function createTypecheckPool(vitest: Vitest): ProcessPool {
const promisesMap = new WeakMap<TestProject, DeferPromise<void>>()
const rerunTriggered = new WeakSet<TestProject>()
async function onParseEnd(
project: TestProject,
{ files, sourceErrors }: TypecheckResults,
) {
const checker = project.typechecker!
const { packs, events } = checker.getTestPacksAndEvents()
await vitest._testRun.updated(packs, events)
if (!project.config.typecheck.ignoreSourceErrors) {
sourceErrors.forEach(error =>
vitest.state.catchError(error, 'Unhandled Source Error'),
)
}
const processError
= !hasFailed(files) && !sourceErrors.length && checker.getExitCode()
if (processError) {
const error = new Error(checker.getOutput())
error.stack = ''
vitest.state.catchError(error, 'Typecheck Error')
}
promisesMap.get(project)?.resolve()
rerunTriggered.delete(project)
// triggered by TSC watcher, not Vitest watcher, so we need to emulate what Vitest does in this case
if (vitest.config.watch && !vitest.runningPromise) {
const modules = files.map(file => vitest.state.getReportedEntity(file)).filter(e => e?.type === 'module')
const state: TestRunEndReason = vitest.isCancelling
? 'interrupted'
: modules.some(m => !m.ok())
? 'failed'
: 'passed'
await vitest.report('onTestRunEnd', modules, [], state)
await vitest.report('onWatcherStart', files, [
...(project.config.typecheck.ignoreSourceErrors ? [] : sourceErrors),
...vitest.state.getUnhandledErrors(),
])
}
}
async function createWorkspaceTypechecker(
project: TestProject,
files: string[],
) {
const checker = project.typechecker ?? new Typechecker(project)
if (project.typechecker) {
return checker
}
project.typechecker = checker
checker.setFiles(files)
checker.onParseStart(async () => {
const files = checker.getTestFiles()
for (const file of files) {
await vitest._testRun.enqueued(project, file)
}
await vitest._testRun.collected(project, files)
})
checker.onParseEnd(result => onParseEnd(project, result))
checker.onWatcherRerun(async () => {
rerunTriggered.add(project)
if (!vitest.runningPromise) {
vitest.state.clearErrors()
await vitest.report(
'onWatcherRerun',
files,
'File change detected. Triggering rerun.',
)
}
await checker.collectTests()
const testFiles = checker.getTestFiles()
for (const file of testFiles) {
await vitest._testRun.enqueued(project, file)
}
await vitest._testRun.collected(project, testFiles)
const { packs, events } = checker.getTestPacksAndEvents()
await vitest._testRun.updated(packs, events)
})
return checker
}
async function startTypechecker(project: TestProject, files: string[]) {
if (project.typechecker) {
return
}
const checker = await createWorkspaceTypechecker(project, files)
await checker.collectTests()
await checker.start()
}
async function collectTests(specs: TestSpecification[]) {
const specsByProject = groupBy(specs, spec => spec.project.name)
for (const name in specsByProject) {
const project = specsByProject[name][0].project
const files = specsByProject[name].map(spec => spec.moduleId)
const checker = await createWorkspaceTypechecker(project, files)
checker.setFiles(files)
await checker.collectTests()
const testFiles = checker.getTestFiles()
vitest.state.collectFiles(project, testFiles)
}
}
async function runTests(specs: TestSpecification[]) {
const specsByProject = groupBy(specs, spec => spec.project.name)
const promises: Promise<void>[] = []
for (const name in specsByProject) {
const project = specsByProject[name][0].project
const files = specsByProject[name].map(spec => spec.moduleId)
const promise = createDefer<void>()
// check that watcher actually triggered rerun
const _p = new Promise<boolean>((resolve) => {
const _i = setInterval(() => {
if (!project.typechecker || rerunTriggered.has(project)) {
resolve(true)
clearInterval(_i)
}
})
setTimeout(() => {
resolve(false)
clearInterval(_i)
}, 500).unref()
})
const triggered = await _p
if (project.typechecker && !triggered) {
const testFiles = project.typechecker.getTestFiles()
for (const file of testFiles) {
await vitest._testRun.enqueued(project, file)
}
await vitest._testRun.collected(project, testFiles)
await onParseEnd(project, project.typechecker.getResult())
continue
}
promises.push(promise)
promisesMap.set(project, promise)
promises.push(startTypechecker(project, files))
}
await Promise.all(promises)
}
return {
name: 'typescript',
runTests,
collectTests,
async close() {
const promises = vitest.projects.map(project =>
project.typechecker?.stop(),
)
await Promise.all(promises)
},
}
}
+59
View File
@@ -0,0 +1,59 @@
import type { ContextRPC } from '../../types/worker'
import type { TestProject } from '../project'
export interface PoolRunnerInitializer {
readonly name: string
createPoolWorker: (options: PoolOptions) => PoolWorker
}
export interface PoolOptions {
distPath: string
project: TestProject
method: 'run' | 'collect'
cacheFs?: boolean
environment: string
execArgv: string[]
env: Record<string, string>
}
export interface PoolWorker {
readonly name: string
readonly execArgv: string[]
readonly env: Record<string, string>
readonly reportMemory?: boolean
readonly cacheFs?: boolean
on: (event: string, callback: (arg: any) => void) => void
off: (event: string, callback: (arg: any) => void) => void
send: (message: WorkerRequest) => void
deserialize: (data: unknown) => unknown
start: () => Promise<void>
stop: () => Promise<void>
}
export interface PoolTask {
worker: 'forks' | 'threads' | 'vmForks' | 'vmThreads' | (string & {})
project: TestProject
isolate: boolean
env: Record<string, string>
execArgv: string[]
context: ContextRPC
memoryLimit: number | null
}
export type WorkerRequest
= { __vitest_worker_request__: true } & (
| { type: 'start'; options: { reportMemory: boolean } }
| { type: 'stop' }
| { type: 'run'; context: ContextRPC; poolId: number }
| { type: 'collect'; context: ContextRPC; poolId: number }
| { type: 'cancel' }
)
export type WorkerResponse
= { __vitest_worker_response__: true } & (
| { type: 'started' }
| { type: 'stopped'; error?: unknown }
| { type: 'testfileFinished'; usedMemory?: number; error?: unknown }
)
-247
View File
@@ -1,247 +0,0 @@
import type { FileSpecification } from '@vitest/runner'
import type { TinypoolChannel, Options as TinypoolOptions } from 'tinypool'
import type { RunnerRPC, RuntimeRPC } from '../../types/rpc'
import type { ContextRPC, ContextTestEnvironment } from '../../types/worker'
import type { Vitest } from '../core'
import type { PoolProcessOptions, ProcessPool, RunWithFiles } from '../pool'
import type { TestProject } from '../project'
import type { TestSpecification } from '../spec'
import type { ResolvedConfig, SerializedConfig } from '../types/config'
import EventEmitter from 'node:events'
import * as nodeos from 'node:os'
import { resolve } from 'node:path'
import v8 from 'node:v8'
import { createBirpc } from 'birpc'
import Tinypool from 'tinypool'
import { wrapSerializableConfig } from '../../utils/config-helpers'
import { getWorkerMemoryLimit, stringToBytes } from '../../utils/memory-limit'
import { groupFilesByEnv } from '../../utils/test-helpers'
import { createMethodsRPC } from './rpc'
function createChildProcessChannel(project: TestProject, collect: boolean) {
const emitter = new EventEmitter()
const events = { message: 'message', response: 'response' }
const rpc = createBirpc<RunnerRPC, RuntimeRPC>(
createMethodsRPC(project, { cacheFs: true, collect }),
{
eventNames: ['onCancel'],
serialize: v8.serialize,
deserialize: (v) => {
try {
return v8.deserialize(Buffer.from(v))
}
catch (error) {
let stringified = ''
try {
stringified = `\nReceived value: ${JSON.stringify(v)}`
}
catch {}
throw new Error(`[vitest-pool]: Unexpected call to process.send(). Make sure your test cases are not interfering with process's channel.${stringified}`, { cause: error })
}
},
post(v) {
emitter.emit(events.message, v)
},
on(fn) {
emitter.on(events.response, fn)
},
timeout: -1,
},
)
project.vitest.onCancel(reason => rpc.onCancel(reason))
const channel = {
onMessage: callback => emitter.on(events.message, callback),
postMessage: message => emitter.emit(events.response, message),
onClose: () => {
emitter.removeAllListeners()
rpc.$close(new Error('[vitest-pool]: Pending methods while closing rpc'))
},
} satisfies TinypoolChannel
return { channel }
}
export function createVmForksPool(
vitest: Vitest,
{ execArgv, env }: PoolProcessOptions,
specifications: TestSpecification[],
): ProcessPool {
const numCpus
= typeof nodeos.availableParallelism === 'function'
? nodeos.availableParallelism()
: nodeos.cpus().length
const threadsCount = vitest.config.watch
? Math.max(Math.floor(numCpus / 2), 1)
: Math.max(numCpus - 1, 1)
const recommendedCount = vitest.config.watch
? threadsCount
: Math.min(threadsCount, specifications.length)
const poolOptions = vitest.config.poolOptions?.vmForks ?? {}
const maxThreads
= poolOptions.maxForks ?? vitest.config.maxWorkers ?? recommendedCount
const minThreads = vitest.config.watch
? Math.min(recommendedCount, maxThreads)
// avoid recreating forks when tests are finished
: 0
const options: TinypoolOptions = {
runtime: 'child_process',
filename: resolve(vitest.distPath, 'worker-vm.js'),
maxThreads,
minThreads,
env,
execArgv: [
'--experimental-vm-modules',
...(poolOptions.execArgv ?? []),
...execArgv,
],
terminateTimeout: vitest.config.teardownTimeout,
concurrentTasksPerWorker: 1,
maxMemoryLimitBeforeRecycle: getMemoryLimit(vitest.config) || undefined,
}
if (poolOptions.singleFork || !vitest.config.fileParallelism) {
options.maxThreads = 1
options.minThreads = 1
}
const pool = new Tinypool(options)
const runWithFiles = (name: string): RunWithFiles => {
let id = 0
async function runFiles(
project: TestProject,
config: SerializedConfig,
files: FileSpecification[],
environment: ContextTestEnvironment,
invalidates: string[] = [],
) {
const paths = files.map(f => f.filepath)
vitest.state.clearFiles(project, paths)
const { channel } = createChildProcessChannel(project, name === 'collect')
const workerId = ++id
const data: ContextRPC = {
pool: 'vmForks',
config,
files,
invalidates,
environment,
workerId,
projectName: project.name,
providedContext: project.getProvidedContext(),
}
try {
await pool.run(data, { name, channel })
}
catch (error) {
// Worker got stuck and won't terminate - this may cause process to hang
if (
error instanceof Error
&& /Failed to terminate worker/.test(error.message)
) {
vitest.state.addProcessTimeoutCause(
`Failed to terminate worker while running ${paths.join(', ')}.`,
)
}
// Intentionally cancelled
else if (
vitest.isCancelling
&& error instanceof Error
&& /The task has been cancelled/.test(error.message)
) {
vitest.state.cancelFiles(paths, project)
}
else {
throw error
}
}
finally {
channel.onClose()
}
}
return async (specs, invalidates) => {
// Cancel pending tasks from pool when possible
vitest.onCancel(() => pool.cancelPendingTasks())
const configs = new Map<TestProject, SerializedConfig>()
const getConfig = (project: TestProject): SerializedConfig => {
if (configs.has(project)) {
return configs.get(project)!
}
const _config = project.serializedConfig
const config = wrapSerializableConfig(_config)
configs.set(project, config)
return config
}
const filesByEnv = await groupFilesByEnv(specs)
const promises = Object.values(filesByEnv).flat()
const results = await Promise.allSettled(
promises.map(({ file, environment, project }) =>
runFiles(
project,
getConfig(project),
[file],
environment,
invalidates,
),
),
)
const errors = results
.filter((r): r is PromiseRejectedResult => r.status === 'rejected')
.map(r => r.reason)
if (errors.length > 0) {
throw new AggregateError(
errors,
'Errors occurred while running tests. For more information, see serialized error.',
)
}
}
}
return {
name: 'vmForks',
runTests: runWithFiles('run'),
collectTests: runWithFiles('collect'),
close: () => pool.destroy(),
}
}
function getMemoryLimit(config: ResolvedConfig) {
const memory = nodeos.totalmem()
const limit = getWorkerMemoryLimit(config, 'vmForks')
if (typeof memory === 'number') {
return stringToBytes(limit, config.watch ? memory / 2 : memory)
}
// If totalmem is not supported we cannot resolve percentage based values like 0.5, "50%"
if (
(typeof limit === 'number' && limit > 1)
|| (typeof limit === 'string' && limit.at(-1) !== '%')
) {
return stringToBytes(limit)
}
// just ignore "memoryLimit" value because we cannot detect memory limit
return null
}
-227
View File
@@ -1,227 +0,0 @@
import type { FileSpecification } from '@vitest/runner'
import type { Options as TinypoolOptions } from 'tinypool'
import type { RunnerRPC, RuntimeRPC } from '../../types/rpc'
import type { ContextTestEnvironment } from '../../types/worker'
import type { Vitest } from '../core'
import type { PoolProcessOptions, ProcessPool, RunWithFiles } from '../pool'
import type { TestProject } from '../project'
import type { TestSpecification } from '../spec'
import type { ResolvedConfig, SerializedConfig } from '../types/config'
import type { WorkerContext } from '../types/worker'
import * as nodeos from 'node:os'
import { resolve } from 'node:path'
import { MessageChannel } from 'node:worker_threads'
import { createBirpc } from 'birpc'
import Tinypool from 'tinypool'
import { getWorkerMemoryLimit, stringToBytes } from '../../utils/memory-limit'
import { groupFilesByEnv } from '../../utils/test-helpers'
import { createMethodsRPC } from './rpc'
function createWorkerChannel(project: TestProject, collect: boolean) {
const channel = new MessageChannel()
const port = channel.port2
const workerPort = channel.port1
const rpc = createBirpc<RunnerRPC, RuntimeRPC>(createMethodsRPC(project, { collect }), {
eventNames: ['onCancel'],
post(v) {
port.postMessage(v)
},
on(fn) {
port.on('message', fn)
},
timeout: -1,
})
project.vitest.onCancel(reason => rpc.onCancel(reason))
function onClose() {
workerPort.close()
port.close()
rpc.$close(new Error('[vitest-pool]: Pending methods while closing rpc'))
}
return { workerPort, onClose }
}
export function createVmThreadsPool(
vitest: Vitest,
{ execArgv, env }: PoolProcessOptions,
specifications: TestSpecification[],
): ProcessPool {
const numCpus
= typeof nodeos.availableParallelism === 'function'
? nodeos.availableParallelism()
: nodeos.cpus().length
const threadsCount = vitest.config.watch
? Math.max(Math.floor(numCpus / 2), 1)
: Math.max(numCpus - 1, 1)
const recommendedCount = vitest.config.watch
? threadsCount
: Math.min(threadsCount, specifications.length)
const poolOptions = vitest.config.poolOptions?.vmThreads ?? {}
const maxThreads
= poolOptions.maxThreads ?? vitest.config.maxWorkers ?? recommendedCount
const minThreads = vitest.config.watch
? Math.min(recommendedCount, maxThreads)
// avoid recreating threads when tests are finished
: 0
const options: TinypoolOptions = {
filename: resolve(vitest.distPath, 'worker-vm.js'),
// TODO: investigate further
// It seems atomics introduced V8 Fatal Error https://github.com/vitest-dev/vitest/issues/1191
useAtomics: poolOptions.useAtomics ?? false,
maxThreads,
minThreads,
env,
execArgv: [
'--experimental-vm-modules',
...(poolOptions.execArgv ?? []),
...execArgv,
],
terminateTimeout: vitest.config.teardownTimeout,
concurrentTasksPerWorker: 1,
maxMemoryLimitBeforeRecycle: getMemoryLimit(vitest.config) || undefined,
}
if (poolOptions.singleThread || !vitest.config.fileParallelism) {
options.maxThreads = 1
options.minThreads = 1
}
const pool = new Tinypool(options)
const runWithFiles = (name: string): RunWithFiles => {
let id = 0
async function runFiles(
project: TestProject,
config: SerializedConfig,
files: FileSpecification[],
environment: ContextTestEnvironment,
invalidates: string[] = [],
) {
const paths = files.map(f => f.filepath)
vitest.state.clearFiles(project, paths)
const { workerPort, onClose } = createWorkerChannel(project, name === 'collect')
const workerId = ++id
const data: WorkerContext = {
pool: 'vmThreads',
port: workerPort,
config,
files: paths,
invalidates,
environment,
workerId,
projectName: project.name,
providedContext: project.getProvidedContext(),
}
try {
await pool.run(data, { transferList: [workerPort], name })
}
catch (error) {
// Worker got stuck and won't terminate - this may cause process to hang
if (
error instanceof Error
&& /Failed to terminate worker/.test(error.message)
) {
vitest.state.addProcessTimeoutCause(
`Failed to terminate worker while running ${paths.join(
', ',
)}. \nSee https://vitest.dev/guide/common-errors.html#failed-to-terminate-worker for troubleshooting.`,
)
}
// Intentionally cancelled
else if (
vitest.isCancelling
&& error instanceof Error
&& /The task has been cancelled/.test(error.message)
) {
vitest.state.cancelFiles(paths, project)
}
else {
throw error
}
}
finally {
onClose()
}
}
return async (specs, invalidates) => {
// Cancel pending tasks from pool when possible
vitest.onCancel(() => pool.cancelPendingTasks())
const configs = new Map<TestProject, SerializedConfig>()
const getConfig = (project: TestProject): SerializedConfig => {
if (configs.has(project)) {
return configs.get(project)!
}
const config = project.serializedConfig
configs.set(project, config)
return config
}
const filesByEnv = await groupFilesByEnv(specs)
const promises = Object.values(filesByEnv).flat()
const results = await Promise.allSettled(
promises.map(({ file, environment, project }) =>
runFiles(
project,
getConfig(project),
[file],
environment,
invalidates,
),
),
)
const errors = results
.filter((r): r is PromiseRejectedResult => r.status === 'rejected')
.map(r => r.reason)
if (errors.length > 0) {
throw new AggregateError(
errors,
'Errors occurred while running tests. For more information, see serialized error.',
)
}
}
}
return {
name: 'vmThreads',
runTests: runWithFiles('run'),
collectTests: runWithFiles('collect'),
close: () => pool.destroy(),
}
}
function getMemoryLimit(config: ResolvedConfig) {
const memory = nodeos.totalmem()
const limit = getWorkerMemoryLimit(config, 'vmThreads')
if (typeof memory === 'number') {
return stringToBytes(limit, config.watch ? memory / 2 : memory)
}
// If totalmem is not supported we cannot resolve percentage based values like 0.5, "50%"
if (
(typeof limit === 'number' && limit > 1)
|| (typeof limit === 'string' && limit.at(-1) !== '%')
) {
return stringToBytes(limit)
}
// just ignore "memoryLimit" value because we cannot detect memory limit
return null
}
@@ -0,0 +1,133 @@
import type { ChildProcess } from 'node:child_process'
import type { SerializedConfig } from '../../types/config'
import type { PoolOptions, PoolWorker, WorkerRequest } from '../types'
import { fork } from 'node:child_process'
import { resolve } from 'node:path'
import v8 from 'node:v8'
const SIGKILL_TIMEOUT = 500 /** jest does 500ms by default, let's follow it */
/** @experimental */
export class ForksPoolWorker implements PoolWorker {
public readonly name: string = 'forks'
public readonly execArgv: string[]
public readonly env: Record<string, string>
public readonly cacheFs: boolean = true
protected readonly entrypoint: string
private _fork?: ChildProcess
constructor(options: PoolOptions) {
this.execArgv = options.execArgv
this.env = options.env
/** Loads {@link file://./../../../runtime/workers/forks.ts} */
this.entrypoint = resolve(options.distPath, 'workers/forks.js')
}
on(event: string, callback: (arg: any) => void): void {
this.fork.on(event, callback)
}
off(event: string, callback: (arg: any) => void): void {
this.fork.off(event, callback)
}
send(message: WorkerRequest): void {
if ('context' in message) {
message = {
...message,
context: {
...message.context,
config: wrapSerializableConfig(message.context.config),
},
}
}
this.fork.send(v8.serialize(message))
}
async start(): Promise<void> {
this._fork ||= fork(this.entrypoint, [], {
env: this.env,
execArgv: this.execArgv,
})
}
async stop(): Promise<void> {
const fork = this.fork
const waitForExit = new Promise<void>((resolve) => {
if (fork.exitCode != null) {
resolve()
}
else {
fork.once('exit', resolve)
}
})
/*
* If process running user's code does not stop on SIGTERM, send SIGKILL.
* This is similar to
* - https://github.com/jestjs/jest/blob/25a8785584c9d54a05887001ee7f498d489a5441/packages/jest-worker/src/workers/ChildProcessWorker.ts#L463-L477
* - https://github.com/tinylibs/tinypool/blob/40b4b3eb926dabfbfd3d0a7e3d1222d4dd1c0d2d/src/runtime/process-worker.ts#L56
*/
const sigkillTimeout = setTimeout(
() => fork.kill('SIGKILL'),
SIGKILL_TIMEOUT,
)
fork.kill()
await waitForExit
clearTimeout(sigkillTimeout)
this._fork = undefined
}
deserialize(data: unknown): unknown {
try {
return v8.deserialize(Buffer.from(data as ArrayBuffer))
}
catch (error) {
let stringified = ''
try {
stringified = `\nReceived value: ${JSON.stringify(data)}`
}
catch {}
throw new Error(`[vitest-pool]: Unexpected call to process.send(). Make sure your test cases are not interfering with process's channel.${stringified}`, { cause: error })
}
}
private get fork() {
if (!this._fork) {
throw new Error(`The child process was torn down or never initialized. This is a bug in Vitest.`)
}
return this._fork
}
}
/**
* Prepares `SerializedConfig` for serialization, e.g. `node:v8.serialize`
* - Unwrapping done in {@link file://./../../../runtime/workers/init-forks.ts}
*/
function wrapSerializableConfig(config: SerializedConfig) {
let testNamePattern = config.testNamePattern
let defines = config.defines
// v8 serialize does not support regex
if (testNamePattern && typeof testNamePattern !== 'string') {
testNamePattern = `$$vitest:${testNamePattern.toString()}` as unknown as RegExp
}
// v8 serialize drops properties with undefined value
if (defines) {
defines = { keys: Object.keys(defines), original: defines }
}
return {
...config,
testNamePattern,
defines,
} as SerializedConfig
}
@@ -0,0 +1,57 @@
import type { PoolOptions, PoolWorker, WorkerRequest } from '../types'
import { resolve } from 'node:path'
import { Worker } from 'node:worker_threads'
/** @experimental */
export class ThreadsPoolWorker implements PoolWorker {
public readonly name: string = 'threads'
public readonly execArgv: string[]
public readonly env: Record<string, string>
protected readonly entrypoint: string
private _thread?: Worker
constructor(options: PoolOptions) {
this.execArgv = options.execArgv
this.env = options.env
/** Loads {@link file://./../../../runtime/workers/threads.ts} */
this.entrypoint = resolve(options.distPath, 'workers/threads.js')
}
on(event: string, callback: (arg: any) => void): void {
this.thread.on(event, callback)
}
off(event: string, callback: (arg: any) => void): void {
this.thread.off(event, callback)
}
send(message: WorkerRequest): void {
this.thread.postMessage(message)
}
async start(): Promise<void> {
// This can be called multiple times if the runtime is shared.
this._thread ||= new Worker(this.entrypoint, {
env: this.env,
execArgv: this.execArgv,
})
}
async stop(): Promise<void> {
await this.thread.terminate().then(() => {
this._thread = undefined
})
}
deserialize(data: unknown): unknown {
return data
}
private get thread() {
if (!this._thread) {
throw new Error(`The worker thread was torn down or never initialized. This is a bug in Vitest.`)
}
return this._thread
}
}
@@ -0,0 +1,257 @@
import type { FileSpecification } from '@vitest/runner'
import type { DeferPromise } from '@vitest/utils'
import type { TypecheckResults } from '../../../typecheck/typechecker'
import type { Vitest } from '../../core'
import type { TestProject } from '../../project'
import type { TestRunEndReason } from '../../types/reporter'
import type { PoolOptions, PoolWorker, WorkerRequest, WorkerResponse } from '../types'
import EventEmitter from 'node:events'
import { hasFailed } from '@vitest/runner/utils'
import { createDefer } from '@vitest/utils/helpers'
import { Typechecker } from '../../../typecheck/typechecker'
/** @experimental */
export class TypecheckPoolWorker implements PoolWorker {
public readonly name: string = 'typecheck'
public readonly execArgv: string[]
public readonly env: Record<string, string>
private readonly project: TestProject
private _eventEmitter = new EventEmitter()
constructor(options: PoolOptions) {
this.execArgv = options.execArgv
this.env = options.env
this.project = options.project
}
async start(): Promise<void> {
// noop, onMessage handles it
}
async stop(): Promise<void> {
// noop, onMessage handles it
}
send(message: WorkerRequest): void {
void onMessage(message, this.project).then((response) => {
if (response) {
this._eventEmitter.emit('message', response)
}
})
}
on(event: string, callback: (arg: any) => any): void {
this._eventEmitter.on(event, callback)
}
off(event: string, callback: (arg: any) => any): void {
this._eventEmitter.on(event, callback)
}
deserialize(data: unknown): unknown {
return data
}
}
const __vitest_worker_response__ = true
const runners = new WeakMap<Vitest, ReturnType<typeof createRunner>>()
async function onMessage(message: WorkerRequest, project: TestProject): Promise<WorkerResponse | void> {
if (message?.__vitest_worker_request__ !== true) {
return undefined
}
let runner = runners.get(project.vitest)
if (!runner) {
runner = createRunner(project.vitest)
runners.set(project.vitest, runner)
}
let runPromise: Promise<unknown> | undefined
switch (message.type) {
case 'start': {
return { type: 'started', __vitest_worker_response__ }
}
case 'run': {
runPromise = runner.runTests(message.context.files, project)
.catch(error => error)
const error = await runPromise
return { type: 'testfileFinished', error, __vitest_worker_response__ }
}
case 'collect': {
runPromise = runner.collectTests(message.context.files, project)
.catch(error => error)
const error = await runPromise
return { type: 'testfileFinished', error, __vitest_worker_response__ }
}
case 'stop': {
await runPromise
await project.typechecker?.stop()
return { type: 'stopped', __vitest_worker_response__ }
}
}
throw new Error(`Unexpected message ${JSON.stringify(message, null, 2)}`)
}
function createRunner(vitest: Vitest) {
const promisesMap = new WeakMap<TestProject, DeferPromise<void>>()
const rerunTriggered = new WeakSet<TestProject>()
async function onParseEnd(
project: TestProject,
{ files, sourceErrors }: TypecheckResults,
) {
const checker = project.typechecker!
const { packs, events } = checker.getTestPacksAndEvents()
await vitest._testRun.updated(packs, events)
if (!project.config.typecheck.ignoreSourceErrors) {
sourceErrors.forEach(error =>
vitest.state.catchError(error, 'Unhandled Source Error'),
)
}
const processError = !hasFailed(files) && !sourceErrors.length && checker.getExitCode()
if (processError) {
const error = new Error(checker.getOutput())
error.stack = ''
vitest.state.catchError(error, 'Typecheck Error')
}
promisesMap.get(project)?.resolve()
rerunTriggered.delete(project)
// triggered by TSC watcher, not Vitest watcher, so we need to emulate what Vitest does in this case
if (vitest.config.watch && !vitest.runningPromise) {
const modules = files.map(file => vitest.state.getReportedEntity(file)).filter(e => e?.type === 'module')
const state: TestRunEndReason = vitest.isCancelling
? 'interrupted'
: modules.some(m => !m.ok())
? 'failed'
: 'passed'
await vitest.report('onTestRunEnd', modules, [], state)
await vitest.report('onWatcherStart', files, [
...(project.config.typecheck.ignoreSourceErrors ? [] : sourceErrors),
...vitest.state.getUnhandledErrors(),
])
}
}
async function createWorkspaceTypechecker(
project: TestProject,
files: string[],
) {
const checker = project.typechecker ?? new Typechecker(project)
if (project.typechecker) {
return checker
}
project.typechecker = checker
checker.setFiles(files)
checker.onParseStart(async () => {
const files = checker.getTestFiles()
for (const file of files) {
await vitest._testRun.enqueued(project, file)
}
await vitest._testRun.collected(project, files)
})
checker.onParseEnd(result => onParseEnd(project, result))
checker.onWatcherRerun(async () => {
rerunTriggered.add(project)
if (!vitest.runningPromise) {
vitest.state.clearErrors()
await vitest.report(
'onWatcherRerun',
files,
'File change detected. Triggering rerun.',
)
}
await checker.collectTests()
const testFiles = checker.getTestFiles()
for (const file of testFiles) {
await vitest._testRun.enqueued(project, file)
}
await vitest._testRun.collected(project, testFiles)
const { packs, events } = checker.getTestPacksAndEvents()
await vitest._testRun.updated(packs, events)
})
return checker
}
async function startTypechecker(project: TestProject, files: string[]) {
if (project.typechecker) {
return
}
const checker = await createWorkspaceTypechecker(project, files)
await checker.collectTests()
await checker.start()
}
async function collectTests(specs: FileSpecification[], project: TestProject) {
const files = specs.map(spec => spec.filepath)
const checker = await createWorkspaceTypechecker(project, files)
checker.setFiles(files)
await checker.collectTests()
const testFiles = checker.getTestFiles()
vitest.state.collectFiles(project, testFiles)
}
async function runTests(specs: FileSpecification[], project: TestProject) {
const promises: Promise<void>[] = []
const files = specs.map(spec => spec.filepath)
const promise = createDefer<void>()
// check that watcher actually triggered rerun
const _p = new Promise<boolean>((resolve) => {
const _i = setInterval(() => {
if (!project.typechecker || rerunTriggered.has(project)) {
resolve(true)
clearInterval(_i)
}
})
setTimeout(() => {
resolve(false)
clearInterval(_i)
}, 500).unref()
})
const triggered = await _p
if (project.typechecker && !triggered) {
const testFiles = project.typechecker.getTestFiles()
for (const file of testFiles) {
await vitest._testRun.enqueued(project, file)
}
await vitest._testRun.collected(project, testFiles)
await onParseEnd(project, project.typechecker.getResult())
}
promises.push(promise)
promisesMap.set(project, promise)
promises.push(startTypechecker(project, files))
await Promise.all(promises)
}
return {
runTests,
collectTests,
}
}
@@ -0,0 +1,17 @@
import type { PoolOptions } from '../types'
import { resolve } from 'node:path'
import { ForksPoolWorker } from './forksWorker'
/** @experimental */
export class VmForksPoolWorker extends ForksPoolWorker {
public readonly name = 'vmForks'
public readonly reportMemory = true
protected readonly entrypoint: string
constructor(options: PoolOptions) {
super({ ...options, execArgv: [...options.execArgv, '--experimental-vm-modules'] })
/** Loads {@link file://./../../../runtime/workers/vmForks.ts} */
this.entrypoint = resolve(options.distPath, 'workers/vmForks.js')
}
}
@@ -0,0 +1,17 @@
import type { PoolOptions } from '../types'
import { resolve } from 'node:path'
import { ThreadsPoolWorker } from './threadsWorker'
/** @experimental */
export class VmThreadsPoolWorker extends ThreadsPoolWorker {
public readonly name = 'vmThreads'
public readonly reportMemory = true
protected readonly entrypoint: string
constructor(options: PoolOptions) {
super({ ...options, execArgv: [...options.execArgv, '--experimental-vm-modules'] })
/** Loads {@link file://./../../../runtime/workers/vmThreads.ts} */
this.entrypoint = resolve(options.distPath, 'workers/vmThreads.js')
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
import type { SerializedTestSpecification } from '../runtime/types/utils'
import type { TestProject } from './project'
import type { TestModule } from './reporters/reported-tasks'
import type { Pool } from './types/pool-options'
import type { Pool } from './types/config'
import { generateFileHash } from '@vitest/runner/utils'
import { relative } from 'pathe'
+13 -14
View File
@@ -1,9 +1,10 @@
import type { File, Task, TaskResultPack } from '@vitest/runner'
import type { File, FileSpecification, Task, TaskResultPack } from '@vitest/runner'
import type { UserConsoleLog } from '../types/general'
import type { TestProject } from './project'
import type { MergedBlobs } from './reporters/blob'
import type { OnUnhandledErrorCallback } from './types/config'
import { createFileTask } from '@vitest/runner/utils'
import { createFileTask, generateFileHash } from '@vitest/runner/utils'
import { relative } from 'pathe'
import { defaultBrowserPort } from '../constants'
import { TestCase, TestModule, TestSuite } from './reporters/reported-tasks'
@@ -21,7 +22,6 @@ export class StateManager {
idMap: Map<string, Task> = new Map()
taskFileMap: WeakMap<Task, File> = new WeakMap()
errorsSet: Set<unknown> = new Set()
processTimeoutCauses: Set<string> = new Set()
reportedTasksMap: WeakMap<Task, TestModule | TestCase | TestSuite> = new WeakMap()
blobs?: MergedBlobs
transformTime = 0
@@ -90,14 +90,6 @@ export class StateManager {
return Array.from(this.errorsSet.values())
}
addProcessTimeoutCause(cause: string): void {
this.processTimeoutCauses.add(cause)
}
getProcessTimeoutCauses(): string[] {
return Array.from(this.processTimeoutCauses.values())
}
getPaths(): string[] {
return Array.from(this.pathsSet)
}
@@ -257,11 +249,18 @@ export class StateManager {
).length
}
cancelFiles(files: string[], project: TestProject): void {
cancelFiles(files: FileSpecification[], project: TestProject): void {
// if we don't filter existing modules, they will be overriden by `collectFiles`
const nonRegisteredFiles = files.filter(({ filepath }) => {
const relativePath = relative(project.config.root, filepath)
const id = generateFileHash(relativePath, project.name)
return !this.idMap.has(id)
})
this.collectFiles(
project,
files.map(filepath =>
createFileTask(filepath, project.config.root, project.config.name),
nonRegisteredFiles.map(file =>
createFileTask(file.filepath, project.config.root, project.config.name),
),
)
}
+1
View File
@@ -135,6 +135,7 @@ export class TestRun {
return modules.some(m => !m.ok())
}
// make sure the error always has a "stacks" property
private syncUpdateStacks(update: TaskResultPack[]): void {
update.forEach(([taskId, result]) => {
const task = this.vitest.state.idMap.get(taskId)
@@ -65,7 +65,6 @@ type UnsupportedProperties
| 'sequence'
| 'root'
| 'pool'
| 'poolOptions'
// browser mode doesn't support a custom runner
| 'runner'
// non-browser options
+41 -27
View File
@@ -10,6 +10,7 @@ import type { SerializedConfig } from '../../runtime/config'
import type { LabelColor, ParsedStack, ProvidedContext, TestError } from '../../types/general'
import type { HappyDOMOptions } from '../../types/happy-dom-options'
import type { JSDOMOptions } from '../../types/jsdom-options'
import type { PoolRunnerInitializer } from '../pools/types'
import type {
BuiltinReporterOptions,
BuiltinReporters,
@@ -20,7 +21,6 @@ import type { WatcherTriggerPattern } from '../watcher'
import type { BenchmarkUserOptions } from './benchmark'
import type { BrowserConfigOptions, ResolvedBrowserOptions } from './browser'
import type { CoverageOptions, ResolvedCoverageOptions } from './coverage'
import type { Pool, PoolOptions, ResolvedPoolOptions } from './pool-options'
import type { Reporter } from './reporter'
export type { CoverageOptions, ResolvedCoverageOptions }
@@ -39,7 +39,6 @@ export type BuiltinEnvironment
export type VitestEnvironment
= | BuiltinEnvironment
| (string & Record<never, never>)
export type { Pool, PoolOptions }
export type CSSModuleScopeStrategy = 'stable' | 'scoped' | 'non-scoped'
export type ApiConfig = Pick<
@@ -205,6 +204,16 @@ export type ResolveSnapshotPathHandler = (
context: ResolveSnapshotPathHandlerContext
) => string
export type BuiltinPool
= | 'browser'
| 'threads'
| 'forks'
| 'vmThreads'
| 'vmForks'
| 'typescript'
export type Pool = BuiltinPool | (string & {})
export interface InlineConfig {
/**
* Name of the project. Will be used to display in the reporter.
@@ -298,28 +307,42 @@ export interface InlineConfig {
/**
* Run tests in an isolated environment. This option has no effect on vmThreads pool.
*
* Disabling this option might improve performance if your code doesn't rely on side effects.
* Disabling this option improves performance if your code doesn't rely on side effects.
*
* @default true
*/
isolate?: boolean
/**
* Pass additional arguments to `node` process when spawning the worker.
*
* See [Command-line API | Node.js](https://nodejs.org/docs/latest/api/cli.html) for more information.
*
* Set to `process.execArgv` to pass all arguments of the current process.
*
* Be careful when using, it as some options may crash worker, e.g. --prof, --title. See https://github.com/nodejs/node/issues/41103
*
* @default [] // no execution arguments are passed
*/
execArgv?: string[]
/**
* Specifies the memory limit for `worker_thread` or `child_process` before they are recycled.
* If you see memory leaks, try to tinker this value.
*/
vmMemoryLimit?: string | number
/**
* Pool used to run tests in.
*
* Supports 'threads', 'forks', 'vmThreads'
* Supports 'threads', 'forks', 'vmThreads', 'vmForks'
*
* @default 'forks'
*/
pool?: Exclude<Pool, 'browser'>
pool?: Exclude<Pool, 'browser'> | PoolRunnerInitializer
/**
* Pool options
*/
poolOptions?: PoolOptions
/**
* Maximum number or percentage of workers to run tests in. `poolOptions.{threads,vmThreads}.maxThreads`/`poolOptions.forks.maxForks` has higher priority.
* Maximum number or percentage of workers to run tests in.
*/
maxWorkers?: number | string
@@ -667,7 +690,7 @@ export interface InlineConfig {
* Debug tests by opening `node:inspector` in worker / child process.
* Provides similar experience as `--inspect` Node CLI argument.
*
* Requires `poolOptions.threads.singleThread: true` OR `poolOptions.forks.singleFork: true`.
* Requires `fileParallelism: false`.
*/
inspect?: boolean | string
@@ -675,7 +698,7 @@ export interface InlineConfig {
* Debug tests by opening `node:inspector` in worker / child process and wait for debugger to connect.
* Provides similar experience as `--inspect-brk` Node CLI argument.
*
* Requires `poolOptions.threads.singleThread: true` OR `poolOptions.forks.singleFork: true`.
* Requires `fileParallelism: false`.
*/
inspectBrk?: boolean | string
@@ -953,7 +976,6 @@ export interface ResolvedConfig
| 'sequence'
| 'typecheck'
| 'runner'
| 'poolOptions'
| 'pool'
| 'cliExclude'
| 'diff'
@@ -961,6 +983,7 @@ export interface ResolvedConfig
| 'snapshotEnvironment'
| 'bail'
| 'name'
| 'vmMemoryLimit'
> {
mode: VitestRunMode
@@ -983,7 +1006,7 @@ export interface ResolvedConfig
browser: ResolvedBrowserOptions
pool: Pool
poolOptions?: ResolvedPoolOptions
poolRunner?: PoolRunnerInitializer
reporters: (InlineReporter | ReporterWithOptions)[]
@@ -1028,6 +1051,7 @@ export interface ResolvedConfig
maxWorkers: number
vmMemoryLimit?: UserConfig['vmMemoryLimit']
dumpDir?: string
}
@@ -1046,7 +1070,7 @@ type NonProjectOptions
| 'ui'
| 'open'
| 'uiBase'
// TODO: allow snapshot options
// TODO: allow snapshot options
| 'snapshotFormat'
| 'resolveSnapshotPath'
| 'passWithNoTests'
@@ -1057,8 +1081,6 @@ type NonProjectOptions
| 'inspect'
| 'inspectBrk'
| 'coverage'
| 'maxWorkers'
| 'fileParallelism'
| 'watchTriggerPatterns'
export interface ServerDepsOptions {
@@ -1091,19 +1113,11 @@ export type ProjectConfig = Omit<
NonProjectOptions
| 'sequencer'
| 'deps'
| 'poolOptions'
> & {
mode?: string
sequencer?: Omit<SequenceOptions, 'sequencer' | 'seed'>
deps?: Omit<DepsOptions, 'moduleDirectories'>
poolOptions?: {
threads?: Pick<
NonNullable<PoolOptions['threads']>,
'singleThread' | 'isolate'
>
vmThreads?: Pick<NonNullable<PoolOptions['vmThreads']>, 'singleThread'>
forks?: Pick<NonNullable<PoolOptions['forks']>, 'singleFork' | 'isolate'>
}
fileParallelism?: boolean
}
export type ResolvedProjectConfig = Omit<
@@ -1,137 +0,0 @@
export type BuiltinPool
= | 'browser'
| 'threads'
| 'forks'
| 'vmThreads'
| 'vmForks'
| 'typescript'
export type Pool = BuiltinPool | (string & {})
export interface PoolOptions extends Record<string, unknown> {
/**
* Run tests in `node:worker_threads`.
*
* Test isolation (when enabled) is done by spawning a new thread for each test file.
*
* This pool is used by default.
*/
threads?: ThreadsOptions & WorkerContextOptions
/**
* Run tests in `node:child_process` using [`fork()`](https://nodejs.org/api/child_process.html#child_processforkmodulepath-args-options)
*
* Test isolation (when enabled) is done by spawning a new child process for each test file.
*/
forks?: ForksOptions & WorkerContextOptions
/**
* Run tests in isolated `node:vm`.
* Test files are run parallel using `node:worker_threads`.
*
* This makes tests run faster, but VM module is unstable. Your tests might leak memory.
*/
vmThreads?: ThreadsOptions & VmOptions
/**
* Run tests in isolated `node:vm`.
*
* Test files are run parallel using `node:child_process` [`fork()`](https://nodejs.org/api/child_process.html#child_processforkmodulepath-args-options)
*
* This makes tests run faster, but VM module is unstable. Your tests might leak memory.
*/
vmForks?: ForksOptions & VmOptions
}
export interface ResolvedPoolOptions extends PoolOptions {
threads?: ResolvedThreadsOptions & WorkerContextOptions
forks?: ResolvedForksOptions & WorkerContextOptions
vmThreads?: ResolvedThreadsOptions & VmOptions
vmForks?: ResolvedForksOptions & VmOptions
}
export interface ThreadsOptions {
/** Maximum amount of threads to use */
maxThreads?: number | string
/**
* Run tests inside a single thread.
*
* @default false
*/
singleThread?: boolean
/**
* Use Atomics to synchronize threads
*
* This can improve performance in some cases, but might cause segfault in older Node versions.
*
* @default false
*/
useAtomics?: boolean
}
export interface ResolvedThreadsOptions extends ThreadsOptions {
maxThreads?: number
}
export interface ForksOptions {
/** Maximum amount of child processes to use */
maxForks?: number | string
/**
* Run tests inside a single fork.
*
* @default false
*/
singleFork?: boolean
}
export interface ResolvedForksOptions extends ForksOptions {
maxForks?: number
}
export interface WorkerContextOptions {
/**
* Isolate test environment by recycling `worker_threads` or `child_process` after each test
*
* @default true
*/
isolate?: boolean
/**
* Pass additional arguments to `node` process when spawning `worker_threads` or `child_process`.
*
* See [Command-line API | Node.js](https://nodejs.org/docs/latest/api/cli.html) for more information.
*
* Set to `process.execArgv` to pass all arguments of the current process.
*
* Be careful when using, it as some options may crash worker, e.g. --prof, --title. See https://github.com/nodejs/node/issues/41103
*
* @default [] // no execution arguments are passed
*/
execArgv?: string[]
}
export interface VmOptions {
/**
* Specifies the memory limit for `worker_thread` or `child_process` before they are recycled.
* If you see memory leaks, try to tinker this value.
*/
memoryLimit?: string | number
/** Isolation is always enabled */
isolate?: true
/**
* Pass additional arguments to `node` process when spawning `worker_threads` or `child_process`.
*
* See [Command-line API | Node.js](https://nodejs.org/docs/latest/api/cli.html) for more information.
*
* Set to `process.execArgv` to pass all arguments of the current process.
*
* Be careful when using, it as some options may crash worker, e.g. --prof, --title. See https://github.com/nodejs/node/issues/41103
*
* @default [] // no execution arguments are passed
*/
execArgv?: string[]
}
+1 -4
View File
@@ -1,6 +1,3 @@
import type { MessagePort } from 'node:worker_threads'
import type { ContextRPC } from '../../types/worker'
export interface WorkerContext extends ContextRPC {
port: MessagePort
}
export interface WorkerContext extends ContextRPC {}
+13 -1
View File
@@ -24,6 +24,19 @@ export { resolveFsAllow } from '../node/plugins/utils'
export type { ProcessPool } from '../node/pool'
export { getFilePoolName } from '../node/pool'
export { createMethodsRPC } from '../node/pools/rpc'
export type {
PoolOptions,
PoolRunnerInitializer,
PoolTask,
PoolWorker,
WorkerRequest,
WorkerResponse,
} from '../node/pools/types'
export { ForksPoolWorker } from '../node/pools/workers/forksWorker'
export { ThreadsPoolWorker } from '../node/pools/workers/threadsWorker'
export { TypecheckPoolWorker } from '../node/pools/workers/typecheckWorker'
export { VmForksPoolWorker } from '../node/pools/workers/vmForksWorker'
export { VmThreadsPoolWorker } from '../node/pools/workers/vmThreadsWorker'
export type { SerializedTestProject, TestProject } from '../node/project'
export type { HTMLOptions } from '../node/reporters/html'
export type { JsonOptions } from '../node/reporters/json'
@@ -90,7 +103,6 @@ export type {
EnvironmentOptions,
InlineConfig,
Pool,
PoolOptions,
ProjectConfig,
ResolvedConfig,
ResolvedProjectConfig,
+2
View File
@@ -0,0 +1,2 @@
export { runBaseTests } from '../runtime/workers/base'
export { init } from '../runtime/workers/init'
+2 -16
View File
@@ -15,6 +15,8 @@ export interface SerializedConfig {
disableConsoleIntercept: boolean | undefined
runner: string | undefined
isolate: boolean
fileParallelism: boolean
maxWorkers: number
mode: 'test' | 'benchmark'
bail: number | undefined
environmentOptions?: Record<string, any>
@@ -49,22 +51,6 @@ export interface SerializedConfig {
hooks: SequenceHooks
setupFiles: SequenceSetupFiles
}
poolOptions: {
forks: {
singleFork: boolean
isolate: boolean
}
threads: {
singleThread: boolean
isolate: boolean
}
vmThreads: {
singleThread: boolean
}
vmForks: {
singleFork: boolean
}
}
deps: {
web: {
transformAssets?: boolean
+1 -10
View File
@@ -68,14 +68,5 @@ export function closeInspector(config: SerializedConfig): void {
function shouldKeepOpen(config: SerializedConfig) {
// In watch mode the inspector can persist re-runs if isolation is disabled and a single worker is used
const isIsolatedSingleThread
= config.pool === 'threads'
&& config.poolOptions?.threads?.isolate === false
&& config.poolOptions?.threads?.singleThread
const isIsolatedSingleFork
= config.pool === 'forks'
&& config.poolOptions?.forks?.isolate === false
&& config.poolOptions?.forks?.singleFork
return config.watch && (isIsolatedSingleFork || isIsolatedSingleThread)
return config.watch && config.isolate === false && config.maxWorkers === 1
}
+3 -7
View File
@@ -25,12 +25,8 @@ export async function run(
): Promise<void> {
const workerState = getWorkerState()
const isIsolatedThreads = config.pool === 'threads' && (config.poolOptions?.threads?.isolate ?? true)
const isIsolatedForks = config.pool === 'forks' && (config.poolOptions?.forks?.isolate ?? true)
const isolate = isIsolatedThreads || isIsolatedForks
await setupGlobalEnv(config, environment, moduleRunner)
await startCoverageInsideWorker(config.coverage, moduleRunner, { isolate })
await startCoverageInsideWorker(config.coverage, moduleRunner, { isolate: config.isolate })
if (config.chaiConfig) {
setupChaiConfig(config.chaiConfig)
@@ -54,7 +50,7 @@ export async function run(
= performance.now() - workerState.durations.environment
for (const file of files) {
if (isolate) {
if (config.isolate) {
moduleRunner.mocker.reset()
resetModules(workerState.evaluatedModules, true)
}
@@ -74,7 +70,7 @@ export async function run(
vi.restoreAllMocks()
}
await stopCoverageInsideWorker(config.coverage, moduleRunner, { isolate })
await stopCoverageInsideWorker(config.coverage, moduleRunner, { isolate: config.isolate })
},
)
@@ -1,16 +0,0 @@
import type { ContextRPC } from '../types/worker'
import * as worker from './worker'
import forks from './workers/forks'
import threads from './workers/threads'
export async function run(ctx: ContextRPC): Promise<void> {
await worker.execute('run', ctx, ctx.pool === 'forks' ? forks : threads)
}
export async function collect(ctx: ContextRPC): Promise<void> {
await worker.execute('collect', ctx, ctx.pool === 'forks' ? forks : threads)
}
export async function teardown(): Promise<void> {
await worker.teardown()
}
-16
View File
@@ -1,16 +0,0 @@
import type { ContextRPC } from '../types/worker'
import * as worker from './worker'
import vmForks from './workers/vmForks'
import vmThreads from './workers/vmThreads'
export async function run(ctx: ContextRPC): Promise<void> {
await worker.execute('run', ctx, ctx.pool === 'vmForks' ? vmForks : vmThreads)
}
export async function collect(ctx: ContextRPC): Promise<void> {
await worker.execute('collect', ctx, ctx.pool === 'vmForks' ? vmForks : vmThreads)
}
export async function teardown(): Promise<void> {
await worker.teardown()
}
+15 -39
View File
@@ -6,53 +6,21 @@ import { loadEnvironment } from '../integrations/env/loader'
import { setupInspect } from './inspector'
import { VitestEvaluatedModules } from './moduleRunner/evaluatedModules'
import { createRuntimeRpc, rpcDone } from './rpc'
import { isChildProcess } from './utils'
import { disposeInternalListeners } from './workers/utils'
if (isChildProcess()) {
const isProfiling = process.execArgv.some(
execArg =>
execArg.startsWith('--prof')
|| execArg.startsWith('--cpu-prof')
|| execArg.startsWith('--heap-prof')
|| execArg.startsWith('--diagnostic-dir'),
)
if (isProfiling) {
// Work-around for nodejs/node#55094
process.on('SIGTERM', () => {
process.exit()
})
}
}
const resolvingModules = new Set<string>()
const globalListeners = new Set<() => unknown>()
// this is what every pool executes when running tests
export async function execute(method: 'run' | 'collect', ctx: ContextRPC, worker: VitestWorker): Promise<void> {
disposeInternalListeners()
async function execute(method: 'run' | 'collect', ctx: ContextRPC, worker: VitestWorker) {
const prepareStart = performance.now()
const cleanups: (() => void | Promise<void>)[] = [setupInspect(ctx)]
process.env.VITEST_WORKER_ID = String(ctx.workerId)
const poolId = process.__tinypool_state__?.workerId
process.env.VITEST_POOL_ID = String(poolId)
let environmentLoader: ModuleRunner | undefined
// RPC is used to communicate between worker (be it a thread worker or child process or a custom implementation) and the main thread
const { rpc, onCancel } = createRuntimeRpc(worker)
try {
if (!worker.getRpcOptions || typeof worker.getRpcOptions !== 'function') {
throw new TypeError(
`Test worker should expose "getRpcOptions" method. Received "${typeof worker.getRpcOptions}".`,
)
}
// RPC is used to communicate between worker (be it a thread worker or child process or a custom implementation) and the main thread
const { rpc, onCancel } = createRuntimeRpc(worker.getRpcOptions(ctx))
// do not close the RPC channel so that we can get the error messages sent to the main thread
cleanups.push(async () => {
await Promise.all(rpc.$rejectPendingCalls(({ method, reject }) => {
@@ -100,13 +68,21 @@ export async function execute(method: 'run' | 'collect', ctx: ContextRPC, worker
await Promise.all(cleanups.map(fn => fn()))
await rpcDone().catch(() => {})
environmentLoader?.close()
await environmentLoader?.close()
rpc.$close()
}
}
export function run(ctx: ContextRPC, worker: VitestWorker): Promise<void> {
return execute('run', ctx, worker)
}
export function collect(ctx: ContextRPC, worker: VitestWorker): Promise<void> {
return execute('collect', ctx, worker)
}
export async function teardown(): Promise<void> {
const promises = [...globalListeners].map(l => l())
await Promise.all(promises)
await Promise.all([...globalListeners].map(l => l()))
}
function createImportMetaEnvProxy(): WorkerGlobalState['metaEnv'] {
@@ -23,6 +23,7 @@ function startModuleRunner(options: ContextModuleRunnerOptions) {
return _moduleRunner
}
/** @experimental */
export async function runBaseTests(method: 'run' | 'collect', state: WorkerGlobalState): Promise<void> {
const { ctx } = state
// state has new context, but we want to reuse existing ones
+2 -34
View File
@@ -1,36 +1,4 @@
import type { WorkerGlobalState } from '../../types/worker'
import type { VitestWorker, WorkerRpcOptions } from './types'
import v8 from 'node:v8'
import { runBaseTests } from './base'
import { createForksRpcOptions, unwrapSerializableConfig } from './utils'
import workerInit from './init-forks'
class ForksBaseWorker implements VitestWorker {
getRpcOptions(): WorkerRpcOptions {
return createForksRpcOptions(v8)
}
async executeTests(method: 'run' | 'collect', state: WorkerGlobalState): Promise<void> {
// TODO: don't rely on reassigning process.exit
// https://github.com/vitest-dev/vitest/pull/4441#discussion_r1443771486
const exit = process.exit
state.ctx.config = unwrapSerializableConfig(state.ctx.config)
try {
await runBaseTests(method, state)
}
finally {
process.exit = exit
}
}
runTests(state: WorkerGlobalState): Promise<void> {
return this.executeTests('run', state)
}
collectTests(state: WorkerGlobalState): Promise<void> {
return this.executeTests('collect', state)
}
}
const worker: ForksBaseWorker = new ForksBaseWorker()
export default worker
workerInit({ runTests: runBaseTests })
@@ -0,0 +1,107 @@
import type { ResolvedConfig, SerializedConfig } from '../../node/types/config'
import type { WorkerGlobalState } from '../../types/worker'
import v8 from 'node:v8'
import { init } from './init'
if (!process.send) {
throw new Error('Expected worker to be run in node:child_process')
}
// Store globals in case tests overwrite them
const processExit = process.exit.bind(process)
const processSend = process.send.bind(process)
const processOn = process.on.bind(process)
const processOff = process.off.bind(process)
const processRemoveAllListeners = process.removeAllListeners.bind(process)
const isProfiling = process.execArgv.some(
execArg =>
execArg.startsWith('--prof')
|| execArg.startsWith('--cpu-prof')
|| execArg.startsWith('--heap-prof')
|| execArg.startsWith('--diagnostic-dir'),
)
// Work-around for nodejs/node#55094
if (isProfiling) {
processOn('SIGTERM', () => processExit())
}
export default function workerInit(options: {
runTests: (method: 'run' | 'collect', state: WorkerGlobalState) => Promise<void>
}): void {
const { runTests } = options
init({
post: v => processSend(v),
on: cb => processOn('message', cb),
off: cb => processOff('message', cb),
teardown: () => processRemoveAllListeners('message'),
serialize: v8.serialize,
deserialize: v => v8.deserialize(Buffer.from(v)),
runTests: state => executeTests('run', state),
collectTests: state => executeTests('collect', state),
})
async function executeTests(method: 'run' | 'collect', state: WorkerGlobalState) {
state.ctx.config = unwrapSerializableConfig(state.ctx.config)
try {
await runTests(method, state)
}
finally {
process.exit = processExit
}
}
}
/**
* Reverts the wrapping done by `wrapSerializableConfig` in {@link file://./../../node/pool/runtimes/forks.ts}
*/
function unwrapSerializableConfig(config: SerializedConfig): SerializedConfig {
if (config.testNamePattern && typeof config.testNamePattern === 'string') {
const testNamePattern = config.testNamePattern as string
if (testNamePattern.startsWith('$$vitest:')) {
config.testNamePattern = parseRegexp(testNamePattern.slice('$$vitest:'.length))
}
}
if (
config.defines
&& Array.isArray(config.defines.keys)
&& config.defines.original
) {
const { keys, original } = config.defines
const defines: ResolvedConfig['defines'] = {}
// Apply all keys from the original. Entries which had undefined value are missing from original now
for (const key of keys) {
defines[key] = original[key]
}
config.defines = defines
}
return config
}
function parseRegexp(input: string): RegExp {
// Parse input
// eslint-disable-next-line regexp/no-misleading-capturing-group
const m = input.match(/(\/?)(.+)\1([a-z]*)/i)
// match nothing
if (!m) {
return /$^/
}
// Invalid flags
// eslint-disable-next-line regexp/optimal-quantifier-concatenation
if (m[3] && !/^(?!.*?(.).*?\1)[gmixXsuUAJ]+$/.test(m[3])) {
return new RegExp(input)
}
// Create the regular expression
return new RegExp(m[2], m[3])
}
@@ -0,0 +1,22 @@
import type { WorkerGlobalState } from '../../types/worker'
import { isMainThread, parentPort } from 'node:worker_threads'
import { init } from './init'
if (isMainThread || !parentPort) {
throw new Error('Expected worker to be run in node:worker_threads')
}
export default function workerInit(options: {
runTests: (method: 'run' | 'collect', state: WorkerGlobalState) => Promise<void>
}): void {
const { runTests } = options
init({
post: response => parentPort!.postMessage(response),
on: callback => parentPort!.on('message', callback),
off: callback => parentPort!.off('message', callback),
teardown: () => parentPort!.removeAllListeners('message'),
runTests: async state => runTests('run', state),
collectTests: async state => runTests('collect', state),
})
}
+124
View File
@@ -0,0 +1,124 @@
import type { WorkerRequest, WorkerResponse } from '../../node/pools/types'
import type { VitestWorker } from './types'
import { serializeError } from '@vitest/utils/error'
import * as entrypoint from '../worker'
interface Options extends VitestWorker {
teardown?: () => void
}
const __vitest_worker_response__ = true
const memoryUsage = process.memoryUsage.bind(process)
let reportMemory = false
/** @experimental */
export function init(worker: Options): void {
worker.on(onMessage)
let runPromise: Promise<unknown> | undefined
let isRunning = false
function send(response: WorkerResponse) {
worker.post(worker.serialize ? worker.serialize(response) : response)
}
async function onMessage(rawMessage: unknown) {
const message: WorkerRequest = worker.deserialize
? worker.deserialize(rawMessage)
: rawMessage
if (message?.__vitest_worker_request__ !== true) {
return
}
switch (message.type) {
case 'start': {
reportMemory = message.options.reportMemory
send({ type: 'started', __vitest_worker_response__ })
break
}
case 'run': {
// Prevent concurrent execution if worker is already running
if (isRunning) {
send({
type: 'testfileFinished',
__vitest_worker_response__,
error: serializeError(new Error('[vitest-worker]: Worker is already running tests')),
})
return
}
isRunning = true
process.env.VITEST_POOL_ID = String(message.poolId)
process.env.VITEST_WORKER_ID = String(message.context.workerId)
try {
runPromise = entrypoint.run(message.context, worker)
.catch(error => serializeError(error))
const error = await runPromise
send({
type: 'testfileFinished',
__vitest_worker_response__,
error,
usedMemory: reportMemory ? memoryUsage().heapUsed : undefined,
})
}
finally {
runPromise = undefined
isRunning = false
}
break
}
case 'collect': {
// Prevent concurrent execution if worker is already running
if (isRunning) {
send({
type: 'testfileFinished',
__vitest_worker_response__,
error: serializeError(new Error('[vitest-worker]: Worker is already running tests')),
})
return
}
isRunning = true
process.env.VITEST_POOL_ID = String(message.poolId)
process.env.VITEST_WORKER_ID = String(message.context.workerId)
try {
runPromise = entrypoint.collect(message.context, worker)
.catch(error => serializeError(error))
const error = await runPromise
send({
type: 'testfileFinished',
__vitest_worker_response__,
error,
usedMemory: reportMemory ? memoryUsage().heapUsed : undefined,
})
}
finally {
runPromise = undefined
isRunning = false
}
break
}
case 'stop': {
await runPromise
const error = await entrypoint.teardown()
.catch(error => serializeError(error))
send({ type: 'stopped', error, __vitest_worker_response__ })
worker.teardown?.()
break
}
}
}
}
+2 -20
View File
@@ -1,22 +1,4 @@
import type { WorkerContext } from '../../node/types/worker'
import type { ContextRPC, WorkerGlobalState } from '../../types/worker'
import type { VitestWorker, WorkerRpcOptions } from './types'
import { runBaseTests } from './base'
import { createThreadsRpcOptions } from './utils'
import workerInit from './init-threads'
class ThreadsBaseWorker implements VitestWorker {
getRpcOptions(ctx: ContextRPC): WorkerRpcOptions {
return createThreadsRpcOptions(ctx as WorkerContext)
}
runTests(state: WorkerGlobalState): unknown {
return runBaseTests('run', state)
}
collectTests(state: WorkerGlobalState): unknown {
return runBaseTests('collect', state)
}
}
const worker: ThreadsBaseWorker = new ThreadsBaseWorker()
export default worker
workerInit({ runTests: runBaseTests })
+4 -5
View File
@@ -1,15 +1,14 @@
import type { Awaitable } from '@vitest/utils'
import type { BirpcOptions } from 'birpc'
import type { RuntimeRPC } from '../../types/rpc'
import type { ContextRPC, WorkerGlobalState } from '../../types/worker'
import type { WorkerGlobalState } from '../../types/worker'
export type WorkerRpcOptions = Pick<
type WorkerRpcOptions = Pick<
BirpcOptions<RuntimeRPC>,
'on' | 'post' | 'serialize' | 'deserialize'
'on' | 'off' | 'post' | 'serialize' | 'deserialize'
>
export interface VitestWorker {
getRpcOptions: (ctx: ContextRPC) => WorkerRpcOptions
export interface VitestWorker extends WorkerRpcOptions {
runTests: (state: WorkerGlobalState) => Awaitable<unknown>
collectTests: (state: WorkerGlobalState) => Awaitable<unknown>
}
@@ -1,112 +0,0 @@
import type { TinypoolWorkerMessage } from 'tinypool'
import type { ResolvedConfig, SerializedConfig } from '../../node/types/config'
import type { WorkerContext } from '../../node/types/worker'
import type { WorkerRpcOptions } from './types'
const REGEXP_WRAP_PREFIX = '$$vitest:'
// Store global APIs in case process is overwritten by tests
const processSend = process.send?.bind(process)
const processOn = process.on?.bind(process)
const processOff = process.off?.bind(process)
const dispose: (() => void)[] = []
export function createThreadsRpcOptions({
port,
}: WorkerContext): WorkerRpcOptions {
return {
post: (v) => {
port.postMessage(v)
},
on: (fn) => {
port.addListener('message', fn)
},
}
}
export function disposeInternalListeners(): void {
for (const fn of dispose) {
try {
fn()
}
catch {}
}
dispose.length = 0
}
export function createForksRpcOptions(
nodeV8: typeof import('v8'),
): WorkerRpcOptions {
return {
serialize: nodeV8.serialize,
deserialize: v => nodeV8.deserialize(Buffer.from(v)),
post(v) {
processSend!(v)
},
on(fn) {
const handler = (message: any, ...extras: any) => {
// Do not react on Tinypool's internal messaging
if ((message as TinypoolWorkerMessage)?.__tinypool_worker_message__) {
return
}
return fn(message, ...extras)
}
processOn('message', handler)
dispose.push(() => processOff('message', handler))
},
}
}
/**
* Reverts the wrapping done by `utils/config-helpers.ts`'s `wrapSerializableConfig`
*/
export function unwrapSerializableConfig(config: SerializedConfig): SerializedConfig {
if (config.testNamePattern && typeof config.testNamePattern === 'string') {
const testNamePattern = config.testNamePattern as string
if (testNamePattern.startsWith(REGEXP_WRAP_PREFIX)) {
config.testNamePattern = parseRegexp(
testNamePattern.slice(REGEXP_WRAP_PREFIX.length),
)
}
}
if (
config.defines
&& Array.isArray(config.defines.keys)
&& config.defines.original
) {
const { keys, original } = config.defines
const defines: ResolvedConfig['defines'] = {}
// Apply all keys from the original. Entries which had undefined value are missing from original now
for (const key of keys) {
defines[key] = original[key]
}
config.defines = defines
}
return config
}
function parseRegexp(input: string): RegExp {
// Parse input
// eslint-disable-next-line regexp/no-misleading-capturing-group
const m = input.match(/(\/?)(.+)\1([a-z]*)/i)
// match nothing
if (!m) {
return /$^/
}
// Invalid flags
// eslint-disable-next-line regexp/optimal-quantifier-concatenation
if (m[3] && !/^(?!.*?(.).*?\1)[gmixXsuUAJ]+$/.test(m[3])) {
return new RegExp(input)
}
// Create the regular expression
return new RegExp(m[2], m[3])
}
+2 -32
View File
@@ -1,34 +1,4 @@
import type { WorkerGlobalState } from '../../types/worker'
import type { VitestWorker, WorkerRpcOptions } from './types'
import v8 from 'node:v8'
import { createForksRpcOptions, unwrapSerializableConfig } from './utils'
import workerInit from './init-forks'
import { runVmTests } from './vm'
class ForksVmWorker implements VitestWorker {
getRpcOptions(): WorkerRpcOptions {
return createForksRpcOptions(v8)
}
async executeTests(method: 'run' | 'collect', state: WorkerGlobalState): Promise<void> {
const exit = process.exit
state.ctx.config = unwrapSerializableConfig(state.ctx.config)
try {
await runVmTests(method, state)
}
finally {
process.exit = exit
}
}
runTests(state: WorkerGlobalState): Promise<void> {
return this.executeTests('run', state)
}
collectTests(state: WorkerGlobalState): Promise<void> {
return this.executeTests('collect', state)
}
}
const worker: ForksVmWorker = new ForksVmWorker()
export default worker
workerInit({ runTests: runVmTests })
@@ -1,22 +1,4 @@
import type { WorkerContext } from '../../node/types/worker'
import type { ContextRPC, WorkerGlobalState } from '../../types/worker'
import type { VitestWorker, WorkerRpcOptions } from './types'
import { createThreadsRpcOptions } from './utils'
import workerInit from './init-threads'
import { runVmTests } from './vm'
class ThreadsVmWorker implements VitestWorker {
getRpcOptions(ctx: ContextRPC): WorkerRpcOptions {
return createThreadsRpcOptions(ctx as WorkerContext)
}
runTests(state: WorkerGlobalState): unknown {
return runVmTests('run', state)
}
collectTests(state: WorkerGlobalState): unknown {
return runVmTests('collect', state)
}
}
const worker: ThreadsVmWorker = new ThreadsVmWorker()
export default worker
workerInit({ runTests: runVmTests })
+2 -1
View File
@@ -1,7 +1,8 @@
import type { FileSpecification } from '@vitest/runner'
import type { TestExecutionMethod } from './worker'
export interface BrowserTesterOptions {
method: TestExecutionMethod
files: string[]
files: FileSpecification[]
providedContext: string
}
+4 -2
View File
@@ -16,13 +16,15 @@ export type TestExecutionMethod = 'run' | 'collect'
export interface ContextRPC {
pool: string
workerId: number
config: SerializedConfig
projectName: string
files: string[] | FileSpecification[]
files: FileSpecification[]
environment: ContextTestEnvironment
providedContext: Record<string, any>
invalidates?: string[]
/** Exposed to test runner as `VITEST_WORKER_ID`. Value is unique per each isolated worker. */
workerId: number
}
export interface WorkerGlobalState {
@@ -2,9 +2,6 @@ import type {
BenchmarkBuiltinReporters,
BuiltinReporters,
} from '../node/reporters'
import type { SerializedConfig } from '../node/types/config'
const REGEXP_WRAP_PREFIX = '$$vitest:'
interface PotentialConfig {
outputFile?: string | Partial<Record<string, string>>
@@ -25,31 +22,6 @@ export function getOutputFile(
return config.outputFile[reporter]
}
/**
* Prepares `SerializedConfig` for serialization, e.g. `node:v8.serialize`
*/
export function wrapSerializableConfig(config: SerializedConfig) {
let testNamePattern = config.testNamePattern
let defines = config.defines
// v8 serialize does not support regex
if (testNamePattern && typeof testNamePattern !== 'string') {
testNamePattern
= `${REGEXP_WRAP_PREFIX}${testNamePattern.toString()}` as unknown as RegExp
}
// v8 serialize drops properties with undefined value
if (defines) {
defines = { keys: Object.keys(defines), original: defines }
}
return {
...config,
testNamePattern,
defines,
} as SerializedConfig
}
export function createDefinesScript(define: Record<string, any> | undefined): string {
if (!define) {
return ''
+7 -18
View File
@@ -8,7 +8,7 @@
import type { ResolvedConfig } from '../node/types/config'
import * as nodeos from 'node:os'
function getDefaultThreadsCount(config: ResolvedConfig) {
function getDefaultThreadsCount(config: Pick<ResolvedConfig, 'watch'>) {
const numCpus
= typeof nodeos.availableParallelism === 'function'
? nodeos.availableParallelism()
@@ -19,25 +19,14 @@ function getDefaultThreadsCount(config: ResolvedConfig) {
: Math.max(numCpus - 1, 1)
}
export function getWorkerMemoryLimit(config: ResolvedConfig, pool: 'vmThreads' | 'vmForks'): string | number {
if (pool === 'vmForks') {
const opts = config.poolOptions?.vmForks ?? {}
if (opts.memoryLimit) {
return opts.memoryLimit
}
const workers = opts.maxForks ?? getDefaultThreadsCount(config)
return 1 / workers
export function getWorkerMemoryLimit(config: Pick<ResolvedConfig, 'vmMemoryLimit' | 'maxWorkers' | 'watch'>): string | number {
if (config.vmMemoryLimit) {
return config.vmMemoryLimit
}
else {
const opts = config.poolOptions?.vmThreads ?? {}
if (opts.memoryLimit) {
return opts.memoryLimit
}
const workers = opts.maxThreads ?? getDefaultThreadsCount(config)
return 1 / workers
}
const workers = config.maxWorkers ?? getDefaultThreadsCount(config)
return 1 / workers
}
/**
+1
View File
@@ -0,0 +1 @@
export * from './dist/worker.js'
-9
View File
@@ -1011,9 +1011,6 @@ importers:
tinyglobby:
specifier: 'catalog:'
version: 0.2.15
tinypool:
specifier: ^2.0.0
version: 2.0.0
tinyrainbow:
specifier: 'catalog:'
version: 3.0.3
@@ -8817,10 +8814,6 @@ packages:
picocolors:
optional: true
[email protected]:
resolution: {integrity: sha512-/RX9RzeH2xU5ADE7n2Ykvmi9ED3FBGPAjw9u3zucrNNaEBIO0HPSYgL0NT7+3p147ojeSdaVu08F6hjpv31HJg==}
engines: {node: ^20.0.0 || >=22.0.0}
[email protected]:
resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==}
engines: {node: '>=14.0.0'}
@@ -17643,8 +17636,6 @@ snapshots:
optionalDependencies:
picocolors: 1.1.1
[email protected]: {}
[email protected]: {}
[email protected]: {}
+1 -5
View File
@@ -5,11 +5,7 @@ export default defineConfig({
test: {
include: ['specs/**/*.{spec,test}.ts'],
pool: 'threads',
poolOptions: {
threads: {
singleThread: true,
},
},
fileParallelism: false,
reporters: 'verbose',
setupFiles: ['./setup.unit.ts'],
// 3 is the maximum of browser instances - in a perfect world they will run in parallel
@@ -1,19 +1,82 @@
import type { RunnerTestCase } from 'vitest'
import type { ProcessPool, Vitest } from 'vitest/node'
import type { PoolWorker, PoolRunnerInitializer, TestProject, Vitest, WorkerRequest, WorkerResponse, PoolOptions } from 'vitest/node'
import { createFileTask } from '@vitest/runner/utils'
import { normalize } from 'pathe'
import EventEmitter from 'node:events';
export default (vitest: Vitest): ProcessPool => {
const options = vitest.config.poolOptions?.custom as any
interface OptionsCustomPool {
print: any;
array: any;
}
export function createCustomPool(settings: OptionsCustomPool): PoolRunnerInitializer {
return {
name: 'custom',
async collectTests() {
throw new Error('Not implemented')
},
async runTests(specs) {
createPoolWorker: (options) => new CustomRuntimeWorker(options, settings),
}
}
export class CustomRuntimeWorker implements PoolWorker {
public readonly name = 'custom'
private vitest: Vitest
private customEvents = new EventEmitter()
readonly execArgv: string[]
readonly env: Record<string, string>
private project: TestProject
constructor(options: PoolOptions, private settings: OptionsCustomPool) {
this.execArgv = options.execArgv
this.env = options.env
this.vitest = options.project.vitest
this.project = options.project
}
send(request: WorkerRequest) {
void onMessage(request, this.project, this.settings).then((response) => {
if (response) {
this.customEvents.emit('message', response)
}
})
}
on(event: string, callback: (arg: any) => void): void {
this.customEvents.on(event, callback)
}
off(event: string, callback: (arg: any) => void): void {
this.customEvents.off(event, callback)
}
deserialize(data: unknown): unknown {
return data
}
async start() {
// noop
}
async stop() {
this.vitest.logger.console.warn('[pool] custom pool is closed!')
}
}
const __vitest_worker_response__ = true
async function onMessage(message: WorkerRequest, project: TestProject, options: OptionsCustomPool): Promise<WorkerResponse | void> {
if (message?.__vitest_worker_request__ !== true) {
return undefined
}
const vitest = project.vitest
switch (message.type) {
case 'start': {
return { type: 'started', __vitest_worker_response__ }
}
case 'run': {
vitest.logger.console.warn('[pool] printing:', options.print)
vitest.logger.console.warn('[pool] array option', options.array)
for (const { project, moduleId } of specs) {
for (const { filepath: moduleId } of message.context.files) {
vitest.state.clearFiles(project)
vitest.logger.console.warn('[pool] running tests for', project.name, 'in', normalize(moduleId).toLowerCase().replace(normalize(process.cwd()).toLowerCase(), ''))
const taskFile = createFileTask(
@@ -42,9 +105,14 @@ export default (vitest: Vitest): ProcessPool => {
taskFile.tasks.push(taskTest)
await vitest._reportFileTask(taskFile)
}
},
close() {
vitest.logger.console.warn('[pool] custom pool is closed!')
},
return { type: 'testfileFinished', __vitest_worker_response__ }
}
case 'stop': {
return { type: 'stopped', __vitest_worker_response__ }
}
}
throw new Error(`Unexpected message ${JSON.stringify(message, null, 2)}`)
}
@@ -1,19 +1,17 @@
import { defineConfig } from 'vitest/config'
import { createCustomPool } from './pool/custom-pool'
export default defineConfig({
test: {
poolOptions: {
custom: {
print: 'options are respected',
array: [1, 2, 3],
},
},
projects: [
{
extends: true,
test: {
name: 'custom-pool-test',
pool: './pool/custom-pool.ts',
pool: createCustomPool({
print: 'options are respected',
array: [1, 2, 3],
}),
exclude: ['**/*.threads.spec.ts'],
},
},
+1 -5
View File
@@ -3,10 +3,6 @@ import { defineConfig } from 'vite'
export default defineConfig({
test: {
pool: 'forks',
poolOptions: {
forks: {
isolate: false,
},
},
isolate: false,
},
})
+1 -5
View File
@@ -3,11 +3,7 @@ import { defineConfig } from 'vite'
export default defineConfig({
test: {
pool: 'forks',
poolOptions: {
forks: {
isolate: false,
},
},
isolate: false,
expect: {
requireAssertions: true,
}
+1 -1
View File
@@ -7,6 +7,6 @@ test.each(['forks', 'vmForks'] as const)('test case\'s process.send() calls are
pool,
})
expect(stderr).toContain('⎯⎯⎯⎯ Unhandled Rejection ⎯⎯⎯⎯⎯')
expect(stderr).toContain('⎯⎯⎯⎯ Unhandled Errors ⎯⎯⎯⎯⎯')
expect(stderr).toContain('Error: [vitest-pool]: Unexpected call to process.send(). Make sure your test cases are not interfering with process\'s channel.')
})
+5 -13
View File
@@ -2,34 +2,26 @@ import { expect, it } from 'vitest'
import { runVitest } from '../../test-utils'
const config = {
poolOptions: {
threads: {
execArgv: ['--experimental-network-imports'],
},
forks: {
execArgv: ['--experimental-network-imports'],
},
vmThreads: {
execArgv: ['--experimental-network-imports'],
},
},
execArgv: ['--experimental-network-imports'],
// let vite serve public/[email protected]
api: 9602,
}
const [major] = process.version.slice(1).split('.')
// TODO: remove when we drop support for Node 20
// --experimental-network-imports was removed in Node 22 in favor of module loaders
it.runIf(Number(major) <= 20).each([
'threads',
'forks',
'vmThreads',
])('importing from network in %s', async (pool) => {
const { stderr, exitCode } = await runVitest({
const { ctx, exitCode } = await runVitest({
...config,
root: './fixtures/network-imports',
pool,
})
expect(stderr).toBe('')
expect(ctx!.state.getTestModules()).toHaveLength(1)
expect(ctx!.state.getTestModules()[0].state()).toBe('passed')
expect(exitCode).toBe(0)
})
+4 -4
View File
@@ -96,11 +96,11 @@ test('cancelling test run aborts the signal', async () => {
import { test } from 'vitest'
test('aborted', async ({ signal, task }) => {
return new Promise(resolve => {
console.log('ready')
signal.addEventListener('abort', () => {
task.meta.aborted = true
resolve()
})
console.log('ready')
})
}, Infinity)
`,
@@ -126,13 +126,13 @@ test('cancelling test run aborts the signal in all concurrent tests', async () =
{ timeout: Infinity },
async (number, { signal, task }) => {
return new Promise(resolve => {
if (number === 3) {
console.log('ready')
}
signal.addEventListener('abort', () => {
task.meta.aborted = true
resolve()
})
if (number === 3) {
console.log('ready')
}
})
})
`,
+1 -5
View File
@@ -7,11 +7,7 @@ export default defineConfig({
reporters: ['verbose'],
testTimeout: 60_000,
globals: true,
poolOptions: {
forks: {
singleFork: true,
},
},
fileParallelism: false,
chaiConfig: {
truncateThreshold: 999,
},
@@ -1,17 +0,0 @@
import { expect, test } from 'vitest'
import type { TestUserConfig } from 'vitest/config'
const pool = process.env.TESTED_POOL as "forks" | "threads";
test('is isolated', () => {
// @ts-expect-error -- internal
const config: TestUserConfig = globalThis.__vitest_worker__.config
if (pool === 'forks') {
expect(config.poolOptions?.forks?.isolate).toBe(true)
}
else {
expect(pool).toBe('threads')
expect(config.poolOptions?.threads?.isolate).toBe(true)
}
})
@@ -1,17 +0,0 @@
import { expect, test } from 'vitest'
import type { TestUserConfig } from 'vitest/config'
const pool = process.env.TESTED_POOL as "forks" | "threads";
test('is isolated', () => {
// @ts-expect-error -- internal
const config: TestUserConfig = globalThis.__vitest_worker__.config
if (pool === 'forks') {
expect(config.poolOptions?.forks?.isolate).toBe(false)
}
else {
expect(pool).toBe('threads')
expect(config.poolOptions?.threads?.isolate).toBe(false)
}
})
@@ -0,0 +1,6 @@
import { test } from 'vitest'
test('print config', () => {
// @ts-expect-error -- internal
console.log(JSON.stringify(globalThis.__vitest_worker__.config, null, 2))
})
+4 -10
View File
@@ -8,7 +8,7 @@ const configs: TestUserConfig[] = []
const pools: TestUserConfig[] = [
{ pool: 'threads' },
{ pool: 'forks' },
{ pool: 'threads', poolOptions: { threads: { singleThread: true } } },
{ pool: 'threads', fileParallelism: false },
]
if (process.platform !== 'win32') {
@@ -42,13 +42,7 @@ for (const isolate of [true, false]) {
for (const pool of pools) {
configs.push({
...pool,
poolOptions: {
threads: {
...pool.poolOptions?.threads,
isolate,
},
forks: { isolate },
},
isolate,
browser: {
...pool.browser!,
isolate,
@@ -65,8 +59,8 @@ for (const config of configs) {
},
async () => {
const isParallel
= (config.pool === 'threads' && config.poolOptions?.threads?.singleThread !== true)
|| (config.pool === 'forks' && config.poolOptions?.forks?.singleFork !== true)
= (config.pool === 'threads' && config.fileParallelism !== false)
|| (config.pool === 'forks' && config.fileParallelism !== false)
|| (config.browser?.enabled && config.browser.fileParallelism)
// THREADS here means that multiple tests are run parallel
-1
View File
@@ -19,7 +19,6 @@ describe('define project helper', () => {
test('allows expected project fields on a project config', () => {
expectProjectTestConfig.toHaveProperty('pool')
expectProjectTestConfig.toHaveProperty('poolOptions')
})
})
+1 -5
View File
@@ -23,11 +23,7 @@ test.each([
root,
include: [fileToTest],
pool,
poolOptions: {
[pool]: {
execArgv,
},
},
execArgv: [...execArgv],
})
expect(vitest.stdout).toContain(`✓ ${fileToTest}`)
+7 -7
View File
@@ -72,31 +72,31 @@ test('shard count can be smaller than count of test files when passWithNoTests',
test('inspect requires changing pool and singleThread/singleFork', async () => {
const { stderr } = await runVitest({ inspect: true })
expect(stderr).toMatch('Error: You cannot use --inspect without "--no-file-parallelism", "poolOptions.threads.singleThread" or "poolOptions.forks.singleFork"')
expect(stderr).toMatch('Error: You cannot use --inspect without "--no-file-parallelism"')
})
test('inspect cannot be used with multi-threading', async () => {
const { stderr } = await runVitest({ inspect: true, pool: 'threads', poolOptions: { threads: { singleThread: false } } })
const { stderr } = await runVitest({ inspect: true, pool: 'threads', fileParallelism: true })
expect(stderr).toMatch('Error: You cannot use --inspect without "--no-file-parallelism", "poolOptions.threads.singleThread" or "poolOptions.forks.singleFork"')
expect(stderr).toMatch('Error: You cannot use --inspect without "--no-file-parallelism"')
})
test('inspect in browser mode requires no-file-parallelism', async () => {
const { stderr } = await runVitest({ inspect: true, browser: { enabled: true, instances: [{ browser: 'chromium' }], provider: playwright() } })
expect(stderr).toMatch('Error: You cannot use --inspect without "--no-file-parallelism", "poolOptions.threads.singleThread" or "poolOptions.forks.singleFork"')
expect(stderr).toMatch('Error: You cannot use --inspect without "--no-file-parallelism"')
})
test('inspect-brk cannot be used with multi processing', async () => {
const { stderr } = await runVitest({ inspect: true, pool: 'forks', poolOptions: { forks: { singleFork: false } } })
const { stderr } = await runVitest({ inspect: true, pool: 'forks', fileParallelism: true })
expect(stderr).toMatch('Error: You cannot use --inspect without "--no-file-parallelism", "poolOptions.threads.singleThread" or "poolOptions.forks.singleFork"')
expect(stderr).toMatch('Error: You cannot use --inspect without "--no-file-parallelism"')
})
test('inspect-brk in browser mode requires no-file-parallelism', async () => {
const { stderr } = await runVitest({ inspectBrk: true, browser: { enabled: true, instances: [{ browser: 'chromium' }], provider: playwright() } })
expect(stderr).toMatch('Error: You cannot use --inspect-brk without "--no-file-parallelism", "poolOptions.threads.singleThread" or "poolOptions.forks.singleFork"')
expect(stderr).toMatch('Error: You cannot use --inspect-brk without "--no-file-parallelism"')
})
test('inspect and --inspect-brk cannot be used when not playwright + chromium', async () => {
-1
View File
@@ -6,7 +6,6 @@ test.for([
{ isolate: true },
{ isolate: false, maxWorkers: 1 },
{ isolate: false, fileParallelism: false },
{ isolate: false, poolOptions: { forks: { singleFork: true } } },
] satisfies TestUserConfig[])(`getState().testPath during collection %s`, async (config) => {
const result = await runVitest({
root: './fixtures/get-state',
+5 -5
View File
@@ -4,11 +4,11 @@ import { expect, test } from 'vitest'
import { runVitest } from '../../test-utils'
const configs: TestUserConfig[] = [
{ pool: 'threads', poolOptions: { threads: { isolate: false, singleThread: true } } },
{ pool: 'threads', poolOptions: { threads: { isolate: false, singleThread: false } } },
{ pool: 'threads', poolOptions: { threads: { isolate: false, maxThreads: 1 } } },
{ pool: 'forks', poolOptions: { forks: { isolate: true } } },
{ pool: 'forks', poolOptions: { forks: { isolate: false } } },
{ pool: 'threads', isolate: false, fileParallelism: false },
{ pool: 'threads', isolate: false, fileParallelism: true },
{ pool: 'threads', isolate: false, maxWorkers: 1 },
{ pool: 'forks', isolate: true },
{ pool: 'forks', isolate: false },
]
test.each(configs)('should isolate environments when %s', async (config) => {
+1 -5
View File
@@ -8,11 +8,7 @@ test.runIf(nodeMajor >= 22)('import node:sqlite', async () => {
'vitest.config.ts': {
test: {
pool: 'forks',
poolOptions: {
forks: {
execArgv: ['--experimental-sqlite'],
},
},
execArgv: ['--experimental-sqlite'],
},
},
'basic.test.ts': ts`
-112
View File
@@ -16,118 +16,6 @@ async function config(cliOptions: TestUserConfig, configValue: TestUserConfig =
return v.config
}
describe('correctly defines isolated flags', async () => {
it('does not merge user-defined poolOptions with itself', async () => {
const c = await config({}, {
poolOptions: {
array: [1, 2, 3],
},
})
// Ensure poolOptions.array has not been merged with itself
// Previously, this would have been [1,2,3,1,2,3]
expect(c.poolOptions?.array).toMatchInlineSnapshot(`
[
1,
2,
3,
]
`)
})
it('prefers CLI poolOptions flags over config', async () => {
const c = await config({
isolate: true,
poolOptions: {
threads: {
isolate: false,
},
forks: {
isolate: false,
},
},
})
expect(c.poolOptions?.threads?.isolate).toBe(false)
expect(c.poolOptions?.forks?.isolate).toBe(false)
expect(c.isolate).toBe(true)
})
it('override CLI poolOptions flags over isolate', async () => {
const c = await config({
isolate: false,
poolOptions: {
threads: {
isolate: true,
},
forks: {
isolate: true,
},
},
}, {
poolOptions: {
threads: {
isolate: false,
},
forks: {
isolate: false,
},
},
})
expect(c.poolOptions?.threads?.isolate).toBe(true)
expect(c.poolOptions?.forks?.isolate).toBe(true)
expect(c.isolate).toBe(false)
})
it('override CLI isolate flag if poolOptions is not set via CLI', async () => {
const c = await config({
isolate: true,
}, {
poolOptions: {
threads: {
isolate: false,
},
forks: {
isolate: false,
},
},
})
expect(c.poolOptions?.threads?.isolate).toBe(true)
expect(c.poolOptions?.forks?.isolate).toBe(true)
expect(c.isolate).toBe(true)
})
it('keeps user configured poolOptions if no CLI flag is provided', async () => {
const c = await config({}, {
poolOptions: {
threads: {
isolate: false,
},
forks: {
isolate: false,
},
},
})
expect(c.poolOptions?.threads?.isolate).toBe(false)
expect(c.poolOptions?.forks?.isolate).toBe(false)
expect(c.isolate).toBe(true)
})
it('isolate config value overrides poolOptions defaults', async () => {
const c = await config({}, {
isolate: false,
})
expect(c.poolOptions?.threads?.isolate).toBe(false)
expect(c.poolOptions?.forks?.isolate).toBe(false)
expect(c.isolate).toBe(false)
})
it('if no isolation is defined in the config, fallback ot undefined', async () => {
const c = await config({}, {})
expect(c.poolOptions?.threads?.isolate).toBe(undefined)
expect(c.poolOptions?.forks?.isolate).toBe(undefined)
// set in configDefaults, so it's always defined
expect(c.isolate).toBe(true)
})
})
describe('correctly defines api flag', () => {
it('CLI overrides disabling api', async () => {
const c = await vitest({ api: false }, {
-39
View File
@@ -1,39 +0,0 @@
import { describe, expect, test } from 'vitest'
import { runVitest } from '../../test-utils'
describe.each(['forks', 'threads'] as const)('%s', async (pool) => {
test('is isolated', async () => {
const { stderr, exitCode } = await runVitest({
root: './fixtures/pool-isolation',
include: ['isolated.test.ts'],
pool,
poolOptions: {
// Use default value on the tested pool and disable on the other one
[invertPool(pool)]: { isolate: false },
},
env: { TESTED_POOL: pool },
})
expect(stderr).toBe('')
expect(exitCode).toBe(0)
})
test('is not isolated', async () => {
const { stderr, exitCode } = await runVitest({
root: './fixtures/pool-isolation',
include: ['non-isolated.test.ts'],
pool,
poolOptions: {
[pool]: { isolate: false },
},
env: { TESTED_POOL: pool },
})
expect(stderr).toBe('')
expect(exitCode).toBe(0)
})
})
function invertPool(pool: 'threads' | 'forks') {
return pool === 'threads' ? 'forks' : 'threads'
}
+56
View File
@@ -0,0 +1,56 @@
import type { SerializedConfig } from 'vitest'
import type { TestUserConfig } from 'vitest/node'
import { assert, describe, expect, test } from 'vitest'
import { runVitest } from '../../test-utils'
describe.each(['forks', 'threads', 'vmThreads', 'vmForks'])('%s', async (pool) => {
test('resolves top-level pool', async () => {
const config = await getConfig({ pool })
expect(config.pool).toBe(pool)
})
})
test('extended project inherits top-level pool related options', async () => {
const config = await getConfig({
projects: [{
extends: true,
test: { name: 'example' },
}],
},
// project.extends works weirdly with runVitest(). Need to pass it here in cli options instead.
{ pool: 'threads', isolate: false })
expect(config.pool).toBe('threads')
expect(config.isolate).toBe(false)
})
test('project level pool options overwrites top-level', async () => {
const config = await getConfig({
pool: 'vmForks',
fileParallelism: true,
projects: [{
extends: true,
test: { pool: 'vmThreads', fileParallelism: false },
}],
})
expect(config.pool).toBe('vmThreads')
expect(config.fileParallelism).toBe(false)
})
async function getConfig(options: Partial<TestUserConfig>, cliOptions: Partial<TestUserConfig> = {}) {
let config: SerializedConfig | undefined
await runVitest({
root: './fixtures/pool',
include: ['print-config.test.ts'],
...cliOptions,
onConsoleLog(log) {
config = JSON.parse(log)
},
}, undefined, undefined, { test: options }, { })
assert(config)
return config
}
+3 -13
View File
@@ -37,22 +37,12 @@ test('workers percent argument should not throw error', async () => {
})
test.each([
{ poolOption: 'threads' },
{ pool: 'threads' },
{ poolOption: 'vmThreads' },
{ poolOption: 'forks' },
{ poolOption: 'vmForks' },
] as const)('workers percent argument in $poolOption should not throw error', async ({ poolOption }) => {
let workerOptions = {}
if (poolOption.toLowerCase().includes('threads')) {
workerOptions = { maxThreads: '100%' }
}
if (poolOption.toLowerCase().includes('forks')) {
workerOptions = { maxForks: '100%' }
}
const { stderr } = await runVitest({ poolOptions: { [poolOption]: workerOptions } })
] as const)('workers percent argument in $poolOption should not throw error', async ({ pool }) => {
const { stderr } = await runVitest({ maxWorkers: '100%', pool })
expect(stderr).toBe('')
})
+1 -8
View File
@@ -7,14 +7,7 @@ export default defineConfig({
reporters: ['verbose'],
testTimeout: 60_000,
pool: 'forks',
poolOptions: {
forks: {
singleFork: true,
},
threads: {
singleThread: true,
},
},
fileParallelism: false,
chaiConfig: {
truncateThreshold: 999,
},
+22
View File
@@ -96,11 +96,17 @@ it('exports snapshot', async ({ skip, task }) => {
"mockObject": "function",
},
"./node": {
"BaseRuntime": "function",
"BaseSequencer": "function",
"ForksRuntime": "function",
"GitNotFoundError": "function",
"TestsNotFoundError": "function",
"ThreadsRuntime": "function",
"TypecheckRuntime": "function",
"VitestPackageInstaller": "function",
"VitestPlugin": "function",
"VmForksRuntime": "function",
"VmThreadsRuntime": "function",
"createDebugger": "function",
"createMethodsRPC": "function",
"createViteLogger": "function",
@@ -163,6 +169,13 @@ it('exports snapshot', async ({ skip, task }) => {
"setFn": "function",
"setHooks": "function",
},
"./workers": {
"collectVitestWorkerTests": "function",
"provideWorkerState": "function",
"runBaseTests": "function",
"runVitestWorker": "function",
"runVmTests": "function",
},
}
`)
}
@@ -249,10 +262,15 @@ it('exports snapshot', async ({ skip, task }) => {
},
"./node": {
"BaseSequencer": "function",
"ForksPoolWorker": "function",
"GitNotFoundError": "function",
"TestsNotFoundError": "function",
"ThreadsPoolWorker": "function",
"TypecheckPoolWorker": "function",
"VitestPackageInstaller": "function",
"VitestPlugin": "function",
"VmForksPoolWorker": "function",
"VmThreadsPoolWorker": "function",
"createDebugger": "function",
"createMethodsRPC": "function",
"createViteLogger": "function",
@@ -315,6 +333,10 @@ it('exports snapshot', async ({ skip, task }) => {
"setFn": "function",
"setHooks": "function",
},
"./worker": {
"init": "function",
"runBaseTests": "function",
},
}
`)
}

Some files were not shown because too many files have changed in this diff Show More