337 lines
14 KiB
Go
337 lines
14 KiB
Go
package commandkit
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"reflect"
|
|
"strconv"
|
|
"testing"
|
|
)
|
|
|
|
type bindingInput struct {
|
|
message Supplied[string]
|
|
count Supplied[int]
|
|
enabled Supplied[bool]
|
|
tags Supplied[[]string]
|
|
runtime string
|
|
}
|
|
|
|
func TestScalarBindingsKeepDefaultsSeparateFromPresence(t *testing.T) {
|
|
defaultMessage := "fallback"
|
|
factoryCalls := 0
|
|
spec := CommandSpec[bindingInput]{
|
|
Name: "run",
|
|
NewInput: func() bindingInput {
|
|
factoryCalls++
|
|
return bindingInput{runtime: "caller state"}
|
|
},
|
|
Flags: []Flag[bindingInput]{
|
|
ValueFlag(FlagMeta{Name: "message"}, &defaultMessage, func(value string) (string, error) { return value, nil }, func(input *bindingInput, value Supplied[string]) { input.message = value }),
|
|
ValueFlag(FlagMeta{Name: "count"}, (*int)(nil), strconv.Atoi, func(input *bindingInput, value Supplied[int]) { input.count = value }),
|
|
BoolFlag(FlagMeta{Name: "enabled"}, true, func(input *bindingInput, value Supplied[bool]) { input.enabled = value }),
|
|
},
|
|
}
|
|
if err := validateCommandDeclaration(NewCommand(spec)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defaultMessage = "changed after declaration"
|
|
prepared, err := prepareInput(spec, [][]string{nil, nil, nil}, nil, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if factoryCalls != 0 {
|
|
t.Fatal("preparing typed values invoked NewInput")
|
|
}
|
|
input := prepared.build()
|
|
if factoryCalls != 1 {
|
|
t.Fatalf("NewInput calls = %d, want 1", factoryCalls)
|
|
}
|
|
if input.message.Value != "fallback" || input.message.Set {
|
|
t.Fatalf("omitted message = %#v, want fallback with Set=false", input.message)
|
|
}
|
|
if input.count.Value != 0 || input.count.Set {
|
|
t.Fatalf("omitted count = %#v, want zero with Set=false", input.count)
|
|
}
|
|
if !input.enabled.Value || input.enabled.Set {
|
|
t.Fatalf("omitted enabled = %#v, want true default with Set=false", input.enabled)
|
|
}
|
|
if input.runtime != "caller state" {
|
|
t.Fatalf("NewInput runtime value = %q, want caller value", input.runtime)
|
|
}
|
|
|
|
prepared, err = prepareInput(spec, [][]string{{"", "last"}, {"0"}, {"true", "false"}}, nil, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
input = prepared.build()
|
|
if input.message.Value != "last" || !input.message.Set {
|
|
t.Fatalf("message = %#v, want last supplied value", input.message)
|
|
}
|
|
if input.count.Value != 0 || !input.count.Set {
|
|
t.Fatalf("count = %#v, want explicit zero", input.count)
|
|
}
|
|
if input.enabled.Value || !input.enabled.Set {
|
|
t.Fatalf("enabled = %#v, want explicit false", input.enabled)
|
|
}
|
|
|
|
prepared, err = prepareInput(spec, [][]string{{""}, nil, nil}, nil, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
input = prepared.build()
|
|
if input.message.Value != "" || !input.message.Set {
|
|
t.Fatalf("message = %#v, want explicitly supplied empty string", input.message)
|
|
}
|
|
prepared, err = prepareInput(spec, [][]string{nil, nil, nil}, nil, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
input = prepared.build()
|
|
if input.message.Value != "fallback" || input.message.Set || input.count.Set || input.enabled.Set {
|
|
t.Fatalf("omitted values after a supplied invocation = %#v", input)
|
|
}
|
|
}
|
|
|
|
func TestBoolFlagRejectsExplicitEmptyValue(t *testing.T) {
|
|
flag := BoolFlag(FlagMeta{Name: "enabled"}, false, func(*bool, Supplied[bool]) {})
|
|
_, err := flag.binding.prepare([]string{""})
|
|
if err == nil {
|
|
t.Fatal("BoolFlag accepted an explicitly empty value")
|
|
}
|
|
if _, err := flag.binding.prepare([]string{"true"}); err != nil {
|
|
t.Fatalf("BoolFlag rejected the presence value: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRepeatFlagUsesFreshDefaultsAndWholeOccurrenceValues(t *testing.T) {
|
|
defaults := []string{"base"}
|
|
flag := RepeatFlag(FlagMeta{Name: "tag"}, defaults, func(value string) (string, error) { return value, nil }, func(input *bindingInput, value Supplied[[]string]) { input.tags = value })
|
|
defaults[0] = "changed after declaration"
|
|
|
|
applyDefault, err := flag.binding.prepare(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
first := &bindingInput{}
|
|
applyDefault.apply(first)
|
|
second := &bindingInput{}
|
|
applyDefault.apply(second)
|
|
if !reflect.DeepEqual(first.tags.Value, []string{"base"}) || first.tags.Set {
|
|
t.Fatalf("omitted repeat value = %#v, want copied default with Set=false", first.tags)
|
|
}
|
|
first.tags.Value[0] = "changed one input"
|
|
if !reflect.DeepEqual(second.tags.Value, []string{"base"}) {
|
|
t.Fatalf("repeat default collection was reused: %#v", second.tags.Value)
|
|
}
|
|
|
|
applyValues, err := flag.binding.prepare([]string{"a,b", "c"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
third := &bindingInput{}
|
|
applyValues.apply(third)
|
|
if !reflect.DeepEqual(third.tags.Value, []string{"a,b", "c"}) || !third.tags.Set {
|
|
t.Fatalf("supplied repeat values = %#v, want two ordered occurrences", third.tags)
|
|
}
|
|
}
|
|
|
|
func TestSingletonParsesEveryOccurrenceAndPreservesCallerCause(t *testing.T) {
|
|
parseFailure := errors.New("bad number")
|
|
flag := ValueFlag[struct{}, int](FlagMeta{Name: "count"}, nil, func(value string) (int, error) {
|
|
if value == "bad" {
|
|
return 0, parseFailure
|
|
}
|
|
return strconv.Atoi(value)
|
|
}, func(*struct{}, Supplied[int]) {})
|
|
_, err := flag.binding.prepare([]string{"bad", "9"})
|
|
if !errors.Is(err, parseFailure) {
|
|
t.Fatalf("parse error = %v, want caller cause", err)
|
|
}
|
|
}
|
|
|
|
func TestPreparedScalarConvertsEveryOccurrenceOnceBeforeInputCreation(t *testing.T) {
|
|
parseCalls := 0
|
|
var received Supplied[string]
|
|
flag := ValueFlag(FlagMeta{Name: "value"}, (*string)(nil), func(value string) (string, error) {
|
|
parseCalls++
|
|
return value, nil
|
|
}, func(input *bindingInput, value Supplied[string]) { input.message = value })
|
|
prepared, err := flag.binding.prepare([]string{"earlier", "last"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if parseCalls != 2 || prepared.identity != flag.binding.identity || !prepared.supplied {
|
|
t.Fatalf("prepared scalar = %#v, parse calls = %d", prepared, parseCalls)
|
|
}
|
|
copy := flag
|
|
lookalike := ValueFlag(FlagMeta{Name: "value"}, (*string)(nil), func(value string) (string, error) { return value, nil }, func(*bindingInput, Supplied[string]) {})
|
|
if copy.binding.identity != flag.binding.identity || lookalike.binding.identity == flag.binding.identity {
|
|
t.Fatal("binding identity was not preserved by copying or distinct across constructors")
|
|
}
|
|
input := &bindingInput{}
|
|
prepared.apply(input)
|
|
received = input.message
|
|
if parseCalls != 2 || received.Value != "last" || !received.Set {
|
|
t.Fatalf("applied value = %#v, parse calls = %d", received, parseCalls)
|
|
}
|
|
}
|
|
|
|
func TestInvalidBindingConstructionIsReportedAtInstallation(t *testing.T) {
|
|
badFlag := ValueFlag[struct{}, string](FlagMeta{Name: "broken"}, nil, nil, func(*struct{}, Supplied[string]) {})
|
|
badArgument := RepeatArgument[struct{}, string](ArgumentMeta{Name: "items"}, 2, 1, func(value string) (string, error) { return value, nil }, func(*struct{}, Supplied[[]string]) {})
|
|
command := NewCommand(CommandSpec[struct{}]{Name: "run", Flags: []Flag[struct{}]{badFlag}, Arguments: []Argument[struct{}]{badArgument}})
|
|
app, err := New(Identity{Name: "example"}, command)
|
|
if err == nil {
|
|
t.Fatal("installation accepted nil parser wiring and invalid argument cardinality")
|
|
}
|
|
if app != nil {
|
|
t.Fatal("failed installation returned a usable app")
|
|
}
|
|
}
|
|
|
|
func TestRepeatArgumentAllocationReservesLaterMinimums(t *testing.T) {
|
|
type operandsInput struct {
|
|
prefix Supplied[[]string]
|
|
id Supplied[string]
|
|
suffix Supplied[[]string]
|
|
}
|
|
spec := CommandSpec[operandsInput]{
|
|
Name: "restore",
|
|
Arguments: []Argument[operandsInput]{
|
|
RepeatArgument(ArgumentMeta{Name: "prefix"}, 0, 2, func(value string) (string, error) { return value, nil }, func(input *operandsInput, value Supplied[[]string]) { input.prefix = value }),
|
|
ValueArgument(ArgumentMeta{Name: "id"}, true, func(value string) (string, error) { return value, nil }, func(input *operandsInput, value Supplied[string]) { input.id = value }),
|
|
RepeatArgument(ArgumentMeta{Name: "suffix"}, 1, -1, func(value string) (string, error) { return value, nil }, func(input *operandsInput, value Supplied[[]string]) { input.suffix = value }),
|
|
},
|
|
}
|
|
if err := validateCommandDeclaration(NewCommand(spec)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
prepared, err := prepareInput(spec, nil, []string{"a", "b", "id-1", "x", "y"}, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
input := prepared.build()
|
|
if !reflect.DeepEqual(input.prefix.Value, []string{"a", "b"}) || !input.prefix.Set {
|
|
t.Fatalf("prefix = %#v", input.prefix)
|
|
}
|
|
if input.id.Value != "id-1" || !input.id.Set {
|
|
t.Fatalf("id = %#v", input.id)
|
|
}
|
|
if !reflect.DeepEqual(input.suffix.Value, []string{"x", "y"}) || !input.suffix.Set {
|
|
t.Fatalf("suffix = %#v", input.suffix)
|
|
}
|
|
}
|
|
|
|
func TestIncompletePreparationSkipsMissingRequirementsAndInputFactory(t *testing.T) {
|
|
factoryCalls := 0
|
|
spec := CommandSpec[struct{}]{
|
|
Name: "show",
|
|
NewInput: func() struct{} {
|
|
factoryCalls++
|
|
return struct{}{}
|
|
},
|
|
Flags: []Flag[struct{}]{ValueFlag(FlagMeta{Name: "required", Required: true}, (*string)(nil), func(value string) (string, error) { return value, nil }, func(*struct{}, Supplied[string]) {})},
|
|
Arguments: []Argument[struct{}]{ValueArgument(ArgumentMeta{Name: "id"}, true, func(value string) (string, error) { return value, nil }, func(*struct{}, Supplied[string]) {})},
|
|
}
|
|
if err := validateCommandDeclaration(NewCommand(spec)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := prepareInput(spec, [][]string{nil}, nil, true); err == nil {
|
|
t.Fatal("strict preparation accepted missing requirements")
|
|
}
|
|
if _, err := prepareInput(spec, [][]string{nil}, nil, false); err != nil {
|
|
t.Fatalf("help preparation rejected omitted execution inputs: %v", err)
|
|
}
|
|
if factoryCalls != 0 {
|
|
t.Fatalf("preparation invoked NewInput %d times", factoryCalls)
|
|
}
|
|
}
|
|
|
|
func TestExecuteBindsMixedFlagSpellingsWithFreshPresence(t *testing.T) {
|
|
type input struct {
|
|
count Supplied[int]
|
|
enabled Supplied[bool]
|
|
message Supplied[string]
|
|
tags Supplied[[]string]
|
|
required Supplied[string]
|
|
instance int
|
|
}
|
|
type observed struct {
|
|
count Supplied[int]
|
|
enabled Supplied[bool]
|
|
message Supplied[string]
|
|
tags Supplied[[]string]
|
|
required Supplied[string]
|
|
instance int
|
|
}
|
|
defaultCount := 9
|
|
defaultMessage := "fallback"
|
|
defaultTags := []string{"base"}
|
|
countParses, inputFactories := 0, 0
|
|
var calls []observed
|
|
command := NewCommand(CommandSpec[input]{
|
|
Name: "run",
|
|
NewInput: func() input {
|
|
inputFactories++
|
|
return input{instance: inputFactories}
|
|
},
|
|
Flags: []Flag[input]{
|
|
ValueFlag(FlagMeta{Name: "count"}, &defaultCount, func(value string) (int, error) {
|
|
countParses++
|
|
return strconv.Atoi(value)
|
|
}, func(target *input, value Supplied[int]) { target.count = value }),
|
|
BoolFlag(FlagMeta{Name: "enabled"}, true, func(target *input, value Supplied[bool]) { target.enabled = value }),
|
|
ValueFlag(FlagMeta{Name: "message"}, &defaultMessage, func(value string) (string, error) { return value, nil }, func(target *input, value Supplied[string]) { target.message = value }),
|
|
RepeatFlag(FlagMeta{Name: "tag"}, defaultTags, func(value string) (string, error) { return value, nil }, func(target *input, value Supplied[[]string]) { target.tags = value }),
|
|
ValueFlag(FlagMeta{Name: "required", Required: true}, (*string)(nil), func(value string) (string, error) { return value, nil }, func(target *input, value Supplied[string]) { target.required = value }),
|
|
},
|
|
Run: func(_ Invocation, value input) Outcome {
|
|
value.tags.Value = append([]string(nil), value.tags.Value...)
|
|
calls = append(calls, observed(value))
|
|
return Success()
|
|
},
|
|
})
|
|
app, err := New(Identity{Name: "Example"}, command,
|
|
Alias("shortcut", "run"),
|
|
FlagAliasFor("run", "--number", "--count"),
|
|
FlagAliasFor("run", "--active", "--enabled"),
|
|
FlagAliasFor("run", "--text", "--message"),
|
|
FlagAliasFor("run", "--label", "--tag"),
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for _, args := range [][]string{
|
|
{"shortcut", "--number=0", "--count=5", "--active=false", "--enabled=true", "--text=", "--message=last", "--label=a", "--tag=b", "--required=first"},
|
|
{"run", "--number=0", "--active=false", "--text=", "--label=c", "--required=second"},
|
|
} {
|
|
if outcome := app.Execute(context.Background(), args, Streams{Out: io.Discard, Err: io.Discard}); outcome.Kind != OutcomeSuccess {
|
|
t.Fatalf("Execute(%q) = %#v, want success", args, outcome)
|
|
}
|
|
}
|
|
if countParses != 3 {
|
|
t.Fatalf("count conversions = %d, want every one of three occurrences", countParses)
|
|
}
|
|
if len(calls) != 2 || calls[0].count != (Supplied[int]{Value: 5, Set: true}) || calls[0].enabled != (Supplied[bool]{Value: true, Set: true}) || calls[0].message != (Supplied[string]{Value: "last", Set: true}) || !reflect.DeepEqual(calls[0].tags, Supplied[[]string]{Value: []string{"a", "b"}, Set: true}) || calls[0].required != (Supplied[string]{Value: "first", Set: true}) {
|
|
t.Fatalf("first mixed-spelling invocation = %#v", calls)
|
|
}
|
|
if calls[1].count != (Supplied[int]{Value: 0, Set: true}) || calls[1].enabled != (Supplied[bool]{Value: false, Set: true}) || calls[1].message != (Supplied[string]{Value: "", Set: true}) || !reflect.DeepEqual(calls[1].tags, Supplied[[]string]{Value: []string{"c"}, Set: true}) || calls[1].required != (Supplied[string]{Value: "second", Set: true}) || calls[0].instance == calls[1].instance {
|
|
t.Fatalf("second mixed-spelling invocation = %#v", calls[1])
|
|
}
|
|
|
|
if outcome := app.Execute(context.Background(), []string{"shortcut", "--count=7"}, Streams{Out: io.Discard, Err: io.Discard}); outcome.Kind != OutcomeUsage {
|
|
t.Fatalf("missing required flag outcome = %#v, want usage", outcome)
|
|
}
|
|
if inputFactories != 2 || len(calls) != 2 {
|
|
t.Fatalf("missing required flag created or ran input: factories=%d calls=%d", inputFactories, len(calls))
|
|
}
|
|
if outcome := app.Execute(context.Background(), []string{"shortcut", "--required=defaults"}, Streams{Out: io.Discard, Err: io.Discard}); outcome.Kind != OutcomeSuccess {
|
|
t.Fatalf("defaulted invocation = %#v, want success", outcome)
|
|
}
|
|
if len(calls) != 3 || calls[2].count != (Supplied[int]{Value: 9}) || calls[2].enabled != (Supplied[bool]{Value: true}) || calls[2].message != (Supplied[string]{Value: "fallback"}) || !reflect.DeepEqual(calls[2].tags, Supplied[[]string]{Value: []string{"base"}}) || calls[2].required != (Supplied[string]{Value: "defaults", Set: true}) || calls[2].instance == calls[1].instance {
|
|
t.Fatalf("omitted values reused prior invocation state: %#v", calls[2])
|
|
}
|
|
}
|