Browse Source

libs/cli: clean up package (#7806)

pull/7811/head
Sam Kleinman 3 years ago
committed by GitHub
parent
commit
9e59fc6924
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 188 additions and 197 deletions
  1. +46
    -0
      cmd/tendermint/commands/completion.go
  2. +27
    -6
      cmd/tendermint/commands/root_test.go
  3. +2
    -2
      cmd/tendermint/main.go
  4. +22
    -92
      libs/cli/helper.go
  5. +14
    -84
      libs/cli/setup.go
  6. +77
    -13
      libs/cli/setup_test.go

+ 46
- 0
cmd/tendermint/commands/completion.go View File

@ -0,0 +1,46 @@
package commands
import (
"fmt"
"github.com/spf13/cobra"
)
// NewCompletionCmd returns a cobra.Command that generates bash and zsh
// completion scripts for the given root command. If hidden is true, the
// command will not show up in the root command's list of available commands.
func NewCompletionCmd(rootCmd *cobra.Command, hidden bool) *cobra.Command {
flagZsh := "zsh"
cmd := &cobra.Command{
Use: "completion",
Short: "Generate shell completion scripts",
Long: fmt.Sprintf(`Generate Bash and Zsh completion scripts and print them to STDOUT.
Once saved to file, a completion script can be loaded in the shell's
current session as shown:
$ . <(%s completion)
To configure your bash shell to load completions for each session add to
your $HOME/.bashrc or $HOME/.profile the following instruction:
. <(%s completion)
`, rootCmd.Use, rootCmd.Use),
RunE: func(cmd *cobra.Command, _ []string) error {
zsh, err := cmd.Flags().GetBool(flagZsh)
if err != nil {
return err
}
if zsh {
return rootCmd.GenZshCompletion(cmd.OutOrStdout())
}
return rootCmd.GenBashCompletion(cmd.OutOrStdout())
},
Hidden: hidden,
Args: cobra.NoArgs,
}
cmd.Flags().Bool(flagZsh, false, "Generate Zsh completion script")
return cmd
}

+ 27
- 6
cmd/tendermint/commands/root_test.go View File

@ -1,6 +1,7 @@
package commands package commands
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
@ -17,6 +18,17 @@ import (
tmos "github.com/tendermint/tendermint/libs/os" tmos "github.com/tendermint/tendermint/libs/os"
) )
// writeConfigVals writes a toml file with the given values.
// It returns an error if writing was impossible.
func writeConfigVals(dir string, vals map[string]string) error {
data := ""
for k, v := range vals {
data += fmt.Sprintf("%s = \"%s\"\n", k, v)
}
cfile := filepath.Join(dir, "config.toml")
return os.WriteFile(cfile, []byte(data), 0600)
}
// clearConfig clears env vars, the given root dir, and resets viper. // clearConfig clears env vars, the given root dir, and resets viper.
func clearConfig(t *testing.T, dir string) *cfg.Config { func clearConfig(t *testing.T, dir string) *cfg.Config {
t.Helper() t.Helper()
@ -41,7 +53,7 @@ func testRootCmd(conf *cfg.Config) *cobra.Command {
return cmd return cmd
} }
func testSetup(t *testing.T, conf *cfg.Config, args []string, env map[string]string) error {
func testSetup(ctx context.Context, t *testing.T, conf *cfg.Config, args []string, env map[string]string) error {
t.Helper() t.Helper()
cmd := testRootCmd(conf) cmd := testRootCmd(conf)
@ -49,7 +61,7 @@ func testSetup(t *testing.T, conf *cfg.Config, args []string, env map[string]str
// run with the args and env // run with the args and env
args = append([]string{cmd.Use}, args...) args = append([]string{cmd.Use}, args...)
return cli.RunWithArgs(cmd, args, env)
return cli.RunWithArgs(ctx, cmd, args, env)
} }
func TestRootHome(t *testing.T) { func TestRootHome(t *testing.T) {
@ -65,11 +77,14 @@ func TestRootHome(t *testing.T) {
{nil, map[string]string{"TMHOME": newRoot}, newRoot}, {nil, map[string]string{"TMHOME": newRoot}, newRoot},
} }
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
for i, tc := range cases { for i, tc := range cases {
t.Run(fmt.Sprint(i), func(t *testing.T) { t.Run(fmt.Sprint(i), func(t *testing.T) {
conf := clearConfig(t, tc.root) conf := clearConfig(t, tc.root)
err := testSetup(t, conf, tc.args, tc.env)
err := testSetup(ctx, t, conf, tc.args, tc.env)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, tc.root, conf.RootDir) require.Equal(t, tc.root, conf.RootDir)
@ -99,11 +114,14 @@ func TestRootFlagsEnv(t *testing.T) {
{nil, map[string]string{"TM_LOG_LEVEL": "debug"}, "debug"}, // right env {nil, map[string]string{"TM_LOG_LEVEL": "debug"}, "debug"}, // right env
} }
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
for i, tc := range cases { for i, tc := range cases {
t.Run(fmt.Sprint(i), func(t *testing.T) { t.Run(fmt.Sprint(i), func(t *testing.T) {
conf := clearConfig(t, defaultDir) conf := clearConfig(t, defaultDir)
err := testSetup(t, conf, tc.args, tc.env)
err := testSetup(ctx, t, conf, tc.args, tc.env)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, tc.logLevel, conf.LogLevel) assert.Equal(t, tc.logLevel, conf.LogLevel)
@ -113,6 +131,9 @@ func TestRootFlagsEnv(t *testing.T) {
} }
func TestRootConfig(t *testing.T) { func TestRootConfig(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// write non-default config // write non-default config
nonDefaultLogLvl := "debug" nonDefaultLogLvl := "debug"
cvals := map[string]string{ cvals := map[string]string{
@ -142,14 +163,14 @@ func TestRootConfig(t *testing.T) {
// write the non-defaults to a different path // write the non-defaults to a different path
// TODO: support writing sub configs so we can test that too // TODO: support writing sub configs so we can test that too
err = WriteConfigVals(configFilePath, cvals)
err = writeConfigVals(configFilePath, cvals)
require.NoError(t, err) require.NoError(t, err)
cmd := testRootCmd(conf) cmd := testRootCmd(conf)
// run with the args and env // run with the args and env
tc.args = append([]string{cmd.Use}, tc.args...) tc.args = append([]string{cmd.Use}, tc.args...)
err = cli.RunWithArgs(cmd, tc.args, tc.env)
err = cli.RunWithArgs(ctx, cmd, tc.args, tc.env)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, tc.logLvl, conf.LogLevel) require.Equal(t, tc.logLvl, conf.LogLevel)


+ 2
- 2
cmd/tendermint/main.go View File

@ -44,7 +44,7 @@ func main() {
commands.MakeRollbackStateCommand(conf), commands.MakeRollbackStateCommand(conf),
commands.MakeKeyMigrateCommand(conf, logger), commands.MakeKeyMigrateCommand(conf, logger),
debug.DebugCmd, debug.DebugCmd,
cli.NewCompletionCmd(rcmd, true),
commands.NewCompletionCmd(rcmd, true),
) )
// NOTE: // NOTE:
@ -60,7 +60,7 @@ func main() {
// Create & start node // Create & start node
rcmd.AddCommand(commands.NewRunNodeCmd(nodeFunc, conf, logger)) rcmd.AddCommand(commands.NewRunNodeCmd(nodeFunc, conf, logger))
if err := rcmd.ExecuteContext(ctx); err != nil {
if err := cli.RunWithTrace(ctx, rcmd); err != nil {
panic(err) panic(err)
} }
} }

+ 22
- 92
libs/cli/helper.go View File

@ -1,29 +1,20 @@
package cli package cli
import ( import (
"bytes"
"context"
"fmt" "fmt"
"io"
"os" "os"
"path/filepath"
"runtime"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper"
) )
// WriteConfigVals writes a toml file with the given values.
// It returns an error if writing was impossible.
func WriteConfigVals(dir string, vals map[string]string) error {
data := ""
for k, v := range vals {
data += fmt.Sprintf("%s = \"%s\"\n", k, v)
}
cfile := filepath.Join(dir, "config.toml")
return os.WriteFile(cfile, []byte(data), 0600)
}
// RunWithArgs executes the given command with the specified command line args // RunWithArgs executes the given command with the specified command line args
// and environmental variables set. It returns any error returned from cmd.Execute() // and environmental variables set. It returns any error returned from cmd.Execute()
func RunWithArgs(cmd Executable, args []string, env map[string]string) error {
//
// This is only used in testing.
func RunWithArgs(ctx context.Context, cmd *cobra.Command, args []string, env map[string]string) error {
oargs := os.Args oargs := os.Args
oenv := map[string]string{} oenv := map[string]string{}
// defer returns the environment back to normal // defer returns the environment back to normal
@ -46,85 +37,24 @@ func RunWithArgs(cmd Executable, args []string, env map[string]string) error {
} }
// and finally run the command // and finally run the command
return cmd.Execute()
return RunWithTrace(ctx, cmd)
} }
// RunCaptureWithArgs executes the given command with the specified command
// line args and environmental variables set. It returns string fields
// representing output written to stdout and stderr, additionally any error
// from cmd.Execute() is also returned
func RunCaptureWithArgs(cmd Executable, args []string, env map[string]string) (stdout, stderr string, err error) {
oldout, olderr := os.Stdout, os.Stderr // keep backup of the real stdout
rOut, wOut, _ := os.Pipe()
rErr, wErr, _ := os.Pipe()
os.Stdout, os.Stderr = wOut, wErr
defer func() {
os.Stdout, os.Stderr = oldout, olderr // restoring the real stdout
}()
// copy the output in a separate goroutine so printing can't block indefinitely
copyStd := func(reader *os.File) *(chan string) {
stdC := make(chan string)
go func() {
var buf bytes.Buffer
// io.Copy will end when we call reader.Close() below
io.Copy(&buf, reader) //nolint:errcheck //ignore error
select {
case <-cmd.Context().Done():
case stdC <- buf.String():
}
}()
return &stdC
}
outC := copyStd(rOut)
errC := copyStd(rErr)
// now run the command
err = RunWithArgs(cmd, args, env)
// and grab the stdout to return
wOut.Close()
wErr.Close()
stdout = <-*outC
stderr = <-*errC
return stdout, stderr, err
}
// NewCompletionCmd returns a cobra.Command that generates bash and zsh
// completion scripts for the given root command. If hidden is true, the
// command will not show up in the root command's list of available commands.
func NewCompletionCmd(rootCmd *cobra.Command, hidden bool) *cobra.Command {
flagZsh := "zsh"
cmd := &cobra.Command{
Use: "completion",
Short: "Generate shell completion scripts",
Long: fmt.Sprintf(`Generate Bash and Zsh completion scripts and print them to STDOUT.
Once saved to file, a completion script can be loaded in the shell's
current session as shown:
$ . <(%s completion)
To configure your bash shell to load completions for each session add to
your $HOME/.bashrc or $HOME/.profile the following instruction:
func RunWithTrace(ctx context.Context, cmd *cobra.Command) error {
cmd.SilenceUsage = true
cmd.SilenceErrors = true
if err := cmd.ExecuteContext(ctx); err != nil {
if viper.GetBool(TraceFlag) {
const size = 64 << 10
buf := make([]byte, size)
buf = buf[:runtime.Stack(buf, false)]
fmt.Fprintf(os.Stderr, "ERROR: %v\n%s\n", err, buf)
} else {
fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
}
. <(%s completion)
`, rootCmd.Use, rootCmd.Use),
RunE: func(cmd *cobra.Command, _ []string) error {
zsh, err := cmd.Flags().GetBool(flagZsh)
if err != nil {
return err
}
if zsh {
return rootCmd.GenZshCompletion(cmd.OutOrStdout())
}
return rootCmd.GenBashCompletion(cmd.OutOrStdout())
},
Hidden: hidden,
Args: cobra.NoArgs,
return err
} }
cmd.Flags().Bool(flagZsh, false, "Generate Zsh completion script")
return cmd
return nil
} }

+ 14
- 84
libs/cli/setup.go View File

@ -1,11 +1,8 @@
package cli package cli
import ( import (
"context"
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"strings" "strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -13,52 +10,27 @@ import (
) )
const ( const (
HomeFlag = "home"
TraceFlag = "trace"
OutputFlag = "output"
EncodingFlag = "encoding"
HomeFlag = "home"
TraceFlag = "trace"
OutputFlag = "output" // used in the cli
) )
// Executable is the minimal interface to *corba.Command, so we can
// wrap if desired before the test
type Executable interface {
Execute() error
Context() context.Context
}
// PrepareBaseCmd is meant for tendermint and other servers // PrepareBaseCmd is meant for tendermint and other servers
func PrepareBaseCmd(cmd *cobra.Command, envPrefix, defaultHome string) Executor {
func PrepareBaseCmd(cmd *cobra.Command, envPrefix, defaultHome string) *cobra.Command {
// the primary caller of this command is in the SDK and
// returning the cobra.Command object avoids breaking that
// code. In the long term, the SDK could avoid this entirely.
cobra.OnInitialize(func() { InitEnv(envPrefix) }) cobra.OnInitialize(func() { InitEnv(envPrefix) })
cmd.PersistentFlags().StringP(HomeFlag, "", defaultHome, "directory for config and data") cmd.PersistentFlags().StringP(HomeFlag, "", defaultHome, "directory for config and data")
cmd.PersistentFlags().Bool(TraceFlag, false, "print out full stack trace on errors") cmd.PersistentFlags().Bool(TraceFlag, false, "print out full stack trace on errors")
cmd.PersistentPreRunE = concatCobraCmdFuncs(BindFlagsLoadViper, cmd.PersistentPreRunE) cmd.PersistentPreRunE = concatCobraCmdFuncs(BindFlagsLoadViper, cmd.PersistentPreRunE)
return Executor{cmd, os.Exit}
}
// PrepareMainCmd is meant for client side libs that want some more flags
//
// This adds --encoding (hex, btc, base64) and --output (text, json) to
// the command. These only really make sense in interactive commands.
func PrepareMainCmd(cmd *cobra.Command, envPrefix, defaultHome string) Executor {
cmd.PersistentFlags().StringP(EncodingFlag, "e", "hex", "Binary encoding (hex|b64|btc)")
cmd.PersistentFlags().StringP(OutputFlag, "o", "text", "Output format (text|json)")
cmd.PersistentPreRunE = concatCobraCmdFuncs(validateOutput, cmd.PersistentPreRunE)
return PrepareBaseCmd(cmd, envPrefix, defaultHome)
return cmd
} }
// InitEnv sets to use ENV variables if set. // InitEnv sets to use ENV variables if set.
func InitEnv(prefix string) { func InitEnv(prefix string) {
copyEnvVars(prefix)
// env variables with TM prefix (eg. TM_ROOT)
viper.SetEnvPrefix(prefix)
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
viper.AutomaticEnv()
}
// This copies all variables like TMROOT to TM_ROOT,
// so we can support both formats for the user
func copyEnvVars(prefix string) {
// This copies all variables like TMROOT to TM_ROOT,
// so we can support both formats for the user
prefix = strings.ToUpper(prefix) prefix = strings.ToUpper(prefix)
ps := prefix + "_" ps := prefix + "_"
for _, e := range os.Environ() { for _, e := range os.Environ() {
@ -71,42 +43,11 @@ func copyEnvVars(prefix string) {
} }
} }
} }
}
// Executor wraps the cobra Command with a nicer Execute method
type Executor struct {
*cobra.Command
Exit func(int) // this is os.Exit by default, override in tests
}
type ExitCoder interface {
ExitCode() int
}
// execute adds all child commands to the root command sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func (e Executor) Execute() error {
e.SilenceUsage = true
e.SilenceErrors = true
err := e.Command.Execute()
if err != nil {
if viper.GetBool(TraceFlag) {
const size = 64 << 10
buf := make([]byte, size)
buf = buf[:runtime.Stack(buf, false)]
fmt.Fprintf(os.Stderr, "ERROR: %v\n%s\n", err, buf)
} else {
fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
}
// return error code 1 by default, can override it with a special error type
exitCode := 1
if ec, ok := err.(ExitCoder); ok {
exitCode = ec.ExitCode()
}
e.Exit(exitCode)
}
return err
// env variables with TM prefix (eg. TM_ROOT)
viper.SetEnvPrefix(prefix)
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
viper.AutomaticEnv()
} }
type cobraCmdFunc func(cmd *cobra.Command, args []string) error type cobraCmdFunc func(cmd *cobra.Command, args []string) error
@ -149,14 +90,3 @@ func BindFlagsLoadViper(cmd *cobra.Command, args []string) error {
} }
return nil return nil
} }
func validateOutput(cmd *cobra.Command, args []string) error {
// validate output format
output := viper.GetString(OutputFlag)
switch output {
case "text", "json":
default:
return fmt.Errorf("unsupported output format: %s", output)
}
return nil
}

+ 77
- 13
libs/cli/setup_test.go View File

@ -1,8 +1,12 @@
package cli package cli
import ( import (
"bytes"
"context"
"fmt" "fmt"
"io"
"os" "os"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
"testing" "testing"
@ -14,6 +18,9 @@ import (
) )
func TestSetupEnv(t *testing.T) { func TestSetupEnv(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
cases := []struct { cases := []struct {
args []string args []string
env map[string]string env map[string]string
@ -44,11 +51,10 @@ func TestSetupEnv(t *testing.T) {
} }
demo.Flags().String("foobar", "", "Some test value from config") demo.Flags().String("foobar", "", "Some test value from config")
cmd := PrepareBaseCmd(demo, "DEMO", "/qwerty/asdfgh") // some missing dir.. cmd := PrepareBaseCmd(demo, "DEMO", "/qwerty/asdfgh") // some missing dir..
cmd.Exit = func(int) {}
viper.Reset() viper.Reset()
args := append([]string{cmd.Use}, tc.args...) args := append([]string{cmd.Use}, tc.args...)
err := RunWithArgs(cmd, args, tc.env)
err := RunWithArgs(ctx, cmd, args, tc.env)
require.NoError(t, err, i) require.NoError(t, err, i)
assert.Equal(t, tc.expected, foo, i) assert.Equal(t, tc.expected, foo, i)
} }
@ -61,12 +67,27 @@ func tempDir(t *testing.T) string {
return cdir return cdir
} }
// writeConfigVals writes a toml file with the given values.
// It returns an error if writing was impossible.
func writeConfigVals(dir string, vals map[string]string) error {
lines := make([]string, 0, len(vals))
for k, v := range vals {
lines = append(lines, fmt.Sprintf("%s = %q", k, v))
}
data := strings.Join(lines, "\n")
cfile := filepath.Join(dir, "config.toml")
return os.WriteFile(cfile, []byte(data), 0600)
}
func TestSetupConfig(t *testing.T) { func TestSetupConfig(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// we pre-create two config files we can refer to in the rest of // we pre-create two config files we can refer to in the rest of
// the test cases. // the test cases.
cval1 := "fubble" cval1 := "fubble"
conf1 := tempDir(t) conf1 := tempDir(t)
err := WriteConfigVals(conf1, map[string]string{"boo": cval1})
err := writeConfigVals(conf1, map[string]string{"boo": cval1})
require.NoError(t, err) require.NoError(t, err)
cases := []struct { cases := []struct {
@ -103,11 +124,10 @@ func TestSetupConfig(t *testing.T) {
boo.Flags().String("boo", "", "Some test value from config") boo.Flags().String("boo", "", "Some test value from config")
boo.Flags().String("two-words", "", "Check out env handling -") boo.Flags().String("two-words", "", "Check out env handling -")
cmd := PrepareBaseCmd(boo, "RD", "/qwerty/asdfgh") // some missing dir... cmd := PrepareBaseCmd(boo, "RD", "/qwerty/asdfgh") // some missing dir...
cmd.Exit = func(int) {}
viper.Reset() viper.Reset()
args := append([]string{cmd.Use}, tc.args...) args := append([]string{cmd.Use}, tc.args...)
err := RunWithArgs(cmd, args, tc.env)
err := RunWithArgs(ctx, cmd, args, tc.env)
require.NoError(t, err, i) require.NoError(t, err, i)
assert.Equal(t, tc.expected, foo, i) assert.Equal(t, tc.expected, foo, i)
assert.Equal(t, tc.expectedTwo, two, i) assert.Equal(t, tc.expectedTwo, two, i)
@ -121,15 +141,18 @@ type DemoConfig struct {
} }
func TestSetupUnmarshal(t *testing.T) { func TestSetupUnmarshal(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// we pre-create two config files we can refer to in the rest of // we pre-create two config files we can refer to in the rest of
// the test cases. // the test cases.
cval1, cval2 := "someone", "else" cval1, cval2 := "someone", "else"
conf1 := tempDir(t) conf1 := tempDir(t)
err := WriteConfigVals(conf1, map[string]string{"name": cval1})
err := writeConfigVals(conf1, map[string]string{"name": cval1})
require.NoError(t, err) require.NoError(t, err)
// even with some ignored fields, should be no problem // even with some ignored fields, should be no problem
conf2 := tempDir(t) conf2 := tempDir(t)
err = WriteConfigVals(conf2, map[string]string{"name": cval2, "foo": "bar"})
err = writeConfigVals(conf2, map[string]string{"name": cval2, "foo": "bar"})
require.NoError(t, err) require.NoError(t, err)
// unused is not declared on a flag and remains from base // unused is not declared on a flag and remains from base
@ -182,17 +205,19 @@ func TestSetupUnmarshal(t *testing.T) {
// from the default config here // from the default config here
marsh.Flags().Int("age", base.Age, "Some test value from config") marsh.Flags().Int("age", base.Age, "Some test value from config")
cmd := PrepareBaseCmd(marsh, "MR", "/qwerty/asdfgh") // some missing dir... cmd := PrepareBaseCmd(marsh, "MR", "/qwerty/asdfgh") // some missing dir...
cmd.Exit = func(int) {}
viper.Reset() viper.Reset()
args := append([]string{cmd.Use}, tc.args...) args := append([]string{cmd.Use}, tc.args...)
err := RunWithArgs(cmd, args, tc.env)
err := RunWithArgs(ctx, cmd, args, tc.env)
require.NoError(t, err, i) require.NoError(t, err, i)
assert.Equal(t, tc.expected, cfg, i) assert.Equal(t, tc.expected, cfg, i)
} }
} }
func TestSetupTrace(t *testing.T) { func TestSetupTrace(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
cases := []struct { cases := []struct {
args []string args []string
env map[string]string env map[string]string
@ -215,18 +240,16 @@ func TestSetupTrace(t *testing.T) {
}, },
} }
cmd := PrepareBaseCmd(trace, "DBG", "/qwerty/asdfgh") // some missing dir.. cmd := PrepareBaseCmd(trace, "DBG", "/qwerty/asdfgh") // some missing dir..
cmd.Exit = func(int) {}
viper.Reset() viper.Reset()
args := append([]string{cmd.Use}, tc.args...) args := append([]string{cmd.Use}, tc.args...)
stdout, stderr, err := RunCaptureWithArgs(cmd, args, tc.env)
stdout, stderr, err := runCaptureWithArgs(ctx, cmd, args, tc.env)
require.Error(t, err, i) require.Error(t, err, i)
require.Equal(t, "", stdout, i) require.Equal(t, "", stdout, i)
require.NotEqual(t, "", stderr, i) require.NotEqual(t, "", stderr, i)
msg := strings.Split(stderr, "\n") msg := strings.Split(stderr, "\n")
desired := fmt.Sprintf("ERROR: %s", tc.expected) desired := fmt.Sprintf("ERROR: %s", tc.expected)
assert.Equal(t, desired, msg[0], i)
t.Log(msg)
assert.Equal(t, desired, msg[0], i, msg)
if tc.long && assert.True(t, len(msg) > 2, i) { if tc.long && assert.True(t, len(msg) > 2, i) {
// the next line starts the stack trace... // the next line starts the stack trace...
assert.Contains(t, stderr, "TestSetupTrace", i) assert.Contains(t, stderr, "TestSetupTrace", i)
@ -234,3 +257,44 @@ func TestSetupTrace(t *testing.T) {
} }
} }
} }
// runCaptureWithArgs executes the given command with the specified command
// line args and environmental variables set. It returns string fields
// representing output written to stdout and stderr, additionally any error
// from cmd.Execute() is also returned
func runCaptureWithArgs(ctx context.Context, cmd *cobra.Command, args []string, env map[string]string) (stdout, stderr string, err error) {
oldout, olderr := os.Stdout, os.Stderr // keep backup of the real stdout
rOut, wOut, _ := os.Pipe()
rErr, wErr, _ := os.Pipe()
os.Stdout, os.Stderr = wOut, wErr
defer func() {
os.Stdout, os.Stderr = oldout, olderr // restoring the real stdout
}()
// copy the output in a separate goroutine so printing can't block indefinitely
copyStd := func(reader *os.File) *(chan string) {
stdC := make(chan string)
go func() {
var buf bytes.Buffer
// io.Copy will end when we call reader.Close() below
io.Copy(&buf, reader) //nolint:errcheck //ignore error
select {
case <-cmd.Context().Done():
case stdC <- buf.String():
}
}()
return &stdC
}
outC := copyStd(rOut)
errC := copyStd(rErr)
// now run the command
err = RunWithArgs(ctx, cmd, args, env)
// and grab the stdout to return
wOut.Close()
wErr.Close()
stdout = <-*outC
stderr = <-*errC
return stdout, stderr, err
}

Loading…
Cancel
Save