Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,15 @@ func NewCommand() (*cobra.Command, error) {
// exits, so Cobra must not also print them.
SilenceUsage: true,
SilenceErrors: true,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
clientOpts.RequestContext = cmd.Context()
if err := cmd.ValidateRequiredFlags(); err != nil {
return wrapUsageError(cmd, err)
}
if err := cmd.ValidateFlagGroups(); err != nil {
return wrapUsageError(cmd, err)
}
return nil
},
Run: func(cmd *cobra.Command, args []string) {
cmd.HelpFunc()(cmd, args)
Expand Down Expand Up @@ -70,5 +77,7 @@ func NewCommand() (*cobra.Command, error) {
command.PersistentFlags().StringVar(&clientOpts.ServerAddr, "microcksURL", "", "Microcks API URL")
command.MarkFlagsRequiredTogether("keycloakClientId", "keycloakClientSecret")

setupUsageErrors(command)

return command, nil
}
101 changes: 101 additions & 0 deletions cmd/exit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,104 @@ func TestUsageErrorIsKindUsage(t *testing.T) {
t.Fatalf("KindOf = %v, want KindUsage", got)
}
}

func TestCobraSyntaxAndArgumentErrorsReturnKindUsage(t *testing.T) {
cases := []struct {
name string
args []string
wantErrSubstring string
wantUsageSnippet string
}{
{
name: "unknown flag on root",
args: []string{"--unknown-flag"},
wantErrSubstring: "unknown flag: --unknown-flag",
wantUsageSnippet: "microcks [command]",
},
{
name: "unknown flag on subcommand",
args: []string{"test", "--unknown-flag"},
wantErrSubstring: "unknown flag: --unknown-flag",
wantUsageSnippet: "microcks test",
},
{
name: "missing flag argument",
args: []string{"start", "--port"},
wantErrSubstring: "flag needs an argument: --port",
wantUsageSnippet: "microcks start",
},
{
name: "missing required positional arguments",
args: []string{"test"},
wantErrSubstring: "accepts 3 arg(s), received 0",
wantUsageSnippet: "microcks test",
},
{
name: "required together flags missing",
args: []string{"--keycloakClientId=test"},
wantErrSubstring: "missing [keycloakClientSecret]",
wantUsageSnippet: "microcks [command]",
},
{
name: "unknown command",
args: []string{"non-existent-command"},
wantErrSubstring: `unknown command "non-existent-command" for "microcks"`,
wantUsageSnippet: "microcks [command]",
},
}

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
cmd, err := NewCommand()
if err != nil {
t.Fatalf("NewCommand returned error: %v", err)
}
cmd.SetArgs(c.args)

execErr := cmd.Execute()
if execErr == nil {
t.Fatalf("expected error for args %v, got nil", c.args)
}

if gotKind := errors.KindOf(execErr); gotKind != errors.KindUsage {
t.Errorf("KindOf = %v, want KindUsage", gotKind)
}

if gotCode := ExitCodeFor(execErr); gotCode != 2 {
t.Errorf("ExitCodeFor = %d, want 2", gotCode)
}

var out bytes.Buffer
printError(&out, execErr)
gotOutput := out.String()

if !strings.Contains(gotOutput, "Usage:") {
t.Errorf("printError output missing 'Usage:': %q", gotOutput)
}
if !strings.Contains(gotOutput, c.wantUsageSnippet) {
t.Errorf("printError output missing snippet %q: %q", c.wantUsageSnippet, gotOutput)
}
if !strings.Contains(gotOutput, c.wantErrSubstring) {
t.Errorf("printError output missing error substring %q: %q", c.wantErrSubstring, gotOutput)
}
})
}
}

func TestWrapUsageError(t *testing.T) {
if got := wrapUsageError(nil, nil); got != nil {
t.Fatalf("wrapUsageError(nil, nil) = %v, want nil", got)
}

origErr := stderrors.New("some error")
wrapped := wrapUsageError(nil, origErr)
if gotKind := errors.KindOf(wrapped); gotKind != errors.KindUsage {
t.Fatalf("KindOf = %v, want KindUsage", gotKind)
}

// Double wrapping should return existing *usageError
doubleWrapped := wrapUsageError(nil, wrapped)
if doubleWrapped != wrapped {
t.Fatalf("expected idempotent wrapping, got %v", doubleWrapped)
}
}
71 changes: 71 additions & 0 deletions cmd/usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
package cmd

import (
stderrors "errors"
"fmt"
"strings"

"github.com/microcks/microcks-cli/pkg/errors"
"github.com/spf13/cobra"
)
Expand All @@ -38,3 +42,70 @@ func usageErrorf(cmd *cobra.Command, format string, a ...any) error {
usage: usage,
}
}

func wrapUsageError(cmd *cobra.Command, err error) error {
if err == nil {
return nil
}
var ue *usageError
if stderrors.As(err, &ue) {
return err
}
usage := ""
if cmd != nil {
usage = cmd.UsageString()
}
return &usageError{
err: errors.Wrap(errors.KindUsage, err),
usage: usage,
}
}

func unknownCommandError(cmd *cobra.Command, arg string) error {
var sb strings.Builder
if cmd != nil {
if suggestions := cmd.SuggestionsFor(arg); len(suggestions) > 0 {
sb.WriteString("\n\nDid you mean this?\n")
for _, s := range suggestions {
fmt.Fprintf(&sb, "\t%v\n", s)
}
}
}
cmdPath := "microcks"
if cmd != nil {
cmdPath = cmd.CommandPath()
}
return wrapUsageError(cmd, fmt.Errorf("unknown command %q for %q%s", arg, cmdPath, sb.String()))
}

func setupUsageErrors(root *cobra.Command) {
// Child commands inherit FlagErrorFunc from the root command in Cobra.
root.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error {
return wrapUsageError(cmd, err)
})

wrapArgs(root)
}

func wrapArgs(c *cobra.Command) {
if c.Args != nil {
origArgs := c.Args
c.Args = func(cmd *cobra.Command, args []string) error {
if err := origArgs(cmd, args); err != nil {
return wrapUsageError(cmd, err)
}
return nil
}
} else if !c.HasParent() {
c.Args = func(cmd *cobra.Command, args []string) error {
if len(args) > 0 {
return unknownCommandError(cmd, args[0])
}
return nil
}
}

for _, child := range c.Commands() {
wrapArgs(child)
}
}
Loading