Preserve alias context in usage errors
This commit is contained in:
159
route_test.go
159
route_test.go
@@ -3,6 +3,8 @@ package commandkit
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
@@ -316,6 +318,163 @@ func TestExecuteAppliesCanonicalCommandFlagAliasesAfterRouting(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAliasConversionErrorsRetainOccurrenceAndEnteredUsage(t *testing.T) {
|
||||
colorFailure := errors.New("invalid color")
|
||||
tagFailure := errors.New("invalid tag")
|
||||
type input struct {
|
||||
color Supplied[string]
|
||||
tags Supplied[[]string]
|
||||
}
|
||||
command := NewCommand(CommandSpec[input]{
|
||||
Name: "inspect",
|
||||
Flags: []Flag[input]{
|
||||
ValueFlag(FlagMeta{Name: "color"}, (*string)(nil), func(value string) (string, error) {
|
||||
if value == "bad" {
|
||||
return "", colorFailure
|
||||
}
|
||||
return value, nil
|
||||
}, func(target *input, value Supplied[string]) { target.color = value }),
|
||||
RepeatFlag(FlagMeta{Name: "tag"}, nil, func(value string) (string, error) {
|
||||
if value == "bad" {
|
||||
return "", tagFailure
|
||||
}
|
||||
return value, nil
|
||||
}, func(target *input, value Supplied[[]string]) { target.tags = value }),
|
||||
},
|
||||
Run: func(Invocation, input) Outcome { return Success() },
|
||||
})
|
||||
app, err := New(Identity{Name: "Example"}, command,
|
||||
Alias("shortcut", "inspect"),
|
||||
FlagAliasFor("inspect", "--colour", "--color"),
|
||||
FlagAliasFor("inspect", "--labels", "--tag"),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
args []string
|
||||
cause error
|
||||
binding string
|
||||
spelling string
|
||||
occurrence int
|
||||
}{
|
||||
{
|
||||
name: "invalid scalar before valid overwrite",
|
||||
args: []string{"shortcut", "--colour=bad", "--color=good"},
|
||||
cause: colorFailure,
|
||||
binding: "color",
|
||||
spelling: "--colour",
|
||||
occurrence: 1,
|
||||
},
|
||||
{
|
||||
name: "invalid repeat occurrence after mixed spelling",
|
||||
args: []string{"shortcut", "--labels=first", "--tag=bad"},
|
||||
cause: tagFailure,
|
||||
binding: "tag",
|
||||
spelling: "--tag",
|
||||
occurrence: 2,
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
var diagnostics bytes.Buffer
|
||||
outcome := app.Execute(context.Background(), test.args, Streams{Out: io.Discard, Err: &diagnostics})
|
||||
if outcome.Kind != OutcomeUsage {
|
||||
t.Fatalf("outcome kind = %v, want usage", outcome.Kind)
|
||||
}
|
||||
if !errors.Is(outcome.Cause, test.cause) {
|
||||
t.Fatalf("outcome cause = %v, want wrapped %v", outcome.Cause, test.cause)
|
||||
}
|
||||
var conversionErr *flagConversionError
|
||||
if !errors.As(outcome.Cause, &conversionErr) {
|
||||
t.Fatalf("outcome cause %T does not expose flag conversion context", outcome.Cause)
|
||||
}
|
||||
if conversionErr.binding != test.binding || conversionErr.spelling != test.spelling || conversionErr.occurrence != test.occurrence {
|
||||
t.Fatalf("conversion error = %#v, want binding/spelling/occurrence for %s %d", conversionErr, test.spelling, test.occurrence)
|
||||
}
|
||||
for _, fragment := range []string{fmt.Sprintf("flag %s occurrence %d: %s", test.spelling, test.occurrence, test.cause), "Usage: Example shortcut", "Alias for: Example inspect"} {
|
||||
if !strings.Contains(diagnostics.String(), fragment) {
|
||||
t.Errorf("diagnostics %q do not contain %q", diagnostics.String(), fragment)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAliasUsageSurvivesRegistrationDuringExecution(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
register func(*App) error
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "successful registration",
|
||||
register: func(app *App) error {
|
||||
return app.Register(NewCommand(CommandSpec[struct{}]{Name: "late", Run: func(Invocation, struct{}) Outcome { return Success() }}))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "failed registration",
|
||||
register: func(app *App) error { return app.Register(Alias("shortcut", "other")) },
|
||||
wantErr: true,
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
var app *App
|
||||
var registrationErr error
|
||||
command := NewCommand(CommandSpec[struct{}]{
|
||||
Name: "inspect",
|
||||
Run: func(Invocation, struct{}) Outcome {
|
||||
registrationErr = test.register(app)
|
||||
return Usage(errors.New("operation rejected"))
|
||||
},
|
||||
})
|
||||
var err error
|
||||
app, err = New(Identity{Name: "Example"}, command, Alias("shortcut", "inspect"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var diagnostics bytes.Buffer
|
||||
outcome := app.Execute(context.Background(), []string{"shortcut"}, Streams{Out: io.Discard, Err: &diagnostics})
|
||||
if outcome.Kind != OutcomeUsage {
|
||||
t.Fatalf("outcome kind = %v, want usage", outcome.Kind)
|
||||
}
|
||||
if (registrationErr != nil) != test.wantErr {
|
||||
t.Fatalf("registration error = %v, want error %t", registrationErr, test.wantErr)
|
||||
}
|
||||
for _, fragment := range []string{"operation rejected", "Usage: Example shortcut", "Alias for: Example inspect"} {
|
||||
if !strings.Contains(diagnostics.String(), fragment) {
|
||||
t.Errorf("diagnostics %q do not contain %q", diagnostics.String(), fragment)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnresolvedRouteUsageHasNoCanonicalAliasAnnotation(t *testing.T) {
|
||||
app := routeTestApp(t)
|
||||
var diagnostics bytes.Buffer
|
||||
outcome := app.Execute(context.Background(), []string{"missing", "route"}, Streams{Out: io.Discard, Err: &diagnostics})
|
||||
if outcome.Kind != OutcomeUsage {
|
||||
t.Fatalf("outcome kind = %v, want usage", outcome.Kind)
|
||||
}
|
||||
if strings.Contains(diagnostics.String(), "Alias for:") {
|
||||
t.Fatalf("unresolved route diagnostics invented a canonical target: %q", diagnostics.String())
|
||||
}
|
||||
|
||||
diagnostics.Reset()
|
||||
outcome = app.Execute(context.Background(), []string{"r", "full", "--unknown"}, Streams{Out: io.Discard, Err: &diagnostics})
|
||||
if outcome.Kind != OutcomeUsage {
|
||||
t.Fatalf("unknown flag outcome kind = %v, want usage", outcome.Kind)
|
||||
}
|
||||
for _, fragment := range []string{"unknown flag", "Usage: Example r full", "Alias for: Example inspect"} {
|
||||
if !strings.Contains(diagnostics.String(), fragment) {
|
||||
t.Errorf("unknown flag diagnostics %q do not contain %q", diagnostics.String(), fragment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteAppliesGlobalHelpAliasAtApplicationRoot(t *testing.T) {
|
||||
app, err := New(Identity{Name: "Example"}, FlagAlias("--hlep", "--help"))
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user