123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667 |
- package tengo_test
- import (
- "context"
- "errors"
- "fmt"
- "math/rand"
- "strconv"
- "strings"
- "sync"
- "testing"
- "time"
- "github.com/d5/tengo/v2"
- "github.com/d5/tengo/v2/require"
- "github.com/d5/tengo/v2/stdlib"
- "github.com/d5/tengo/v2/token"
- )
- func TestScript_Add(t *testing.T) {
- s := tengo.NewScript([]byte(`a := b; c := test(b); d := test(5)`))
- require.NoError(t, s.Add("b", 5)) // b = 5
- require.NoError(t, s.Add("b", "foo")) // b = "foo" (re-define before compilation)
- require.NoError(t, s.Add("test",
- func(args ...tengo.Object) (ret tengo.Object, err error) {
- if len(args) > 0 {
- switch arg := args[0].(type) {
- case *tengo.Int:
- return &tengo.Int{Value: arg.Value + 1}, nil
- }
- }
- return &tengo.Int{Value: 0}, nil
- }))
- c, err := s.Compile()
- require.NoError(t, err)
- require.NoError(t, c.Run())
- require.Equal(t, "foo", c.Get("a").Value())
- require.Equal(t, "foo", c.Get("b").Value())
- require.Equal(t, int64(0), c.Get("c").Value())
- require.Equal(t, int64(6), c.Get("d").Value())
- }
- func TestScript_Remove(t *testing.T) {
- s := tengo.NewScript([]byte(`a := b`))
- err := s.Add("b", 5)
- require.NoError(t, err)
- require.True(t, s.Remove("b")) // b is removed
- _, err = s.Compile() // should not compile because b is undefined
- require.Error(t, err)
- }
- func TestScript_Run(t *testing.T) {
- s := tengo.NewScript([]byte(`a := b`))
- err := s.Add("b", 5)
- require.NoError(t, err)
- c, err := s.Run()
- require.NoError(t, err)
- require.NotNil(t, c)
- compiledGet(t, c, "a", int64(5))
- }
- func TestScript_BuiltinModules(t *testing.T) {
- s := tengo.NewScript([]byte(`math := import("math"); a := math.abs(-19.84)`))
- s.SetImports(stdlib.GetModuleMap("math"))
- c, err := s.Run()
- require.NoError(t, err)
- require.NotNil(t, c)
- compiledGet(t, c, "a", 19.84)
- c, err = s.Run()
- require.NoError(t, err)
- require.NotNil(t, c)
- compiledGet(t, c, "a", 19.84)
- s.SetImports(stdlib.GetModuleMap("os"))
- _, err = s.Run()
- require.Error(t, err)
- s.SetImports(nil)
- _, err = s.Run()
- require.Error(t, err)
- }
- func TestScript_SourceModules(t *testing.T) {
- s := tengo.NewScript([]byte(`
- enum := import("enum")
- a := enum.all([1,2,3], func(_, v) {
- return v > 0
- })
- `))
- s.SetImports(stdlib.GetModuleMap("enum"))
- c, err := s.Run()
- require.NoError(t, err)
- require.NotNil(t, c)
- compiledGet(t, c, "a", true)
- s.SetImports(nil)
- _, err = s.Run()
- require.Error(t, err)
- }
- func TestScript_SetMaxConstObjects(t *testing.T) {
- // one constant '5'
- s := tengo.NewScript([]byte(`a := 5`))
- s.SetMaxConstObjects(1) // limit = 1
- _, err := s.Compile()
- require.NoError(t, err)
- s.SetMaxConstObjects(0) // limit = 0
- _, err = s.Compile()
- require.Error(t, err)
- require.Equal(t, "exceeding constant objects limit: 1", err.Error())
- // two constants '5' and '1'
- s = tengo.NewScript([]byte(`a := 5 + 1`))
- s.SetMaxConstObjects(2) // limit = 2
- _, err = s.Compile()
- require.NoError(t, err)
- s.SetMaxConstObjects(1) // limit = 1
- _, err = s.Compile()
- require.Error(t, err)
- require.Equal(t, "exceeding constant objects limit: 2", err.Error())
- // duplicates will be removed
- s = tengo.NewScript([]byte(`a := 5 + 5`))
- s.SetMaxConstObjects(1) // limit = 1
- _, err = s.Compile()
- require.NoError(t, err)
- s.SetMaxConstObjects(0) // limit = 0
- _, err = s.Compile()
- require.Error(t, err)
- require.Equal(t, "exceeding constant objects limit: 1", err.Error())
- // no limit set
- s = tengo.NewScript([]byte(`a := 1 + 2 + 3 + 4 + 5`))
- _, err = s.Compile()
- require.NoError(t, err)
- }
- func TestScriptConcurrency(t *testing.T) {
- solve := func(a, b, c int) (d, e int) {
- a += 2
- b += c
- a += b * 2
- d = a + b + c
- e = 0
- for i := 1; i <= d; i++ {
- e += i
- }
- e *= 2
- return
- }
- code := []byte(`
- mod1 := import("mod1")
- a += 2
- b += c
- a += b * 2
- arr := [a, b, c]
- arrstr := string(arr)
- map := {a: a, b: b, c: c}
- d := a + b + c
- s := 0
- for i:=1; i<=d; i++ {
- s += i
- }
- e := mod1.double(s)
- `)
- mod1 := map[string]tengo.Object{
- "double": &tengo.UserFunction{
- Value: func(args ...tengo.Object) (
- ret tengo.Object,
- err error,
- ) {
- arg0, _ := tengo.ToInt64(args[0])
- ret = &tengo.Int{Value: arg0 * 2}
- return
- },
- },
- }
- scr := tengo.NewScript(code)
- _ = scr.Add("a", 0)
- _ = scr.Add("b", 0)
- _ = scr.Add("c", 0)
- mods := tengo.NewModuleMap()
- mods.AddBuiltinModule("mod1", mod1)
- scr.SetImports(mods)
- compiled, err := scr.Compile()
- require.NoError(t, err)
- executeFn := func(compiled *tengo.Compiled, a, b, c int) (d, e int) {
- _ = compiled.Set("a", a)
- _ = compiled.Set("b", b)
- _ = compiled.Set("c", c)
- err := compiled.Run()
- require.NoError(t, err)
- d = compiled.Get("d").Int()
- e = compiled.Get("e").Int()
- return
- }
- concurrency := 500
- var wg sync.WaitGroup
- wg.Add(concurrency)
- for i := 0; i < concurrency; i++ {
- go func(compiled *tengo.Compiled) {
- time.Sleep(time.Duration(rand.Int63n(50)) * time.Millisecond)
- defer wg.Done()
- a := rand.Intn(10)
- b := rand.Intn(10)
- c := rand.Intn(10)
- d, e := executeFn(compiled, a, b, c)
- expectedD, expectedE := solve(a, b, c)
- require.Equal(t, expectedD, d, "input: %d, %d, %d", a, b, c)
- require.Equal(t, expectedE, e, "input: %d, %d, %d", a, b, c)
- }(compiled.Clone())
- }
- wg.Wait()
- }
- type Counter struct {
- tengo.ObjectImpl
- value int64
- }
- func (o *Counter) TypeName() string {
- return "counter"
- }
- func (o *Counter) String() string {
- return fmt.Sprintf("Counter(%d)", o.value)
- }
- func (o *Counter) BinaryOp(
- op token.Token,
- rhs tengo.Object,
- ) (tengo.Object, error) {
- switch rhs := rhs.(type) {
- case *Counter:
- switch op {
- case token.Add:
- return &Counter{value: o.value + rhs.value}, nil
- case token.Sub:
- return &Counter{value: o.value - rhs.value}, nil
- }
- case *tengo.Int:
- switch op {
- case token.Add:
- return &Counter{value: o.value + rhs.Value}, nil
- case token.Sub:
- return &Counter{value: o.value - rhs.Value}, nil
- }
- }
- return nil, errors.New("invalid operator")
- }
- func (o *Counter) IsFalsy() bool {
- return o.value == 0
- }
- func (o *Counter) Equals(t tengo.Object) bool {
- if tc, ok := t.(*Counter); ok {
- return o.value == tc.value
- }
- return false
- }
- func (o *Counter) Copy() tengo.Object {
- return &Counter{value: o.value}
- }
- func (o *Counter) Call(_ ...tengo.Object) (tengo.Object, error) {
- return &tengo.Int{Value: o.value}, nil
- }
- func (o *Counter) CanCall() bool {
- return true
- }
- func TestScript_CustomObjects(t *testing.T) {
- c := compile(t, `a := c1(); s := string(c1); c2 := c1; c2++`, M{
- "c1": &Counter{value: 5},
- })
- compiledRun(t, c)
- compiledGet(t, c, "a", int64(5))
- compiledGet(t, c, "s", "Counter(5)")
- compiledGetCounter(t, c, "c2", &Counter{value: 6})
- c = compile(t, `
- arr := [1, 2, 3, 4]
- for x in arr {
- c1 += x
- }
- out := c1()
- `, M{
- "c1": &Counter{value: 5},
- })
- compiledRun(t, c)
- compiledGet(t, c, "out", int64(15))
- }
- func compiledGetCounter(
- t *testing.T,
- c *tengo.Compiled,
- name string,
- expected *Counter,
- ) {
- v := c.Get(name)
- require.NotNil(t, v)
- actual := v.Value().(*Counter)
- require.NotNil(t, actual)
- require.Equal(t, expected.value, actual.value)
- }
- func TestScriptSourceModule(t *testing.T) {
- // script1 imports "mod1"
- scr := tengo.NewScript([]byte(`out := import("mod")`))
- mods := tengo.NewModuleMap()
- mods.AddSourceModule("mod", []byte(`export 5`))
- scr.SetImports(mods)
- c, err := scr.Run()
- require.NoError(t, err)
- require.Equal(t, int64(5), c.Get("out").Value())
- // executing module function
- scr = tengo.NewScript([]byte(`fn := import("mod"); out := fn()`))
- mods = tengo.NewModuleMap()
- mods.AddSourceModule("mod",
- []byte(`a := 3; export func() { return a + 5 }`))
- scr.SetImports(mods)
- c, err = scr.Run()
- require.NoError(t, err)
- require.Equal(t, int64(8), c.Get("out").Value())
- scr = tengo.NewScript([]byte(`out := import("mod")`))
- mods = tengo.NewModuleMap()
- mods.AddSourceModule("mod",
- []byte(`text := import("text"); export text.title("foo")`))
- mods.AddBuiltinModule("text",
- map[string]tengo.Object{
- "title": &tengo.UserFunction{
- Name: "title",
- Value: func(args ...tengo.Object) (tengo.Object, error) {
- s, _ := tengo.ToString(args[0])
- return &tengo.String{Value: strings.Title(s)}, nil
- }},
- })
- scr.SetImports(mods)
- c, err = scr.Run()
- require.NoError(t, err)
- require.Equal(t, "Foo", c.Get("out").Value())
- scr.SetImports(nil)
- _, err = scr.Run()
- require.Error(t, err)
- }
- func BenchmarkArrayIndex(b *testing.B) {
- bench(b.N, `a := [1, 2, 3, 4, 5, 6, 7, 8, 9];
- for i := 0; i < 1000; i++ {
- a[0]; a[1]; a[2]; a[3]; a[4]; a[5]; a[6]; a[7]; a[7];
- }
- `)
- }
- func BenchmarkArrayIndexCompare(b *testing.B) {
- bench(b.N, `a := [1, 2, 3, 4, 5, 6, 7, 8, 9];
- for i := 0; i < 1000; i++ {
- 1; 2; 3; 4; 5; 6; 7; 8; 9;
- }
- `)
- }
- func bench(n int, input string) {
- s := tengo.NewScript([]byte(input))
- c, err := s.Compile()
- if err != nil {
- panic(err)
- }
- for i := 0; i < n; i++ {
- if err := c.Run(); err != nil {
- panic(err)
- }
- }
- }
- type M map[string]interface{}
- func TestCompiled_Get(t *testing.T) {
- // simple script
- c := compile(t, `a := 5`, nil)
- compiledRun(t, c)
- compiledGet(t, c, "a", int64(5))
- // user-defined variables
- compileError(t, `a := b`, nil) // compile error because "b" is not defined
- c = compile(t, `a := b`, M{"b": "foo"}) // now compile with b = "foo" defined
- compiledGet(t, c, "a", nil) // a = undefined; because it's before Compiled.Run()
- compiledRun(t, c) // Compiled.Run()
- compiledGet(t, c, "a", "foo") // a = "foo"
- }
- func TestCompiled_GetAll(t *testing.T) {
- c := compile(t, `a := 5`, nil)
- compiledRun(t, c)
- compiledGetAll(t, c, M{"a": int64(5)})
- c = compile(t, `a := b`, M{"b": "foo"})
- compiledRun(t, c)
- compiledGetAll(t, c, M{"a": "foo", "b": "foo"})
- c = compile(t, `a := b; b = 5`, M{"b": "foo"})
- compiledRun(t, c)
- compiledGetAll(t, c, M{"a": "foo", "b": int64(5)})
- }
- func TestCompiled_IsDefined(t *testing.T) {
- c := compile(t, `a := 5`, nil)
- compiledIsDefined(t, c, "a", false) // a is not defined before Run()
- compiledRun(t, c)
- compiledIsDefined(t, c, "a", true)
- compiledIsDefined(t, c, "b", false)
- }
- func TestCompiled_Set(t *testing.T) {
- c := compile(t, `a := b`, M{"b": "foo"})
- compiledRun(t, c)
- compiledGet(t, c, "a", "foo")
- // replace value of 'b'
- err := c.Set("b", "bar")
- require.NoError(t, err)
- compiledRun(t, c)
- compiledGet(t, c, "a", "bar")
- // try to replace undefined variable
- err = c.Set("c", 1984)
- require.Error(t, err) // 'c' is not defined
- // case #2
- c = compile(t, `
- a := func() {
- return func() {
- return b + 5
- }()
- }()`, M{"b": 5})
- compiledRun(t, c)
- compiledGet(t, c, "a", int64(10))
- err = c.Set("b", 10)
- require.NoError(t, err)
- compiledRun(t, c)
- compiledGet(t, c, "a", int64(15))
- }
- func TestCompiled_RunContext(t *testing.T) {
- // machine completes normally
- c := compile(t, `a := 5`, nil)
- err := c.RunContext(context.Background())
- require.NoError(t, err)
- compiledGet(t, c, "a", int64(5))
- // timeout
- c = compile(t, `for true {}`, nil)
- ctx, cancel := context.WithTimeout(context.Background(),
- 1*time.Millisecond)
- defer cancel()
- err = c.RunContext(ctx)
- require.Equal(t, context.DeadlineExceeded, err)
- }
- func TestCompiled_CustomObject(t *testing.T) {
- c := compile(t, `r := (t<130)`, M{"t": &customNumber{value: 123}})
- compiledRun(t, c)
- compiledGet(t, c, "r", true)
- c = compile(t, `r := (t>13)`, M{"t": &customNumber{value: 123}})
- compiledRun(t, c)
- compiledGet(t, c, "r", true)
- }
- // customNumber is a user defined object that can compare to tengo.Int
- // very shitty implementation, just to test that token.Less and token.Greater in BinaryOp works
- type customNumber struct {
- tengo.ObjectImpl
- value int64
- }
- func (n *customNumber) TypeName() string {
- return "Number"
- }
- func (n *customNumber) String() string {
- return strconv.FormatInt(n.value, 10)
- }
- func (n *customNumber) BinaryOp(op token.Token, rhs tengo.Object) (tengo.Object, error) {
- tengoInt, ok := rhs.(*tengo.Int)
- if !ok {
- return nil, tengo.ErrInvalidOperator
- }
- return n.binaryOpInt(op, tengoInt)
- }
- func (n *customNumber) binaryOpInt(op token.Token, rhs *tengo.Int) (tengo.Object, error) {
- i := n.value
- switch op {
- case token.Less:
- if i < rhs.Value {
- return tengo.TrueValue, nil
- }
- return tengo.FalseValue, nil
- case token.Greater:
- if i > rhs.Value {
- return tengo.TrueValue, nil
- }
- return tengo.FalseValue, nil
- case token.LessEq:
- if i <= rhs.Value {
- return tengo.TrueValue, nil
- }
- return tengo.FalseValue, nil
- case token.GreaterEq:
- if i >= rhs.Value {
- return tengo.TrueValue, nil
- }
- return tengo.FalseValue, nil
- }
- return nil, tengo.ErrInvalidOperator
- }
- func TestScript_ImportError(t *testing.T) {
- m := `
- exp := import("expression")
- r := exp(ctx)
- `
- src := `
- export func(ctx) {
- closure := func() {
- if ctx.actiontimes < 0 { // an error is thrown here because actiontimes is undefined
- return true
- }
- return false
- }
- return closure()
- }`
- s := tengo.NewScript([]byte(m))
- mods := tengo.NewModuleMap()
- mods.AddSourceModule("expression", []byte(src))
- s.SetImports(mods)
- err := s.Add("ctx", map[string]interface{}{
- "ctx": 12,
- })
- require.NoError(t, err)
- _, err = s.Run()
- require.True(t, strings.Contains(err.Error(), "expression:4:6"))
- }
- func compile(t *testing.T, input string, vars M) *tengo.Compiled {
- s := tengo.NewScript([]byte(input))
- for vn, vv := range vars {
- err := s.Add(vn, vv)
- require.NoError(t, err)
- }
- c, err := s.Compile()
- require.NoError(t, err)
- require.NotNil(t, c)
- return c
- }
- func compileError(t *testing.T, input string, vars M) {
- s := tengo.NewScript([]byte(input))
- for vn, vv := range vars {
- err := s.Add(vn, vv)
- require.NoError(t, err)
- }
- _, err := s.Compile()
- require.Error(t, err)
- }
- func compiledRun(t *testing.T, c *tengo.Compiled) {
- err := c.Run()
- require.NoError(t, err)
- }
- func compiledGet(
- t *testing.T,
- c *tengo.Compiled,
- name string,
- expected interface{},
- ) {
- v := c.Get(name)
- require.NotNil(t, v)
- require.Equal(t, expected, v.Value())
- }
- func compiledGetAll(
- t *testing.T,
- c *tengo.Compiled,
- expected M,
- ) {
- vars := c.GetAll()
- require.Equal(t, len(expected), len(vars))
- for k, v := range expected {
- var found bool
- for _, e := range vars {
- if e.Name() == k {
- require.Equal(t, v, e.Value())
- found = true
- }
- }
- require.True(t, found, "variable '%s' not found", k)
- }
- }
- func compiledIsDefined(
- t *testing.T,
- c *tengo.Compiled,
- name string,
- expected bool,
- ) {
- require.Equal(t, expected, c.IsDefined(name))
- }
- func TestCompiled_Clone(t *testing.T) {
- script := tengo.NewScript([]byte(`
- count += 1
- data["b"] = 2
- `))
- err := script.Add("data", map[string]interface{}{"a": 1})
- require.NoError(t, err)
- err = script.Add("count", 1000)
- require.NoError(t, err)
- compiled, err := script.Compile()
- require.NoError(t, err)
- clone := compiled.Clone()
- err = clone.RunContext(context.Background())
- require.NoError(t, err)
- require.Equal(t, 1000, compiled.Get("count").Int())
- require.Equal(t, 1, len(compiled.Get("data").Map()))
- require.Equal(t, 1001, clone.Get("count").Int())
- require.Equal(t, 2, len(clone.Get("data").Map()))
- }
|