test apply

This commit is contained in:
Arpad Ryszka 2025-09-06 21:38:50 +02:00
parent e348cce3d2
commit 890fae55ca
10 changed files with 362 additions and 52 deletions

View File

@ -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
View 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", ""))
})
}

View File

@ -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) {

View File

@ -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)")
}

View File

@ -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
View 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", ""),
)
})
}

View File

@ -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

View File

@ -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
}
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
}
if _, ok := v.(fmt.Stringer); ok {
_, err := fmt.Fprintln(out, v)
return err
}
r := reflect.ValueOf(v)
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
View File

@ -0,0 +1,12 @@
package wand
import "testing"
func TestOutput(t *testing.T) {
// nil
// simple
// stringer
// pointer
// complex
// reader
}

View File

@ -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 {