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  }