Add WriterUI (#17051)

This special purpose UI provides commands that can benefit from direct
access to the io.Reader and io.Writers of the base cli.Ui. It can
traverse a chain of ColoredUis to find the base. Currently, it can
retrieve writers from a cli.BasicUi (or cli.MockUi for testing).

Renames ui.go and ui_test.go to log_ui.go and log_ui_test.go
This commit is contained in:
Charlie Voiselle 2023-05-02 13:40:44 -04:00 committed by GitHub
parent e9fec4ebc8
commit 61f997d806
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 596 additions and 0 deletions

83
command/ui/writer_ui.go Normal file
View File

@ -0,0 +1,83 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package ui
import (
"errors"
"fmt"
"io"
"github.com/mitchellh/cli"
)
// WriterUI is an implementation of the cli.Ui interface which can be used for
// commands that need to have direct access to the underlying UI readers and
// writers.
type WriterUI struct {
// Ui is the wrapped cli.Ui that supplies the functions for the thin shims
Ui cli.Ui
reader io.Reader
writer io.Writer
errorWriter io.Writer
// baseUi stores the basic UI that was used to create this WriterUI. It
// allows us to call its functions and not implement them again.
baseUi cli.Ui
}
// NewWriterUI generates a new cli.Ui that can be used for commands that
// need access to the underlying UI's writers for copying large amounts of
// data without local buffering. The caller is required to pass a UI
// chain ending in a cli.BasicUi (or a cli.MockUi for testing).
//
// Currently, the UIs in the chain need to be pointers to a cli.ColoredUi,
// cli.BasicUi, or cli.MockUi to work correctly.
func NewWriterUI(ui cli.Ui) (*WriterUI, error) {
var done bool
wUI := WriterUI{Ui: ui}
for !done {
if ui == nil {
break
}
switch u := ui.(type) {
case *cli.MockUi:
wUI.reader = u.InputReader
wUI.writer = u.OutputWriter
wUI.errorWriter = u.ErrorWriter
wUI.baseUi = u
done = true
case *cli.BasicUi:
wUI.reader = u.Reader
wUI.writer = u.Writer
wUI.errorWriter = u.ErrorWriter
wUI.baseUi = u
done = true
case *cli.ColoredUi:
ui = u.Ui
default:
return nil, fmt.Errorf("writer ui: unsupported Ui type: %T", ui)
}
}
if !done {
return nil, errors.New("failed to generate command UI")
}
return &wUI, nil
}
func (w *WriterUI) InputReader() io.Reader { return w.reader }
func (w *WriterUI) OutputWriter() io.Writer { return w.writer }
func (w *WriterUI) ErrorWriter() io.Writer { return w.errorWriter }
func (w *WriterUI) Output(message string) { w.Ui.Output(message) }
func (w *WriterUI) Info(message string) { w.Ui.Info(message) }
func (w *WriterUI) Warn(message string) { w.Ui.Warn(message) }
func (w *WriterUI) Error(message string) { w.Ui.Error(message) }
func (w *WriterUI) Ask(query string) (string, error) { return w.Ui.Ask(query) }
func (w *WriterUI) AskSecret(query string) (string, error) { return w.Ui.AskSecret(query) }

View File

@ -0,0 +1,513 @@
package ui
import (
"bytes"
"fmt"
"io"
"testing"
"github.com/hashicorp/nomad/ci"
"github.com/mitchellh/cli"
"github.com/shoenig/test/must"
)
func TestWriterUI_Implements(t *testing.T) {
ci.Parallel(t)
var _ cli.Ui = new(WriterUI)
}
type writerUITestCase struct {
name string // the testcase name
baseUi cli.Ui // cli.Ui with accessible writers (currently basicUi or mockUi)
ui cli.Ui // the full ui object chain (should end in baseUi)
initFn func(*writerUITestCase) // sets up the struct for the testcase
ow *bytes.Buffer // handle to basicUi's Output writer
ew *bytes.Buffer // handle to basicUi's Error writer
}
func TestWriterUI_OutputWriter(t *testing.T) {
ci.Parallel(t)
tcs := []writerUITestCase{
{
name: "mockUi/simple",
initFn: func(tc *writerUITestCase) {
tc.baseUi = cli.NewMockUi()
tc.ui = tc.baseUi
},
},
{
name: "mockUi/nested_once",
initFn: func(tc *writerUITestCase) {
tc.baseUi = cli.NewMockUi()
tc.ui = &cli.ColoredUi{Ui: tc.baseUi}
},
},
{
name: "mockUi/nested_twice",
initFn: func(tc *writerUITestCase) {
tc.baseUi = cli.NewMockUi()
tc.ui = &cli.ColoredUi{Ui: &cli.ColoredUi{Ui: tc.baseUi}}
},
},
{
name: "basicUi/simple",
initFn: func(tc *writerUITestCase) {
tc.ow = new(bytes.Buffer)
tc.ew = new(bytes.Buffer)
tc.baseUi = &cli.BasicUi{Writer: tc.ow, ErrorWriter: tc.ew}
tc.ui = tc.baseUi
},
},
{
name: "basicUi/nested_once",
initFn: func(tc *writerUITestCase) {
tc.ow = new(bytes.Buffer)
tc.ew = new(bytes.Buffer)
tc.baseUi = &cli.BasicUi{Writer: tc.ow, ErrorWriter: tc.ew}
tc.ui = &cli.ColoredUi{Ui: tc.baseUi}
},
},
{
name: "basicUi/nested_twice",
initFn: func(tc *writerUITestCase) {
tc.ow = new(bytes.Buffer)
tc.ew = new(bytes.Buffer)
tc.baseUi = &cli.BasicUi{Writer: tc.ow, ErrorWriter: tc.ew}
tc.ui = &cli.ColoredUi{Ui: &cli.ColoredUi{Ui: tc.baseUi}}
},
},
}
for _, tc := range tcs {
tc := tc
t.Run(tc.name, func(t *testing.T) {
ci.Parallel(t)
tc.initFn(&tc)
wUI, err := NewWriterUI(tc.ui)
must.NoError(t, err)
fmt.Fprintf(wUI.OutputWriter(), "foobar")
switch bui := tc.baseUi.(type) {
case *cli.MockUi:
must.Eq(t, "foobar", bui.OutputWriter.String())
must.Eq(t, "", bui.ErrorWriter.String())
case *cli.BasicUi:
must.Eq(t, "foobar", tc.ow.String())
must.Eq(t, "", tc.ew.String())
default:
t.Fatal("invalid base cli.Ui type")
}
})
}
}
func TestWriterUI_Output(t *testing.T) {
ci.Parallel(t)
tcs := []writerUITestCase{
{
name: "mockUi/simple",
initFn: func(tc *writerUITestCase) {
tc.baseUi = cli.NewMockUi()
tc.ui = tc.baseUi
},
},
{
name: "mockUi/nested_once",
initFn: func(tc *writerUITestCase) {
tc.baseUi = cli.NewMockUi()
tc.ui = &cli.ColoredUi{Ui: tc.baseUi}
},
},
{
name: "mockUi/nested_twice",
initFn: func(tc *writerUITestCase) {
tc.baseUi = cli.NewMockUi()
tc.ui = &cli.ColoredUi{Ui: &cli.ColoredUi{Ui: tc.baseUi}}
},
},
{
name: "basicUi/simple",
initFn: func(tc *writerUITestCase) {
tc.ow = new(bytes.Buffer)
tc.ew = new(bytes.Buffer)
tc.baseUi = &cli.BasicUi{Writer: tc.ow, ErrorWriter: tc.ew}
tc.ui = tc.baseUi
},
},
{
name: "basicUi/nested_once",
initFn: func(tc *writerUITestCase) {
tc.ow = new(bytes.Buffer)
tc.ew = new(bytes.Buffer)
tc.baseUi = &cli.BasicUi{Writer: tc.ow, ErrorWriter: tc.ew}
tc.ui = &cli.ColoredUi{Ui: tc.baseUi}
},
},
{
name: "basicUi/nested_twice",
initFn: func(tc *writerUITestCase) {
tc.ow = new(bytes.Buffer)
tc.ew = new(bytes.Buffer)
tc.baseUi = &cli.BasicUi{Writer: tc.ow, ErrorWriter: tc.ew}
tc.ui = &cli.ColoredUi{Ui: &cli.ColoredUi{Ui: tc.baseUi}}
},
},
}
for _, tc := range tcs {
tc := tc
t.Run(tc.name, func(t *testing.T) {
ci.Parallel(t)
tc.initFn(&tc)
wUI, err := NewWriterUI(tc.ui)
must.NoError(t, err)
wUI.Output("foobar")
var ov, ev string
switch bui := tc.baseUi.(type) {
case *cli.MockUi:
ov = bui.OutputWriter.String()
ev = bui.ErrorWriter.String()
case *cli.BasicUi:
ov = tc.ow.String()
ev = tc.ew.String()
default:
t.Fatal("invalid base cli.Ui type")
}
must.Eq(t, "foobar\n", ov)
must.Eq(t, "", ev)
})
}
}
func TestWriterUI_Info(t *testing.T) {
ci.Parallel(t)
tcs := []writerUITestCase{
{
name: "mockUi/simple",
initFn: func(tc *writerUITestCase) {
tc.baseUi = cli.NewMockUi()
tc.ui = tc.baseUi
},
},
{
name: "mockUi/nested_once",
initFn: func(tc *writerUITestCase) {
tc.baseUi = cli.NewMockUi()
tc.ui = &cli.ColoredUi{Ui: tc.baseUi}
},
},
{
name: "mockUi/nested_twice",
initFn: func(tc *writerUITestCase) {
tc.baseUi = cli.NewMockUi()
tc.ui = &cli.ColoredUi{Ui: &cli.ColoredUi{Ui: tc.baseUi}}
},
},
{
name: "basicUi/simple",
initFn: func(tc *writerUITestCase) {
tc.ow = new(bytes.Buffer)
tc.ew = new(bytes.Buffer)
tc.baseUi = &cli.BasicUi{Writer: tc.ow, ErrorWriter: tc.ew}
tc.ui = tc.baseUi
},
},
{
name: "basicUi/nested_once",
initFn: func(tc *writerUITestCase) {
tc.ow = new(bytes.Buffer)
tc.ew = new(bytes.Buffer)
tc.baseUi = &cli.BasicUi{Writer: tc.ow, ErrorWriter: tc.ew}
tc.ui = &cli.ColoredUi{Ui: tc.baseUi}
},
},
{
name: "basicUi/nested_twice",
initFn: func(tc *writerUITestCase) {
tc.ow = new(bytes.Buffer)
tc.ew = new(bytes.Buffer)
tc.baseUi = &cli.BasicUi{Writer: tc.ow, ErrorWriter: tc.ew}
tc.ui = &cli.ColoredUi{Ui: &cli.ColoredUi{Ui: tc.baseUi}}
},
},
}
for _, tc := range tcs {
tc := tc
t.Run(tc.name, func(t *testing.T) {
ci.Parallel(t)
tc.initFn(&tc)
wUI, err := NewWriterUI(tc.ui)
must.NoError(t, err)
wUI.Info("INFO")
var ov, ev string
switch bui := tc.baseUi.(type) {
case *cli.MockUi:
ov = bui.OutputWriter.String()
ev = bui.ErrorWriter.String()
case *cli.BasicUi:
ov = tc.ow.String()
ev = tc.ew.String()
default:
t.Fatal("invalid base cli.Ui type")
}
must.Eq(t, "INFO\n", ov)
must.Eq(t, "", ev)
})
}
}
func TestWriterUI_Warn(t *testing.T) {
ci.Parallel(t)
tcs := []writerUITestCase{
{
name: "mockUi/simple",
initFn: func(tc *writerUITestCase) {
tc.baseUi = cli.NewMockUi()
tc.ui = tc.baseUi
},
},
{
name: "mockUi/nested_once",
initFn: func(tc *writerUITestCase) {
tc.baseUi = cli.NewMockUi()
tc.ui = &cli.ColoredUi{Ui: tc.baseUi}
},
},
{
name: "mockUi/nested_twice",
initFn: func(tc *writerUITestCase) {
tc.baseUi = cli.NewMockUi()
tc.ui = &cli.ColoredUi{Ui: &cli.ColoredUi{Ui: tc.baseUi}}
},
},
{
name: "basicUi/simple",
initFn: func(tc *writerUITestCase) {
tc.ow = new(bytes.Buffer)
tc.ew = new(bytes.Buffer)
tc.baseUi = &cli.BasicUi{Writer: tc.ow, ErrorWriter: tc.ew}
tc.ui = tc.baseUi
},
},
{
name: "basicUi/nested_once",
initFn: func(tc *writerUITestCase) {
tc.ow = new(bytes.Buffer)
tc.ew = new(bytes.Buffer)
tc.baseUi = &cli.BasicUi{Writer: tc.ow, ErrorWriter: tc.ew}
tc.ui = &cli.ColoredUi{Ui: tc.baseUi}
},
},
{
name: "basicUi/nested_twice",
initFn: func(tc *writerUITestCase) {
tc.ow = new(bytes.Buffer)
tc.ew = new(bytes.Buffer)
tc.baseUi = &cli.BasicUi{Writer: tc.ow, ErrorWriter: tc.ew}
tc.ui = &cli.ColoredUi{Ui: &cli.ColoredUi{Ui: tc.baseUi}}
},
},
}
for _, tc := range tcs {
tc := tc
t.Run(tc.name, func(t *testing.T) {
ci.Parallel(t)
tc.initFn(&tc)
wUI, err := NewWriterUI(tc.ui)
must.NoError(t, err)
wUI.Warn("WARN")
const expected = "WARN\n"
var ov, ev string
switch bui := tc.baseUi.(type) {
case *cli.MockUi:
ov = bui.OutputWriter.String()
ev = bui.ErrorWriter.String()
case *cli.BasicUi:
ov = tc.ow.String()
ev = tc.ew.String()
default:
t.Fatal("invalid base cli.Ui type")
}
must.Eq(t, "", ov)
must.Eq(t, "WARN\n", ev)
})
}
}
func TestWriterUI_Error(t *testing.T) {
ci.Parallel(t)
tcs := []writerUITestCase{
{
name: "mockUi/simple",
initFn: func(tc *writerUITestCase) {
tc.baseUi = cli.NewMockUi()
tc.ui = tc.baseUi
},
},
{
name: "mockUi/nested_once",
initFn: func(tc *writerUITestCase) {
tc.baseUi = cli.NewMockUi()
tc.ui = &cli.ColoredUi{Ui: tc.baseUi}
},
},
{
name: "mockUi/nested_twice",
initFn: func(tc *writerUITestCase) {
tc.baseUi = cli.NewMockUi()
tc.ui = &cli.ColoredUi{Ui: &cli.ColoredUi{Ui: tc.baseUi}}
},
},
{
name: "basicUi/simple",
initFn: func(tc *writerUITestCase) {
tc.ow = new(bytes.Buffer)
tc.ew = new(bytes.Buffer)
tc.baseUi = &cli.BasicUi{Writer: tc.ow, ErrorWriter: tc.ew}
tc.ui = tc.baseUi
},
},
{
name: "basicUi/nested_once",
initFn: func(tc *writerUITestCase) {
tc.ow = new(bytes.Buffer)
tc.ew = new(bytes.Buffer)
tc.baseUi = &cli.BasicUi{Writer: tc.ow, ErrorWriter: tc.ew}
tc.ui = &cli.ColoredUi{Ui: tc.baseUi}
},
},
{
name: "basicUi/nested_twice",
initFn: func(tc *writerUITestCase) {
tc.ow = new(bytes.Buffer)
tc.ew = new(bytes.Buffer)
tc.baseUi = &cli.BasicUi{Writer: tc.ow, ErrorWriter: tc.ew}
tc.ui = &cli.ColoredUi{Ui: &cli.ColoredUi{Ui: tc.baseUi}}
},
},
}
for _, tc := range tcs {
tc := tc
t.Run(tc.name, func(t *testing.T) {
ci.Parallel(t)
tc.initFn(&tc)
wUI, err := NewWriterUI(tc.ui)
must.NoError(t, err)
wUI.Warn("ERROR")
var ov, ev string
switch bui := tc.baseUi.(type) {
case *cli.MockUi:
ov = bui.OutputWriter.String()
ev = bui.ErrorWriter.String()
case *cli.BasicUi:
ov = tc.ow.String()
ev = tc.ew.String()
default:
t.Fatal("invalid base cli.Ui type")
}
must.Eq(t, "", ov)
must.Eq(t, "ERROR\n", ev)
})
}
}
func TestWriterUI_Ask(t *testing.T) {
ci.Parallel(t)
testCases := []struct {
name string
query string
input string
expectedQuery string
expectedResult string
}{
{
name: "EmptyString",
query: "Middle Name?",
input: "\n",
expectedQuery: "Middle Name? ",
expectedResult: "",
},
{
name: "NonEmptyString",
query: "Name?",
input: "foo bar\nbaz\n",
expectedQuery: "Name? ",
expectedResult: "foo bar",
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
ci.Parallel(t)
inReader, inWriter := io.Pipe()
t.Cleanup(func() {
inReader.Close()
inWriter.Close()
})
writer := new(bytes.Buffer)
WriterUI, err := NewWriterUI(&cli.BasicUi{
Reader: inReader,
Writer: writer,
})
must.NoError(t, err)
go inWriter.Write([]byte(tc.input))
result, err := WriterUI.Ask(tc.query)
must.NoError(t, err)
must.Eq(t, writer.String(), tc.expectedQuery)
must.Eq(t, result, tc.expectedResult)
})
}
}
func TestWriterUI_AskSecret(t *testing.T) {
ci.Parallel(t)
inReader, inWriter := io.Pipe()
t.Cleanup(func() {
inReader.Close()
inWriter.Close()
})
writer := new(bytes.Buffer)
wUI, err := NewWriterUI(&cli.BasicUi{
Reader: inReader,
Writer: writer,
})
must.NoError(t, err)
go inWriter.Write([]byte("foo bar\nbaz\n"))
result, err := wUI.AskSecret("Name?")
must.NoError(t, err)
must.Eq(t, writer.String(), "Name? ")
must.Eq(t, result, "foo bar")
}