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:
|
||||
args = append(args, reflect.ValueOf(stdout))
|
||||
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)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@ -2,30 +2,31 @@
|
||||
Generated with https://code.squareroundforest.org/arpio/docreflect
|
||||
*/
|
||||
|
||||
|
||||
package wand
|
||||
|
||||
import "code.squareroundforest.org/arpio/docreflect"
|
||||
|
||||
func init() {
|
||||
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.Exec", "\nfunc(o, stdin, args)")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions", "")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.CacheDir", "")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.ClearCache", "")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.Import", "")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.InlineImport", "")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.NoCache", "")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.Man", "\nfunc(out, commandDir)")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.Markdown", "\nfunc(out, o, commandDir)")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.MarkdownOptions", "")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.MarkdownOptions.Level", "")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.commandReader", "\nfunc(in)")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execCommandDir", "\nfunc(out, commandDir, env)")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execInternal", "\nfunc(command, args)")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execTransparent", "\nfunc(command, args)")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execWand", "\nfunc(o, args)")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execc", "\nfunc(stdin, stdout, stderr, command, args, env)")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.hash", "\nfunc(expression, imports, inlineImports)")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.printGoFile", "\nfunc(fn, expression, imports, inlineImports)")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.readExec", "\nfunc(o, stdin)")
|
||||
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.Exec", "\nfunc(o, stdin, args)")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions", "")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.CacheDir", "")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.ClearCache", "")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.Import", "")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.InlineImport", "")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.NoCache", "")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.Man", "\nfunc(out, commandDir)")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.Markdown", "\nfunc(out, o, commandDir)")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.MarkdownOptions", "")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.MarkdownOptions.Level", "")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.commandReader", "\nfunc(in)")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execCommandDir", "\nfunc(out, commandDir, env)")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execInternal", "\nfunc(command, args)")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execTransparent", "\nfunc(command, args)")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execWand", "\nfunc(o, args)")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execc", "\nfunc(stdin, stdout, stderr, command, args, env)")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.hash", "\nfunc(expression, imports, inlineImports)")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.printGoFile", "\nfunc(fn, expression, imports, inlineImports)")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.readExec", "\nfunc(o, stdin)")
|
||||
}
|
||||
@ -93,7 +93,7 @@ func testExec(test testCase, err string, expect ...string) func(*testing.T) {
|
||||
}
|
||||
|
||||
output := stdout.String()
|
||||
if output[len(output)-1] != '\n' {
|
||||
if output == "" || output[len(output)-1] != '\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
|
||||
support unix timestamps in bind and in reflect
|
||||
verify that duration can be parsed from integer strings
|
||||
test:
|
||||
- nil return values
|
||||
- options in variadic
|
||||
|
||||
38
output.go
38
output.go
@ -8,32 +8,20 @@ import (
|
||||
)
|
||||
|
||||
func fprintOne(out io.Writer, v any) error {
|
||||
reader, ok := v.(io.Reader)
|
||||
if ok {
|
||||
if reader, ok := v.(io.Reader); ok {
|
||||
_, err := io.Copy(out, reader)
|
||||
return err
|
||||
}
|
||||
|
||||
if _, ok := v.(fmt.Stringer); ok {
|
||||
_, err := fmt.Fprintln(out, v)
|
||||
return err
|
||||
}
|
||||
|
||||
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() {
|
||||
case reflect.Invalid:
|
||||
return nil
|
||||
case reflect.Bool,
|
||||
reflect.Int,
|
||||
reflect.Int8,
|
||||
@ -51,6 +39,16 @@ func fprintOne(out io.Writer, v any) error {
|
||||
reflect.String:
|
||||
_, err := fmt.Fprintln(out, v)
|
||||
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:
|
||||
_, err := notation.Fprintlnwt(out, v)
|
||||
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"
|
||||
)
|
||||
|
||||
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 {
|
||||
var filtered []T
|
||||
for _, item := range list {
|
||||
@ -193,8 +198,13 @@ func structParameters(f any) []reflect.Type {
|
||||
}
|
||||
|
||||
func structFields(s reflect.Type) []bind.Field {
|
||||
s = unpackType(s)
|
||||
return bind.FieldsOf(s)
|
||||
if f, ok := structFieldsCache[s]; ok {
|
||||
return f
|
||||
}
|
||||
|
||||
f := bind.FieldsOf(s)
|
||||
structFieldsCache[s] = f
|
||||
return f
|
||||
}
|
||||
|
||||
func fields(f any) []bind.Field {
|
||||
@ -260,7 +270,13 @@ func ioParameters(f any) ([]reflect.Type, []reflect.Type) {
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user