Skip to content

Commit

Permalink
chore(lib/cmd): improve invalid command errors (#2054)
Browse files Browse the repository at this point in the history
Improve binary errors when subcommands are invalid or not specified,
reverting to default cobra behaviour.

issue: none

---------

Co-authored-by: Gregor G. <[email protected]>
  • Loading branch information
corverroos and sideninja authored Oct 3, 2024
1 parent b3e951e commit 5aee7a6
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 10 deletions.
4 changes: 2 additions & 2 deletions e2e/app/rpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ func waitForHeight(ctx context.Context, testnet *e2e.Testnet, height int64) (*ty
}

result, err := currentBlock(ctx, client)
if errors.Is(err, context.DeadlineExceeded) {
return nil, nil, errors.Wrap(err, "timeout")
if ctx.Err() != nil {
return nil, nil, errors.Wrap(err, "parent context canceled")
} else if err != nil {
continue
}
Expand Down
31 changes: 31 additions & 0 deletions halo/cmd/cmd_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,37 @@ func TestTomlConfig(t *testing.T) {
tutil.RequireNoError(t, rootCmd.Execute())
}

func TestInvalidCmds(t *testing.T) {
t.Parallel()
tests := []struct {
Name string
Args []string
ErrContains string
}{
{
Name: "no args",
Args: []string{},
ErrContains: "no sub-command specified, see --help",
},
{
Name: "invalid args",
Args: []string{"invalid"},
ErrContains: "unknown command \"invalid\" for \"halo\"",
},
}

for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
t.Parallel()
rootCmd := libcmd.NewRootCmd("halo", "", New())
rootCmd.SetArgs(test.Args)
err := rootCmd.Execute()
require.Error(t, err)
require.Contains(t, err.Error(), test.ErrContains)
})
}
}

// slice is a convenience function for creating string slice literals.
func slice(strs ...string) []string {
return strs
Expand Down
1 change: 1 addition & 0 deletions halo/cmd/testdata/TestCLIReference_halo.golden
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
Halo is a consensus client implementation for the Omni Protocol

Usage:
halo [flags]
halo [command]

Available Commands:
Expand Down
46 changes: 38 additions & 8 deletions lib/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,32 +20,27 @@ import (
"github.com/spf13/viper"
)

// Main is the main entry point for the command line tool.
// Main is the main entry point for the omni application binaries.
// Usage:
//
// func main() {
// libcmd.Main(appcmd.New())
// }
func Main(cmd *cobra.Command) {
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)

SilenceErrUsage(cmd)
wrapRunCmd(cmd)

ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
err := cmd.ExecuteContext(ctx)

cancel()

if err != nil {
log.Error(ctx, "!! Fatal error occurred, app died unexpectedly !!", err)

const errExitCode = 1
os.Exit(errExitCode) //nolint:revive // Deep exit is exactly the point of this helper function.
}
}

// NewRootCmd returns a new root cobra command that handles our command line tool.
// It sets up the general viper config and binds the cobra flags to the viper flags.
// It also silences the usage printing when commands error during "running".
func NewRootCmd(appName string, appDescription string, subCmds ...*cobra.Command) *cobra.Command {
root := &cobra.Command{
Use: appName,
Expand All @@ -54,6 +49,10 @@ func NewRootCmd(appName string, appDescription string, subCmds ...*cobra.Command
PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
return initializeConfig(appName, cmd)
},
RunE: func(*cobra.Command, []string) error {
// Callers should either add sub-commands or override RunE.
return errors.New("no sub-command specified, see --help")
},
}

root.AddCommand(subCmds...)
Expand Down Expand Up @@ -168,3 +167,34 @@ func bindFlags(cmd *cobra.Command, v *viper.Viper) error {

return lastErr
}

// wrapRunCmd wraps the "app run" command to custom fatal error log and silence cobra output.
func wrapRunCmd(cmd *cobra.Command) {
runCmd := getRunCmd(cmd)
SilenceErrUsage(runCmd)
runFunc := runCmd.RunE
runCmd.RunE = func(cmd *cobra.Command, args []string) error {
if runFunc == nil {
return errors.New("run command RunE nil [BUG]")
}

err := runFunc(cmd, args)
if err != nil {
log.Error(cmd.Context(), "!! Fatal error occurred, app died !!", err)
}

return err
}
}

// getRunCmd returns the "run" subcommand of the given command or the command itself.
func getRunCmd(cmd *cobra.Command) *cobra.Command {
const name = "run"
for _, sub := range cmd.Commands() {
if sub.Use == name {
return sub
}
}

return cmd
}

0 comments on commit 5aee7a6

Please sign in to comment.