wand/command_test.go

348 lines
8.7 KiB
Go
Raw Normal View History

2025-09-06 02:46:28 +02:00
package wand
import (
"fmt"
"io"
"testing"
)
func TestCommand(t *testing.T) {
t.Run("fields", func(t *testing.T) {
type s0 struct {
FooBar int
Foo struct{ Bar bool }
}
type s1 struct {
Config string
}
f0 := func(a s0) int { return a.FooBar }
t.Run(
"incompatible field types",
testExec(testCase{impl: f0, command: "foo"}, "duplicate fields with different types", ""),
)
f1 := func(a s1) string { return a.Config }
t.Run(
"config option shadowed",
testExec(
testCase{impl: f1, mergeConfTyped: []Config{ConfigFromOption()}, command: "foo"},
"option reserved for config",
"",
),
)
})
t.Run("positional", func(t *testing.T) {
f := func(a, b, c int) int { return a + b + c }
t.Run("ok", testExec(testCase{impl: Args(Command("foo", f), 3, 3), command: "foo 1 2 3"}, "", "6"))
t.Run(
"min less than fixed",
testExec(
testCase{impl: Args(Command("foo", f), 2, 3), command: "foo 1 2 3"},
"minimum positional",
"",
),
)
t.Run(
"min more than fixed",
testExec(
testCase{impl: Args(Command("foo", f), 4, 4), command: "foo 1 2 3"},
"minimum positional",
"",
),
)
t.Run(
"max less than fixed",
testExec(
testCase{impl: Args(Command("foo", f), 0, 2), command: "foo 1 2 3"},
"maximum positional",
"",
),
)
fv := func(a, b int, c ...int) int { return a + b + len(c) }
t.Run("ok variadic", testExec(testCase{impl: Args(Command("foo", fv), 3, 5), command: "foo 1 2 3 4 5"}, "", "6"))
t.Run(
"min less than fixed variadic",
testExec(
testCase{impl: Args(Command("foo", fv), 1, 5), command: "foo 1 2 3"},
"minimum positional",
"",
),
)
t.Run(
"max less than fixed variadic",
testExec(
testCase{impl: Args(Command("foo", fv), 0, 1), command: "foo 1 2 3"},
"maximum positional",
"",
),
)
t.Run(
"min more than max variadic",
testExec(
testCase{impl: Args(Command("foo", fv), 4, 3), command: "foo 1 2 3 4 5"},
"minimum positional",
"",
),
)
fvio := func(a int, in io.Reader, out io.Writer, b int, c ...int) int { return a + b + len(c) }
t.Run("ok io", testExec(testCase{impl: Args(Command("foo", fvio), 3, 5), command: "foo 1 2 3 4 5"}, "", "6"))
t.Run(
"min less than fixed io",
testExec(
testCase{impl: Args(Command("foo", fvio), 1, 5), command: "foo 1 2 3"},
"minimum positional",
"",
),
)
t.Run(
"max less than fixed io",
testExec(
testCase{impl: Args(Command("foo", fvio), 0, 1), command: "foo 1 2 3"},
"maximum positional",
"",
),
)
t.Run(
"min more than max io",
testExec(
testCase{impl: Args(Command("foo", fvio), 4, 3), command: "foo 1 2 3 4 5"},
"minimum positional",
"",
),
)
fio := func(a, b io.Reader) {}
t.Run(
"max one io of a kind",
testExec(testCase{impl: fio, command: "foo"}, "only zero or one reader", ""),
)
})
t.Run("impl", func(t *testing.T) {
t.Run(
"must be func",
testExec(
testCase{impl: []func(){func() {}}, command: "foo"},
"implementation must be a function",
"",
),
)
t.Run(
"paramters must be bindable",
testExec(testCase{impl: func(chan int) {}, command: "foo"}, "unsupported parameter type", ""),
)
})
t.Run("command tree", func(t *testing.T) {
f := func(a, b int) int { return a + b }
cmd := Command("foo", f)
cmd = Version(cmd, "v42")
t.Run("version pass", testExec(testCase{impl: cmd, command: "foo 1 2"}, "", "3"))
cmd = Command("foo", nil)
t.Run(
"no impl and not group",
testExec(testCase{impl: cmd, command: "foo"}, "command does not have an implementation", ""),
)
cmd = Group("foo")
t.Run(
"no impl and no subcommands",
testExec(testCase{impl: cmd, command: "foo"}, "empty command category", ""),
)
cmd = Group("foo", Command("bar-baz", func() {}), Command("qux quux", func() {}))
t.Run(
"invalid subcommand name",
testExec(testCase{impl: cmd, command: "foo bar-baz"}, "command name is not a valid", ""),
)
cmd = Group("foo", Command("bar", func() {}), Command("bar", func() {}))
t.Run(
"duplicate subcommand name",
testExec(testCase{impl: cmd, command: "foo bar"}, "subcommand name conflict", ""),
)
cmd = Command("foo", func() {}, Default(Command("bar", func() {})), Command("bar", func() {}))
t.Run(
"implementation conflict",
testExec(
testCase{impl: cmd, command: "foo bar"},
"default subcommand defined for a command that has an explicit implementation",
"",
),
)
cmd = Group("foo", Default(Command("bar", func() {})), Default(Command("baz", func() {})))
t.Run(
"multiple defaults",
testExec(
testCase{impl: cmd, command: "foo bar"},
"multiple default subcommands",
"",
),
)
cmd = Command("foo", func() {}, Command("bar", func() {}), Command("baz", func(chan int) {}))
t.Run(
"invalid subcommand",
testExec(
testCase{impl: cmd, command: "foo bar"},
"foo:",
"",
),
)
})
t.Run("short forms", func(t *testing.T) {
type (
s0 struct {
Foo int
Qux int
}
s1 struct{ Bar int }
s2 struct{ Baz int }
)
f0 := func(a s0) int { return a.Foo }
f1 := func(a s1) int { return a.Bar }
f2 := func(a s2) int { return a.Baz }
cmd := ShortForm(Command("foo", f0, ShortForm(Command("bar", f1), "a", "bar")), "a", "foo")
t.Run(
"same short form for different options",
testExec(testCase{impl: cmd, command: "foo"}, "same short form for different options", ""),
)
cmd = ShortForm(Command("foo", f0), "a", "foo", "a", "qux")
t.Run(
"same short form for different options on the same command",
testExec(testCase{impl: cmd, command: "foo"}, "same short form for different options", ""),
)
cmd = ShortForm(Command("foo", f0, Command("bar", f1)), "a", "foo", "a", "qux")
t.Run(
"same short form for different options on the same command with subcommand",
testExec(testCase{impl: cmd, command: "foo"}, "same short form for different options", ""),
)
cmd = Command(
"foo",
f0,
ShortForm(Command("bar", f1), "a", "bar"),
ShortForm(Command("baz", f2), "a", "baz"),
)
t.Run(
"same short form for different options on different branches in the tree",
testExec(testCase{impl: cmd, command: "foo"}, "same short form for different options", ""),
)
cmd = ShortForm(Command("foo", f0, ShortForm(Command("bar", f1), "b", "baz")), "a", "foo")
t.Run(
"unmapped short form",
testExec(testCase{impl: cmd, command: "foo"}, "unmapped short forms", ""),
)
cmd = ShortForm(
Command(
"foo",
f0,
ShortForm(Command("bar", f1), "b", "baz"),
ShortForm(Command("qux", f1), "b", "qux"),
),
"a",
"foo",
)
t.Run(
"conflicting unmapped",
testExec(
testCase{impl: cmd, command: "foo"},
"using the same short form for different options",
"",
),
)
cmd = ShortForm(Command("foo", f0), "ab", "foo")
t.Run(
"short form not a single character",
testExec(testCase{impl: cmd, command: "foo"}, "invalid short form", ""),
)
cmd = ShortForm(Command("foo", f0), "6", "foo")
t.Run(
"short form not a single character",
testExec(testCase{impl: cmd, command: "foo"}, "invalid short form", ""),
)
cmd = ShortForm(Group("foo", Command("bar", f1)), "b", "bar")
t.Run(
"short form with command category",
testExec(testCase{impl: cmd, command: "foo bar"}, "", "0"),
)
})
t.Run("bool options", func(t *testing.T) {
type s0 struct {
Foo int
Bar bool
Baz bool
}
type s1 struct {
Foo int
Bar bool
Baz bool
Help bool
}
f0 := func(a s0, b, c string) string {
return fmt.Sprint(a.Foo) + fmt.Sprint(a.Bar) + fmt.Sprint(a.Baz) + b + c
}
f1 := func(a s1, b, c string) string {
return fmt.Sprint(a.Foo) + fmt.Sprint(a.Bar) + fmt.Sprint(a.Baz) + fmt.Sprint(a.Help) + b + c
}
cmd := ShortForm(Command("foo", f0), "b", "bar")
t.Run(
"no help in fields and no help in short forms",
testExec(testCase{impl: cmd, command: "foo --foo 42 -b qux --baz quux"}, "", "42truetruequxquux"),
)
t.Run(
"no help in fields and no help in short forms help",
testExec(testCase{impl: cmd, command: "foo --help", contains: true}, "", "Synopsis"),
)
cmd = ShortForm(Command("foo", f0), "h", "help")
t.Run(
"help from short forms",
testExec(testCase{impl: cmd, command: "foo -h", contains: true}, "", "Synopsis"),
)
cmd = ShortForm(Command("foo", f1), "b", "bar")
t.Run(
"help taken by a field",
testExec(testCase{impl: cmd, command: "foo --help qux quux"}, "", "0falsefalsetruequxquux"),
)
cmd = ShortForm(Command("foo", f1), "h", "bar")
t.Run(
"help taken by a field and help short form used for another field",
testExec(testCase{impl: cmd, command: "foo --help qux -h quux"}, "", "0truefalsetruequxquux"),
)
})
}