From 890fae55caf74c58b3b0289b081a2acf8ebdf818 Mon Sep 17 00:00:00 2001 From: Arpad Ryszka Date: Sat, 6 Sep 2025 21:38:50 +0200 Subject: [PATCH] test apply --- apply.go | 2 +- apply_test.go | 129 ++++++++++++++++++++++++++++++++++++++ config.go | 4 +- docreflect.gen.go | 49 ++++++++------- exec_test.go | 2 +- input_test.go | 155 ++++++++++++++++++++++++++++++++++++++++++++++ notes.txt | 3 +- output.go | 36 +++++------ output_test.go | 12 ++++ reflect.go | 22 ++++++- 10 files changed, 362 insertions(+), 52 deletions(-) create mode 100644 apply_test.go create mode 100644 input_test.go create mode 100644 output_test.go diff --git a/apply.go b/apply.go index 8cb662c..dc1086c 100644 --- a/apply.go +++ b/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: diff --git a/apply_test.go b/apply_test.go new file mode 100644 index 0000000..91ae8bc --- /dev/null +++ b/apply_test.go @@ -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", "")) + }) +} diff --git a/config.go b/config.go index df273b3..42d2cf0 100644 --- a/config.go +++ b/config.go @@ -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) { diff --git a/docreflect.gen.go b/docreflect.gen.go index 45379f1..c003166 100644 --- a/docreflect.gen.go +++ b/docreflect.gen.go @@ -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)") -} \ No newline at end of file + 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)") +} diff --git a/exec_test.go b/exec_test.go index a0aae87..b95632e 100644 --- a/exec_test.go +++ b/exec_test.go @@ -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" } diff --git a/input_test.go b/input_test.go new file mode 100644 index 0000000..12d1273 --- /dev/null +++ b/input_test.go @@ -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", ""), + ) + }) +} diff --git a/notes.txt b/notes.txt index e46d872..6df9ef0 100644 --- a/notes.txt +++ b/notes.txt @@ -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 diff --git a/output.go b/output.go index bdfa4f2..2d949fa 100644 --- a/output.go +++ b/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 } - 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 diff --git a/output_test.go b/output_test.go new file mode 100644 index 0000000..c28d9f4 --- /dev/null +++ b/output_test.go @@ -0,0 +1,12 @@ +package wand + +import "testing" + +func TestOutput(t *testing.T) { + // nil + // simple + // stringer + // pointer + // complex + // reader +} diff --git a/reflect.go b/reflect.go index 6f96e05..4f660fb 100644 --- a/reflect.go +++ b/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 {