From a3fd5f853964337325f8842254738a0ff929b952 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Sat, 18 Nov 2023 10:42:07 +0100 Subject: [PATCH] feat: allow custom pools (#4417) --- docs/.vitepress/config.ts | 6 +- docs/advanced/pool.md | 90 +++++++++++++++++++ docs/advanced/reporters.md | 6 +- packages/runner/src/suite.ts | 1 + packages/runner/src/types/tasks.ts | 2 +- packages/vitest/src/node/config.ts | 20 +++++ packages/vitest/src/node/core.ts | 10 ++- packages/vitest/src/node/index.ts | 3 +- packages/vitest/src/node/pool.ts | 65 +++++++++++--- packages/vitest/src/node/pools/browser.ts | 3 +- packages/vitest/src/node/pools/child.ts | 14 +-- packages/vitest/src/node/pools/rpc.ts | 10 +-- packages/vitest/src/node/pools/threads.ts | 14 +-- packages/vitest/src/node/pools/typecheck.ts | 1 + packages/vitest/src/node/pools/vm-threads.ts | 3 +- packages/vitest/src/node/state.ts | 3 +- packages/vitest/src/types/pool-options.ts | 5 +- packages/vitest/src/types/rpc.ts | 4 +- pnpm-lock.yaml | 3 + test/reporters/src/data-for-junit.ts | 2 + test/reporters/src/data.ts | 3 + .../tests/__snapshots__/html.test.ts.snap | 3 + test/reporters/tests/junit.test.ts | 1 + test/run/package.json | 1 + .../pool-custom-fixtures/pool/custom-pool.ts | 52 +++++++++++ .../tests/custom-not-run.spec.ts | 1 + .../tests/custom-run.threads.spec.ts | 5 ++ .../run/pool-custom-fixtures/vitest.config.ts | 16 ++++ test/run/test/custom-pool.test.ts | 18 ++++ 29 files changed, 306 insertions(+), 59 deletions(-) create mode 100644 docs/advanced/pool.md create mode 100644 test/run/pool-custom-fixtures/pool/custom-pool.ts create mode 100644 test/run/pool-custom-fixtures/tests/custom-not-run.spec.ts create mode 100644 test/run/pool-custom-fixtures/tests/custom-run.threads.spec.ts create mode 100644 test/run/pool-custom-fixtures/vitest.config.ts create mode 100644 test/run/test/custom-pool.test.ts diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 80548bdb87ce..54b3c0610646 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -131,9 +131,13 @@ export default withPwa(defineConfig({ link: '/advanced/metadata', }, { - text: 'Extending default reporters', + text: 'Extending Reporters', link: '/advanced/reporters', }, + { + text: 'Custom Pool', + link: '/advanced/pool', + }, ], }, ], diff --git a/docs/advanced/pool.md b/docs/advanced/pool.md new file mode 100644 index 000000000000..a1f4f06c38d6 --- /dev/null +++ b/docs/advanced/pool.md @@ -0,0 +1,90 @@ +# Custom Pool + +::: warning +This is advanced API. If you are just running tests, you probably don't need this. It is primarily used by library authors. +::: + +Vitest runs tests in pools. By default, there are several pools: + +- `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) +- `vmThreads` to run tests using `node:worker_threads` (but isolation is provided with `vm` module instead of a new worker context) +- `browser` to run tests using browser providers +- `typescript` to run typechecking on tests + +You can provide your own pool by specifying a file path: + +```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, + }, + }, + // you can also specify pool for a subset of files + poolMatchGlobs: [ + ['**/*.custom.test.ts', './my-custom-pool.ts'], + ], + }, +}) +``` + +## 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: + +```ts +import { ProcessPool, WorkspaceProject } from 'vitest/node' + +export interface ProcessPool { + name: string + runTests: (files: [project: WorkspaceProject, testFile: string][], invalidates?: string[]) => Promise + close?: () => Promise +} +``` + +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. + +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 tuples: the first element is a reference to a workspace project and the second one is an absolute path to a test file. 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 [`vitest.workspace.ts`](/guide/workspace) configuration. + +Vitest will wait until `runTests` is executed before finishing a run (i.e., it will emit [`onFinished`](/guide/reporters) only after `runTests` is resolved). + +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/feat/custom-pool/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. + +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: + +```ts +import { createBirpc } from 'birpc' +import { parse, stringify } from 'flatted' +import { WorkspaceProject, createMethodsRPC } from 'vitest/node' + +function createRpc(project: WorkspaceProject, wss: WebSocketServer) { + return createBirpc( + createMethodsRPC(project), + { + post: msg => wss.send(msg), + on: fn => wss.on('message', fn), + serialize: stringify, + deserialize: parse, + }, + ) +} +``` + +To make sure every test is collected, you would call `ctx.state.collectFiles` and report it to Vitest reporters: + +```ts +async function runTests(project: WorkspaceProject, tests: string[]) { + // ... running tests, put into "files" and "tasks" + const methods = createMethodsRPC(project) + await methods.onCollected(files) + // most reporters rely on results being updated in "onTaskUpdate" + await methods.onTaskUpdate(tasks) +} +``` + +You can see a simple example in [pool/custom-pool.ts](https://github.com/vitest-dev/vitest/blob/feat/custom-pool/test/run/pool-custom-fixtures/pool/custom-pool.ts). diff --git a/docs/advanced/reporters.md b/docs/advanced/reporters.md index f78d2926ff53..208ca767d369 100644 --- a/docs/advanced/reporters.md +++ b/docs/advanced/reporters.md @@ -1,8 +1,8 @@ -# Extending default reporters +# Extending Reporters You can import reporters from `vitest/reporters` and extend them to create your custom reporters. -## Extending built-in reporters +## Extending Built-in Reporters In general, you don't need to create your reporter from scratch. `vitest` comes with several default reporting programs that you can extend. @@ -56,7 +56,7 @@ export default defineConfig({ }) ``` -## Exported reporters +## Exported Reporters `vitest` comes with a few [built-in reporters](/guide/reporters) that you can use out of the box. diff --git a/packages/runner/src/suite.ts b/packages/runner/src/suite.ts index 830c5784a995..7dbcf9d4e026 100644 --- a/packages/runner/src/suite.ts +++ b/packages/runner/src/suite.ts @@ -154,6 +154,7 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m shuffle, tasks: [], meta: Object.create(null), + projectName: '', } setHooks(suite, createSuiteHooks()) diff --git a/packages/runner/src/types/tasks.ts b/packages/runner/src/types/tasks.ts index 61d1d9def64f..dd44ce5571c2 100644 --- a/packages/runner/src/types/tasks.ts +++ b/packages/runner/src/types/tasks.ts @@ -52,7 +52,7 @@ export interface Suite extends TaskBase { type: 'suite' tasks: Task[] filepath?: string - projectName?: string + projectName: string } export interface File extends Suite { diff --git a/packages/vitest/src/node/config.ts b/packages/vitest/src/node/config.ts index ce0f47d79076..b1cc28fc3770 100644 --- a/packages/vitest/src/node/config.ts +++ b/packages/vitest/src/node/config.ts @@ -6,10 +6,12 @@ import type { ApiConfig, ResolvedConfig, UserConfig, VitestRunMode } from '../ty import { defaultBrowserPort, defaultPort } from '../constants' import { benchmarkConfigDefaults, configDefaults } from '../defaults' import { isCI, stdProvider, toArray } from '../utils' +import type { BuiltinPool } from '../types/pool-options' import { VitestCache } from './cache' import { BaseSequencer } from './sequencers/BaseSequencer' import { RandomSequencer } from './sequencers/RandomSequencer' import type { BenchmarkBuiltinReporters } from './reporters' +import { builtinPools } from './pool' const extraInlineDeps = [ /^(?!.*(?:node_modules)).*\.mjs$/, @@ -222,6 +224,8 @@ export function resolveConfig( if (options.resolveSnapshotPath) delete (resolved as UserConfig).resolveSnapshotPath + resolved.pool ??= 'threads' + if (process.env.VITEST_MAX_THREADS) { resolved.poolOptions = { ...resolved.poolOptions, @@ -270,6 +274,22 @@ export function resolveConfig( } } + if (!builtinPools.includes(resolved.pool as BuiltinPool)) { + resolved.pool = normalize( + resolveModule(resolved.pool, { paths: [resolved.root] }) + ?? resolve(resolved.root, resolved.pool), + ) + } + resolved.poolMatchGlobs = (resolved.poolMatchGlobs || []).map(([glob, pool]) => { + if (!builtinPools.includes(pool as BuiltinPool)) { + pool = normalize( + resolveModule(pool, { paths: [resolved.root] }) + ?? resolve(resolved.root, pool), + ) + } + return [glob, pool] + }) + if (mode === 'benchmark') { resolved.benchmark = { ...benchmarkConfigDefaults, diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index b661511cb37a..8b605142885e 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -80,7 +80,7 @@ export class Vitest { this.unregisterWatcher?.() clearTimeout(this._rerunTimer) this.restartsCount += 1 - this.pool?.close() + this.pool?.close?.() this.pool = undefined this.coverageProvider = undefined this.runningPromise = undefined @@ -761,8 +761,12 @@ export class Vitest { if (!this.projects.includes(this.coreWorkspaceProject)) closePromises.push(this.coreWorkspaceProject.close().then(() => this.server = undefined as any)) - if (this.pool) - closePromises.push(this.pool.close().then(() => this.pool = undefined)) + if (this.pool) { + closePromises.push((async () => { + await this.pool?.close?.() + this.pool = undefined + })()) + } closePromises.push(...this._onClose.map(fn => fn())) diff --git a/packages/vitest/src/node/index.ts b/packages/vitest/src/node/index.ts index 7d69d38c278f..70ebb835f4f9 100644 --- a/packages/vitest/src/node/index.ts +++ b/packages/vitest/src/node/index.ts @@ -4,8 +4,9 @@ export { createVitest } from './create' export { VitestPlugin } from './plugins' export { startVitest } from './cli-api' export { registerConsoleShortcuts } from './stdin' -export type { WorkspaceSpec } from './pool' export type { GlobalSetupContext } from './globalSetup' +export type { WorkspaceSpec, ProcessPool } from './pool' +export { createMethodsRPC } from './pools/rpc' export type { TestSequencer, TestSequencerConstructor } from './sequencers/types' export { BaseSequencer } from './sequencers/BaseSequencer' diff --git a/packages/vitest/src/node/pool.ts b/packages/vitest/src/node/pool.ts index f44c250709e3..2041e2d86acb 100644 --- a/packages/vitest/src/node/pool.ts +++ b/packages/vitest/src/node/pool.ts @@ -1,5 +1,6 @@ import mm from 'micromatch' -import type { Pool } from '../types' +import type { Awaitable } from '@vitest/utils' +import type { BuiltinPool, Pool } from '../types/pool-options' import type { Vitest } from './core' import { createChildProcessPool } from './pools/child' import { createThreadsPool } from './pools/threads' @@ -9,11 +10,12 @@ import type { WorkspaceProject } from './workspace' import { createTypecheckPool } from './pools/typecheck' export type WorkspaceSpec = [project: WorkspaceProject, testFile: string] -export type RunWithFiles = (files: WorkspaceSpec[], invalidates?: string[]) => Promise +export type RunWithFiles = (files: WorkspaceSpec[], invalidates?: string[]) => Awaitable export interface ProcessPool { + name: string runTests: RunWithFiles - close: () => Promise + close?: () => Awaitable } export interface PoolProcessOptions { @@ -24,6 +26,8 @@ export interface PoolProcessOptions { env: Record } +export const builtinPools: BuiltinPool[] = ['forks', 'threads', 'browser', 'vmThreads', 'typescript'] + export function createPool(ctx: Vitest): ProcessPool { const pools: Record = { forks: null, @@ -48,7 +52,7 @@ export function createPool(ctx: Vitest): ProcessPool { } function getPoolName([project, file]: WorkspaceSpec) { - for (const [glob, pool] of project.config.poolMatchGlobs || []) { + for (const [glob, pool] of project.config.poolMatchGlobs) { if ((pool as Pool) === 'browser') throw new Error('Since Vitest 0.31.0 "browser" pool is not supported in "poolMatchGlobs". You can create a workspace to run some of your tests in browser in parallel. Read more: https://vitest.dev/guide/workspace') if (mm.isMatch(file, glob, { cwd: project.config.root })) @@ -82,6 +86,22 @@ export function createPool(ctx: Vitest): ProcessPool { }, } + const customPools = new Map() + async function resolveCustomPool(filepath: string) { + if (customPools.has(filepath)) + return customPools.get(filepath)! + const pool = await ctx.runner.executeId(filepath) + if (typeof pool.default !== 'function') + throw new Error(`Custom pool "${filepath}" must export a function as default export`) + const poolInstance = await pool.default(ctx, options) + if (typeof poolInstance?.name !== 'string') + throw new Error(`Custom pool "${filepath}" should return an object with "name" property`) + if (typeof poolInstance?.runTests !== 'function') + throw new Error(`Custom pool "${filepath}" should return an object with "runTests" method`) + customPools.set(filepath, poolInstance) + return poolInstance as ProcessPool + } + const filesByPool: Record = { forks: [], threads: [], @@ -92,46 +112,63 @@ export function createPool(ctx: Vitest): ProcessPool { for (const spec of files) { const pool = getPoolName(spec) - if (!(pool in filesByPool)) - throw new Error(`Unknown pool name "${pool}" for ${spec[1]}. Available pools: ${Object.keys(filesByPool).join(', ')}`) + filesByPool[pool] ??= [] filesByPool[pool].push(spec) } - await Promise.all(Object.entries(filesByPool).map((entry) => { + const Sequencer = ctx.config.sequence.sequencer + const sequencer = new Sequencer(ctx) + + async function sortSpecs(specs: WorkspaceSpec[]) { + if (ctx.config.shard) + specs = await sequencer.shard(specs) + return sequencer.sort(specs) + } + + await Promise.all(Object.entries(filesByPool).map(async (entry) => { const [pool, files] = entry as [Pool, WorkspaceSpec[]] if (!files.length) return null + const specs = await sortSpecs(files) + if (pool === 'browser') { pools.browser ??= createBrowserPool(ctx) - return pools.browser.runTests(files, invalidate) + return pools.browser.runTests(specs, invalidate) } if (pool === 'vmThreads') { pools.vmThreads ??= createVmThreadsPool(ctx, options) - return pools.vmThreads.runTests(files, invalidate) + return pools.vmThreads.runTests(specs, invalidate) } if (pool === 'threads') { pools.threads ??= createThreadsPool(ctx, options) - return pools.threads.runTests(files, invalidate) + return pools.threads.runTests(specs, invalidate) } if (pool === 'typescript') { pools.typescript ??= createTypecheckPool(ctx) - return pools.typescript.runTests(files) + return pools.typescript.runTests(specs) + } + + if (pool === 'forks') { + pools.forks ??= createChildProcessPool(ctx, options) + return pools.forks.runTests(specs, invalidate) } - pools.forks ??= createChildProcessPool(ctx, options) - return pools.forks.runTests(files, invalidate) + const poolHandler = await resolveCustomPool(pool) + pools[poolHandler.name] ??= poolHandler + return poolHandler.runTests(specs, invalidate) })) } return { + name: 'default', runTests, async close() { - await Promise.all(Object.values(pools).map(p => p?.close())) + await Promise.all(Object.values(pools).map(p => p?.close?.())) }, } } diff --git a/packages/vitest/src/node/pools/browser.ts b/packages/vitest/src/node/pools/browser.ts index 20f282d0ba59..e26fe75b3f7b 100644 --- a/packages/vitest/src/node/pools/browser.ts +++ b/packages/vitest/src/node/pools/browser.ts @@ -44,7 +44,7 @@ export function createBrowserPool(ctx: Vitest): ProcessPool { if (project.config.browser.isolate) { for (const path of paths) { if (isCancelled) { - ctx.state.cancelFiles(files.slice(paths.indexOf(path)), ctx.config.root) + ctx.state.cancelFiles(files.slice(paths.indexOf(path)), ctx.config.root, project.getName()) break } @@ -77,6 +77,7 @@ export function createBrowserPool(ctx: Vitest): ProcessPool { } return { + name: 'browser', async close() { ctx.state.browserTestPromises.clear() await Promise.all([...providers].map(provider => provider.close())) diff --git a/packages/vitest/src/node/pools/child.ts b/packages/vitest/src/node/pools/child.ts index 67efd9cd9a52..4891c0447550 100644 --- a/packages/vitest/src/node/pools/child.ts +++ b/packages/vitest/src/node/pools/child.ts @@ -116,7 +116,7 @@ export function createChildProcessPool(ctx: Vitest, { execArgv, env, forksPath } // Intentionally cancelled else if (ctx.isCancelling && error instanceof Error && /The task has been cancelled/.test(error.message)) - ctx.state.cancelFiles(files, ctx.config.root) + ctx.state.cancelFiles(files, ctx.config.root, project.getName()) else throw error @@ -126,9 +126,6 @@ export function createChildProcessPool(ctx: Vitest, { execArgv, env, forksPath } } } - const Sequencer = ctx.config.sequence.sequencer - const sequencer = new Sequencer(ctx) - return async (specs, invalidates) => { // Cancel pending tasks from pool when possible ctx.onCancel(() => pool.cancelPendingTasks()) @@ -159,14 +156,6 @@ export function createChildProcessPool(ctx: Vitest, { execArgv, env, forksPath } workspaceMap.set(file, workspaceFiles) } - // it's possible that project defines a file that is also defined by another project - const { shard } = ctx.config - - if (shard) - specs = await sequencer.shard(specs) - - specs = await sequencer.sort(specs) - const singleFork = specs.filter(([project]) => project.config.poolOptions?.forks?.singleFork) const multipleForks = specs.filter(([project]) => !project.config.poolOptions?.forks?.singleFork) @@ -228,6 +217,7 @@ export function createChildProcessPool(ctx: Vitest, { execArgv, env, forksPath } } return { + name: 'forks', runTests: runWithFiles('run'), close: async () => { await pool.destroy() diff --git a/packages/vitest/src/node/pools/rpc.ts b/packages/vitest/src/node/pools/rpc.ts index fca27d780c75..e291c63bf2e8 100644 --- a/packages/vitest/src/node/pools/rpc.ts +++ b/packages/vitest/src/node/pools/rpc.ts @@ -35,28 +35,28 @@ export function createMethodsRPC(project: WorkspaceProject): RuntimeRPC { }, onPathsCollected(paths) { ctx.state.collectPaths(paths) - project.report('onPathsCollected', paths) + return project.report('onPathsCollected', paths) }, onCollected(files) { ctx.state.collectFiles(files) - project.report('onCollected', files) + return project.report('onCollected', files) }, onAfterSuiteRun(meta) { ctx.coverageProvider?.onAfterSuiteRun(meta) }, onTaskUpdate(packs) { ctx.state.updateTasks(packs) - project.report('onTaskUpdate', packs) + return project.report('onTaskUpdate', packs) }, onUserConsoleLog(log) { ctx.state.updateUserLog(log) - project.report('onUserConsoleLog', log) + return project.report('onUserConsoleLog', log) }, onUnhandledError(err, type) { ctx.state.catchError(err, type) }, onFinished(files) { - project.report('onFinished', files, ctx.state.getUnhandledErrors()) + return project.report('onFinished', files, ctx.state.getUnhandledErrors()) }, onCancel(reason) { ctx.cancelCurrentRun(reason) diff --git a/packages/vitest/src/node/pools/threads.ts b/packages/vitest/src/node/pools/threads.ts index 38588fd17090..4d78c458a546 100644 --- a/packages/vitest/src/node/pools/threads.ts +++ b/packages/vitest/src/node/pools/threads.ts @@ -104,7 +104,7 @@ export function createThreadsPool(ctx: Vitest, { execArgv, env, workerPath }: Po // Intentionally cancelled else if (ctx.isCancelling && error instanceof Error && /The task has been cancelled/.test(error.message)) - ctx.state.cancelFiles(files, ctx.config.root) + ctx.state.cancelFiles(files, ctx.config.root, project.getName()) else throw error @@ -115,9 +115,6 @@ export function createThreadsPool(ctx: Vitest, { execArgv, env, workerPath }: Po } } - const Sequencer = ctx.config.sequence.sequencer - const sequencer = new Sequencer(ctx) - return async (specs, invalidates) => { // Cancel pending tasks from pool when possible ctx.onCancel(() => pool.cancelPendingTasks()) @@ -139,14 +136,6 @@ export function createThreadsPool(ctx: Vitest, { execArgv, env, workerPath }: Po workspaceMap.set(file, workspaceFiles) } - // it's possible that project defines a file that is also defined by another project - const { shard } = ctx.config - - if (shard) - specs = await sequencer.shard(specs) - - specs = await sequencer.sort(specs) - const singleThreads = specs.filter(([project]) => project.config.poolOptions?.threads?.singleThread) const multipleThreads = specs.filter(([project]) => !project.config.poolOptions?.threads?.singleThread) @@ -208,6 +197,7 @@ export function createThreadsPool(ctx: Vitest, { execArgv, env, workerPath }: Po } return { + name: 'threads', runTests: runWithFiles('run'), close: async () => { // node before 16.17 has a bug that causes FATAL ERROR because of the race condition diff --git a/packages/vitest/src/node/pools/typecheck.ts b/packages/vitest/src/node/pools/typecheck.ts index a794d604fdd9..281f2a71d5f6 100644 --- a/packages/vitest/src/node/pools/typecheck.ts +++ b/packages/vitest/src/node/pools/typecheck.ts @@ -114,6 +114,7 @@ export function createTypecheckPool(ctx: Vitest): ProcessPool { } return { + name: 'typescript', runTests, async close() { const promises = ctx.projects.map(project => project.typechecker?.stop()) diff --git a/packages/vitest/src/node/pools/vm-threads.ts b/packages/vitest/src/node/pools/vm-threads.ts index 3f2d659dfa4b..c3c5f6223a44 100644 --- a/packages/vitest/src/node/pools/vm-threads.ts +++ b/packages/vitest/src/node/pools/vm-threads.ts @@ -109,7 +109,7 @@ export function createVmThreadsPool(ctx: Vitest, { execArgv, env, vmPath }: Pool // Intentionally cancelled else if (ctx.isCancelling && error instanceof Error && /The task has been cancelled/.test(error.message)) - ctx.state.cancelFiles(files, ctx.config.root) + ctx.state.cancelFiles(files, ctx.config.root, project.getName()) else throw error @@ -153,6 +153,7 @@ export function createVmThreadsPool(ctx: Vitest, { execArgv, env, vmPath }: Pool } return { + name: 'vmThreads', runTests: runWithFiles('run'), close: async () => { // node before 16.17 has a bug that causes FATAL ERROR because of the race condition diff --git a/packages/vitest/src/node/state.ts b/packages/vitest/src/node/state.ts index 2b849be8aef5..3da0eed04782 100644 --- a/packages/vitest/src/node/state.ts +++ b/packages/vitest/src/node/state.ts @@ -150,7 +150,7 @@ export class StateManager { return Array.from(this.idMap.values()).filter(t => t.result?.state === 'fail').length } - cancelFiles(files: string[], root: string) { + cancelFiles(files: string[], root: string, projectName: string) { this.collectFiles(files.map(filepath => ({ filepath, name: relative(root, filepath), @@ -163,6 +163,7 @@ export class StateManager { meta: {}, // Cancelled files have not yet collected tests tasks: [], + projectName, }))) } } diff --git a/packages/vitest/src/types/pool-options.ts b/packages/vitest/src/types/pool-options.ts index da4c91c6a478..49b6471bbd47 100644 --- a/packages/vitest/src/types/pool-options.ts +++ b/packages/vitest/src/types/pool-options.ts @@ -1,6 +1,7 @@ -export type Pool = 'browser' | 'threads' | 'forks' | 'vmThreads' | 'typescript' // | 'vmForks' +export type BuiltinPool = 'browser' | 'threads' | 'forks' | 'vmThreads' | 'typescript' // | 'vmForks' +export type Pool = BuiltinPool | (string & {}) -export interface PoolOptions { +export interface PoolOptions extends Record { /** * Run tests in `node:worker_threads`. * diff --git a/packages/vitest/src/types/rpc.ts b/packages/vitest/src/types/rpc.ts index d050f3928363..9e72918d95af 100644 --- a/packages/vitest/src/types/rpc.ts +++ b/packages/vitest/src/types/rpc.ts @@ -19,9 +19,9 @@ export interface RuntimeRPC { onPathsCollected: (paths: string[]) => void onUserConsoleLog: (log: UserConsoleLog) => void onUnhandledError: (err: unknown, type: string) => void - onCollected: (files: File[]) => void + onCollected: (files: File[]) => Promise onAfterSuiteRun: (meta: AfterSuiteRunMeta) => void - onTaskUpdate: (pack: TaskResultPack[]) => void + onTaskUpdate: (pack: TaskResultPack[]) => Promise onCancel: (reason: CancelReason) => void getCountOfFailedTests: () => number diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dc8a08d79e7b..236668b65d1e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1800,6 +1800,9 @@ importers: test/run: devDependencies: + '@vitest/runner': + specifier: workspace:* + version: link:../../packages/runner execa: specifier: ^7.1.1 version: 7.1.1 diff --git a/test/reporters/src/data-for-junit.ts b/test/reporters/src/data-for-junit.ts index 26a36f49591f..8d2e8ecc057a 100644 --- a/test/reporters/src/data-for-junit.ts +++ b/test/reporters/src/data-for-junit.ts @@ -11,6 +11,7 @@ function createSuiteHavingFailedTestWithXmlInError(): File[] { filepath: '/vitest/test/core/test/basic.test.ts', result: { state: 'fail', duration: 145.99284195899963 }, tasks: [], + projectName: '', } const suite: Suite = { @@ -22,6 +23,7 @@ function createSuiteHavingFailedTestWithXmlInError(): File[] { file, result: { state: 'pass', duration: 1.90183687210083 }, tasks: [], + projectName: '', } const errorWithXml = new AssertionError({ diff --git a/test/reporters/src/data.ts b/test/reporters/src/data.ts index eff8c586e489..3e26931a9a78 100644 --- a/test/reporters/src/data.ts +++ b/test/reporters/src/data.ts @@ -10,6 +10,7 @@ const file: File = { filepath: '/vitest/test/core/test/basic.test.ts', result: { state: 'fail', duration: 145.99284195899963 }, tasks: [], + projectName: '', } const suite: Suite = { @@ -21,6 +22,7 @@ const suite: Suite = { file, result: { state: 'pass', duration: 1.90183687210083 }, tasks: [], + projectName: '', } const innerSuite: Suite = { @@ -33,6 +35,7 @@ const innerSuite: Suite = { suite, result: { state: 'pass', duration: 1.90183687210083 }, tasks: [], + projectName: '', } const error: ErrorWithDiff = new AssertionError({ diff --git a/test/reporters/tests/__snapshots__/html.test.ts.snap b/test/reporters/tests/__snapshots__/html.test.ts.snap index a702e4507282..780d73733359 100644 --- a/test/reporters/tests/__snapshots__/html.test.ts.snap +++ b/test/reporters/tests/__snapshots__/html.test.ts.snap @@ -78,6 +78,7 @@ exports[`html reporter > resolves to "failing" status for test file "json-fail" "meta": {}, "mode": "run", "name": "", + "projectName": "", "tasks": [ [Circular], ], @@ -153,6 +154,7 @@ exports[`html reporter > resolves to "passing" status for test file "all-passing "meta": {}, "mode": "run", "name": "", + "projectName": "", "tasks": [ [Circular], { @@ -181,6 +183,7 @@ exports[`html reporter > resolves to "passing" status for test file "all-passing "meta": {}, "mode": "run", "name": "", + "projectName": "", "tasks": [ { "file": [Circular], diff --git a/test/reporters/tests/junit.test.ts b/test/reporters/tests/junit.test.ts index 99d1b39395d5..f869ccd206e7 100644 --- a/test/reporters/tests/junit.test.ts +++ b/test/reporters/tests/junit.test.ts @@ -15,6 +15,7 @@ test('calc the duration used by junit', () => { mode: 'run', tasks: [], meta: {}, + projectName: '', } const task: Task = { id: '1', diff --git a/test/run/package.json b/test/run/package.json index 2f1a3352f2f5..d3599cd88044 100644 --- a/test/run/package.json +++ b/test/run/package.json @@ -6,6 +6,7 @@ "test": "vitest" }, "devDependencies": { + "@vitest/runner": "workspace:*", "execa": "^7.1.1", "vite": "latest", "vitest": "workspace:*" diff --git a/test/run/pool-custom-fixtures/pool/custom-pool.ts b/test/run/pool-custom-fixtures/pool/custom-pool.ts new file mode 100644 index 000000000000..31889be91456 --- /dev/null +++ b/test/run/pool-custom-fixtures/pool/custom-pool.ts @@ -0,0 +1,52 @@ +import type { File, Test } from 'vitest' +import type { ProcessPool, Vitest } from 'vitest/node' +import { createMethodsRPC } from 'vitest/node' +import { getTasks } from '@vitest/runner/utils' +import { normalize, relative } from 'pathe' + +export default (ctx: Vitest): ProcessPool => { + const options = ctx.config.poolOptions?.custom as any + return { + name: 'custom', + async runTests(specs) { + console.warn('[pool] printing:', options.print) + for await (const [project, file] of specs) { + ctx.state.clearFiles(project) + const methods = createMethodsRPC(project) + console.warn('[pool] running tests for', project.getName(), 'in', normalize(file).toLowerCase().replace(normalize(process.cwd()).toLowerCase(), '')) + const path = relative(project.config.root, file) + const taskFile: File = { + id: `${path}${project.getName()}`, + name: path, + mode: 'run', + meta: {}, + projectName: project.getName(), + filepath: file, + type: 'suite', + tasks: [], + result: { + state: 'pass', + }, + } + const taskTest: Test = { + type: 'test', + name: 'custom test', + id: 'custom-test', + context: {} as any, + suite: taskFile, + mode: 'run', + meta: {}, + result: { + state: 'pass', + }, + } + taskFile.tasks.push(taskTest) + await methods.onCollected([taskFile]) + await methods.onTaskUpdate(getTasks(taskFile).map(task => [task.id, task.result, task.meta])) + } + }, + close() { + console.warn('[pool] custom pool is closed!') + }, + } +} diff --git a/test/run/pool-custom-fixtures/tests/custom-not-run.spec.ts b/test/run/pool-custom-fixtures/tests/custom-not-run.spec.ts new file mode 100644 index 000000000000..f476cf8f0411 --- /dev/null +++ b/test/run/pool-custom-fixtures/tests/custom-not-run.spec.ts @@ -0,0 +1 @@ +throw new Error('this file is not actually running') diff --git a/test/run/pool-custom-fixtures/tests/custom-run.threads.spec.ts b/test/run/pool-custom-fixtures/tests/custom-run.threads.spec.ts new file mode 100644 index 000000000000..65a6c158212c --- /dev/null +++ b/test/run/pool-custom-fixtures/tests/custom-run.threads.spec.ts @@ -0,0 +1,5 @@ +import { expect, test } from 'vitest' + +test('correctly runs threads test while there is a custom pool', () => { + expect(1 + 1).toBe(2) +}) diff --git a/test/run/pool-custom-fixtures/vitest.config.ts b/test/run/pool-custom-fixtures/vitest.config.ts new file mode 100644 index 000000000000..f8af2635f787 --- /dev/null +++ b/test/run/pool-custom-fixtures/vitest.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + name: 'custom-pool-test', + pool: './pool/custom-pool.ts', + poolOptions: { + custom: { + print: 'options are respected', + }, + }, + poolMatchGlobs: [ + ['**/*.threads.spec.ts', 'threads'], + ], + }, +}) diff --git a/test/run/test/custom-pool.test.ts b/test/run/test/custom-pool.test.ts new file mode 100644 index 000000000000..e62b12294008 --- /dev/null +++ b/test/run/test/custom-pool.test.ts @@ -0,0 +1,18 @@ +import { expect, test } from 'vitest' +import { runVitestCli } from '../../test-utils' + +test('can run custom pools with Vitest', async () => { + const vitest = await runVitestCli('--run', '--root', 'pool-custom-fixtures') + + expect(vitest.stderr).toMatchInlineSnapshot(` + "[pool] printing: options are respected + [pool] running tests for custom-pool-test in /pool-custom-fixtures/tests/custom-not-run.spec.ts + [pool] custom pool is closed! + " + `) + + expect(vitest.stdout).toContain('✓ |custom-pool-test| tests/custom-not-run.spec.ts') + expect(vitest.stdout).toContain('✓ |custom-pool-test| tests/custom-run.threads.spec.ts') + expect(vitest.stdout).toContain('Test Files 2 passed') + expect(vitest.stdout).toContain('Tests 2 passed') +})