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) } }