From 5cc6d813ab7a6952fbbc47e1aacfc78663f7331b Mon Sep 17 00:00:00 2001 From: yingjinhui Date: Mon, 14 Nov 2022 17:30:05 +0800 Subject: [PATCH] fix error when convert empty lua table Signed-off-by: yingjinhui --- .../luavm/lua_convert.go | 54 +++++++++ .../luavm/lua_convert_test.go | 111 ++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 pkg/resourceinterpreter/configurableinterpreter/luavm/lua_convert_test.go diff --git a/pkg/resourceinterpreter/configurableinterpreter/luavm/lua_convert.go b/pkg/resourceinterpreter/configurableinterpreter/luavm/lua_convert.go index f0e97d986..67e1ffd1e 100644 --- a/pkg/resourceinterpreter/configurableinterpreter/luavm/lua_convert.go +++ b/pkg/resourceinterpreter/configurableinterpreter/luavm/lua_convert.go @@ -16,6 +16,60 @@ func ConvertLuaResultInto(luaResult lua.LValue, obj interface{}) error { if err != nil { return fmt.Errorf("obj is not pointer") } + + // For example, `GetReplicas` returns requirement with empty: + // { + // nodeClaim: {}, + // resourceRequest: { + // cpu: "100m" + // } + // } + // Luajson encodes it to + // {"nodeClaim": [], "resourceRequest": {"cpu": "100m"}} + // + // While go json fails to unmarshal `[]` to ReplicaRequirements.NodeClaim object. + // ReplicaRequirements object. + // + // Here we handle it as follows: + // 1. Walk the object (lua table), delete the key with empty value (`nodeClaim` in this example): + // { + // resourceRequest: { + // cpu: "100m" + // } + // } + // 2. Encode the object with luajson to be: + // {"resourceRequest": {"cpu": "100m"}} + // 4. Finally, unmarshal the new json to object, get + // { + // resourceRequest: { + // cpu: "100m" + // } + // } + isEmptyDic := func(v *lua.LTable) bool { + count := 0 + v.ForEach(func(lua.LValue, lua.LValue) { + count++ + }) + return count == 0 + } + + var walkValue func(v lua.LValue) + walkValue = func(v lua.LValue) { + if t, ok := v.(*lua.LTable); ok { + t.ForEach(func(key lua.LValue, value lua.LValue) { + if tt, ok := value.(*lua.LTable); ok { + if isEmptyDic(tt) { + // set nil to delete key + t.RawSetH(key, lua.LNil) + } else { + walkValue(value) + } + } + }) + } + } + walkValue(luaResult) + jsonBytes, err := luajson.Encode(luaResult) if err != nil { return fmt.Errorf("json Encode obj eroor %v", err) diff --git a/pkg/resourceinterpreter/configurableinterpreter/luavm/lua_convert_test.go b/pkg/resourceinterpreter/configurableinterpreter/luavm/lua_convert_test.go new file mode 100644 index 000000000..396d371a6 --- /dev/null +++ b/pkg/resourceinterpreter/configurableinterpreter/luavm/lua_convert_test.go @@ -0,0 +1,111 @@ +package luavm + +import ( + "reflect" + "testing" + + lua "github.com/yuin/gopher-lua" +) + +func TestConvertLuaResultInto(t *testing.T) { + type barStruct struct { + Bar string + } + type fooStruct struct { + Bar barStruct + } + + type args struct { + luaResult lua.LValue + obj interface{} + } + tests := []struct { + name string + args args + wantErr bool + want interface{} + }{ + { + name: "not a pointer", + args: args{ + luaResult: &lua.LTable{}, + obj: struct{}{}, + }, + wantErr: true, + want: struct{}{}, + }, + { + name: "empty table into struct", + args: args{ + luaResult: &lua.LTable{}, + obj: &fooStruct{}, + }, + wantErr: false, + want: &fooStruct{}, + }, + { + name: "empty table into slice", + args: args{ + luaResult: &lua.LTable{}, + obj: &[]string{}, + }, + wantErr: false, + want: &[]string{}, + }, + { + name: "non-empty table into slice", + args: args{ + luaResult: func() lua.LValue { + v := &lua.LTable{} + v.Append(lua.LString("foo")) + v.Append(lua.LString("bar")) + return v + }(), + obj: &[]string{}, + }, + wantErr: false, + want: &[]string{"foo", "bar"}, + }, + { + name: "table with empty table into slice", + args: args{ + luaResult: func() lua.LValue { + v := &lua.LTable{} + v.RawSetString("Bar", &lua.LTable{}) + return v + }(), + obj: &fooStruct{}, + }, + wantErr: false, + want: &fooStruct{}, + }, + { + name: "struct is not empty, and convert successfully", + args: args{ + luaResult: func() lua.LValue { + bar := &lua.LTable{} + bar.RawSetString("Bar", lua.LString("bar")) + + v := &lua.LTable{} + v.RawSetString("Bar", bar) + return v + }(), + obj: &fooStruct{}, + }, + wantErr: false, + want: &fooStruct{ + Bar: barStruct{Bar: "bar"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := ConvertLuaResultInto(tt.args.luaResult, tt.args.obj); (err != nil) != tt.wantErr { + t.Errorf("ConvertLuaResultInto() error = %v, wantErr %v", err, tt.wantErr) + } + if got := tt.args.obj; !reflect.DeepEqual(tt.want, got) { + t.Errorf("ConvertLuaResultInto() got = %v, want %v", got, tt.want) + } + }) + } +}