github.com/markusbkk/elvish@v0.0.0-20231204143114-91dc52438621/pkg/cli/clitest/apptest.go (about) 1 // Package clitest provides utilities for testing cli.App. 2 package clitest 3 4 import ( 5 "testing" 6 7 "github.com/markusbkk/elvish/pkg/cli" 8 "github.com/markusbkk/elvish/pkg/cli/term" 9 "github.com/markusbkk/elvish/pkg/ui" 10 ) 11 12 // Styles defines a common stylesheet for unit tests. 13 var Styles = ui.RuneStylesheet{ 14 '_': ui.Underlined, 15 'b': ui.Bold, 16 '*': ui.Stylings(ui.Bold, ui.FgWhite, ui.BgMagenta), 17 '+': ui.Inverse, 18 '/': ui.FgBlue, 19 '#': ui.Stylings(ui.Inverse, ui.FgBlue), 20 '!': ui.FgRed, 21 '?': ui.Stylings(ui.FgBrightWhite, ui.BgRed), 22 '-': ui.FgMagenta, 23 'X': ui.Stylings(ui.Inverse, ui.FgMagenta), 24 'v': ui.FgGreen, 25 'V': ui.Stylings(ui.Underlined, ui.FgGreen), 26 '$': ui.FgMagenta, 27 'c': ui.FgCyan, // mnemonic "Comment" 28 } 29 30 // Fixture is a test fixture. 31 type Fixture struct { 32 App cli.App 33 TTY TTYCtrl 34 width int 35 codeCh <-chan string 36 errCh <-chan error 37 } 38 39 // Setup sets up a test fixture. It contains an App whose ReadCode method has 40 // been started asynchronously. 41 func Setup(fns ...func(*cli.AppSpec, TTYCtrl)) *Fixture { 42 tty, ttyCtrl := NewFakeTTY() 43 spec := cli.AppSpec{TTY: tty} 44 for _, fn := range fns { 45 fn(&spec, ttyCtrl) 46 } 47 app := cli.NewApp(spec) 48 codeCh, errCh := StartReadCode(app.ReadCode) 49 _, width := tty.Size() 50 return &Fixture{app, ttyCtrl, width, codeCh, errCh} 51 } 52 53 // WithSpec takes a function that operates on *cli.AppSpec, and wraps it into a 54 // form suitable for passing to Setup. 55 func WithSpec(f func(*cli.AppSpec)) func(*cli.AppSpec, TTYCtrl) { 56 return func(spec *cli.AppSpec, _ TTYCtrl) { f(spec) } 57 } 58 59 // WithTTY takes a function that operates on TTYCtrl, and wraps it to a form 60 // suitable for passing to Setup. 61 func WithTTY(f func(TTYCtrl)) func(*cli.AppSpec, TTYCtrl) { 62 return func(_ *cli.AppSpec, tty TTYCtrl) { f(tty) } 63 } 64 65 // Wait waits for ReaCode to finish, and returns its return values. 66 func (f *Fixture) Wait() (string, error) { 67 return <-f.codeCh, <-f.errCh 68 } 69 70 // Stop stops ReadCode and waits for it to finish. If ReadCode has already been 71 // stopped, it is a no-op. 72 func (f *Fixture) Stop() { 73 f.App.CommitEOF() 74 f.Wait() 75 } 76 77 // MakeBuffer is a helper for building a buffer. It is equivalent to 78 // term.NewBufferBuilder(width of terminal).MarkLines(args...).Buffer(). 79 func (f *Fixture) MakeBuffer(args ...interface{}) *term.Buffer { 80 return term.NewBufferBuilder(f.width).MarkLines(args...).Buffer() 81 } 82 83 // TestTTY is equivalent to f.TTY.TestBuffer(f.MakeBuffer(args...)). 84 func (f *Fixture) TestTTY(t *testing.T, args ...interface{}) { 85 t.Helper() 86 f.TTY.TestBuffer(t, f.MakeBuffer(args...)) 87 } 88 89 // TestTTYNotes is equivalent to f.TTY.TestNotesBuffer(f.MakeBuffer(args...)). 90 func (f *Fixture) TestTTYNotes(t *testing.T, args ...interface{}) { 91 t.Helper() 92 f.TTY.TestNotesBuffer(t, f.MakeBuffer(args...)) 93 } 94 95 // StartReadCode starts the readCode function asynchronously, and returns two 96 // channels that deliver its return values. The two channels are closed after 97 // return values are delivered, so that subsequent reads will return zero values 98 // and not block. 99 func StartReadCode(readCode func() (string, error)) (<-chan string, <-chan error) { 100 codeCh := make(chan string, 1) 101 errCh := make(chan error, 1) 102 go func() { 103 code, err := readCode() 104 codeCh <- code 105 errCh <- err 106 close(codeCh) 107 close(errCh) 108 }() 109 return codeCh, errCh 110 }