From b71ba3323141b42f5cf3e1dd6348d0bab89f9625 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=87=8E=E5=A3=B0?= Date: Wed, 20 Mar 2024 10:51:17 +0800 Subject: [PATCH 1/8] fix: unmarshal should based on type of golang --- internal/luai/decode.go | 26 ++- .../{encoding_test.go => example_test.go} | 63 ++++--- internal/plugin_test.go | 23 +++ internal/testdata/plugins/java/buggy.lua | 169 ++++++++++++++++++ 4 files changed, 251 insertions(+), 30 deletions(-) rename internal/luai/{encoding_test.go => example_test.go} (81%) create mode 100644 internal/testdata/plugins/java/buggy.lua diff --git a/internal/luai/decode.go b/internal/luai/decode.go index 907bdc2e..04fb1919 100644 --- a/internal/luai/decode.go +++ b/internal/luai/decode.go @@ -88,13 +88,18 @@ func indirect(v reflect.Value) reflect.Value { func storeLiteral(value reflect.Value, lvalue lua.LValue) { value = indirect(value) - switch lvalue.Type() { - case lua.LTString: + + switch value.Kind() { + case reflect.String: value.SetString(lvalue.String()) - case lua.LTNumber: - value.SetInt(int64(lvalue.(lua.LNumber))) - case lua.LTBool: + case reflect.Bool: value.SetBool(bool(lvalue.(lua.LBool))) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + value.SetInt(int64(lvalue.(lua.LNumber))) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + value.SetUint(uint64(lvalue.(lua.LNumber))) + case reflect.Float32, reflect.Float64: + value.SetFloat(float64(lvalue.(lua.LNumber))) } } @@ -106,6 +111,15 @@ func objectInterface(lvalue *lua.LTable) any { return v } +// To unmarshal lua obj into an interface value, +// Unmarshal stores one of these in the interface value: +// +// - bool, for LTBool +// - float64, for LTNumber +// - string, for LTString +// - []interface{}, for LTTable arrays +// - map[string]interface{}, for LTTable objects +// - nil for LTNil func valueInterface(lvalue lua.LValue) any { switch lvalue.Type() { case lua.LTTable: @@ -117,7 +131,7 @@ func valueInterface(lvalue lua.LValue) any { case lua.LTString: return lvalue.String() case lua.LTNumber: - return int(lvalue.(lua.LNumber)) + return float64(lvalue.(lua.LNumber)) case lua.LTBool: return bool(lvalue.(lua.LBool)) } diff --git a/internal/luai/encoding_test.go b/internal/luai/example_test.go similarity index 81% rename from internal/luai/encoding_test.go rename to internal/luai/example_test.go index ae304a5d..f4bcf951 100644 --- a/internal/luai/encoding_test.go +++ b/internal/luai/example_test.go @@ -55,7 +55,7 @@ type complexStruct struct { Slice []any } -func TestEncoding(t *testing.T) { +func TestExample(t *testing.T) { teardownSuite := setupSuite(t) defer teardownSuite(t) @@ -64,8 +64,14 @@ func TestEncoding(t *testing.T) { "key2": 2, "key3": true, } + mFloat64 := map[string]interface{}{ + "key1": "value1", + "key2": float64(2), + "key3": true, + } s := []any{"value1", 2, true} + sFloat64 := []any{"value1", float64(2), true} t.Run("Struct", func(t *testing.T) { luaVm := lua.NewState() @@ -128,13 +134,13 @@ func TestEncoding(t *testing.T) { assert(table.field3 == true) print("lua Struct with Tag done") `); err != nil { - t.Fatal(err) + t.Fatalf("struct with tag test failed: %v", err) } struct2 := testStructTag{} err = Unmarshal(table, &struct2) if err != nil { - t.Fatal(err) + t.Fatalf("unmarshal struct with tag failed: %v", err) } if !reflect.DeepEqual(test, struct2) { @@ -177,43 +183,44 @@ func TestEncoding(t *testing.T) { // Unmarshal // Test case for map - m2 := map[string]any{} + mUnmarshaled := map[string]any{} fmt.Println("==== start unmarshal ====") - err = Unmarshal(table, &m2) + err = Unmarshal(table, &mUnmarshaled) if err != nil { t.Fatalf("unmarshal map failed: %v", err) } - fmt.Printf("m2: %+v\n", m2) + fmt.Printf("mUnmarshaled: %+v\n", mUnmarshaled) - if !reflect.DeepEqual(m, m2) { - t.Errorf("expected %+v, got %+v", m, m2) + // unmarshal a LTNumber to any will be converted to float64 + if !reflect.DeepEqual(mFloat64, mUnmarshaled) { + t.Errorf("expected %+v, got %+v", mFloat64, mUnmarshaled) } // Test case for slice - s2 := []any{} + sUnmarshaled := []any{} - err = Unmarshal(slice, &s2) + err = Unmarshal(slice, &sUnmarshaled) if err != nil { t.Fatalf("unmarshal slice failed: %v", err) } - fmt.Printf("s2: %+v\n", s2) + fmt.Printf("sUnmarshaled: %+v\n", sUnmarshaled) - if !reflect.DeepEqual(s, s2) { - t.Errorf("expected %+v, got %+v", s, s2) + if !reflect.DeepEqual(sFloat64, sUnmarshaled) { + t.Errorf("expected %+v, got %+v", sFloat64, sUnmarshaled) } - var s3 any - err = Unmarshal(slice, &s3) + var sUnmarshalAny any + err = Unmarshal(slice, &sUnmarshalAny) if err != nil { t.Fatalf("unmarshal slice failed: %v", err) } - if !reflect.DeepEqual(s, s3) { - t.Errorf("expected %+v, got %+v", s, s3) + if !reflect.DeepEqual(sFloat64, sUnmarshalAny) { + t.Errorf("expected %+v, got %+v", sFloat64, sUnmarshalAny) } }) @@ -266,15 +273,23 @@ func TestEncoding(t *testing.T) { t.Fatalf("unmarshal map failed: %v", err) } - isEqual := reflect.DeepEqual(input, output) - if !isEqual { - t.Fatalf("expected %+v, got %+v", input, output) - } - fmt.Printf("output: %+v\n", output) - if !reflect.DeepEqual(input, output) { - t.Errorf("expected %+v, got %+v", input, output) + expected := complexStruct{ + Field1: "value1", + Field2: 123, + Field3: true, + Struct: testStructTag{ + Field1: "value1", + Field2: 2, + Field3: true, + }, + Map: mFloat64, + Slice: sFloat64, + } + + if !reflect.DeepEqual(expected, output) { + t.Errorf("expected %+v, got %+v", expected, output) } }) diff --git a/internal/plugin_test.go b/internal/plugin_test.go index 103028a9..417f6634 100644 --- a/internal/plugin_test.go +++ b/internal/plugin_test.go @@ -225,3 +225,26 @@ func TestPlugin(t *testing.T) { } }) } + +//go:embed testdata/plugins/java/buggy.lua +var buggyPluginContent string +var buggyPluginPath = "testdata/plugins/java/buggy.lua" + +func TestInvalidPlugin(t *testing.T) { + teardownSuite := setupSuite(t) + defer teardownSuite(t) + + t.Run("Version field is type int", func(t *testing.T) { + manager := NewSdkManager() + plugin, _ := NewLuaPlugin(buggyPluginContent, buggyPluginPath, manager) + + pkgs, _ := plugin.Available() + + pkg0 := pkgs[0] + addition0 := pkg0.Additions[0] + + if addition0.Version != "8" { + t.Errorf("expected version '8', got '%s'", addition0.Version) + } + }) +} diff --git a/internal/testdata/plugins/java/buggy.lua b/internal/testdata/plugins/java/buggy.lua new file mode 100644 index 00000000..cbada7f6 --- /dev/null +++ b/internal/testdata/plugins/java/buggy.lua @@ -0,0 +1,169 @@ +--- Common libraries provided by VersionFox (optional) +local http = require("http") +local json = require("json") +local html = require("html") + +--- The following two parameters are injected by VersionFox at runtime +--- Operating system type at runtime (Windows, Linux, Darwin) +OS_TYPE = "" +--- Operating system architecture at runtime (amd64, arm64, etc.) +ARCH_TYPE = "" + +PLUGIN = { + --- Plugin name + name = "java", + --- Plugin author + author = "Lihan", + --- Plugin version + version = "0.0.1", + --- Plugin description + description = "xxx", + -- Update URL + updateUrl = "{URL}/sdk.lua", + -- minimum compatible vfox version + minRuntimeVersion = "0.2.2", +} + +--- Returns some pre-installed information, such as version number, download address, local files, etc. +--- If checksum is provided, vfox will automatically check it for you. +--- @param ctx table +--- @field ctx.version string User-input version +--- @return table Version information +function PLUGIN:PreInstall(ctx) + local version = ctx.version + local runtimeVersion = ctx.runtimeVersion + return { + --- Version number + version = "version", + --- remote URL or local file path [optional] + url = "xxx", + --- SHA256 checksum [optional] + sha256 = "xxx", + --- md5 checksum [optional] + md5 = "xxx", + --- sha1 checksum [optional] + sha1 = "xxx", + --- sha512 checksum [optional] + sha512 = "xx", + --- additional need files [optional] + addition = { + { + --- additional file name ! + name = "xxx", + --- remote URL or local file path [optional] + url = "xxx", + --- SHA256 checksum [optional] + sha256 = "xxx", + --- md5 checksum [optional] + md5 = "xxx", + --- sha1 checksum [optional] + sha1 = "xxx", + --- sha512 checksum [optional] + sha512 = "xx", + } + } + } +end + +--- Extension point, called after PreInstall, can perform additional operations, +--- such as file operations for the SDK installation directory or compile source code +--- Currently can be left unimplemented! +function PLUGIN:PostInstall(ctx) + --- ctx.rootPath SDK installation directory + local rootPath = ctx.rootPath + local runtimeVersion = ctx.runtimeVersion + local sdkInfo = ctx.sdkInfo['sdk-name'] + local path = sdkInfo.path + local version = sdkInfo.version + local name = sdkInfo.name +end + +--- Return all available versions provided by this plugin +--- @param ctx table Empty table used as context, for future extension +--- @return table Descriptions of available versions and accompanying tool descriptions +function PLUGIN:Available(ctx) + local runtimeVersion = ctx.runtimeVersion + return { + { + version = "xxxx", + note = "LTS", + addition = { + { + name = "npm", + version = 8, + } + } + } + } +end + +--- Each SDK may have different environment variable configurations. +--- This allows plugins to define custom environment variables (including PATH settings) +--- Note: Be sure to distinguish between environment variable settings for different platforms! +--- @param ctx table Context information +--- @field ctx.path string SDK installation directory +function PLUGIN:EnvKeys(ctx) + --- this variable is same as ctx.sdkInfo['plugin-name'].path + local mainPath = ctx.path + local runtimeVersion = ctx.runtimeVersion + local sdkInfo = ctx.sdkInfo['sdk-name'] + local path = sdkInfo.path + local version = sdkInfo.version + local name = sdkInfo.name + return { + { + key = "JAVA_HOME", + value = mainPath + }, + { + key = "PATH", + value = mainPath .. "/bin" + }, + { + key = "PATH", + value = mainPath .. "/bin2" + } + } +end + +--- When user invoke `use` command, this function will be called to get the +--- valid version information. +--- @param ctx table Context information +function PLUGIN:PreUse(ctx) + local runtimeVersion = ctx.runtimeVersion + --- user input version + local version = ctx.version + --- installed sdks + local sdkInfo = ctx.installedSdks['xxxx'] + local path = sdkInfo.path + local name = sdkInfo.name + local sdkVersion = sdkInfo.version + + --- working directory + local cwd = ctx.cwd + + printTable(ctx) + + --- user input scope + local scope = ctx.scope + + if (scope == "global") then + print("return 9.9.9") + return { + version = "9.9.9", + } + end + + if (scope == "project") then + print("return 10.0.0") + return { + version = "10.0.0", + } + end + + print("return 1.0.0") + + return { + version = "1.0.0" + } +end From d52a2dfaf5098e3db2fea7b96428bdcbc25c940c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=87=8E=E5=A3=B0?= Date: Fri, 22 Mar 2024 17:29:31 +0800 Subject: [PATCH 2/8] fix: encoding map[int] will cause error --- internal/luai/encode.go | 12 ++++- internal/luai/example_test.go | 96 +++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 1 deletion(-) diff --git a/internal/luai/encode.go b/internal/luai/encode.go index f85eaf3d..1cff0f64 100644 --- a/internal/luai/encode.go +++ b/internal/luai/encode.go @@ -98,7 +98,17 @@ func Marshal(state *lua.LState, v any) (lua.LValue, error) { return nil, err } - table.RawSetString(key.String(), value) + switch key.Kind() { + case reflect.String: + table.RawSetString(key.String(), value) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + table.RawSetInt(int(key.Int()), value) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + table.RawSetInt(int(key.Uint()), value) + default: + return nil, errors.New("marshal: unsupported type " + key.Kind().String() + " for key") + } + } return table, nil default: diff --git a/internal/luai/example_test.go b/internal/luai/example_test.go index f4bcf951..85fb3bd0 100644 --- a/internal/luai/example_test.go +++ b/internal/luai/example_test.go @@ -326,3 +326,99 @@ func TestExample(t *testing.T) { } }) } + +func TestCases(t *testing.T) { + teardownSuite := setupSuite(t) + defer teardownSuite(t) + + m := map[string]interface{}{ + "key1": "value1", + "key2": 2, + "key3": true, + } + mFloat64 := map[string]interface{}{ + "key1": "value1", + "key2": float64(2), + "key3": true, + } + + s := []any{"value1", 2, true} + sFloat64 := []any{"value1", float64(2), true} + + var unmarshalTests = []struct { + CaseName string + in any + ptr any // new(type) + out any + err error + }{ + { + CaseName: "UnmarshalAnyMap", + in: m, + ptr: &map[string]any{}, + out: &mFloat64, + }, + { + CaseName: "UnmarshalAnySlice", + in: s, + ptr: &[]any{}, + out: &sFloat64, + }, + { + CaseName: "Map[Int]", + in: map[int]int{ + 1: 1, + 2: 2, + }, + ptr: &map[int]int{}, + out: &map[int]int{ + 1: 1, + 2: 2, + }, + }, + } + + for _, tt := range unmarshalTests { + if tt.CaseName != "Map[Int]" { + continue + } + t.Run(tt.CaseName, func(t *testing.T) { + L := lua.NewState() + defer L.Close() + + table, err := Marshal(L, tt.in) + if err != nil { + t.Fatalf("marshal map failed: %v", err) + } + + L.SetGlobal("m", table) + + if err := L.DoString(` + function printTable(t, indent) + indent = indent or 0 + local strIndent = string.rep(" ", indent) + for key, value in pairs(t) do + local keyStr = tostring(key) + local valueStr = tostring(value) + if type(value) == "table" then + print(strIndent .. "[" .. keyStr .. "] =>") + printTable(value, indent + 1) + else + print(strIndent .. "[" .. keyStr .. "] => " .. valueStr) + end + end + end + printTable(m) + `); err != nil { + t.Errorf("print table error: %v", err) + } + err = Unmarshal(table, tt.ptr) + if err != tt.err { + t.Errorf("expected %+v, got %+v", tt.err, err) + } + if !reflect.DeepEqual(tt.out, tt.ptr) { + t.Errorf("expected %+v, got %+v", tt.out, tt.ptr) + } + }) + } +} From 890433ffdb97ae5fb8b13e088a2a61ef431b8010 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=87=8E=E5=A3=B0?= Date: Fri, 22 Mar 2024 17:31:52 +0800 Subject: [PATCH 3/8] chore: update comment --- internal/luai/decode.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/luai/decode.go b/internal/luai/decode.go index 04fb1919..5890d465 100644 --- a/internal/luai/decode.go +++ b/internal/luai/decode.go @@ -161,7 +161,7 @@ func unmarshalWorker(value lua.LValue, reflected reflect.Value) error { result := valueInterface(value) reflected.Set(reflect.ValueOf(result)) } - // map[T1]T2 where T1 is string, an integer type + // map[T1]T2 where T1 is string or an integer type case reflect.Map: t := reflected.Type() keyType := t.Key() From 0afd2669449ccd331b1750ab08d0b6548fb90a0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=87=8E=E5=A3=B0?= Date: Fri, 22 Mar 2024 17:36:22 +0800 Subject: [PATCH 4/8] test: update testcase --- internal/luai/example_test.go | 39 ++++++++++++++--------------------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/internal/luai/example_test.go b/internal/luai/example_test.go index 85fb3bd0..c129576c 100644 --- a/internal/luai/example_test.go +++ b/internal/luai/example_test.go @@ -346,11 +346,12 @@ func TestCases(t *testing.T) { sFloat64 := []any{"value1", float64(2), true} var unmarshalTests = []struct { - CaseName string - in any - ptr any // new(type) - out any - err error + CaseName string + in any + ptr any // new(type) + out any + luaValidationScript string + err error }{ { CaseName: "UnmarshalAnyMap", @@ -375,6 +376,11 @@ func TestCases(t *testing.T) { 1: 1, 2: 2, }, + luaValidationScript: ` + assert(m[1] == 1) + assert(m[2] == 2) + print("lua Map[Int] done") + `, }, } @@ -393,25 +399,12 @@ func TestCases(t *testing.T) { L.SetGlobal("m", table) - if err := L.DoString(` - function printTable(t, indent) - indent = indent or 0 - local strIndent = string.rep(" ", indent) - for key, value in pairs(t) do - local keyStr = tostring(key) - local valueStr = tostring(value) - if type(value) == "table" then - print(strIndent .. "[" .. keyStr .. "] =>") - printTable(value, indent + 1) - else - print(strIndent .. "[" .. keyStr .. "] => " .. valueStr) - end - end - end - printTable(m) - `); err != nil { - t.Errorf("print table error: %v", err) + if tt.luaValidationScript != "" { + if err := L.DoString(tt.luaValidationScript); err != nil { + t.Errorf("validate %s error: %v", tt.CaseName, err) + } } + err = Unmarshal(table, tt.ptr) if err != tt.err { t.Errorf("expected %+v, got %+v", tt.err, err) From 6b719ae0f97f8933523ec72b77788ce4b7c6113e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=87=8E=E5=A3=B0?= Date: Fri, 22 Mar 2024 19:25:41 +0800 Subject: [PATCH 5/8] test: update testcase --- internal/luai/example_test.go | 345 +++++++---------------- internal/plugin_test.go | 23 -- internal/testdata/plugins/java/buggy.lua | 169 ----------- 3 files changed, 100 insertions(+), 437 deletions(-) delete mode 100644 internal/testdata/plugins/java/buggy.lua diff --git a/internal/luai/example_test.go b/internal/luai/example_test.go index c129576c..43ae79f4 100644 --- a/internal/luai/example_test.go +++ b/internal/luai/example_test.go @@ -59,260 +59,27 @@ func TestExample(t *testing.T) { teardownSuite := setupSuite(t) defer teardownSuite(t) - m := map[string]interface{}{ - "key1": "value1", - "key2": 2, - "key3": true, - } - mFloat64 := map[string]interface{}{ - "key1": "value1", - "key2": float64(2), - "key3": true, - } - - s := []any{"value1", 2, true} - sFloat64 := []any{"value1", float64(2), true} - - t.Run("Struct", func(t *testing.T) { - luaVm := lua.NewState() - defer luaVm.Close() - - test := testStruct{ - Field1: "test", - Field2: 1, - Field3: true, - } - - _table, err := Marshal(luaVm, &test) - if err != nil { - t.Fatal(err) - } - - luaVm.SetGlobal("table", _table) - - if err := luaVm.DoString(` - assert(table.Field1 == "test") - assert(table.Field2 == 1) - assert(table.Field3 == true) - print("lua Struct done") - `); err != nil { - t.Fatal(err) - } - - struct2 := testStruct{} - err = Unmarshal(_table, &struct2) - if err != nil { - t.Fatal(err) - } - - if !reflect.DeepEqual(test, struct2) { - t.Errorf("expected %+v, got %+v", test, struct2) - } - }) - - t.Run("Struct with Tag", func(t *testing.T) { - luaVm := lua.NewState() - defer luaVm.Close() - - test := testStructTag{ - Field1: "test", - Field2: 1, - Field3: true, - } - - _table, err := Marshal(luaVm, &test) - if err != nil { - t.Fatal(err) - } - - table := _table.(*lua.LTable) - - luaVm.SetGlobal("table", table) - if err := luaVm.DoString(` - assert(table.field1 == "test") - assert(table.field2 == 1) - assert(table.field3 == true) - print("lua Struct with Tag done") - `); err != nil { - t.Fatalf("struct with tag test failed: %v", err) - } - - struct2 := testStructTag{} - err = Unmarshal(table, &struct2) - if err != nil { - t.Fatalf("unmarshal struct with tag failed: %v", err) - } - - if !reflect.DeepEqual(test, struct2) { - t.Errorf("expected %+v, got %+v", test, struct2) - } - }) - - t.Run("Support Map, Slice and Any", func(t *testing.T) { - L := lua.NewState() - defer L.Close() - table, err := Marshal(L, m) - if err != nil { - t.Fatalf("marshal map failed: %v", err) - } - L.SetGlobal("m", table) - if err := L.DoString(` - assert(m.key1 == "value1") - assert(m.key2 == 2) - assert(m.key3 == true) - print("lua Map done") - `); err != nil { - t.Errorf("map test failed: %v", err) - } - - slice, err := Marshal(L, s) - if err != nil { - t.Fatalf("marshal slice failed: %v", err) - } - - L.SetGlobal("s", slice) - if err := L.DoString(` - assert(s[1] == "value1") - assert(s[2] == 2) - assert(s[3] == true) - print("lua Slice done") - `); err != nil { - t.Errorf("slice test failed: %v", err) - } - - // Unmarshal - - // Test case for map - mUnmarshaled := map[string]any{} - - fmt.Println("==== start unmarshal ====") - - err = Unmarshal(table, &mUnmarshaled) - if err != nil { - t.Fatalf("unmarshal map failed: %v", err) - } - - fmt.Printf("mUnmarshaled: %+v\n", mUnmarshaled) - - // unmarshal a LTNumber to any will be converted to float64 - if !reflect.DeepEqual(mFloat64, mUnmarshaled) { - t.Errorf("expected %+v, got %+v", mFloat64, mUnmarshaled) - } - - // Test case for slice - sUnmarshaled := []any{} - - err = Unmarshal(slice, &sUnmarshaled) - if err != nil { - t.Fatalf("unmarshal slice failed: %v", err) - } - - fmt.Printf("sUnmarshaled: %+v\n", sUnmarshaled) - - if !reflect.DeepEqual(sFloat64, sUnmarshaled) { - t.Errorf("expected %+v, got %+v", sFloat64, sUnmarshaled) - } - - var sUnmarshalAny any - err = Unmarshal(slice, &sUnmarshalAny) - if err != nil { - t.Fatalf("unmarshal slice failed: %v", err) - } - - if !reflect.DeepEqual(sFloat64, sUnmarshalAny) { - t.Errorf("expected %+v, got %+v", sFloat64, sUnmarshalAny) - } - }) - - t.Run("MapSliceStructUnified", func(t *testing.T) { - L := lua.NewState() - defer L.Close() - - input := complexStruct{ - Field1: "value1", - Field2: 123, - Field3: true, - Struct: testStructTag{ - Field1: "value1", - Field2: 2, - Field3: true, - }, - Map: m, - Slice: s, - } - - table, err := Marshal(L, input) - if err != nil { - t.Fatalf("marshal map failed: %v", err) - } - - L.SetGlobal("m", table) - - if err := L.DoString(` - assert(m.Field1 == "value1") - assert(m.Field2 == 123) - assert(m.Field3 == true) - assert(m.Struct.field1 == "value1") - assert(m.Struct.field2 == 2) - assert(m.Struct.field3 == true) - assert(m.Map.key1 == "value1") - assert(m.Map.key2 == 2) - assert(m.Map.key3 == true) - assert(m.Slice[1] == "value1") - assert(m.Slice[2] == 2) - assert(m.Slice[3] == true) - print("lua MapSliceStructUnified done") - `); err != nil { - t.Errorf("map test failed: %v", err) - } - - // Unmarshal - output := complexStruct{} - err = Unmarshal(table, &output) - if err != nil { - t.Fatalf("unmarshal map failed: %v", err) - } - - fmt.Printf("output: %+v\n", output) - - expected := complexStruct{ - Field1: "value1", - Field2: 123, - Field3: true, - Struct: testStructTag{ - Field1: "value1", - Field2: 2, - Field3: true, - }, - Map: mFloat64, - Slice: sFloat64, - } - - if !reflect.DeepEqual(expected, output) { - t.Errorf("expected %+v, got %+v", expected, output) - } - }) - - t.Run("TableWithEmptyField", func(t *testing.T) { - L := lua.NewState() + t.Run("TableWithEmptyFieldAndIncompitibleType", func(t *testing.T) { + L := NewLuaVM() defer L.Close() output := struct { - Field1 string `luai:"field1"` - Field2 *string `luai:"field2"` + Field1 string `luai:"field1"` + Field2 *string `luai:"field2"` + AString string `luai:"a_string"` }{} - if err := L.DoString(` + if err := L.Instance.DoString(` return { field1 = "value1", + --- notice: here we return a number + a_string = 8, } `); err != nil { t.Errorf("map test failed: %v", err) } - table := L.ToTable(-1) // returned value - L.Pop(1) - // Unmarshal + table := L.ReturnedValue() err := Unmarshal(table, &output) if err != nil { t.Fatalf("unmarshal map failed: %v", err) @@ -324,6 +91,9 @@ func TestExample(t *testing.T) { if output.Field2 != nil { t.Errorf("expected %+v, got %+v", nil, output.Field2) } + if output.AString != "8" { + t.Errorf("expected %+v, got %+v", "", output.AString) + } }) } @@ -345,26 +115,67 @@ func TestCases(t *testing.T) { s := []any{"value1", 2, true} sFloat64 := []any{"value1", float64(2), true} + normalStruct := testStruct{ + Field1: "test", + Field2: 1, + Field3: true, + } + normalStructWithTag := testStructTag{ + Field1: "test", + Field2: 1, + Field3: true, + } + var unmarshalTests = []struct { CaseName string in any - ptr any // new(type) + ptr any out any luaValidationScript string err error }{ { - CaseName: "UnmarshalAnyMap", + CaseName: "Struct", + in: normalStruct, + ptr: new(testStruct), + out: &normalStruct, + luaValidationScript: ` + assert(table.Field1 == "test") + assert(table.Field2 == 1) + assert(table.Field3 == true) + print("lua Struct done") + `, + }, + { + CaseName: "Struct with Tag", + in: normalStructWithTag, + ptr: &testStructTag{}, + out: &normalStructWithTag, + luaValidationScript: ` + assert(table.field1 == "test") + assert(table.field2 == 1) + assert(table.field3 == true) + print("lua Struct with Tag done") + `, + }, + { + CaseName: "Map", in: m, ptr: &map[string]any{}, out: &mFloat64, }, { - CaseName: "UnmarshalAnySlice", + CaseName: "Slice", in: s, ptr: &[]any{}, out: &sFloat64, }, + { + CaseName: "Any", + in: m, + ptr: new(any), + out: &mFloat64, + }, { CaseName: "Map[Int]", in: map[int]int{ @@ -382,6 +193,50 @@ func TestCases(t *testing.T) { print("lua Map[Int] done") `, }, + { + CaseName: "MapSliceStructUnified", + in: complexStruct{ + Field1: "value1", + Field2: 123, + Field3: true, + Struct: testStructTag{ + Field1: "value1", + Field2: 2, + Field3: true, + }, + Map: m, + Slice: s, + }, + ptr: &complexStruct{}, + out: &complexStruct{ + Field1: "value1", + Field2: 123, + Field3: true, + Struct: testStructTag{ + Field1: "value1", + Field2: 2, + Field3: true, + }, + Map: mFloat64, + Slice: sFloat64, + }, + luaValidationScript: ` + + assert(m.Field1 == "value1") + assert(m.Field2 == 123) + assert(m.Field3 == true) + assert(m.Struct.field1 == "value1") + assert(m.Struct.field2 == 2) + assert(m.Struct.field3 == true) + assert(m.Map.key1 == "value1") + assert(m.Map.key2 == 2) + assert(m.Map.key3 == true) + assert(m.Slice[1] == "value1") + assert(m.Slice[2] == 2) + assert(m.Slice[3] == true) + print("lua MapSliceStructUnified done") + `, + }, } for _, tt := range unmarshalTests { diff --git a/internal/plugin_test.go b/internal/plugin_test.go index 417f6634..103028a9 100644 --- a/internal/plugin_test.go +++ b/internal/plugin_test.go @@ -225,26 +225,3 @@ func TestPlugin(t *testing.T) { } }) } - -//go:embed testdata/plugins/java/buggy.lua -var buggyPluginContent string -var buggyPluginPath = "testdata/plugins/java/buggy.lua" - -func TestInvalidPlugin(t *testing.T) { - teardownSuite := setupSuite(t) - defer teardownSuite(t) - - t.Run("Version field is type int", func(t *testing.T) { - manager := NewSdkManager() - plugin, _ := NewLuaPlugin(buggyPluginContent, buggyPluginPath, manager) - - pkgs, _ := plugin.Available() - - pkg0 := pkgs[0] - addition0 := pkg0.Additions[0] - - if addition0.Version != "8" { - t.Errorf("expected version '8', got '%s'", addition0.Version) - } - }) -} diff --git a/internal/testdata/plugins/java/buggy.lua b/internal/testdata/plugins/java/buggy.lua deleted file mode 100644 index cbada7f6..00000000 --- a/internal/testdata/plugins/java/buggy.lua +++ /dev/null @@ -1,169 +0,0 @@ ---- Common libraries provided by VersionFox (optional) -local http = require("http") -local json = require("json") -local html = require("html") - ---- The following two parameters are injected by VersionFox at runtime ---- Operating system type at runtime (Windows, Linux, Darwin) -OS_TYPE = "" ---- Operating system architecture at runtime (amd64, arm64, etc.) -ARCH_TYPE = "" - -PLUGIN = { - --- Plugin name - name = "java", - --- Plugin author - author = "Lihan", - --- Plugin version - version = "0.0.1", - --- Plugin description - description = "xxx", - -- Update URL - updateUrl = "{URL}/sdk.lua", - -- minimum compatible vfox version - minRuntimeVersion = "0.2.2", -} - ---- Returns some pre-installed information, such as version number, download address, local files, etc. ---- If checksum is provided, vfox will automatically check it for you. ---- @param ctx table ---- @field ctx.version string User-input version ---- @return table Version information -function PLUGIN:PreInstall(ctx) - local version = ctx.version - local runtimeVersion = ctx.runtimeVersion - return { - --- Version number - version = "version", - --- remote URL or local file path [optional] - url = "xxx", - --- SHA256 checksum [optional] - sha256 = "xxx", - --- md5 checksum [optional] - md5 = "xxx", - --- sha1 checksum [optional] - sha1 = "xxx", - --- sha512 checksum [optional] - sha512 = "xx", - --- additional need files [optional] - addition = { - { - --- additional file name ! - name = "xxx", - --- remote URL or local file path [optional] - url = "xxx", - --- SHA256 checksum [optional] - sha256 = "xxx", - --- md5 checksum [optional] - md5 = "xxx", - --- sha1 checksum [optional] - sha1 = "xxx", - --- sha512 checksum [optional] - sha512 = "xx", - } - } - } -end - ---- Extension point, called after PreInstall, can perform additional operations, ---- such as file operations for the SDK installation directory or compile source code ---- Currently can be left unimplemented! -function PLUGIN:PostInstall(ctx) - --- ctx.rootPath SDK installation directory - local rootPath = ctx.rootPath - local runtimeVersion = ctx.runtimeVersion - local sdkInfo = ctx.sdkInfo['sdk-name'] - local path = sdkInfo.path - local version = sdkInfo.version - local name = sdkInfo.name -end - ---- Return all available versions provided by this plugin ---- @param ctx table Empty table used as context, for future extension ---- @return table Descriptions of available versions and accompanying tool descriptions -function PLUGIN:Available(ctx) - local runtimeVersion = ctx.runtimeVersion - return { - { - version = "xxxx", - note = "LTS", - addition = { - { - name = "npm", - version = 8, - } - } - } - } -end - ---- Each SDK may have different environment variable configurations. ---- This allows plugins to define custom environment variables (including PATH settings) ---- Note: Be sure to distinguish between environment variable settings for different platforms! ---- @param ctx table Context information ---- @field ctx.path string SDK installation directory -function PLUGIN:EnvKeys(ctx) - --- this variable is same as ctx.sdkInfo['plugin-name'].path - local mainPath = ctx.path - local runtimeVersion = ctx.runtimeVersion - local sdkInfo = ctx.sdkInfo['sdk-name'] - local path = sdkInfo.path - local version = sdkInfo.version - local name = sdkInfo.name - return { - { - key = "JAVA_HOME", - value = mainPath - }, - { - key = "PATH", - value = mainPath .. "/bin" - }, - { - key = "PATH", - value = mainPath .. "/bin2" - } - } -end - ---- When user invoke `use` command, this function will be called to get the ---- valid version information. ---- @param ctx table Context information -function PLUGIN:PreUse(ctx) - local runtimeVersion = ctx.runtimeVersion - --- user input version - local version = ctx.version - --- installed sdks - local sdkInfo = ctx.installedSdks['xxxx'] - local path = sdkInfo.path - local name = sdkInfo.name - local sdkVersion = sdkInfo.version - - --- working directory - local cwd = ctx.cwd - - printTable(ctx) - - --- user input scope - local scope = ctx.scope - - if (scope == "global") then - print("return 9.9.9") - return { - version = "9.9.9", - } - end - - if (scope == "project") then - print("return 10.0.0") - return { - version = "10.0.0", - } - end - - print("return 1.0.0") - - return { - version = "1.0.0" - } -end From bb0f8df7f0ce74c010b4f29acef7d043819df211 Mon Sep 17 00:00:00 2001 From: Han Li Date: Mon, 25 Mar 2024 11:32:33 +0800 Subject: [PATCH 6/8] Update internal/luai/example_test.go --- internal/luai/example_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/luai/example_test.go b/internal/luai/example_test.go index 43ae79f4..89a2d620 100644 --- a/internal/luai/example_test.go +++ b/internal/luai/example_test.go @@ -59,7 +59,7 @@ func TestExample(t *testing.T) { teardownSuite := setupSuite(t) defer teardownSuite(t) - t.Run("TableWithEmptyFieldAndIncompitibleType", func(t *testing.T) { + t.Run("TableWithEmptyFieldAndIncompatibleType", func(t *testing.T) { L := NewLuaVM() defer L.Close() From bfae6de09304d14f68f37e722a448868153d7d89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=87=8E=E5=A3=B0?= Date: Mon, 25 Mar 2024 11:37:49 +0800 Subject: [PATCH 7/8] test: fix testcases --- internal/luai/example_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/internal/luai/example_test.go b/internal/luai/example_test.go index 89a2d620..371f2da4 100644 --- a/internal/luai/example_test.go +++ b/internal/luai/example_test.go @@ -240,9 +240,6 @@ func TestCases(t *testing.T) { } for _, tt := range unmarshalTests { - if tt.CaseName != "Map[Int]" { - continue - } t.Run(tt.CaseName, func(t *testing.T) { L := lua.NewState() defer L.Close() From a47540e64b736dec5282ac6ece3b2428b08025de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=87=8E=E5=A3=B0?= Date: Mon, 25 Mar 2024 13:18:48 +0800 Subject: [PATCH 8/8] test: fix testcases --- internal/luai/decode.go | 5 +- internal/luai/example_test.go | 171 ++++++++++++++++++++++------------ 2 files changed, 113 insertions(+), 63 deletions(-) diff --git a/internal/luai/decode.go b/internal/luai/decode.go index 5890d465..5f61ea08 100644 --- a/internal/luai/decode.go +++ b/internal/luai/decode.go @@ -148,11 +148,10 @@ func arrayInterface(lvalue *lua.LTable) any { } func unmarshalWorker(value lua.LValue, reflected reflect.Value) error { + reflected = indirect(reflected) switch value.Type() { case lua.LTTable: - reflected = indirect(reflected) - tagMap := make(map[string]int) switch reflected.Kind() { case reflect.Interface: @@ -253,6 +252,8 @@ func unmarshalWorker(value lua.LValue, reflected reflect.Value) error { reflected.Set(reflect.MakeSlice(reflected.Type(), 0, 0)) } case reflect.Struct: + tagMap := make(map[string]int) + for i := 0; i < reflected.NumField(); i++ { fieldTypeField := reflected.Type().Field(i) tag := fieldTypeField.Tag.Get("luai") diff --git a/internal/luai/example_test.go b/internal/luai/example_test.go index 371f2da4..2ecb4b97 100644 --- a/internal/luai/example_test.go +++ b/internal/luai/example_test.go @@ -101,80 +101,93 @@ func TestCases(t *testing.T) { teardownSuite := setupSuite(t) defer teardownSuite(t) - m := map[string]interface{}{ - "key1": "value1", - "key2": 2, - "key3": true, - } - mFloat64 := map[string]interface{}{ - "key1": "value1", - "key2": float64(2), - "key3": true, - } - - s := []any{"value1", 2, true} - sFloat64 := []any{"value1", float64(2), true} - - normalStruct := testStruct{ - Field1: "test", - Field2: 1, - Field3: true, - } - normalStructWithTag := testStructTag{ - Field1: "test", - Field2: 1, - Field3: true, - } - var unmarshalTests = []struct { CaseName string in any - ptr any + ptr any // new(type) out any luaValidationScript string err error }{ { CaseName: "Struct", - in: normalStruct, - ptr: new(testStruct), - out: &normalStruct, + in: testStruct{ + Field1: "test", + Field2: 1, + Field3: true, + }, + ptr: new(testStruct), + out: testStruct{ + Field1: "test", + Field2: 1, + Field3: true, + }, luaValidationScript: ` - assert(table.Field1 == "test") - assert(table.Field2 == 1) - assert(table.Field3 == true) + assert(m.Field1 == "test") + assert(m.Field2 == 1) + assert(m.Field3 == true) print("lua Struct done") `, }, { CaseName: "Struct with Tag", - in: normalStructWithTag, - ptr: &testStructTag{}, - out: &normalStructWithTag, + in: testStructTag{ + Field1: "test", + Field2: 1, + Field3: true, + }, + ptr: new(testStructTag), + out: testStructTag{ + Field1: "test", + Field2: 1, + Field3: true, + }, luaValidationScript: ` - assert(table.field1 == "test") - assert(table.field2 == 1) - assert(table.field3 == true) + assert(m.field1 == "test") + assert(m.field2 == 1) + assert(m.field3 == true) print("lua Struct with Tag done") `, }, { CaseName: "Map", - in: m, - ptr: &map[string]any{}, - out: &mFloat64, + in: map[string]interface{}{ + "key1": "value1", + "key2": 2, + "key3": true, + }, + ptr: new(map[string]any), + out: map[string]interface{}{ + "key1": "value1", + "key2": float64(2), + "key3": true, + }, }, { CaseName: "Slice", - in: s, - ptr: &[]any{}, - out: &sFloat64, + in: []any{"value1", 2, true}, + ptr: new([]any), + out: []any{"value1", float64(2), true}, }, { CaseName: "Any", - in: m, - ptr: new(any), - out: &mFloat64, + in: map[string]interface{}{ + "key1": "value1", + "key2": 2, + "key3": true, + }, + ptr: new(any), + out: map[string]interface{}{ + "key1": "value1", + "key2": float64(2), + "key3": true, + }, + luaValidationScript: ` + assert(m.key1 == "value1") + assert(m.key2 == 2) + assert(m.key3 == true) + print("Any Done") + `, }, { CaseName: "Map[Int]", @@ -182,8 +195,8 @@ func TestCases(t *testing.T) { 1: 1, 2: 2, }, - ptr: &map[int]int{}, - out: &map[int]int{ + ptr: new(map[int]int), + out: map[int]int{ 1: 1, 2: 2, }, @@ -204,11 +217,15 @@ func TestCases(t *testing.T) { Field2: 2, Field3: true, }, - Map: m, - Slice: s, + Map: map[string]interface{}{ + "key1": "value1", + "key2": float64(2), + "key3": true, + }, + Slice: []any{"value1", 2, true}, }, - ptr: &complexStruct{}, - out: &complexStruct{ + ptr: new(complexStruct), + out: complexStruct{ Field1: "value1", Field2: 123, Field3: true, @@ -217,11 +234,14 @@ func TestCases(t *testing.T) { Field2: 2, Field3: true, }, - Map: mFloat64, - Slice: sFloat64, + Map: map[string]interface{}{ + "key1": "value1", + "key2": float64(2), + "key3": true, + }, + Slice: []any{"value1", float64(2), true}, }, luaValidationScript: ` - assert(m.Field1 == "value1") assert(m.Field2 == 123) assert(m.Field3 == true) @@ -249,20 +269,49 @@ func TestCases(t *testing.T) { t.Fatalf("marshal map failed: %v", err) } - L.SetGlobal("m", table) - if tt.luaValidationScript != "" { + L.SetGlobal("m", table) + if err := L.DoString(tt.luaValidationScript); err != nil { t.Errorf("validate %s error: %v", tt.CaseName, err) } } - err = Unmarshal(table, tt.ptr) + if tt.ptr == nil { + return + } + + typ := reflect.TypeOf(tt.ptr) + if typ.Kind() != reflect.Pointer { + t.Fatalf("%s: unmarshalTest.ptr %T is not a pointer type", tt.CaseName, tt.ptr) + } + + typ = typ.Elem() + + // equals to: v = new(right-type) + v := reflect.New(typ) + + if !reflect.DeepEqual(tt.ptr, v.Interface()) { + // There's no reason for ptr to point to non-zero data, + // as we decode into new(right-type), so the data is + // discarded. + // This can easily mean tests that silently don't test + // what they should. To test decoding into existing + // data, see TestPrefilled. + t.Fatalf("%s: unmarshalTest.ptr %#v is not a pointer to a zero value", tt.CaseName, tt.ptr) + } + + err = Unmarshal(table, v.Interface()) + if err != tt.err { t.Errorf("expected %+v, got %+v", tt.err, err) } - if !reflect.DeepEqual(tt.out, tt.ptr) { - t.Errorf("expected %+v, got %+v", tt.out, tt.ptr) + + // get the value out of the pointer, equals to: v = *v + got := v.Elem().Interface() + + if !reflect.DeepEqual(tt.out, got) { + t.Errorf("expected %+v, got %+v", tt.out, got) } }) }