ucl/cmdlang/userbuiltin_test.go

217 lines
5.3 KiB
Go

package cmdlang_test
import (
"bytes"
"context"
"strings"
"testing"
"github.com/lmika/cmdlang-proto/cmdlang"
"github.com/stretchr/testify/assert"
)
func TestInst_SetBuiltin(t *testing.T) {
t.Run("simple builtin accepting and returning strings", func(t *testing.T) {
inst := cmdlang.New()
inst.SetBuiltin("add2", func(ctx context.Context, args cmdlang.CallArgs) (any, error) {
var x, y string
if err := args.Bind(&x, &y); err != nil {
return nil, err
}
return x + y, nil
})
res, err := inst.Eval(context.Background(), `add2 "Hello, " "World"`)
assert.NoError(t, err)
assert.Equal(t, "Hello, World", res)
})
t.Run("simple builtin with optional switches and strings", func(t *testing.T) {
inst := cmdlang.New()
inst.SetBuiltin("add2", func(ctx context.Context, args cmdlang.CallArgs) (any, error) {
var x, y, sep string
if err := args.BindSwitch("sep", &sep); err != nil {
return nil, err
}
if err := args.BindSwitch("left", &x); err != nil {
return nil, err
}
if err := args.BindSwitch("right", &y); err != nil {
return nil, err
}
v := x + sep + y
if args.HasSwitch("upcase") {
v = strings.ToUpper(v)
}
return v, nil
})
tests := []struct {
descr string
expr string
want string
}{
{"plain eval", `add2 -sep ", " -right "world" -left "Hello"`, "Hello, world"},
{"swap 1", `add2 -right "right" -left "left" -sep ":"`, "left:right"},
{"swap 2", `add2 -left "left" -sep ":" -right "right" -upcase`, "LEFT:RIGHT"},
}
for _, tt := range tests {
t.Run(tt.descr, func(t *testing.T) {
res, err := inst.Eval(context.Background(), tt.expr)
assert.NoError(t, err)
assert.Equal(t, tt.want, res)
})
}
})
t.Run("builtin return proxy object", func(t *testing.T) {
type pair struct {
x, y string
}
inst := cmdlang.New()
inst.SetBuiltin("add2", func(ctx context.Context, args cmdlang.CallArgs) (any, error) {
var x, y string
if err := args.Bind(&x, &y); err != nil {
return nil, err
}
return pair{x, y}, nil
})
res, err := inst.Eval(context.Background(), `add2 "Hello" "World"`)
assert.NoError(t, err)
assert.Equal(t, pair{"Hello", "World"}, res)
})
t.Run("builtin operating on and returning proxy object", func(t *testing.T) {
type pair struct {
x, y string
}
tests := []struct {
descr string
expr string
want string
}{
{descr: "pass via args", expr: `join (add2 "left" "right")`, want: "left:right"},
{descr: "pass via vars", expr: `set x (add2 "blue" "green") ; join $x`, want: "blue:green"},
}
for _, tt := range tests {
t.Run(tt.descr, func(t *testing.T) {
inst := cmdlang.New()
inst.SetBuiltin("add2", func(ctx context.Context, args cmdlang.CallArgs) (any, error) {
var x, y string
if err := args.Bind(&x, &y); err != nil {
return nil, err
}
return pair{x, y}, nil
})
inst.SetBuiltin("join", func(ctx context.Context, args cmdlang.CallArgs) (any, error) {
var x pair
if err := args.Bind(&x); err != nil {
return nil, err
}
return x.x + ":" + x.y, nil
})
res, err := inst.Eval(context.Background(), tt.expr)
assert.NoError(t, err)
assert.Equal(t, tt.want, res)
})
}
})
t.Run("slices returned by commands treated as lists", func(t *testing.T) {
tests := []struct {
descr string
expr string
want any
wantOut string
}{
{descr: "return as is", expr: `countTo3`, want: []string{"1", "2", "3"}},
{descr: "iterate over", expr: `foreach (countTo3) { |x| echo $x }`, wantOut: "1\n2\n3\n"},
{descr: "iterate via foreach", expr: `["2" "4" "6"] | foreach { |x| echo $x }`, wantOut: "2\n4\n6\n"},
}
for _, tt := range tests {
t.Run(tt.descr, func(t *testing.T) {
outW := bytes.NewBuffer(nil)
inst := cmdlang.New(cmdlang.WithOut(outW))
inst.SetBuiltin("countTo3", func(ctx context.Context, args cmdlang.CallArgs) (any, error) {
return []string{"1", "2", "3"}, nil
})
res, err := inst.Eval(context.Background(), tt.expr)
assert.NoError(t, err)
assert.Equal(t, tt.want, res)
assert.Equal(t, tt.wantOut, outW.String())
})
}
})
}
func TestCallArgs_Bind(t *testing.T) {
t.Run("bind to an interface", func(t *testing.T) {
ctx := context.Background()
inst := cmdlang.New()
inst.SetBuiltin("sa", func(ctx context.Context, args cmdlang.CallArgs) (any, error) {
return doStringA{this: "a val"}, nil
})
inst.SetBuiltin("sb", func(ctx context.Context, args cmdlang.CallArgs) (any, error) {
return doStringB{left: "foo", right: "bar"}, nil
})
inst.SetBuiltin("dostr", func(ctx context.Context, args cmdlang.CallArgs) (any, error) {
var ds doStringable
if err := args.Bind(&ds); err != nil {
return nil, err
}
return ds.DoString(), nil
})
va, err := inst.Eval(ctx, `dostr (sa)`)
assert.NoError(t, err)
assert.Equal(t, "do string A: a val", va)
vb, err := inst.Eval(ctx, `dostr (sb)`)
assert.NoError(t, err)
assert.Equal(t, "do string B: foo bar", vb)
})
}
type doStringable interface {
DoString() string
}
type doStringA struct {
this string
}
func (da doStringA) DoString() string {
return "do string A: " + da.this
}
type doStringB struct {
left, right string
}
func (da doStringB) DoString() string {
return "do string B: " + da.left + " " + da.right
}