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