Add --debug flag to return full stack trace on error

This commit is contained in:
Ethan Frey
2017-05-03 11:25:07 +02:00
committed by Ethan Buchman
parent ef3b9610a1
commit 8efeeb5f38
2 changed files with 80 additions and 20 deletions

View File

@ -16,25 +16,36 @@ import (
const ( const (
RootFlag = "root" RootFlag = "root"
HomeFlag = "home" HomeFlag = "home"
DebugFlag = "debug"
OutputFlag = "output" OutputFlag = "output"
EncodingFlag = "encoding" EncodingFlag = "encoding"
) )
// Executable is the minimal interface to *corba.Command, so we can
// wrap if desired before the test
type Executable interface {
Execute() error
}
// PrepareBaseCmd is meant for tendermint and other servers // PrepareBaseCmd is meant for tendermint and other servers
func PrepareBaseCmd(cmd *cobra.Command, envPrefix, defautRoot string) func() { func PrepareBaseCmd(cmd *cobra.Command, envPrefix, defautRoot string) Executor {
cobra.OnInitialize(func() { initEnv(envPrefix) }) cobra.OnInitialize(func() { initEnv(envPrefix) })
cmd.PersistentFlags().StringP(RootFlag, "r", defautRoot, "DEPRECATED. Use --home") cmd.PersistentFlags().StringP(RootFlag, "r", defautRoot, "DEPRECATED. Use --home")
// -h is already reserved for --help as part of the cobra framework // -h is already reserved for --help as part of the cobra framework
// do you want to try something else??
// also, default must be empty, so we can detect this unset and fall back
// to --root / TM_ROOT / TMROOT
cmd.PersistentFlags().String(HomeFlag, "", "root directory for config and data") cmd.PersistentFlags().String(HomeFlag, "", "root directory for config and data")
cmd.PersistentFlags().Bool(DebugFlag, false, "print out full stack trace on errors")
cmd.PersistentPreRunE = concatCobraCmdFuncs(bindFlagsLoadViper, cmd.PersistentPreRunE) cmd.PersistentPreRunE = concatCobraCmdFuncs(bindFlagsLoadViper, cmd.PersistentPreRunE)
return func() { execute(cmd) } return Executor{cmd}
} }
// PrepareMainCmd is meant for client side libs that want some more flags // PrepareMainCmd is meant for client side libs that want some more flags
// //
// This adds --encoding (hex, btc, base64) and --output (text, json) to // This adds --encoding (hex, btc, base64) and --output (text, json) to
// the command. These only really make sense in interactive commands. // the command. These only really make sense in interactive commands.
func PrepareMainCmd(cmd *cobra.Command, envPrefix, defautRoot string) func() { func PrepareMainCmd(cmd *cobra.Command, envPrefix, defautRoot string) Executor {
cmd.PersistentFlags().StringP(EncodingFlag, "e", "hex", "Binary encoding (hex|b64|btc)") cmd.PersistentFlags().StringP(EncodingFlag, "e", "hex", "Binary encoding (hex|b64|btc)")
cmd.PersistentFlags().StringP(OutputFlag, "o", "text", "Output format (text|json)") cmd.PersistentFlags().StringP(OutputFlag, "o", "text", "Output format (text|json)")
cmd.PersistentPreRunE = concatCobraCmdFuncs(setEncoding, validateOutput, cmd.PersistentPreRunE) cmd.PersistentPreRunE = concatCobraCmdFuncs(setEncoding, validateOutput, cmd.PersistentPreRunE)
@ -68,15 +79,27 @@ func copyEnvVars(prefix string) {
} }
} }
// Executor wraps the cobra Command with a nicer Execute method
type Executor struct {
*cobra.Command
}
// execute adds all child commands to the root command sets flags appropriately. // 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. // This is called by main.main(). It only needs to happen once to the rootCmd.
func execute(cmd *cobra.Command) { func (e Executor) Execute() error {
// TODO: this can do something cooler with debug and log-levels e.SilenceUsage = true
if err := cmd.Execute(); err != nil { e.SilenceErrors = true
fmt.Println(err) err := e.Command.Execute()
os.Exit(-1) if err != nil {
// TODO: something cooler with log-levels
if viper.GetBool(DebugFlag) {
fmt.Printf("ERROR: %+v\n", err)
} else {
fmt.Println("ERROR:", err.Error())
} }
} }
return err
}
type cobraCmdFunc func(cmd *cobra.Command, args []string) error type cobraCmdFunc func(cmd *cobra.Command, args []string) error

