Alex Roitman b55300b729 Lua scripting feature. (#224)
* Start on lua scripting

* Implement evalsha, script load, script exists, and script flush

* Type conversions from lua to resp/json.
Refactor to make luastate and luascripts persistent in the controller.

* Change controller.command and all underlying commands to return resp.Value.
Serialize only during the ouput.

* First stab at tile38 call from lua

* Change tile38 into tile38.call in Lua

* Property return errors from scripts

* Minor refactoring.  No locking on script run

* Cleanup/refactoring

* Create a pool of 5 lua states, allow for more as needed. Refactor.

* Use safe map for scripts.  Add a limit for max number of lua states.  Refactor.

* Refactor

* Refactor script commands into atomic, read-only, and non-atomic classes.
Proper locking for all three classes.
Add tests for scripts

* More tests for scripts

* Properly escape newlines in lua-produced errors

* Better test for readonly failure

* Correctly convert ok/err messages between lua and resp.
Add pcall, sha1hex, error_reply, status_reply functions to tile38 namespace in lua.

* Add pcall test. Change writeErr to work with string argument

* Make sure eval/evalsha never attempt to write AOF

* Add eval-set and eval-get to benchmarks

* Fix eval benchmark tests, add more

* Improve benchmarks

* Optimizations and refactoring.

* Add lua memtest

* Typo

* Add dependency

* golint fixes

* gofmt fixes

* Add scripting commands to the core/commands.json

* Use ARGV for args inside lua
2017-10-05 08:20:40 -07:00

434 lines
11 KiB
Go

package lua
import (
"context"
"strings"
"testing"
"time"
)
func TestCallStackOverflow(t *testing.T) {
L := NewState(Options{
CallStackSize: 3,
})
defer L.Close()
errorIfScriptNotFail(t, L, `
local function a()
end
local function b()
a()
end
local function c()
print(_printregs())
b()
end
c()
`, "stack overflow")
}
func TestSkipOpenLibs(t *testing.T) {
L := NewState(Options{SkipOpenLibs: true})
defer L.Close()
errorIfScriptNotFail(t, L, `print("")`,
"attempt to call a non-function object")
L2 := NewState()
defer L2.Close()
errorIfScriptFail(t, L2, `print("")`)
}
func TestGetAndReplace(t *testing.T) {
L := NewState()
defer L.Close()
L.Push(LString("a"))
L.Replace(1, LString("b"))
L.Replace(0, LString("c"))
errorIfNotEqual(t, LNil, L.Get(0))
errorIfNotEqual(t, LNil, L.Get(-10))
errorIfNotEqual(t, L.Env, L.Get(EnvironIndex))
errorIfNotEqual(t, LString("b"), L.Get(1))
L.Push(LString("c"))
L.Push(LString("d"))
L.Replace(-2, LString("e"))
errorIfNotEqual(t, LString("e"), L.Get(-2))
registry := L.NewTable()
L.Replace(RegistryIndex, registry)
L.G.Registry = registry
errorIfGFuncNotFail(t, L, func(L *LState) int {
L.Replace(RegistryIndex, LNil)
return 0
}, "registry must be a table")
errorIfGFuncFail(t, L, func(L *LState) int {
env := L.NewTable()
L.Replace(EnvironIndex, env)
errorIfNotEqual(t, env, L.Get(EnvironIndex))
return 0
})
errorIfGFuncNotFail(t, L, func(L *LState) int {
L.Replace(EnvironIndex, LNil)
return 0
}, "environment must be a table")
errorIfGFuncFail(t, L, func(L *LState) int {
gbl := L.NewTable()
L.Replace(GlobalsIndex, gbl)
errorIfNotEqual(t, gbl, L.G.Global)
return 0
})
errorIfGFuncNotFail(t, L, func(L *LState) int {
L.Replace(GlobalsIndex, LNil)
return 0
}, "_G must be a table")
L2 := NewState()
defer L2.Close()
clo := L2.NewClosure(func(L2 *LState) int {
L2.Replace(UpvalueIndex(1), LNumber(3))
errorIfNotEqual(t, LNumber(3), L2.Get(UpvalueIndex(1)))
return 0
}, LNumber(1), LNumber(2))
L2.SetGlobal("clo", clo)
errorIfScriptFail(t, L2, `clo()`)
}
func TestRemove(t *testing.T) {
L := NewState()
defer L.Close()
L.Push(LString("a"))
L.Push(LString("b"))
L.Push(LString("c"))
L.Remove(4)
errorIfNotEqual(t, LString("a"), L.Get(1))
errorIfNotEqual(t, LString("b"), L.Get(2))
errorIfNotEqual(t, LString("c"), L.Get(3))
errorIfNotEqual(t, 3, L.GetTop())
L.Remove(3)
errorIfNotEqual(t, LString("a"), L.Get(1))
errorIfNotEqual(t, LString("b"), L.Get(2))
errorIfNotEqual(t, LNil, L.Get(3))
errorIfNotEqual(t, 2, L.GetTop())
L.Push(LString("c"))
L.Remove(-10)
errorIfNotEqual(t, LString("a"), L.Get(1))
errorIfNotEqual(t, LString("b"), L.Get(2))
errorIfNotEqual(t, LString("c"), L.Get(3))
errorIfNotEqual(t, 3, L.GetTop())
L.Remove(2)
errorIfNotEqual(t, LString("a"), L.Get(1))
errorIfNotEqual(t, LString("c"), L.Get(2))
errorIfNotEqual(t, LNil, L.Get(3))
errorIfNotEqual(t, 2, L.GetTop())
}
func TestToInt(t *testing.T) {
L := NewState()
defer L.Close()
L.Push(LNumber(10))
L.Push(LString("99.9"))
L.Push(L.NewTable())
errorIfNotEqual(t, 10, L.ToInt(1))
errorIfNotEqual(t, 99, L.ToInt(2))
errorIfNotEqual(t, 0, L.ToInt(3))
}
func TestToInt64(t *testing.T) {
L := NewState()
defer L.Close()
L.Push(LNumber(10))
L.Push(LString("99.9"))
L.Push(L.NewTable())
errorIfNotEqual(t, int64(10), L.ToInt64(1))
errorIfNotEqual(t, int64(99), L.ToInt64(2))
errorIfNotEqual(t, int64(0), L.ToInt64(3))
}
func TestToNumber(t *testing.T) {
L := NewState()
defer L.Close()
L.Push(LNumber(10))
L.Push(LString("99.9"))
L.Push(L.NewTable())
errorIfNotEqual(t, LNumber(10), L.ToNumber(1))
errorIfNotEqual(t, LNumber(99.9), L.ToNumber(2))
errorIfNotEqual(t, LNumber(0), L.ToNumber(3))
}
func TestToString(t *testing.T) {
L := NewState()
defer L.Close()
L.Push(LNumber(10))
L.Push(LString("99.9"))
L.Push(L.NewTable())
errorIfNotEqual(t, "10", L.ToString(1))
errorIfNotEqual(t, "99.9", L.ToString(2))
errorIfNotEqual(t, "", L.ToString(3))
}
func TestToTable(t *testing.T) {
L := NewState()
defer L.Close()
L.Push(LNumber(10))
L.Push(LString("99.9"))
L.Push(L.NewTable())
errorIfFalse(t, L.ToTable(1) == nil, "index 1 must be nil")
errorIfFalse(t, L.ToTable(2) == nil, "index 2 must be nil")
errorIfNotEqual(t, L.Get(3), L.ToTable(3))
}
func TestToFunction(t *testing.T) {
L := NewState()
defer L.Close()
L.Push(LNumber(10))
L.Push(LString("99.9"))
L.Push(L.NewFunction(func(L *LState) int { return 0 }))
errorIfFalse(t, L.ToFunction(1) == nil, "index 1 must be nil")
errorIfFalse(t, L.ToFunction(2) == nil, "index 2 must be nil")
errorIfNotEqual(t, L.Get(3), L.ToFunction(3))
}
func TestToUserData(t *testing.T) {
L := NewState()
defer L.Close()
L.Push(LNumber(10))
L.Push(LString("99.9"))
L.Push(L.NewUserData())
errorIfFalse(t, L.ToUserData(1) == nil, "index 1 must be nil")
errorIfFalse(t, L.ToUserData(2) == nil, "index 2 must be nil")
errorIfNotEqual(t, L.Get(3), L.ToUserData(3))
}
func TestToChannel(t *testing.T) {
L := NewState()
defer L.Close()
L.Push(LNumber(10))
L.Push(LString("99.9"))
var ch chan LValue
L.Push(LChannel(ch))
errorIfFalse(t, L.ToChannel(1) == nil, "index 1 must be nil")
errorIfFalse(t, L.ToChannel(2) == nil, "index 2 must be nil")
errorIfNotEqual(t, ch, L.ToChannel(3))
}
func TestObjLen(t *testing.T) {
L := NewState()
defer L.Close()
errorIfNotEqual(t, 3, L.ObjLen(LString("abc")))
tbl := L.NewTable()
tbl.Append(LTrue)
tbl.Append(LTrue)
errorIfNotEqual(t, 2, L.ObjLen(tbl))
mt := L.NewTable()
L.SetField(mt, "__len", L.NewFunction(func(L *LState) int {
tbl := L.CheckTable(1)
L.Push(LNumber(tbl.Len() + 1))
return 1
}))
L.SetMetatable(tbl, mt)
errorIfNotEqual(t, 3, L.ObjLen(tbl))
errorIfNotEqual(t, 0, L.ObjLen(LNumber(10)))
}
func TestConcat(t *testing.T) {
L := NewState()
defer L.Close()
errorIfNotEqual(t, "a1c", L.Concat(LString("a"), LNumber(1), LString("c")))
}
func TestPCall(t *testing.T) {
L := NewState()
defer L.Close()
L.Register("f1", func(L *LState) int {
panic("panic!")
return 0
})
errorIfScriptNotFail(t, L, `f1()`, "panic!")
L.Push(L.GetGlobal("f1"))
err := L.PCall(0, 0, L.NewFunction(func(L *LState) int {
L.Push(LString("by handler"))
return 1
}))
errorIfFalse(t, strings.Contains(err.Error(), "by handler"), "")
err = L.PCall(0, 0, L.NewFunction(func(L *LState) int {
L.RaiseError("error!")
return 1
}))
errorIfFalse(t, strings.Contains(err.Error(), "error!"), "")
err = L.PCall(0, 0, L.NewFunction(func(L *LState) int {
panic("panicc!")
return 1
}))
errorIfFalse(t, strings.Contains(err.Error(), "panicc!"), "")
}
func TestCoroutineApi1(t *testing.T) {
L := NewState()
defer L.Close()
co, _ := L.NewThread()
errorIfScriptFail(t, L, `
function coro(v)
assert(v == 10)
local ret1, ret2 = coroutine.yield(1,2,3)
assert(ret1 == 11)
assert(ret2 == 12)
coroutine.yield(4)
return 5
end
`)
fn := L.GetGlobal("coro").(*LFunction)
st, err, values := L.Resume(co, fn, LNumber(10))
errorIfNotEqual(t, ResumeYield, st)
errorIfNotNil(t, err)
errorIfNotEqual(t, 3, len(values))
errorIfNotEqual(t, LNumber(1), values[0].(LNumber))
errorIfNotEqual(t, LNumber(2), values[1].(LNumber))
errorIfNotEqual(t, LNumber(3), values[2].(LNumber))
st, err, values = L.Resume(co, fn, LNumber(11), LNumber(12))
errorIfNotEqual(t, ResumeYield, st)
errorIfNotNil(t, err)
errorIfNotEqual(t, 1, len(values))
errorIfNotEqual(t, LNumber(4), values[0].(LNumber))
st, err, values = L.Resume(co, fn)
errorIfNotEqual(t, ResumeOK, st)
errorIfNotNil(t, err)
errorIfNotEqual(t, 1, len(values))
errorIfNotEqual(t, LNumber(5), values[0].(LNumber))
L.Register("myyield", func(L *LState) int {
return L.Yield(L.ToNumber(1))
})
errorIfScriptFail(t, L, `
function coro_error()
coroutine.yield(1,2,3)
myyield(4)
assert(false, "--failed--")
end
`)
fn = L.GetGlobal("coro_error").(*LFunction)
co, _ = L.NewThread()
st, err, values = L.Resume(co, fn)
errorIfNotEqual(t, ResumeYield, st)
errorIfNotNil(t, err)
errorIfNotEqual(t, 3, len(values))
errorIfNotEqual(t, LNumber(1), values[0].(LNumber))
errorIfNotEqual(t, LNumber(2), values[1].(LNumber))
errorIfNotEqual(t, LNumber(3), values[2].(LNumber))
st, err, values = L.Resume(co, fn)
errorIfNotEqual(t, ResumeYield, st)
errorIfNotNil(t, err)
errorIfNotEqual(t, 1, len(values))
errorIfNotEqual(t, LNumber(4), values[0].(LNumber))
st, err, values = L.Resume(co, fn)
errorIfNotEqual(t, ResumeError, st)
errorIfNil(t, err)
errorIfFalse(t, strings.Contains(err.Error(), "--failed--"), "error message must be '--failed--'")
st, err, values = L.Resume(co, fn)
errorIfNotEqual(t, ResumeError, st)
errorIfNil(t, err)
errorIfFalse(t, strings.Contains(err.Error(), "can not resume a dead thread"), "can not resume a dead thread")
}
func TestContextTimeout(t *testing.T) {
L := NewState()
defer L.Close()
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
L.SetContext(ctx)
errorIfNotEqual(t, ctx, L.Context())
err := L.DoString(`
local clock = os.clock
function sleep(n) -- seconds
local t0 = clock()
while clock() - t0 <= n do end
end
sleep(3)
`)
errorIfNil(t, err)
errorIfFalse(t, strings.Contains(err.Error(), "context deadline exceeded"), "execution must be canceled")
oldctx := L.RemoveContext()
errorIfNotEqual(t, ctx, oldctx)
errorIfNotNil(t, L.ctx)
}
func TestContextCancel(t *testing.T) {
L := NewState()
defer L.Close()
ctx, cancel := context.WithCancel(context.Background())
errch := make(chan error, 1)
L.SetContext(ctx)
go func() {
errch <- L.DoString(`
local clock = os.clock
function sleep(n) -- seconds
local t0 = clock()
while clock() - t0 <= n do end
end
sleep(3)
`)
}()
time.Sleep(1 * time.Second)
cancel()
err := <-errch
errorIfNil(t, err)
errorIfFalse(t, strings.Contains(err.Error(), "context canceled"), "execution must be canceled")
}
func TestContextWithCroutine(t *testing.T) {
L := NewState()
defer L.Close()
ctx, cancel := context.WithCancel(context.Background())
L.SetContext(ctx)
defer cancel()
L.DoString(`
function coro()
local i = 0
while true do
coroutine.yield(i)
i = i+1
end
return i
end
`)
co, cocancel := L.NewThread()
defer cocancel()
fn := L.GetGlobal("coro").(*LFunction)
_, err, values := L.Resume(co, fn)
errorIfNotNil(t, err)
errorIfNotEqual(t, LNumber(0), values[0])
// cancel the parent context
cancel()
_, err, values = L.Resume(co, fn)
errorIfNil(t, err)
errorIfFalse(t, strings.Contains(err.Error(), "context canceled"), "coroutine execution must be canceled when the parent context is canceled")
}
func TestPCallAfterFail(t *testing.T) {
L := NewState()
defer L.Close()
errFn := L.NewFunction(func(L *LState) int {
L.RaiseError("error!")
return 0
})
changeError := L.NewFunction(func(L *LState) int {
L.Push(errFn)
err := L.PCall(0, 0, nil)
if err != nil {
L.RaiseError("A New Error")
}
return 0
})
L.Push(changeError)
err := L.PCall(0, 0, nil)
errorIfFalse(t, strings.Contains(err.Error(), "A New Error"), "error not propogated correctly")
}