diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 57e8b3852476..3ed5e8fbef68 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,8 +81,8 @@ jobs: - name: Test run: pnpm run test:ci - - name: Test Single Thread - run: pnpm run test:ci:single-thread + - name: Test No Threads + run: pnpm run test:ci:no-threads - name: Test Vm Threads run: pnpm run test:ci:vm-threads diff --git a/package.json b/package.json index de7c1751278c..e5d2a0b8fbd5 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "test:all": "CI=true pnpm -r --stream run test --allowOnly", "test:ci": "CI=true pnpm -r --stream --filter !test-fails --filter !test-browser --filter !test-esm --filter !test-browser run test --allowOnly", "test:ci:vm-threads": "CI=true pnpm -r --stream --filter !test-fails --filter !test-single-thread --filter !test-browser --filter !test-esm --filter !test-browser run test --allowOnly --experimental-vm-threads", - "test:ci:single-thread": "CI=true pnpm -r --stream --filter !test-fails --filter !test-coverage --filter !test-watch --filter !test-bail --filter !test-esm --filter !test-browser run test --allowOnly --no-threads", + "test:ci:no-threads": "CI=true pnpm -r --stream --filter !test-fails --filter !test-coverage --filter !test-watch --filter !test-bail --filter !test-esm --filter !test-browser run test --allowOnly --no-threads", "typecheck": "tsc --noEmit", "typecheck:why": "tsc --noEmit --explainFiles > explainTypes.txt", "ui:build": "vite build packages/ui", diff --git a/packages/ui/package.json b/packages/ui/package.json index 2e61ebf09d6d..e86179e7aeab 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -69,7 +69,7 @@ "@vitest/ws-client": "workspace:*", "@vueuse/core": "^10.2.1", "ansi-to-html": "^0.7.2", - "birpc": "0.2.12", + "birpc": "0.2.13", "codemirror": "^5.65.13", "codemirror-theme-vars": "^0.1.2", "cypress": "^12.16.0", diff --git a/packages/vitest/package.json b/packages/vitest/package.json index 073ec4087db3..20531043f404 100644 --- a/packages/vitest/package.json +++ b/packages/vitest/package.json @@ -162,7 +162,7 @@ "std-env": "^3.3.3", "strip-literal": "^1.0.1", "tinybench": "^2.5.0", - "tinypool": "^0.7.0", + "tinypool": "^0.8.0", "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0", "vite-node": "workspace:*", "why-is-node-running": "^2.2.2" @@ -180,7 +180,7 @@ "@types/micromatch": "^4.0.2", "@types/prompts": "^2.4.4", "@types/sinonjs__fake-timers": "^8.1.2", - "birpc": "0.2.12", + "birpc": "0.2.13", "chai-subset": "^1.6.0", "cli-truncate": "^3.1.0", "event-target-polyfill": "^0.0.3", diff --git a/packages/vitest/src/api/setup.ts b/packages/vitest/src/api/setup.ts index df830dda504b..b6837d631435 100644 --- a/packages/vitest/src/api/setup.ts +++ b/packages/vitest/src/api/setup.ts @@ -21,7 +21,7 @@ export function setup(vitestOrWorkspace: Vitest | WorkspaceProject, server?: Vit const wss = new WebSocketServer({ noServer: true }) - const clients = new Map>() + const clients = new Map>() ;(server || ctx.server).httpServer?.on('upgrade', (request, socket, head) => { if (!request.url) @@ -155,7 +155,7 @@ class WebSocketReporter implements Reporter { constructor( public ctx: Vitest, public wss: WebSocketServer, - public clients: Map>, + public clients: Map>, ) {} onCollected(files?: File[]) { diff --git a/packages/vitest/src/node/pools/child.ts b/packages/vitest/src/node/pools/child.ts index a22e91fb3c12..01e49f2b7282 100644 --- a/packages/vitest/src/node/pools/child.ts +++ b/packages/vitest/src/node/pools/child.ts @@ -1,21 +1,32 @@ import v8 from 'node:v8' -import type { ChildProcess } from 'node:child_process' -import { fork } from 'node:child_process' import { fileURLToPath, pathToFileURL } from 'node:url' +import { cpus } from 'node:os' +import EventEmitter from 'node:events' +import { Tinypool } from 'tinypool' +import type { TinypoolChannel, Options as TinypoolOptions } from 'tinypool' import { createBirpc } from 'birpc' import { resolve } from 'pathe' import type { ContextTestEnvironment, ResolvedConfig, RunnerRPC, RuntimeRPC, Vitest } from '../../types' import type { ChildContext } from '../../types/child' -import type { PoolProcessOptions, ProcessPool, WorkspaceSpec } from '../pool' +import type { PoolProcessOptions, ProcessPool, RunWithFiles } from '../pool' import { distDir } from '../../paths' -import { groupBy } from '../../utils/base' -import { envsOrder, groupFilesByEnv } from '../../utils/test-helpers' import type { WorkspaceProject } from '../workspace' +import { envsOrder, groupFilesByEnv } from '../../utils/test-helpers' +import { groupBy } from '../../utils' import { createMethodsRPC } from './rpc' const childPath = fileURLToPath(pathToFileURL(resolve(distDir, './child.js')).href) -function setupChildProcessChannel(project: WorkspaceProject, fork: ChildProcess): void { +function createChildProcessChannel(project: WorkspaceProject) { + const emitter = new EventEmitter() + const cleanup = () => emitter.removeAllListeners() + + const events = { message: 'message', response: 'response' } + const channel: TinypoolChannel = { + onMessage: callback => emitter.on(events.message, callback), + postMessage: message => emitter.emit(events.response, message), + } + const rpc = createBirpc( createMethodsRPC(project), { @@ -23,15 +34,17 @@ function setupChildProcessChannel(project: WorkspaceProject, fork: ChildProcess) serialize: v8.serialize, deserialize: v => v8.deserialize(Buffer.from(v)), post(v) { - fork.send(v) + emitter.emit(events.message, v) }, on(fn) { - fork.on('message', fn) + emitter.on(events.response, fn) }, }, ) project.ctx.onCancel(reason => rpc.onCancel(reason)) + + return { channel, cleanup } } function stringifyRegex(input: RegExp | string): string { @@ -40,101 +53,180 @@ function stringifyRegex(input: RegExp | string): string { return `$$vitest:${input.toString()}` } -function getTestConfig(ctx: WorkspaceProject): ResolvedConfig { - const config = ctx.getSerializableConfig() - // v8 serialize does not support regex - return { - ...config, - testNamePattern: config.testNamePattern - ? stringifyRegex(config.testNamePattern) - : undefined, +export function createChildProcessPool(ctx: Vitest, { execArgv, env }: PoolProcessOptions): ProcessPool { + const threadsCount = ctx.config.watch + ? Math.max(Math.floor(cpus().length / 2), 1) + : Math.max(cpus().length - 1, 1) + + const maxThreads = ctx.config.maxThreads ?? threadsCount + const minThreads = ctx.config.minThreads ?? threadsCount + + const options: TinypoolOptions = { + runtime: 'child_process', + filename: childPath, + + maxThreads, + minThreads, + + env, + execArgv, + + terminateTimeout: ctx.config.teardownTimeout, } -} -export function createChildProcessPool(ctx: Vitest, { execArgv, env }: PoolProcessOptions): ProcessPool { - const children = new Set() + if (ctx.config.isolate) { + options.isolateWorkers = true + options.concurrentTasksPerWorker = 1 + } - const Sequencer = ctx.config.sequence.sequencer - const sequencer = new Sequencer(ctx) + if (ctx.config.singleThread) { + options.concurrentTasksPerWorker = 1 + options.maxThreads = 1 + options.minThreads = 1 + } - function runFiles(project: WorkspaceProject, files: string[], environment: ContextTestEnvironment, invalidates: string[] = []) { - const config = getTestConfig(project) - ctx.state.clearFiles(project, files) + const pool = new Tinypool(options) + + const runWithFiles = (name: string): RunWithFiles => { + let id = 0 + + async function runFiles(project: WorkspaceProject, config: ResolvedConfig, files: string[], environment: ContextTestEnvironment, invalidates: string[] = []) { + ctx.state.clearFiles(project, files) + const { channel, cleanup } = createChildProcessChannel(project) + const workerId = ++id + const data: ChildContext = { + config, + files, + invalidates, + environment, + workerId, + } + 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)) + ctx.state.addProcessTimeoutCause(`Failed to terminate worker while running ${files.join(', ')}.`) - const data: ChildContext = { - command: 'start', - config, - files, - invalidates, - environment, - } + // Intentionally cancelled + else if (ctx.isCancelling && error instanceof Error && /The task has been cancelled/.test(error.message)) + ctx.state.cancelFiles(files, ctx.config.root) - const child = fork(childPath, [], { - execArgv, - env, - // TODO: investigate - // serialization: 'advanced', - }) - children.add(child) - setupChildProcessChannel(project, child) - - return new Promise((resolve, reject) => { - child.send(data, (err) => { - if (err) - reject(err) - }) - child.on('close', (code) => { - if (!code) - resolve() else - reject(new Error(`Child process exited unexpectedly with code ${code}`)) + throw error + } + finally { + cleanup() + } + } - children.delete(child) - }) - }) - } + 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()) - async function runTests(specs: WorkspaceSpec[], invalidates: string[] = []): Promise { - const { shard } = ctx.config + const configs = new Map() + const getConfig = (project: WorkspaceProject): ResolvedConfig => { + if (configs.has(project)) + return configs.get(project)! - if (shard) - specs = await sequencer.shard(specs) + const _config = project.getSerializableConfig() + + const config = { + ..._config, + // v8 serialize does not support regex + testNamePattern: _config.testNamePattern + ? stringifyRegex(_config.testNamePattern) + : undefined, + } as ResolvedConfig + + configs.set(project, config) + return config + } + + const workspaceMap = new Map() + for (const [project, file] of specs) { + const workspaceFiles = workspaceMap.get(file) ?? [] + workspaceFiles.push(project) + workspaceMap.set(file, workspaceFiles) + } - specs = await sequencer.sort(specs) + // 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) + + // TODO: What to do about singleThread flag? + const singleThreads = specs.filter(([project]) => project.config.singleThread) + const multipleThreads = specs.filter(([project]) => !project.config.singleThread) + + if (multipleThreads.length) { + const filesByEnv = await groupFilesByEnv(multipleThreads) + const files = Object.values(filesByEnv).flat() + const results: PromiseSettledResult[] = [] + + if (ctx.config.isolate) { + 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.getName() + 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(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.') + } - const filesByEnv = await groupFilesByEnv(specs) - const envs = envsOrder.concat( - Object.keys(filesByEnv).filter(env => !envsOrder.includes(env)), - ) + if (singleThreads.length) { + const filesByEnv = await groupFilesByEnv(singleThreads) + const envs = envsOrder.concat( + Object.keys(filesByEnv).filter(env => !envsOrder.includes(env)), + ) - // always run environments isolated between each other - for (const env of envs) { - const files = filesByEnv[env] + for (const env of envs) { + const files = filesByEnv[env] - if (!files?.length) - continue + if (!files?.length) + continue - const filesByOptions = groupBy(files, ({ project, environment }) => project.getName() + JSON.stringify(environment.options) + environment.transformMode) + const filesByOptions = groupBy(files, ({ project, environment }) => project.getName() + JSON.stringify(environment.options)) - for (const option in filesByOptions) { - const files = filesByOptions[option] + for (const files of Object.values(filesByOptions)) { + // Always run environments isolated between each other + await pool.recycleWorkers() - if (files?.length) { - const filenames = files.map(f => f.file) - await runFiles(files[0].project, filenames, files[0].environment, invalidates) + const filenames = files.map(f => f.file) + await runFiles(files[0].project, getConfig(files[0].project), filenames, files[0].environment, invalidates) + } } } } } return { - runTests, - async close() { - children.forEach((child) => { - if (!child.killed) - child.kill() - }) - children.clear() + 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 6ca1c736c1f9..78b31bdc579a 100644 --- a/packages/vitest/src/node/pools/rpc.ts +++ b/packages/vitest/src/node/pools/rpc.ts @@ -46,7 +46,7 @@ export function createMethodsRPC(project: WorkspaceProject): RuntimeRPC { }, onTaskUpdate(packs) { ctx.state.updateTasks(packs) - project.report('onTaskUpdate', packs) + return project.report('onTaskUpdate', packs) }, onUserConsoleLog(log) { ctx.state.updateUserLog(log) diff --git a/packages/vitest/src/runtime/child.ts b/packages/vitest/src/runtime/child.ts index 5bdf6cd984ad..a75124083395 100644 --- a/packages/vitest/src/runtime/child.ts +++ b/packages/vitest/src/runtime/child.ts @@ -2,6 +2,8 @@ import { performance } from 'node:perf_hooks' import v8 from 'node:v8' import { createBirpc } from 'birpc' import { parseRegexp } from '@vitest/utils' +import { workerId as poolId } from 'tinypool' +import type { TinypoolWorkerMessage } from 'tinypool' import type { CancelReason } from '@vitest/runner' import type { ResolvedConfig, WorkerGlobalState } from '../types' import type { RunnerRPC, RuntimeRPC } from '../types/rpc' @@ -12,10 +14,10 @@ import { createSafeRpc, rpcDone } from './rpc' import { setupInspect } from './inspector' async function init(ctx: ChildContext) { - const { config } = ctx + const { config, workerId } = ctx - process.env.VITEST_WORKER_ID = '1' - process.env.VITEST_POOL_ID = '1' + process.env.VITEST_WORKER_ID = String(workerId) + process.env.VITEST_POOL_ID = String(poolId) let setCancel = (_reason: CancelReason) => {} const onCancel = new Promise((resolve) => { @@ -33,7 +35,15 @@ async function init(ctx: ChildContext) { post(v) { process.send?.(v) }, - on(fn) { process.on('message', fn) }, + on(fn) { + process.on('message', (message: any, ...extras: any) => { + // Do not react on Tinypool's internal messaging + if ((message as TinypoolWorkerMessage)?.__tinypool_worker_message__) + return + + return fn(message, ...extras) + }) + }, }, ) @@ -83,6 +93,7 @@ function unwrapConfig(config: ResolvedConfig) { } export async function run(ctx: ChildContext) { + ctx.config = unwrapConfig(ctx.config) const inspectorCleanup = setupInspect(ctx.config) try { @@ -97,17 +108,3 @@ export async function run(ctx: ChildContext) { inspectorCleanup() } } - -const procesExit = process.exit - -process.on('message', async (message: any) => { - if (typeof message === 'object' && message.command === 'start') { - try { - message.config = unwrapConfig(message.config) - await run(message) - } - finally { - procesExit() - } - } -}) diff --git a/packages/vitest/src/runtime/rpc.ts b/packages/vitest/src/runtime/rpc.ts index 0357c059f5a1..e2ee890beebe 100644 --- a/packages/vitest/src/runtime/rpc.ts +++ b/packages/vitest/src/runtime/rpc.ts @@ -3,7 +3,7 @@ import { } from '@vitest/utils' import type { BirpcReturn } from 'birpc' import { getWorkerState } from '../utils/global' -import type { RuntimeRPC } from '../types/rpc' +import type { RunnerRPC, RuntimeRPC } from '../types/rpc' import type { WorkerRPC } from '../types' const { get } = Reflect @@ -73,7 +73,7 @@ export function createSafeRpc(rpc: WorkerRPC) { }) } -export function rpc(): BirpcReturn { +export function rpc(): BirpcReturn { const { rpc } = getWorkerState() return rpc } diff --git a/packages/vitest/src/runtime/vm.ts b/packages/vitest/src/runtime/vm.ts index e9a12e015ba8..9dcf24f1b857 100644 --- a/packages/vitest/src/runtime/vm.ts +++ b/packages/vitest/src/runtime/vm.ts @@ -7,7 +7,7 @@ import { createBirpc } from 'birpc' import { resolve } from 'pathe' import { installSourcemapsSupport } from 'vite-node/source-map' import type { CancelReason } from '@vitest/runner' -import type { RuntimeRPC, WorkerContext, WorkerGlobalState } from '../types' +import type { RunnerRPC, RuntimeRPC, WorkerContext, WorkerGlobalState } from '../types' import { distDir } from '../paths' import { loadEnvironment } from '../integrations/env' import { startVitestExecutor } from './execute' @@ -26,7 +26,7 @@ export async function run(ctx: WorkerContext) { setCancel = resolve }) - const rpc = createBirpc( + const rpc = createBirpc( { onCancel: setCancel, }, diff --git a/packages/vitest/src/types/child.ts b/packages/vitest/src/types/child.ts index 023e1dc77a96..722912fce5da 100644 --- a/packages/vitest/src/types/child.ts +++ b/packages/vitest/src/types/child.ts @@ -1,5 +1,5 @@ import type { ContextRPC } from './rpc' export interface ChildContext extends ContextRPC { - command: 'start' + workerId: number } diff --git a/packages/vitest/src/types/worker.ts b/packages/vitest/src/types/worker.ts index 7d3c4cde3a6f..1cf6a708542e 100644 --- a/packages/vitest/src/types/worker.ts +++ b/packages/vitest/src/types/worker.ts @@ -4,7 +4,7 @@ import type { ModuleCacheMap, ViteNodeResolveId } from 'vite-node' import type { BirpcReturn } from 'birpc' import type { MockMap } from './mocker' import type { ResolvedConfig } from './config' -import type { ContextRPC, RuntimeRPC } from './rpc' +import type { ContextRPC, RunnerRPC, RuntimeRPC } from './rpc' import type { Environment } from './general' export interface WorkerContext extends ContextRPC { @@ -18,7 +18,7 @@ export interface AfterSuiteRunMeta { coverage?: unknown } -export type WorkerRPC = BirpcReturn +export type WorkerRPC = BirpcReturn export interface WorkerGlobalState { ctx: ContextRPC diff --git a/packages/ws-client/package.json b/packages/ws-client/package.json index 6d42cd09c3c6..4af308f41817 100644 --- a/packages/ws-client/package.json +++ b/packages/ws-client/package.json @@ -39,7 +39,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "birpc": "0.2.12", + "birpc": "0.2.13", "flatted": "^3.2.7", "ws": "^8.13.0" }, diff --git a/packages/ws-client/src/index.ts b/packages/ws-client/src/index.ts index d34af68200d5..7a94d4be87b2 100644 --- a/packages/ws-client/src/index.ts +++ b/packages/ws-client/src/index.ts @@ -22,7 +22,7 @@ export interface VitestClientOptions { export interface VitestClient { ws: WebSocket state: StateManager - rpc: BirpcReturn + rpc: BirpcReturn waitForConnection(): Promise reconnect(): Promise } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 784c1879f653..c80f0ca5251b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1196,8 +1196,8 @@ importers: specifier: ^0.7.2 version: 0.7.2 birpc: - specifier: 0.2.12 - version: 0.2.12 + specifier: 0.2.13 + version: 0.2.13 codemirror: specifier: ^5.65.13 version: 5.65.13 @@ -1354,8 +1354,8 @@ importers: specifier: ^2.5.0 version: 2.5.0 tinypool: - specifier: ^0.7.0 - version: 0.7.0 + specifier: ^0.8.0 + version: 0.8.0 vite: specifier: ^4.3.9 version: 4.3.9(@types/node@18.7.13) @@ -1403,8 +1403,8 @@ importers: specifier: ^8.1.2 version: 8.1.2 birpc: - specifier: 0.2.12 - version: 0.2.12 + specifier: 0.2.13 + version: 0.2.13 chai-subset: specifier: ^1.6.0 version: 1.6.0 @@ -1497,8 +1497,8 @@ importers: packages/ws-client: dependencies: birpc: - specifier: 0.2.12 - version: 0.2.12 + specifier: 0.2.13 + version: 0.2.13 flatted: specifier: ^3.2.7 version: 3.2.7 @@ -11627,8 +11627,8 @@ packages: dev: true optional: true - /birpc@0.2.12: - resolution: {integrity: sha512-6Wz9FXuJ/FE4gDH+IGQhrYdalAvAQU1Yrtcu1UlMk3+9mMXxIRXiL+MxUcGokso42s+Fy+YoUXGLOdOs0siV3A==} + /birpc@0.2.13: + resolution: {integrity: sha512-30rz9OBSJoGfiWox7dpyqoSVo6664PBEYSTfmmG1GBridUxnMysyovNpnwhaPMvjtKn3Y1UfII+HMTU0kqJFjA==} /bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -24006,8 +24006,8 @@ packages: resolution: {integrity: sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==} dev: false - /tinypool@0.7.0: - resolution: {integrity: sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==} + /tinypool@0.8.0: + resolution: {integrity: sha512-BkMhpw8M8y9+XBOEP57Wzbw/8IoJYtL1OvFjX+88IvwzAqVhwEV2TID0lZ1l4b5dPhjzSFQrhWdD2CLWt+oXRw==} engines: {node: '>=14.0.0'} dev: false diff --git a/test/bail/test/bail.test.ts b/test/bail/test/bail.test.ts index ddd8197d9728..5dce88cc6c7f 100644 --- a/test/bail/test/bail.test.ts +++ b/test/bail/test/bail.test.ts @@ -15,7 +15,7 @@ for (const isolate of [true, false]) { for (const config of configs) { test(`should bail with "${JSON.stringify(config)}"`, async () => { - process.env.THREADS = config?.threads ? 'true' : 'false' + process.env.THREADS = config?.singleThread ? 'false' : 'true' const { exitCode, stdout } = await runVitest({ root: './fixtures', diff --git a/test/setup/tests/setup-files.test.ts b/test/setup/tests/setup-files.test.ts index 4146e2b9b44c..d843ea1e65e8 100644 --- a/test/setup/tests/setup-files.test.ts +++ b/test/setup/tests/setup-files.test.ts @@ -19,7 +19,7 @@ describe('setup files with forceRerunTrigger', () => { }) // Note that this test will fail locally if you have uncommitted changes - it('should run no tests if setup file is not changed', async () => { + it.skipIf(!process.env.GITHUB_ACTION)('should run no tests if setup file is not changed', async () => { const { stdout } = await run() expect(stdout).toContain('No test files found, exiting with code 0') }, 60_000) diff --git a/test/test-utils/index.ts b/test/test-utils/index.ts index fb4ffbeea0f6..316525e473d7 100644 --- a/test/test-utils/index.ts +++ b/test/test-utils/index.ts @@ -11,6 +11,7 @@ import { dirname, resolve } from 'pathe' export async function runVitest(config: UserConfig, cliFilters: string[] = [], mode: VitestRunMode = 'test') { // Reset possible previous runs + let exitCode = process.exitCode process.exitCode = 0 // Prevent possible process.exit() calls, e.g. from --browser @@ -31,18 +32,18 @@ export async function runVitest(config: UserConfig, cliFilters: string[] = [], m return { stderr: `${getLogs().stderr}\n${e.message}`, stdout: getLogs().stdout, - exitCode: process.exitCode, + exitCode, vitest, } } finally { + exitCode = process.exitCode + process.exitCode = 0 + process.exit = exit + restore() } - const exitCode = process.exitCode - process.exitCode = 0 - process.exit = exit - return { ...getLogs(), exitCode, vitest } }