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 }