testing/protocmp: add Ignore options and Any support

This CL adds the following helper options:
	func IgnoreEnums(...protoreflect.Enum) cmp.Option
	func IgnoreMessages(...proto.Message) cmp.Option
	func IgnoreFields(proto.Message, ...protoreflect.Name) cmp.Option
	func IgnoreOneofs(proto.Message, ...protoreflect.Name) cmp.Option
	func IgnoreDescriptors(...protoreflect.Descriptor) cmp.Option
	func IgnoreDefaultScalars() cmp.Option
	func IgnoreEmptyMessages() cmp.Option
	func IgnoreUnknown() cmp.Option

It also augments transformMessage to unmarshal and expand Any messages
with the value of the underlying message. At this moment in time
we do not provide an API to provide a custom type resolver.

Change-Id: I51e1d9ff0d56d71161e510f366a7dcc32236d760
Reviewed-on: https://go-review.googlesource.com/c/protobuf/+/204577
Reviewed-by: Damien Neil <dneil@google.com>
This commit is contained in:
Joe Tsai 2019-10-31 12:39:06 -07:00
parent fab1c8d9a3
commit 1c31032e00
4 changed files with 1070 additions and 9 deletions

427
testing/protocmp/util.go Normal file
View File

@ -0,0 +1,427 @@
// 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"
"math"
"reflect"
"strings"
"github.com/google/go-cmp/cmp"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
)
var (
enumReflectType = reflect.TypeOf(Enum{})
messageReflectType = reflect.TypeOf(Message{})
)
// IgnoreEnums ignores all enums of the specified types.
// See IgnoreDescriptors with regard to EnumDescriptors for more information.
//
// This must be used in conjunction with Transform.
func IgnoreEnums(enums ...protoreflect.Enum) cmp.Option {
var ds []protoreflect.Descriptor
for _, e := range enums {
ds = append(ds, e.Descriptor())
}
return IgnoreDescriptors(ds...)
}
// IgnoreMessages ignores all messages of the specified types.
// See IgnoreDescriptors with regard to MessageDescriptors for more information.
//
// This must be used in conjunction with Transform.
func IgnoreMessages(messages ...proto.Message) cmp.Option {
var ds []protoreflect.Descriptor
for _, m := range messages {
ds = append(ds, m.ProtoReflect().Descriptor())
}
return IgnoreDescriptors(ds...)
}
// IgnoreFields ignores the specified fields in messages of type m.
// This panics if a field of the given name does not exist.
// See IgnoreDescriptors with regard to FieldDescriptors for more information.
//
// This must be used in conjunction with Transform.
func IgnoreFields(message proto.Message, names ...protoreflect.Name) cmp.Option {
var ds []protoreflect.Descriptor
md := message.ProtoReflect().Descriptor()
for _, s := range names {
ds = append(ds, mustFindFieldDescriptor(md, s))
}
return IgnoreDescriptors(ds...)
}
// IgnoreOneofs ignores fields in the specified oneofs in messages of type m.
// This panics if a oneof of the given name does not exist.
// See IgnoreDescriptors with regard to OneofDescriptors for more information.
//
// This must be used in conjunction with Transform.
func IgnoreOneofs(message proto.Message, names ...protoreflect.Name) cmp.Option {
var ds []protoreflect.Descriptor
md := message.ProtoReflect().Descriptor()
for _, s := range names {
ds = append(ds, mustFindOneofDescriptor(md, s))
}
return IgnoreDescriptors(ds...)
}
// IgnoreDescriptors ignores the specified set of descriptors.
// The following descriptor types may be specified:
//
// • EnumDescriptor: Enums of this type or messages containing singular fields,
// list fields, or map fields with enum values of this type are ignored.
// Enums are matched based on their full name.
//
// • MessageDescriptor: Messages of this type or messages containing
// singular fields, list fields, or map fields with message values of this type
// are ignored. Messages are matched based on their full name.
//
// • ExtensionDescriptor: Extensions fields that match the given descriptor
// by full name are ignored.
//
// • FieldDescriptor: Message fields that match the given descriptor
// by full name are ignored.
//
// • OneofDescriptor: Message fields that match the set of fields in the given
// oneof descriptor by full name are ignored.
//
// This must be used in conjunction with Transform.
func IgnoreDescriptors(descs ...protoreflect.Descriptor) cmp.Option {
return cmp.FilterPath(newNameFilters(descs...).Filter, cmp.Ignore())
}
func mustFindFieldDescriptor(md protoreflect.MessageDescriptor, s protoreflect.Name) protoreflect.FieldDescriptor {
d := findDescriptor(md, s)
if fd, ok := d.(protoreflect.FieldDescriptor); ok && fd.Name() == s {
return fd
}
var suggestion string
switch d.(type) {
case protoreflect.FieldDescriptor:
suggestion = fmt.Sprintf("; consider specifying field %q instead", d.Name())
case protoreflect.OneofDescriptor:
suggestion = fmt.Sprintf("; consider specifying oneof %q with IgnoreOneofs instead", d.Name())
}
panic(fmt.Sprintf("message %q has no field %q%s", md.FullName(), s, suggestion))
}
func mustFindOneofDescriptor(md protoreflect.MessageDescriptor, s protoreflect.Name) protoreflect.OneofDescriptor {
d := findDescriptor(md, s)
if od, ok := d.(protoreflect.OneofDescriptor); ok && d.Name() == s {
return od
}
var suggestion string
switch d.(type) {
case protoreflect.OneofDescriptor:
suggestion = fmt.Sprintf("; consider specifying oneof %q instead", d.Name())
case protoreflect.FieldDescriptor:
suggestion = fmt.Sprintf("; consider specifying field %q with IgnoreFields instead", d.Name())
}
panic(fmt.Sprintf("message %q has no oneof %q%s", md.FullName(), s, suggestion))
}
func findDescriptor(md protoreflect.MessageDescriptor, s protoreflect.Name) protoreflect.Descriptor {
// Exact match.
if fd := md.Fields().ByName(s); fd != nil {
return fd
}
if od := md.Oneofs().ByName(s); od != nil {
return od
}
// Best-effort match.
//
// It's a common user mistake to use the CameCased field name as it appears
// in the generated Go struct. Instead of complaining that it doesn't exist,
// suggest the real protobuf name that the user may have desired.
normalize := func(s protoreflect.Name) string {
return strings.Replace(strings.ToLower(string(s)), "_", "", -1)
}
for i := 0; i < md.Fields().Len(); i++ {
if fd := md.Fields().Get(i); normalize(fd.Name()) == normalize(s) {
return fd
}
}
for i := 0; i < md.Oneofs().Len(); i++ {
if od := md.Oneofs().Get(i); normalize(od.Name()) == normalize(s) {
return od
}
}
return nil
}
type nameFilters struct {
names map[protoreflect.FullName]bool
}
func newNameFilters(descs ...protoreflect.Descriptor) *nameFilters {
f := &nameFilters{names: make(map[protoreflect.FullName]bool)}
for _, d := range descs {
switch d := d.(type) {
case protoreflect.EnumDescriptor:
f.names[d.FullName()] = true
case protoreflect.MessageDescriptor:
f.names[d.FullName()] = true
case protoreflect.FieldDescriptor:
f.names[d.FullName()] = true
case protoreflect.OneofDescriptor:
for i := 0; i < d.Fields().Len(); i++ {
f.names[d.Fields().Get(i).FullName()] = true
}
default:
panic("invalid descriptor type")
}
}
return f
}
func (f *nameFilters) Filter(p cmp.Path) bool {
vx, vy := p.Last().Values()
return (f.filterValue(vx) && f.filterValue(vy)) || f.filterFields(p)
}
func (f *nameFilters) filterFields(p cmp.Path) bool {
// Filter for Message maps.
mi, ok := p.Index(-1).(cmp.MapIndex)
if !ok {
return false
}
ps := p.Index(-2)
if ps.Type() != messageReflectType {
return false
}
// Check field name.
vx, vy := ps.Values()
mx := vx.Interface().(Message)
my := vy.Interface().(Message)
k := mi.Key().String()
if f.filterFieldName(mx, k) && f.filterFieldName(my, k) {
return true
}
// Check field value.
vx, vy = mi.Values()
if f.filterFieldValue(vx) && f.filterFieldValue(vy) {
return true
}
return false
}
func (f *nameFilters) filterFieldName(m Message, k string) bool {
if md := m.Descriptor(); md != nil {
switch {
case protoreflect.Name(k).IsValid():
return f.names[md.Fields().ByName(protoreflect.Name(k)).FullName()]
case strings.HasPrefix(k, "[") && strings.HasSuffix(k, "]"):
return f.names[protoreflect.FullName(k[1:len(k)-1])]
}
}
return false
}
func (f *nameFilters) filterFieldValue(v reflect.Value) bool {
if !v.IsValid() {
return true // implies missing slice element or map entry
}
v = v.Elem() // map entries are always populated values
switch t := v.Type(); {
case t == enumReflectType || t == messageReflectType:
// Check for singular message or enum field.
return f.filterValue(v)
case t.Kind() == reflect.Slice && (t.Elem() == enumReflectType || t.Elem() == messageReflectType):
// Check for list field of enum or message type.
return f.filterValue(v.Index(0))
case t.Kind() == reflect.Map && (t.Elem() == enumReflectType || t.Elem() == messageReflectType):
// Check for map field of enum or message type.
return f.filterValue(v.MapIndex(v.MapKeys()[0]))
}
return false
}
func (f *nameFilters) filterValue(v reflect.Value) bool {
if !v.IsValid() {
return true // implies missing slice element or map entry
}
if !v.CanInterface() {
return false // implies unexported struct field
}
switch v := v.Interface().(type) {
case Enum:
return v.Descriptor() != nil && f.names[v.Descriptor().FullName()]
case Message:
return v.Descriptor() != nil && f.names[v.Descriptor().FullName()]
}
return false
}
// IgnoreDefaultScalars ignores singular scalars that are unpopulated or
// explicitly set to the default value.
// This option does not effect elements in a list or entries in a map.
//
// This must be used in conjunction with Transform.
func IgnoreDefaultScalars() cmp.Option {
return cmp.FilterPath(func(p cmp.Path) bool {
// Filter for Message maps.
mi, ok := p.Index(-1).(cmp.MapIndex)
if !ok {
return false
}
ps := p.Index(-2)
if ps.Type() != messageReflectType {
return false
}
// Check whether both fields are default or unpopulated scalars.
vx, vy := ps.Values()
mx := vx.Interface().(Message)
my := vy.Interface().(Message)
k := mi.Key().String()
return isDefaultScalar(mx, k) && isDefaultScalar(my, k)
}, cmp.Ignore())
}
func isDefaultScalar(m Message, k string) bool {
if _, ok := m[k]; !ok {
return true
}
var fd protoreflect.FieldDescriptor
switch mt := m[messageTypeKey].(messageType); {
case protoreflect.Name(k).IsValid():
fd = mt.md.Fields().ByName(protoreflect.Name(k))
case strings.HasPrefix(k, "[") && strings.HasSuffix(k, "]"):
fd = mt.xds[protoreflect.FullName(k[1:len(k)-1])]
}
if fd == nil || !fd.Default().IsValid() {
return false
}
switch fd.Kind() {
case protoreflect.BytesKind:
v, ok := m[k].([]byte)
return ok && bytes.Equal(fd.Default().Bytes(), v)
case protoreflect.FloatKind:
v, ok := m[k].(float32)
return ok && equalFloat64(fd.Default().Float(), float64(v))
case protoreflect.DoubleKind:
v, ok := m[k].(float64)
return ok && equalFloat64(fd.Default().Float(), float64(v))
case protoreflect.EnumKind:
v, ok := m[k].(Enum)
return ok && fd.Default().Enum() == v.Number()
default:
return reflect.DeepEqual(fd.Default().Interface(), m[k])
}
}
func equalFloat64(x, y float64) bool {
return x == y || (math.IsNaN(x) && math.IsNaN(y))
}
// IgnoreEmptyMessages ignores messages that are empty or unpopulated.
// It applies to standalone Messages, singular message fields,
// list fields of messages, and map fields of message values.
//
// This must be used in conjunction with Transform.
func IgnoreEmptyMessages() cmp.Option {
return cmp.FilterPath(func(p cmp.Path) bool {
vx, vy := p.Last().Values()
return (isEmptyMessage(vx) && isEmptyMessage(vy)) || isEmptyMessageFields(p)
}, cmp.Ignore())
}
func isEmptyMessageFields(p cmp.Path) bool {
// Filter for Message maps.
mi, ok := p.Index(-1).(cmp.MapIndex)
if !ok {
return false
}
ps := p.Index(-2)
if ps.Type() != messageReflectType {
return false
}
// Check field value.
vx, vy := mi.Values()
if isEmptyMessageFieldValue(vx) && isEmptyMessageFieldValue(vy) {
return true
}
return false
}
func isEmptyMessageFieldValue(v reflect.Value) bool {
if !v.IsValid() {
return true // implies missing slice element or map entry
}
v = v.Elem() // map entries are always populated values
switch t := v.Type(); {
case t == messageReflectType:
// Check singular field for empty message.
if !isEmptyMessage(v) {
return false
}
case t.Kind() == reflect.Slice && t.Elem() == messageReflectType:
// Check list field for all empty message elements.
for i := 0; i < v.Len(); i++ {
if !isEmptyMessage(v.Index(i)) {
return false
}
}
case t.Kind() == reflect.Map && t.Elem() == messageReflectType:
// Check map field for all empty message values.
for _, k := range v.MapKeys() {
if !isEmptyMessage(v.MapIndex(k)) {
return false
}
}
default:
return false
}
return true
}
func isEmptyMessage(v reflect.Value) bool {
if !v.IsValid() {
return true // implies missing slice element or map entry
}
if !v.CanInterface() {
return false // implies unexported struct field
}
if m, ok := v.Interface().(Message); ok {
return len(m) == 0 || (len(m) == 1 && m[messageTypeKey] != nil)
}
return false
}
// IgnoreUnknown ignores unknown fields in all messages.
//
// This must be used in conjunction with Transform.
func IgnoreUnknown() cmp.Option {
return cmp.FilterPath(func(p cmp.Path) bool {
// Filter for Message maps.
mi, ok := p.Index(-1).(cmp.MapIndex)
if !ok {
return false
}
ps := p.Index(-2)
if ps.Type() != messageReflectType {
return false
}
// Filter for unknown fields (which always have a numeric map key).
return strings.Trim(mi.Key().String(), "0123456789") == ""
}, cmp.Ignore())
}

