Files
commandkit/occurrence_test.go

153 lines
6.9 KiB
Go
Raw Permalink Normal View History

2026-11-07 03:49:34 +00:00
package commandkit
import (
"errors"
"fmt"
"reflect"
"strings"
"testing"
)
type occurrenceInput struct{ events []string }
func occurrenceFor(name string, required bool, calls *int) Flag[occurrenceInput] {
return OccurrenceFlag(FlagMeta{Name: name, Required: required, Short: "x"}, func(raw string) (string, error) {
*calls++
if raw == "bad" {
return "", errors.New("invalid occurrence")
}
return raw, nil
}, func(input *occurrenceInput, value Supplied[string]) {
input.events = append(input.events, name+"="+value.Value)
})
}
func TestOccurrenceFlagsApplyInEnteredOrder(t *testing.T) {
calls, factories := 0, 0
first := occurrenceFor("first", false, &calls)
second := RenameFlag(occurrenceFor("second", false, &calls), "second")
ordinary := ValueFlag(FlagMeta{Name: "ordinary"}, strptr("default"), func(s string) (string, error) { return s, nil }, func(i *occurrenceInput, v Supplied[string]) { i.events = append(i.events, "ordinary="+v.Value) })
arg := ValueArgument(ArgumentMeta{Name: "ARG"}, false, func(s string) (string, error) { return s, nil }, func(i *occurrenceInput, v Supplied[string]) { i.events = append(i.events, "operand="+v.Value) })
var got []string
cmd := NewCommand(CommandSpec[occurrenceInput]{Name: "run", Flags: []Flag[occurrenceInput]{first, second, ordinary}, Arguments: []Argument[occurrenceInput]{arg}, NewInput: func() occurrenceInput { factories++; return occurrenceInput{} }, CheckInput: func(i occurrenceInput) error { got = append([]string(nil), i.events...); return nil }, Run: func(Invocation, occurrenceInput) Outcome { return Success() }})
app, err := New(Identity{Name: "Example"}, cmd, FlagAliasFor("run", "--other", "--second"))
if err != nil {
t.Fatal(err)
}
result := app.Execute(t.Context(), []string{"run", "--other=a", "--first=", "--second=b", "--ordinary=z", "tail"}, Streams{})
want := []string{"ordinary=z", "second=a", "first=", "second=b", "operand=tail"}
if result.Status != 0 || !reflect.DeepEqual(got, want) || calls != 3 || factories != 1 {
t.Fatalf("%#v events=%q calls=%d factories=%d", result, got, calls, factories)
}
result = app.Execute(t.Context(), []string{"run", "--first=bad"}, Streams{})
if result.Kind != OutcomeUsage || factories != 1 || !strings.Contains(result.Explanation.Message, "--first") {
t.Fatalf("invalid result %#v factories=%d", result, factories)
}
result = app.Execute(t.Context(), []string{"run"}, Streams{})
if result.Status != 0 || !reflect.DeepEqual(got, []string{"ordinary=default", "operand="}) {
t.Fatalf("fresh %#v, %q", result, got)
}
}
type occurrenceOuter struct{ flags FlagValues[occurrenceInput] }
func TestCapturedOccurrencesReplaySelectedOrder(t *testing.T) {
calls := 0
a := occurrenceFor("alpha", false, &calls)
b := RenameFlag(a, "beta")
missing := RenameFlag(occurrenceFor("required", true, &calls), "required")
originals := []Flag[occurrenceInput]{a, b, missing}
captured := CaptureFlags(originals, func(o *occurrenceOuter) *FlagValues[occurrenceInput] { return &o.flags })
var stored FlagValues[occurrenceInput]
cmd := NewCommand(CommandSpec[occurrenceOuter]{Name: "run", Flags: captured, Run: func(_ Invocation, i occurrenceOuter) Outcome { stored = i.flags; return Success() }})
app, err := New(Identity{Name: "Example"}, cmd, FlagAliasFor("run", "--b", "--beta"))
if err != nil {
t.Fatal(err)
}
result := app.Execute(t.Context(), []string{"run", "--b=1", "--alpha=2", "--beta=3", "--alpha=4"}, Streams{})
if result.Status != 0 || calls != 4 {
t.Fatalf("%#v calls=%d", result, calls)
}
var got occurrenceInput
if err := stored.ApplyTo(&got, a, b); err != nil {
t.Fatal(err)
}
want := []string{"alpha=1", "alpha=2", "alpha=3", "alpha=4"}
if !reflect.DeepEqual(got.events, want) {
t.Fatalf("events %q want %q", got.events, want)
}
var subset occurrenceInput
if err := stored.ApplyTo(&subset, b); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(subset.events, []string{"alpha=1", "alpha=3"}) || calls != 4 {
t.Fatalf("subset %q calls=%d", subset.events, calls)
}
var repeated occurrenceInput
if err := stored.ApplyTo(&repeated, a, b); err != nil {
t.Fatal(err)
}
got.events[0] = "changed"
if !reflect.DeepEqual(repeated.events, want) {
t.Fatalf("shared state %q", repeated.events)
}
if err := stored.ApplyTo(&repeated, missing); !errors.Is(err, ErrFlagInput) {
t.Fatalf("required error %v", err)
}
other := RenameFlag(a, "other")
if err := stored.ApplyTo(&repeated, other); !errors.Is(err, ErrFlagWiring) {
t.Fatalf("identity error %v", err)
}
}
func TestRenameFlagKeepsBindingBehavior(t *testing.T) {
parse := func(s string) (string, error) { return strings.ToUpper(s), nil }
original := ValueFlag(FlagMeta{Name: "original", Short: "o", Required: true, Completion: CompleteChoices(CompletionChoice{Value: "one"})}, strptr("default"), parse, func(i *occurrenceInput, v Supplied[string]) {
i.events = append(i.events, fmt.Sprint(v.Value, ":", v.Set))
})
renamed := RenameFlag(original, "renamed")
if renamed.binding.identity == original.binding.identity || renamed.binding.meta.Short != "" || !renamed.binding.meta.Required || !reflect.DeepEqual(renamed.binding.meta.Completion, original.binding.meta.Completion) {
t.Fatalf("renamed %#v", renamed.binding.meta)
}
value, err := renamed.binding.prepare(nil)
if err != nil {
t.Fatal(err)
}
var input occurrenceInput
value.apply(&input)
if !reflect.DeepEqual(input.events, []string{"default:false"}) {
t.Fatalf("default %q", input.events)
}
if _, err := New(Identity{Name: "Example"}, NewCommand(CommandSpec[occurrenceInput]{Name: "run", Flags: []Flag[occurrenceInput]{original, RenameFlag(original, "bad name")}})); err == nil {
t.Fatal("invalid name accepted")
}
}
func TestOccurrenceRequiredAndAliasedConversion(t *testing.T) {
parseErr := errors.New("cannot parse")
builds := 0
flag := OccurrenceFlag(FlagMeta{Name: "item", Required: true}, func(raw string) (string, error) {
if raw == "bad" {
return "", parseErr
}
return raw, nil
}, func(*occurrenceInput, Supplied[string]) {})
cmd := NewCommand(CommandSpec[occurrenceInput]{Name: "run", Flags: []Flag[occurrenceInput]{flag}, NewInput: func() occurrenceInput { builds++; return occurrenceInput{} }, Run: func(Invocation, occurrenceInput) Outcome { return Success() }})
app, err := New(Identity{Name: "Example"}, cmd, FlagAliasFor("run", "--entry", "--item"))
if err != nil {
t.Fatal(err)
}
missing := app.Execute(t.Context(), []string{"run"}, Streams{})
if missing.Kind != OutcomeUsage || builds != 0 {
t.Fatalf("missing: %#v", missing)
}
invalid := app.Execute(t.Context(), []string{"run", "--item=ok", "--entry=bad"}, Streams{})
if invalid.Kind != OutcomeUsage || !errors.Is(invalid.Cause, parseErr) || !strings.Contains(invalid.Explanation.Message, "--entry occurrence 2") || builds != 0 {
t.Fatalf("invalid: %#v", invalid)
}
help := app.Execute(t.Context(), []string{"run", "--help"}, Streams{})
if help.Status != 0 || builds != 0 {
t.Fatalf("help: %#v", help)
}
}