Install command aliases atomically
This commit is contained in:
156
registry_test.go
156
registry_test.go
@@ -2,7 +2,10 @@ package commandkit
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/CommandKit/internal/commandpath"
|
||||
)
|
||||
|
||||
type registryInput struct{ value string }
|
||||
@@ -41,11 +44,11 @@ func TestCapturedDeclarationsRetainCallbacksWithoutCallingThem(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRoutingRuleFactoriesCaptureNormalizedValues(t *testing.T) {
|
||||
commandRule := Alias(" old\tpath ", "new path")
|
||||
commandRule := Alias(" old\tpath ", " new ")
|
||||
if got, want := commandRule.record.sourcePath.String(), "old path"; got != want {
|
||||
t.Fatalf("source path = %q, want %q", got, want)
|
||||
}
|
||||
if got, want := commandRule.record.targetPath.String(), "new path"; got != want {
|
||||
if got, want := commandRule.record.targetPath.String(), "new"; got != want {
|
||||
t.Fatalf("target path = %q, want %q", got, want)
|
||||
}
|
||||
globalRule := FlagAlias("--colour", "-c")
|
||||
@@ -117,6 +120,155 @@ func TestNilAndZeroDeclarationsFailWithoutMutation(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAliasTargetsCanBeDeclaredInEitherOrder(t *testing.T) {
|
||||
canonical := simpleRunnable("canonical")
|
||||
for name, declarations := range map[string][]Declaration{
|
||||
"targets last": {
|
||||
Alias("shortcut", "alternate"),
|
||||
Alias("alternate", "canonical"),
|
||||
canonical,
|
||||
},
|
||||
"targets first": {
|
||||
canonical,
|
||||
Alias("alternate", "canonical"),
|
||||
Alias("shortcut", "alternate"),
|
||||
},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
app, err := New(Identity{Name: "Example"}, declarations...)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := app.aliases.Resolve(commandpath.Path{"shortcut"}); !got.Equal(commandpath.Path{"canonical"}) {
|
||||
t.Fatalf("shortcut resolves to %#v", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdenticalCommandAliasesAreIdempotentAcrossRegistrations(t *testing.T) {
|
||||
app, err := New(Identity{Name: "Example"}, simpleRunnable("canonical"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rule := Alias("shortcut", "canonical")
|
||||
if err := app.Register(rule, Alias(" shortcut ", " canonical ")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(app.routingRules) != 1 {
|
||||
t.Fatalf("routing rules = %d, want one normalized rule", len(app.routingRules))
|
||||
}
|
||||
snapshot := app.routeSnapshot()
|
||||
if err := app.Register(Alias("shortcut", "canonical")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(app.routingRules) != 1 || app.routeSnapshot() != snapshot {
|
||||
t.Fatal("identical registration changed aliases or invalidated the snapshot")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandAliasesRejectMissingAndCompetingTargets(t *testing.T) {
|
||||
if _, err := New(Identity{Name: "Example"}, Alias("shortcut", "missing")); err == nil {
|
||||
t.Fatal("New() accepted a missing alias target")
|
||||
}
|
||||
_, err := New(Identity{Name: "Example"},
|
||||
simpleRunnable("canonical"),
|
||||
Alias("first", "canonical"),
|
||||
Alias("second", "canonical"),
|
||||
Alias("shortcut", "first"),
|
||||
Alias("shortcut", "second"),
|
||||
)
|
||||
if err == nil {
|
||||
t.Fatal("New() accepted competing declared targets with a shared endpoint")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandAliasesRejectCanonicalRoutesAndOperands(t *testing.T) {
|
||||
group := Group("group", "group", simpleRunnable("show"))
|
||||
if _, err := New(Identity{Name: "Example"}, group, simpleRunnable("target"), Alias("group show", "target")); err == nil {
|
||||
t.Fatal("New() accepted an alias source that replaces a canonical child")
|
||||
}
|
||||
|
||||
run := runnableWithOperands("run", 1, 1)
|
||||
if _, err := New(Identity{Name: "Example"}, run, simpleRunnable("inspect"), Alias("run full", "inspect")); err == nil {
|
||||
t.Fatal("New() accepted an alias source that intercepts a valid operand")
|
||||
}
|
||||
if _, err := New(Identity{Name: "Example"}, run, Alias("fast", "run production")); err == nil {
|
||||
t.Fatal("New() accepted an operand-bearing alias target")
|
||||
}
|
||||
|
||||
promptkit := Group("promptkit", "promptkit", runnableWithOperands("test", 1, 1))
|
||||
if _, err := New(Identity{Name: "Example"}, promptkit, Alias("promptkit test full", "promptkit test all")); err == nil {
|
||||
t.Fatal("New() accepted an alias that replaces the test command's KIND operand")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLongerAliasBeneathAlternateSpellingCanUseOperandWords(t *testing.T) {
|
||||
app, err := New(Identity{Name: "Example"},
|
||||
runnableWithOperands("run", 1, 1),
|
||||
simpleRunnable("inspect"),
|
||||
Alias("r", "run"),
|
||||
Alias("r full", "inspect"),
|
||||
Alias("run too many words", "inspect"),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := app.aliases.Resolve(commandpath.Path{"r", "full"}); !got.Equal(commandpath.Path{"inspect"}) {
|
||||
t.Fatalf("longer alternate route resolves to %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFailedAliasRegistrationPreservesCommandsRulesAndSnapshot(t *testing.T) {
|
||||
app, err := New(Identity{Name: "Example"}, simpleRunnable("canonical"), Alias("existing", "canonical"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
snapshot := app.routeSnapshot()
|
||||
oldResolver := app.aliases
|
||||
retained := errors.New("retained construction failure")
|
||||
invalid := WithError(simpleRunnable("new"), fmt.Errorf("integration: %w", retained))
|
||||
err = app.Register(invalid, Alias("later", "missing"))
|
||||
if !errors.Is(err, retained) {
|
||||
t.Fatalf("Register() error = %v, want retained cause", err)
|
||||
}
|
||||
if app.commands["new"] != nil || len(app.routingRules) != 1 || app.routeSnapshot() != snapshot || app.aliases != oldResolver {
|
||||
t.Fatal("failed registration changed the app registry or derived metadata")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCanonicalCommandIsCheckedAgainstExistingAliases(t *testing.T) {
|
||||
app, err := New(Identity{Name: "Example"}, simpleRunnable("canonical"), Alias("shortcut", "canonical"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
snapshot := app.routeSnapshot()
|
||||
if err := app.Register(simpleRunnable("shortcut")); err == nil {
|
||||
t.Fatal("Register() accepted a canonical command that collides with an alias source")
|
||||
}
|
||||
if app.commands["shortcut"] != nil || len(app.routingRules) != 1 || app.routeSnapshot() != snapshot {
|
||||
t.Fatal("failed canonical registration changed the app registry or snapshot")
|
||||
}
|
||||
}
|
||||
|
||||
func simpleRunnable(name string) Command {
|
||||
return NewCommand(CommandSpec[struct{}]{
|
||||
Name: name, Summary: name,
|
||||
Run: func(Invocation, struct{}) Outcome { return Success() },
|
||||
})
|
||||
}
|
||||
|
||||
func runnableWithOperands(name string, min, max int) Command {
|
||||
return NewCommand(CommandSpec[struct{}]{
|
||||
Name: name, Summary: name,
|
||||
Arguments: []Argument[struct{}]{RepeatArgument(ArgumentMeta{Name: "ARG"}, min, max,
|
||||
func(value string) (string, error) { return value, nil },
|
||||
func(*struct{}, Supplied[[]string]) {},
|
||||
)},
|
||||
Run: func(Invocation, struct{}) Outcome { return Success() },
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppsHaveIndependentRegistries(t *testing.T) {
|
||||
first, err := New(Identity{Name: "First"})
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user