test(cli): cover shutdown timer invariants
This commit is contained in:
@@ -30,6 +30,7 @@ export function createProcessShutdown(
|
|||||||
const exitOnce = (code: number): void => {
|
const exitOnce = (code: number): void => {
|
||||||
if (exited) return
|
if (exited) return
|
||||||
exited = true
|
exited = true
|
||||||
|
/* v8 ignore else -- shutdown() arms the timer before any asynchronous exit path can run. */
|
||||||
if (timeout !== undefined) clearTimeout(timeout)
|
if (timeout !== undefined) clearTimeout(timeout)
|
||||||
exit(code)
|
exit(code)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,10 @@ function deferred(): { promise: Promise<void>; resolve: () => void; reject: (err
|
|||||||
return { promise, resolve, reject }
|
return { promise, resolve, reject }
|
||||||
}
|
}
|
||||||
|
|
||||||
afterEach(() => { vi.useRealTimers() })
|
afterEach(() => {
|
||||||
|
vi.useRealTimers()
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
describe('process shutdown', () => {
|
describe('process shutdown', () => {
|
||||||
it('exits once after graceful disposal resolves or rejects', async () => {
|
it('exits once after graceful disposal resolves or rejects', async () => {
|
||||||
@@ -31,6 +34,16 @@ describe('process shutdown', () => {
|
|||||||
expect(rejectedExit).toHaveBeenCalledWith(1)
|
expect(rejectedExit).toHaveBeenCalledWith(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('uses process.exit as the default process boundary', async () => {
|
||||||
|
const exit = vi.spyOn(process, 'exit').mockImplementation(_code => undefined as never)
|
||||||
|
const shutdown = createProcessShutdown(() => Promise.resolve())
|
||||||
|
|
||||||
|
await shutdown.shutdown(7)
|
||||||
|
|
||||||
|
expect(exit).toHaveBeenCalledOnce()
|
||||||
|
expect(exit).toHaveBeenCalledWith(7)
|
||||||
|
})
|
||||||
|
|
||||||
it('forces exit when graceful disposal reaches its bound', async () => {
|
it('forces exit when graceful disposal reaches its bound', async () => {
|
||||||
vi.useFakeTimers()
|
vi.useFakeTimers()
|
||||||
const disposal = deferred()
|
const disposal = deferred()
|
||||||
@@ -49,6 +62,22 @@ describe('process shutdown', () => {
|
|||||||
expect(exit).toHaveBeenCalledOnce()
|
expect(exit).toHaveBeenCalledOnce()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('honors a caller-supplied grace period', async () => {
|
||||||
|
vi.useFakeTimers()
|
||||||
|
const disposal = deferred()
|
||||||
|
const exit = vi.fn()
|
||||||
|
const shutdown = createProcessShutdown(() => disposal.promise, exit, 25)
|
||||||
|
const pending = shutdown.shutdown(0)
|
||||||
|
|
||||||
|
await vi.advanceTimersByTimeAsync(24)
|
||||||
|
expect(exit).not.toHaveBeenCalled()
|
||||||
|
await vi.advanceTimersByTimeAsync(1)
|
||||||
|
expect(exit).toHaveBeenCalledOnce()
|
||||||
|
|
||||||
|
disposal.resolve()
|
||||||
|
await pending
|
||||||
|
})
|
||||||
|
|
||||||
it('lets Ctrl+C force a normal shutdown already stuck in disposal', async () => {
|
it('lets Ctrl+C force a normal shutdown already stuck in disposal', async () => {
|
||||||
const disposal = deferred()
|
const disposal = deferred()
|
||||||
const exit = vi.fn()
|
const exit = vi.fn()
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ export class TelemetryOtel extends Telemetry {
|
|||||||
try {
|
try {
|
||||||
await Promise.race([providerShutdown, deadline])
|
await Promise.race([providerShutdown, deadline])
|
||||||
} finally {
|
} finally {
|
||||||
|
/* v8 ignore else -- the Promise executor assigns timer synchronously before this race starts. */
|
||||||
if (timer !== undefined) clearTimeout(timer)
|
if (timer !== undefined) clearTimeout(timer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user