View File

@ -8,20 +8,16 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings"
"testing" "testing"
"github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
// Executable is the minimal interface to *corba.Command, so we can
// wrap if desired before the test
type Executable interface {
Execute() error
}
func TestSetupEnv(t *testing.T) { func TestSetupEnv(t *testing.T) {
assert, require := assert.New(t), require.New(t) assert, require := assert.New(t), require.New(t)
@ -46,15 +42,15 @@ func TestSetupEnv(t *testing.T) {
i := strconv.Itoa(idx) i := strconv.Itoa(idx)
// test command that store value of foobar in local variable // test command that store value of foobar in local variable
var foo string var foo string
cmd := &cobra.Command{ demo := &cobra.Command{
Use: "demo", Use: "demo",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
foo = viper.GetString("foobar") foo = viper.GetString("foobar")
return nil return nil
}, },
} }
cmd.Flags().String("foobar", "", "Some test value from config") demo.Flags().String("foobar", "", "Some test value from config")
PrepareBaseCmd(cmd, "DEMO", "/qwerty/asdfgh") // some missing dir.. cmd := PrepareBaseCmd(demo, "DEMO", "/qwerty/asdfgh") // some missing dir..
viper.Reset() viper.Reset()
args := append([]string{cmd.Use}, tc.args...) args := append([]string{cmd.Use}, tc.args...)
@ -112,15 +108,15 @@ func TestSetupConfig(t *testing.T) {
i := strconv.Itoa(idx) i := strconv.Itoa(idx)
// test command that store value of foobar in local variable // test command that store value of foobar in local variable
var foo string var foo string
cmd := &cobra.Command{ boo := &cobra.Command{
Use: "reader", Use: "reader",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
foo = viper.GetString("boo") foo = viper.GetString("boo")
return nil return nil
}, },
} }
cmd.Flags().String("boo", "", "Some test value from config") boo.Flags().String("boo", "", "Some test value from config")
PrepareBaseCmd(cmd, "RD", "/qwerty/asdfgh") // some missing dir... cmd := PrepareBaseCmd(boo, "RD", "/qwerty/asdfgh") // some missing dir...
viper.Reset() viper.Reset()
args := append([]string{cmd.Use}, tc.args...) args := append([]string{cmd.Use}, tc.args...)
@ -130,6 +126,47 @@ func TestSetupConfig(t *testing.T) {
} }
} }
func TestSetupDebug(t *testing.T) {
assert, require := assert.New(t), require.New(t)
cases := []struct {
args []string
env map[string]string
long bool
expected string
}{
{nil, nil, false, "Debug flag = false"},
{[]string{"--debug"}, nil, true, "Debug flag = true"},
{[]string{"--no-such-flag"}, nil, false, "unknown flag: --no-such-flag"},
{nil, map[string]string{"DBG_DEBUG": "true"}, true, "Debug flag = true"},
}
for idx, tc := range cases {
i := strconv.Itoa(idx)
// test command that store value of foobar in local variable
debug := &cobra.Command{
Use: "debug",
RunE: func(cmd *cobra.Command, args []string) error {
return errors.Errorf("Debug flag = %t", viper.GetBool(DebugFlag))
},
}
cmd := PrepareBaseCmd(debug, "DBG", "/qwerty/asdfgh") // some missing dir..
viper.Reset()
args := append([]string{cmd.Use}, tc.args...)
out, err := runCaptureWithArgs(cmd, args, tc.env)
require.NotNil(err, i)
msg := strings.Split(out, "\n")
desired := fmt.Sprintf("ERROR: %s", tc.expected)
assert.Equal(desired, msg[0], i)
if tc.long && assert.True(len(msg) > 2, i) {
// the next line starts the stack trace...
assert.Contains(msg[1], "TestSetupDebug", i)
assert.Contains(msg[2], "setup_test.go", i)
}
}
}
// 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 { func runWithArgs(cmd Executable, args []string, env map[string]string) error {