github.com/markusbkk/elvish@v0.0.0-20231204143114-91dc52438621/pkg/cli/clitest/fake_tty.go (about) 1 package clitest 2 3 import ( 4 "os" 5 "reflect" 6 "sync" 7 "testing" 8 "time" 9 10 "github.com/markusbkk/elvish/pkg/cli" 11 "github.com/markusbkk/elvish/pkg/cli/term" 12 "github.com/markusbkk/elvish/pkg/testutil" 13 ) 14 15 const ( 16 // Maximum number of buffer updates FakeTTY expect to see. 17 fakeTTYBufferUpdates = 4096 18 // Maximum number of events FakeTTY produces. 19 fakeTTYEvents = 4096 20 // Maximum number of signals FakeTTY produces. 21 fakeTTYSignals = 4096 22 ) 23 24 // An implementation of the cli.TTY interface that is useful in tests. 25 type fakeTTY struct { 26 setup func() (func(), error) 27 // Channel that StartRead returns. Can be used to inject additional events. 28 eventCh chan term.Event 29 // Whether eventCh has been closed. 30 eventChClosed bool 31 // Mutex for synchronizing writing and closing eventCh. 32 eventChMutex sync.Mutex 33 // Channel for publishing updates of the main buffer and notes buffer. 34 bufCh, notesBufCh chan *term.Buffer 35 // Records history of the main buffer and notes buffer. 36 bufs, notesBufs []*term.Buffer 37 // Mutexes for guarding bufs and notesBufs. 38 bufMutex sync.RWMutex 39 // Channel that NotifySignals returns. Can be used to inject signals. 40 sigCh chan os.Signal 41 // Argument that SetRawInput got. 42 raw int 43 // Number of times the TTY screen has been cleared, incremented in 44 // ClearScreen. 45 cleared int 46 47 sizeMutex sync.RWMutex 48 // Predefined sizes. 49 height, width int 50 } 51 52 // Initial size of fake TTY. 53 const ( 54 FakeTTYHeight = 20 55 FakeTTYWidth = 50 56 ) 57 58 // NewFakeTTY creates a new FakeTTY and a handle for controlling it. The initial 59 // size of the terminal is FakeTTYHeight and FakeTTYWidth. 60 func NewFakeTTY() (cli.TTY, TTYCtrl) { 61 tty := &fakeTTY{ 62 eventCh: make(chan term.Event, fakeTTYEvents), 63 sigCh: make(chan os.Signal, fakeTTYSignals), 64 bufCh: make(chan *term.Buffer, fakeTTYBufferUpdates), 65 notesBufCh: make(chan *term.Buffer, fakeTTYBufferUpdates), 66 height: FakeTTYHeight, width: FakeTTYWidth, 67 } 68 return tty, TTYCtrl{tty} 69 } 70 71 // Delegates to the setup function specified using the SetSetup method of 72 // TTYCtrl, or return a nop function and a nil error. 73 func (t *fakeTTY) Setup() (func(), error) { 74 if t.setup == nil { 75 return func() {}, nil 76 } 77 return t.setup() 78 } 79 80 // Returns the size specified by using the SetSize method of TTYCtrl. 81 func (t *fakeTTY) Size() (h, w int) { 82 t.sizeMutex.RLock() 83 defer t.sizeMutex.RUnlock() 84 return t.height, t.width 85 } 86 87 // Returns next event from t.eventCh. 88 func (t *fakeTTY) ReadEvent() (term.Event, error) { 89 return <-t.eventCh, nil 90 } 91 92 // Records the argument. 93 func (t *fakeTTY) SetRawInput(n int) { 94 t.raw = n 95 } 96 97 // Closes eventCh. 98 func (t *fakeTTY) CloseReader() { 99 t.eventChMutex.Lock() 100 defer t.eventChMutex.Unlock() 101 close(t.eventCh) 102 t.eventChClosed = true 103 } 104 105 // Returns the last recorded buffer. 106 func (t *fakeTTY) Buffer() *term.Buffer { 107 t.bufMutex.RLock() 108 defer t.bufMutex.RUnlock() 109 return t.bufs[len(t.bufs)-1] 110 } 111 112 // Records a nil buffer. 113 func (t *fakeTTY) ResetBuffer() { 114 t.bufMutex.Lock() 115 defer t.bufMutex.Unlock() 116 t.recordBuf(nil) 117 } 118 119 // UpdateBuffer records a new pair of buffers, i.e. sending them to their 120 // respective channels and appending them to their respective slices. 121 func (t *fakeTTY) UpdateBuffer(bufNotes, buf *term.Buffer, _ bool) error { 122 t.bufMutex.Lock() 123 defer t.bufMutex.Unlock() 124 t.recordNotesBuf(bufNotes) 125 t.recordBuf(buf) 126 return nil 127 } 128 129 func (t *fakeTTY) HideCursor() { 130 } 131 132 func (t *fakeTTY) ShowCursor() { 133 } 134 135 func (t *fakeTTY) ClearScreen() { 136 t.cleared++ 137 } 138 139 func (t *fakeTTY) NotifySignals() <-chan os.Signal { return t.sigCh } 140 141 func (t *fakeTTY) StopSignals() { close(t.sigCh) } 142 143 func (t *fakeTTY) recordBuf(buf *term.Buffer) { 144 t.bufs = append(t.bufs, buf) 145 t.bufCh <- buf 146 } 147 148 func (t *fakeTTY) recordNotesBuf(buf *term.Buffer) { 149 t.notesBufs = append(t.notesBufs, buf) 150 t.notesBufCh <- buf 151 } 152 153 // TTYCtrl is an interface for controlling a fake terminal. 154 type TTYCtrl struct{ *fakeTTY } 155 156 // GetTTYCtrl takes a TTY and returns a TTYCtrl and true, if the TTY is a fake 157 // terminal. Otherwise it returns an invalid TTYCtrl and false. 158 func GetTTYCtrl(t cli.TTY) (TTYCtrl, bool) { 159 fake, ok := t.(*fakeTTY) 160 return TTYCtrl{fake}, ok 161 } 162 163 // SetSetup sets the return values of the Setup method of the fake terminal. 164 func (t TTYCtrl) SetSetup(restore func(), err error) { 165 t.setup = func() (func(), error) { 166 return restore, err 167 } 168 } 169 170 // SetSize sets the size of the fake terminal. 171 func (t TTYCtrl) SetSize(h, w int) { 172 t.sizeMutex.Lock() 173 defer t.sizeMutex.Unlock() 174 t.height, t.width = h, w 175 } 176 177 // Inject injects events to the fake terminal. 178 func (t TTYCtrl) Inject(events ...term.Event) { 179 for _, event := range events { 180 t.inject(event) 181 } 182 } 183 184 func (t TTYCtrl) inject(event term.Event) { 185 t.eventChMutex.Lock() 186 defer t.eventChMutex.Unlock() 187 if !t.eventChClosed { 188 t.eventCh <- event 189 } 190 } 191 192 // EventCh returns the underlying channel for delivering events. 193 func (t TTYCtrl) EventCh() chan term.Event { 194 return t.eventCh 195 } 196 197 // InjectSignal injects signals. 198 func (t TTYCtrl) InjectSignal(sigs ...os.Signal) { 199 for _, sig := range sigs { 200 t.sigCh <- sig 201 } 202 } 203 204 // RawInput returns the argument in the last call to the SetRawInput method of 205 // the TTY. 206 func (t TTYCtrl) RawInput() int { 207 return t.raw 208 } 209 210 // ScreenCleared returns the number of times ClearScreen has been called on the 211 // TTY. 212 func (t TTYCtrl) ScreenCleared() int { 213 return t.cleared 214 } 215 216 // TestBuffer verifies that a buffer will appear within the timeout of 4 217 // seconds, and fails the test if it doesn't 218 func (t TTYCtrl) TestBuffer(tt *testing.T, b *term.Buffer) { 219 tt.Helper() 220 ok := testBuffer(tt, b, t.bufCh) 221 if !ok { 222 t.bufMutex.RLock() 223 defer t.bufMutex.RUnlock() 224 lastBuf := t.LastBuffer() 225 tt.Logf("Last buffer: %s", lastBuf.TTYString()) 226 if lastBuf == nil { 227 bufs := t.BufferHistory() 228 for i := len(bufs) - 1; i >= 0; i-- { 229 if bufs[i] != nil { 230 tt.Logf("Last non-nil buffer: %s", bufs[i].TTYString()) 231 return 232 } 233 } 234 } 235 } 236 } 237 238 // TestNotesBuffer verifies that a notes buffer will appear within the timeout of 4 239 // seconds, and fails the test if it doesn't 240 func (t TTYCtrl) TestNotesBuffer(tt *testing.T, b *term.Buffer) { 241 tt.Helper() 242 ok := testBuffer(tt, b, t.notesBufCh) 243 if !ok { 244 t.bufMutex.RLock() 245 defer t.bufMutex.RUnlock() 246 bufs := t.NotesBufferHistory() 247 tt.Logf("There has been %d notes buffers. None-nil ones are:", len(bufs)) 248 for i, buf := range bufs { 249 if buf != nil { 250 tt.Logf("#%d:\n%s", i, buf.TTYString()) 251 } 252 } 253 } 254 } 255 256 // BufferHistory returns a slice of all buffers that have appeared. 257 func (t TTYCtrl) BufferHistory() []*term.Buffer { 258 t.bufMutex.RLock() 259 defer t.bufMutex.RUnlock() 260 return t.bufs 261 } 262 263 // LastBuffer returns the last buffer that has appeared. 264 func (t TTYCtrl) LastBuffer() *term.Buffer { 265 t.bufMutex.RLock() 266 defer t.bufMutex.RUnlock() 267 if len(t.bufs) == 0 { 268 return nil 269 } 270 return t.bufs[len(t.bufs)-1] 271 } 272 273 // NotesBufferHistory returns a slice of all notes buffers that have appeared. 274 func (t TTYCtrl) NotesBufferHistory() []*term.Buffer { 275 t.bufMutex.RLock() 276 defer t.bufMutex.RUnlock() 277 return t.notesBufs 278 } 279 280 func (t TTYCtrl) LastNotesBuffer() *term.Buffer { 281 t.bufMutex.RLock() 282 defer t.bufMutex.RUnlock() 283 if len(t.notesBufs) == 0 { 284 return nil 285 } 286 return t.notesBufs[len(t.notesBufs)-1] 287 } 288 289 // Tests that an expected buffer will appear within the timeout. 290 func testBuffer(t *testing.T, want *term.Buffer, ch <-chan *term.Buffer) bool { 291 t.Helper() 292 293 timeout := time.After(testutil.Scaled(100 * time.Millisecond)) 294 for { 295 select { 296 case buf := <-ch: 297 if reflect.DeepEqual(buf, want) { 298 return true 299 } 300 case <-timeout: 301 t.Errorf("Wanted buffer not shown") 302 t.Logf("Want: %s", want.TTYString()) 303 return false 304 } 305 } 306 }