/
fake_tty.go
287 lines (251 loc) · 7.12 KB
/
fake_tty.go
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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
package clitest
import (
"os"
"reflect"
"sync"
"testing"
"time"
"github.com/elves/elvish/pkg/cli"
"github.com/elves/elvish/pkg/cli/term"
"github.com/elves/elvish/pkg/testutil"
)
const (
// Maximum number of buffer updates FakeTTY expect to see.
fakeTTYBufferUpdates = 4096
// Maximum number of events FakeTTY produces.
fakeTTYEvents = 4096
// Maximum number of signals FakeTTY produces.
fakeTTYSignals = 4096
)
// An implementation of the cli.TTY interface that is useful in tests.
type fakeTTY struct {
setup func() (func(), error)
// Channel that StartRead returns. Can be used to inject additional events.
eventCh chan term.Event
// Whether eventCh has been closed.
eventChClosed bool
// Mutex for synchronizing writing and closing eventCh.
eventChMutex sync.Mutex
// Channel for publishing updates of the main buffer and notes buffer.
bufCh, notesBufCh chan *term.Buffer
// Records history of the main buffer and notes buffer.
bufs, notesBufs []*term.Buffer
// Mutexes for guarding bufs and notesBufs.
bufMutex sync.RWMutex
// Channel that NotifySignals returns. Can be used to inject signals.
sigCh chan os.Signal
// Argument that SetRawInput got.
raw int
sizeMutex sync.RWMutex
// Predefined sizes.
height, width int
}
// Initial size of fake TTY.
const (
FakeTTYHeight = 20
FakeTTYWidth = 50
)
// NewFakeTTY creates a new FakeTTY and a handle for controlling it. The initial
// size of the terminal is FakeTTYHeight and FakeTTYWidth.
func NewFakeTTY() (cli.TTY, TTYCtrl) {
tty := &fakeTTY{
eventCh: make(chan term.Event, fakeTTYEvents),
sigCh: make(chan os.Signal, fakeTTYSignals),
bufCh: make(chan *term.Buffer, fakeTTYBufferUpdates),
notesBufCh: make(chan *term.Buffer, fakeTTYBufferUpdates),
height: FakeTTYHeight, width: FakeTTYWidth,
}
return tty, TTYCtrl{tty}
}
// Delegates to the setup function specified using the SetSetup method of
// TTYCtrl, or return a nop function and a nil error.
func (t *fakeTTY) Setup() (func(), error) {
if t.setup == nil {
return func() {}, nil
}
return t.setup()
}
// Returns the size specified by using the SetSize method of TTYCtrl.
func (t *fakeTTY) Size() (h, w int) {
t.sizeMutex.RLock()
defer t.sizeMutex.RUnlock()
return t.height, t.width
}
// Returns next event from t.eventCh.
func (t *fakeTTY) ReadEvent() (term.Event, error) {
return <-t.eventCh, nil
}
// Records the argument.
func (t *fakeTTY) SetRawInput(n int) {
t.raw = n
}
// Closes eventCh.
func (t *fakeTTY) StopInput() {
t.eventChMutex.Lock()
defer t.eventChMutex.Unlock()
close(t.eventCh)
t.eventChClosed = true
}
// Returns the last recorded buffer.
func (t *fakeTTY) Buffer() *term.Buffer {
t.bufMutex.RLock()
defer t.bufMutex.RUnlock()
return t.bufs[len(t.bufs)-1]
}
// Records a nil buffer.
func (t *fakeTTY) ResetBuffer() {
t.bufMutex.Lock()
defer t.bufMutex.Unlock()
t.recordBuf(nil)
}
// UpdateBuffer records a new pair of buffers, i.e. sending them to their
// respective channels and appending them to their respective slices.
func (t *fakeTTY) UpdateBuffer(bufNotes, buf *term.Buffer, _ bool) error {
t.bufMutex.Lock()
defer t.bufMutex.Unlock()
t.recordNotesBuf(bufNotes)
t.recordBuf(buf)
return nil
}
func (t *fakeTTY) NotifySignals() <-chan os.Signal { return t.sigCh }
func (t *fakeTTY) StopSignals() { close(t.sigCh) }
func (t *fakeTTY) recordBuf(buf *term.Buffer) {
t.bufs = append(t.bufs, buf)
t.bufCh <- buf
}
func (t *fakeTTY) recordNotesBuf(buf *term.Buffer) {
t.notesBufs = append(t.notesBufs, buf)
t.notesBufCh <- buf
}
// TTYCtrl is an interface for controlling a fake terminal.
type TTYCtrl struct{ *fakeTTY }
// GetTTYCtrl takes a TTY and returns a TTYCtrl and true, if the TTY is a fake
// terminal. Otherwise it returns an invalid TTYCtrl and false.
func GetTTYCtrl(t cli.TTY) (TTYCtrl, bool) {
fake, ok := t.(*fakeTTY)
return TTYCtrl{fake}, ok
}
// SetSetup sets the return values of the Setup method of the fake terminal.
func (t TTYCtrl) SetSetup(restore func(), err error) {
t.setup = func() (func(), error) {
return restore, err
}
}
// SetSize sets the size of the fake terminal.
func (t TTYCtrl) SetSize(h, w int) {
t.sizeMutex.Lock()
defer t.sizeMutex.Unlock()
t.height, t.width = h, w
}
// Inject injects events to the fake terminal.
func (t TTYCtrl) Inject(events ...term.Event) {
for _, event := range events {
t.inject(event)
}
}
func (t TTYCtrl) inject(event term.Event) {
t.eventChMutex.Lock()
defer t.eventChMutex.Unlock()
if !t.eventChClosed {
t.eventCh <- event
}
}
// EventCh returns the underlying channel for delivering events.
func (t TTYCtrl) EventCh() chan term.Event {
return t.eventCh
}
// InjectSignal injects signals.
func (t TTYCtrl) InjectSignal(sigs ...os.Signal) {
for _, sig := range sigs {
t.sigCh <- sig
}
}
// RawInput returns the argument in the last call to the SetRawInput method of
// the TTY.
func (t TTYCtrl) RawInput() int {
return t.raw
}
// TestBuffer verifies that a buffer will appear within the timeout of 4
// seconds, and fails the test if it doesn't
func (t TTYCtrl) TestBuffer(tt *testing.T, b *term.Buffer) {
tt.Helper()
ok := testBuffer(tt, b, t.bufCh)
if !ok {
t.bufMutex.RLock()
defer t.bufMutex.RUnlock()
lastBuf := t.LastBuffer()
tt.Logf("Last buffer: %s", lastBuf.TTYString())
if lastBuf == nil {
bufs := t.BufferHistory()
for i := len(bufs) - 1; i >= 0; i-- {
if bufs[i] != nil {
tt.Logf("Last non-nil buffer: %s", bufs[i].TTYString())
return
}
}
}
}
}
// TestNotesBuffer verifies that a notes buffer will appear within the timeout of 4
// seconds, and fails the test if it doesn't
func (t TTYCtrl) TestNotesBuffer(tt *testing.T, b *term.Buffer) {
tt.Helper()
ok := testBuffer(tt, b, t.notesBufCh)
if !ok {
t.bufMutex.RLock()
defer t.bufMutex.RUnlock()
bufs := t.NotesBufferHistory()
tt.Logf("There has been %d notes buffers. None-nil ones are:", len(bufs))
for i, buf := range bufs {
if buf != nil {
tt.Logf("#%d:\n%s", i, buf.TTYString())
}
}
}
}
// BufferHistory returns a slice of all buffers that have appeared.
func (t TTYCtrl) BufferHistory() []*term.Buffer {
t.bufMutex.RLock()
defer t.bufMutex.RUnlock()
return t.bufs
}
// LastBuffer returns the last buffer that has appeared.
func (t TTYCtrl) LastBuffer() *term.Buffer {
t.bufMutex.RLock()
defer t.bufMutex.RUnlock()
if len(t.bufs) == 0 {
return nil
}
return t.bufs[len(t.bufs)-1]
}
// NotesBufferHistory returns a slice of all notes buffers that have appeared.
func (t TTYCtrl) NotesBufferHistory() []*term.Buffer {
t.bufMutex.RLock()
defer t.bufMutex.RUnlock()
return t.notesBufs
}
func (t TTYCtrl) LastNotesBuffer() *term.Buffer {
t.bufMutex.RLock()
defer t.bufMutex.RUnlock()
if len(t.notesBufs) == 0 {
return nil
}
return t.notesBufs[len(t.notesBufs)-1]
}
// Tests that an expected buffer will appear within the timeout.
func testBuffer(t *testing.T, want *term.Buffer, ch <-chan *term.Buffer) bool {
t.Helper()
timeout := time.After(testutil.ScaledMs(100))
for {
select {
case buf := <-ch:
if reflect.DeepEqual(buf, want) {
return true
}
case <-timeout:
t.Errorf("Wanted buffer not shown")
t.Logf("Want: %s", want.TTYString())
return false
}
}
}