diff --git a/go/protomodule/protomodule_message_test.go b/go/protomodule/protomodule_message_test.go index bf8d4b1..f06f98c 100644 --- a/go/protomodule/protomodule_message_test.go +++ b/go/protomodule/protomodule_message_test.go @@ -288,237 +288,201 @@ func TestMessageV3(t *testing.T) { } func TestAttrValidation(t *testing.T) { - tests := []struct { - name string - src string - wantErr string - }{ + globals := starlark.StringDict{ + "pb": NewProtoPackage(newRegistry(), "skycfg.test_proto"), + } + runSkycfgTests(t, []skycfgTest{ // Scalar type mismatch { name: "int32", - src: `MessageV3(f_int32 = '')`, - wantErr: `TypeError: value "" (type "string") can't be assigned to type "int32".`, + src: `pb.MessageV3(f_int32 = '')`, + wantErr: fmt.Errorf(`TypeError: value "" (type "string") can't be assigned to type "int32".`), }, { name: "int64", - src: `MessageV3(f_int64 = '')`, - wantErr: `TypeError: value "" (type "string") can't be assigned to type "int64".`, + src: `pb.MessageV3(f_int64 = '')`, + wantErr: fmt.Errorf(`TypeError: value "" (type "string") can't be assigned to type "int64".`), }, { name: "uint32", - src: `MessageV3(f_uint32 = '')`, - wantErr: `TypeError: value "" (type "string") can't be assigned to type "uint32".`, + src: `pb.MessageV3(f_uint32 = '')`, + wantErr: fmt.Errorf(`TypeError: value "" (type "string") can't be assigned to type "uint32".`), }, { name: "uint64", - src: `MessageV3(f_uint64 = '')`, - wantErr: `TypeError: value "" (type "string") can't be assigned to type "uint64".`, + src: `pb.MessageV3(f_uint64 = '')`, + wantErr: fmt.Errorf(`TypeError: value "" (type "string") can't be assigned to type "uint64".`), }, { name: "float32", - src: `MessageV3(f_float32 = '')`, - wantErr: `TypeError: value "" (type "string") can't be assigned to type "float".`, + src: `pb.MessageV3(f_float32 = '')`, + wantErr: fmt.Errorf(`TypeError: value "" (type "string") can't be assigned to type "float".`), }, { name: "float64", - src: `MessageV3(f_float64 = '')`, - wantErr: `TypeError: value "" (type "string") can't be assigned to type "double".`, + src: `pb.MessageV3(f_float64 = '')`, + wantErr: fmt.Errorf(`TypeError: value "" (type "string") can't be assigned to type "double".`), }, { name: "string", - src: `MessageV3(f_string = 0)`, - wantErr: `TypeError: value 0 (type "int") can't be assigned to type "string".`, + src: `pb.MessageV3(f_string = 0)`, + wantErr: fmt.Errorf(`TypeError: value 0 (type "int") can't be assigned to type "string".`), }, { name: "bool", - src: `MessageV3(f_bool = '')`, - wantErr: `TypeError: value "" (type "string") can't be assigned to type "bool".`, + src: `pb.MessageV3(f_bool = '')`, + wantErr: fmt.Errorf(`TypeError: value "" (type "string") can't be assigned to type "bool".`), }, { name: "enum", - src: `MessageV3(f_toplevel_enum = 0)`, - wantErr: `TypeError: value 0 (type "int") can't be assigned to type "skycfg.test_proto.ToplevelEnumV3".`, + src: `pb.MessageV3(f_toplevel_enum = 0)`, + wantErr: fmt.Errorf(`TypeError: value 0 (type "int") can't be assigned to type "skycfg.test_proto.ToplevelEnumV3".`), }, // Non-scalar type mismatch { name: "string list assignment", - src: `MessageV3(r_string = {'': ''})`, - wantErr: `TypeError: value {"": ""} (type "dict") can't be assigned to type "[]string".`, + src: `pb.MessageV3(r_string = {'': ''})`, + wantErr: fmt.Errorf(`TypeError: value {"": ""} (type "dict") can't be assigned to type "[]string".`), }, { name: "string list field assignment", - src: `MessageV3(r_string = [123])`, - wantErr: `TypeError: value 123 (type "int") can't be assigned to type "string".`, + src: `pb.MessageV3(r_string = [123])`, + wantErr: fmt.Errorf(`TypeError: value 123 (type "int") can't be assigned to type "string".`), }, { name: "string map assignment", - src: `MessageV3(map_string = [123])`, - wantErr: `TypeError: value [123] (type "list") can't be assigned to type "map[string]string".`, + src: `pb.MessageV3(map_string = [123])`, + wantErr: fmt.Errorf(`TypeError: value [123] (type "list") can't be assigned to type "map[string]string".`), }, { name: "string map key assignment", - src: `MessageV3(map_string = {123: ''})`, - wantErr: `TypeError: value 123 (type "int") can't be assigned to type "string".`, + src: `pb.MessageV3(map_string = {123: ''})`, + wantErr: fmt.Errorf(`TypeError: value 123 (type "int") can't be assigned to type "string".`), }, { name: "string map value assignment", - src: `MessageV3(map_string = {'': 456})`, - wantErr: `TypeError: value 456 (type "int") can't be assigned to type "string".`, + src: `pb.MessageV3(map_string = {'': 456})`, + wantErr: fmt.Errorf(`TypeError: value 456 (type "int") can't be assigned to type "string".`), }, { name: "message map value assignment", - src: `MessageV3(map_submsg = {'': 456})`, - wantErr: `TypeError: value 456 (type "int") can't be assigned to type "skycfg.test_proto.MessageV3".`, + src: `pb.MessageV3(map_submsg = {'': 456})`, + wantErr: fmt.Errorf(`TypeError: value 456 (type "int") can't be assigned to type "skycfg.test_proto.MessageV3".`), }, { name: "message assignment with wrong type", - src: `MessageV3(f_submsg = pb.MessageV2())`, - wantErr: `TypeError: value (type "skycfg.test_proto.MessageV2") can't be assigned to type "skycfg.test_proto.MessageV3".`, + src: `pb.MessageV3(f_submsg = pb.MessageV2())`, + wantErr: fmt.Errorf(`TypeError: value (type "skycfg.test_proto.MessageV2") can't be assigned to type "skycfg.test_proto.MessageV3".`), }, // Repeated and map fields can't be assigned `None`. Scalar fields can't be assigned `None` // in proto3, but the error message is specialized. { name: "none to scalar", - src: `MessageV3(f_int32 = None)`, - wantErr: `TypeError: value None (type "NoneType") can't be assigned to type "int32" in proto3 mode.`, + src: `pb.MessageV3(f_int32 = None)`, + wantErr: fmt.Errorf(`TypeError: value None (type "NoneType") can't be assigned to type "int32" in proto3 mode.`), }, { name: "none to string list", - src: `MessageV3(r_string = None)`, - wantErr: `TypeError: value None (type "NoneType") can't be assigned to type "[]string".`, + src: `pb.MessageV3(r_string = None)`, + wantErr: fmt.Errorf(`TypeError: value None (type "NoneType") can't be assigned to type "[]string".`), }, { name: "none to string map", - src: `MessageV3(map_string = None)`, - wantErr: `TypeError: value None (type "NoneType") can't be assigned to type "map[string]string".`, + src: `pb.MessageV3(map_string = None)`, + wantErr: fmt.Errorf(`TypeError: value None (type "NoneType") can't be assigned to type "map[string]string".`), }, { name: "none to message is allowed", - src: `MessageV3(f_submsg = None)`, - wantErr: "", + src: `pb.MessageV3(f_submsg = None)`, + wantErr: nil, + want: &pb.MessageV3{}, }, { name: "none to message list", - src: `MessageV3(r_submsg = None)`, - wantErr: `TypeError: value None (type "NoneType") can't be assigned to type "[]skycfg.test_proto.MessageV3".`, + src: `pb.MessageV3(r_submsg = None)`, + wantErr: fmt.Errorf(`TypeError: value None (type "NoneType") can't be assigned to type "[]skycfg.test_proto.MessageV3".`), }, // Numeric overflow { name: "int32 overflow", - src: fmt.Sprintf(`MessageV3(f_int32 = %d + 1)`, math.MaxInt32), - wantErr: `ValueError: value 2147483648 overflows type "int32".`, + src: fmt.Sprintf(`pb.MessageV3(f_int32 = %d + 1)`, math.MaxInt32), + wantErr: fmt.Errorf(`ValueError: value 2147483648 overflows type "int32".`), }, { name: "int32 underflow", - src: fmt.Sprintf(`MessageV3(f_int32 = %d - 1)`, math.MinInt32), - wantErr: `ValueError: value -2147483649 overflows type "int32".`, + src: fmt.Sprintf(`pb.MessageV3(f_int32 = %d - 1)`, math.MinInt32), + wantErr: fmt.Errorf(`ValueError: value -2147483649 overflows type "int32".`), }, { name: "int64 overflow", - src: fmt.Sprintf(`MessageV3(f_int64 = %d + 1)`, math.MaxInt64), - wantErr: `ValueError: value 9223372036854775808 overflows type "int64".`, + src: fmt.Sprintf(`pb.MessageV3(f_int64 = %d + 1)`, math.MaxInt64), + wantErr: fmt.Errorf(`ValueError: value 9223372036854775808 overflows type "int64".`), }, { name: "int64 underflow", - src: fmt.Sprintf(`MessageV3(f_int64 = %d - 1)`, math.MinInt64), - wantErr: `ValueError: value -9223372036854775809 overflows type "int64".`, + src: fmt.Sprintf(`pb.MessageV3(f_int64 = %d - 1)`, math.MinInt64), + wantErr: fmt.Errorf(`ValueError: value -9223372036854775809 overflows type "int64".`), }, { name: "uint32 overflow", - src: fmt.Sprintf(`MessageV3(f_uint32 = %d + 1)`, math.MaxUint32), - wantErr: `ValueError: value 4294967296 overflows type "uint32".`, + src: fmt.Sprintf(`pb.MessageV3(f_uint32 = %d + 1)`, math.MaxUint32), + wantErr: fmt.Errorf(`ValueError: value 4294967296 overflows type "uint32".`), }, { name: "uint32 underflow", - src: fmt.Sprintf(`MessageV3(f_uint32 = %d - 1)`, 0), - wantErr: `ValueError: value -1 overflows type "uint32".`, + src: fmt.Sprintf(`pb.MessageV3(f_uint32 = %d - 1)`, 0), + wantErr: fmt.Errorf(`ValueError: value -1 overflows type "uint32".`), }, { name: "uint64 overflow", - src: fmt.Sprintf(`MessageV3(f_uint64 = %d + 1)`, uint64(math.MaxUint64)), - wantErr: `ValueError: value 18446744073709551616 overflows type "uint64".`, + src: fmt.Sprintf(`pb.MessageV3(f_uint64 = %d + 1)`, uint64(math.MaxUint64)), + wantErr: fmt.Errorf(`ValueError: value 18446744073709551616 overflows type "uint64".`), }, { name: "uint64 underflow", - src: fmt.Sprintf(`MessageV3(f_uint64 = %d - 1)`, 0), - wantErr: `ValueError: value -1 overflows type "uint64".`, + src: fmt.Sprintf(`pb.MessageV3(f_uint64 = %d - 1)`, 0), + wantErr: fmt.Errorf(`ValueError: value -1 overflows type "uint64".`), }, - } - - globals := starlark.StringDict{ - "pb": NewProtoPackage(newRegistry(), "skycfg.test_proto"), - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - _, err := starlark.Eval(&starlark.Thread{}, "", `pb.`+test.src, globals) - if test.wantErr != "" { - if err == nil { - t.Errorf("eval(%q): expected error", test.src) - } else if test.wantErr != err.Error() { - t.Errorf("eval(%q): expected error\nexpected: %q\ngot: %q", test.src, test.wantErr, err.Error()) - } - } else { - if err != nil { - t.Fatalf("Expected no error got: %v\n", err) - } - } - }) - } + }, withGlobals(globals)) } func TestProtoMessageString(t *testing.T) { - val, err := eval(`proto.package("skycfg.test_proto").MessageV3( - f_string = "some string", - )`, nil) - if err != nil { - t.Fatal(err) - } - got := val.String() - want := `` - if want != got { - t.Fatalf("protoMessage.String(): wanted %q, got %q", want, got) - } + runSkycfgTests(t, []skycfgTest{ + { + src: `proto.package("skycfg.test_proto").MessageV3( + f_string = "some string", + )`, + want: ``, + }, + }) } func TestNestedMessages(t *testing.T) { testPb := `proto.package("skycfg.test_proto").` - tests := []struct { - src string - wantVal string - }{ + runSkycfgTests(t, []skycfgTest{ { - src: testPb + `MessageV2.NestedMessage()`, - wantVal: ``, + src: testPb + `MessageV2.NestedMessage()`, + want: ``, }, { - src: testPb + `MessageV2.NestedMessage.DoubleNestedMessage()`, - wantVal: ``, + src: testPb + `MessageV2.NestedMessage.DoubleNestedMessage()`, + want: ``, }, { - src: testPb + `MessageV3.NestedMessage()`, - wantVal: ``, + src: testPb + `MessageV3.NestedMessage()`, + want: ``, }, { - src: testPb + `MessageV3.NestedMessage.DoubleNestedMessage()`, - wantVal: ``, + src: testPb + `MessageV3.NestedMessage.DoubleNestedMessage()`, + want: ``, }, - } - for _, test := range tests { - gotVal, err := eval(test.src, nil) - if err != nil { - t.Fatal(err) - } - if test.wantVal != gotVal.String() { - t.Errorf("eval(%q): expected value %q, got %q", test.src, test.wantVal, gotVal) - } - } + }) } func TestProtoComparisonEqual(t *testing.T) { @@ -575,10 +539,8 @@ func TestProtoSetDefaultV2(t *testing.T) { setString := "abc" defaultString := "default_str" - tests := []struct { - src string - want *pb.MessageV2 - }{ + runSkycfgTests(t, []skycfgTest{ + // V2 { src: `proto.set_defaults(proto.package("skycfg.test_proto").MessageV2(f_int32 = 123))`, want: &pb.MessageV2{ @@ -604,22 +566,8 @@ func TestProtoSetDefaultV2(t *testing.T) { FString: &defaultString, }, }, - } - for _, test := range tests { - val, err := eval(test.src, nil) - if err != nil { - t.Fatal(err) - } - gotMsg := mustProtoMessage(t, val) - checkProtoEqual(t, test.want, gotMsg) - } -} -func TestProtoSetDefaultV3(t *testing.T) { - tests := []struct { - src string - want *pb.MessageV3 - }{ + // V3 { src: `proto.set_defaults(proto.package("skycfg.test_proto").MessageV3(f_int32 = 123))`, want: &pb.MessageV3{ @@ -643,48 +591,35 @@ func TestProtoSetDefaultV3(t *testing.T) { FSubmsg: &pb.MessageV3{}, }, }, - } - for _, test := range tests { - val, err := eval(test.src, nil) - if err != nil { - t.Fatal(err) - } - gotMsg := mustProtoMessage(t, val) - checkProtoEqual(t, test.want, gotMsg) - } + }) } -func TestProtoClearV2(t *testing.T) { - val, err := eval(`proto.clear(proto.package("skycfg.test_proto").MessageV2( - f_string = "some string", - ))`, nil) - if err != nil { - t.Fatal(err) - } - gotMsg := mustProtoMessage(t, val) - wantMsg := &pb.MessageV2{} - checkProtoEqual(t, wantMsg, gotMsg) -} - -func TestProtoClearV3(t *testing.T) { - val, err := eval(`proto.clear(proto.package("skycfg.test_proto").MessageV3( - f_string = "some string", - ))`, nil) - if err != nil { - t.Fatal(err) - } - gotMsg := mustProtoMessage(t, val) - wantMsg := &pb.MessageV3{ - FInt32: 0, - FInt64: 0, - FUint32: 0, - FUint64: 0, - FFloat32: 0.0, - FFloat64: 0.0, - FString: "", - FBool: false, - } - checkProtoEqual(t, wantMsg, gotMsg) +func TestProtoClear(t *testing.T) { + runSkycfgTests(t, []skycfgTest{ + { + name: "proto.clear V2", + src: `proto.clear(proto.package("skycfg.test_proto").MessageV2( + f_string = "some string", + ))`, + want: &pb.MessageV2{}, + }, + { + name: "proto.clear V3", + src: `proto.clear(proto.package("skycfg.test_proto").MessageV3( + f_string = "some string", + ))`, + want: &pb.MessageV3{ + FInt32: 0, + FInt64: 0, + FUint32: 0, + FUint64: 0, + FFloat32: 0.0, + FFloat64: 0.0, + FString: "", + FBool: false, + }, + }, + }) } func TestProtoMergeV2(t *testing.T) { @@ -970,15 +905,18 @@ func TestProtoMergeDiffTypes(t *testing.T) { // Pre 1.0 Skycfg allowed maps to be constructed with None values for proto2 (see protoMap.SetKey) func TestMapNoneCompatibility(t *testing.T) { - val, err := evalFunc(` + runSkycfgTests(t, []skycfgTest{ + { + name: "Set map with None clears values", + srcFunc: ` def fun(): pb = proto.package("skycfg.test_proto") msg = pb.MessageV2() m = { - "a": pb.MessageV2(), - "b": pb.MessageV2(), - "c": pb.MessageV2(), - "d": None, + "a": pb.MessageV2(), + "b": pb.MessageV2(), + "c": pb.MessageV2(), + "d": None, } msg.map_submsg = m @@ -988,21 +926,19 @@ def fun(): m2.update([("c", None)]) return msg -`, nil) - if err != nil { - t.Fatal(err) - } - got := mustProtoMessage(t, val).(*pb.MessageV2) - - checkProtoEqual(t, &pb.MessageV2{ - MapSubmsg: map[string]*pb.MessageV2{ - "a": &pb.MessageV2{}, +`, + want: &pb.MessageV2{ + MapSubmsg: map[string]*pb.MessageV2{ + "a": &pb.MessageV2{}, + }, + }, }, - }, got) - // Confirm this only works for all in proto2, only message values in proto3 - // This is an artifact of set to None being allow for scalar values in proto2 - val, err = evalFunc(` + // Confirm this only works for all in proto2, only message values in proto3 + // This is an artifact of set to None being allow for scalar values in proto2 + { + name: "Set a scalar value to None in proto2 works", + srcFunc: ` def fun(): pb = proto.package("skycfg.test_proto") msg = pb.MessageV2( @@ -1011,12 +947,14 @@ def fun(): } ) return msg -`, nil) - if err != nil { - t.Fatal(err) - } - - val, err = evalFunc(` +`, + want: &pb.MessageV2{ + MapString: map[string]string{}, + }, + }, + { + name: "Set a scalar value to None in proto3 is not allowed", + srcFunc: ` def fun(): pb = proto.package("skycfg.test_proto") msg = pb.MessageV3( @@ -1025,16 +963,15 @@ def fun(): } ) return msg -`, nil) - wantErr := fmt.Errorf(`TypeError: value None (type "NoneType") can't be assigned to type "string" in proto3 mode.`) - if !checkError(err, wantErr) { - t.Fatalf("eval: expected error %v, got %v", wantErr, err) - } - - // An odd resulting behavior of both ensuring assignment does not copy - // and setting to None deletes is that assignment can mutate a raw starlark dict - // This is not ideal but this test is here to just document the behavior - val, err = evalFunc(` +`, + wantErr: fmt.Errorf(`TypeError: value None (type "NoneType") can't be assigned to type "string" in proto3 mode.`), + }, + // An odd resulting behavior of both ensuring assignment does not copy + // and setting to None deletes is that assignment can mutate a raw starlark dict + // This is not ideal but this test is here to just document the behavior + { + name: "None and no copy on assignment mutates raw starlark dict", + srcFunc: ` def fun(): pb = proto.package("skycfg.test_proto") a = { @@ -1045,27 +982,22 @@ def fun(): map_string = a ) return a -`, nil) - if err != nil { - t.Fatal(err) - } - want := `{"ka": "va"}` - if want != val.String() { - t.Fatalf("Result differed\nwant: %s\ngot : %s", want, val.String()) - } +`, + want: `{"ka": "va"}`, + }, + }) } func TestUnsetProto2Fields(t *testing.T) { // Proto v2 distinguishes between unset and set-to-empty. - msg, err := eval(`proto.package("skycfg.test_proto").MessageV2( - f_string = None, - )`, nil) - if err != nil { - t.Fatal(err) - } - got := mustProtoMessage(t, msg) - want := &pb.MessageV2{ - FString: nil, - } - checkProtoEqual(t, want, got) + runSkycfgTests(t, []skycfgTest{ + { + src: `proto.package("skycfg.test_proto").MessageV2( + f_string = None, + )`, + want: &pb.MessageV2{ + FString: nil, + }, + }, + }) } diff --git a/go/protomodule/protomodule_test.go b/go/protomodule/protomodule_test.go index 7fc58f1..2372244 100644 --- a/go/protomodule/protomodule_test.go +++ b/go/protomodule/protomodule_test.go @@ -60,50 +60,28 @@ func TestProtoPackage(t *testing.T) { }, } - tests := []struct { - expr string - want string - wantErr error - }{ + runSkycfgTests(t, []skycfgTest{ { - expr: `proto.package("skycfg.test_proto")`, + src: `proto.package("skycfg.test_proto")`, want: ``, }, { - expr: `dir(proto.package("skycfg.test_proto"))`, + src: `dir(proto.package("skycfg.test_proto"))`, want: `["MessageV2", "MessageV3", "ToplevelEnumV2", "ToplevelEnumV3"]`, }, { - expr: `proto.package("skycfg.test_proto").MessageV2`, + src: `proto.package("skycfg.test_proto").MessageV2`, want: ``, }, { - expr: `proto.package("skycfg.test_proto").ToplevelEnumV2`, + src: `proto.package("skycfg.test_proto").ToplevelEnumV2`, want: ``, }, { - expr: `proto.package("skycfg.test_proto").NoExist`, + src: `proto.package("skycfg.test_proto").NoExist`, wantErr: errors.New(`Protobuf type "skycfg.test_proto.NoExist" not found`), }, - } - for _, test := range tests { - t.Run("", func(t *testing.T) { - val, err := starlark.Eval(&starlark.Thread{}, "", test.expr, globals) - - if test.wantErr != nil { - if !checkError(err, test.wantErr) { - t.Fatalf("eval(%q): expected error %v, got %v", test.expr, test.wantErr, err) - } - return - } - if err != nil { - t.Fatalf("eval(%q): %v", test.expr, err) - } - if test.want != val.String() { - t.Errorf("eval(%q): expected value %q, got %q", test.expr, test.want, val.String()) - } - }) - } + }, withGlobals(globals)) } func TestMessageType(t *testing.T) { @@ -111,50 +89,28 @@ func TestMessageType(t *testing.T) { "pb": NewProtoPackage(newRegistry(), "skycfg.test_proto"), } - tests := []struct { - expr string - want string - wantErr error - }{ + runSkycfgTests(t, []skycfgTest{ { - expr: `pb.MessageV2`, + src: `pb.MessageV2`, want: ``, }, { - expr: `dir(pb.MessageV2)`, + src: `dir(pb.MessageV2)`, want: `["NestedEnum", "NestedMessage"]`, }, { - expr: `pb.MessageV2.NestedMessage`, + src: `pb.MessageV2.NestedMessage`, want: ``, }, { - expr: `pb.MessageV2.NestedEnum`, + src: `pb.MessageV2.NestedEnum`, want: ``, }, { - expr: `pb.MessageV2.NoExist`, + src: `pb.MessageV2.NoExist`, wantErr: errors.New(`Protobuf type "skycfg.test_proto.MessageV2.NoExist" not found`), }, - } - for _, test := range tests { - t.Run("", func(t *testing.T) { - val, err := starlark.Eval(&starlark.Thread{}, "", test.expr, globals) - - if test.wantErr != nil { - if !checkError(err, test.wantErr) { - t.Fatalf("eval(%q): expected error %v, got %v", test.expr, test.wantErr, err) - } - return - } - if err != nil { - t.Fatalf("eval(%q): %v", test.expr, err) - } - if test.want != val.String() { - t.Errorf("eval(%q): expected value %q, got %q", test.expr, test.want, val.String()) - } - }) - } + }, withGlobals(globals)) } func TestEnumType(t *testing.T) { @@ -162,50 +118,28 @@ func TestEnumType(t *testing.T) { "pb": NewProtoPackage(newRegistry(), "skycfg.test_proto"), } - tests := []struct { - expr string - want string - wantErr error - }{ + runSkycfgTests(t, []skycfgTest{ { - expr: `pb.ToplevelEnumV2`, + src: `pb.ToplevelEnumV2`, want: ``, }, { - expr: `dir(pb.ToplevelEnumV2)`, + src: `dir(pb.ToplevelEnumV2)`, want: `["TOPLEVEL_ENUM_V2_A", "TOPLEVEL_ENUM_V2_B"]`, }, { - expr: `pb.MessageV2.NestedEnum`, + src: `pb.MessageV2.NestedEnum`, want: ``, }, { - expr: `dir(pb.MessageV2.NestedEnum)`, + src: `dir(pb.MessageV2.NestedEnum)`, want: `["NESTED_ENUM_A", "NESTED_ENUM_B"]`, }, { - expr: `pb.ToplevelEnumV2.NoExist`, + src: `pb.ToplevelEnumV2.NoExist`, wantErr: errors.New(`proto.EnumType has no .NoExist field or method`), }, - } - for _, test := range tests { - t.Run("", func(t *testing.T) { - val, err := starlark.Eval(&starlark.Thread{}, "", test.expr, globals) - - if test.wantErr != nil { - if !checkError(err, test.wantErr) { - t.Fatalf("eval(%q): expected error %v, got %v", test.expr, test.wantErr, err) - } - return - } - if err != nil { - t.Fatalf("eval(%q): %v", test.expr, err) - } - if test.want != val.String() { - t.Errorf("eval(%q): expected value %q, got %q", test.expr, test.want, val.String()) - } - }) - } + }, withGlobals(globals)) } func TestListType(t *testing.T) { @@ -224,27 +158,21 @@ func TestListType(t *testing.T) { }), } - tests := []struct { - name string - expr string - exprFun string - want string - wantErr error - }{ + runSkycfgTests(t, []skycfgTest{ { name: "new list", - expr: `list()`, + src: `list()`, want: `[]`, }, { name: "list AttrNames", - expr: `dir(list())`, + src: `dir(list())`, want: `["append", "clear", "extend", "index", "insert", "pop", "remove"]`, }, // List methods { name: "list.Append", - exprFun: ` + srcFunc: ` def fun(): l = list() l.append("some string") @@ -254,7 +182,7 @@ def fun(): }, { name: "list.Extend", - exprFun: ` + srcFunc: ` def fun(): l = list() l.extend(["a", "b"]) @@ -264,7 +192,7 @@ def fun(): }, { name: "list.Clear", - exprFun: ` + srcFunc: ` def fun(): l = list() l.extend(["a", "b"]) @@ -275,7 +203,7 @@ def fun(): }, { name: "list.SetIndex", - exprFun: ` + srcFunc: ` def fun(): l = list() l.extend(["a", "b"]) @@ -286,7 +214,7 @@ def fun(): }, { name: "list binary add operation", - exprFun: ` + srcFunc: ` def fun(): l = list() l2 = list() @@ -301,17 +229,17 @@ def fun(): // List typechecking { name: "list append typchecks", - expr: `list().append(1)`, + src: `list().append(1)`, wantErr: errors.New(`TypeError: value 1 (type "int") can't be assigned to type "string".`), }, { name: "list extend typchecks", - expr: `list().extend([1,2])`, + src: `list().extend([1,2])`, wantErr: errors.New(`TypeError: value 1 (type "int") can't be assigned to type "string".`), }, { name: "list set index typchecks", - exprFun: ` + srcFunc: ` def fun(): l = list() l.extend(["a", "b"]) @@ -320,31 +248,7 @@ def fun(): `, wantErr: errors.New(`TypeError: value 1 (type "int") can't be assigned to type "string".`), }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var val starlark.Value - var err error - if test.expr != "" { - val, err = starlark.Eval(&starlark.Thread{}, "", test.expr, globals) - } else { - val, err = evalFunc(test.exprFun, globals) - } - - if test.wantErr != nil { - if !checkError(err, test.wantErr) { - t.Fatalf("eval(%q): expected error %v, got %v", test.expr, test.wantErr, err) - } - return - } - if err != nil { - t.Fatalf("eval(%q): %v", test.expr, err) - } - if test.want != val.String() { - t.Errorf("eval(%q): expected value %q, got %q", test.expr, test.want, val.String()) - } - }) - } + }, withGlobals(globals)) } func TestMapType(t *testing.T) { @@ -363,27 +267,21 @@ func TestMapType(t *testing.T) { }), } - tests := []struct { - name string - expr string - exprFun string - want string - wantErr error - }{ + runSkycfgTests(t, []skycfgTest{ { name: "new map", - expr: `map()`, + src: `map()`, want: `{}`, }, { name: "map AttrNames", - expr: `dir(map())`, + src: `dir(map())`, want: `["clear", "get", "items", "keys", "pop", "popitem", "setdefault", "update", "values"]`, }, // Map methods { name: "map.SetDefault", - exprFun: ` + srcFunc: ` def fun(): m = map() m["a"] = "A" @@ -395,7 +293,7 @@ def fun(): }, { name: "map.SetKey", - exprFun: ` + srcFunc: ` def fun(): m = map() m["a"] = "some string" @@ -405,7 +303,7 @@ def fun(): }, { name: "map.Update", - exprFun: ` + srcFunc: ` def fun(): m = map() m.update([("a", "a_string"), ("b", "b_string")]) @@ -415,7 +313,7 @@ def fun(): }, { name: "map.Clear", - exprFun: ` + srcFunc: ` def fun(): m = map() m["a"] = "some string" @@ -428,7 +326,7 @@ def fun(): // Map typechecking { name: "map.SetKey typechecks", - exprFun: ` + srcFunc: ` def fun(): m = map() m["a"] = 1 @@ -438,48 +336,26 @@ def fun(): }, { name: "map.Update typechecks", - expr: `map().update([("a", 1)])`, + src: `map().update([("a", 1)])`, wantErr: errors.New(`TypeError: value 1 (type "int") can't be assigned to type "string".`), }, { name: "map.SetDefault typechecks", - expr: `map().setdefault("a", 1)`, + src: `map().setdefault("a", 1)`, wantErr: errors.New(`TypeError: value 1 (type "int") can't be assigned to type "string".`), }, { name: "map.SetDefault typechecks key", - expr: `map().setdefault(1, "a")`, + src: `map().setdefault(1, "a")`, wantErr: errors.New(`TypeError: value 1 (type "int") can't be assigned to type "string".`), }, - } - for _, test := range tests { - t.Run("", func(t *testing.T) { - var val starlark.Value - var err error - if test.expr != "" { - val, err = starlark.Eval(&starlark.Thread{}, "", test.expr, globals) - } else { - val, err = evalFunc(test.exprFun, globals) - } - - if test.wantErr != nil { - if !checkError(err, test.wantErr) { - t.Fatalf("eval(%q): expected error %v, got %v", test.expr, test.wantErr, err) - } - return - } - if err != nil { - t.Fatalf("eval(%q): %v", test.expr, err) - } - if test.want != val.String() { - t.Errorf("eval(%q): expected value %q, got %q", test.expr, test.want, val.String()) - } - }) - } + }, withGlobals(globals)) } func TestRepeatedProtoFieldMutation(t *testing.T) { - val, err := evalFunc(` + runSkycfgTests(t, []skycfgTest{ + { + srcFunc: ` def fun(): pkg = proto.package("skycfg.test_proto") msg = pkg.MessageV3() @@ -487,28 +363,20 @@ def fun(): msg.r_submsg[0].f_string = "foo" msg.r_submsg.extend([pkg.MessageV3()]) msg.r_submsg[1].f_string = "bar" - return msg`, nil) - if err != nil { - t.Fatal(err) - } - got := removeRandomSpace(val.String()) - want := `` - if want != got { - t.Fatalf("skyProtoMessage.String(): wanted %q, got %q", want, got) - } + return msg`, + want: ``, + removeRandomSpace: true, + }, + }) } // Skycfg has had inconsistent copy on assignment behavior // Test that Skycfg does not copy lists/maps on assignment, matching Starlark/Python's behavior func TestNoCopyOnAssignment(t *testing.T) { - tests := []struct { - name string - fun string - want string - }{ + runSkycfgTests(t, []skycfgTest{ { name: "list does not copy on assignment, *protoRepeated", - fun: ` + srcFunc: ` def fun(): pkg = proto.package("skycfg.test_proto") msg1 = pkg.MessageV3() @@ -523,7 +391,7 @@ def fun(): }, { name: "list does not copy on assignment, *stalark.List", - fun: ` + srcFunc: ` def fun(): pkg = proto.package("skycfg.test_proto") a = ["a","b"] @@ -537,7 +405,7 @@ def fun(): }, { name: "map does not copy on assignment, *protoMap", - fun: ` + srcFunc: ` def fun(): pkg = proto.package("skycfg.test_proto") msg1 = pkg.MessageV3() @@ -555,7 +423,7 @@ def fun(): }, { name: "map does not copy on assignment, *stalark.Dict", - fun: ` + srcFunc: ` def fun(): pkg = proto.package("skycfg.test_proto") msg1 = pkg.MessageV3() @@ -572,7 +440,7 @@ def fun(): }, { name: "message does not copy on assignment", - fun: ` + srcFunc: ` def fun(): pkg = proto.package("skycfg.test_proto") msg1 = pkg.MessageV3() @@ -585,165 +453,101 @@ def fun(): `, want: `["a", "a", "a"]`, }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - val, err := evalFunc(test.fun, nil) - if err != nil { - t.Fatal(err) - } - got := val.String() - if test.want != got { - t.Fatalf("Output differed\nwanted: %s\ngot : %s", test.want, got) - } - }) - } + }) } func TestProtoEnumEqual(t *testing.T) { - val, err := eval(`proto.package("skycfg.test_proto").ToplevelEnumV2.TOPLEVEL_ENUM_V2_A == proto.package("skycfg.test_proto").ToplevelEnumV2.TOPLEVEL_ENUM_V2_A`, nil) - if err != nil { - t.Fatal(err) - } - got := val.(starlark.Bool) - if !bool(got) { - t.Error("Expected equal enums") - } - - val, err = eval(`proto.package("skycfg.test_proto").ToplevelEnumV2.TOPLEVEL_ENUM_V2_A == proto.package("skycfg.test_proto").ToplevelEnumV2.TOPLEVEL_ENUM_V2_B`, nil) - if err != nil { - t.Fatal(err) - } - got = val.(starlark.Bool) - if bool(got) { - t.Error("Expected unequal enums") - } - - val, err = eval(`proto.package("skycfg.test_proto").ToplevelEnumV2.TOPLEVEL_ENUM_V2_A != proto.package("skycfg.test_proto").ToplevelEnumV2.TOPLEVEL_ENUM_V2_A`, nil) - if err != nil { - t.Fatal(err) - } - got = val.(starlark.Bool) - if bool(got) { - t.Error("Expected equal enums") - } - - val, err = eval(`proto.package("skycfg.test_proto").ToplevelEnumV2.TOPLEVEL_ENUM_V2_A != proto.package("skycfg.test_proto").ToplevelEnumV2.TOPLEVEL_ENUM_V2_B`, nil) - if err != nil { - t.Fatal(err) - } - got = val.(starlark.Bool) - if !bool(got) { - t.Error("Expected unequal enums") - } -} - -func TestProtoToText(t *testing.T) { - val, err := eval(`proto.encode_text(proto.package("skycfg.test_proto").MessageV3( - f_string = "some string", - ))`, nil) - if err != nil { - t.Fatal(err) - } - got := string(val.(starlark.String)) - want := "f_string:\"some string\"" - if want != got { - t.Fatalf("encode_text: wanted %q, got %q", want, got) - } -} - -func TestProtoToTextCompact(t *testing.T) { - val, err := eval(`proto.encode_text(proto.package("skycfg.test_proto").MessageV3( - f_string = "some string", - ), compact=True)`, nil) - if err != nil { - t.Fatal(err) - } - got := string(val.(starlark.String)) - want := "f_string:\"some string\"" - if want != got { - t.Fatalf("encode_text_compact: wanted %q, got %q", want, got) - } -} - -func TestProtoToTextFull(t *testing.T) { - val, err := eval(`proto.encode_text(proto.package("skycfg.test_proto").MessageV3( - f_string = "some string", - ), compact=False)`, nil) - if err != nil { - t.Fatal(err) - } - got := removeRandomSpace(string(val.(starlark.String))) - want := "f_string: \"some string\"\n" - if want != got { - t.Fatalf("encode_text_full: wanted %q, got %q", want, got) - } -} - -func TestProtoFromText(t *testing.T) { - val, err := eval(`proto.decode_text(proto.package("skycfg.test_proto").MessageV3, "f_int32: 1010").f_int32`, nil) - if err != nil { - t.Fatal(err) - } - got := val.String() - want := "1010" - if want != got { - t.Fatalf("decode_text: wanted %q, got %q", want, got) - } -} - -func TestProtoToJson(t *testing.T) { - val, err := eval(`proto.encode_json(proto.package("skycfg.test_proto").MessageV3( - f_string = "some string", - ))`, nil) - if err != nil { - t.Fatal(err) - } - got := string(val.(starlark.String)) - want := `{"f_string":"some string"}` - if want != got { - t.Fatalf("encode_json: wanted %q, got %q", want, got) - } -} - -func TestProtoToJsonCompact(t *testing.T) { - val, err := eval(`proto.encode_json(proto.package("skycfg.test_proto").MessageV3( - f_string = "some string", - ), compact=True)`, nil) - if err != nil { - t.Fatal(err) - } - got := string(val.(starlark.String)) - want := `{"f_string":"some string"}` - if want != got { - t.Fatalf("encode_json_compact: wanted %q, got %q", want, got) - } + runSkycfgTests(t, []skycfgTest{ + { + src: `proto.package("skycfg.test_proto").ToplevelEnumV2.TOPLEVEL_ENUM_V2_A == proto.package("skycfg.test_proto").ToplevelEnumV2.TOPLEVEL_ENUM_V2_A`, + want: true, + }, + { + src: `proto.package("skycfg.test_proto").ToplevelEnumV2.TOPLEVEL_ENUM_V2_A == proto.package("skycfg.test_proto").ToplevelEnumV2.TOPLEVEL_ENUM_V2_B`, + want: false, + }, + { + src: `proto.package("skycfg.test_proto").ToplevelEnumV2.TOPLEVEL_ENUM_V2_A != proto.package("skycfg.test_proto").ToplevelEnumV2.TOPLEVEL_ENUM_V2_A`, + want: false, + }, + { + src: `proto.package("skycfg.test_proto").ToplevelEnumV2.TOPLEVEL_ENUM_V2_A != proto.package("skycfg.test_proto").ToplevelEnumV2.TOPLEVEL_ENUM_V2_B`, + want: true, + }, + }) } -func TestProtoToJsonFull(t *testing.T) { - val, err := eval(`proto.encode_json(proto.package("skycfg.test_proto").MessageV3( - f_string = "some string", - ), compact=False)`, nil) - if err != nil { - t.Fatal(err) - } - got := removeRandomSpace(string(val.(starlark.String))) - want := "{\n \"f_string\": \"some string\"\n}" - if want != got { - t.Fatalf("encode_json_full: wanted %q, got %q", want, got) - } +func TestProtoText(t *testing.T) { + runSkycfgTests(t, []skycfgTest{ + { + name: "proto.encode_text", + src: ` +proto.encode_text(proto.package("skycfg.test_proto").MessageV3( + f_string = "some string", +))`, + want: `"f_string:\"some string\""`, + wantType: "string", + }, + { + name: "proto.encode_text compact", + src: ` +proto.encode_text(proto.package("skycfg.test_proto").MessageV3( + f_string = "some string", +), compact=True)`, + want: `"f_string:\"some string\""`, + wantType: "string", + }, + { + name: "proto.encode_text full", + src: ` +proto.encode_text(proto.package("skycfg.test_proto").MessageV3( + f_string = "some string", +), compact=False)`, + want: `"f_string: \"some string\"\n"`, + wantType: "string", + removeRandomSpace: true, + }, + { + name: "proto.decode_text", + src: `proto.decode_text(proto.package("skycfg.test_proto").MessageV3, "f_int32: 1010").f_int32`, + want: "1010", + }, + }) } -func TestProtoFromJson(t *testing.T) { - val, err := eval(`proto.decode_json(proto.package("skycfg.test_proto").MessageV3, "{\"f_int32\": 1010}").f_int32`, nil) - if err != nil { - t.Fatal(err) - } - got := val.String() - want := "1010" - if want != got { - t.Fatalf("decode_json: wanted %q, got %q", want, got) - } +func TestProtoJson(t *testing.T) { + runSkycfgTests(t, []skycfgTest{ + { + name: "proto.encode_json", + src: `proto.encode_json(proto.package("skycfg.test_proto").MessageV3( + f_string = "some string", + ))`, + want: `"{\"f_string\":\"some string\"}"`, + wantType: "string", + }, + { + name: "proto.encode_json compact", + src: `proto.encode_json(proto.package("skycfg.test_proto").MessageV3( + f_string = "some string", + ), compact=True)`, + want: `"{\"f_string\":\"some string\"}"`, + wantType: "string", + }, + { + name: "proto.encode_json full", + src: `proto.encode_json(proto.package("skycfg.test_proto").MessageV3( + f_string = "some string", + ), compact=False)`, + want: `"{\n \"f_string\": \"some string\"\n}"`, + wantType: "string", + removeRandomSpace: true, + }, + { + name: "proto.decode_json", + src: `proto.decode_json(proto.package("skycfg.test_proto").MessageV3, "{\"f_int32\": 1010}").f_int32`, + want: "1010", + }, + }) } func TestProtoToAnyV2(t *testing.T) { @@ -799,6 +603,87 @@ func TestProtoToAnyV3(t *testing.T) { } } +type skycfgTest struct { + name string + src string + srcFunc string + wantErr error + want interface{} + wantType string + + // Options + globals starlark.StringDict + removeRandomSpace bool +} + +// Mutates all tests +type globalTestOption func(*skycfgTest) + +func withGlobals(globals starlark.StringDict) globalTestOption { + return func(test *skycfgTest) { + test.globals = globals + } +} + +func runSkycfgTests(t *testing.T, tests []skycfgTest, opts ...globalTestOption) { + t.Helper() + for _, test := range tests { + for _, opt := range opts { + opt(&test) + } + + name := test.name + if name == "" { + name = test.src + } + t.Run(name, func(t *testing.T) { + var val starlark.Value + var err error + if test.src != "" { + val, err = eval(test.src, test.globals) + } else if test.srcFunc != "" { + val, err = evalFunc(test.srcFunc, test.globals) + } else { + t.Fatal("Test has no src or srcFunc") + } + + checkError(t, err, test.wantErr) + + // Only check values if evaluation is not expected to error + if test.wantErr != nil { + return + } + + switch want := test.want.(type) { + case proto.Message: + got := mustProtoMessage(t, val) + checkProtoEqual(t, want, got) + case string: + got := val.String() + if test.removeRandomSpace { + got = removeRandomSpace(got) + } + if want != got { + t.Fatalf("Expected\nwanted: %s\ngot : %s", want, got) + } + case bool: + got := val.(starlark.Bool) + if bool(got) != want { + t.Fatalf("Expected\nwanted: %t\ngot : %t", want, got) + } + default: + t.Fatalf("runSkycfgTests does not support comparing %T yet", want) + } + + if test.wantType != "" { + if val.Type() != test.wantType { + t.Fatalf("Expected type\nwanted: %t\ngot : %t", test.wantType, val.Type()) + } + } + }) + } +} + func eval(src string, globals starlark.StringDict) (starlark.Value, error) { if globals == nil { globals = starlark.StringDict{ @@ -840,11 +725,14 @@ func mustProtoMessage(t *testing.T, v starlark.Value) proto.Message { return nil } -func checkError(got, want error) bool { - if got == nil { - return false +func checkError(t *testing.T, got, want error) { + t.Helper() + + if want == nil && got != nil { + t.Fatalf("Expected no error, got: %v\n", got) + } else if want != nil && got.Error() != want.Error() { + t.Fatalf("Expected error\nwanted: %q\ngot : %q", want.Error(), got.Error()) } - return got.Error() == want.Error() } // Generate a diff of two structs, which may contain protobuf messages.