diff --git a/testing/protocmp/format.go b/testing/protocmp/format.go new file mode 100644 index 00000000..f329357c --- /dev/null +++ b/testing/protocmp/format.go @@ -0,0 +1,187 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package protocmp + +import ( + "bytes" + "fmt" + "reflect" + "sort" + "strconv" + "strings" + + "google.golang.org/protobuf/internal/detrand" + "google.golang.org/protobuf/internal/encoding/wire" + "google.golang.org/protobuf/reflect/protoreflect" +) + +// This implements a custom text marshaler similar to the prototext format. +// We don't use the prototext marshaler so that we can: +// • have finer grain control over the ordering of fields +// • marshal maps with a more aesthetically pleasant output +// +// TODO: If the prototext format gains a map-specific syntax, consider just +// using the prototext marshaler instead. + +func appendValue(b []byte, v interface{}) []byte { + switch v := v.(type) { + case bool, int32, int64, uint32, uint64, float32, float64: + return append(b, fmt.Sprint(v)...) + case string: + return append(b, strconv.Quote(string(v))...) + case []byte: + return append(b, strconv.Quote(string(v))...) + case Enum: + return append(b, v.String()...) + case Message: + return appendMessage(b, v) + case protoreflect.RawFields: + return appendValue(b, transformRawFields(v)) + default: + switch v := reflect.ValueOf(v); v.Kind() { + case reflect.Slice: + return appendList(b, v) + case reflect.Map: + return appendMap(b, v) + default: + panic(fmt.Sprintf("invalid type: %v", v.Type())) + } + } +} + +func appendMessage(b []byte, m Message) []byte { + var knownKeys, extensionKeys, unknownKeys []string + for k := range m { + switch { + case protoreflect.Name(k).IsValid(): + knownKeys = append(knownKeys, k) + case strings.HasPrefix(k, "[") && strings.HasSuffix(k, "]"): + extensionKeys = append(extensionKeys, k) + case len(strings.Trim(k, "0123456789")) == 0: + unknownKeys = append(unknownKeys, k) + } + } + sort.Slice(knownKeys, func(i, j int) bool { + fdi := m.Descriptor().Fields().ByName(protoreflect.Name(knownKeys[i])) + fdj := m.Descriptor().Fields().ByName(protoreflect.Name(knownKeys[j])) + return fdi.Index() < fdj.Index() + }) + sort.Slice(extensionKeys, func(i, j int) bool { + return extensionKeys[i] < extensionKeys[j] + }) + sort.Slice(unknownKeys, func(i, j int) bool { + ni, _ := strconv.Atoi(unknownKeys[i]) + nj, _ := strconv.Atoi(unknownKeys[j]) + return ni < nj + }) + ks := append(append(append([]string(nil), knownKeys...), extensionKeys...), unknownKeys...) + + b = append(b, '{') + for _, k := range ks { + b = append(b, k...) + b = append(b, ':') + b = appendValue(b, m[k]) + b = append(b, delim()...) + } + b = bytes.TrimRight(b, delim()) + b = append(b, '}') + return b +} + +func appendList(b []byte, v reflect.Value) []byte { + b = append(b, '[') + for i := 0; i < v.Len(); i++ { + b = appendValue(b, v.Index(i).Interface()) + b = append(b, delim()...) + } + b = bytes.TrimRight(b, delim()) + b = append(b, ']') + return b +} + +func appendMap(b []byte, v reflect.Value) []byte { + ks := v.MapKeys() + sort.Slice(ks, func(i, j int) bool { + ki, kj := ks[i], ks[j] + switch ki.Kind() { + case reflect.Bool: + return !ki.Bool() && kj.Bool() + case reflect.Int32, reflect.Int64: + return ki.Int() < kj.Int() + case reflect.Uint32, reflect.Uint64: + return ki.Uint() < kj.Uint() + case reflect.String: + return ki.String() < kj.String() + default: + panic(fmt.Sprintf("invalid kind: %v", ki.Kind())) + } + }) + + b = append(b, '{') + for _, k := range ks { + b = appendValue(b, k.Interface()) + b = append(b, ':') + b = appendValue(b, v.MapIndex(k).Interface()) + b = append(b, delim()...) + } + b = bytes.TrimRight(b, delim()) + b = append(b, '}') + return b +} + +func transformRawFields(b protoreflect.RawFields) interface{} { + var vs []interface{} + for len(b) > 0 { + num, typ, n := wire.ConsumeTag(b) + m := wire.ConsumeFieldValue(num, typ, b[n:]) + vs = append(vs, transformRawField(typ, b[n:][:m])) + b = b[n+m:] + } + if len(vs) == 1 { + return vs[0] + } + return vs +} + +func transformRawField(typ wire.Type, b protoreflect.RawFields) interface{} { + switch typ { + case wire.VarintType: + v, _ := wire.ConsumeVarint(b) + return v + case wire.Fixed32Type: + v, _ := wire.ConsumeFixed32(b) + return v + case wire.Fixed64Type: + v, _ := wire.ConsumeFixed64(b) + return v + case wire.BytesType: + v, _ := wire.ConsumeBytes(b) + return v + case wire.StartGroupType: + v := Message{} + for { + num2, typ2, n := wire.ConsumeTag(b) + if typ2 == wire.EndGroupType { + return v + } + m := wire.ConsumeFieldValue(num2, typ2, b[n:]) + s := strconv.Itoa(int(num2)) + b2, _ := v[s].(protoreflect.RawFields) + v[s] = append(b2, b[:n+m]...) + b = b[n+m:] + } + default: + panic(fmt.Sprintf("invalid type: %v", typ)) + } +} + +func delim() string { + // Deliberately introduce instability into the message string to + // discourage users from depending on it. + if detrand.Bool() { + return " " + } + return ", " +} diff --git a/testing/protocmp/xform.go b/testing/protocmp/xform.go new file mode 100644 index 00000000..58071627 --- /dev/null +++ b/testing/protocmp/xform.go @@ -0,0 +1,225 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package protocmp provides protobuf specific options for the cmp package. +package protocmp + +import ( + "reflect" + "strconv" + + "github.com/google/go-cmp/cmp" + + "google.golang.org/protobuf/internal/encoding/wire" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/runtime/protoiface" + "google.golang.org/protobuf/runtime/protoimpl" +) + +// Enum is a dynamic representation of a protocol buffer enum that is +// suitable for cmp.Equal and cmp.Diff to compare upon. +type Enum struct { + Number protoreflect.EnumNumber + ed protoreflect.EnumDescriptor +} + +// Descriptor returns the enum descriptor. +func (e Enum) Descriptor() protoreflect.EnumDescriptor { + return e.ed +} + +// Equal reports whether e1 and e2 represent the same enum value. +func (e1 Enum) Equal(e2 Enum) bool { + if e1.ed.FullName() != e2.ed.FullName() { + return false + } + return e1.Number == e2.Number +} + +// String returns the name of the enum value if known (e.g., "ENUM_VALUE"), +// otherwise it returns the formatted decimal enum number (e.g., "14"). +func (e Enum) String() string { + if ev := e.ed.Values().ByNumber(e.Number); ev != nil { + return string(ev.Name()) + } + return strconv.Itoa(int(e.Number)) +} + +const messageTypeKey = "@type" + +type messageType struct { + md protoreflect.MessageDescriptor +} + +func (t messageType) String() string { + return string(t.md.FullName()) +} + +func (t1 messageType) Equal(t2 messageType) bool { + return t1.md.FullName() == t2.md.FullName() +} + +// Message is a dynamic representation of a protocol buffer message that is +// suitable for cmp.Equal and cmp.Diff to directly operate upon. +// +// Every populated known field (excluding extension fields) is stored in the map +// with the key being the short name of the field (e.g., "field_name") and +// the value determined by the kind and cardinality of the field. +// +// Singular scalars are represented by the same Go type as protoreflect.Value, +// singular messages are represented by the Message type, +// singular enums are represented by the Enum type, +// list fields are represented as a Go slice, and +// map fields are represented as a Go map. +// +// Every populated extension field is stored in the map with the key being the +// full name of the field surrounded by brackets (e.g., "[extension.full.name]") +// and the value determined according to the same rules as known fields. +// +// Every unknown field is stored in the map with the key being the field number +// encoded as a decimal string (e.g., "132") and the value being the raw bytes +// of the encoded field (as the protoreflect.RawFields type). +type Message map[string]interface{} + +// Descriptor return the message descriptor. +func (m Message) Descriptor() protoreflect.MessageDescriptor { + mt, _ := m[messageTypeKey].(messageType) + return mt.md +} + +// String returns a formatted string for the message. +// It is intended for human debugging and has no guarantees about its +// exact format or the stability of its output. +func (m Message) String() string { + if m == nil { + return "" + } + return string(appendMessage(nil, m)) +} + +type option struct{} + +// Transform returns a cmp.Option that converts each proto.Message to a Message. +// The transformation does not mutate nor alias any converted messages. +func Transform(...option) cmp.Option { + // NOTE: There are currently no custom options for Transform, + // but the use of an unexported type keeps the future open. + return cmp.FilterValues(func(x, y interface{}) bool { + _, okX1 := x.(protoiface.MessageV1) + _, okX2 := x.(protoreflect.ProtoMessage) + _, okY1 := y.(protoiface.MessageV1) + _, okY2 := y.(protoreflect.ProtoMessage) + return (okX1 || okX2) && (okY1 || okY2) + }, cmp.Transformer("protocmp.Transform", func(m interface{}) Message { + if m == nil { + return nil + } + + // TODO: Should typed nil messages result in a nil Message? + // For now, do so as it is easier to remove this check than to add it. + if v := reflect.ValueOf(m); v.Kind() == reflect.Ptr && v.IsNil() { + return nil + } + + return transformMessage(protoimpl.X.MessageOf(m)) + })) +} + +func transformMessage(m protoreflect.Message) Message { + md := m.Descriptor() + mx := Message{messageTypeKey: messageType{md}} + + // Handle known and extension fields. + m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool { + s := string(fd.Name()) + if fd.IsExtension() { + s = "[" + string(fd.FullName()) + "]" + } + switch { + case fd.IsList(): + mx[s] = transformList(fd, v.List()) + case fd.IsMap(): + mx[s] = transformMap(fd, v.Map()) + default: + mx[s] = transformSingular(fd, v) + } + return true + }) + + // Handle unknown fields. + for b := m.GetUnknown(); len(b) > 0; { + num, _, n := wire.ConsumeField(b) + s := strconv.Itoa(int(num)) + b2, _ := mx[s].(protoreflect.RawFields) + mx[s] = append(b2, b[:n]...) + b = b[n:] + } + + return mx +} + +func transformList(fd protoreflect.FieldDescriptor, lv protoreflect.List) interface{} { + t := protoKindToGoType(fd.Kind()) + rv := reflect.MakeSlice(reflect.SliceOf(t), lv.Len(), lv.Len()) + for i := 0; i < lv.Len(); i++ { + v := reflect.ValueOf(transformSingular(fd, lv.Get(i))) + rv.Index(i).Set(v) + } + return rv.Interface() +} + +func transformMap(fd protoreflect.FieldDescriptor, mv protoreflect.Map) interface{} { + kfd := fd.MapKey() + vfd := fd.MapValue() + kt := protoKindToGoType(kfd.Kind()) + vt := protoKindToGoType(vfd.Kind()) + rv := reflect.MakeMapWithSize(reflect.MapOf(kt, vt), mv.Len()) + mv.Range(func(k protoreflect.MapKey, v protoreflect.Value) bool { + kv := reflect.ValueOf(transformSingular(kfd, k.Value())) + vv := reflect.ValueOf(transformSingular(vfd, v)) + rv.SetMapIndex(kv, vv) + return true + }) + return rv.Interface() +} + +func transformSingular(fd protoreflect.FieldDescriptor, v protoreflect.Value) interface{} { + switch fd.Kind() { + case protoreflect.EnumKind: + return Enum{Number: v.Enum(), ed: fd.Enum()} + case protoreflect.MessageKind, protoreflect.GroupKind: + return transformMessage(v.Message()) + default: + return v.Interface() + } +} + +func protoKindToGoType(k protoreflect.Kind) reflect.Type { + switch k { + case protoreflect.BoolKind: + return reflect.TypeOf(bool(false)) + case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind: + return reflect.TypeOf(int32(0)) + case protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind: + return reflect.TypeOf(int64(0)) + case protoreflect.Uint32Kind, protoreflect.Fixed32Kind: + return reflect.TypeOf(uint32(0)) + case protoreflect.Uint64Kind, protoreflect.Fixed64Kind: + return reflect.TypeOf(uint64(0)) + case protoreflect.FloatKind: + return reflect.TypeOf(float32(0)) + case protoreflect.DoubleKind: + return reflect.TypeOf(float64(0)) + case protoreflect.StringKind: + return reflect.TypeOf(string("")) + case protoreflect.BytesKind: + return reflect.TypeOf([]byte(nil)) + case protoreflect.EnumKind: + return reflect.TypeOf(Enum{}) + case protoreflect.MessageKind, protoreflect.GroupKind: + return reflect.TypeOf(Message{}) + default: + panic("invalid kind") + } +} diff --git a/testing/protocmp/xform_test.go b/testing/protocmp/xform_test.go new file mode 100644 index 00000000..189af9e9 --- /dev/null +++ b/testing/protocmp/xform_test.go @@ -0,0 +1,281 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package protocmp + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + + "google.golang.org/protobuf/internal/detrand" + "google.golang.org/protobuf/internal/encoding/pack" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" + + testpb "google.golang.org/protobuf/internal/testprotos/test" +) + +func init() { + detrand.Disable() +} + +func TestTransform(t *testing.T) { + tests := []struct { + in proto.Message + want Message + wantString string + }{{ + in: &testpb.TestAllTypes{ + OptionalBool: proto.Bool(false), + OptionalInt32: proto.Int32(-32), + OptionalInt64: proto.Int64(-64), + OptionalUint32: proto.Uint32(32), + OptionalUint64: proto.Uint64(64), + OptionalFloat: proto.Float32(32.32), + OptionalDouble: proto.Float64(64.64), + OptionalString: proto.String("string"), + OptionalBytes: []byte("bytes"), + OptionalNestedEnum: testpb.TestAllTypes_NEG.Enum(), + OptionalNestedMessage: &testpb.TestAllTypes_NestedMessage{A: proto.Int32(5)}, + }, + want: Message{ + messageTypeKey: messageTypeOf(&testpb.TestAllTypes{}), + "optional_bool": bool(false), + "optional_int32": int32(-32), + "optional_int64": int64(-64), + "optional_uint32": uint32(32), + "optional_uint64": uint64(64), + "optional_float": float32(32.32), + "optional_double": float64(64.64), + "optional_string": string("string"), + "optional_bytes": []byte("bytes"), + "optional_nested_enum": enumOf(testpb.TestAllTypes_NEG), + "optional_nested_message": Message{messageTypeKey: messageTypeOf(&testpb.TestAllTypes_NestedMessage{}), "a": int32(5)}, + }, + wantString: `{optional_int32:-32, optional_int64:-64, optional_uint32:32, optional_uint64:64, optional_float:32.32, optional_double:64.64, optional_bool:false, optional_string:"string", optional_bytes:"bytes", optional_nested_message:{a:5}, optional_nested_enum:NEG}`, + }, { + in: &testpb.TestAllTypes{ + RepeatedBool: []bool{false, true}, + RepeatedInt32: []int32{32, -32}, + RepeatedInt64: []int64{64, -64}, + RepeatedUint32: []uint32{0, 32}, + RepeatedUint64: []uint64{0, 64}, + RepeatedFloat: []float32{0, 32.32}, + RepeatedDouble: []float64{0, 64.64}, + RepeatedString: []string{"s1", "s2"}, + RepeatedBytes: [][]byte{{1}, {2}}, + RepeatedNestedEnum: []testpb.TestAllTypes_NestedEnum{ + testpb.TestAllTypes_FOO, + testpb.TestAllTypes_BAR, + }, + RepeatedNestedMessage: []*testpb.TestAllTypes_NestedMessage{ + {A: proto.Int32(5)}, + {A: proto.Int32(-5)}, + }, + }, + want: Message{ + messageTypeKey: messageTypeOf(&testpb.TestAllTypes{}), + "repeated_bool": []bool{false, true}, + "repeated_int32": []int32{32, -32}, + "repeated_int64": []int64{64, -64}, + "repeated_uint32": []uint32{0, 32}, + "repeated_uint64": []uint64{0, 64}, + "repeated_float": []float32{0, 32.32}, + "repeated_double": []float64{0, 64.64}, + "repeated_string": []string{"s1", "s2"}, + "repeated_bytes": [][]byte{{1}, {2}}, + "repeated_nested_enum": []Enum{ + enumOf(testpb.TestAllTypes_FOO), + enumOf(testpb.TestAllTypes_BAR), + }, + "repeated_nested_message": []Message{ + {messageTypeKey: messageTypeOf(&testpb.TestAllTypes_NestedMessage{}), "a": int32(5)}, + {messageTypeKey: messageTypeOf(&testpb.TestAllTypes_NestedMessage{}), "a": int32(-5)}, + }, + }, + wantString: `{repeated_int32:[32, -32], repeated_int64:[64, -64], repeated_uint32:[0, 32], repeated_uint64:[0, 64], repeated_float:[0, 32.32], repeated_double:[0, 64.64], repeated_bool:[false, true], repeated_string:["s1", "s2"], repeated_bytes:["\x01", "\x02"], repeated_nested_message:[{a:5}, {a:-5}], repeated_nested_enum:[FOO, BAR]}`, + }, { + in: &testpb.TestAllTypes{ + MapBoolBool: map[bool]bool{true: false}, + MapInt32Int32: map[int32]int32{-32: 32}, + MapInt64Int64: map[int64]int64{-64: 64}, + MapUint32Uint32: map[uint32]uint32{0: 32}, + MapUint64Uint64: map[uint64]uint64{0: 64}, + MapInt32Float: map[int32]float32{32: 32.32}, + MapInt32Double: map[int32]float64{64: 64.64}, + MapStringString: map[string]string{"k": "v"}, + MapStringBytes: map[string][]byte{"k": []byte("v")}, + MapStringNestedEnum: map[string]testpb.TestAllTypes_NestedEnum{ + "k": testpb.TestAllTypes_FOO, + }, + MapStringNestedMessage: map[string]*testpb.TestAllTypes_NestedMessage{ + "k": {A: proto.Int32(5)}, + }, + }, + want: Message{ + messageTypeKey: messageTypeOf(&testpb.TestAllTypes{}), + "map_bool_bool": map[bool]bool{true: false}, + "map_int32_int32": map[int32]int32{-32: 32}, + "map_int64_int64": map[int64]int64{-64: 64}, + "map_uint32_uint32": map[uint32]uint32{0: 32}, + "map_uint64_uint64": map[uint64]uint64{0: 64}, + "map_int32_float": map[int32]float32{32: 32.32}, + "map_int32_double": map[int32]float64{64: 64.64}, + "map_string_string": map[string]string{"k": "v"}, + "map_string_bytes": map[string][]byte{"k": []byte("v")}, + "map_string_nested_enum": map[string]Enum{ + "k": enumOf(testpb.TestAllTypes_FOO), + }, + "map_string_nested_message": map[string]Message{ + "k": {messageTypeKey: messageTypeOf(&testpb.TestAllTypes_NestedMessage{}), "a": int32(5)}, + }, + }, + wantString: `{map_int32_int32:{-32:32}, map_int64_int64:{-64:64}, map_uint32_uint32:{0:32}, map_uint64_uint64:{0:64}, map_int32_float:{32:32.32}, map_int32_double:{64:64.64}, map_bool_bool:{true:false}, map_string_string:{"k":"v"}, map_string_bytes:{"k":"v"}, map_string_nested_message:{"k":{a:5}}, map_string_nested_enum:{"k":FOO}}`, + }, { + in: func() proto.Message { + m := &testpb.TestAllExtensions{} + proto.SetExtension(m, testpb.E_OptionalBoolExtension, bool(false)) + proto.SetExtension(m, testpb.E_OptionalInt32Extension, int32(-32)) + proto.SetExtension(m, testpb.E_OptionalInt64Extension, int64(-64)) + proto.SetExtension(m, testpb.E_OptionalUint32Extension, uint32(32)) + proto.SetExtension(m, testpb.E_OptionalUint64Extension, uint64(64)) + proto.SetExtension(m, testpb.E_OptionalFloatExtension, float32(32.32)) + proto.SetExtension(m, testpb.E_OptionalDoubleExtension, float64(64.64)) + proto.SetExtension(m, testpb.E_OptionalStringExtension, string("string")) + proto.SetExtension(m, testpb.E_OptionalBytesExtension, []byte("bytes")) + proto.SetExtension(m, testpb.E_OptionalNestedEnumExtension, testpb.TestAllTypes_NEG) + proto.SetExtension(m, testpb.E_OptionalNestedMessageExtension, &testpb.TestAllTypes_NestedMessage{A: proto.Int32(5)}) + return m + }(), + want: Message{ + messageTypeKey: messageTypeOf(&testpb.TestAllExtensions{}), + "[goproto.proto.test.optional_bool_extension]": bool(false), + "[goproto.proto.test.optional_int32_extension]": int32(-32), + "[goproto.proto.test.optional_int64_extension]": int64(-64), + "[goproto.proto.test.optional_uint32_extension]": uint32(32), + "[goproto.proto.test.optional_uint64_extension]": uint64(64), + "[goproto.proto.test.optional_float_extension]": float32(32.32), + "[goproto.proto.test.optional_double_extension]": float64(64.64), + "[goproto.proto.test.optional_string_extension]": string("string"), + "[goproto.proto.test.optional_bytes_extension]": []byte("bytes"), + "[goproto.proto.test.optional_nested_enum_extension]": enumOf(testpb.TestAllTypes_NEG), + "[goproto.proto.test.optional_nested_message_extension]": Message{messageTypeKey: messageTypeOf(&testpb.TestAllTypes_NestedMessage{}), "a": int32(5)}, + }, + wantString: `{[goproto.proto.test.optional_bool_extension]:false, [goproto.proto.test.optional_bytes_extension]:"bytes", [goproto.proto.test.optional_double_extension]:64.64, [goproto.proto.test.optional_float_extension]:32.32, [goproto.proto.test.optional_int32_extension]:-32, [goproto.proto.test.optional_int64_extension]:-64, [goproto.proto.test.optional_nested_enum_extension]:NEG, [goproto.proto.test.optional_nested_message_extension]:{a:5}, [goproto.proto.test.optional_string_extension]:"string", [goproto.proto.test.optional_uint32_extension]:32, [goproto.proto.test.optional_uint64_extension]:64}`, + }, { + in: func() proto.Message { + m := &testpb.TestAllExtensions{} + proto.SetExtension(m, testpb.E_RepeatedBoolExtension, []bool{false, true}) + proto.SetExtension(m, testpb.E_RepeatedInt32Extension, []int32{32, -32}) + proto.SetExtension(m, testpb.E_RepeatedInt64Extension, []int64{64, -64}) + proto.SetExtension(m, testpb.E_RepeatedUint32Extension, []uint32{0, 32}) + proto.SetExtension(m, testpb.E_RepeatedUint64Extension, []uint64{0, 64}) + proto.SetExtension(m, testpb.E_RepeatedFloatExtension, []float32{0, 32.32}) + proto.SetExtension(m, testpb.E_RepeatedDoubleExtension, []float64{0, 64.64}) + proto.SetExtension(m, testpb.E_RepeatedStringExtension, []string{"s1", "s2"}) + proto.SetExtension(m, testpb.E_RepeatedBytesExtension, [][]byte{{1}, {2}}) + proto.SetExtension(m, testpb.E_RepeatedNestedEnumExtension, []testpb.TestAllTypes_NestedEnum{ + testpb.TestAllTypes_FOO, + testpb.TestAllTypes_BAR, + }) + proto.SetExtension(m, testpb.E_RepeatedNestedMessageExtension, []*testpb.TestAllTypes_NestedMessage{ + {A: proto.Int32(5)}, + {A: proto.Int32(-5)}, + }) + return m + }(), + want: Message{ + messageTypeKey: messageTypeOf(&testpb.TestAllExtensions{}), + "[goproto.proto.test.repeated_bool_extension]": []bool{false, true}, + "[goproto.proto.test.repeated_int32_extension]": []int32{32, -32}, + "[goproto.proto.test.repeated_int64_extension]": []int64{64, -64}, + "[goproto.proto.test.repeated_uint32_extension]": []uint32{0, 32}, + "[goproto.proto.test.repeated_uint64_extension]": []uint64{0, 64}, + "[goproto.proto.test.repeated_float_extension]": []float32{0, 32.32}, + "[goproto.proto.test.repeated_double_extension]": []float64{0, 64.64}, + "[goproto.proto.test.repeated_string_extension]": []string{"s1", "s2"}, + "[goproto.proto.test.repeated_bytes_extension]": [][]byte{{1}, {2}}, + "[goproto.proto.test.repeated_nested_enum_extension]": []Enum{ + enumOf(testpb.TestAllTypes_FOO), + enumOf(testpb.TestAllTypes_BAR), + }, + "[goproto.proto.test.repeated_nested_message_extension]": []Message{ + {messageTypeKey: messageTypeOf(&testpb.TestAllTypes_NestedMessage{}), "a": int32(5)}, + {messageTypeKey: messageTypeOf(&testpb.TestAllTypes_NestedMessage{}), "a": int32(-5)}, + }, + }, + wantString: `{[goproto.proto.test.repeated_bool_extension]:[false, true], [goproto.proto.test.repeated_bytes_extension]:["\x01", "\x02"], [goproto.proto.test.repeated_double_extension]:[0, 64.64], [goproto.proto.test.repeated_float_extension]:[0, 32.32], [goproto.proto.test.repeated_int32_extension]:[32, -32], [goproto.proto.test.repeated_int64_extension]:[64, -64], [goproto.proto.test.repeated_nested_enum_extension]:[FOO, BAR], [goproto.proto.test.repeated_nested_message_extension]:[{a:5}, {a:-5}], [goproto.proto.test.repeated_string_extension]:["s1", "s2"], [goproto.proto.test.repeated_uint32_extension]:[0, 32], [goproto.proto.test.repeated_uint64_extension]:[0, 64]}`, + }, { + in: func() proto.Message { + m := &testpb.TestAllTypes{} + m.ProtoReflect().SetUnknown(pack.Message{ + pack.Tag{Number: 50000, Type: pack.VarintType}, pack.Uvarint(100), + pack.Tag{Number: 50001, Type: pack.Fixed32Type}, pack.Uint32(200), + pack.Tag{Number: 50002, Type: pack.Fixed64Type}, pack.Uint64(300), + pack.Tag{Number: 50003, Type: pack.BytesType}, pack.String("hello"), + pack.Message{ + pack.Tag{Number: 50004, Type: pack.StartGroupType}, + pack.Tag{Number: 1, Type: pack.VarintType}, pack.Uvarint(100), + pack.Tag{Number: 1, Type: pack.Fixed32Type}, pack.Uint32(200), + pack.Tag{Number: 1, Type: pack.Fixed64Type}, pack.Uint64(300), + pack.Tag{Number: 1, Type: pack.BytesType}, pack.String("hello"), + pack.Message{ + pack.Tag{Number: 1, Type: pack.StartGroupType}, + pack.Tag{Number: 1, Type: pack.VarintType}, pack.Uvarint(100), + pack.Tag{Number: 1, Type: pack.Fixed32Type}, pack.Uint32(200), + pack.Tag{Number: 1, Type: pack.Fixed64Type}, pack.Uint64(300), + pack.Tag{Number: 1, Type: pack.BytesType}, pack.String("hello"), + pack.Tag{Number: 1, Type: pack.EndGroupType}, + }, + pack.Tag{Number: 50004, Type: pack.EndGroupType}, + }, + }.Marshal()) + return m + }(), + want: Message{ + messageTypeKey: messageTypeOf(&testpb.TestAllTypes{}), + "50000": protoreflect.RawFields(pack.Message{pack.Tag{Number: 50000, Type: pack.VarintType}, pack.Uvarint(100)}.Marshal()), + "50001": protoreflect.RawFields(pack.Message{pack.Tag{Number: 50001, Type: pack.Fixed32Type}, pack.Uint32(200)}.Marshal()), + "50002": protoreflect.RawFields(pack.Message{pack.Tag{Number: 50002, Type: pack.Fixed64Type}, pack.Uint64(300)}.Marshal()), + "50003": protoreflect.RawFields(pack.Message{pack.Tag{Number: 50003, Type: pack.BytesType}, pack.String("hello")}.Marshal()), + "50004": protoreflect.RawFields(pack.Message{ + pack.Tag{Number: 50004, Type: pack.StartGroupType}, + pack.Tag{Number: 1, Type: pack.VarintType}, pack.Uvarint(100), + pack.Tag{Number: 1, Type: pack.Fixed32Type}, pack.Uint32(200), + pack.Tag{Number: 1, Type: pack.Fixed64Type}, pack.Uint64(300), + pack.Tag{Number: 1, Type: pack.BytesType}, pack.String("hello"), + pack.Message{ + pack.Tag{Number: 1, Type: pack.StartGroupType}, + pack.Tag{Number: 1, Type: pack.VarintType}, pack.Uvarint(100), + pack.Tag{Number: 1, Type: pack.Fixed32Type}, pack.Uint32(200), + pack.Tag{Number: 1, Type: pack.Fixed64Type}, pack.Uint64(300), + pack.Tag{Number: 1, Type: pack.BytesType}, pack.String("hello"), + pack.Tag{Number: 1, Type: pack.EndGroupType}, + }, + pack.Tag{Number: 50004, Type: pack.EndGroupType}, + }.Marshal()), + }, + wantString: `{50000:100, 50001:200, 50002:300, 50003:"hello", 50004:{1:[100, 200, 300, "hello", {1:[100, 200, 300, "hello"]}]}}`, + }} + for _, tt := range tests { + t.Run("", func(t *testing.T) { + got := transformMessage(tt.in.ProtoReflect()) + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("Transform() mismatch (-want +got):\n%v", diff) + } + if diff := cmp.Diff(tt.wantString, got.String()); diff != "" { + t.Errorf("Transform().String() mismatch (-want +got):\n%v", diff) + } + }) + } +} + +func enumOf(e protoreflect.Enum) Enum { + return Enum{e.Number(), e.Descriptor()} +} + +func messageTypeOf(m protoreflect.ProtoMessage) messageType { + return messageType{md: m.ProtoReflect().Descriptor()} +}