View File

@ -0,0 +1,575 @@
// 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/encoding/pack"
testpb "google.golang.org/protobuf/internal/testprotos/test"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/dynamicpb"
)
func TestEqual(t *testing.T) {
type test struct {
x, y interface{}
opts cmp.Options
want bool
}
var tests []test
allTypesDesc := (*testpb.TestAllTypes)(nil).ProtoReflect().Descriptor()
// Test nil and empty messages of differing types.
tests = append(tests, []test{{
x: (*testpb.TestAllTypes)(nil),
y: (*testpb.TestAllTypes)(nil),
opts: cmp.Options{Transform()},
want: true,
}, {
x: (*testpb.TestAllTypes)(nil),
y: new(testpb.TestAllTypes),
opts: cmp.Options{Transform()},
want: false,
}, {
x: (*testpb.TestAllTypes)(nil),
y: dynamicpb.NewMessage(allTypesDesc),
opts: cmp.Options{Transform()},
want: false,
}, {
x: (*testpb.TestAllTypes)(nil),
y: new(testpb.TestAllTypes),
opts: cmp.Options{Transform(), IgnoreEmptyMessages()},
want: true,
}, {
x: (*testpb.TestAllTypes)(nil),
y: dynamicpb.NewMessage(allTypesDesc),
opts: cmp.Options{Transform(), IgnoreEmptyMessages()},
want: true,
}, {
x: new(testpb.TestAllTypes),
y: new(testpb.TestAllTypes),
opts: cmp.Options{Transform()},
want: true,
}, {
x: new(testpb.TestAllTypes),
y: dynamicpb.NewMessage(allTypesDesc),
opts: cmp.Options{Transform()},
want: true,
}, {
x: new(testpb.TestAllTypes),
y: new(testpb.TestAllExtensions),
opts: cmp.Options{Transform()},
want: false,
}, {
x: struct{ I interface{} }{(*testpb.TestAllTypes)(nil)},
y: struct{ I interface{} }{(*testpb.TestAllTypes)(nil)},
opts: cmp.Options{Transform()},
want: true,
}, {
x: struct{ I interface{} }{(*testpb.TestAllTypes)(nil)},
y: struct{ I interface{} }{new(testpb.TestAllTypes)},
opts: cmp.Options{Transform()},
want: false,
}, {
x: struct{ I interface{} }{(*testpb.TestAllTypes)(nil)},
y: struct{ I interface{} }{dynamicpb.NewMessage(allTypesDesc)},
opts: cmp.Options{Transform()},
want: false,
}, {
x: struct{ I interface{} }{(*testpb.TestAllTypes)(nil)},
y: struct{ I interface{} }{new(testpb.TestAllTypes)},
opts: cmp.Options{Transform(), IgnoreEmptyMessages()},
want: true,
}, {
x: struct{ I interface{} }{(*testpb.TestAllTypes)(nil)},
y: struct{ I interface{} }{dynamicpb.NewMessage(allTypesDesc)},
opts: cmp.Options{Transform(), IgnoreEmptyMessages()},
want: true,
}, {
x: struct{ I interface{} }{new(testpb.TestAllTypes)},
y: struct{ I interface{} }{new(testpb.TestAllTypes)},
opts: cmp.Options{Transform()},
want: true,
}, {
x: struct{ I interface{} }{new(testpb.TestAllTypes)},
y: struct{ I interface{} }{dynamicpb.NewMessage(allTypesDesc)},
opts: cmp.Options{Transform()},
want: true,
}, {
x: struct{ M proto.Message }{(*testpb.TestAllTypes)(nil)},
y: struct{ M proto.Message }{(*testpb.TestAllTypes)(nil)},
opts: cmp.Options{Transform()},
want: true,
}, {
x: struct{ M proto.Message }{(*testpb.TestAllTypes)(nil)},
y: struct{ M proto.Message }{new(testpb.TestAllTypes)},
opts: cmp.Options{Transform()},
want: false,
}, {
x: struct{ M proto.Message }{(*testpb.TestAllTypes)(nil)},
y: struct{ M proto.Message }{dynamicpb.NewMessage(allTypesDesc)},
opts: cmp.Options{Transform()},
want: false,
}, {
x: struct{ M proto.Message }{(*testpb.TestAllTypes)(nil)},
y: struct{ M proto.Message }{new(testpb.TestAllTypes)},
opts: cmp.Options{Transform(), IgnoreEmptyMessages()},
want: true,
}, {
x: struct{ M proto.Message }{(*testpb.TestAllTypes)(nil)},
y: struct{ M proto.Message }{dynamicpb.NewMessage(allTypesDesc)},
opts: cmp.Options{Transform(), IgnoreEmptyMessages()},
want: true,
}, {
x: struct{ M proto.Message }{new(testpb.TestAllTypes)},
y: struct{ M proto.Message }{new(testpb.TestAllTypes)},
opts: cmp.Options{Transform()},
want: true,
}, {
x: struct{ M proto.Message }{new(testpb.TestAllTypes)},
y: struct{ M proto.Message }{dynamicpb.NewMessage(allTypesDesc)},
opts: cmp.Options{Transform()},
want: true,
}}...)
// Test IgnoreUnknown.
raw := pack.Message{
pack.Tag{1, pack.BytesType}, pack.String("Hello, goodbye!"),
}.Marshal()
tests = append(tests, []test{{
x: apply(&testpb.TestAllTypes{OptionalSint64: proto.Int64(5)}, setUnknown{raw}),
y: &testpb.TestAllTypes{OptionalSint64: proto.Int64(5)},
opts: cmp.Options{Transform()},
want: false,
}, {
x: apply(&testpb.TestAllTypes{OptionalSint64: proto.Int64(5)}, setUnknown{raw}),
y: &testpb.TestAllTypes{OptionalSint64: proto.Int64(5)},
opts: cmp.Options{Transform(), IgnoreUnknown()},
want: true,
}, {
x: apply(&testpb.TestAllTypes{OptionalSint64: proto.Int64(5)}, setUnknown{raw}),
y: &testpb.TestAllTypes{OptionalSint64: proto.Int64(6)},
opts: cmp.Options{Transform(), IgnoreUnknown()},
want: false,
}, {
x: apply(&testpb.TestAllTypes{OptionalSint64: proto.Int64(5)}, setUnknown{raw}),
y: apply(dynamicpb.NewMessage(allTypesDesc), setField{6, int64(5)}),
opts: cmp.Options{Transform()},
want: false,
}, {
x: apply(&testpb.TestAllTypes{OptionalSint64: proto.Int64(5)}, setUnknown{raw}),
y: apply(dynamicpb.NewMessage(allTypesDesc), setField{6, int64(5)}),
opts: cmp.Options{Transform(), IgnoreUnknown()},
want: true,
}}...)
// Test IgnoreDefaultScalars.
tests = append(tests, []test{{
x: &testpb.TestAllTypes{
DefaultInt32: proto.Int32(81),
DefaultUint32: proto.Uint32(83),
DefaultFloat: proto.Float32(91.5),
DefaultBool: proto.Bool(true),
DefaultBytes: []byte("world"),
},
y: &testpb.TestAllTypes{
DefaultInt64: proto.Int64(82),
DefaultUint64: proto.Uint64(84),
DefaultDouble: proto.Float64(92e3),
DefaultString: proto.String("hello"),
DefaultForeignEnum: testpb.ForeignEnum_FOREIGN_BAR.Enum(),
},
opts: cmp.Options{Transform()},
want: false,
}, {
x: &testpb.TestAllTypes{
DefaultInt32: proto.Int32(81),
DefaultUint32: proto.Uint32(83),
DefaultFloat: proto.Float32(91.5),
DefaultBool: proto.Bool(true),
DefaultBytes: []byte("world"),
},
y: &testpb.TestAllTypes{
DefaultInt64: proto.Int64(82),
DefaultUint64: proto.Uint64(84),
DefaultDouble: proto.Float64(92e3),
DefaultString: proto.String("hello"),
DefaultForeignEnum: testpb.ForeignEnum_FOREIGN_BAR.Enum(),
},
opts: cmp.Options{Transform(), IgnoreDefaultScalars()},
want: true,
}, {
x: &testpb.TestAllTypes{
OptionalInt32: proto.Int32(81),
OptionalUint32: proto.Uint32(83),
OptionalFloat: proto.Float32(91.5),
OptionalBool: proto.Bool(true),
OptionalBytes: []byte("world"),
},
y: &testpb.TestAllTypes{
OptionalInt64: proto.Int64(82),
OptionalUint64: proto.Uint64(84),
OptionalDouble: proto.Float64(92e3),
OptionalString: proto.String("hello"),
OptionalForeignEnum: testpb.ForeignEnum_FOREIGN_BAR.Enum(),
},
opts: cmp.Options{Transform(), IgnoreDefaultScalars()},
want: false,
}, {
x: &testpb.TestAllTypes{
OptionalInt32: proto.Int32(0),
OptionalUint32: proto.Uint32(0),
OptionalFloat: proto.Float32(0),
OptionalBool: proto.Bool(false),
OptionalBytes: []byte(""),
},
y: &testpb.TestAllTypes{
OptionalInt64: proto.Int64(0),
OptionalUint64: proto.Uint64(0),
OptionalDouble: proto.Float64(0),
OptionalString: proto.String(""),
OptionalForeignEnum: testpb.ForeignEnum_FOREIGN_FOO.Enum(),
},
opts: cmp.Options{Transform(), IgnoreDefaultScalars()},
want: true,
}, {
x: apply(new(testpb.TestAllExtensions),
setExtension{testpb.E_DefaultInt32Extension, int32(81)},
setExtension{testpb.E_DefaultUint32Extension, uint32(83)},
setExtension{testpb.E_DefaultFloatExtension, float32(91.5)},
setExtension{testpb.E_DefaultBoolExtension, bool(true)},
setExtension{testpb.E_DefaultBytesExtension, []byte("world")}),
y: apply(new(testpb.TestAllExtensions),
setExtension{testpb.E_DefaultInt64Extension, int64(82)},
setExtension{testpb.E_DefaultUint64Extension, uint64(84)},
setExtension{testpb.E_DefaultDoubleExtension, float64(92e3)},
setExtension{testpb.E_DefaultStringExtension, string("hello")}),
opts: cmp.Options{Transform()},
want: false,
}, {
x: apply(new(testpb.TestAllExtensions),
setExtension{testpb.E_DefaultInt32Extension, int32(81)},
setExtension{testpb.E_DefaultUint32Extension, uint32(83)},
setExtension{testpb.E_DefaultFloatExtension, float32(91.5)},
setExtension{testpb.E_DefaultBoolExtension, bool(true)},
setExtension{testpb.E_DefaultBytesExtension, []byte("world")}),
y: apply(new(testpb.TestAllExtensions),
setExtension{testpb.E_DefaultInt64Extension, int64(82)},
setExtension{testpb.E_DefaultUint64Extension, uint64(84)},
setExtension{testpb.E_DefaultDoubleExtension, float64(92e3)},
setExtension{testpb.E_DefaultStringExtension, string("hello")}),
opts: cmp.Options{Transform(), IgnoreDefaultScalars()},
want: true,
}, {
x: apply(new(testpb.TestAllExtensions),
setExtension{testpb.E_OptionalInt32Extension, int32(0)},
setExtension{testpb.E_OptionalUint32Extension, uint32(0)},
setExtension{testpb.E_OptionalFloatExtension, float32(0)},
setExtension{testpb.E_OptionalBoolExtension, bool(false)},
setExtension{testpb.E_OptionalBytesExtension, []byte("")}),
y: apply(new(testpb.TestAllExtensions),
setExtension{testpb.E_OptionalInt64Extension, int64(0)},
setExtension{testpb.E_OptionalUint64Extension, uint64(0)},
setExtension{testpb.E_OptionalDoubleExtension, float64(0)},
setExtension{testpb.E_OptionalStringExtension, string("")}),
opts: cmp.Options{Transform()},
want: false,
}, {
x: apply(new(testpb.TestAllExtensions),
setExtension{testpb.E_OptionalInt32Extension, int32(0)},
setExtension{testpb.E_OptionalUint32Extension, uint32(0)},
setExtension{testpb.E_OptionalFloatExtension, float32(0)},
setExtension{testpb.E_OptionalBoolExtension, bool(false)},
setExtension{testpb.E_OptionalBytesExtension, []byte("")}),
y: apply(new(testpb.TestAllExtensions),
setExtension{testpb.E_OptionalInt64Extension, int64(0)},
setExtension{testpb.E_OptionalUint64Extension, uint64(0)},
setExtension{testpb.E_OptionalDoubleExtension, float64(0)},
setExtension{testpb.E_OptionalStringExtension, string("")}),
opts: cmp.Options{Transform(), IgnoreDefaultScalars()},
want: true,
}, {
x: &testpb.TestAllTypes{
DefaultFloat: proto.Float32(91.6),
},
y: &testpb.TestAllTypes{},
opts: cmp.Options{Transform(), IgnoreDefaultScalars()},
want: false,
}, {
x: &testpb.TestAllTypes{
OptionalForeignMessage: &testpb.ForeignMessage{},
},
y: &testpb.TestAllTypes{},
opts: cmp.Options{Transform(), IgnoreDefaultScalars()},
want: false,
}}...)
// Test IgnoreEmptyMessages.
tests = append(tests, []test{{
x: []*testpb.TestAllTypes{nil, {}, {OptionalInt32: proto.Int32(5)}},
y: []*testpb.TestAllTypes{nil, {}, {OptionalInt32: proto.Int32(5)}},
opts: cmp.Options{Transform()},
want: true,
}, {
x: []*testpb.TestAllTypes{nil, {}, {OptionalInt32: proto.Int32(5)}},
y: []*testpb.TestAllTypes{{OptionalInt32: proto.Int32(5)}},
opts: cmp.Options{Transform(), IgnoreEmptyMessages()},
want: false,
}, {
x: &testpb.TestAllTypes{OptionalForeignMessage: &testpb.ForeignMessage{}},
y: &testpb.TestAllTypes{OptionalForeignMessage: nil},
opts: cmp.Options{Transform()},
want: false,
}, {
x: &testpb.TestAllTypes{OptionalForeignMessage: &testpb.ForeignMessage{}},
y: &testpb.TestAllTypes{OptionalForeignMessage: nil},
opts: cmp.Options{Transform(), IgnoreEmptyMessages()},
want: true,
}, {
x: &testpb.TestAllTypes{OptionalForeignMessage: &testpb.ForeignMessage{C: proto.Int32(5)}},
y: &testpb.TestAllTypes{OptionalForeignMessage: nil},
opts: cmp.Options{Transform(), IgnoreEmptyMessages()},
want: false,
}, {
x: &testpb.TestAllTypes{RepeatedForeignMessage: []*testpb.ForeignMessage{}},
y: &testpb.TestAllTypes{RepeatedForeignMessage: nil},
opts: cmp.Options{Transform()},
want: true,
}, {
x: &testpb.TestAllTypes{RepeatedForeignMessage: []*testpb.ForeignMessage{nil, {}}},
y: &testpb.TestAllTypes{RepeatedForeignMessage: nil},
opts: cmp.Options{Transform()},
want: false,
}, {
x: &testpb.TestAllTypes{RepeatedForeignMessage: []*testpb.ForeignMessage{nil, {}}},
y: &testpb.TestAllTypes{RepeatedForeignMessage: nil},
opts: cmp.Options{Transform(), IgnoreEmptyMessages()},
want: true,
}, {
x: &testpb.TestAllTypes{RepeatedForeignMessage: []*testpb.ForeignMessage{nil, {C: proto.Int32(5)}, {}}},
y: &testpb.TestAllTypes{RepeatedForeignMessage: nil},
opts: cmp.Options{Transform(), IgnoreEmptyMessages()},
want: false,
}, {
x: &testpb.TestAllTypes{RepeatedForeignMessage: []*testpb.ForeignMessage{nil, {C: proto.Int32(5)}, {}}},
y: &testpb.TestAllTypes{RepeatedForeignMessage: []*testpb.ForeignMessage{{}, {}, nil, {}, {C: proto.Int32(5)}, {}}},
opts: cmp.Options{Transform()},
want: false,
}, {
x: &testpb.TestAllTypes{RepeatedForeignMessage: []*testpb.ForeignMessage{nil, {C: proto.Int32(5)}, {}}},
y: &testpb.TestAllTypes{RepeatedForeignMessage: []*testpb.ForeignMessage{{}, {}, nil, {}, {C: proto.Int32(5)}, {}}},
opts: cmp.Options{Transform(), IgnoreEmptyMessages()},
want: true,
// TODO
}, {
x: &testpb.TestAllTypes{MapStringNestedMessage: map[string]*testpb.TestAllTypes_NestedMessage{}},
y: &testpb.TestAllTypes{MapStringNestedMessage: nil},
opts: cmp.Options{Transform()},
want: true,
}, {
x: &testpb.TestAllTypes{MapStringNestedMessage: map[string]*testpb.TestAllTypes_NestedMessage{"1": nil, "2": {}}},
y: &testpb.TestAllTypes{MapStringNestedMessage: nil},
opts: cmp.Options{Transform()},
want: false,
}, {
x: &testpb.TestAllTypes{MapStringNestedMessage: map[string]*testpb.TestAllTypes_NestedMessage{"1": nil, "2": {}}},
y: &testpb.TestAllTypes{MapStringNestedMessage: nil},
opts: cmp.Options{Transform(), IgnoreEmptyMessages()},
want: true,
}, {
x: &testpb.TestAllTypes{MapStringNestedMessage: map[string]*testpb.TestAllTypes_NestedMessage{"1": nil, "2": {A: proto.Int32(5)}, "3": {}}},
y: &testpb.TestAllTypes{MapStringNestedMessage: nil},
opts: cmp.Options{Transform(), IgnoreEmptyMessages()},
want: false,
}, {
x: &testpb.TestAllTypes{MapStringNestedMessage: map[string]*testpb.TestAllTypes_NestedMessage{"1": nil, "2": {A: proto.Int32(5)}, "3": {}}},
y: &testpb.TestAllTypes{MapStringNestedMessage: map[string]*testpb.TestAllTypes_NestedMessage{"1": {}, "1a": {}, "1b": nil, "2": {A: proto.Int32(5)}, "4": {}}},
opts: cmp.Options{Transform()},
want: false,
}, {
x: &testpb.TestAllTypes{MapStringNestedMessage: map[string]*testpb.TestAllTypes_NestedMessage{"1": nil, "2": {A: proto.Int32(5)}, "3": {}}},
y: &testpb.TestAllTypes{MapStringNestedMessage: map[string]*testpb.TestAllTypes_NestedMessage{"1": {}, "1a": {}, "1b": nil, "2": {A: proto.Int32(5)}, "4": {}}},
opts: cmp.Options{Transform(), IgnoreEmptyMessages()},
want: true,
}}...)
// Test IgnoreEnums and IgnoreMessages.
tests = append(tests, []test{{
x: &testpb.TestAllTypes{
OptionalNestedMessage: &testpb.TestAllTypes_NestedMessage{A: proto.Int32(1)},
RepeatedNestedMessage: []*testpb.TestAllTypes_NestedMessage{{A: proto.Int32(2)}},
MapStringNestedMessage: map[string]*testpb.TestAllTypes_NestedMessage{"3": {A: proto.Int32(3)}},
},
y: &testpb.TestAllTypes{},
opts: cmp.Options{Transform()},
want: false,
}, {
x: &testpb.TestAllTypes{
OptionalNestedMessage: &testpb.TestAllTypes_NestedMessage{A: proto.Int32(1)},
RepeatedNestedMessage: []*testpb.TestAllTypes_NestedMessage{{A: proto.Int32(2)}},
MapStringNestedMessage: map[string]*testpb.TestAllTypes_NestedMessage{"3": {A: proto.Int32(3)}},
},
y: &testpb.TestAllTypes{},
opts: cmp.Options{Transform(), IgnoreMessages(&testpb.TestAllTypes{})},
want: true,
}, {
x: &testpb.TestAllTypes{
OptionalNestedEnum: testpb.TestAllTypes_FOO.Enum(),
RepeatedNestedEnum: []testpb.TestAllTypes_NestedEnum{testpb.TestAllTypes_BAR},
MapStringNestedEnum: map[string]testpb.TestAllTypes_NestedEnum{"baz": testpb.TestAllTypes_BAZ},
},
y: &testpb.TestAllTypes{},
opts: cmp.Options{Transform()},
want: false,
}, {
x: &testpb.TestAllTypes{
OptionalNestedEnum: testpb.TestAllTypes_FOO.Enum(),
RepeatedNestedEnum: []testpb.TestAllTypes_NestedEnum{testpb.TestAllTypes_BAR},
MapStringNestedEnum: map[string]testpb.TestAllTypes_NestedEnum{"baz": testpb.TestAllTypes_BAZ},
},
y: &testpb.TestAllTypes{},
opts: cmp.Options{Transform(), IgnoreEnums(testpb.TestAllTypes_NestedEnum(0))},
want: true,
}, {
x: &testpb.TestAllTypes{
OptionalNestedEnum: testpb.TestAllTypes_FOO.Enum(),
RepeatedNestedEnum: []testpb.TestAllTypes_NestedEnum{testpb.TestAllTypes_BAR},
MapStringNestedEnum: map[string]testpb.TestAllTypes_NestedEnum{"baz": testpb.TestAllTypes_BAZ},
OptionalNestedMessage: &testpb.TestAllTypes_NestedMessage{A: proto.Int32(1)},
RepeatedNestedMessage: []*testpb.TestAllTypes_NestedMessage{{A: proto.Int32(2)}},
MapStringNestedMessage: map[string]*testpb.TestAllTypes_NestedMessage{"3": {A: proto.Int32(3)}},
},
y: &testpb.TestAllTypes{},
opts: cmp.Options{Transform(),
IgnoreMessages(&testpb.TestAllExtensions{}),
IgnoreEnums(testpb.ForeignEnum(0)),
},
want: false,
}}...)
// Test IgnoreFields and IgnoreOneofs.
tests = append(tests, []test{{
x: &testpb.TestAllTypes{OptionalInt32: proto.Int32(5)},
y: &testpb.TestAllTypes{OptionalInt32: proto.Int32(6)},
opts: cmp.Options{Transform()},
want: false,
}, {
x: &testpb.TestAllTypes{OptionalInt32: proto.Int32(5)},
y: &testpb.TestAllTypes{},
opts: cmp.Options{Transform(),
IgnoreFields(&testpb.TestAllTypes{}, "optional_int32")},
want: true,
}, {
x: &testpb.TestAllTypes{OptionalInt32: proto.Int32(5)},
y: &testpb.TestAllTypes{OptionalInt32: proto.Int32(6)},
opts: cmp.Options{Transform(),
IgnoreFields(&testpb.TestAllTypes{}, "optional_int32")},
want: true,
}, {
x: &testpb.TestAllTypes{OptionalInt32: proto.Int32(5)},
y: &testpb.TestAllTypes{OptionalInt32: proto.Int32(6)},
opts: cmp.Options{Transform(),
IgnoreFields(&testpb.TestAllTypes{}, "optional_int64")},
want: false,
}, {
x: &testpb.TestAllTypes{OneofField: &testpb.TestAllTypes_OneofUint32{5}},
y: &testpb.TestAllTypes{OneofField: &testpb.TestAllTypes_OneofString{"5"}},
opts: cmp.Options{Transform()},
want: false,
}, {
x: &testpb.TestAllTypes{OneofField: &testpb.TestAllTypes_OneofUint32{5}},
y: &testpb.TestAllTypes{OneofField: &testpb.TestAllTypes_OneofString{"5"}},
opts: cmp.Options{Transform(),
IgnoreFields(&testpb.TestAllTypes{}, "oneof_uint32"),
IgnoreFields(&testpb.TestAllTypes{}, "oneof_string")},
want: true,
}, {
x: &testpb.TestAllTypes{OneofField: &testpb.TestAllTypes_OneofUint32{5}},
y: &testpb.TestAllTypes{OneofField: &testpb.TestAllTypes_OneofString{"5"}},
opts: cmp.Options{Transform(),
IgnoreOneofs(&testpb.TestAllTypes{}, "oneof_field")},
want: true,
}, {
x: apply(new(testpb.TestAllExtensions),
setExtension{testpb.E_OptionalStringExtension, "hello"}),
y: apply(new(testpb.TestAllExtensions),
setExtension{testpb.E_OptionalStringExtension, "goodbye"}),
opts: cmp.Options{Transform()},
want: false,
}, {
x: apply(new(testpb.TestAllExtensions),
setExtension{testpb.E_OptionalStringExtension, "hello"}),
y: new(testpb.TestAllExtensions),
opts: cmp.Options{Transform(),
IgnoreDescriptors(testpb.E_OptionalStringExtension.TypeDescriptor())},
want: true,
}, {
x: apply(new(testpb.TestAllExtensions),
setExtension{testpb.E_OptionalStringExtension, "hello"}),
y: apply(new(testpb.TestAllExtensions),
setExtension{testpb.E_OptionalStringExtension, "goodbye"}),
opts: cmp.Options{Transform(),
IgnoreDescriptors(testpb.E_OptionalStringExtension.TypeDescriptor())},
want: true,
}, {
x: apply(new(testpb.TestAllExtensions),
setExtension{testpb.E_OptionalStringExtension, "hello"}),
y: apply(new(testpb.TestAllExtensions),
setExtension{testpb.E_OptionalStringExtension, "goodbye"}),
opts: cmp.Options{Transform(),
IgnoreDescriptors(testpb.E_OptionalInt32Extension.TypeDescriptor())},
want: false,
}}...)
for _, tt := range tests {
t.Run("", func(t *testing.T) {
got := cmp.Equal(tt.x, tt.y, tt.opts)
if got != tt.want {
if !got {
t.Errorf("cmp.Equal = false, want true; diff:\n%v", cmp.Diff(tt.x, tt.y, tt.opts))
} else {
t.Errorf("cmp.Equal = true, want false")
}
}
})
}
}
type setField struct {
num protoreflect.FieldNumber
val interface{}
}
type setUnknown struct {
raw protoreflect.RawFields
}
type setExtension struct {
typ protoreflect.ExtensionType
val interface{}
}
// apply applies a sequence of mutating operations to m.
func apply(m proto.Message, ops ...interface{}) proto.Message {
mr := m.ProtoReflect()
md := mr.Descriptor()
for _, op := range ops {
switch op := op.(type) {
case setField:
fd := md.Fields().ByNumber(op.num)
mr.Set(fd, protoreflect.ValueOf(op.val))
case setUnknown:
mr.SetUnknown(op.raw)
case setExtension:
mr.Set(op.typ.TypeDescriptor(), protoreflect.ValueOf(op.val))
}
}
return m
}

