Skip to content

Commit b01df47

Browse files
authoredNov 8, 2024··
fix: stop the runner before restarting, restart on workspace config change (#6859)
1 parent 6e793c6 commit b01df47

File tree

8 files changed

+180
-19
lines changed

8 files changed

+180
-19
lines changed
 

‎packages/vitest/src/node/cli/cli-api.ts

-4
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,6 @@ export async function startVitest(
7373
stdinCleanup = registerConsoleShortcuts(ctx, stdin, stdout)
7474
}
7575

76-
ctx.onServerRestart((reason) => {
77-
ctx.report('onServerRestart', reason)
78-
})
79-
8076
ctx.onAfterSetServer(() => {
8177
if (ctx.config.standalone) {
8278
ctx.init()

‎packages/vitest/src/node/core.ts

+17-10
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export class Vitest {
8383
public distPath = distDir
8484

8585
private _cachedSpecs = new Map<string, WorkspaceSpec[]>()
86+
private _workspaceConfigPath?: string
8687

8788
/** @deprecated use `_cachedSpecs` */
8889
projectTestFiles = this._cachedSpecs
@@ -110,6 +111,10 @@ export class Vitest {
110111
this._browserLastPort = defaultBrowserPort
111112
this.pool?.close?.()
112113
this.pool = undefined
114+
this.closingPromise = undefined
115+
this.projects = []
116+
this.resolvedProjects = []
117+
this._workspaceConfigPath = undefined
113118
this.coverageProvider = undefined
114119
this.runningPromise = undefined
115120
this._cachedSpecs.clear()
@@ -145,22 +150,22 @@ export class Vitest {
145150
const serverRestart = server.restart
146151
server.restart = async (...args) => {
147152
await Promise.all(this._onRestartListeners.map(fn => fn()))
153+
this.report('onServerRestart')
154+
await this.close()
148155
await serverRestart(...args)
149-
// watcher is recreated on restart
150-
this.unregisterWatcher()
151-
this.registerWatcher()
152156
}
153157

154158
// since we set `server.hmr: false`, Vite does not auto restart itself
155159
server.watcher.on('change', async (file) => {
156160
file = normalize(file)
157161
const isConfig = file === server.config.configFile
162+
|| this.resolvedProjects.some(p => p.server.config.configFile === file)
163+
|| file === this._workspaceConfigPath
158164
if (isConfig) {
159165
await Promise.all(this._onRestartListeners.map(fn => fn('config')))
166+
this.report('onServerRestart', 'config')
167+
await this.close()
160168
await serverRestart()
161-
// watcher is recreated on restart
162-
this.unregisterWatcher()
163-
this.registerWatcher()
164169
}
165170
})
166171
}
@@ -175,8 +180,6 @@ export class Vitest {
175180
}
176181
catch { }
177182

178-
await Promise.all(this._onSetServer.map(fn => fn()))
179-
180183
const projects = await this.resolveWorkspace(cliOptions)
181184
this.resolvedProjects = projects
182185
this.projects = projects
@@ -193,6 +196,8 @@ export class Vitest {
193196
if (this.config.testNamePattern) {
194197
this.configOverride.testNamePattern = this.config.testNamePattern
195198
}
199+
200+
await Promise.all(this._onSetServer.map(fn => fn()))
196201
}
197202

198203
public provide<T extends keyof ProvidedContext & string>(key: T, value: ProvidedContext[T]) {
@@ -235,7 +240,7 @@ export class Vitest {
235240
|| this.projects[0]
236241
}
237242

238-
private async getWorkspaceConfigPath(): Promise<string | null> {
243+
private async getWorkspaceConfigPath(): Promise<string | undefined> {
239244
if (this.config.workspace) {
240245
return this.config.workspace
241246
}
@@ -251,7 +256,7 @@ export class Vitest {
251256
})
252257

253258
if (!workspaceConfigName) {
254-
return null
259+
return undefined
255260
}
256261

257262
return join(configDir, workspaceConfigName)
@@ -260,6 +265,8 @@ export class Vitest {
260265
private async resolveWorkspace(cliOptions: UserConfig) {
261266
const workspaceConfigPath = await this.getWorkspaceConfigPath()
262267

268+
this._workspaceConfigPath = workspaceConfigPath
269+
263270
if (!workspaceConfigPath) {
264271
return [await this._createCoreProject()]
265272
}

‎packages/vitest/src/node/workspace.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,7 @@ export class WorkspaceProject {
432432
)
433433
}
434434

435+
this.closingPromise = undefined
435436
this.testProject = new TestProject(this)
436437

437438
this.server = server
@@ -476,7 +477,7 @@ export class WorkspaceProject {
476477
if (!this.closingPromise) {
477478
this.closingPromise = Promise.all(
478479
[
479-
this.server.close(),
480+
this.server?.close(),
480481
this.typechecker?.stop(),
481482
this.browser?.close(),
482483
this.clearTmpDir(),

‎test/test-utils/index.ts

+59
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Options } from 'tinyexec'
22
import type { UserConfig as ViteUserConfig } from 'vite'
3+
import type { WorkspaceProjectConfiguration } from 'vitest/config'
34
import type { UserConfig, Vitest, VitestRunMode } from 'vitest/node'
45
import fs from 'node:fs'
56
import { Readable, Writable } from 'node:stream'
@@ -234,3 +235,61 @@ export function resolvePath(baseUrl: string, path: string) {
234235
const filename = fileURLToPath(baseUrl)
235236
return resolve(dirname(filename), path)
236237
}
238+
239+
export function useFS(root: string, structure: Record<string, string | ViteUserConfig | WorkspaceProjectConfiguration[]>) {
240+
const files = new Set<string>()
241+
const hasConfig = Object.keys(structure).some(file => file.includes('.config.'))
242+
if (!hasConfig) {
243+
structure['./vitest.config.js'] = {}
244+
}
245+
for (const file in structure) {
246+
const filepath = resolve(root, file)
247+
files.add(filepath)
248+
const content = typeof structure[file] === 'string'
249+
? structure[file]
250+
: `export default ${JSON.stringify(structure[file])}`
251+
fs.mkdirSync(dirname(filepath), { recursive: true })
252+
fs.writeFileSync(filepath, String(content), 'utf-8')
253+
}
254+
onTestFinished(() => {
255+
if (process.env.VITEST_FS_CLEANUP !== 'false') {
256+
fs.rmSync(root, { recursive: true, force: true })
257+
}
258+
})
259+
return {
260+
editFile: (file: string, callback: (content: string) => string) => {
261+
const filepath = resolve(root, file)
262+
if (!files.has(filepath)) {
263+
throw new Error(`file ${file} is outside of the test file system`)
264+
}
265+
const content = fs.readFileSync(filepath, 'utf-8')
266+
fs.writeFileSync(filepath, callback(content))
267+
},
268+
createFile: (file: string, content: string) => {
269+
if (file.startsWith('..')) {
270+
throw new Error(`file ${file} is outside of the test file system`)
271+
}
272+
const filepath = resolve(root, file)
273+
if (!files.has(filepath)) {
274+
throw new Error(`file ${file} already exists in the test file system`)
275+
}
276+
createFile(filepath, content)
277+
},
278+
}
279+
}
280+
281+
export async function runInlineTests(
282+
structure: Record<string, string | ViteUserConfig | WorkspaceProjectConfiguration[]>,
283+
config?: UserConfig,
284+
) {
285+
const root = resolve(process.cwd(), `vitest-test-${crypto.randomUUID()}`)
286+
const fs = useFS(root, structure)
287+
const vitest = await runVitest({
288+
root,
289+
...config,
290+
})
291+
return {
292+
fs,
293+
...vitest,
294+
}
295+
}
+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { expect, test } from 'vitest'
2+
import { runInlineTests } from '../../test-utils'
3+
4+
const ts = String.raw
5+
6+
test('reruns tests when configs change', async () => {
7+
const { fs, vitest } = await runInlineTests({
8+
'vitest.workspace.ts': [
9+
'./project-1',
10+
'./project-2',
11+
],
12+
'vitest.config.ts': {},
13+
'project-1/vitest.config.ts': {},
14+
'project-1/basic.test.ts': ts`
15+
import { test } from 'vitest'
16+
test('basic test 1', () => {})
17+
`,
18+
'project-2/vitest.config.ts': {},
19+
'project-2/basic.test.ts': ts`
20+
import { test } from 'vitest'
21+
test('basic test 2', () => {})
22+
`,
23+
}, { watch: true })
24+
25+
await vitest.waitForStdout('Waiting for file changes')
26+
vitest.resetOutput()
27+
28+
// editing the project config should trigger a restart
29+
fs.editFile('./project-1/vitest.config.ts', c => `\n${c}`)
30+
31+
await vitest.waitForStdout('Restarting due to config changes...')
32+
await vitest.waitForStdout('Waiting for file changes')
33+
vitest.resetOutput()
34+
35+
// editing the root config should trigger a restart
36+
fs.editFile('./vitest.config.ts', c => `\n${c}`)
37+
38+
await vitest.waitForStdout('Restarting due to config changes...')
39+
await vitest.waitForStdout('Waiting for file changes')
40+
vitest.resetOutput()
41+
42+
// editing the workspace config should trigger a restart
43+
fs.editFile('./vitest.workspace.ts', c => `\n${c}`)
44+
45+
await vitest.waitForStdout('Restarting due to config changes...')
46+
await vitest.waitForStdout('Waiting for file changes')
47+
})
48+
49+
test('rerun stops the previous browser server and restarts multiple times without port mismatch', async () => {
50+
const { fs, vitest } = await runInlineTests({
51+
'vitest.workspace.ts': [
52+
'./project-1',
53+
],
54+
'vitest.config.ts': {},
55+
'project-1/vitest.config.ts': {
56+
test: {
57+
browser: {
58+
enabled: true,
59+
name: 'chromium',
60+
provider: 'playwright',
61+
headless: true,
62+
},
63+
},
64+
},
65+
'project-1/basic.test.ts': ts`
66+
import { test } from 'vitest'
67+
test('basic test 1', () => {})
68+
`,
69+
}, { watch: true })
70+
71+
await vitest.waitForStdout('Waiting for file changes')
72+
vitest.resetOutput()
73+
74+
// editing the project config the first time restarts the browser server
75+
fs.editFile('./project-1/vitest.config.ts', c => `\n${c}`)
76+
77+
await vitest.waitForStdout('Restarting due to config changes...')
78+
await vitest.waitForStdout('Waiting for file changes')
79+
80+
expect(vitest.stdout).not.toContain('is in use, trying another one...')
81+
expect(vitest.stderr).not.toContain('is in use, trying another one...')
82+
vitest.resetOutput()
83+
84+
// editing the project the second time also restarts the server
85+
fs.editFile('./project-1/vitest.config.ts', c => `\n${c}`)
86+
87+
await vitest.waitForStdout('Restarting due to config changes...')
88+
await vitest.waitForStdout('Waiting for file changes')
89+
90+
expect(vitest.stdout).not.toContain('is in use, trying another one...')
91+
expect(vitest.stderr).not.toContain('is in use, trying another one...')
92+
vitest.resetOutput()
93+
})

‎test/watch/vitest.config.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { defineConfig } from 'vitest/config'
22

33
export default defineConfig({
4+
server: {
5+
watch: {
6+
ignored: ['**/fixtures/**'],
7+
},
8+
},
49
test: {
510
reporters: 'verbose',
611
include: ['test/**/*.test.*'],

‎test/workspaces-browser/space_browser/vitest.config.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ export default defineProject({
44
test: {
55
browser: {
66
enabled: true,
7-
name: process.env.BROWSER || 'chrome',
7+
name: process.env.BROWSER || 'chromium',
88
headless: true,
9-
provider: process.env.PROVIDER || 'webdriverio',
9+
provider: process.env.PROVIDER || 'playwright',
1010
},
1111
},
1212
})

‎test/workspaces-browser/vitest.workspace.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ export default defineWorkspace([
88
root: './space_browser_inline',
99
browser: {
1010
enabled: true,
11-
name: process.env.BROWSER || 'chrome',
11+
name: process.env.BROWSER || 'chromium',
1212
headless: true,
13-
provider: process.env.PROVIDER || 'webdriverio',
13+
provider: process.env.PROVIDER || 'playwright',
1414
},
1515
alias: {
1616
'test-alias-from-vitest': new URL('./space_browser_inline/test-alias-to.ts', import.meta.url).pathname,

0 commit comments

Comments
 (0)
Please sign in to comment.