diff --git a/internal/cmd/envs.go b/internal/cmd/envs.go index 73d71dd8..67739398 100644 --- a/internal/cmd/envs.go +++ b/internal/cmd/envs.go @@ -74,7 +74,7 @@ func lsEnvsCommand() *cobra.Command { switch outputFmt { case humanOutput: - err := tablewriter.WriteTable(len(envs), func(i int) interface{} { + err := tablewriter.WriteTable(cmd.OutOrStdout(), len(envs), func(i int) interface{} { return envs[i] }) if err != nil { diff --git a/internal/cmd/envs_test.go b/internal/cmd/envs_test.go new file mode 100644 index 00000000..879a20a0 --- /dev/null +++ b/internal/cmd/envs_test.go @@ -0,0 +1,76 @@ +package cmd + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "strings" + "testing" + + "cdr.dev/slog" + "cdr.dev/slog/sloggers/slogtest" + "cdr.dev/slog/sloggers/slogtest/assert" + "golang.org/x/xerrors" + + "cdr.dev/coder-cli/internal/config" + "cdr.dev/coder-cli/pkg/clog" +) + +func init() { + tmpDir, err := ioutil.TempDir("", "coder-cli-config-dir") + if err != nil { + panic(err) + } + config.SetRoot(tmpDir) +} + +func TestEnvsCommand(t *testing.T) { + res := execute(t, []string{"envs", "ls"}, nil) + assert.Error(t, "execute without auth", res.ExitErr) + + err := assertClogErr(t, res.ExitErr) + assert.True(t, "login hint in error", strings.Contains(err.String(), "did you run \"coder login")) +} + +type result struct { + OutBuffer *bytes.Buffer + ErrBuffer *bytes.Buffer + ExitErr error +} + +func execute(t *testing.T, args []string, in io.Reader) result { + cmd := Make() + + outStream := bytes.NewBuffer(nil) + errStream := bytes.NewBuffer(nil) + + cmd.SetArgs(args) + + cmd.SetIn(in) + cmd.SetOut(outStream) + cmd.SetErr(errStream) + + err := cmd.Execute() + + slogtest.Debug(t, "execute command", + slog.F("outBuffer", outStream.String()), + slog.F("errBuffer", errStream.String()), + slog.F("args", args), + slog.F("execute_error", err), + ) + return result{ + OutBuffer: outStream, + ErrBuffer: errStream, + ExitErr: err, + } +} + +func assertClogErr(t *testing.T, err error) clog.CLIError { + var cliErr clog.CLIError + if !xerrors.As(err, &cliErr) { + slogtest.Fatal(t, "expected clog error, none found", slog.Error(err), slog.F("type", fmt.Sprintf("%T", err))) + } + slogtest.Debug(t, "clog error", slog.F("message", cliErr.String())) + return cliErr +} diff --git a/internal/cmd/images.go b/internal/cmd/images.go index ff47bf61..70364a59 100644 --- a/internal/cmd/images.go +++ b/internal/cmd/images.go @@ -70,7 +70,7 @@ func lsImgsCommand(user *string) *cobra.Command { } return nil case humanOutput: - err = tablewriter.WriteTable(len(imgs), func(i int) interface{} { + err = tablewriter.WriteTable(cmd.OutOrStdout(), len(imgs), func(i int) interface{} { return imgs[i] }) if err != nil { diff --git a/internal/cmd/providers.go b/internal/cmd/providers.go index e771dcf1..75c2066c 100644 --- a/internal/cmd/providers.go +++ b/internal/cmd/providers.go @@ -54,7 +54,7 @@ coder providers create my-new-workspace-provider`, return xerrors.Errorf("create workspace provider: %w", err) } - err = tablewriter.WriteTable(1, func(i int) interface{} { + err = tablewriter.WriteTable(cmd.OutOrStdout(), 1, func(i int) interface{} { return *wp }) if err != nil { @@ -86,7 +86,7 @@ coder providers ls`, return xerrors.Errorf("list workspace providers: %w", err) } - err = tablewriter.WriteTable(len(wps.Kubernetes), func(i int) interface{} { + err = tablewriter.WriteTable(cmd.OutOrStdout(), len(wps.Kubernetes), func(i int) interface{} { return wps.Kubernetes[i] }) if err != nil { diff --git a/internal/cmd/tags.go b/internal/cmd/tags.go index 3a357f14..91d7ba19 100644 --- a/internal/cmd/tags.go +++ b/internal/cmd/tags.go @@ -107,7 +107,7 @@ func tagsLsCmd() *cobra.Command { switch outputFmt { case humanOutput: - err = tablewriter.WriteTable(len(tags), func(i int) interface{} { return tags[i] }) + err = tablewriter.WriteTable(cmd.OutOrStdout(), len(tags), func(i int) interface{} { return tags[i] }) if err != nil { return err } diff --git a/internal/cmd/tokens.go b/internal/cmd/tokens.go index 81a705c6..21fd478f 100644 --- a/internal/cmd/tokens.go +++ b/internal/cmd/tokens.go @@ -48,7 +48,7 @@ func lsTokensCmd() *cobra.Command { switch outputFmt { case humanOutput: - err := tablewriter.WriteTable(len(tokens), func(i int) interface{} { + err := tablewriter.WriteTable(cmd.OutOrStdout(), len(tokens), func(i int) interface{} { return tokens[i] }) if err != nil { diff --git a/internal/cmd/urls.go b/internal/cmd/urls.go index cb7f7ca1..587f843d 100644 --- a/internal/cmd/urls.go +++ b/internal/cmd/urls.go @@ -99,7 +99,7 @@ func listDevURLsCmd(outputFmt *string) func(cmd *cobra.Command, args []string) e clog.LogInfo(fmt.Sprintf("no devURLs found for environment %q", envName)) return nil } - err := tablewriter.WriteTable(len(devURLs), func(i int) interface{} { + err := tablewriter.WriteTable(cmd.OutOrStdout(), len(devURLs), func(i int) interface{} { return devURLs[i] }) if err != nil { diff --git a/internal/cmd/users.go b/internal/cmd/users.go index a844fd41..03929366 100644 --- a/internal/cmd/users.go +++ b/internal/cmd/users.go @@ -46,7 +46,7 @@ func listUsers(outputFmt *string) func(cmd *cobra.Command, args []string) error case humanOutput: // For each element, return the user. each := func(i int) interface{} { return users[i] } - if err := tablewriter.WriteTable(len(users), each); err != nil { + if err := tablewriter.WriteTable(cmd.OutOrStdout(), len(users), each); err != nil { return xerrors.Errorf("write table: %w", err) } case "json": diff --git a/internal/config/dir.go b/internal/config/dir.go index 34cc7ab5..aff69fca 100644 --- a/internal/config/dir.go +++ b/internal/config/dir.go @@ -8,14 +8,17 @@ import ( "github.com/kirsle/configdir" ) -func dir() string { - return configdir.LocalConfig("coder") +var configRoot = configdir.LocalConfig("coder") + +// SetRoot overrides the package-level config root configuration. +func SetRoot(root string) { + configRoot = root } // open opens a file in the configuration directory, // creating all intermediate directories. func open(path string, flag int, mode os.FileMode) (*os.File, error) { - path = filepath.Join(dir(), path) + path = filepath.Join(configRoot, path) err := os.MkdirAll(filepath.Dir(path), 0750) if err != nil { @@ -45,5 +48,5 @@ func read(path string) ([]byte, error) { } func rm(path string) error { - return os.Remove(filepath.Join(dir(), path)) + return os.Remove(filepath.Join(configRoot, path)) } diff --git a/pkg/tablewriter/tablewriter.go b/pkg/tablewriter/tablewriter.go index 36a86bdf..9f12becd 100644 --- a/pkg/tablewriter/tablewriter.go +++ b/pkg/tablewriter/tablewriter.go @@ -3,7 +3,6 @@ package tablewriter import ( "fmt" "io" - "os" "reflect" "strings" "text/tabwriter" @@ -55,19 +54,16 @@ func StructFieldNames(data interface{}) string { return s.String() } -// The output io.Writer for WriteTable. This is globally defined to allow overriding in tests. -var tableOutput io.Writer = os.Stdout - // WriteTable writes the given list elements to stdout in a human readable // tabular format. Headers abide by the `table` struct tag. // // `table:"-"` omits the field and no tag defaults to the Go identifier. // `table:"_"` flattens a fields subfields. -func WriteTable(length int, each func(i int) interface{}) error { +func WriteTable(writer io.Writer, length int, each func(i int) interface{}) error { if length < 1 { return nil } - w := tabwriter.NewWriter(tableOutput, 0, 0, 4, ' ', 0) + w := tabwriter.NewWriter(writer, 0, 0, 4, ' ', 0) defer func() { _ = w.Flush() }() // Best effort. for ix := 0; ix < length; ix++ { item := each(ix) diff --git a/pkg/tablewriter/tablewriter_test.go b/pkg/tablewriter/tablewriter_test.go index 5318dd1e..e611e52c 100644 --- a/pkg/tablewriter/tablewriter_test.go +++ b/pkg/tablewriter/tablewriter_test.go @@ -49,8 +49,7 @@ func TestTableWriter(t *testing.T) { } buf := bytes.NewBuffer(nil) - tableOutput = buf - err := WriteTable(len(items), func(i int) interface{} { return items[i] }) + err := WriteTable(buf, len(items), func(i int) interface{} { return items[i] }) assert.Success(t, "write table", err) assertGolden(t, "table_output.golden", buf.Bytes())