186 lines
6.7 KiB
Go
186 lines
6.7 KiB
Go
package commandkit
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
)
|
|
|
|
type registryInput struct{ value string }
|
|
|
|
func TestCapturedDeclarationsRetainCallbacksWithoutCallingThem(t *testing.T) {
|
|
var factoryCalls, prepareCalls, setterCalls, checkCalls, runCalls int
|
|
flag := Flag[registryInput]{binding: flagBinding[registryInput]{
|
|
meta: FlagMeta{Name: "mode"}, valid: true,
|
|
prepare: func([]string) (func(*registryInput), error) {
|
|
prepareCalls++
|
|
return func(*registryInput) { setterCalls++ }, nil
|
|
},
|
|
}}
|
|
command := NewCommand(CommandSpec[registryInput]{
|
|
Name: "Inspect",
|
|
NewInput: func() registryInput { factoryCalls++; return registryInput{} },
|
|
Flags: []Flag[registryInput]{flag},
|
|
CheckInput: func(registryInput) error { checkCalls++; return nil },
|
|
Run: func(Invocation, registryInput) Outcome { runCalls++; return Success() },
|
|
})
|
|
group := Group("Tools", "tools", command)
|
|
app, err := New(Identity{Name: "Example"}, group)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := app.Register(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if factoryCalls+prepareCalls+setterCalls+checkCalls+runCalls != 0 {
|
|
t.Fatalf("construction invoked callback: factory=%d prepare=%d setter=%d check=%d run=%d", factoryCalls, prepareCalls, setterCalls, checkCalls, runCalls)
|
|
}
|
|
if !app.commands["Tools"].command.record.children[0].record.payload.hasRun() {
|
|
t.Fatal("registered command lost its handler")
|
|
}
|
|
}
|
|
|
|
func TestAppsHaveIndependentRegistries(t *testing.T) {
|
|
first, err := New(Identity{Name: "First"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second, err := New(Identity{Name: "Second"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
command := NewCommand(CommandSpec[struct{}]{Name: "custom"})
|
|
if err := first.Register(command); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, exists := second.commands["custom"]; exists {
|
|
t.Fatal("registration leaked to another app")
|
|
}
|
|
if !first.commands["help"].reserved || !first.commands["version"].reserved {
|
|
t.Fatal("New did not reserve generated root commands")
|
|
}
|
|
}
|
|
|
|
func TestDeclarationIssuesAreReportedAtInstallation(t *testing.T) {
|
|
for _, name := range []string{"", "--", "-option", "two words", " leading", "trailing "} {
|
|
app, err := New(Identity{Name: "Example"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := app.Register(NewCommand(CommandSpec[struct{}]{Name: name})); err == nil {
|
|
t.Errorf("installation accepted invalid name %q", name)
|
|
}
|
|
}
|
|
app, err := New(Identity{Name: "Example"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := app.Register(NewCommand(CommandSpec[struct{}]{Name: "CamelCase"})); err != nil {
|
|
t.Fatalf("mixed-case name rejected: %v", err)
|
|
}
|
|
|
|
duplicate := Group("parent", "", NewCommand(CommandSpec[struct{}]{Name: "repeat"}), NewCommand(CommandSpec[struct{}]{Name: "repeat"}))
|
|
if err := app.Register(duplicate); err == nil {
|
|
t.Fatal("installation accepted duplicate sibling names")
|
|
}
|
|
if err := app.Register(Group("parent", "", Command{})); err == nil {
|
|
t.Fatal("installation accepted a zero child handle")
|
|
}
|
|
if err := app.Register(Command{}); err == nil {
|
|
t.Fatal("installation accepted a zero command handle")
|
|
}
|
|
}
|
|
|
|
func TestRunnableCommandCapturesChildrenAndLeavesOriginalUnchanged(t *testing.T) {
|
|
child := NewCommand(CommandSpec[struct{}]{Name: "child"})
|
|
children := []Command{child}
|
|
parent := NewCommand(CommandSpec[struct{}]{Name: "parent", Children: children, Run: func(Invocation, struct{}) Outcome { return Success() }})
|
|
children[0] = NewCommand(CommandSpec[struct{}]{Name: "changed"})
|
|
if parent.record.payload == nil || !parent.record.payload.hasRun() {
|
|
t.Fatal("runnable parent lost its handler")
|
|
}
|
|
if len(parent.record.children) != 1 || parent.record.children[0].record.name != "child" {
|
|
t.Fatalf("captured children = %#v", parent.record.children)
|
|
}
|
|
app, err := New(Identity{Name: "Example"}, parent)
|
|
if err != nil {
|
|
t.Fatalf("runnable parent with children rejected: %v", err)
|
|
}
|
|
if _, ok := app.commandAt([]string{"parent", "child"}); !ok {
|
|
t.Fatal("child declaration was not installed")
|
|
}
|
|
}
|
|
|
|
func TestGroupCapturesChildrenAndSkipsNestedAbsence(t *testing.T) {
|
|
children := []Command{NewCommand(CommandSpec[struct{}]{Name: "visible"}), NoCommand()}
|
|
group := Group("parent", "", children...)
|
|
children[0] = NewCommand(CommandSpec[struct{}]{Name: "changed"})
|
|
if len(group.record.children) != 2 || group.record.children[0].record.name != "visible" {
|
|
t.Fatalf("group did not capture child handles: %#v", group.record.children)
|
|
}
|
|
app, err := New(Identity{Name: "Example"}, group)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := app.commandAt([]string{"parent", "visible"}); !ok {
|
|
t.Fatal("captured child missing")
|
|
}
|
|
}
|
|
|
|
func TestRootGroupRetainsPlacementMetadata(t *testing.T) {
|
|
root := RootGroup("config", "Configuration.")
|
|
if !root.record.rootOnly || root.record.absent || root.record.name != "config" {
|
|
t.Fatalf("root group metadata = %#v", root.record)
|
|
}
|
|
copy := root
|
|
if copy.record != root.record {
|
|
t.Fatal("copied handle did not retain captured record")
|
|
}
|
|
}
|
|
|
|
func TestWithErrorCopiesDeclarationAndRetainsWrappedCauses(t *testing.T) {
|
|
first := errors.New("first construction issue")
|
|
second := errors.New("second construction issue")
|
|
base := NewCommand(CommandSpec[struct{}]{Name: "run"})
|
|
withFirst := WithError(base, first)
|
|
withBoth := WithError(withFirst, errors.Join(errors.New("wrapper"), second))
|
|
if len(base.record.issues) != 0 {
|
|
t.Fatal("WithError mutated its source declaration")
|
|
}
|
|
if len(withFirst.record.issues) != 1 || len(withBoth.record.issues) != 2 {
|
|
t.Fatal("WithError did not retain prior causes")
|
|
}
|
|
app, err := New(Identity{Name: "Example"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = app.Register(withBoth)
|
|
if !errors.Is(err, first) || !errors.Is(err, second) {
|
|
t.Fatalf("installation error %v did not preserve causes", err)
|
|
}
|
|
if err := app.Register(WithError(NoCommand(), first)); !errors.Is(err, first) {
|
|
t.Fatalf("error on absence = %v", err)
|
|
}
|
|
if err := app.Register(NoCommand()); err != nil {
|
|
t.Fatalf("plain absence was not skipped: %v", err)
|
|
}
|
|
if err := app.Register(RootGroup("empty", "")); err != nil {
|
|
t.Fatalf("real empty root group rejected: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestNewRejectsEmptyIdentity(t *testing.T) {
|
|
if _, err := New(Identity{}); err == nil {
|
|
t.Fatal("New accepted an empty identity name")
|
|
}
|
|
}
|
|
|
|
func TestNewCommandCopiesBindingMetadata(t *testing.T) {
|
|
flags := []Flag[registryInput]{{binding: flagBinding[registryInput]{meta: FlagMeta{Name: "mode"}, valid: true, prepare: func([]string) (func(*registryInput), error) { return func(*registryInput) {}, nil }}}}
|
|
command := NewCommand(CommandSpec[registryInput]{Name: "run", Flags: flags})
|
|
flags[0].binding.meta.Name = "changed"
|
|
payload := command.record.payload.(typedCommand[registryInput])
|
|
if got := payload.spec.Flags[0].binding.meta.Name; got != "mode" {
|
|
t.Fatalf("stored flag name = %q, want mode", got)
|
|
}
|
|
}
|