View File

@ -3,6 +3,10 @@
// license that can be found in the LICENSE file.
// Package protocmp provides protobuf specific options for the cmp package.
//
// The primary feature is the Transform option, which transform proto.Message
// types into a Message map that is suitable for cmp to introspect upon.
// All other options in this package must be used in conjunction with Transform.
package protocmp
import (
@ -12,11 +16,18 @@ import (
"github.com/google/go-cmp/cmp"
"google.golang.org/protobuf/internal/encoding/wire"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
"google.golang.org/protobuf/runtime/protoiface"
"google.golang.org/protobuf/runtime/protoimpl"
)
var (
messageV1Type = reflect.TypeOf((*protoiface.MessageV1)(nil)).Elem()
messageV2Type = reflect.TypeOf((*proto.Message)(nil)).Elem()
)
// 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 {
@ -25,6 +36,7 @@ type Enum struct {
}
// Descriptor returns the enum descriptor.
// It returns nil for a zero Enum value.
func (e Enum) Descriptor() protoreflect.EnumDescriptor {
return e.ed
}
@ -54,7 +66,8 @@ func (e Enum) String() string {
const messageTypeKey = "@type"
type messageType struct {
md protoreflect.MessageDescriptor
md protoreflect.MessageDescriptor
xds map[protoreflect.FullName]protoreflect.ExtensionDescriptor
}
func (t messageType) String() string {
@ -85,14 +98,21 @@ func (t1 messageType) Equal(t2 messageType) bool {
// 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).
//
// Message values must not be created by or mutated by users.
type Message map[string]interface{}
// Descriptor return the message descriptor.
// It returns nil for a zero Message value.
func (m Message) Descriptor() protoreflect.MessageDescriptor {
mt, _ := m[messageTypeKey].(messageType)
return mt.md
}
// TODO: There is currently no public API for retrieving the FieldDescriptors
// for extension fields. Rather than adding a specialized API to support that,
// perhaps Message should just implement protoreflect.ProtoMessage instead.
// 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.
@ -107,15 +127,32 @@ 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.
//
// The google.protobuf.Any message is automatically unmarshaled such that the
// "value" field is a Message representing the underlying message value
// assuming it could be resolved and properly unmarshaled.
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)
// TODO: Should this transform protoreflect.Enum types to Enum as well?
return cmp.FilterPath(func(p cmp.Path) bool {
ps := p.Last()
if isMessageType(ps.Type()) {
return true
}
// Check whether the concrete values of an interface both satisfy
// the Message interface.
if ps.Type().Kind() == reflect.Interface {
vx, vy := ps.Values()
if !vx.IsValid() || vx.IsNil() || !vy.IsValid() || vy.IsNil() {
return false
}
return isMessageType(vx.Elem().Type()) && isMessageType(vy.Elem().Type())
}
return false
}, cmp.Transformer("protocmp.Transform", func(m interface{}) Message {
if m == nil {
return nil
@ -131,15 +168,20 @@ func Transform(...option) cmp.Option {
}))
}
func isMessageType(t reflect.Type) bool {
return t.Implements(messageV1Type) || t.Implements(messageV2Type)
}
func transformMessage(m protoreflect.Message) Message {
md := m.Descriptor()
mx := Message{messageTypeKey: messageType{md}}
mx := Message{}
mt := messageType{md: m.Descriptor(), xds: make(map[protoreflect.FullName]protoreflect.FieldDescriptor)}
// 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()) + "]"
mt.xds[fd.FullName()] = fd
}
switch {
case fd.IsList():
@ -161,6 +203,22 @@ func transformMessage(m protoreflect.Message) Message {
b = b[n:]
}
// Expand Any messages.
if mt.md.FullName() == "google.protobuf.Any" {
// TODO: Expose Transform option to specify a custom resolver?
s, _ := mx["type_url"].(string)
b, _ := mx["value"].([]byte)
mt, err := protoregistry.GlobalTypes.FindMessageByURL(s)
if mt != nil && err == nil {
m2 := mt.New()
err := proto.UnmarshalOptions{AllowPartial: true}.Unmarshal(b, m2.Interface())
if err == nil {
mx["value"] = transformMessage(m2)
}
}
}
mx[messageTypeKey] = mt
return mx
}

View File

@ -22,6 +22,7 @@ func init() {
}
func TestTransform(t *testing.T) {
t.Skip()
tests := []struct {
in proto.Message
want Message