Files
commandkit/declaration_external_test.go

242 lines
11 KiB
Go

package commandkit_test
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"reflect"
"strings"
"testing"
commandkit "gitea.maximumdirect.net/eric/CommandKit"
)
func TestPublicInstallationAcceptsMixedDeclarations(t *testing.T) {
command := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{
Name: "show",
Summary: "Show a value.",
Flags: []commandkit.Flag[struct{}]{
commandkit.ValueFlag(commandkit.FlagMeta{Name: "format"}, (*string)(nil), func(value string) (string, error) { return value, nil }, func(*struct{}, commandkit.Supplied[string]) {}),
},
Run: func(commandkit.Invocation, struct{}) commandkit.Outcome { return commandkit.Success() },
})
declarations := []commandkit.Declaration{
command,
commandkit.Alias("inspect", "show"),
commandkit.FlagAlias("--fmt", "--format"),
commandkit.FlagAliasFor("show", "-f", "--format"),
}
app, err := commandkit.New(commandkit.Identity{Name: "Example"}, declarations...)
if err != nil {
t.Fatal(err)
}
if app == nil {
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)
}
}