diff --git a/command/ui/ui.go b/command/ui/log_ui.go similarity index 100% rename from command/ui/ui.go rename to command/ui/log_ui.go diff --git a/command/ui/ui_test.go b/command/ui/log_ui_test.go similarity index 100% rename from command/ui/ui_test.go rename to command/ui/log_ui_test.go diff --git a/command/ui/writer_ui.go b/command/ui/writer_ui.go new file mode 100644 index 000000000..8c013247d --- /dev/null +++ b/command/ui/writer_ui.go @@ -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) } diff --git a/command/ui/writer_ui_test.go b/command/ui/writer_ui_test.go new file mode 100644 index 000000000..fc4936129 --- /dev/null +++ b/command/ui/writer_ui_test.go @@ -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") +}