374 lines
8.5 KiB
Go
374 lines
8.5 KiB
Go
package runner
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
|
|
)
|
|
|
|
// Helper function to create bytecode for testing
|
|
func createTestBytecode(t *testing.T, code string) []byte {
|
|
state := luajit.New()
|
|
if state == nil {
|
|
t.Fatal("Failed to create Lua state")
|
|
}
|
|
defer state.Close()
|
|
|
|
bytecode, err := state.CompileBytecode(code, "test")
|
|
if err != nil {
|
|
t.Fatalf("Failed to compile test bytecode: %v", err)
|
|
}
|
|
|
|
return bytecode
|
|
}
|
|
|
|
func TestRunnerBasic(t *testing.T) {
|
|
runner, err := NewRunner()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create runner: %v", err)
|
|
}
|
|
defer runner.Close()
|
|
|
|
bytecode := createTestBytecode(t, "return 42")
|
|
|
|
result, err := runner.Run(bytecode, nil, "")
|
|
if err != nil {
|
|
t.Fatalf("Failed to run script: %v", err)
|
|
}
|
|
|
|
num, ok := result.(float64)
|
|
if !ok {
|
|
t.Fatalf("Expected float64 result, got %T", result)
|
|
}
|
|
|
|
if num != 42 {
|
|
t.Errorf("Expected 42, got %f", num)
|
|
}
|
|
}
|
|
|
|
func TestRunnerWithContext(t *testing.T) {
|
|
runner, err := NewRunner()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create runner: %v", err)
|
|
}
|
|
defer runner.Close()
|
|
|
|
bytecode := createTestBytecode(t, `
|
|
return {
|
|
num = ctx.number,
|
|
str = ctx.text,
|
|
flag = ctx.enabled,
|
|
list = {ctx.table[1], ctx.table[2], ctx.table[3]},
|
|
}
|
|
`)
|
|
|
|
execCtx := NewContext()
|
|
execCtx.Set("number", 42.5)
|
|
execCtx.Set("text", "hello")
|
|
execCtx.Set("enabled", true)
|
|
execCtx.Set("table", []float64{10, 20, 30})
|
|
|
|
result, err := runner.Run(bytecode, execCtx, "")
|
|
if err != nil {
|
|
t.Fatalf("Failed to run job: %v", err)
|
|
}
|
|
|
|
// Result should be a map
|
|
resultMap, ok := result.(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("Expected map result, got %T", result)
|
|
}
|
|
|
|
// Check values
|
|
if resultMap["num"] != 42.5 {
|
|
t.Errorf("Expected num=42.5, got %v", resultMap["num"])
|
|
}
|
|
if resultMap["str"] != "hello" {
|
|
t.Errorf("Expected str=hello, got %v", resultMap["str"])
|
|
}
|
|
if resultMap["flag"] != true {
|
|
t.Errorf("Expected flag=true, got %v", resultMap["flag"])
|
|
}
|
|
|
|
arr, ok := resultMap["list"].([]float64)
|
|
if !ok {
|
|
t.Fatalf("Expected []float64, got %T", resultMap["list"])
|
|
}
|
|
|
|
expected := []float64{10, 20, 30}
|
|
for i, v := range expected {
|
|
if arr[i] != v {
|
|
t.Errorf("Expected list[%d]=%f, got %f", i, v, arr[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRunnerWithTimeout(t *testing.T) {
|
|
runner, err := NewRunner()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create runner: %v", err)
|
|
}
|
|
defer runner.Close()
|
|
|
|
// Create bytecode that sleeps
|
|
bytecode := createTestBytecode(t, `
|
|
-- Sleep for 500ms
|
|
local start = os.time()
|
|
while os.difftime(os.time(), start) < 0.5 do end
|
|
return "done"
|
|
`)
|
|
|
|
// Test with timeout that should succeed
|
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
|
defer cancel()
|
|
|
|
result, err := runner.RunWithContext(ctx, bytecode, nil, "")
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error with sufficient timeout: %v", err)
|
|
}
|
|
if result != "done" {
|
|
t.Errorf("Expected 'done', got %v", result)
|
|
}
|
|
|
|
// Test with timeout that should fail
|
|
ctx, cancel = context.WithTimeout(context.Background(), 50*time.Millisecond)
|
|
defer cancel()
|
|
|
|
_, err = runner.RunWithContext(ctx, bytecode, nil, "")
|
|
if err == nil {
|
|
t.Errorf("Expected timeout error, got nil")
|
|
}
|
|
}
|
|
|
|
func TestSandboxIsolation(t *testing.T) {
|
|
runner, err := NewRunner()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create runner: %v", err)
|
|
}
|
|
defer runner.Close()
|
|
|
|
// Create a script that tries to modify a global variable
|
|
bytecode1 := createTestBytecode(t, `
|
|
-- Set a "global" variable
|
|
my_global = "test value"
|
|
return true
|
|
`)
|
|
|
|
_, err = runner.Run(bytecode1, nil, "")
|
|
if err != nil {
|
|
t.Fatalf("Failed to execute first script: %v", err)
|
|
}
|
|
|
|
// Now try to access that variable from another script
|
|
bytecode2 := createTestBytecode(t, `
|
|
-- Try to access the previously set global
|
|
return my_global ~= nil
|
|
`)
|
|
|
|
result, err := runner.Run(bytecode2, nil, "")
|
|
if err != nil {
|
|
t.Fatalf("Failed to execute second script: %v", err)
|
|
}
|
|
|
|
// The variable should not be accessible (sandbox isolation)
|
|
if result.(bool) {
|
|
t.Errorf("Expected sandbox isolation, but global variable was accessible")
|
|
}
|
|
}
|
|
|
|
func TestRunnerWithInit(t *testing.T) {
|
|
// Define an init function that registers a simple "math" module
|
|
mathInit := func(state *luajit.State) error {
|
|
// Register the "add" function
|
|
err := state.RegisterGoFunction("add", func(s *luajit.State) int {
|
|
a := s.ToNumber(1)
|
|
b := s.ToNumber(2)
|
|
s.PushNumber(a + b)
|
|
return 1 // Return one result
|
|
})
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Register a whole module
|
|
mathFuncs := map[string]luajit.GoFunction{
|
|
"multiply": func(s *luajit.State) int {
|
|
a := s.ToNumber(1)
|
|
b := s.ToNumber(2)
|
|
s.PushNumber(a * b)
|
|
return 1
|
|
},
|
|
"subtract": func(s *luajit.State) int {
|
|
a := s.ToNumber(1)
|
|
b := s.ToNumber(2)
|
|
s.PushNumber(a - b)
|
|
return 1
|
|
},
|
|
}
|
|
|
|
return RegisterModule(state, "math2", mathFuncs)
|
|
}
|
|
|
|
// Create a runner with our init function
|
|
runner, err := NewRunner(WithInitFunc(mathInit))
|
|
if err != nil {
|
|
t.Fatalf("Failed to create runner: %v", err)
|
|
}
|
|
defer runner.Close()
|
|
|
|
// Test the add function
|
|
bytecode1 := createTestBytecode(t, "return add(5, 7)")
|
|
result1, err := runner.Run(bytecode1, nil, "")
|
|
if err != nil {
|
|
t.Fatalf("Failed to call add function: %v", err)
|
|
}
|
|
|
|
num1, ok := result1.(float64)
|
|
if !ok || num1 != 12 {
|
|
t.Errorf("Expected add(5, 7) = 12, got %v", result1)
|
|
}
|
|
|
|
// Test the math2 module
|
|
bytecode2 := createTestBytecode(t, "return math2.multiply(6, 8)")
|
|
result2, err := runner.Run(bytecode2, nil, "")
|
|
if err != nil {
|
|
t.Fatalf("Failed to call math2.multiply: %v", err)
|
|
}
|
|
|
|
num2, ok := result2.(float64)
|
|
if !ok || num2 != 48 {
|
|
t.Errorf("Expected math2.multiply(6, 8) = 48, got %v", result2)
|
|
}
|
|
}
|
|
|
|
func TestConcurrentExecution(t *testing.T) {
|
|
const jobs = 20
|
|
|
|
runner, err := NewRunner(WithBufferSize(20))
|
|
if err != nil {
|
|
t.Fatalf("Failed to create runner: %v", err)
|
|
}
|
|
defer runner.Close()
|
|
|
|
// Create bytecode that returns its input
|
|
bytecode := createTestBytecode(t, "return ctx.n")
|
|
|
|
// Run multiple jobs concurrently
|
|
results := make(chan int, jobs)
|
|
for i := 0; i < jobs; i++ {
|
|
i := i // Capture loop variable
|
|
go func() {
|
|
execCtx := NewContext()
|
|
execCtx.Set("n", float64(i))
|
|
|
|
result, err := runner.Run(bytecode, execCtx, "")
|
|
if err != nil {
|
|
t.Errorf("Job %d failed: %v", i, err)
|
|
results <- -1
|
|
return
|
|
}
|
|
|
|
num, ok := result.(float64)
|
|
if !ok {
|
|
t.Errorf("Job %d: expected float64, got %T", i, result)
|
|
results <- -1
|
|
return
|
|
}
|
|
|
|
results <- int(num)
|
|
}()
|
|
}
|
|
|
|
// Collect results
|
|
seen := make(map[int]bool)
|
|
for i := 0; i < jobs; i++ {
|
|
result := <-results
|
|
if result != -1 {
|
|
seen[result] = true
|
|
}
|
|
}
|
|
|
|
// Verify all jobs were processed
|
|
if len(seen) != jobs {
|
|
t.Errorf("Expected %d unique results, got %d", jobs, len(seen))
|
|
}
|
|
}
|
|
|
|
func TestRunnerClose(t *testing.T) {
|
|
runner, err := NewRunner()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create runner: %v", err)
|
|
}
|
|
|
|
// Submit a job to verify runner works
|
|
bytecode := createTestBytecode(t, "return 42")
|
|
_, err = runner.Run(bytecode, nil, "")
|
|
if err != nil {
|
|
t.Fatalf("Failed to run job: %v", err)
|
|
}
|
|
|
|
// Close
|
|
if err := runner.Close(); err != nil {
|
|
t.Errorf("Close failed: %v", err)
|
|
}
|
|
|
|
// Run after close should fail
|
|
_, err = runner.Run(bytecode, nil, "")
|
|
if err != ErrRunnerClosed {
|
|
t.Errorf("Expected ErrRunnerClosed, got %v", err)
|
|
}
|
|
|
|
// Second close should return error
|
|
if err := runner.Close(); err != ErrRunnerClosed {
|
|
t.Errorf("Expected ErrRunnerClosed on second close, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestErrorHandling(t *testing.T) {
|
|
runner, err := NewRunner()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create runner: %v", err)
|
|
}
|
|
defer runner.Close()
|
|
|
|
// Test invalid bytecode
|
|
_, err = runner.Run([]byte("not valid bytecode"), nil, "")
|
|
if err == nil {
|
|
t.Errorf("Expected error for invalid bytecode, got nil")
|
|
}
|
|
|
|
// Test Lua runtime error
|
|
bytecode := createTestBytecode(t, `
|
|
error("intentional error")
|
|
return true
|
|
`)
|
|
|
|
_, err = runner.Run(bytecode, nil, "")
|
|
if err == nil {
|
|
t.Errorf("Expected error from Lua error() call, got nil")
|
|
}
|
|
|
|
// Test with nil context
|
|
bytecode = createTestBytecode(t, "return ctx == nil")
|
|
result, err := runner.Run(bytecode, nil, "")
|
|
if err != nil {
|
|
t.Errorf("Unexpected error with nil context: %v", err)
|
|
}
|
|
if result.(bool) != true {
|
|
t.Errorf("Expected ctx to be nil in Lua, but it wasn't")
|
|
}
|
|
|
|
// Test invalid context value
|
|
execCtx := NewContext()
|
|
execCtx.Set("param", complex128(1+2i)) // Unsupported type
|
|
|
|
bytecode = createTestBytecode(t, "return ctx.param")
|
|
_, err = runner.Run(bytecode, execCtx, "")
|
|
if err == nil {
|
|
t.Errorf("Expected error for unsupported context value type, got nil")
|
|
}
|
|
}
|