test apply
This commit is contained in:
parent
e348cce3d2
commit
890fae55ca
2
apply.go
2
apply.go
@ -73,7 +73,7 @@ func createArgs(stdin io.Reader, stdout io.Writer, t reflect.Type, shortForms []
|
|||||||
case iow:
|
case iow:
|
||||||
args = append(args, reflect.ValueOf(stdout))
|
args = append(args, reflect.ValueOf(stdout))
|
||||||
case structure && variadic:
|
case structure && variadic:
|
||||||
if arg, ok := createStructArg(ti, shortForms, c, e, cl.options); ok {
|
if arg, ok := createStructArg(ti.Elem(), shortForms, c, e, cl.options); ok {
|
||||||
args = append(args, arg)
|
args = append(args, arg)
|
||||||
}
|
}
|
||||||
case structure:
|
case structure:
|
||||||
|
|||||||
129
apply_test.go
Normal file
129
apply_test.go
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
package wand
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestApply(t *testing.T) {
|
||||||
|
t.Run("input", func(t *testing.T) {
|
||||||
|
type s0 struct {
|
||||||
|
Foo string
|
||||||
|
Bar string
|
||||||
|
}
|
||||||
|
|
||||||
|
type s2 struct {
|
||||||
|
Foo bool
|
||||||
|
Bar bool
|
||||||
|
}
|
||||||
|
|
||||||
|
f0 := func(a s0) string { return a.Foo + a.Bar }
|
||||||
|
f0a := func(a ...s0) int { return len(a) }
|
||||||
|
f0b := func(a *s0) string { return a.Foo + a.Bar }
|
||||||
|
f0c := func(a []s0) int { return len(a) }
|
||||||
|
f0d := func(a *[]**s0) int { return len(*a) }
|
||||||
|
f0io := func(out io.Writer, a s0, in io.Reader) { io.Copy(out, in); out.Write([]byte(a.Foo + a.Bar)) }
|
||||||
|
f1 := func(a, b, c int) int { return a + b + c }
|
||||||
|
f1a := func(a, b int, c ...int) int { return a + b + len(c) }
|
||||||
|
f2 := func(a s2) bool { return a.Foo != a.Bar }
|
||||||
|
t.Run("config", testExec(testCase{impl: f0, command: "foo", conf: "foo=bar"}, "", "bar"))
|
||||||
|
t.Run("env", testExec(testCase{impl: f0, command: "foo", env: "foo_foo=bar"}, "", "bar"))
|
||||||
|
t.Run("options", testExec(testCase{impl: f0, command: "foo --foo bar"}, "", "bar"))
|
||||||
|
t.Run(
|
||||||
|
"env overrides config",
|
||||||
|
testExec(
|
||||||
|
testCase{impl: f0, command: "foo", conf: "foo=bar\nbar=baz", env: "foo_foo=qux"},
|
||||||
|
"",
|
||||||
|
"quxbaz",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run(
|
||||||
|
"options override env and config",
|
||||||
|
testExec(
|
||||||
|
testCase{impl: f0, command: "foo --bar quux", conf: "foo=bar\nbar=baz", env: "foo_foo=qux"},
|
||||||
|
"",
|
||||||
|
"quxquux",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("variadic structure not set", testExec(testCase{impl: f0a, command: "foo"}, "", "0"))
|
||||||
|
t.Run(
|
||||||
|
"variadic structure from env",
|
||||||
|
testExec(testCase{impl: f0a, command: "foo", env: "foo_foo=bar"}, "", "1"),
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run(
|
||||||
|
"variadic structure from options",
|
||||||
|
testExec(testCase{impl: f0a, command: "foo --foo bar"}, "", "1"),
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run(
|
||||||
|
"variadic with multiple entries not allowed",
|
||||||
|
testExec(testCase{impl: f0a, command: "foo --foo bar --foo baz"}, "expected only one value", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run(
|
||||||
|
"pointer",
|
||||||
|
testExec(testCase{impl: f0b, command: "foo --foo bar"}, "", "bar"),
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run(
|
||||||
|
"list",
|
||||||
|
testExec(testCase{impl: f0c, command: "foo --foo bar"}, "", "1"),
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run(
|
||||||
|
"list with multiple entries not allowed",
|
||||||
|
testExec(testCase{impl: f0c, command: "foo --foo bar --foo baz"}, "expected only one value", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run(
|
||||||
|
"variadic list pointer",
|
||||||
|
testExec(testCase{impl: f0d, command: "foo --foo bar"}, "", "1"),
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run(
|
||||||
|
"reader and writer set",
|
||||||
|
testExec(
|
||||||
|
testCase{impl: f0io, stdin: "foobar", command: "foo --foo baz --bar qux"},
|
||||||
|
"",
|
||||||
|
"foobarbazqux",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("positional", testExec(testCase{impl: f1, command: "foo 1 2 3"}, "", "6"))
|
||||||
|
t.Run(
|
||||||
|
"variadic positional",
|
||||||
|
testExec(testCase{impl: f1a, command: "foo 1 2 3 4 5"}, "", "6"),
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("boolean options", testExec(testCase{impl: f2, command: "foo --foo --bar"}, "", "false"))
|
||||||
|
t.Run(
|
||||||
|
"short form options",
|
||||||
|
testExec(
|
||||||
|
testCase{impl: ShortForm(Command("foo", f0), "f", "foo"), command: "foo -f bar"},
|
||||||
|
"",
|
||||||
|
"bar",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("output", func(t *testing.T) {
|
||||||
|
f0 := func() {}
|
||||||
|
f1 := func() int { return 42 }
|
||||||
|
f2 := func() (int, int, int) { return 21, 42, 84 }
|
||||||
|
f3 := func() error { return nil }
|
||||||
|
f4 := func() error { return errors.New("test error") }
|
||||||
|
f5 := func() (int, int, error) { return 42, 84, nil }
|
||||||
|
f6 := func() (int, int, error) { return 42, 84, errors.New("test error") }
|
||||||
|
t.Run("no output", testExec(testCase{impl: f0, command: "foo"}, "", ""))
|
||||||
|
t.Run("non-error output", testExec(testCase{impl: f1, command: "foo"}, "", "42"))
|
||||||
|
t.Run("multiple outputs", testExec(testCase{impl: f2, command: "foo"}, "", "21\n42\n84"))
|
||||||
|
t.Run("error output no error", testExec(testCase{impl: f3, command: "foo"}, "", ""))
|
||||||
|
t.Run("error output error", testExec(testCase{impl: f4, command: "foo"}, "test error", ""))
|
||||||
|
t.Run("mixed output no error", testExec(testCase{impl: f5, command: "foo"}, "", "42\n84"))
|
||||||
|
t.Run("mixed output error", testExec(testCase{impl: f6, command: "foo"}, "test error", ""))
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -182,10 +182,10 @@ func readConfigFromOption(cmd Cmd, cl commandLine, conf Config) (config, error)
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
c = append(c, Config{file: func(Cmd) *file { return fileReader(o.value.str) }})
|
c = append(c, ConfigFile(o.value.str))
|
||||||
}
|
}
|
||||||
|
|
||||||
return readConfig(cmd, cl, Config{merge: c})
|
return readConfig(cmd, cl, MergeConfig(c...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func readMergeConfig(cmd Cmd, cl commandLine, conf Config) (config, error) {
|
func readMergeConfig(cmd Cmd, cl commandLine, conf Config) (config, error) {
|
||||||
|
|||||||
@ -2,9 +2,10 @@
|
|||||||
Generated with https://code.squareroundforest.org/arpio/docreflect
|
Generated with https://code.squareroundforest.org/arpio/docreflect
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
package wand
|
package wand
|
||||||
|
|
||||||
import "code.squareroundforest.org/arpio/docreflect"
|
import "code.squareroundforest.org/arpio/docreflect"
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools", "")
|
docreflect.Register("code.squareroundforest.org/arpio/wand/tools", "")
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.Docreflect", "\nfunc(out, packageName, gopaths)")
|
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.Docreflect", "\nfunc(out, packageName, gopaths)")
|
||||||
|
|||||||
@ -93,7 +93,7 @@ func testExec(test testCase, err string, expect ...string) func(*testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
output := stdout.String()
|
output := stdout.String()
|
||||||
if output[len(output)-1] != '\n' {
|
if output == "" || output[len(output)-1] != '\n' {
|
||||||
output = output + "\n"
|
output = output + "\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
155
input_test.go
Normal file
155
input_test.go
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
package wand
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestInput(t *testing.T) {
|
||||||
|
t.Run("keyvals", func(t *testing.T) {
|
||||||
|
type s0 struct {
|
||||||
|
Foo string
|
||||||
|
Bar int
|
||||||
|
}
|
||||||
|
|
||||||
|
f0 := func(a s0) string { return a.Foo }
|
||||||
|
t.Run(
|
||||||
|
"ignore if not defined",
|
||||||
|
testExec(
|
||||||
|
testCase{
|
||||||
|
impl: f0,
|
||||||
|
conf: "bar=42",
|
||||||
|
command: "foo",
|
||||||
|
},
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run(
|
||||||
|
"multiple values for field that does not allow lists",
|
||||||
|
testExec(
|
||||||
|
testCase{
|
||||||
|
impl: f0,
|
||||||
|
conf: "foo=42\nfoo=baz",
|
||||||
|
command: "foo",
|
||||||
|
},
|
||||||
|
"expected only one value",
|
||||||
|
"",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run(
|
||||||
|
"unscannable value",
|
||||||
|
testExec(
|
||||||
|
testCase{
|
||||||
|
impl: f0,
|
||||||
|
conf: "bar=baz",
|
||||||
|
command: "foo",
|
||||||
|
},
|
||||||
|
"type mismatch",
|
||||||
|
"",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("options", func(t *testing.T) {
|
||||||
|
type s0 struct {
|
||||||
|
Foo string
|
||||||
|
Bar int
|
||||||
|
}
|
||||||
|
|
||||||
|
f0 := func(a s0) string { return a.Foo }
|
||||||
|
t.Run(
|
||||||
|
"undefined short form",
|
||||||
|
testExec(testCase{impl: f0, command: "foo -f"}, "option not supported", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run(
|
||||||
|
"undefined option",
|
||||||
|
testExec(testCase{impl: f0, command: "foo --baz"}, "option not supported", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run(
|
||||||
|
"multiple values for field that does not allow lists",
|
||||||
|
testExec(testCase{impl: f0, command: "foo --foo bar --foo baz"}, "expected only one value", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run(
|
||||||
|
"bool value for field that does not accept it",
|
||||||
|
testExec(testCase{impl: f0, command: "foo --foo"}, "received boolean value", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run(
|
||||||
|
"cannot scan",
|
||||||
|
testExec(testCase{impl: f0, command: "foo --bar baz"}, "type mismatch", ""),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("positional args", func(t *testing.T) {
|
||||||
|
f := func(a, b, c int) int { return a + b + c }
|
||||||
|
|
||||||
|
var fv func(int, int, ...int) int
|
||||||
|
fv = func(a, b int, c ...int) int {
|
||||||
|
if len(c) == 0 {
|
||||||
|
return a + b
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c) == 1 {
|
||||||
|
return a + b + c[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return a + b + fv(c[0], c[1], c[2:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("min max ok", testExec(testCase{impl: f, command: "foo 1 2 3"}, "", "6"))
|
||||||
|
t.Run(
|
||||||
|
"min missed",
|
||||||
|
testExec(testCase{impl: f, command: "foo 1 2"}, "not enough positional arguments", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run(
|
||||||
|
"max missed",
|
||||||
|
testExec(testCase{impl: f, command: "foo 1 2 3 4"}, "too many positional arguments", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run(
|
||||||
|
"min max with variadic ok",
|
||||||
|
testExec(testCase{impl: fv, command: "foo 1 2 3 4 5"}, "", "15"),
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run(
|
||||||
|
"min with variadic missed",
|
||||||
|
testExec(testCase{impl: fv, command: "foo 1"}, "not enough positional arguments", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run(
|
||||||
|
"min max with variadic and constraints ok",
|
||||||
|
testExec(
|
||||||
|
testCase{impl: Args(Command("foo", fv), 3, 5), command: "foo 1 2 3 4"},
|
||||||
|
"",
|
||||||
|
"10",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run(
|
||||||
|
"min with variadic and constraints missed",
|
||||||
|
testExec(
|
||||||
|
testCase{impl: Args(Command("foo", fv), 3, 5), command: "foo 1 2"},
|
||||||
|
"not enough positional arguments",
|
||||||
|
"",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run(
|
||||||
|
"max with variadic and constraints missed",
|
||||||
|
testExec(
|
||||||
|
testCase{impl: Args(Command("foo", fv), 3, 5), command: "foo 1 2 3 4 5 6"},
|
||||||
|
"too many positional arguments",
|
||||||
|
"",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run(
|
||||||
|
"cannot scan",
|
||||||
|
testExec(testCase{impl: f, command: "foo 42 bar 84"}, "cannot apply positional argument", ""),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -1,6 +1,5 @@
|
|||||||
|
turn testExec into a wandtesting package
|
||||||
use a type cache
|
use a type cache
|
||||||
support unix timestamps in bind and in reflect
|
|
||||||
verify that duration can be parsed from integer strings
|
|
||||||
test:
|
test:
|
||||||
- nil return values
|
- nil return values
|
||||||
- options in variadic
|
- options in variadic
|
||||||
|
|||||||
38
output.go
38
output.go
@ -8,32 +8,20 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func fprintOne(out io.Writer, v any) error {
|
func fprintOne(out io.Writer, v any) error {
|
||||||
reader, ok := v.(io.Reader)
|
if reader, ok := v.(io.Reader); ok {
|
||||||
if ok {
|
|
||||||
_, err := io.Copy(out, reader)
|
_, err := io.Copy(out, reader)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, ok := v.(fmt.Stringer); ok {
|
||||||
|
_, err := fmt.Fprintln(out, v)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
r := reflect.ValueOf(v)
|
r := reflect.ValueOf(v)
|
||||||
if r.IsValid() {
|
|
||||||
t := r.Type()
|
|
||||||
if t.Implements(reflect.TypeFor[fmt.Stringer]()) {
|
|
||||||
_, err := fmt.Fprintln(out, r.Interface())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if t.Kind() == reflect.Slice {
|
|
||||||
for i := 0; i < r.Len(); i++ {
|
|
||||||
if err := fprintOne(out, r.Index(i).Interface()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch r.Kind() {
|
switch r.Kind() {
|
||||||
|
case reflect.Invalid:
|
||||||
|
return nil
|
||||||
case reflect.Bool,
|
case reflect.Bool,
|
||||||
reflect.Int,
|
reflect.Int,
|
||||||
reflect.Int8,
|
reflect.Int8,
|
||||||
@ -51,6 +39,16 @@ func fprintOne(out io.Writer, v any) error {
|
|||||||
reflect.String:
|
reflect.String:
|
||||||
_, err := fmt.Fprintln(out, v)
|
_, err := fmt.Fprintln(out, v)
|
||||||
return err
|
return err
|
||||||
|
case reflect.Slice:
|
||||||
|
for i := 0; i < r.Len(); i++ {
|
||||||
|
if err := fprintOne(out, r.Index(i).Interface()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
case reflect.Pointer:
|
||||||
|
return fprintOne(out, r.Elem().Interface())
|
||||||
default:
|
default:
|
||||||
_, err := notation.Fprintlnwt(out, v)
|
_, err := notation.Fprintlnwt(out, v)
|
||||||
return err
|
return err
|
||||||
|
|||||||
12
output_test.go
Normal file
12
output_test.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package wand
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestOutput(t *testing.T) {
|
||||||
|
// nil
|
||||||
|
// simple
|
||||||
|
// stringer
|
||||||
|
// pointer
|
||||||
|
// complex
|
||||||
|
// reader
|
||||||
|
}
|
||||||
22
reflect.go
22
reflect.go
@ -8,6 +8,11 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
structFieldsCache = make(map[reflect.Type][]bind.Field)
|
||||||
|
bindableTypes = make(map[reflect.Type]bool)
|
||||||
|
)
|
||||||
|
|
||||||
func filter[T any](list []T, predicate func(T) bool) []T {
|
func filter[T any](list []T, predicate func(T) bool) []T {
|
||||||
var filtered []T
|
var filtered []T
|
||||||
for _, item := range list {
|
for _, item := range list {
|
||||||
@ -193,8 +198,13 @@ func structParameters(f any) []reflect.Type {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func structFields(s reflect.Type) []bind.Field {
|
func structFields(s reflect.Type) []bind.Field {
|
||||||
s = unpackType(s)
|
if f, ok := structFieldsCache[s]; ok {
|
||||||
return bind.FieldsOf(s)
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
f := bind.FieldsOf(s)
|
||||||
|
structFieldsCache[s] = f
|
||||||
|
return f
|
||||||
}
|
}
|
||||||
|
|
||||||
func fields(f any) []bind.Field {
|
func fields(f any) []bind.Field {
|
||||||
@ -260,7 +270,13 @@ func ioParameters(f any) ([]reflect.Type, []reflect.Type) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func bindable(t reflect.Type) bool {
|
func bindable(t reflect.Type) bool {
|
||||||
return bind.BindableType(t)
|
if bindable, ok := bindableTypes[t]; ok {
|
||||||
|
return bindable
|
||||||
|
}
|
||||||
|
|
||||||
|
bindable := bind.BindableType(t)
|
||||||
|
bindableTypes[t] = bindable
|
||||||
|
return bindable
|
||||||
}
|
}
|
||||||
|
|
||||||
func scalarTypeString(t bind.FieldType) string {
|
func scalarTypeString(t bind.FieldType) string {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user