298 lines
12 KiB
Go
298 lines
12 KiB
Go
package commandkit
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
commandmeta "gitea.maximumdirect.net/eric/CommandKit/internal/commandmeta"
|
|
"gitea.maximumdirect.net/eric/CommandKit/internal/commandpath"
|
|
)
|
|
|
|
func flagAliasCommand(name string, flags ...Flag[struct{}]) Command {
|
|
return NewCommand(CommandSpec[struct{}]{
|
|
Name: name, Summary: name, Flags: flags,
|
|
Run: func(Invocation, struct{}) Outcome { return Success() },
|
|
})
|
|
}
|
|
|
|
func stringFlag(name, short string) Flag[struct{}] {
|
|
return ValueFlag(FlagMeta{Name: name, Short: short}, (*string)(nil),
|
|
func(value string) (string, error) { return value, nil },
|
|
func(*struct{}, Supplied[string]) {},
|
|
)
|
|
}
|
|
|
|
func TestFlagAliasChainsUseScopedOverridesAndCanonicalEndpoints(t *testing.T) {
|
|
inspect := flagAliasCommand("inspect", stringFlag("color", "c"), stringFlag("format", "f"))
|
|
show := flagAliasCommand("show", stringFlag("color", "c"), stringFlag("format", "f"))
|
|
group := Group("Tools", "tools", show)
|
|
app, err := New(Identity{Name: "Example"},
|
|
inspect,
|
|
group,
|
|
Alias("look", "inspect"),
|
|
FlagAlias("--colour", "--hue"),
|
|
FlagAlias("--hue", "--color"),
|
|
FlagAlias("-x", "--color"),
|
|
FlagAlias("--tint", "-c"),
|
|
FlagAliasFor("Tools", "--group-help", "--help"),
|
|
FlagAliasFor("inspect", "--hue", "--format"),
|
|
FlagAliasFor("inspect", "--local", "--format"),
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
inspectAliases := app.flagAliases[commandpath.Path{"inspect"}.Key()].bySpelling
|
|
for source, target := range map[string]string{
|
|
"--colour": "--format",
|
|
"--hue": "--format",
|
|
"-x": "--color",
|
|
"--tint": "-c",
|
|
"--local": "--format",
|
|
} {
|
|
alias, ok := inspectAliases[flagSpellingKey(capturedSpelling(source))]
|
|
if !ok || alias.target.token() != target {
|
|
t.Errorf("inspect alias %s = (%s, %v), want %s", source, alias.target.token(), ok, target)
|
|
}
|
|
}
|
|
|
|
groupAliases := app.flagAliases[commandpath.Path{"Tools"}.Key()].bySpelling
|
|
if _, active := groupAliases[flagSpellingKey(capturedFlagSpelling{name: "colour"})]; active {
|
|
t.Error("global alias was active on a group without its destination flag")
|
|
}
|
|
if _, inherited := groupAliases[flagSpellingKey(capturedFlagSpelling{name: "local"})]; inherited {
|
|
t.Fatal("command-scoped alias was inherited by its parent group")
|
|
}
|
|
if _, inherited := app.flagAliases[commandpath.Path{"Tools", "show"}.Key()].bySpelling[flagSpellingKey(capturedFlagSpelling{name: "local"})]; inherited {
|
|
t.Fatal("command-scoped alias was inherited by a different command")
|
|
}
|
|
groupHelp := flagSpellingKey(capturedFlagSpelling{name: "group-help"})
|
|
if got := groupAliases[groupHelp].target.token(); got != "--help" {
|
|
t.Fatalf("group-scoped help alias target = %s, want --help", got)
|
|
}
|
|
if _, inherited := app.flagAliases[commandpath.Path{"Tools", "show"}.Key()].bySpelling[groupHelp]; inherited {
|
|
t.Fatal("group-scoped alias was inherited by its child command")
|
|
}
|
|
}
|
|
|
|
func TestFlagAliasDiscoveryContextsUseGlobalReportingAliases(t *testing.T) {
|
|
show := flagAliasCommand("show")
|
|
app, err := New(Identity{Name: "Example"},
|
|
Group("Tools", "tools", show),
|
|
Alias("shortcut show", "Tools show"),
|
|
FlagAlias("--hlep", "--help"),
|
|
FlagAlias("--silent", "--quiet"),
|
|
FlagAlias("--chatty", "--verbose"),
|
|
FlagAlias("--trace", "--debug"),
|
|
FlagAlias("--show-progress", "--progress"),
|
|
FlagAliasFor("Tools show", "--local-help", "--help"),
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
globalHelp := flagSpellingKey(capturedFlagSpelling{name: "hlep"})
|
|
if got := app.flagAliases[commandpath.Path(nil).Key()].bySpelling[globalHelp].target.token(); got != "--help" {
|
|
t.Fatalf("root --hlep target = %s, want --help", got)
|
|
}
|
|
if got := app.flagAliases[commandpath.Path{"Tools"}.Key()].bySpelling[globalHelp].target.token(); got != "--help" {
|
|
t.Fatalf("group --hlep target = %s, want --help", got)
|
|
}
|
|
discovery := app.discoveryFlags[commandpath.Path{"shortcut"}.Key()].bySpelling
|
|
if got := discovery[globalHelp].target.token(); got != "--help" {
|
|
t.Fatalf("discovery --hlep target = %s, want --help", got)
|
|
}
|
|
for label, aliases := range map[string]resolvedFlagAliases{
|
|
"root": app.flagAliases[commandpath.Path(nil).Key()],
|
|
"group": app.flagAliases[commandpath.Path{"Tools"}.Key()],
|
|
"command": app.flagAliases[commandpath.Path{"Tools", "show"}.Key()],
|
|
"discovery": app.discoveryFlags[commandpath.Path{"shortcut"}.Key()],
|
|
} {
|
|
for source, target := range map[string]string{
|
|
"silent": "quiet",
|
|
"chatty": "verbose",
|
|
"trace": "debug",
|
|
"show-progress": "progress",
|
|
} {
|
|
key := flagSpellingKey(capturedFlagSpelling{name: source})
|
|
if got := aliases.bySpelling[key].target.token(); got != "--"+target {
|
|
t.Errorf("%s --%s target = %s, want --%s", label, source, got, target)
|
|
}
|
|
}
|
|
}
|
|
if _, inherited := discovery[flagSpellingKey(capturedFlagSpelling{name: "local-help"})]; inherited {
|
|
t.Fatal("discovery context inherited a command-scoped alias")
|
|
}
|
|
}
|
|
|
|
func TestReportingFlagsAreReservedAndAcceptApplicationShortAliases(t *testing.T) {
|
|
for _, name := range []string{"quiet", "verbose", "debug", "progress"} {
|
|
command := flagAliasCommand("reserved", stringFlag(name, ""))
|
|
if len(command.record.issues) == 0 {
|
|
t.Errorf("capturing --%s did not retain a declaration error", name)
|
|
}
|
|
app, err := New(Identity{Name: "Example"}, flagAliasCommand("safe"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
snapshot := app.routeSnapshot()
|
|
if err := app.Register(command, Alias("reserved-shortcut", "reserved")); err == nil || !strings.Contains(err.Error(), "reserved for reporting") {
|
|
t.Errorf("registering --%s returned %v, want a retained reporting reservation error", name, err)
|
|
}
|
|
if app.routeSnapshot() != snapshot || app.commands["reserved"] != nil || app.commands["safe"] == nil || len(app.routingRules) != 0 {
|
|
t.Fatalf("failed registration of --%s changed the installed commands", name)
|
|
}
|
|
}
|
|
|
|
declarations := []Declaration{flagAliasCommand("run")}
|
|
for short, target := range map[string]string{"q": "quiet", "v": "verbose", "d": "debug"} {
|
|
declarations = append(declarations, FlagAlias("-"+short, "--"+target))
|
|
}
|
|
if _, err := New(Identity{Name: "Example"}, declarations...); err != nil {
|
|
t.Fatalf("application short aliases for reporting flags were rejected: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestReportingFlagSpellingsConflictWithAliasSources(t *testing.T) {
|
|
for _, name := range []string{"quiet", "verbose", "debug", "progress"} {
|
|
if _, err := New(Identity{Name: "Example"}, FlagAlias("--"+name, "--help")); err == nil || !strings.Contains(err.Error(), "conflicts with a canonical spelling") {
|
|
t.Errorf("alias source --%s returned %v, want a canonical spelling collision", name, err)
|
|
}
|
|
}
|
|
if _, err := New(Identity{Name: "Example"}, Group("tools", "", flagAliasCommand("show")), FlagAliasFor("tools", "--quiet", "--help")); err == nil || !strings.Contains(err.Error(), "conflicts with a canonical spelling") {
|
|
t.Errorf("group-scoped canonical reporting spelling returned %v, want a collision", err)
|
|
}
|
|
}
|
|
|
|
func TestRoutingSnapshotCarriesReportingMetadataOutsideConsumerBindings(t *testing.T) {
|
|
app, err := New(Identity{Name: "Example"},
|
|
flagAliasCommand("run", stringFlag("format", "f")),
|
|
Group("tools", "", flagAliasCommand("show")),
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var check func(*commandmeta.Command)
|
|
check = func(node *commandmeta.Command) {
|
|
if len(node.CoreFlags) != 4 {
|
|
t.Errorf("%s core reporting flags = %#v", node.Name, node.CoreFlags)
|
|
}
|
|
for _, flag := range node.Flags {
|
|
if commandmeta.IsReportingFlag(flag.Name) {
|
|
t.Errorf("%s reporting flag %q was added to consumer bindings", node.Name, flag.Name)
|
|
}
|
|
}
|
|
for _, child := range node.Children {
|
|
check(child)
|
|
}
|
|
}
|
|
check(app.routeSnapshot().metadata)
|
|
}
|
|
|
|
func TestFlagAliasDuplicatesAndScopesAreDeterministic(t *testing.T) {
|
|
command := flagAliasCommand("inspect", stringFlag("color", "c"), stringFlag("format", "f"))
|
|
duplicate := FlagAlias("--colour", "--color")
|
|
app, err := New(Identity{Name: "Example"}, command, flagAliasCommand("plain", stringFlag("color", "c")), duplicate, FlagAlias("--colour", "--color"),
|
|
FlagAliasFor("inspect", "--colour", "--format"))
|
|
if err != nil {
|
|
t.Fatalf("identical duplicates and scoped override were rejected: %v", err)
|
|
}
|
|
if got := app.flagAliases[commandpath.Path{"inspect"}.Key()].bySpelling[flagSpellingKey(capturedFlagSpelling{name: "colour"})].target.token(); got != "--format" {
|
|
t.Fatalf("scoped alias target = %s, want --format", got)
|
|
}
|
|
if got := app.flagAliases[commandpath.Path{"plain"}.Key()].bySpelling[flagSpellingKey(capturedFlagSpelling{name: "colour"})].target.token(); got != "--color" {
|
|
t.Fatalf("effective global alias target = %s, want --color", got)
|
|
}
|
|
|
|
for name, rules := range map[string][]RoutingRule{
|
|
"competing global targets": {
|
|
FlagAlias("--colour", "--color"),
|
|
FlagAlias("--colour", "--format"),
|
|
},
|
|
"competing scoped targets": {
|
|
FlagAliasFor("inspect", "--colour", "--color"),
|
|
FlagAliasFor("inspect", "--colour", "--format"),
|
|
},
|
|
"unknown scope": {FlagAliasFor("missing", "--colour", "--color")},
|
|
"missing scoped destination": {FlagAliasFor("inspect", "--colour", "--missing")},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
declarations := []Declaration{command}
|
|
for _, rule := range rules {
|
|
declarations = append(declarations, rule)
|
|
}
|
|
if _, err := New(Identity{Name: "Example"}, declarations...); err == nil {
|
|
t.Fatal("New() accepted invalid flag alias rules")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFlagAliasCyclesIdentifyParticipatingSpellings(t *testing.T) {
|
|
for name, declarations := range map[string][]Declaration{
|
|
"global": {
|
|
FlagAlias("--first", "--second"),
|
|
FlagAlias("--second", "--first"),
|
|
},
|
|
"scoped": {
|
|
flagAliasCommand("inspect"),
|
|
FlagAliasFor("inspect", "-a", "--second"),
|
|
FlagAliasFor("inspect", "--second", "-a"),
|
|
},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
_, err := New(Identity{Name: "Example"}, declarations...)
|
|
if err == nil {
|
|
t.Fatal("New() accepted a flag alias cycle")
|
|
}
|
|
message := err.Error()
|
|
for _, spelling := range []string{"--first", "--second"} {
|
|
if name == "scoped" {
|
|
continue
|
|
}
|
|
if !strings.Contains(message, spelling) {
|
|
t.Errorf("cycle error %q does not identify %s", message, spelling)
|
|
}
|
|
}
|
|
if name == "scoped" && (!strings.Contains(message, "-a") || !strings.Contains(message, "--second")) {
|
|
t.Errorf("scoped cycle error %q does not identify both spellings", message)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestInactiveGlobalAliasesActivateAtomicallyWhenDestinationAppears(t *testing.T) {
|
|
command := flagAliasCommand("inspect", stringFlag("color", "c"))
|
|
app, err := New(Identity{Name: "Example"}, command,
|
|
FlagAlias("--color", "--missing"),
|
|
FlagAlias("--colour", "--color"),
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
aliases := app.flagAliases[commandpath.Path{"inspect"}.Key()].bySpelling
|
|
if _, active := aliases[flagSpellingKey(capturedFlagSpelling{name: "color"})]; active {
|
|
t.Fatal("global alias with an unavailable destination became active")
|
|
}
|
|
if got := aliases[flagSpellingKey(capturedFlagSpelling{name: "colour"})].target.token(); got != "--color" {
|
|
t.Fatalf("--colour endpoint = %s, want canonical --color", got)
|
|
}
|
|
|
|
snapshot := app.routeSnapshot()
|
|
commands := len(app.commands)
|
|
rules := len(app.routingRules)
|
|
originalAlias := aliases[flagSpellingKey(capturedFlagSpelling{name: "colour"})]
|
|
if err := app.Register(flagAliasCommand("later", stringFlag("color", "c"), stringFlag("missing", ""))); err == nil {
|
|
t.Fatal("Register() accepted a newly active global source collision")
|
|
}
|
|
currentAlias := app.flagAliases[commandpath.Path{"inspect"}.Key()].bySpelling[flagSpellingKey(capturedFlagSpelling{name: "colour"})]
|
|
if app.routeSnapshot() != snapshot || len(app.commands) != commands || len(app.routingRules) != rules || currentAlias != originalAlias {
|
|
t.Fatal("failed registration changed the existing command or routing state")
|
|
}
|
|
}
|
|
|
|
func capturedSpelling(token string) capturedFlagSpelling {
|
|
issues := []error(nil)
|
|
return captureFlagSpelling("test", token, &issues)
|
|
}
|