Skip to content

Commit d05718d

Browse files
committed
test: isolate runtime e2e ports in dev tests and fix force-kill
1 parent f668b74 commit d05718d

1 file changed

Lines changed: 35 additions & 8 deletions

File tree

packages/nuxt-cli/test/e2e/runtimes.spec.ts

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { rm } from 'node:fs/promises'
77
import { join, resolve } from 'node:path'
88

99
import { fileURLToPath } from 'node:url'
10-
import { getPort, waitForPort } from 'get-port-please'
10+
import { checkPort, getPort, waitForPort } from 'get-port-please'
1111
import { isCI } from 'std-env'
1212
import { WebSocket } from 'undici'
1313
import { it as _it, afterAll, beforeAll, describe, expect, vi } from 'vitest'
@@ -108,6 +108,7 @@ describe.sequential.each(runtimes)('dev server (%s)', (runtimeName) => {
108108
server = await startDevServer({
109109
cwd,
110110
runtime: runtimeName,
111+
basePort: 3100 + runtimes.indexOf(runtimeName) * 10,
111112
})
112113
})
113114

@@ -261,11 +262,13 @@ interface DevServerInstance {
261262
async function startDevServer(options: {
262263
cwd: string
263264
port?: number
265+
/** Port to prefer, if free. Each runtime gets its own range so a server that is slow to release its listener cannot collide with the next one. */
266+
basePort?: number
264267
runtime?: 'node' | 'bun' | 'deno'
265268
env?: Record<string, string>
266269
}): Promise<DevServerInstance> {
267-
const { cwd, port: preferredPort, runtime = 'node', env = {} } = options
268-
const port = preferredPort || await getPort({ port: 3100 })
270+
const { cwd, port: preferredPort, basePort = 3100, runtime = 'node', env = {} } = options
271+
const port = preferredPort || await getPort({ port: basePort, host: '127.0.0.1' })
269272
const host = '127.0.0.1'
270273
const url = `http://${host}:${port}`
271274

@@ -328,19 +331,43 @@ async function startDevServer(options: {
328331
url,
329332
port,
330333
close: async () => {
331-
return new Promise<void>((resolve) => {
332-
child.kill('SIGTERM')
333-
setTimeout(() => {
334-
if (!child.killed) {
334+
await new Promise<void>((resolve) => {
335+
if (child.exitCode !== null || child.signalCode !== null) {
336+
resolve()
337+
return
338+
}
339+
340+
// `child.killed` only records that a signal was sent, so escalation has to
341+
// look at whether the process has actually gone away.
342+
const force = setTimeout(() => {
343+
if (child.exitCode === null && child.signalCode === null) {
335344
child.kill('SIGKILL')
336345
}
337346
}, 5000)
338-
child.on('exit', () => resolve())
347+
force.unref?.()
348+
349+
child.on('exit', () => {
350+
clearTimeout(force)
351+
resolve()
352+
})
353+
child.kill('SIGTERM')
339354
})
355+
356+
await waitForPortRelease(port, host)
340357
},
341358
}
342359
}
343360

361+
/** Wait for a dev server's listener to be released, so the next runtime can bind it. */
362+
async function waitForPortRelease(port: number, host: string, retries = 50, delay = 100): Promise<void> {
363+
for (let i = 0; i < retries; i++) {
364+
if (await checkPort(port, host)) {
365+
return
366+
}
367+
await new Promise(resolve => setTimeout(resolve, delay))
368+
}
369+
}
370+
344371
interface WebSocketTestOptions {
345372
url: string
346373
timeout?: number

0 commit comments

Comments
 (0)