testing/protocmp: automatically promote message values

For user convenience, automatically transform message values by
shallow copying them if necessary. Storing messages as values is
frowned upon, but is sometimes done by APIs that a user does not own.

Change-Id: I7e927d1a1e050bf4cea1aa889f56d23e99355f26
Reviewed-on: https://go-review.googlesource.com/c/protobuf/+/221423
Reviewed-by: Damien Neil <dneil@google.com>
This commit is contained in:
Joe Tsai 2020-02-27 14:27:08 -08:00 committed by Joe Tsai
parent a89afdc06f
commit ca0b25e48f
2 changed files with 77 additions and 2 deletions
testing/protocmp

@ -145,6 +145,65 @@ func TestEqual(t *testing.T) {
want: true,
}}...)
// Test message values.
tests = append(tests, []test{{
x: testpb.TestAllTypes{OptionalSint64: proto.Int64(1)},
y: testpb.TestAllTypes{OptionalSint64: proto.Int64(1)},
opts: cmp.Options{Transform()},
want: true,
}, {
x: testpb.TestAllTypes{OptionalSint64: proto.Int64(1)},
y: testpb.TestAllTypes{OptionalSint64: proto.Int64(2)},
opts: cmp.Options{Transform()},
want: false,
}, {
x: struct{ M testpb.TestAllTypes }{M: testpb.TestAllTypes{OptionalSint64: proto.Int64(1)}},
y: struct{ M testpb.TestAllTypes }{M: testpb.TestAllTypes{OptionalSint64: proto.Int64(1)}},
opts: cmp.Options{Transform()},
want: true,
}, {
x: struct{ M testpb.TestAllTypes }{M: testpb.TestAllTypes{OptionalSint64: proto.Int64(1)}},
y: struct{ M testpb.TestAllTypes }{M: testpb.TestAllTypes{OptionalSint64: proto.Int64(2)}},
opts: cmp.Options{Transform()},
want: false,
}, {
x: struct{ M []testpb.TestAllTypes }{M: []testpb.TestAllTypes{{OptionalSint64: proto.Int64(1)}}},
y: struct{ M []testpb.TestAllTypes }{M: []testpb.TestAllTypes{{OptionalSint64: proto.Int64(1)}}},
opts: cmp.Options{Transform()},
want: true,
}, {
x: struct{ M []testpb.TestAllTypes }{M: []testpb.TestAllTypes{{OptionalSint64: proto.Int64(1)}}},
y: struct{ M []testpb.TestAllTypes }{M: []testpb.TestAllTypes{{OptionalSint64: proto.Int64(2)}}},
opts: cmp.Options{Transform()},
want: false,
}, {
x: struct {
M map[string]testpb.TestAllTypes
}{
M: map[string]testpb.TestAllTypes{"k": {OptionalSint64: proto.Int64(1)}},
},
y: struct {
M map[string]testpb.TestAllTypes
}{
M: map[string]testpb.TestAllTypes{"k": {OptionalSint64: proto.Int64(1)}},
},
opts: cmp.Options{Transform()},
want: true,
}, {
x: struct {
M map[string]testpb.TestAllTypes
}{
M: map[string]testpb.TestAllTypes{"k": {OptionalSint64: proto.Int64(1)}},
},
y: struct {
M map[string]testpb.TestAllTypes
}{
M: map[string]testpb.TestAllTypes{"k": {OptionalSint64: proto.Int64(2)}},
},
opts: cmp.Options{Transform()},
want: false,
}}...)
// Test IgnoreUnknown.
raw := pack.Message{
pack.Tag{1, pack.BytesType}, pack.String("Hello, goodbye!"),

@ -161,10 +161,18 @@ 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.
// addrType returns a pointer to t if t isn't a pointer or interface.
addrType := func(t reflect.Type) reflect.Type {
if k := t.Kind(); k == reflect.Interface || k == reflect.Ptr {
return t
}
return reflect.PtrTo(t)
}
// 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()) {
if isMessageType(addrType(ps.Type())) {
return true
}
@ -175,11 +183,19 @@ func Transform(...option) cmp.Option {
if !vx.IsValid() || vx.IsNil() || !vy.IsValid() || vy.IsNil() {
return false
}
return isMessageType(vx.Elem().Type()) && isMessageType(vy.Elem().Type())
return isMessageType(addrType(vx.Elem().Type())) && isMessageType(addrType(vy.Elem().Type()))
}
return false
}, cmp.Transformer("protocmp.Transform", func(v interface{}) Message {
// For user convenience, shallow copy the message value if necessary
// in order for it to implement the message interface.
if rv := reflect.ValueOf(v); rv.IsValid() && rv.Kind() != reflect.Ptr && !isMessageType(rv.Type()) {
pv := reflect.New(rv.Type())
pv.Elem().Set(rv)
v = pv.Interface()
}
m := protoimpl.X.MessageOf(v)
switch {
case m == nil: