Moonshark/core/workers/eventloop_test.go
2025-03-15 18:27:33 -05:00

742 lines
17 KiB
Go

package workers
import (
"context"
"sync"
"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
}
// Test creating a new event loop with default and custom configs
func TestNewEventLoop(t *testing.T) {
tests := []struct {
name string
config EventLoopConfig
expectError bool
}{
{
name: "Default config",
config: EventLoopConfig{},
expectError: false,
},
{
name: "Custom buffer size",
config: EventLoopConfig{
BufferSize: 200,
},
expectError: false,
},
{
name: "Custom timeout",
config: EventLoopConfig{
Timeout: 5 * time.Second,
},
expectError: false,
},
{
name: "With init function",
config: EventLoopConfig{
StateInit: func(state *luajit.State) error {
return nil
},
},
expectError: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
el, err := NewEventLoopWithConfig(tc.config)
if tc.expectError {
if err == nil {
t.Errorf("Expected error but got nil")
}
} else {
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if el == nil {
t.Errorf("Expected non-nil event loop")
} else {
el.Shutdown()
}
}
})
}
}
// Test basic job submission and execution
func TestEventLoopBasicSubmission(t *testing.T) {
el, err := NewEventLoop()
if err != nil {
t.Fatalf("Failed to create event loop: %v", err)
}
defer el.Shutdown()
// Simple return a value
bytecode := createTestBytecode(t, "return 42")
result, err := el.Submit(bytecode, nil)
if err != nil {
t.Fatalf("Failed to submit job: %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)
}
// Test more complex Lua code
bytecode = createTestBytecode(t, `
local result = 0
for i = 1, 10 do
result = result + i
end
return result
`)
result, err = el.Submit(bytecode, nil)
if err != nil {
t.Fatalf("Failed to submit job: %v", err)
}
num, ok = result.(float64)
if !ok {
t.Fatalf("Expected float64 result, got %T", result)
}
if num != 55 {
t.Errorf("Expected 55, got %f", num)
}
}
// Test context passing between Go and Lua
func TestEventLoopContext(t *testing.T) {
el, err := NewEventLoop()
if err != nil {
t.Fatalf("Failed to create event loop: %v", err)
}
defer el.Shutdown()
bytecode := createTestBytecode(t, `
return {
num = ctx.number,
str = ctx.text,
flag = ctx.enabled,
list = {ctx.items[1], ctx.items[2], ctx.items[3]},
}
`)
execCtx := NewContext()
execCtx.Set("number", 42.5)
execCtx.Set("text", "hello")
execCtx.Set("enabled", true)
execCtx.Set("items", []float64{10, 20, 30})
result, err := el.Submit(bytecode, execCtx)
if err != nil {
t.Fatalf("Failed to submit 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])
}
}
// Test complex nested context
nestedCtx := NewContext()
nestedCtx.Set("user", map[string]any{
"id": 123,
"name": "test user",
"roles": []any{
"admin",
"editor",
},
})
bytecode = createTestBytecode(t, `
return {
id = ctx.user.id,
name = ctx.user.name,
role1 = ctx.user.roles[1],
role2 = ctx.user.roles[2],
}
`)
result, err = el.Submit(bytecode, nestedCtx)
if err != nil {
t.Fatalf("Failed to submit job with nested context: %v", err)
}
resultMap, ok = result.(map[string]any)
if !ok {
t.Fatalf("Expected map result, got %T", result)
}
if resultMap["id"] != float64(123) {
t.Errorf("Expected id=123, got %v", resultMap["id"])
}
if resultMap["name"] != "test user" {
t.Errorf("Expected name='test user', got %v", resultMap["name"])
}
if resultMap["role1"] != "admin" {
t.Errorf("Expected role1='admin', got %v", resultMap["role1"])
}
if resultMap["role2"] != "editor" {
t.Errorf("Expected role2='editor', got %v", resultMap["role2"])
}
}
// Test execution timeout
func TestEventLoopTimeout(t *testing.T) {
// Create event loop with short timeout
el, err := NewEventLoopWithConfig(EventLoopConfig{
Timeout: 100 * time.Millisecond,
})
if err != nil {
t.Fatalf("Failed to create event loop: %v", err)
}
defer el.Shutdown()
// Create bytecode that runs for longer than the timeout
bytecode := createTestBytecode(t, `
-- Loop for 500ms
local start = os.time()
while os.difftime(os.time(), start) < 0.5 do end
return "done"
`)
// This should time out
_, err = el.Submit(bytecode, nil)
if err != ErrExecutionTimeout {
t.Errorf("Expected timeout error, got: %v", err)
}
// Now set a longer timeout and try again
el.SetTimeout(1 * time.Second)
// This should succeed
result, err := el.Submit(bytecode, nil)
if err != nil {
t.Fatalf("Expected success with longer timeout, got: %v", err)
}
if result != "done" {
t.Errorf("Expected 'done', got %v", result)
}
// Test per-call timeout with SubmitWithTimeout
bytecode = createTestBytecode(t, `
-- Loop for 300ms
local start = os.time()
while os.difftime(os.time(), start) < 0.3 do end
return "done again"
`)
// This should time out with a custom timeout
_, err = el.SubmitWithTimeout(bytecode, nil, 50*time.Millisecond)
if err == nil {
t.Errorf("Expected timeout error, got success")
}
// This should succeed with a longer custom timeout
result, err = el.SubmitWithTimeout(bytecode, nil, 500*time.Millisecond)
if err != nil {
t.Fatalf("Expected success with custom timeout, got: %v", err)
}
if result != "done again" {
t.Errorf("Expected 'done again', got %v", result)
}
}
// Test module registration and execution
func TestEventLoopModules(t *testing.T) {
// Define an init function that registers a simple "math" module
mathInit := func(state *luajit.State) error {
// Register the "add" function directly
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 math module with multiple functions
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 an event loop with our init function
el, err := NewEventLoopWithInit(mathInit)
if err != nil {
t.Fatalf("Failed to create event loop: %v", err)
}
defer el.Shutdown()
// Test the add function
bytecode1 := createTestBytecode(t, "return add(5, 7)")
result1, err := el.Submit(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 := el.Submit(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)
}
// Test multiple operations
bytecode3 := createTestBytecode(t, `
local a = add(10, 20)
local b = math2.subtract(a, 5)
return math2.multiply(b, 2)
`)
result3, err := el.Submit(bytecode3, nil)
if err != nil {
t.Fatalf("Failed to execute combined operations: %v", err)
}
num3, ok := result3.(float64)
if !ok || num3 != 50 {
t.Errorf("Expected ((10 + 20) - 5) * 2 = 50, got %v", result3)
}
}
// Test combined module init functions
func TestEventLoopCombinedModules(t *testing.T) {
// First init function adds a function to get a constant value
init1 := func(state *luajit.State) error {
return state.RegisterGoFunction("getAnswer", func(s *luajit.State) int {
s.PushNumber(42)
return 1
})
}
// Second init function registers a function that multiplies a number by 2
init2 := func(state *luajit.State) error {
return state.RegisterGoFunction("double", func(s *luajit.State) int {
n := s.ToNumber(1)
s.PushNumber(n * 2)
return 1
})
}
// Combine the init functions
combinedInit := CombineInitFuncs(init1, init2)
// Create an event loop with the combined init function
el, err := NewEventLoopWithInit(combinedInit)
if err != nil {
t.Fatalf("Failed to create event loop: %v", err)
}
defer el.Shutdown()
// Test using both functions together in a single script
bytecode := createTestBytecode(t, "return double(getAnswer())")
result, err := el.Submit(bytecode, nil)
if err != nil {
t.Fatalf("Failed to execute: %v", err)
}
num, ok := result.(float64)
if !ok || num != 84 {
t.Errorf("Expected double(getAnswer()) = 84, got %v", result)
}
}
// Test sandbox isolation between executions
func TestEventLoopSandboxIsolation(t *testing.T) {
el, err := NewEventLoop()
if err != nil {
t.Fatalf("Failed to create event loop: %v", err)
}
defer el.Shutdown()
// Create a script that tries to modify a global variable
bytecode1 := createTestBytecode(t, `
-- Set a "global" variable
my_global = "test value"
return true
`)
_, err = el.Submit(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 := el.Submit(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")
}
}
// Test error handling
func TestEventLoopErrorHandling(t *testing.T) {
el, err := NewEventLoop()
if err != nil {
t.Fatalf("Failed to create event loop: %v", err)
}
defer el.Shutdown()
// Test invalid bytecode
_, err = el.Submit([]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 = el.Submit(bytecode, nil)
if err == nil {
t.Errorf("Expected error from Lua error() call, got nil")
}
// Test with nil context (should work fine)
bytecode = createTestBytecode(t, "return ctx == nil")
result, err := el.Submit(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 access to restricted library
bytecode = createTestBytecode(t, `
-- Try to access io library directly
return io ~= nil
`)
result, err = el.Submit(bytecode, nil)
if err != nil {
t.Fatalf("Failed to execute sandbox test: %v", err)
}
// io should not be directly accessible
if result.(bool) {
t.Errorf("Expected io library to be restricted, but it was accessible")
}
}
// Test concurrent job submission
func TestEventLoopConcurrency(t *testing.T) {
el, err := NewEventLoopWithConfig(EventLoopConfig{
BufferSize: 100, // Buffer for concurrent submissions
Timeout: 5 * time.Second,
})
if err != nil {
t.Fatalf("Failed to create event loop: %v", err)
}
defer el.Shutdown()
// Create bytecode that returns its input value
bytecode := createTestBytecode(t, "return ctx.n")
// Submit multiple jobs concurrently
const jobCount = 50
var wg sync.WaitGroup
results := make([]int, jobCount)
wg.Add(jobCount)
for i := 0; i < jobCount; i++ {
i := i // Capture loop variable
go func() {
defer wg.Done()
// Create context with job number
ctx := NewContext()
ctx.Set("n", float64(i))
// Submit job
result, err := el.Submit(bytecode, ctx)
if err != nil {
t.Errorf("Job %d failed: %v", i, err)
return
}
// Verify result matches job number
num, ok := result.(float64)
if !ok {
t.Errorf("Job %d: expected float64, got %T", i, result)
return
}
results[i] = int(num)
}()
}
wg.Wait()
// Verify all results
for i, res := range results {
if res != i && res != 0 { // 0 means error already logged
t.Errorf("Expected result[%d] = %d, got %d", i, i, res)
}
}
}
// Test state consistency across multiple calls
func TestEventLoopStateConsistency(t *testing.T) {
// Create an event loop with a module that maintains count between calls
initFunc := func(state *luajit.State) error {
// Create a closure that increments a counter in upvalue
code := `
-- Create a counter with initial value 0
local counter = 0
-- Create a function that returns and increments the counter
function get_next_count()
local current = counter
counter = counter + 1
return current
end
`
return state.DoString(code)
}
el, err := NewEventLoopWithInit(initFunc)
if err != nil {
t.Fatalf("Failed to create event loop: %v", err)
}
defer el.Shutdown()
// Now run multiple scripts that call the counter function
bytecode := createTestBytecode(t, "return get_next_count()")
// Each call should return an incremented value
for i := 0; i < 5; i++ {
result, err := el.Submit(bytecode, nil)
if err != nil {
t.Fatalf("Call %d failed: %v", i, err)
}
num, ok := result.(float64)
if !ok {
t.Fatalf("Expected float64 result, got %T", result)
}
if int(num) != i {
t.Errorf("Expected count %d, got %d", i, int(num))
}
}
}
// Test shutdown and cleanup
func TestEventLoopShutdown(t *testing.T) {
el, err := NewEventLoop()
if err != nil {
t.Fatalf("Failed to create event loop: %v", err)
}
// Submit a job to verify it works
bytecode := createTestBytecode(t, "return 42")
_, err = el.Submit(bytecode, nil)
if err != nil {
t.Fatalf("Failed to submit job: %v", err)
}
// Shutdown
if err := el.Shutdown(); err != nil {
t.Errorf("Shutdown failed: %v", err)
}
// Submit after shutdown should fail
_, err = el.Submit(bytecode, nil)
if err != ErrLoopClosed {
t.Errorf("Expected ErrLoopClosed, got %v", err)
}
// Second shutdown should return error
if err := el.Shutdown(); err != ErrLoopClosed {
t.Errorf("Expected ErrLoopClosed on second shutdown, got %v", err)
}
}
// Test high load with multiple sequential and concurrent jobs
func TestEventLoopHighLoad(t *testing.T) {
el, err := NewEventLoopWithConfig(EventLoopConfig{
BufferSize: 1000, // Large buffer for high load
Timeout: 5 * time.Second,
})
if err != nil {
t.Fatalf("Failed to create event loop: %v", err)
}
defer el.Shutdown()
// Sequential load test
bytecode := createTestBytecode(t, `
-- Do some work
local result = 0
for i = 1, 1000 do
result = result + i
end
return result
`)
start := time.Now()
for i := 0; i < 100; i++ {
_, err := el.Submit(bytecode, nil)
if err != nil {
t.Fatalf("Sequential job %d failed: %v", i, err)
}
}
seqDuration := time.Since(start)
t.Logf("Sequential load test: 100 jobs in %v", seqDuration)
// Concurrent load test
start = time.Now()
var wg sync.WaitGroup
wg.Add(100)
for i := 0; i < 100; i++ {
go func() {
defer wg.Done()
_, err := el.Submit(bytecode, nil)
if err != nil {
t.Errorf("Concurrent job failed: %v", err)
}
}()
}
wg.Wait()
concDuration := time.Since(start)
t.Logf("Concurrent load test: 100 jobs in %v", concDuration)
}
// Test context cancellation
func TestEventLoopCancel(t *testing.T) {
el, err := NewEventLoop()
if err != nil {
t.Fatalf("Failed to create event loop: %v", err)
}
defer el.Shutdown()
// Create a long-running script
bytecode := createTestBytecode(t, `
-- Sleep for 500ms
local start = os.time()
while os.difftime(os.time(), start) < 0.5 do end
return "done"
`)
// Create a context that we can cancel
ctx, cancel := context.WithCancel(context.Background())
// Start execution in a goroutine
resultCh := make(chan any, 1)
errCh := make(chan error, 1)
go func() {
res, err := el.SubmitWithContext(ctx, bytecode, nil)
if err != nil {
errCh <- err
} else {
resultCh <- res
}
}()
// Cancel quickly
time.Sleep(50 * time.Millisecond)
cancel()
// Should get cancellation error
select {
case err := <-errCh:
if ctx.Err() == nil || err == nil {
t.Errorf("Expected context cancellation error")
}
case res := <-resultCh:
t.Errorf("Expected cancellation, got result: %v", res)
case <-time.After(1 * time.Second):
t.Errorf("Timed out waiting for cancellation")
}
}