Skip to content

Commit

Permalink
refactor: make a standard API to run tests inside a worker (#4441)
Browse files Browse the repository at this point in the history
  • Loading branch information
sheremet-va committed Jan 12, 2024
1 parent 8c969de commit 3ca3174
Show file tree
Hide file tree
Showing 46 changed files with 836 additions and 550 deletions.
69 changes: 62 additions & 7 deletions docs/config/index.md
Expand Up @@ -256,7 +256,7 @@ Should Vitest process assets (.png, .svg, .jpg, etc) files and resolve them like
This module will have a default export equal to the path to the asset, if no query is specified.

::: warning
At the moment, this option only works with [`vmThreads`](#vmthreads) pool.
At the moment, this option only works with [`vmThreads`](#vmthreads) and [`vmForks`](#vmForks) pools.
:::

#### deps.web.transformCss
Expand All @@ -269,7 +269,7 @@ Should Vitest process CSS (.css, .scss, .sass, etc) files and resolve them like
If CSS files are disabled with [`css`](#css) options, this option will just silence `ERR_UNKNOWN_FILE_EXTENSION` errors.

::: warning
At the moment, this option only works with [`vmThreads`](#vmthreads) pool.
At the moment, this option only works with [`vmThreads`](#vmthreads) and [`vmForks`](#vmForks) pools.
:::

#### deps.web.transformGlobPattern
Expand All @@ -282,7 +282,7 @@ Regexp pattern to match external files that should be transformed.
By default, files inside `node_modules` are externalized and not transformed, unless it's CSS or an asset, and corresponding option is not disabled.

::: warning
At the moment, this option only works with [`vmThreads`](#vmthreads) pool.
At the moment, this option only works with [`vmThreads`](#vmthreads) and [`vmForks`](#vmForks) pools.
:::

#### deps.interopDefault
Expand Down Expand Up @@ -545,7 +545,7 @@ export default defineConfig({

### poolMatchGlobs <Badge type="info">0.29.4+</Badge>

- **Type:** `[string, 'threads' | 'forks' | 'vmThreads' | 'typescript'][]`
- **Type:** `[string, 'threads' | 'forks' | 'vmThreads' | 'vmForks' | 'typescript'][]`
- **Default:** `[]`

Automatically assign pool in which tests will run based on globs. The first match will be used.
Expand Down Expand Up @@ -610,7 +610,7 @@ By providing an object instead of a string you can define individual outputs whe

### pool<NonProjectOption /> <Badge type="info">1.0.0+</Badge>

- **Type:** `'threads' | 'forks' | 'vmThreads'`
- **Type:** `'threads' | 'forks' | 'vmThreads' | 'vmForks'`
- **Default:** `'threads'`
- **CLI:** `--pool=threads`

Expand Down Expand Up @@ -650,9 +650,13 @@ 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 />

Similar as `vmThreads` 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 `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 /> <Badge type="info">1.0.0+</Badge>

- **Type:** `Record<'threads' | 'forks' | 'vmThreads', {}>`
- **Type:** `Record<'threads' | 'forks' | 'vmThreads' | 'vmForks', {}>`
- **Default:** `{}`

#### poolOptions.threads
Expand Down Expand Up @@ -873,6 +877,57 @@ Pass additional arguments to `node` process in the VM context. See [Command-line
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`
- **Default:** _available CPUs_

Maximum number of threads. You can also use `VITEST_MAX_FORKS` environment variable.

##### poolOptions.vmForks.minForks<NonProjectOption />

- **Type:** `number`
- **Default:** _available CPUs_

Minimum number of threads. You can also use `VITEST_MIN_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 <Badge type="info">1.1.0+</Badge>

- **Type:** `boolean`
Expand Down Expand Up @@ -2064,7 +2119,7 @@ Path to a [workspace](/guide/workspace) config file relative to [root](#root).
- **Default:** `true`
- **CLI:** `--no-isolate`, `--isolate=false`

Run tests in an isolated environment. This option has no effect on `vmThreads` pool.
Run tests in an isolated environment. This option has no effect on `vmThreads` and `vmForks` pools.

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).

Expand Down
4 changes: 4 additions & 0 deletions packages/vitest/package.json
Expand Up @@ -52,6 +52,10 @@
"types": "./dist/execute.d.ts",
"default": "./dist/execute.js"
},
"./workers": {
"types": "./dist/workers.d.ts",
"import": "./dist/workers.js"
},
"./browser": {
"types": "./dist/browser.d.ts",
"default": "./dist/browser.js"
Expand Down
49 changes: 28 additions & 21 deletions packages/vitest/rollup.config.js
Expand Up @@ -15,27 +15,33 @@ import { defineConfig } from 'rollup'
const require = createRequire(import.meta.url)
const pkg = require('./package.json')

const entries = [
'src/paths.ts',
'src/index.ts',
'src/node/cli.ts',
'src/node/cli-wrapper.ts',
'src/node.ts',
'src/suite.ts',
'src/browser.ts',
'src/runners.ts',
'src/environments.ts',
'src/runtime/worker.ts',
'src/runtime/vm.ts',
'src/runtime/child.ts',
'src/runtime/entry.ts',
'src/runtime/entry-vm.ts',
'src/integrations/spy.ts',
'src/coverage.ts',
'src/public/utils.ts',
'src/public/execute.ts',
'src/public/reporters.ts',
]
const entries = {
'path': 'src/paths.ts',
'index': 'src/index.ts',
'cli': 'src/node/cli.ts',
'cli-wrapper': 'src/node/cli-wrapper.ts',
'node': 'src/node.ts',
'suite': 'src/suite.ts',
'browser': 'src/browser.ts',
'runners': 'src/runners.ts',
'environments': 'src/environments.ts',
'spy': 'src/integrations/spy.ts',
'coverage': 'src/coverage.ts',
'utils': 'src/public/utils.ts',
'execute': 'src/public/execute.ts',
'reporters': 'src/public/reporters.ts',
// TODO: advanced docs
'workers': 'src/workers.ts',

// for performance reasons we bundle them separately so we don't import everything at once
'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',
}

const dtsEntries = {
index: 'src/index.ts',
Expand All @@ -49,6 +55,7 @@ const dtsEntries = {
utils: 'src/public/utils.ts',
execute: 'src/public/execute.ts',
reporters: 'src/public/reporters.ts',
workers: 'src/workers.ts',
}

const external = [
Expand Down
3 changes: 2 additions & 1 deletion packages/vitest/src/integrations/env/happy-dom.ts
Expand Up @@ -16,7 +16,7 @@ export default <Environment>({
transformMode: 'web',
async setupVM({ happyDOM = {} }) {
const { Window } = await import('happy-dom')
const win = new Window({
let win = new Window({
...happyDOM,
console: (console && globalThis.console) ? globalThis.console : undefined,
url: happyDOM.url || 'http://localhost:3000',
Expand All @@ -39,6 +39,7 @@ export default <Environment>({
},
async teardown() {
await teardownWindow(win)
win = undefined
},
}
},
Expand Down
3 changes: 2 additions & 1 deletion packages/vitest/src/integrations/env/jsdom.ts
Expand Up @@ -48,7 +48,7 @@ export default <Environment>({
cookieJar = false,
...restOptions
} = jsdom as any
const dom = new JSDOM(
let dom = new JSDOM(
html,
{
pretendToBeVisual,
Expand Down Expand Up @@ -97,6 +97,7 @@ export default <Environment>({
teardown() {
clearWindowErrors()
dom.window.close()
dom = undefined as any
},
}
},
Expand Down
13 changes: 9 additions & 4 deletions packages/vitest/src/integrations/env/loader.ts
Expand Up @@ -2,7 +2,7 @@ import { normalize, resolve } from 'pathe'
import { ViteNodeRunner } from 'vite-node/client'
import type { ViteNodeRunnerOptions } from 'vite-node'
import type { BuiltinEnvironment, VitestEnvironment } from '../../types/config'
import type { Environment } from '../../types'
import type { ContextRPC, Environment, WorkerRPC } from '../../types'
import { environments } from './index'

function isBuiltinEnvironment(env: VitestEnvironment): env is BuiltinEnvironment {
Expand All @@ -20,14 +20,19 @@ export async function createEnvironmentLoader(options: ViteNodeRunnerOptions) {
return _loaders.get(options.root)!
}

export async function loadEnvironment(name: VitestEnvironment, options: ViteNodeRunnerOptions): Promise<Environment> {
export async function loadEnvironment(ctx: ContextRPC, rpc: WorkerRPC): Promise<Environment> {
const name = ctx.environment.name
if (isBuiltinEnvironment(name))
return environments[name]
const loader = await createEnvironmentLoader(options)
const loader = await createEnvironmentLoader({
root: ctx.config.root,
fetchModule: id => rpc.fetch(id, 'ssr'),
resolveId: (id, importer) => rpc.resolveId(id, importer, 'ssr'),
})
const root = loader.root
const packageId = name[0] === '.' || name[0] === '/'
? resolve(root, name)
: (await options.resolveId!(`vitest-environment-${name}`))?.id ?? resolve(root, name)
: (await rpc.resolveId(`vitest-environment-${name}`, undefined, 'ssr'))?.id ?? resolve(root, name)
const pkg = await loader.executeId(normalize(packageId))
if (!pkg || !pkg.default || typeof pkg.default !== 'object') {
throw new TypeError(
Expand Down
7 changes: 4 additions & 3 deletions packages/vitest/src/integrations/env/node.ts
Expand Up @@ -36,8 +36,8 @@ export default <Environment>({
// this is largely copied from jest's node environment
async setupVM() {
const vm = await import('node:vm')
const context = vm.createContext()
const global = vm.runInContext(
let context = vm.createContext()
let global = vm.runInContext(
'this',
context,
)
Expand Down Expand Up @@ -108,7 +108,8 @@ export default <Environment>({
return context
},
teardown() {
//
context = undefined as any
global = undefined
},
}
},
Expand Down
4 changes: 2 additions & 2 deletions packages/vitest/src/integrations/mock/timers.ts
Expand Up @@ -13,6 +13,7 @@ import type {
import {
withGlobal,
} from '@sinonjs/fake-timers'
import { isChildProcess } from '../../utils/base'
import { RealDate, mockDate, resetDate } from './date'

export class FakeTimers {
Expand Down Expand Up @@ -134,8 +135,7 @@ export class FakeTimers {
// Do not mock nextTick by default. It can still be mocked through userConfig.
.filter(timer => timer !== 'nextTick') as (keyof FakeTimerWithContext['timers'])[]

// @ts-expect-error -- untyped internal
if (this._userConfig?.toFake?.includes('nextTick') && globalThis.__vitest_worker__.isChildProcess)
if (this._userConfig?.toFake?.includes('nextTick') && isChildProcess())
throw new Error('process.nextTick cannot be mocked inside child_process')

this._clock = this._fakeTimers.install({
Expand Down
3 changes: 2 additions & 1 deletion packages/vitest/src/integrations/vi.ts
Expand Up @@ -6,6 +6,7 @@ import type { ResolvedConfig, RuntimeConfig } from '../types'
import type { MockFactoryWithHelper } from '../types/mocker'
import { getWorkerState } from '../utils/global'
import { resetModules, waitForImportsToResolve } from '../utils/modules'
import { isChildProcess } from '../utils/base'
import { FakeTimers } from './mock/timers'
import type { MaybeMocked, MaybeMockedDeep, MaybePartiallyMocked, MaybePartiallyMockedDeep, MockInstance } from './spy'
import { fn, isMockFunction, mocks, spyOn } from './spy'
Expand Down Expand Up @@ -370,7 +371,7 @@ function createVitest(): VitestUtils {

const utils: VitestUtils = {
useFakeTimers(config?: FakeTimerInstallOpts) {
if (workerState.isChildProcess) {
if (isChildProcess()) {
if (config?.toFake?.includes('nextTick') || workerState.config?.fakeTimers?.toFake?.includes('nextTick')) {
throw new Error(
'vi.useFakeTimers({ toFake: ["nextTick"] }) is not supported in node:child_process. Use --pool=threads if mocking nextTick is required.',
Expand Down
8 changes: 8 additions & 0 deletions packages/vitest/src/node/config.ts
Expand Up @@ -274,6 +274,10 @@ export function resolveConfig(
...resolved.poolOptions?.forks,
maxForks: Number.parseInt(process.env.VITEST_MAX_FORKS),
},
vmForks: {
...resolved.poolOptions?.vmForks,
maxForks: Number.parseInt(process.env.VITEST_MAX_FORKS),
},
}
}

Expand All @@ -284,6 +288,10 @@ export function resolveConfig(
...resolved.poolOptions?.forks,
minForks: Number.parseInt(process.env.VITEST_MIN_FORKS),
},
vmForks: {
...resolved.poolOptions?.vmForks,
minForks: Number.parseInt(process.env.VITEST_MIN_FORKS),
},
}
}

Expand Down
12 changes: 2 additions & 10 deletions packages/vitest/src/node/core.ts
Expand Up @@ -59,11 +59,7 @@ export class Vitest {
public projects: WorkspaceProject[] = []
private projectsTestFiles = new Map<string, Set<WorkspaceProject>>()

projectFiles!: {
workerPath: string
forksPath: string
vmPath: string
}
public distPath!: string

constructor(
public readonly mode: VitestRunMode,
Expand Down Expand Up @@ -102,11 +98,7 @@ export class Vitest {
// if Vitest is running globally, then we should still import local vitest if possible
const projectVitestPath = await this.vitenode.resolveId('vitest')
const vitestDir = projectVitestPath ? resolve(projectVitestPath.id, '../..') : rootDir
this.projectFiles = {
workerPath: join(vitestDir, 'dist/worker.js'),
forksPath: join(vitestDir, 'dist/child.js'),
vmPath: join(vitestDir, 'dist/vm.js'),
}
this.distPath = join(vitestDir, 'dist')

const node = this.vitenode
this.runner = new ViteNodeRunner({
Expand Down
10 changes: 8 additions & 2 deletions packages/vitest/src/node/error.ts
Expand Up @@ -58,8 +58,14 @@ export async function printError(error: unknown, project: WorkspaceProject | und

const nearest = error instanceof TypeCheckError
? error.stacks[0]
: stacks.find(stack =>
project.server && project.getModuleById(stack.file) && existsSync(stack.file),
: stacks.find((stack) => {
try {
return project.server && project.getModuleById(stack.file) && existsSync(stack.file)
}
catch {
return false
}
},
)

const errorProperties = getErrorProperties(e)
Expand Down

0 comments on commit 3ca3174

Please sign in to comment.