diff --git a/e2e/app/rpc.go b/e2e/app/rpc.go index 06baa8410..8ae661f66 100644 --- a/e2e/app/rpc.go +++ b/e2e/app/rpc.go @@ -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 } diff --git a/halo/cmd/cmd_internal_test.go b/halo/cmd/cmd_internal_test.go index 667b9573b..ff7be2bfa 100644 --- a/halo/cmd/cmd_internal_test.go +++ b/halo/cmd/cmd_internal_test.go @@ -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 diff --git a/halo/cmd/testdata/TestCLIReference_halo.golden b/halo/cmd/testdata/TestCLIReference_halo.golden index c824390f7..fe6074db6 100644 --- a/halo/cmd/testdata/TestCLIReference_halo.golden +++ b/halo/cmd/testdata/TestCLIReference_halo.golden @@ -1,6 +1,7 @@ Halo is a consensus client implementation for the Omni Protocol Usage: + halo [flags] halo [command] Available Commands: diff --git a/lib/cmd/cmd.go b/lib/cmd/cmd.go index cc13748ad..2613b6be0 100644 --- a/lib/cmd/cmd.go +++ b/lib/cmd/cmd.go @@ -20,24 +20,20 @@ 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. } @@ -45,7 +41,6 @@ func Main(cmd *cobra.Command) { // 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, @@ -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...) @@ -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 +}