test: add comprehensive lua-engine tests (51 parameterized cases)

- Add tests for basic Lua operations (numbers, strings, booleans)
- Add tests for table handling and context data access
- Add tests for function execution (simple, recursive, closures)
- Add tests for control flow (if/else, for/while loops)
- Add tests for logging (log/print capture)
- Add tests for error handling (syntax, runtime, explicit)
- Add tests for multiple return values and standard library
- Coverage improved from 62% to 89% for lua-engine.ts
- Overall coverage now at 83% (up from 63%)
This commit is contained in:
2025-12-25 16:15:34 +00:00
parent b4350abb9e
commit 98357d0381

View File

@@ -0,0 +1,357 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { LuaEngine, createLuaEngine, type LuaExecutionContext } from './lua-engine'
describe('lua-engine', () => {
let engine: LuaEngine
beforeEach(() => {
engine = createLuaEngine()
})
afterEach(() => {
engine.destroy()
})
describe('createLuaEngine', () => {
it('should create a new LuaEngine instance', () => {
const newEngine = createLuaEngine()
expect(newEngine).toBeInstanceOf(LuaEngine)
newEngine.destroy()
})
})
describe('execute', () => {
describe('basic operations', () => {
it.each([
{ name: 'return number', code: 'return 42', expected: 42 },
{ name: 'return string', code: 'return "hello"', expected: 'hello' },
{ name: 'return boolean true', code: 'return true', expected: true },
{ name: 'return boolean false', code: 'return false', expected: false },
{ name: 'return nil', code: 'return nil', expected: null },
{ name: 'arithmetic', code: 'return 10 + 5 * 2', expected: 20 },
{ name: 'string concatenation', code: 'return "hello" .. " " .. "world"', expected: 'hello world' },
{ name: 'comparison', code: 'return 5 > 3', expected: true },
])('should execute $name', async ({ code, expected }) => {
const result = await engine.execute(code)
expect(result.success).toBe(true)
expect(result.result).toBe(expected)
expect(result.error).toBeUndefined()
})
})
describe('table handling', () => {
it.each([
{
name: 'array-like table returns object with numeric keys',
code: 'return {1, 2, 3, 4, 5}',
// Note: Lua tables with numeric keys are returned as objects with string keys
// This is a known limitation of the current implementation
expected: { '1': 1, '2': 2, '3': 3, '4': 4, '5': 5 },
},
{
name: 'object-like table',
code: 'return {name = "Alice", age = 30}',
expected: { name: 'Alice', age: 30 },
},
{
name: 'nested table',
code: 'return {user = {name = "Bob"}, count = 1}',
expected: { user: { name: 'Bob' }, count: 1 },
},
{
name: 'mixed table with nested numeric keys',
code: 'return {items = {1, 2, 3}, total = 6}',
// Nested array-like tables also become objects with numeric string keys
expected: { items: { '1': 1, '2': 2, '3': 3 }, total: 6 },
},
{
name: 'empty table',
code: 'return {}',
expected: {},
},
])('should handle $name', async ({ code, expected }) => {
const result = await engine.execute(code)
expect(result.success).toBe(true)
expect(result.result).toEqual(expected)
})
})
describe('context data', () => {
it.each([
{
name: 'access context.data number',
code: 'return context.data.value * 2',
context: { data: { value: 21 } },
expected: 42,
},
{
name: 'access context.data string',
code: 'return context.data.name',
context: { data: { name: 'test' } },
expected: 'test',
},
{
name: 'access context.data boolean',
code: 'return context.data.active',
context: { data: { active: true } },
expected: true,
},
{
name: 'access nested context.data',
code: 'return context.data.user.name',
context: { data: { user: { name: 'Alice' } } },
expected: 'Alice',
},
{
name: 'access context.data array',
code: 'return context.data.items[2]',
context: { data: { items: [10, 20, 30] } },
expected: 20,
},
])('should $name', async ({ code, context, expected }) => {
const result = await engine.execute(code, context)
expect(result.success).toBe(true)
expect(result.result).toBe(expected)
})
it('should access context.user', async () => {
const result = await engine.execute(
'return context.user.id',
{ user: { id: 'user123', name: 'Test' } }
)
expect(result.success).toBe(true)
expect(result.result).toBe('user123')
})
})
describe('functions', () => {
it.each([
{
name: 'simple function',
code: `
function add(a, b)
return a + b
end
return add(3, 4)
`,
expected: 7,
},
{
name: 'recursive function',
code: `
function factorial(n)
if n <= 1 then return 1 end
return n * factorial(n - 1)
end
return factorial(5)
`,
expected: 120,
},
{
name: 'higher-order function',
code: `
function apply(f, x)
return f(x)
end
function double(n)
return n * 2
end
return apply(double, 10)
`,
expected: 20,
},
{
name: 'closure',
code: `
function counter()
local count = 0
return function()
count = count + 1
return count
end
end
local c = counter()
c()
c()
return c()
`,
expected: 3,
},
])('should execute $name', async ({ code, expected }) => {
const result = await engine.execute(code)
expect(result.success).toBe(true)
expect(result.result).toBe(expected)
})
})
describe('control flow', () => {
it.each([
{
name: 'if-then-else true branch',
code: `
local x = 10
if x > 5 then
return "big"
else
return "small"
end
`,
expected: 'big',
},
{
name: 'if-then-else false branch',
code: `
local x = 3
if x > 5 then
return "big"
else
return "small"
end
`,
expected: 'small',
},
{
name: 'for loop',
code: `
local sum = 0
for i = 1, 5 do
sum = sum + i
end
return sum
`,
expected: 15,
},
{
name: 'while loop',
code: `
local count = 0
local n = 10
while n > 0 do
count = count + 1
n = n - 2
end
return count
`,
expected: 5,
},
])('should execute $name', async ({ code, expected }) => {
const result = await engine.execute(code)
expect(result.success).toBe(true)
expect(result.result).toBe(expected)
})
})
describe('logging', () => {
it('should capture log() calls', async () => {
const result = await engine.execute(`
log("message 1")
log("message 2")
return "done"
`)
expect(result.success).toBe(true)
expect(result.logs).toContain('message 1')
expect(result.logs).toContain('message 2')
})
it('should capture print() calls', async () => {
const result = await engine.execute(`
print("printed output")
return true
`)
expect(result.success).toBe(true)
expect(result.logs).toContain('printed output')
})
it.each([
{ name: 'number', code: 'log(42)', expected: '42' },
{ name: 'boolean', code: 'log(true)', expected: 'true' },
{ name: 'multiple args', code: 'log("a", "b", "c")', expected: 'a b c' },
])('should log $name correctly', async ({ code, expected }) => {
const result = await engine.execute(code)
expect(result.logs).toContain(expected)
})
})
describe('error handling', () => {
it.each([
{
name: 'syntax error',
code: 'this is not valid lua !!!',
errorContains: 'Syntax error',
},
{
name: 'undefined variable',
code: 'return undefinedVar.property',
errorContains: 'Runtime error',
},
{
name: 'type error',
code: 'return "string" + 5',
errorContains: 'error',
},
{
name: 'explicit error',
code: 'error("intentional error")',
errorContains: 'intentional error',
},
])('should handle $name', async ({ code, errorContains }) => {
const result = await engine.execute(code)
expect(result.success).toBe(false)
expect(result.error?.toLowerCase()).toContain(errorContains.toLowerCase())
})
})
describe('multiple return values', () => {
it('should handle multiple return values', async () => {
const result = await engine.execute('return 1, 2, 3')
expect(result.success).toBe(true)
expect(result.result).toEqual([1, 2, 3])
})
it('should handle function with multiple returns', async () => {
const result = await engine.execute(`
function swap(a, b)
return b, a
end
return swap(1, 2)
`)
expect(result.success).toBe(true)
expect(result.result).toEqual([2, 1])
})
})
describe('no return value', () => {
it('should handle code with no return', async () => {
const result = await engine.execute('local x = 1 + 1')
expect(result.success).toBe(true)
expect(result.result).toBeNull()
})
})
describe('standard library', () => {
it.each([
{ name: 'math.floor', code: 'return math.floor(3.7)', expected: 3 },
{ name: 'math.ceil', code: 'return math.ceil(3.2)', expected: 4 },
{ name: 'math.abs', code: 'return math.abs(-5)', expected: 5 },
{ name: 'math.max', code: 'return math.max(1, 5, 3)', expected: 5 },
{ name: 'math.min', code: 'return math.min(1, 5, 3)', expected: 1 },
{ name: 'string.upper', code: 'return string.upper("hello")', expected: 'HELLO' },
{ name: 'string.lower', code: 'return string.lower("HELLO")', expected: 'hello' },
{ name: 'string.len', code: 'return string.len("test")', expected: 4 },
{ name: 'string.sub', code: 'return string.sub("hello", 2, 4)', expected: 'ell' },
{ name: 'table.concat', code: 'return table.concat({"a", "b", "c"}, ", ")', expected: 'a, b, c' },
])('should support $name', async ({ code, expected }) => {
const result = await engine.execute(code)
expect(result.success).toBe(true)
expect(result.result).toBe(expected)
})
})
})
describe('destroy', () => {
it('should cleanup without error', () => {
const testEngine = createLuaEngine()
expect(() => testEngine.destroy()).not.toThrow()
})
})
})