Verify public alias registration flows
This commit is contained in:
@@ -1,6 +1,13 @@
|
||||
package commandkit_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
||||
@@ -29,3 +36,206 @@ func TestPublicInstallationAcceptsMixedDeclarations(t *testing.T) {
|
||||
t.Fatal("New() returned a nil app")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicAliasesPreserveCommandsAcrossDeclarationOrders(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
rulesFirst bool
|
||||
}{
|
||||
{name: "commands first"},
|
||||
{name: "routing rules first", rulesFirst: true},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
type runInput struct{ words commandkit.Supplied[[]string] }
|
||||
type inspectInput struct{ id commandkit.Supplied[string] }
|
||||
type showInput struct {
|
||||
profile commandkit.Supplied[string]
|
||||
format commandkit.Supplied[string]
|
||||
}
|
||||
var runParses, inspectParses, optionalParses int
|
||||
var runValues [][]string
|
||||
var inspectValues []string
|
||||
var showValues []string
|
||||
run := commandkit.NewCommand(commandkit.CommandSpec[runInput]{
|
||||
Name: "run",
|
||||
Arguments: []commandkit.Argument[runInput]{commandkit.RepeatArgument(commandkit.ArgumentMeta{Name: "words"}, 0, -1,
|
||||
func(value string) (string, error) { runParses++; return value, nil },
|
||||
func(target *runInput, value commandkit.Supplied[[]string]) { target.words = value },
|
||||
)},
|
||||
Run: func(_ commandkit.Invocation, value runInput) commandkit.Outcome {
|
||||
runValues = append(runValues, append([]string(nil), value.words.Value...))
|
||||
return commandkit.Success()
|
||||
},
|
||||
})
|
||||
inspect := commandkit.NewCommand(commandkit.CommandSpec[inspectInput]{
|
||||
Name: "inspect",
|
||||
Arguments: []commandkit.Argument[inspectInput]{commandkit.ValueArgument(commandkit.ArgumentMeta{Name: "id"}, true,
|
||||
func(value string) (string, error) { inspectParses++; return value, nil },
|
||||
func(target *inspectInput, value commandkit.Supplied[string]) { target.id = value },
|
||||
)},
|
||||
Run: func(_ commandkit.Invocation, value inspectInput) commandkit.Outcome {
|
||||
inspectValues = append(inspectValues, value.id.Value)
|
||||
return commandkit.Success()
|
||||
},
|
||||
})
|
||||
show := commandkit.NewCommand(commandkit.CommandSpec[showInput]{
|
||||
Name: "show",
|
||||
Arguments: []commandkit.Argument[showInput]{commandkit.ValueArgument(commandkit.ArgumentMeta{Name: "profile"}, false,
|
||||
func(value string) (string, error) { optionalParses++; return value, nil },
|
||||
func(target *showInput, value commandkit.Supplied[string]) { target.profile = value },
|
||||
)},
|
||||
Flags: []commandkit.Flag[showInput]{commandkit.ValueFlag(commandkit.FlagMeta{Name: "format"}, (*string)(nil),
|
||||
func(value string) (string, error) { return value, nil },
|
||||
func(target *showInput, value commandkit.Supplied[string]) { target.format = value },
|
||||
)},
|
||||
Run: func(_ commandkit.Invocation, value showInput) commandkit.Outcome {
|
||||
showValues = append(showValues, value.profile.Value+":"+value.format.Value)
|
||||
return commandkit.Success()
|
||||
},
|
||||
})
|
||||
commands := []commandkit.Declaration{run, inspect, show}
|
||||
rules := []commandkit.Declaration{
|
||||
commandkit.Alias("r", "run"),
|
||||
commandkit.Alias("r full", "inspect"),
|
||||
commandkit.Alias("inspect-shortcut", "inspect"),
|
||||
commandkit.Alias("i", "inspect-shortcut"),
|
||||
commandkit.FlagAlias("--fmt", "--format"),
|
||||
}
|
||||
declarations := append([]commandkit.Declaration(nil), commands...)
|
||||
if test.rulesFirst {
|
||||
declarations = append(append([]commandkit.Declaration(nil), rules...), commands...)
|
||||
} else {
|
||||
declarations = append(declarations, rules...)
|
||||
}
|
||||
|
||||
app, err := commandkit.New(commandkit.Identity{Name: "Example"}, declarations...)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if runParses != 0 || inspectParses != 0 || optionalParses != 0 {
|
||||
t.Fatalf("installation invoked operand converters: %d/%d/%d", runParses, inspectParses, optionalParses)
|
||||
}
|
||||
for _, args := range [][]string{{"run", "full"}, {"r", "full", "item-1"}, {"i", "item-2"}, {"show", "profile", "--fmt=json"}} {
|
||||
if outcome := app.Execute(context.Background(), args, commandkit.Streams{Out: io.Discard, Err: io.Discard}); outcome.Kind != commandkit.OutcomeSuccess {
|
||||
t.Fatalf("Execute(%q) = %#v, want success", args, outcome)
|
||||
}
|
||||
}
|
||||
if !reflect.DeepEqual(runValues, [][]string{{"full"}}) {
|
||||
t.Fatalf("canonical run operands = %#v, want [[full]]", runValues)
|
||||
}
|
||||
if !reflect.DeepEqual(inspectValues, []string{"item-1", "item-2"}) {
|
||||
t.Fatalf("routed required operands = %#v", inspectValues)
|
||||
}
|
||||
if !reflect.DeepEqual(showValues, []string{"profile:json"}) || optionalParses != 1 {
|
||||
t.Fatalf("optional operand/flag values = %#v, converter calls %d", showValues, optionalParses)
|
||||
}
|
||||
|
||||
parsesBeforeReuse := [3]int{runParses, inspectParses, optionalParses}
|
||||
secondApp, err := commandkit.New(commandkit.Identity{Name: "Second"}, declarations...)
|
||||
if err != nil {
|
||||
t.Fatalf("reusing declarations: %v", err)
|
||||
}
|
||||
if got := [3]int{runParses, inspectParses, optionalParses}; got != parsesBeforeReuse {
|
||||
t.Fatalf("reinstalling declarations invoked operand converters: %v -> %v", parsesBeforeReuse, got)
|
||||
}
|
||||
if outcome := secondApp.Execute(context.Background(), []string{"run", "again"}, commandkit.Streams{Out: io.Discard, Err: io.Discard}); outcome.Kind != commandkit.OutcomeSuccess {
|
||||
t.Fatalf("reused declaration execution = %#v", outcome)
|
||||
}
|
||||
if !reflect.DeepEqual(runValues, [][]string{{"full"}, {"again"}}) {
|
||||
t.Fatalf("reused command declaration values = %#v", runValues)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicRootGroupRegistrationAndFailedBatchRecovery(t *testing.T) {
|
||||
type showInput struct{ format commandkit.Supplied[string] }
|
||||
var formatParses, lateRuns int
|
||||
var formats []string
|
||||
show := commandkit.NewCommand(commandkit.CommandSpec[showInput]{
|
||||
Name: "show",
|
||||
Flags: []commandkit.Flag[showInput]{commandkit.ValueFlag(commandkit.FlagMeta{Name: "format"}, (*string)(nil),
|
||||
func(value string) (string, error) { formatParses++; return value, nil },
|
||||
func(target *showInput, value commandkit.Supplied[string]) { target.format = value },
|
||||
)},
|
||||
Run: func(_ commandkit.Invocation, value showInput) commandkit.Outcome {
|
||||
formats = append(formats, value.format.Value)
|
||||
return commandkit.Success()
|
||||
},
|
||||
})
|
||||
family := commandkit.RootGroup("config", "Configuration", show)
|
||||
constructionFailure := errors.New("family construction failed")
|
||||
if _, err := commandkit.New(commandkit.Identity{Name: "Broken"}, commandkit.WithError(family, fmt.Errorf("adapter: %w", constructionFailure))); !errors.Is(err, constructionFailure) {
|
||||
t.Fatalf("New() error = %v, want retained family construction failure", err)
|
||||
}
|
||||
|
||||
app, err := commandkit.New(commandkit.Identity{Name: "Example"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var before bytes.Buffer
|
||||
app.Execute(context.Background(), []string{"--help"}, commandkit.Streams{Out: &before, Err: io.Discard})
|
||||
if strings.Contains(before.String(), "config") {
|
||||
t.Fatalf("root help showed an unregistered family: %q", before.String())
|
||||
}
|
||||
if err := app.Register(family,
|
||||
commandkit.Alias("cfg-show", "config show"),
|
||||
commandkit.Alias("cs", "cfg-show"),
|
||||
commandkit.FlagAlias("--fmt", "--format"),
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if formatParses != 0 {
|
||||
t.Fatalf("registration invoked flag converter %d times", formatParses)
|
||||
}
|
||||
var after bytes.Buffer
|
||||
app.Execute(context.Background(), []string{"--help"}, commandkit.Streams{Out: &after, Err: io.Discard})
|
||||
for _, fragment := range []string{"config", "Aliases:", "cfg-show", "cs"} {
|
||||
if !strings.Contains(after.String(), fragment) {
|
||||
t.Errorf("updated root help %q does not contain %q", after.String(), fragment)
|
||||
}
|
||||
}
|
||||
if outcome := app.Execute(context.Background(), []string{"cs", "--fmt=yaml"}, commandkit.Streams{Out: io.Discard, Err: io.Discard}); outcome.Kind != commandkit.OutcomeSuccess {
|
||||
t.Fatalf("registered alias execution = %#v", outcome)
|
||||
}
|
||||
if !reflect.DeepEqual(formats, []string{"yaml"}) || formatParses != 1 {
|
||||
t.Fatalf("registered family values = %#v, converter calls %d", formats, formatParses)
|
||||
}
|
||||
if err := app.Register(commandkit.NewCommand(commandkit.CommandSpec[struct{}]{Name: "cfg-show", Run: func(commandkit.Invocation, struct{}) commandkit.Outcome { return commandkit.Success() }})); err == nil {
|
||||
t.Fatal("registration accepted a canonical command that conflicts with an existing alias")
|
||||
}
|
||||
var afterConflict bytes.Buffer
|
||||
app.Execute(context.Background(), []string{"--help"}, commandkit.Streams{Out: &afterConflict, Err: io.Discard})
|
||||
if !strings.Contains(afterConflict.String(), "cfg-show") {
|
||||
t.Fatalf("failed relationship validation changed registered aliases: %q", afterConflict.String())
|
||||
}
|
||||
|
||||
late := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{Name: "late", Run: func(commandkit.Invocation, struct{}) commandkit.Outcome {
|
||||
lateRuns++
|
||||
return commandkit.Success()
|
||||
}})
|
||||
registrationFailure := errors.New("retained registration failure")
|
||||
err = app.Register(commandkit.WithError(late, fmt.Errorf("family: %w", registrationFailure)))
|
||||
if !errors.Is(err, registrationFailure) {
|
||||
t.Fatalf("Register() error = %v, want retained failure", err)
|
||||
}
|
||||
if err := app.Register(late, commandkit.Alias("late-shortcut", "missing")); err == nil {
|
||||
t.Fatal("registration accepted a batch whose alias target is absent")
|
||||
}
|
||||
var afterFailure bytes.Buffer
|
||||
app.Execute(context.Background(), []string{"--help"}, commandkit.Streams{Out: &afterFailure, Err: io.Discard})
|
||||
if strings.Contains(afterFailure.String(), "late-shortcut") || strings.Contains(afterFailure.String(), " late ") {
|
||||
t.Fatalf("failed batch changed root help: %q", afterFailure.String())
|
||||
}
|
||||
if err := app.Register(late, commandkit.Alias("late-shortcut", "late")); err != nil {
|
||||
t.Fatalf("registration after failed batch: %v", err)
|
||||
}
|
||||
var recovered bytes.Buffer
|
||||
app.Execute(context.Background(), []string{"--help"}, commandkit.Streams{Out: &recovered, Err: io.Discard})
|
||||
if !strings.Contains(recovered.String(), "late-shortcut") {
|
||||
t.Fatalf("recovered root help omitted alias: %q", recovered.String())
|
||||
}
|
||||
if outcome := app.Execute(context.Background(), []string{"late-shortcut"}, commandkit.Streams{Out: io.Discard, Err: io.Discard}); outcome.Kind != commandkit.OutcomeSuccess || lateRuns != 1 {
|
||||
t.Fatalf("recovered alias execution = %#v, runs %d", outcome, lateRuns)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user