From c663f39cbd91bb520c8c33e9178b9944e60fa83e Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Tue, 16 Aug 2022 12:00:18 +0800 Subject: [PATCH] feat: restart vitest on config change --- packages/vitest/src/constants.ts | 2 + packages/vitest/src/node/cli-api.ts | 11 ++- packages/vitest/src/node/cli-wrapper.ts | 90 ++++++++++++++-------- packages/vitest/src/node/core.ts | 35 ++++++--- packages/vitest/src/node/plugins/index.ts | 5 -- packages/vitest/src/node/reporters/base.ts | 8 +- packages/vitest/src/types/general.ts | 2 + packages/vitest/src/types/reporter.ts | 2 +- packages/vitest/src/utils/index.ts | 3 +- 9 files changed, 103 insertions(+), 55 deletions(-) diff --git a/packages/vitest/src/constants.ts b/packages/vitest/src/constants.ts index b280c9ca8b8a..18b6f0938b25 100644 --- a/packages/vitest/src/constants.ts +++ b/packages/vitest/src/constants.ts @@ -7,6 +7,8 @@ export const distDir = resolve(url.fileURLToPath(import.meta.url), '../../dist') // if changed, update also jsdocs and docs export const defaultPort = 51204 +export const EXIT_CODE_RESTART = 43 + export const API_PATH = '/__vitest_api__' export const configFiles = [ diff --git a/packages/vitest/src/node/cli-api.ts b/packages/vitest/src/node/cli-api.ts index 6106cdb609f8..1241d0551461 100644 --- a/packages/vitest/src/node/cli-api.ts +++ b/packages/vitest/src/node/cli-api.ts @@ -1,5 +1,6 @@ import { resolve } from 'pathe' import type { UserConfig as ViteUserConfig } from 'vite' +import { EXIT_CODE_RESTART } from '../constants' import { CoverageProviderMap } from '../integrations/coverage' import { envPackageNames } from '../integrations/env' import type { UserConfig } from '../types' @@ -60,9 +61,13 @@ export async function startVitest(cliFilters: string[], options: CliOptions, vit if (process.stdin.isTTY && ctx.config.watch) registerConsoleShortcuts(ctx) - ctx.onServerRestarted(() => { - // TODO: re-consider how to re-run the tests the server smartly - ctx.start(cliFilters) + ctx.onServerRestart((reason) => { + ctx.report('onServerRestart', reason) + + if (process.env.VITEST_CLI_WRAPPER) + process.exit(EXIT_CODE_RESTART) + else + ctx.start(cliFilters) }) try { diff --git a/packages/vitest/src/node/cli-wrapper.ts b/packages/vitest/src/node/cli-wrapper.ts index ebdc6cd0b052..49a528bbce54 100644 --- a/packages/vitest/src/node/cli-wrapper.ts +++ b/packages/vitest/src/node/cli-wrapper.ts @@ -5,17 +5,23 @@ import { fileURLToPath } from 'url' import c from 'picocolors' import { execa } from 'execa' +import { EXIT_CODE_RESTART } from '../constants' const ENTRY = new URL('./cli.mjs', import.meta.url) -const NODE_ARGS = ['--inspect', '--inspect-brk', '--trace-deprecation'] + +/** Arguments passed to Node before the script */ +const NODE_ARGS = [ + '--inspect', + '--inspect-brk', + '--trace-deprecation', +] interface ErrorDef { trigger: string url: string } -// Node errors seen in Vitest (vitejs/vite#9492) -const ERRORS: ErrorDef[] = [ +const SegfaultErrors: ErrorDef[] = [ { trigger: 'Check failed: result.second.', url: 'https://github.com/nodejs/node/issues/43617', @@ -30,9 +36,10 @@ const ERRORS: ErrorDef[] = [ }, ] +main() + async function main() { // default exit code = 100, as in retries were exhausted - const exitCode = 100 let retries = 0 const args = process.argv.slice(2) @@ -52,24 +59,6 @@ async function main() { } } - retries = Math.max(1, retries || 1) - - for (let i = 1; i <= retries; i++) { - if (i !== 1) - console.log(`${c.inverse(c.bold(c.magenta(' Retrying ')))} vitest ${args.join(' ')} ${c.gray(`(${i} of ${retries})`)}`) - await start(args) - if (i === 1 && retries === 1) { - console.log(c.yellow(`It seems to be an upstream bug of Node.js. To improve the test stability, -you could pass ${c.bold(c.green('--segfault-retry=3'))} or set env ${c.bold(c.green('VITEST_SEGFAULT_RETRY=3'))} to -have Vitest auto retries on flaky segfaults.\n`)) - } - } - process.exit(exitCode) -} - -main() - -async function start(args: string[]) { const nodeArgs: string[] = [] const vitestArgs: string[] = [] @@ -87,23 +76,62 @@ async function start(args: string[]) { vitestArgs.push(args[i]) } - const child = execa('node', [...nodeArgs, fileURLToPath(ENTRY), ...vitestArgs], { - reject: false, - stderr: 'pipe', - stdout: 'inherit', - stdin: 'inherit', - }) + retries = Math.max(1, retries || 1) + + for (let i = 1; i <= retries; i++) { + const result = await start(nodeArgs, vitestArgs) + + if (result === 'restart') { + i -= 1 + continue + } + + if (i === 1 && retries === 1) { + console.log(c.yellow(`It seems to be an upstream bug of Node.js. To improve the test stability, +you could pass ${c.bold(c.green('--segfault-retry=3'))} or set env ${c.bold(c.green('VITEST_SEGFAULT_RETRY=3'))} to +have Vitest auto retries on flaky segfaults.\n`)) + } + + if (i !== retries) + console.log(`${c.inverse(c.bold(c.magenta(' Retrying ')))} vitest ${args.join(' ')} ${c.gray(`(${i + 1} of ${retries})`)}`) + } + + // retry out + process.exit(1) +} + +async function start(preArgs: string[], postArgs: string[]) { + const child = execa( + 'node', + [ + ...preArgs, + fileURLToPath(ENTRY), + ...postArgs, + ], + { + reject: false, + stderr: 'pipe', + stdout: 'inherit', + stdin: 'inherit', + env: { + ...process.env, + VITEST_CLI_WRAPPER: 'true', + }, + }, + ) child.stderr?.pipe(process.stderr) const { stderr = '' } = await child - for (const error of ERRORS) { + if (child.exitCode === EXIT_CODE_RESTART) + return 'restart' + + for (const error of SegfaultErrors) { if (stderr.includes(error.trigger)) { if (process.env.GITHUB_ACTIONS) console.log(`::warning:: Segmentfault Error Detected: ${error.trigger}\nRefer to ${error.url}`) - const RED_BLOCK = c.inverse(c.red(' ')) console.log(`\n${c.inverse(c.bold(c.red(' Segmentfault Error Detected ')))}\n${RED_BLOCK} ${c.red(error.trigger)}\n${RED_BLOCK} ${c.red(`Refer to ${error.url}`)}\n`) - return + return 'error' } } diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index ac8aa7da9ad1..3e98a6ab56a2 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -1,12 +1,13 @@ import { existsSync, promises as fs } from 'fs' import type { ViteDevServer } from 'vite' +import { normalizePath } from 'vite' import { relative, toNamespacedPath } from 'pathe' import fg from 'fast-glob' import mm from 'micromatch' import c from 'picocolors' import { ViteNodeRunner } from 'vite-node/client' import { ViteNodeServer } from 'vite-node/server' -import type { ArgumentsType, CoverageProvider, Reporter, ResolvedConfig, UserConfig } from '../types' +import type { ArgumentsType, CoverageProvider, OnServerRestartHandler, Reporter, ResolvedConfig, UserConfig } from '../types' import { SnapshotManager } from '../integrations/snapshot/manager' import { clearTimeout, deepMerge, hasFailed, noop, setTimeout, slash } from '../utils' import { getCoverageProvider } from '../integrations/coverage' @@ -48,7 +49,7 @@ export class Vitest { this.logger = new Logger(this) } - private _onRestartListeners: Array<() => void> = [] + private _onRestartListeners: OnServerRestartHandler[] = [] async setServer(options: UserConfig, server: ViteDevServer) { this.unregisterWatcher?.() @@ -81,12 +82,29 @@ export class Vitest { }, }) + if (this.config.watch) { + // hijack server restart + const serverRestart = server.restart + server.restart = async (...args) => { + await Promise.all(this._onRestartListeners.map(fn => fn())) + return await serverRestart(...args) + } + + // since we set `server.hmr: false`, Vite does not auto restart itself + server.watcher.on('change', async (file) => { + file = normalizePath(file) + const isConfig = file === server.config.configFile + if (isConfig) { + await Promise.all(this._onRestartListeners.map(fn => fn('config'))) + await serverRestart() + } + }) + } + this.reporters = await createReporters(resolved.reporters, this.runner) this.runningPromise = undefined - this._onRestartListeners.forEach(fn => fn()) - await this.coverageProvider?.clean(this.config.coverage.clean) this.cache.results.setConfig(resolved.root, resolved.cache) @@ -331,13 +349,6 @@ export class Vitest { this.isFirstRun = false - // add previously failed files - // if (RERUN_FAILED) { - // ctx.state.getFiles().forEach((file) => { - // if (file.result?.state === 'fail') - // changedTests.add(file.filepath) - // }) - // } this.snapshot.clear() const files = Array.from(this.changedTests) this.changedTests.clear() @@ -523,7 +534,7 @@ export class Vitest { return code.includes('import.meta.vitest') } - onServerRestarted(fn: () => void) { + onServerRestart(fn: OnServerRestartHandler) { this._onRestartListeners.push(fn) } } diff --git a/packages/vitest/src/node/plugins/index.ts b/packages/vitest/src/node/plugins/index.ts index a2a8266151e6..7024634b51dc 100644 --- a/packages/vitest/src/node/plugins/index.ts +++ b/packages/vitest/src/node/plugins/index.ts @@ -11,8 +11,6 @@ import { CSSEnablerPlugin } from './cssEnabler' import { CoverageTransform } from './coverageTransform' export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest()): Promise { - let haveStarted = false - async function UIPlugin() { await ensurePackageInstalled('@vitest/ui', ctx.config?.root || options.root || process.cwd()) return (await import('@vitest/ui')).default(options.uiBase) @@ -145,11 +143,8 @@ export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest()) process.env[name] ??= envs[name] }, async configureServer(server) { - if (haveStarted) - await ctx.report('onServerRestart') try { await ctx.setServer(options, server) - haveStarted = true if (options.api && options.watch) (await import('../../api/setup')).setup(ctx) } diff --git a/packages/vitest/src/node/reporters/base.ts b/packages/vitest/src/node/reporters/base.ts index 2be6815f11a0..f0b761b266e8 100644 --- a/packages/vitest/src/node/reporters/base.ts +++ b/packages/vitest/src/node/reporters/base.ts @@ -180,8 +180,12 @@ export abstract class BaseReporter implements Reporter { return true } - onServerRestart() { - this.ctx.logger.log(c.cyan('Restarted due to config changes...')) + onServerRestart(reason?: string) { + this.ctx.logger.log(c.bold(c.magenta( + reason === 'config' + ? '\nRestarting due to config changes...' + : '\nRestarting Vitest...', + ))) } async reportSummary(files: File[]) { diff --git a/packages/vitest/src/types/general.ts b/packages/vitest/src/types/general.ts index 8b621e0716c7..54f036653da9 100644 --- a/packages/vitest/src/types/general.ts +++ b/packages/vitest/src/types/general.ts @@ -79,3 +79,5 @@ export interface ModuleGraphData { externalized: string[] inlined: string[] } + +export type OnServerRestartHandler = (reason?: string) => Promise | void diff --git a/packages/vitest/src/types/reporter.ts b/packages/vitest/src/types/reporter.ts index 072b6524ef28..f2edcf31f65e 100644 --- a/packages/vitest/src/types/reporter.ts +++ b/packages/vitest/src/types/reporter.ts @@ -13,7 +13,7 @@ export interface Reporter { onWatcherStart?: () => Awaitable onWatcherRerun?: (files: string[], trigger?: string) => Awaitable - onServerRestart?: () => Awaitable + onServerRestart?: (reason?: string) => Awaitable onUserConsoleLog?: (log: UserConsoleLog) => Awaitable } diff --git a/packages/vitest/src/utils/index.ts b/packages/vitest/src/utils/index.ts index 54c235da5eeb..58b2f7651ea9 100644 --- a/packages/vitest/src/utils/index.ts +++ b/packages/vitest/src/utils/index.ts @@ -5,6 +5,7 @@ import { isPackageExists } from 'local-pkg' import { relative as relativeNode } from 'pathe' import type { ModuleCacheMap } from 'vite-node' import type { Suite, Task } from '../types' +import { EXIT_CODE_RESTART } from '../constants' import { getNames } from './tasks' export * from './tasks' @@ -87,7 +88,7 @@ export async function ensurePackageInstalled( await (await import('@antfu/install-pkg')).installPackage(dependency, { dev: true }) // TODO: somehow it fails to load the package after installation, remove this when it's fixed process.stderr.write(c.yellow(`\nPackage ${dependency} installed, re-run the command to start.\n`)) - process.exit(1) + process.exit(EXIT_CODE_RESTART) return true }