github.com/elves/elvish@v0.15.0/pkg/cli/app_test.go (about)

     1  package cli_test
     2  
     3  import (
     4  	"errors"
     5  	"io"
     6  	"strings"
     7  	"syscall"
     8  	"testing"
     9  	"time"
    10  
    11  	. "github.com/elves/elvish/pkg/cli"
    12  	. "github.com/elves/elvish/pkg/cli/clitest"
    13  	"github.com/elves/elvish/pkg/cli/term"
    14  	"github.com/elves/elvish/pkg/sys"
    15  	"github.com/elves/elvish/pkg/ui"
    16  )
    17  
    18  // Lifecycle aspects.
    19  
    20  func TestReadCode_AbortsWhenTTYSetupReturnsError(t *testing.T) {
    21  	ttySetupErr := errors.New("a fake error")
    22  	f := Setup(WithTTY(func(tty TTYCtrl) {
    23  		tty.SetSetup(func() {}, ttySetupErr)
    24  	}))
    25  
    26  	_, err := f.Wait()
    27  
    28  	if err != ttySetupErr {
    29  		t.Errorf("ReadCode returns error %v, want %v", err, ttySetupErr)
    30  	}
    31  }
    32  
    33  func TestReadCode_RestoresTTYBeforeReturning(t *testing.T) {
    34  	restoreCalled := 0
    35  	f := Setup(WithTTY(func(tty TTYCtrl) {
    36  		tty.SetSetup(func() { restoreCalled++ }, nil)
    37  	}))
    38  
    39  	f.Stop()
    40  
    41  	if restoreCalled != 1 {
    42  		t.Errorf("Restore callback called %d times, want once", restoreCalled)
    43  	}
    44  }
    45  
    46  func TestReadCode_ResetsStateBeforeReturning(t *testing.T) {
    47  	f := Setup(WithSpec(func(spec *AppSpec) {
    48  		spec.CodeAreaState.Buffer.Content = "some code"
    49  	}))
    50  
    51  	f.Stop()
    52  
    53  	if code := GetCodeBuffer(f.App); code != (CodeBuffer{}) {
    54  		t.Errorf("Editor state has CodeBuffer %v, want empty", code)
    55  	}
    56  }
    57  
    58  func TestReadCode_CallsBeforeReadline(t *testing.T) {
    59  	callCh := make(chan bool, 1)
    60  	f := Setup(WithSpec(func(spec *AppSpec) {
    61  		spec.BeforeReadline = []func(){func() { callCh <- true }}
    62  	}))
    63  	defer f.Stop()
    64  
    65  	select {
    66  	case <-callCh:
    67  		// OK, do nothing.
    68  	case <-time.After(time.Second):
    69  		t.Errorf("BeforeReadline not called")
    70  	}
    71  }
    72  
    73  func TestReadCode_CallsBeforeReadlineBeforePromptTrigger(t *testing.T) {
    74  	callCh := make(chan string, 2)
    75  	f := Setup(WithSpec(func(spec *AppSpec) {
    76  		spec.BeforeReadline = []func(){func() { callCh <- "hook" }}
    77  		spec.Prompt = testPrompt{trigger: func(bool) { callCh <- "prompt" }}
    78  	}))
    79  	defer f.Stop()
    80  
    81  	if first := <-callCh; first != "hook" {
    82  		t.Errorf("BeforeReadline hook not called before prompt trigger")
    83  	}
    84  }
    85  
    86  func TestReadCode_CallsAfterReadline(t *testing.T) {
    87  	callCh := make(chan string, 1)
    88  	f := Setup(WithSpec(func(spec *AppSpec) {
    89  		spec.AfterReadline = []func(string){func(s string) { callCh <- s }}
    90  	}))
    91  
    92  	feedInput(f.TTY, "abc\n")
    93  	f.Wait()
    94  
    95  	select {
    96  	case calledWith := <-callCh:
    97  		wantCalledWith := "abc"
    98  		if calledWith != wantCalledWith {
    99  			t.Errorf("AfterReadline hook called with %v, want %v",
   100  				calledWith, wantCalledWith)
   101  		}
   102  	case <-time.After(time.Second):
   103  		t.Errorf("AfterReadline not called")
   104  	}
   105  }
   106  
   107  func TestReadCode_FinalRedraw(t *testing.T) {
   108  	f := Setup(WithSpec(func(spec *AppSpec) {
   109  		spec.CodeAreaState.Buffer.Content = "code"
   110  		spec.State.Addon = Label{Content: ui.T("addon")}
   111  	}))
   112  
   113  	// Wait until the stable state.
   114  	wantBuf := bb().
   115  		Write("code").
   116  		Newline().SetDotHere().Write("addon").Buffer()
   117  	f.TTY.TestBuffer(t, wantBuf)
   118  
   119  	f.Stop()
   120  
   121  	// Final redraw hides the addon, and puts the cursor on a new line.
   122  	wantFinalBuf := bb().
   123  		Write("code").Newline().SetDotHere().Buffer()
   124  	f.TTY.TestBuffer(t, wantFinalBuf)
   125  }
   126  
   127  // Signals.
   128  
   129  func TestReadCode_ReturnsEOFOnSIGHUP(t *testing.T) {
   130  	f := Setup()
   131  
   132  	f.TTY.Inject(term.K('a'))
   133  	// Wait until the initial redraw.
   134  	f.TTY.TestBuffer(t, bb().Write("a").SetDotHere().Buffer())
   135  
   136  	f.TTY.InjectSignal(syscall.SIGHUP)
   137  
   138  	_, err := f.Wait()
   139  	if err != io.EOF {
   140  		t.Errorf("want ReadCode to return io.EOF on SIGHUP, got %v", err)
   141  	}
   142  }
   143  
   144  func TestReadCode_ResetsStateOnSIGINT(t *testing.T) {
   145  	f := Setup()
   146  	defer f.Stop()
   147  
   148  	// Ensure that the terminal shows an non-empty state.
   149  	feedInput(f.TTY, "code")
   150  	f.TTY.TestBuffer(t, bb().Write("code").SetDotHere().Buffer())
   151  
   152  	f.TTY.InjectSignal(syscall.SIGINT)
   153  
   154  	// Verify that the state has now reset.
   155  	f.TTY.TestBuffer(t, bb().Buffer())
   156  }
   157  
   158  func TestReadCode_RedrawsOnSIGWINCH(t *testing.T) {
   159  	f := Setup()
   160  	defer f.Stop()
   161  
   162  	// Ensure that the terminal shows the input with the initial width.
   163  	feedInput(f.TTY, "1234567890")
   164  	f.TTY.TestBuffer(t, bb().Write("1234567890").SetDotHere().Buffer())
   165  
   166  	// Emulate a window size change.
   167  	f.TTY.SetSize(24, 4)
   168  	f.TTY.InjectSignal(sys.SIGWINCH)
   169  
   170  	// Test that the editor has redrawn using the new width.
   171  	f.TTY.TestBuffer(t, term.NewBufferBuilder(4).
   172  		Write("1234567890").SetDotHere().Buffer())
   173  }
   174  
   175  // Code area.
   176  
   177  func TestReadCode_LetsCodeAreaHandleEvents(t *testing.T) {
   178  	f := Setup()
   179  	defer f.Stop()
   180  
   181  	feedInput(f.TTY, "code")
   182  	f.TTY.TestBuffer(t, bb().Write("code").SetDotHere().Buffer())
   183  }
   184  
   185  func TestReadCode_ShowsHighlightedCode(t *testing.T) {
   186  	f := Setup(withHighlighter(
   187  		testHighlighter{
   188  			get: func(code string) (ui.Text, []error) {
   189  				return ui.T(code, ui.FgRed), nil
   190  			},
   191  		}))
   192  	defer f.Stop()
   193  
   194  	feedInput(f.TTY, "code")
   195  	wantBuf := bb().Write("code", ui.FgRed).SetDotHere().Buffer()
   196  	f.TTY.TestBuffer(t, wantBuf)
   197  }
   198  
   199  func TestReadCode_ShowsErrorsFromHighlighter(t *testing.T) {
   200  	f := Setup(withHighlighter(
   201  		testHighlighter{
   202  			get: func(code string) (ui.Text, []error) {
   203  				errors := []error{errors.New("ERR 1"), errors.New("ERR 2")}
   204  				return ui.T(code), errors
   205  			},
   206  		}))
   207  	defer f.Stop()
   208  
   209  	feedInput(f.TTY, "code")
   210  
   211  	wantBuf := bb().
   212  		Write("code").SetDotHere().Newline().
   213  		Write("ERR 1").Newline().
   214  		Write("ERR 2").Buffer()
   215  	f.TTY.TestBuffer(t, wantBuf)
   216  }
   217  
   218  func TestReadCode_RedrawsOnLateUpdateFromHighlighter(t *testing.T) {
   219  	var styling ui.Styling
   220  	hl := testHighlighter{
   221  		get: func(code string) (ui.Text, []error) {
   222  			return ui.T(code, styling), nil
   223  		},
   224  		lateUpdates: make(chan struct{}),
   225  	}
   226  	f := Setup(withHighlighter(hl))
   227  	defer f.Stop()
   228  
   229  	feedInput(f.TTY, "code")
   230  
   231  	f.TTY.TestBuffer(t, bb().Write("code").SetDotHere().Buffer())
   232  
   233  	styling = ui.FgRed
   234  	hl.lateUpdates <- struct{}{}
   235  	f.TTY.TestBuffer(t, bb().Write("code", ui.FgRed).SetDotHere().Buffer())
   236  }
   237  
   238  func withHighlighter(hl Highlighter) func(*AppSpec, TTYCtrl) {
   239  	return WithSpec(func(spec *AppSpec) { spec.Highlighter = hl })
   240  }
   241  
   242  func TestReadCode_ShowsPrompt(t *testing.T) {
   243  	f := Setup(WithSpec(func(spec *AppSpec) {
   244  		spec.Prompt = NewConstPrompt(ui.T("> "))
   245  	}))
   246  	defer f.Stop()
   247  
   248  	f.TTY.Inject(term.K('a'))
   249  	f.TTY.TestBuffer(t, bb().Write("> a").SetDotHere().Buffer())
   250  }
   251  
   252  func TestReadCode_CallsPromptTrigger(t *testing.T) {
   253  	triggerCh := make(chan bool, 1)
   254  	f := Setup(WithSpec(func(spec *AppSpec) {
   255  		spec.Prompt = testPrompt{trigger: func(bool) { triggerCh <- true }}
   256  	}))
   257  	defer f.Stop()
   258  
   259  	select {
   260  	case <-triggerCh:
   261  	// Good, test passes
   262  	case <-time.After(time.Second):
   263  		t.Errorf("Trigger not called within 1s")
   264  	}
   265  }
   266  
   267  func TestReadCode_RedrawsOnLateUpdateFromPrompt(t *testing.T) {
   268  	promptContent := "old"
   269  	prompt := testPrompt{
   270  		get:         func() ui.Text { return ui.T(promptContent) },
   271  		lateUpdates: make(chan struct{}),
   272  	}
   273  	f := Setup(WithSpec(func(spec *AppSpec) { spec.Prompt = prompt }))
   274  	defer f.Stop()
   275  
   276  	// Wait until old prompt is rendered
   277  	f.TTY.TestBuffer(t, bb().Write("old").SetDotHere().Buffer())
   278  
   279  	promptContent = "new"
   280  	prompt.lateUpdates <- struct{}{}
   281  	f.TTY.TestBuffer(t, bb().Write("new").SetDotHere().Buffer())
   282  }
   283  
   284  func TestReadCode_ShowsRPrompt(t *testing.T) {
   285  	f := Setup(WithSpec(func(spec *AppSpec) {
   286  		spec.RPrompt = NewConstPrompt(ui.T("R"))
   287  	}))
   288  	defer f.Stop()
   289  
   290  	f.TTY.Inject(term.K('a'))
   291  
   292  	wantBuf := bb().
   293  		Write("a").SetDotHere().
   294  		Write(strings.Repeat(" ", FakeTTYWidth-2)).
   295  		Write("R").Buffer()
   296  	f.TTY.TestBuffer(t, wantBuf)
   297  }
   298  
   299  func TestReadCode_ShowsRPromptInFinalRedrawIfPersistent(t *testing.T) {
   300  	f := Setup(WithSpec(func(spec *AppSpec) {
   301  		spec.CodeAreaState.Buffer.Content = "code"
   302  		spec.RPrompt = NewConstPrompt(ui.T("R"))
   303  		spec.RPromptPersistent = func() bool { return true }
   304  	}))
   305  	defer f.Stop()
   306  
   307  	f.TTY.Inject(term.K('\n'))
   308  
   309  	wantBuf := bb().
   310  		Write("code" + strings.Repeat(" ", FakeTTYWidth-5) + "R").
   311  		Newline().SetDotHere(). // cursor on newline in final redraw
   312  		Buffer()
   313  	f.TTY.TestBuffer(t, wantBuf)
   314  }
   315  
   316  func TestReadCode_HidesRPromptInFinalRedrawIfNotPersistent(t *testing.T) {
   317  	f := Setup(WithSpec(func(spec *AppSpec) {
   318  		spec.CodeAreaState.Buffer.Content = "code"
   319  		spec.RPrompt = NewConstPrompt(ui.T("R"))
   320  		spec.RPromptPersistent = func() bool { return false }
   321  	}))
   322  	defer f.Stop()
   323  
   324  	f.TTY.Inject(term.K('\n'))
   325  
   326  	wantBuf := bb().
   327  		Write("code").          // no rprompt
   328  		Newline().SetDotHere(). // cursor on newline in final redraw
   329  		Buffer()
   330  	f.TTY.TestBuffer(t, wantBuf)
   331  }
   332  
   333  // Addon.
   334  
   335  func TestReadCode_LetsAddonHandleEvents(t *testing.T) {
   336  	f := Setup(WithSpec(func(spec *AppSpec) {
   337  		spec.State.Addon = NewCodeArea(CodeAreaSpec{
   338  			Prompt: func() ui.Text { return ui.T("addon> ") },
   339  		})
   340  	}))
   341  	defer f.Stop()
   342  
   343  	feedInput(f.TTY, "input")
   344  
   345  	wantBuf := bb().Newline(). // empty main code area
   346  					Write("addon> input").SetDotHere(). // addon
   347  					Buffer()
   348  	f.TTY.TestBuffer(t, wantBuf)
   349  }
   350  
   351  type testAddon struct {
   352  	Empty
   353  	focus bool
   354  }
   355  
   356  func (a testAddon) Focus() bool { return a.focus }
   357  
   358  func TestReadCode_RespectsAddonFocusMethod(t *testing.T) {
   359  	addon := testAddon{}
   360  	f := Setup(WithSpec(func(spec *AppSpec) { spec.State.Addon = &addon }))
   361  	defer f.Stop()
   362  
   363  	wantBuf := bb().
   364  		SetDotHere(). // main code area has focus
   365  		Newline().Buffer()
   366  	f.TTY.TestBuffer(t, wantBuf)
   367  
   368  	addon.focus = true
   369  	f.App.Redraw()
   370  
   371  	wantBuf = bb().
   372  		Newline().SetDotHere(). // addon has focus
   373  		Buffer()
   374  	f.TTY.TestBuffer(t, wantBuf)
   375  }
   376  
   377  // Misc features.
   378  
   379  func TestReadCode_TrimsBufferToMaxHeight(t *testing.T) {
   380  	f := Setup(func(spec *AppSpec, tty TTYCtrl) {
   381  		spec.MaxHeight = func() int { return 2 }
   382  		// The code needs 3 lines to completely show.
   383  		spec.CodeAreaState.Buffer.Content = strings.Repeat("a", 15)
   384  		tty.SetSize(10, 5) // Width = 5 to make it easy to test
   385  	})
   386  	defer f.Stop()
   387  
   388  	wantBuf := term.NewBufferBuilder(5).
   389  		Write(strings.Repeat("a", 10)). // Only show 2 lines due to MaxHeight.
   390  		Buffer()
   391  	f.TTY.TestBuffer(t, wantBuf)
   392  }
   393  
   394  func TestReadCode_ShowNotes(t *testing.T) {
   395  	// Set up with a binding where 'a' can block indefinitely. This is useful
   396  	// for testing the behavior of writing multiple notes.
   397  	inHandler := make(chan struct{})
   398  	unblock := make(chan struct{})
   399  	f := Setup(WithSpec(func(spec *AppSpec) {
   400  		spec.OverlayHandler = MapHandler{
   401  			term.K('a'): func() {
   402  				inHandler <- struct{}{}
   403  				<-unblock
   404  			},
   405  		}
   406  	}))
   407  	defer f.Stop()
   408  
   409  	// Wait until initial draw.
   410  	f.TTY.TestBuffer(t, bb().Buffer())
   411  
   412  	// Make sure that the app is blocked within an event handler.
   413  	f.TTY.Inject(term.K('a'))
   414  	<-inHandler
   415  
   416  	// Write two notes, and unblock the event handler
   417  	f.App.Notify("note")
   418  	f.App.Notify("note 2")
   419  	unblock <- struct{}{}
   420  
   421  	// Test that the note is rendered onto the notes buffer.
   422  	wantNotesBuf := bb().Write("note").Newline().Write("note 2").Buffer()
   423  	f.TTY.TestNotesBuffer(t, wantNotesBuf)
   424  
   425  	// Test that notes are flushed after being rendered.
   426  	if n := len(f.App.CopyState().Notes); n > 0 {
   427  		t.Errorf("State.Notes has %d elements after redrawing, want 0", n)
   428  	}
   429  }
   430  
   431  func TestReadCode_DoesNotCrashWithNilTTY(t *testing.T) {
   432  	f := Setup(WithSpec(func(spec *AppSpec) { spec.TTY = nil }))
   433  	defer f.Stop()
   434  }
   435  
   436  // Other properties.
   437  
   438  func TestReadCode_DoesNotLockWithALotOfInputsWithNewlines(t *testing.T) {
   439  	// Regression test for #887
   440  	f := Setup(WithTTY(func(tty TTYCtrl) {
   441  		for i := 0; i < 1000; i++ {
   442  			tty.Inject(term.K('#'), term.K('\n'))
   443  		}
   444  	}))
   445  	terminated := make(chan struct{})
   446  	go func() {
   447  		f.Wait()
   448  		close(terminated)
   449  	}()
   450  	select {
   451  	case <-terminated:
   452  	// OK
   453  	case <-time.After(time.Second):
   454  		t.Errorf("ReadCode did not terminate within 1s")
   455  	}
   456  }
   457  
   458  func TestReadCode_DoesNotReadMoreEventsThanNeeded(t *testing.T) {
   459  	f := Setup()
   460  	defer f.Stop()
   461  	f.TTY.Inject(term.K('a'), term.K('\n'), term.K('b'))
   462  	code, err := f.Wait()
   463  	if code != "a" || err != nil {
   464  		t.Errorf("got (%q, %v), want (%q, nil)", code, err, "a")
   465  	}
   466  	if event := <-f.TTY.EventCh(); event != term.K('b') {
   467  		t.Errorf("got event %v, want %v", event, term.K('b'))
   468  	}
   469  }
   470  
   471  // Test utilities.
   472  
   473  func bb() *term.BufferBuilder {
   474  	return term.NewBufferBuilder(FakeTTYWidth)
   475  }
   476  
   477  func feedInput(ttyCtrl TTYCtrl, input string) {
   478  	for _, r := range input {
   479  		ttyCtrl.Inject(term.K(r))
   480  	}
   481  }
   482  
   483  // A Highlighter implementation useful for testing.
   484  type testHighlighter struct {
   485  	get         func(code string) (ui.Text, []error)
   486  	lateUpdates chan struct{}
   487  }
   488  
   489  func (hl testHighlighter) Get(code string) (ui.Text, []error) {
   490  	return hl.get(code)
   491  }
   492  
   493  func (hl testHighlighter) LateUpdates() <-chan struct{} {
   494  	return hl.lateUpdates
   495  }
   496  
   497  // A Prompt implementation useful for testing.
   498  type testPrompt struct {
   499  	trigger     func(force bool)
   500  	get         func() ui.Text
   501  	lateUpdates chan struct{}
   502  }
   503  
   504  func (p testPrompt) Trigger(force bool) {
   505  	if p.trigger != nil {
   506  		p.trigger(force)
   507  	}
   508  }
   509  
   510  func (p testPrompt) Get() ui.Text {
   511  	if p.get != nil {
   512  		return p.get()
   513  	}
   514  	return nil
   515  }
   516  
   517  func (p testPrompt) LateUpdates() <-chan struct{} {
   518  	return p.lateUpdates
   519  }