mirror of
https://github.com/hanzoai/vitest.git
synced 2026-09-24 17:04:25 +00:00
feat!: rewrite pools without tinypool (#8705)
Co-authored-by: Vladimir Sheremet <[email protected]>
This commit is contained in:
committed by
GitHub
co-authored by
Vladimir Sheremet
parent
1238067a4a
commit
fbddc8024f
+1
-1
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
```
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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]
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,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'
|
||||
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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 {}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export { runBaseTests } from '../runtime/workers/base'
|
||||
export { init } from '../runtime/workers/init'
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 })
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
@@ -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 })
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 ''
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
export * from './dist/worker.js'
|
||||
Generated
-9
@@ -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]: {}
|
||||
|
||||
@@ -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'],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -3,10 +3,6 @@ import { defineConfig } from 'vite'
|
||||
export default defineConfig({
|
||||
test: {
|
||||
pool: 'forks',
|
||||
poolOptions: {
|
||||
forks: {
|
||||
isolate: false,
|
||||
},
|
||||
},
|
||||
isolate: false,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -3,11 +3,7 @@ import { defineConfig } from 'vite'
|
||||
export default defineConfig({
|
||||
test: {
|
||||
pool: 'forks',
|
||||
poolOptions: {
|
||||
forks: {
|
||||
isolate: false,
|
||||
},
|
||||
},
|
||||
isolate: false,
|
||||
expect: {
|
||||
requireAssertions: true,
|
||||
}
|
||||
|
||||
@@ -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.')
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
})
|
||||
})
|
||||
`,
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
@@ -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
|
||||
|
||||
@@ -19,7 +19,6 @@ describe('define project helper', () => {
|
||||
|
||||
test('allows expected project fields on a project config', () => {
|
||||
expectProjectTestConfig.toHaveProperty('pool')
|
||||
expectProjectTestConfig.toHaveProperty('poolOptions')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -23,11 +23,7 @@ test.each([
|
||||
root,
|
||||
include: [fileToTest],
|
||||
pool,
|
||||
poolOptions: {
|
||||
[pool]: {
|
||||
execArgv,
|
||||
},
|
||||
},
|
||||
execArgv: [...execArgv],
|
||||
})
|
||||
|
||||
expect(vitest.stdout).toContain(`✓ ${fileToTest}`)
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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 }, {
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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('')
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user