-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrestart-hook.spec.ts
More file actions
144 lines (111 loc) · 4.56 KB
/
Copy pathrestart-hook.spec.ts
File metadata and controls
144 lines (111 loc) · 4.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
import type { DevRestartReason } from '../../src/dev/reason'
import { EventEmitter } from 'node:events'
import process from 'node:process'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { createRejectionHandler, createRestartHook } from '../../src/dev'
const ERROR_EVENTS = ['uncaughtException', 'unhandledRejection'] as const
function errorListeners() {
return ERROR_EVENTS.flatMap(event => process.listeners(event as 'uncaughtException'))
}
describe('restart hook', () => {
const installed = new Set<(...args: any[]) => void>()
afterEach(() => {
for (const listener of installed) {
for (const event of ERROR_EVENTS) {
process.off(event, listener as never)
}
}
installed.clear()
})
/** Arm the hook, remembering its process listeners so they can be removed. */
function arm(source: EventEmitter, callback: (reason?: DevRestartReason) => void, armRestart = createRestartHook(source)) {
const before = new Set(errorListeners())
armRestart(callback)
for (const listener of errorListeners()) {
if (!before.has(listener)) {
installed.add(listener)
}
}
return armRestart
}
it('should call the restart callback once', () => {
const source = new EventEmitter()
const callback = vi.fn()
arm(source, callback)
source.emit('restart', { type: 'shortcut' })
source.emit('restart', { type: 'shortcut' })
expect(callback).toHaveBeenCalledExactlyOnceWith({ type: 'shortcut' })
})
it('should restart on an error that is not a remote peer error', () => {
const source = new EventEmitter()
const callback = vi.fn()
arm(source, callback)
const [onError] = [...installed]
onError!(Object.assign(new Error('write EPIPE'), { code: 'EPIPE' }))
expect(callback).not.toHaveBeenCalled()
onError!(Object.assign(new Error('read ECONNRESET'), { code: 'ECONNRESET' }))
expect(callback).not.toHaveBeenCalled()
onError!(new Error('boom'))
expect(callback).toHaveBeenCalledWith({ type: 'error', message: expect.stringContaining('boom') })
})
it('should not stack listeners when re-armed', () => {
const source = new EventEmitter()
const first = vi.fn()
const second = vi.fn()
const armRestart = arm(source, first)
source.emit('restart', { type: 'shortcut' })
const afterRestart = errorListeners().length
arm(source, second, armRestart)
expect(errorListeners().length).toBe(afterRestart + ERROR_EVENTS.length)
expect(source.listenerCount('restart')).toBe(1)
source.emit('restart', { type: 'config' })
expect(first).toHaveBeenCalledTimes(1)
expect(second).toHaveBeenCalledExactlyOnceWith({ type: 'config' })
})
it('should not stack listeners when re-armed after an error-triggered restart', () => {
const source = new EventEmitter()
const armRestart = arm(source, vi.fn())
for (let attempt = 0; attempt < 3; attempt++) {
const [onError] = [...installed]
onError!(new Error('boom'))
arm(source, vi.fn(), armRestart)
expect(source.listenerCount('restart')).toBe(1)
}
})
it('should replace the callback without re-arming while still armed', () => {
const source = new EventEmitter()
const first = vi.fn()
const second = vi.fn()
const armRestart = arm(source, first)
const armed = errorListeners().length
arm(source, second, armRestart)
expect(errorListeners().length).toBe(armed)
source.emit('restart')
expect(first).not.toHaveBeenCalled()
expect(second).toHaveBeenCalledTimes(1)
})
})
describe('rejection handler', () => {
it('should report the rejection and stop', () => {
const report = vi.fn()
const stop = vi.fn()
createRejectionHandler(report, stop)(new Error('boom'))
expect(report).toHaveBeenCalledExactlyOnceWith(expect.stringContaining('boom'))
expect(stop).toHaveBeenCalledTimes(1)
})
it('should describe a rejection that is not an error', () => {
const report = vi.fn()
createRejectionHandler(report, vi.fn())('nope')
expect(report).toHaveBeenCalledExactlyOnceWith('Unhandled Rejection')
})
it('should keep the process alive on remote peer errors', () => {
const report = vi.fn()
const stop = vi.fn()
const handle = createRejectionHandler(report, stop)
handle(Object.assign(new Error('write EPIPE'), { code: 'EPIPE' }))
handle(Object.assign(new Error('read ECONNRESET'), { code: 'ECONNRESET' }))
handle(Object.assign(new Error('premature close'), { code: 'ERR_STREAM_PREMATURE_CLOSE' }))
expect(report).not.toHaveBeenCalled()
expect(stop).not.toHaveBeenCalled()
})
})