diff --git a/.forgejo/workflows/test.yaml b/.forgejo/workflows/test.yaml new file mode 100644 index 0000000..8441947 --- /dev/null +++ b/.forgejo/workflows/test.yaml @@ -0,0 +1,20 @@ +name: Test + +on: + push: + branches: + - feature/* + +jobs: + build: + runs-on: docker + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup Go + uses: actions/setup-go@v2 + with: + go-version: 1.22.4 + - name: Build + run: | + make test \ No newline at end of file diff --git a/ucl/builtins.go b/ucl/builtins.go index e984786..3b3a879 100644 --- a/ucl/builtins.go +++ b/ucl/builtins.go @@ -53,6 +53,120 @@ func addBuiltin(ctx context.Context, args invocationArgs) (object, error) { return intObject(n), nil } +func subBuiltin(ctx context.Context, args invocationArgs) (object, error) { + if len(args.args) == 0 { + return intObject(0), nil + } + + n := 0 + for i, a := range args.args { + var p int + switch t := a.(type) { + case intObject: + p = int(t) + case strObject: + v, err := strconv.Atoi(string(t)) + if err != nil { + return nil, fmt.Errorf("arg %v of 'sub' not convertable to an int", i) + } + p = v + default: + return nil, fmt.Errorf("arg %v of 'sub' not convertable to an int", i) + } + if i == 0 { + n = p + } else { + n -= p + } + } + + return intObject(n), nil +} + +func mupBuiltin(ctx context.Context, args invocationArgs) (object, error) { + if len(args.args) == 0 { + return intObject(1), nil + } + + n := 1 + for i, a := range args.args { + switch t := a.(type) { + case intObject: + n *= int(t) + case strObject: + v, err := strconv.Atoi(string(t)) + if err != nil { + return nil, fmt.Errorf("arg %v of 'mup' not convertable to an int", i) + } + n *= v + default: + return nil, fmt.Errorf("arg %v of 'mup' not convertable to an int", i) + } + } + + return intObject(n), nil +} + +func divBuiltin(ctx context.Context, args invocationArgs) (object, error) { + if len(args.args) == 0 { + return intObject(1), nil + } + + n := 1 + for i, a := range args.args { + var p int + switch t := a.(type) { + case intObject: + p = int(t) + case strObject: + v, err := strconv.Atoi(string(t)) + if err != nil { + return nil, fmt.Errorf("arg %v of 'div' not convertable to an int", i) + } + p = v + default: + return nil, fmt.Errorf("arg %v of 'div' not convertable to an int", i) + } + if i == 0 { + n = p + } else { + n /= p + } + } + + return intObject(n), nil +} + +func modBuiltin(ctx context.Context, args invocationArgs) (object, error) { + if len(args.args) == 0 { + return intObject(0), nil + } + + n := 0 + for i, a := range args.args { + var p int + switch t := a.(type) { + case intObject: + p = int(t) + case strObject: + v, err := strconv.Atoi(string(t)) + if err != nil { + return nil, fmt.Errorf("arg %v of 'mod' not convertable to an int", i) + } + p = v + default: + return nil, fmt.Errorf("arg %v of 'mod' not convertable to an int", i) + } + if i == 0 { + n = p + } else { + n %= p + } + } + + return intObject(n), nil +} + func setBuiltin(ctx context.Context, args invocationArgs) (object, error) { if err := args.expectArgn(2); err != nil { return nil, err @@ -88,19 +202,223 @@ func eqBuiltin(ctx context.Context, args invocationArgs) (object, error) { l := args.args[0] r := args.args[1] - switch lv := l.(type) { - case strObject: - if rv, ok := r.(strObject); ok { - return boolObject(lv == rv), nil + return boolObject(objectsEqual(l, r)), nil +} + +func neBuiltin(ctx context.Context, args invocationArgs) (object, error) { + if err := args.expectArgn(2); err != nil { + return nil, err + } + + l := args.args[0] + r := args.args[1] + + return boolObject(!objectsEqual(l, r)), nil +} + +func ltBuiltin(ctx context.Context, args invocationArgs) (object, error) { + if err := args.expectArgn(2); err != nil { + return nil, err + } + + isLess, err := objectsLessThan(args.args[0], args.args[1]) + if err != nil { + return nil, err + } + return boolObject(isLess), nil +} + +func leBuiltin(ctx context.Context, args invocationArgs) (object, error) { + if err := args.expectArgn(2); err != nil { + return nil, err + } + + isLess, err := objectsLessThan(args.args[0], args.args[1]) + if err != nil { + return nil, err + } + return boolObject(isLess || objectsEqual(args.args[0], args.args[1])), nil +} + +func gtBuiltin(ctx context.Context, args invocationArgs) (object, error) { + if err := args.expectArgn(2); err != nil { + return nil, err + } + + isGreater, err := objectsLessThan(args.args[1], args.args[0]) + if err != nil { + return nil, err + } + return boolObject(isGreater), nil +} + +func geBuiltin(ctx context.Context, args invocationArgs) (object, error) { + if err := args.expectArgn(2); err != nil { + return nil, err + } + + isGreater, err := objectsLessThan(args.args[1], args.args[0]) + if err != nil { + return nil, err + } + return boolObject(isGreater || objectsEqual(args.args[0], args.args[1])), nil +} + +func andBuiltin(ctx context.Context, args invocationArgs) (object, error) { + if err := args.expectArgn(2); err != nil { + return nil, err + } + + for _, a := range args.args { + if a == nil || !a.Truthy() { + return boolObject(false), nil } - case intObject: - if rv, ok := r.(intObject); ok { - return boolObject(lv == rv), nil + } + return args.args[len(args.args)-1], nil +} + +func orBuiltin(ctx context.Context, args invocationArgs) (object, error) { + if err := args.expectArgn(2); err != nil { + return nil, err + } + + for _, a := range args.args { + if a != nil && a.Truthy() { + return a, nil } } return boolObject(false), nil } +func notBuiltin(ctx context.Context, args invocationArgs) (object, error) { + if err := args.expectArgn(1); err != nil { + return nil, err + } + + return boolObject(!args.args[0].Truthy()), nil +} + +var errObjectsNotEqual = errors.New("objects not equal") + +func objectsEqual(l, r object) bool { + if l == nil || r == nil { + return l == nil && r == nil + } + + switch lv := l.(type) { + case strObject: + if rv, ok := r.(strObject); ok { + return lv == rv + } + case intObject: + if rv, ok := r.(intObject); ok { + return lv == rv + } + case boolObject: + if rv, ok := r.(boolObject); ok { + return lv == rv + } + case listable: + rv, ok := r.(listable) + if !ok { + return false + } + + if lv.Len() != rv.Len() { + return false + } + for i := 0; i < lv.Len(); i++ { + if !objectsEqual(lv.Index(i), rv.Index(i)) { + return false + } + } + return true + case hashable: + rv, ok := r.(hashable) + if !ok { + return false + } + + if lv.Len() != rv.Len() { + return false + } + if err := lv.Each(func(k string, lkv object) error { + rkv := rv.Value(k) + if rkv == nil { + return errObjectsNotEqual + } else if !objectsEqual(lkv, rkv) { + return errObjectsNotEqual + } + return nil + }); err != nil { + return false + } + return true + case OpaqueObject: + rv, ok := r.(OpaqueObject) + if !ok { + return false + } + + return lv.v == rv.v + } + return false +} + +func objectsLessThan(l, r object) (bool, error) { + switch lv := l.(type) { + case strObject: + if rv, ok := r.(strObject); ok { + return lv < rv, nil + } + case intObject: + if rv, ok := r.(intObject); ok { + return lv < rv, nil + } + } + return false, errors.New("objects are not comparable") +} + +func strBuiltin(ctx context.Context, args invocationArgs) (object, error) { + if err := args.expectArgn(1); err != nil { + return nil, err + } + + if args.args[0] == nil { + return strObject(""), nil + } + + return strObject(args.args[0].String()), nil +} + +func intBuiltin(ctx context.Context, args invocationArgs) (object, error) { + if err := args.expectArgn(1); err != nil { + return nil, err + } + + if args.args[0] == nil { + return intObject(0), nil + } + + switch v := args.args[0].(type) { + case intObject: + return v, nil + case strObject: + i, err := strconv.Atoi(string(v)) + if err != nil { + return nil, errors.New("cannot convert to int") + } + return intObject(i), nil + case boolObject: + if v { + return intObject(1), nil + } + return intObject(0), nil + } + + return nil, errors.New("cannot convert to int") +} + func concatBuiltin(ctx context.Context, args invocationArgs) (object, error) { var sb strings.Builder @@ -114,20 +432,6 @@ func concatBuiltin(ctx context.Context, args invocationArgs) (object, error) { return strObject(sb.String()), nil } -// -//func catBuiltin(ctx context.Context, args invocationArgs) (object, error) { -// if err := args.expectArgn(1); err != nil { -// return nil, err -// } -// -// filename, err := args.stringArg(0) -// if err != nil { -// return nil, err -// } -// -// return &fileLinesStream{filename: filename}, nil -//} - func callBuiltin(ctx context.Context, args invocationArgs) (object, error) { if err := args.expectArgn(1); err != nil { return nil, err diff --git a/ucl/inst.go b/ucl/inst.go index a4d1128..a5f35ed 100644 --- a/ucl/inst.go +++ b/ucl/inst.go @@ -61,7 +61,24 @@ func New(opts ...InstOption) *Inst { rootEC.addCmd("reduce", invokableFunc(reduceBuiltin)) rootEC.addCmd("eq", invokableFunc(eqBuiltin)) + rootEC.addCmd("ne", invokableFunc(neBuiltin)) + rootEC.addCmd("gt", invokableFunc(gtBuiltin)) + rootEC.addCmd("ge", invokableFunc(geBuiltin)) + rootEC.addCmd("lt", invokableFunc(ltBuiltin)) + rootEC.addCmd("le", invokableFunc(leBuiltin)) + + rootEC.addCmd("str", invokableFunc(strBuiltin)) + rootEC.addCmd("int", invokableFunc(intBuiltin)) + rootEC.addCmd("add", invokableFunc(addBuiltin)) + rootEC.addCmd("sub", invokableFunc(subBuiltin)) + rootEC.addCmd("mup", invokableFunc(mupBuiltin)) + rootEC.addCmd("div", invokableFunc(divBuiltin)) + rootEC.addCmd("mod", invokableFunc(modBuiltin)) + + rootEC.addCmd("and", invokableFunc(andBuiltin)) + rootEC.addCmd("or", invokableFunc(orBuiltin)) + rootEC.addCmd("not", invokableFunc(notBuiltin)) rootEC.addCmd("cat", invokableFunc(concatBuiltin)) rootEC.addCmd("break", invokableFunc(breakBuiltin)) @@ -72,10 +89,6 @@ func New(opts ...InstOption) *Inst { rootEC.addMacro("foreach", macroFunc(foreachBuiltin)) rootEC.addMacro("proc", macroFunc(procBuiltin)) - //rootEC.addCmd("testTimebomb", invokableStreamFunc(errorTestBuiltin)) - - rootEC.setOrDefineVar("hello", strObject("world")) - inst := &Inst{ out: os.Stdout, rootEC: rootEC, @@ -88,6 +101,14 @@ func New(opts ...InstOption) *Inst { return inst } +func (inst *Inst) SetVar(name string, value any) { + obj, err := fromGoValue(value) + if err != nil { + return + } + inst.rootEC.setOrDefineVar(name, obj) +} + func (inst *Inst) Out() io.Writer { if inst.out == nil { return os.Stdout diff --git a/ucl/objs.go b/ucl/objs.go index 5527c85..1fa18e0 100644 --- a/ucl/objs.go +++ b/ucl/objs.go @@ -6,6 +6,7 @@ import ( "fmt" "reflect" "strconv" + "strings" "github.com/lmika/gopkgs/fp/slices" ) @@ -51,7 +52,22 @@ func (s listObject) Index(i int) object { type hashObject map[string]object func (s hashObject) String() string { - return fmt.Sprintf("%v", map[string]object(s)) + if len(s) == 0 { + return "[:]" + } + + sb := strings.Builder{} + sb.WriteString("[") + for k, v := range s { + if sb.Len() != 1 { + sb.WriteString(" ") + } + sb.WriteString(k) + sb.WriteString(":") + sb.WriteString(v.String()) + } + sb.WriteString("]") + return sb.String() } func (s hashObject) Truthy() bool { @@ -99,9 +115,9 @@ type boolObject bool func (b boolObject) String() string { if b { - return "(true)" + return "true" } - return "(false))" + return "false" } func (b boolObject) Truthy() bool { @@ -118,6 +134,8 @@ func toGoValue(obj object) (interface{}, bool) { return string(v), true case intObject: return int(v), true + case boolObject: + return bool(v), true case listObject: xs := make([]interface{}, 0, len(v)) for _, va := range v { @@ -159,6 +177,8 @@ func fromGoValue(v any) (object, error) { return strObject(t), nil case int: return intObject(t), nil + case bool: + return boolObject(t), nil } return fromGoReflectValue(reflect.ValueOf(v)) diff --git a/ucl/testbuiltins_test.go b/ucl/testbuiltins_test.go index 34e0220..7d0ae1c 100644 --- a/ucl/testbuiltins_test.go +++ b/ucl/testbuiltins_test.go @@ -229,16 +229,16 @@ func TestBuiltins_Break(t *testing.T) { expr string want string }{ - //{desc: "break unconditionally returning nothing", expr: ` - // foreach ["1" "2" "3"] { |v| - // break - // echo $v - // }`, want: "(nil)\n"}, - //{desc: "break conditionally returning nothing", expr: ` - // foreach ["1" "2" "3"] { |v| - // echo $v - // if (eq $v "2") { break } - // }`, want: "1\n2\n(nil)\n"}, + {desc: "break unconditionally returning nothing", expr: ` + foreach ["1" "2" "3"] { |v| + break + echo $v + }`, want: "(nil)\n"}, + {desc: "break conditionally returning nothing", expr: ` + foreach ["1" "2" "3"] { |v| + echo $v + if (eq $v "2") { break } + }`, want: "1\n2\n(nil)\n"}, {desc: "break inner loop only returning nothing", expr: ` foreach ["a" "b"] { |u| foreach ["1" "2" "3"] { |v| @@ -246,11 +246,11 @@ func TestBuiltins_Break(t *testing.T) { if (eq $v "2") { break } } }`, want: "a1\na2\nb1\nb2\n(nil)\n"}, - //{desc: "break returning value", expr: ` - // echo (foreach ["1" "2" "3"] { |v| - // echo $v - // if (eq $v "2") { break "hello" } - // })`, want: "1\n2\nhello\n(nil)\n"}, + {desc: "break returning value", expr: ` + echo (foreach ["1" "2" "3"] { |v| + echo $v + if (eq $v "2") { break "hello" } + })`, want: "1\n2\nhello\n(nil)\n"}, } for _, tt := range tests { @@ -891,3 +891,342 @@ func TestBuiltins_Reduce(t *testing.T) { }) } } + +func TestBuiltins_LtLeGtLe(t *testing.T) { + tests := []struct { + desc string + expr string + want bool + wantErr bool + }{ + {desc: "str 1 - lt", expr: `lt "hello" "world"`, want: true}, + {desc: "str 1 - le", expr: `le "hello" "world"`, want: true}, + {desc: "str 1 - gt", expr: `gt "hello" "world"`, want: false}, + {desc: "str 1 - ge", expr: `ge "hello" "world"`, want: false}, + {desc: "str 2 - lt", expr: `lt "zzzzz" "world"`, want: false}, + {desc: "str 2 - le", expr: `le "zzzzz" "world"`, want: false}, + {desc: "str 2 - gt", expr: `gt "zzzzz" "world"`, want: true}, + {desc: "str 2 - ge", expr: `ge "zzzzz" "world"`, want: true}, + {desc: "str 3 - lt", expr: `lt "hello" "hello"`, want: false}, + {desc: "str 3 - le", expr: `le "hello" "hello"`, want: true}, + {desc: "str 3 - gt", expr: `gt "hello" "hello"`, want: false}, + {desc: "str 3 - ge", expr: `ge "hello" "hello"`, want: true}, + + {desc: "int 1 - lt", expr: `lt 5 8`, want: true}, + {desc: "int 1 - le", expr: `le 5 8`, want: true}, + {desc: "int 1 - gt", expr: `gt 5 8`, want: false}, + {desc: "int 1 - ge", expr: `ge 5 8`, want: false}, + {desc: "int 2 - lt", expr: `lt 5 -8`, want: false}, + {desc: "int 2 - le", expr: `le 5 -8`, want: false}, + {desc: "int 2 - gt", expr: `gt 5 -8`, want: true}, + {desc: "int 2 - ge", expr: `ge 5 -8`, want: true}, + {desc: "int 3 - lt", expr: `lt 5 5`, want: false}, + {desc: "int 3 - le", expr: `le 5 5`, want: true}, + {desc: "int 3 - gt", expr: `gt 5 5`, want: false}, + {desc: "int 3 - ge", expr: `ge 5 5`, want: true}, + + {desc: "not comparable 1", expr: `lt () ()`, wantErr: true}, + {desc: "not comparable 2", expr: `lt $true $false`, wantErr: true}, + {desc: "not comparable 3", expr: `lt [1 2 3] [2 3 4]`, wantErr: true}, + {desc: "not comparable 4", expr: `lt ["1":2] ["2":3]`, wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + ctx := context.Background() + outW := bytes.NewBuffer(nil) + + inst := New(WithOut(outW), WithTestBuiltin()) + inst.SetVar("true", true) + inst.SetVar("false", false) + + eqRes, err := inst.Eval(ctx, tt.expr) + + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want, eqRes) + } + }) + } +} + +func TestBuiltins_EqNe(t *testing.T) { + tests := []struct { + desc string + expr string + want bool + }{ + {desc: "equal strs 1", expr: `eq "hello" "hello"`, want: true}, + {desc: "equal strs 2", expr: `eq "bla" "bla"`, want: true}, + {desc: "equal strs 3", expr: `eq "" ""`, want: true}, + {desc: "equal ints 1", expr: `eq 123 123`, want: true}, + {desc: "equal ints 2", expr: `eq -21 -21`, want: true}, + {desc: "equal ints 3", expr: `eq 0 0`, want: true}, + {desc: "equal lists 1", expr: `eq [1 2 3] [1 2 3]`, want: true}, + {desc: "equal lists 2", expr: `eq ["foo" "bar"] ["foo" "bar"]`, want: true}, + {desc: "equal lists 3", expr: `eq [] []`, want: true}, + {desc: "equal hashes 1", expr: `eq ["this":1 "that":"thing"] ["that":"thing" "this":1]`, want: true}, + {desc: "equal hashes 2", expr: `eq ["foo":"bar"] ["foo":"bar"]`, want: true}, + {desc: "equal bools 1", expr: `eq $true $true`, want: true}, + {desc: "equal bools 2", expr: `eq $false $false`, want: true}, + {desc: "equal nil 1", expr: `eq () ()`, want: true}, + {desc: "equal opaque 1", expr: `eq $hello $hello`, want: true}, + {desc: "equal opaque 2", expr: `eq $world $world`, want: true}, + + {desc: "not equal strs 1", expr: `eq "hello" "world"`, want: false}, + {desc: "not equal strs 2", expr: `eq "bla" "BLA"`, want: false}, + {desc: "not equal int 1", expr: `eq 131 313`, want: false}, + {desc: "not equal int 2", expr: `eq -2 2`, want: false}, + {desc: "not equal lists 1", expr: `eq [1 2 3] [1 2]`, want: false}, + {desc: "not equal lists 2", expr: `eq ["123" "234"] [123 234]`, want: false}, + {desc: "not equal hashes 1", expr: `eq ["this":1 "that":"thing"] ["that":"thing"]`, want: false}, + {desc: "not equal hashes 2", expr: `eq ["this":1 "that":"thing"] ["this":1 "that":"thing" "other":"thing"]`, want: false}, + {desc: "not equal hashes 3", expr: `eq ["this":1 "that":"thing"] ["this":"1" "that":"other"]`, want: false}, + {desc: "not equal opaque 1", expr: `eq $hello $world`, want: false}, + {desc: "not equal opaque 2", expr: `eq $hello "hello"`, want: false}, + + {desc: "not equal types 1", expr: `eq "123" 123`, want: false}, + {desc: "not equal types 2", expr: `eq 0 ""`, want: false}, + {desc: "not equal types 3", expr: `eq [] [:]`, want: false}, + {desc: "not equal types 4", expr: `eq ["23"] "23"`, want: false}, + {desc: "not equal types 5", expr: `eq $true ()`, want: false}, + {desc: "not equal types 6", expr: `eq () $false`, want: false}, + {desc: "not equal types 7", expr: `eq () "yes"`, want: false}, + {desc: "not equal types 8", expr: `eq () $world`, want: false}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + type testProxyObject struct { + v string + } + + ctx := context.Background() + outW := bytes.NewBuffer(nil) + + inst := New(WithOut(outW), WithTestBuiltin()) + inst.SetVar("hello", Opaque(testProxyObject{v: "hello"})) + inst.SetVar("world", Opaque(testProxyObject{v: "world"})) + inst.SetVar("true", true) + inst.SetVar("false", false) + + eqRes, err := inst.Eval(ctx, tt.expr) + assert.NoError(t, err) + assert.Equal(t, tt.want, eqRes) + + neRes, err := inst.Eval(ctx, strings.ReplaceAll(tt.expr, "eq", "ne")) + assert.NoError(t, err) + assert.Equal(t, !tt.want, neRes) + }) + } +} + +func TestBuiltins_Str(t *testing.T) { + tests := []struct { + desc string + expr string + want string + }{ + {desc: "str", expr: `str "hello"`, want: "hello"}, + {desc: "int", expr: `str 123`, want: "123"}, + {desc: "bool 1", expr: `str (eq 1 1)`, want: "true"}, + {desc: "bool 2", expr: `str (eq 1 0)`, want: "false"}, + {desc: "list 1", expr: `str [1 2 3]`, want: "[1 2 3]"}, + {desc: "list 2", expr: `str []`, want: "[]"}, + {desc: "dict 1", expr: `str ["hello":"world"]`, want: `[hello:world]`}, + {desc: "dict 2", expr: `str [:]`, want: "[:]"}, + {desc: "nil", expr: `str ()`, want: ""}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + ctx := context.Background() + outW := bytes.NewBuffer(nil) + + inst := New(WithOut(outW), WithTestBuiltin()) + + eqRes, err := inst.Eval(ctx, tt.expr) + assert.NoError(t, err) + assert.Equal(t, tt.want, eqRes) + }) + } +} + +func TestBuiltins_Int(t *testing.T) { + tests := []struct { + desc string + expr string + want int + wantErr bool + }{ + {desc: "str 1", expr: `int "123"`, want: 123}, + {desc: "str 2", expr: `int "31452"`, want: 31452}, + {desc: "str 3", expr: `int "-21"`, want: -21}, + {desc: "int 1", expr: `int 123`, want: 123}, + {desc: "int 2", expr: `int -21`, want: -21}, + {desc: "bool 1", expr: `int (eq 1 1)`, want: 1}, + {desc: "bool 2", expr: `int (eq 1 0)`, want: 0}, + {desc: "nil", expr: `int ()`, want: 0}, + + {desc: "list 1", expr: `int [1 2 3]`, wantErr: true}, + {desc: "list 2", expr: `int []`, wantErr: true}, + {desc: "dict 1", expr: `int ["hello":"world"]`, wantErr: true}, + {desc: "dict 2", expr: `int [:]`, wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + ctx := context.Background() + outW := bytes.NewBuffer(nil) + + inst := New(WithOut(outW), WithTestBuiltin()) + + eqRes, err := inst.Eval(ctx, tt.expr) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want, eqRes) + } + }) + } +} + +func TestBuiltins_AddSubMupDivMod(t *testing.T) { + tests := []struct { + desc string + expr string + want int + wantErr bool + }{ + {desc: "add 1", expr: `add 1 2`, want: 3}, + {desc: "add 2", expr: `add "3" 5`, want: 8}, + {desc: "add 3", expr: `add 1 "2" 8`, want: 11}, + {desc: "add 4", expr: `add 1`, want: 1}, + {desc: "add 5", expr: `add`, want: 0}, + {desc: "sub 1", expr: `sub 9 3`, want: 6}, + {desc: "sub 2", expr: `sub 2 "5"`, want: -3}, + {desc: "sub 3", expr: `sub 8 1 8`, want: -1}, + {desc: "sub 4", expr: `sub 4`, want: 4}, + {desc: "sub 5", expr: `sub`, want: 0}, + {desc: "mup 1", expr: `mup 2 4`, want: 8}, + {desc: "mup 2", expr: `mup 3 "4" 5`, want: 60}, + {desc: "mup 3", expr: `mup 7`, want: 7}, + {desc: "mup 4", expr: `mup`, want: 1}, + {desc: "div 1", expr: `div 8 4`, want: 2}, + {desc: "div 2", expr: `div "7" 4`, want: 1}, + {desc: "div 3", expr: `div 7`, want: 7}, + {desc: "div 4", expr: `div`, want: 1}, + {desc: "mod 1", expr: `mod 2 3`, want: 2}, + {desc: "mod 2", expr: `mod "7" 4`, want: 3}, + {desc: "mod 3", expr: `mod 8 4`, want: 0}, + {desc: "mod 4", expr: `mod 3`, want: 3}, + {desc: "mod 5", expr: `mod`, want: 0}, + + {desc: "add err", expr: `add [] [:]`, wantErr: true}, + {desc: "sub err", expr: `sub [] [:]`, wantErr: true}, + {desc: "mup err", expr: `mup [] [:]`, wantErr: true}, + {desc: "div err", expr: `div [] [:]`, wantErr: true}, + {desc: "mod err", expr: `mod [] [:]`, wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + ctx := context.Background() + outW := bytes.NewBuffer(nil) + + inst := New(WithOut(outW), WithTestBuiltin()) + + eqRes, err := inst.Eval(ctx, tt.expr) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want, eqRes) + } + }) + } +} + +func TestBuiltins_AndOrNot(t *testing.T) { + tests := []struct { + desc string + expr string + want any + wantErr bool + }{ + {desc: "and 1", expr: `and $true $true`, want: true}, + {desc: "and 2", expr: `and $false $true`, want: false}, + {desc: "and 3", expr: `and $false $false`, want: false}, + {desc: "or 1", expr: `or $true $true`, want: true}, + {desc: "or 2", expr: `or $false $true`, want: true}, + {desc: "or 3", expr: `or $false $false`, want: false}, + {desc: "not 1", expr: `not $true`, want: false}, + {desc: "not 2", expr: `not $false`, want: true}, + {desc: "not 3", expr: `not $false $true`, want: true}, + + {desc: "short circuit and 1", expr: `and "hello" "world"`, want: "world"}, + {desc: "short circuit and 2", expr: `and () "world"`, want: false}, + {desc: "short circuit or 1", expr: `or "hello" "world"`, want: "hello"}, + {desc: "short circuit or 2", expr: `or () "world"`, want: "world"}, + + {desc: "bad and 1", expr: `and "one"`, wantErr: true}, + {desc: "bad and 2", expr: `and`, wantErr: true}, + {desc: "bad or 1", expr: `or "one"`, wantErr: true}, + {desc: "bad or 2", expr: `or`, wantErr: true}, + {desc: "bad not 2", expr: `not`, wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + ctx := context.Background() + outW := bytes.NewBuffer(nil) + + inst := New(WithOut(outW), WithTestBuiltin()) + inst.SetVar("true", true) + inst.SetVar("false", false) + + eqRes, err := inst.Eval(ctx, tt.expr) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want, eqRes) + } + }) + } +} + +func TestBuiltins_Cat(t *testing.T) { + tests := []struct { + desc string + expr string + want any + }{ + {desc: "cat 1", expr: `cat "hello, " "world"`, want: "hello, world"}, + {desc: "cat 2", expr: `cat "hello, " "world " "and stuff"`, want: "hello, world and stuff"}, + {desc: "cat 3", expr: `cat "int = " 123`, want: "int = 123"}, + {desc: "cat 4", expr: `cat "bool = " $true`, want: "bool = true"}, + {desc: "cat 5", expr: `cat "array = " []`, want: "array = []"}, + {desc: "cat 6", expr: `cat "array = " [1 3 2 4]`, want: "array = [1 3 2 4]"}, + {desc: "cat 7", expr: `cat 1 $true 3 [4]`, want: "1true3[4]"}, + {desc: "cat 8", expr: `cat`, want: ""}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + ctx := context.Background() + outW := bytes.NewBuffer(nil) + + inst := New(WithOut(outW), WithTestBuiltin()) + inst.SetVar("true", true) + inst.SetVar("false", false) + + eqRes, err := inst.Eval(ctx, tt.expr) + assert.NoError(t, err) + assert.Equal(t, tt.want, eqRes) + }) + } +}