feat: added lua

This commit is contained in:
2026-03-25 13:02:08 +03:00
parent 57f3d9e976
commit cf2e095fbe
5 changed files with 1633 additions and 15 deletions
+557
View File
@@ -0,0 +1,557 @@
package generator
import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"github.com/edmand46/arpack/parser"
)
func TestGenerateLua_BasicTypes(t *testing.T) {
schema := parser.Schema{
Messages: []parser.Message{
{
Name: "BasicTypes",
Fields: []parser.Field{
{Name: "Int8Field", Kind: parser.KindPrimitive, Primitive: parser.KindInt8},
{Name: "Int16Field", Kind: parser.KindPrimitive, Primitive: parser.KindInt16},
{Name: "Int32Field", Kind: parser.KindPrimitive, Primitive: parser.KindInt32},
{Name: "Uint8Field", Kind: parser.KindPrimitive, Primitive: parser.KindUint8},
{Name: "Uint16Field", Kind: parser.KindPrimitive, Primitive: parser.KindUint16},
{Name: "Uint32Field", Kind: parser.KindPrimitive, Primitive: parser.KindUint32},
{Name: "Float32Field", Kind: parser.KindPrimitive, Primitive: parser.KindFloat32},
{Name: "Float64Field", Kind: parser.KindPrimitive, Primitive: parser.KindFloat64},
{Name: "BoolField", Kind: parser.KindPrimitive, Primitive: parser.KindBool},
{Name: "StringField", Kind: parser.KindPrimitive, Primitive: parser.KindString},
},
},
},
}
lua, err := GenerateLuaSchema(schema, "test")
if err != nil {
t.Fatalf("GenerateLuaSchema failed: %v", err)
}
luaStr := string(lua)
if !strings.Contains(luaStr, "function M.new_basic_types()") {
t.Error("Missing constructor for BasicTypes")
}
if !strings.Contains(luaStr, "function M.serialize_basic_types(msg)") {
t.Error("Missing serializer for BasicTypes")
}
if !strings.Contains(luaStr, "function M.deserialize_basic_types(data, offset)") {
t.Error("Missing deserializer for BasicTypes")
}
if !strings.Contains(luaStr, "int8_field = 0") {
t.Error("Missing int8_field in constructor")
}
if !strings.Contains(luaStr, "string_field = ''") {
t.Error("Missing string_field default value")
}
if !strings.Contains(luaStr, "bool_field = false") {
t.Error("Missing bool_field default value")
}
}
func TestGenerateLua_Enum(t *testing.T) {
schema := parser.Schema{
Enums: []parser.Enum{
{
Name: "Opcode",
Primitive: parser.KindUint16,
Values: []parser.EnumValue{
{Name: "Unknown", Value: "0"},
{Name: "Join", Value: "1"},
{Name: "Leave", Value: "2"},
},
},
},
Messages: []parser.Message{
{
Name: "MessageWithEnum",
Fields: []parser.Field{
{Name: "Op", Kind: parser.KindPrimitive, Primitive: parser.KindUint16, NamedType: "Opcode"},
},
},
},
}
enumNames := map[string]struct{}{"Opcode": {}}
_ = enumNames
lua, err := GenerateLuaSchema(schema, "test")
if err != nil {
t.Fatalf("GenerateLuaSchema failed: %v", err)
}
luaStr := string(lua)
if !strings.Contains(luaStr, "M.Opcode = {") {
t.Error("Missing Opcode enum table")
}
if !strings.Contains(luaStr, "Unknown = 0") {
t.Error("Missing Unknown enum value")
}
if !strings.Contains(luaStr, "Join = 1") {
t.Error("Missing Join enum value")
}
}
func TestGenerateLua_NestedMessage(t *testing.T) {
schema := parser.Schema{
Messages: []parser.Message{
{
Name: "Vector3",
Fields: []parser.Field{
{Name: "X", Kind: parser.KindPrimitive, Primitive: parser.KindFloat32},
{Name: "Y", Kind: parser.KindPrimitive, Primitive: parser.KindFloat32},
{Name: "Z", Kind: parser.KindPrimitive, Primitive: parser.KindFloat32},
},
},
{
Name: "Player",
Fields: []parser.Field{
{Name: "Position", Kind: parser.KindNested, TypeName: "Vector3"},
{Name: "Health", Kind: parser.KindPrimitive, Primitive: parser.KindInt32},
},
},
},
}
lua, err := GenerateLuaSchema(schema, "test")
if err != nil {
t.Fatalf("GenerateLuaSchema failed: %v", err)
}
luaStr := string(lua)
if !strings.Contains(luaStr, "function M.new_vector3()") {
t.Error("Missing constructor for Vector3")
}
if !strings.Contains(luaStr, "function M.new_player()") {
t.Error("Missing constructor for Player")
}
if !strings.Contains(luaStr, "position = M.new_vector3()") {
t.Error("Missing nested initialization in Player constructor")
}
if !strings.Contains(luaStr, "M.serialize_vector3") {
t.Error("Missing Vector3 serializer call")
}
}
func TestGenerateLua_FixedArray(t *testing.T) {
schema := parser.Schema{
Messages: []parser.Message{
{
Name: "WithFixedArray",
Fields: []parser.Field{
{
Name: "Values",
Kind: parser.KindFixedArray,
FixedLen: 3,
Elem: &parser.Field{
Kind: parser.KindPrimitive,
Primitive: parser.KindFloat32,
},
},
},
},
},
}
lua, err := GenerateLuaSchema(schema, "test")
if err != nil {
t.Fatalf("GenerateLuaSchema failed: %v", err)
}
luaStr := string(lua)
if !strings.Contains(luaStr, "values = {}") {
t.Error("Missing values array initialization")
}
if !strings.Contains(luaStr, "for _i_values = 1, 3 do") {
t.Error("Missing fixed array loop in serializer")
}
}
func TestGenerateLua_Slice(t *testing.T) {
schema := parser.Schema{
Messages: []parser.Message{
{
Name: "WithSlice",
Fields: []parser.Field{
{
Name: "Items",
Kind: parser.KindSlice,
Elem: &parser.Field{
Kind: parser.KindPrimitive,
Primitive: parser.KindInt32,
},
},
},
},
},
}
lua, err := GenerateLuaSchema(schema, "test")
if err != nil {
t.Fatalf("GenerateLuaSchema failed: %v", err)
}
luaStr := string(lua)
if !strings.Contains(luaStr, "items = {}") {
t.Error("Missing items slice initialization")
}
if !strings.Contains(luaStr, "local _len_items = #(msg.items or {})") {
t.Error("Missing slice length serialization")
}
if !strings.Contains(luaStr, "for _i_items = 1, _len_items do") {
t.Error("Missing slice iteration in serializer")
}
}
func TestGenerateLua_BoolPacking(t *testing.T) {
schema := parser.Schema{
Messages: []parser.Message{
{
Name: "WithBools",
Fields: []parser.Field{
{Name: "A", Kind: parser.KindPrimitive, Primitive: parser.KindBool},
{Name: "B", Kind: parser.KindPrimitive, Primitive: parser.KindBool},
{Name: "C", Kind: parser.KindPrimitive, Primitive: parser.KindBool},
{Name: "Value", Kind: parser.KindPrimitive, Primitive: parser.KindInt32},
},
},
},
}
lua, err := GenerateLuaSchema(schema, "test")
if err != nil {
t.Fatalf("GenerateLuaSchema failed: %v", err)
}
luaStr := string(lua)
if !strings.Contains(luaStr, "local _bool_byte_0 = 0") {
t.Error("Missing bool byte packing variable")
}
if !strings.Contains(luaStr, "if msg.a then _bool_byte_0 = bit.bor(_bool_byte_0, 1) end") {
t.Error("Missing first bool packing check with bit.bor")
}
if !strings.Contains(luaStr, "if msg.b then _bool_byte_0 = bit.bor(_bool_byte_0, 2) end") {
t.Error("Missing second bool packing check with bit.bor")
}
if !strings.Contains(luaStr, "msg.a = bit.band(_bool_byte_0, 1) ~= 0") {
t.Error("Missing bit.band for bool deserialization")
}
}
func TestGenerateLua_QuantizedFloat(t *testing.T) {
schema := parser.Schema{
Messages: []parser.Message{
{
Name: "WithQuantized",
Fields: []parser.Field{
{
Name: "Position",
Kind: parser.KindPrimitive,
Primitive: parser.KindFloat32,
Quant: &parser.QuantInfo{
Min: -500,
Max: 500,
Bits: 16,
},
},
},
},
},
}
lua, err := GenerateLuaSchema(schema, "test")
if err != nil {
t.Fatalf("GenerateLuaSchema failed: %v", err)
}
luaStr := string(lua)
if !strings.Contains(luaStr, "math.floor") {
t.Error("Missing math.floor for quantization")
}
if !strings.Contains(luaStr, "write_u16_le") {
t.Error("Missing u16 write for 16-bit quantization")
}
}
func TestToSnakeCase(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"", ""},
{"A", "a"},
{"AB", "ab"},
{"AbCd", "ab_cd"},
{"ABC", "abc"},
{"PlayerID", "player_id"},
{"HTTPResponse", "http_response"},
{"XMLHttpRequest", "xml_http_request"},
{"getHTTPResponse", "get_http_response"},
}
for _, tt := range tests {
result := toSnakeCase(tt.input)
if result != tt.expected {
t.Errorf("toSnakeCase(%q) = %q, want %q", tt.input, result, tt.expected)
}
}
}
func TestLuaHelpersGenerated(t *testing.T) {
schema := parser.Schema{
Messages: []parser.Message{
{
Name: "Empty",
Fields: []parser.Field{},
},
},
}
lua, err := GenerateLuaSchema(schema, "test")
if err != nil {
t.Fatalf("GenerateLuaSchema failed: %v", err)
}
luaStr := string(lua)
helpers := []string{
"local bit = require('bit')",
"buffer too short for u8",
"buffer too short for bool",
"local function write_u8(n)",
"buffer too short for u16",
"local function write_u16_le(n)",
"buffer too short for u32",
"local function write_u32_le(n)",
"local function read_f32_le(data, offset)",
"local function write_f32_le(n)",
"local function read_f64_le(data, offset)",
"local function write_f64_le(n)",
"local function write_bool(v)",
"buffer too short for string",
"local function write_string(s)",
}
for _, helper := range helpers {
if !strings.Contains(luaStr, helper) {
t.Errorf("Missing helper: %s", helper)
}
}
}
func TestGenerateLua_Int64NotSupported(t *testing.T) {
schema := parser.Schema{
Messages: []parser.Message{
{
Name: "WithInt64",
Fields: []parser.Field{
{Name: "Value", Kind: parser.KindPrimitive, Primitive: parser.KindInt64},
},
},
},
}
_, err := GenerateLuaSchema(schema, "test")
if err == nil {
t.Fatal("Expected error for int64 field, got nil")
}
if !strings.Contains(err.Error(), "int64/uint64") {
t.Errorf("Expected error mentioning int64/uint64, got: %v", err)
}
if !strings.Contains(err.Error(), "LuaJIT/Defold") {
t.Errorf("Expected error mentioning LuaJIT/Defold, got: %v", err)
}
}
func TestGenerateLua_Uint64NotSupported(t *testing.T) {
schema := parser.Schema{
Messages: []parser.Message{
{
Name: "WithUint64",
Fields: []parser.Field{
{Name: "Value", Kind: parser.KindPrimitive, Primitive: parser.KindUint64},
},
},
},
}
_, err := GenerateLuaSchema(schema, "test")
if err == nil {
t.Fatal("Expected error for uint64 field, got nil")
}
if !strings.Contains(err.Error(), "int64/uint64") {
t.Errorf("Expected error mentioning int64/uint64, got: %v", err)
}
}
func TestGenerateLua_Int64InSliceNotSupported(t *testing.T) {
schema := parser.Schema{
Messages: []parser.Message{
{
Name: "WithInt64Slice",
Fields: []parser.Field{
{
Name: "Values",
Kind: parser.KindSlice,
Elem: &parser.Field{
Kind: parser.KindPrimitive,
Primitive: parser.KindInt64,
},
},
},
},
},
}
_, err := GenerateLuaSchema(schema, "test")
if err == nil {
t.Fatal("Expected error for int64 in slice, got nil")
}
if !strings.Contains(err.Error(), "int64/uint64") {
t.Errorf("Expected error mentioning int64/uint64, got: %v", err)
}
}
func TestGenerateLua_BoundsChecks(t *testing.T) {
schema := parser.Schema{
Messages: []parser.Message{
{
Name: "SimpleMessage",
Fields: []parser.Field{
{Name: "ID", Kind: parser.KindPrimitive, Primitive: parser.KindUint32},
{Name: "Name", Kind: parser.KindPrimitive, Primitive: parser.KindString},
},
},
},
}
lua, err := GenerateLuaSchema(schema, "test")
if err != nil {
t.Fatalf("GenerateLuaSchema failed: %v", err)
}
luaStr := string(lua)
// Check that bounds check function exists
if !strings.Contains(luaStr, "check_bounds") {
t.Error("Missing check_bounds function")
}
// Check that read_u16_le has bounds check
if !strings.Contains(luaStr, "buffer too short for u16") {
t.Error("Missing bounds check in read_u16_le")
}
// Check that read_u32_le has bounds check
if !strings.Contains(luaStr, "buffer too short for u32") {
t.Error("Missing bounds check in read_u32_le")
}
// Check that read_string has bounds check
if !strings.Contains(luaStr, "buffer too short for string") {
t.Error("Missing bounds check in read_string")
}
// Check that deserialize function has min size check (message name is preserved in error)
if !strings.Contains(luaStr, "buffer too short for SimpleMessage") {
t.Error("Missing min size check in deserialize function")
}
// Check that read_u8 has bounds check
if !strings.Contains(luaStr, "buffer too short for u8") {
t.Error("Missing bounds check in read_u8")
}
// Check that read_bool has bounds check
if !strings.Contains(luaStr, "buffer too short for bool") {
t.Error("Missing bounds check in read_bool")
}
// Check that read_i8 has bounds check
if !strings.Contains(luaStr, "buffer too short for i8") {
t.Error("Missing bounds check in read_i8")
}
}
func TestGenerateLua_RuntimeFloatEdgeCases(t *testing.T) {
if _, err := exec.LookPath("luajit"); err != nil {
t.Skip("luajit not found")
}
schema := parser.Schema{
Messages: []parser.Message{
{
Name: "FloatEdges",
Fields: []parser.Field{
{Name: "F32", Kind: parser.KindPrimitive, Primitive: parser.KindFloat32},
{Name: "F64", Kind: parser.KindPrimitive, Primitive: parser.KindFloat64},
},
},
},
}
lua, err := GenerateLuaSchema(schema, "messages")
if err != nil {
t.Fatalf("GenerateLuaSchema failed: %v", err)
}
dir := t.TempDir()
modulePath := filepath.Join(dir, "messages_gen.lua")
if err := os.WriteFile(modulePath, lua, 0o600); err != nil {
t.Fatalf("write module: %v", err)
}
scriptPath := filepath.Join(dir, "check.lua")
script := `local messages = require("messages_gen")
local function bytes_to_hex(s)
return (s:gsub(".", function(c) return string.format("%02x", string.byte(c)) end))
end
local neg_zero = string.char(0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 128)
local msg = messages.deserialize_float_edges(neg_zero, 1)
print(bytes_to_hex(messages.serialize_float_edges(msg)))
local subnormal = string.char(1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0)
msg = messages.deserialize_float_edges(subnormal, 1)
print(bytes_to_hex(messages.serialize_float_edges(msg)))
`
if err := os.WriteFile(scriptPath, []byte(script), 0o600); err != nil {
t.Fatalf("write script: %v", err)
}
cmd := exec.Command("luajit", "check.lua")
cmd.Dir = dir
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("luajit failed: %v\n%s", err, out)
}
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
if len(lines) != 2 {
t.Fatalf("expected 2 output lines, got %d: %q", len(lines), string(out))
}
if lines[0] != "000000800000000000000080" {
t.Fatalf("negative zero roundtrip mismatch: %s", lines[0])
}
if lines[1] != "010000000100000000000000" {
t.Fatalf("subnormal roundtrip mismatch: %s", lines[1])
}
}