Skip to content

Commit

Permalink
feat: restart vitest on config change
Browse files Browse the repository at this point in the history
  • Loading branch information
antfu committed Aug 16, 2022
1 parent 14279d5 commit c663f39
Show file tree
Hide file tree
Showing 9 changed files with 103 additions and 55 deletions.
2 changes: 2 additions & 0 deletions packages/vitest/src/constants.ts
Expand Up @@ -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 = [
Expand Down
11 changes: 8 additions & 3 deletions 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'
Expand Down Expand Up @@ -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 {
Expand Down
90 changes: 59 additions & 31 deletions packages/vitest/src/node/cli-wrapper.ts
Expand Up @@ -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',
Expand All @@ -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)

Expand All @@ -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[] = []

Expand All @@ -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'
}
}

Expand Down
35 changes: 23 additions & 12 deletions 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'
Expand Down Expand Up @@ -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?.()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -523,7 +534,7 @@ export class Vitest {
return code.includes('import.meta.vitest')
}

onServerRestarted(fn: () => void) {
onServerRestart(fn: OnServerRestartHandler) {
this._onRestartListeners.push(fn)
}
}
5 changes: 0 additions & 5 deletions packages/vitest/src/node/plugins/index.ts
Expand Up @@ -11,8 +11,6 @@ import { CSSEnablerPlugin } from './cssEnabler'
import { CoverageTransform } from './coverageTransform'

export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest()): Promise<VitePlugin[]> {
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)
Expand Down Expand Up @@ -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)
}
Expand Down
8 changes: 6 additions & 2 deletions packages/vitest/src/node/reporters/base.ts
Expand Up @@ -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[]) {
Expand Down
2 changes: 2 additions & 0 deletions packages/vitest/src/types/general.ts
Expand Up @@ -79,3 +79,5 @@ export interface ModuleGraphData {
externalized: string[]
inlined: string[]
}

export type OnServerRestartHandler = (reason?: string) => Promise<void> | void
2 changes: 1 addition & 1 deletion packages/vitest/src/types/reporter.ts
Expand Up @@ -13,7 +13,7 @@ export interface Reporter {
onWatcherStart?: () => Awaitable<void>
onWatcherRerun?: (files: string[], trigger?: string) => Awaitable<void>

onServerRestart?: () => Awaitable<void>
onServerRestart?: (reason?: string) => Awaitable<void>

onUserConsoleLog?: (log: UserConsoleLog) => Awaitable<void>
}
Expand Down
3 changes: 2 additions & 1 deletion packages/vitest/src/utils/index.ts
Expand Up @@ -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'
Expand Down Expand Up @@ -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
}

Expand Down

0 comments on commit c663f39

Please sign in to comment.