From f1f450f1523528b7cb7b58296de8e0ea234db440 Mon Sep 17 00:00:00 2001 From: ericwenn Date: Thu, 4 Feb 2021 17:17:38 +0100 Subject: [PATCH] fix: handle composite in fieldmask Fixes issues with nil composite types in either dst or src. Also expands on the test coverage using the syntaxv1 proto message. --- fieldmask/update.go | 50 ++- fieldmask/update_test.go | 882 +++++++++++++++++++++++++++++---------- 2 files changed, 706 insertions(+), 226 deletions(-) diff --git a/fieldmask/update.go b/fieldmask/update.go index fc7691737d..25553c5e09 100644 --- a/fieldmask/update.go +++ b/fieldmask/update.go @@ -12,6 +12,8 @@ import ( // Update updates fields in dst with values from src according to the provided field mask. // Nested messages are recursively updated in the same manner. // Repeated fields and maps are copied by reference from src to dst. +// Field mask paths referring to Individual entries in maps or +// repeated fields are ignored. // // If no update mask is provided, only non-zero values of src are copied to dst. // If the special value "*" is provided as the field mask, a full replacement of all fields in dst is done. @@ -47,11 +49,18 @@ func Update(mask *fieldmaskpb.FieldMask, dst, src proto.Message) { func updateWireSetFields(dst, src protoreflect.Message) { src.Range(func(field protoreflect.FieldDescriptor, value protoreflect.Value) bool { - if isMessage(field) { + switch { + case field.IsList(): + dst.Set(field, value) + case field.IsMap(): + dst.Set(field, value) + case field.Message() != nil && !dst.Has(field): + dst.Set(field, value) + case field.Message() != nil: updateWireSetFields(dst.Get(field).Message(), value.Message()) - return true + default: + dst.Set(field, value) } - dst.Set(field, value) return true }) } @@ -62,21 +71,34 @@ func updateNamedField(dst, src protoreflect.Message, segments []string) { } field := src.Descriptor().Fields().ByName(protoreflect.Name(segments[0])) if field == nil { - return // no known field by that name + // no known field by that name + return } - // a field in this message + // a named field in this message if len(segments) == 1 { - dst.Set(field, src.Get(field)) + if !src.Has(field) { + dst.Set(field, src.NewField(field)) + } else { + dst.Set(field, src.Get(field)) + } return } - if !isMessage(field) { - // not a message so can not have a field with that name + + // a named field in a nested message + switch { + case field.IsList(), field.IsMap(): + // nested fields in repeated or map not supported + return + case field.Message() != nil: + // if message field is not set, allocate an empty value + if !dst.Has(field) { + dst.Set(field, dst.NewField(field)) + } + if !src.Has(field) { + src.Set(field, src.NewField(field)) + } + updateNamedField(dst.Get(field).Message(), src.Get(field).Message(), segments[1:]) + default: return } - updateNamedField(dst.Get(field).Message(), src.Get(field).Message(), segments[1:]) -} - -func isMessage(field protoreflect.FieldDescriptor) bool { - return (field.Kind() == protoreflect.MessageKind || field.Kind() == protoreflect.GroupKind) && - !field.IsMap() && !field.IsList() } diff --git a/fieldmask/update_test.go b/fieldmask/update_test.go index 3bf6fe150c..e65937ef4a 100644 --- a/fieldmask/update_test.go +++ b/fieldmask/update_test.go @@ -3,6 +3,7 @@ package fieldmask import ( "testing" + syntaxv1 "go.einride.tech/aip/examples/proto/gen/einride/example/syntax/v1" "google.golang.org/genproto/googleapis/example/library/v1" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/testing/protocmp" @@ -20,301 +21,758 @@ func TestUpdate(t *testing.T) { })) }) - t.Run("test cases", func(t *testing.T) { + t.Run("full replacement", func(t *testing.T) { + t.Parallel() + for _, tt := range []struct { + name string + src proto.Message + dst proto.Message + }{ + { + name: "scalars", + src: &syntaxv1.Message{ + Double: 111, + Float: 111, + Bool: true, + String_: "111", + Bytes: []byte{111}, + }, + dst: &syntaxv1.Message{ + Double: 222, + Float: 222, + Bool: false, + String_: "222", + Bytes: []byte{222}, + }, + }, + { + name: "repeated", + src: &syntaxv1.Message{ + RepeatedDouble: []float64{111}, + RepeatedFloat: []float32{111}, + RepeatedBool: []bool{true}, + RepeatedString: []string{"111"}, + RepeatedBytes: [][]byte{{111}}, + }, + dst: &syntaxv1.Message{ + RepeatedDouble: []float64{222}, + RepeatedFloat: []float32{222}, + RepeatedBool: []bool{false}, + RepeatedString: []string{"222"}, + RepeatedBytes: [][]byte{{222}}, + }, + }, + { + name: "nested", + src: &syntaxv1.Message{ + Message: &syntaxv1.Message{ + String_: "src", + }, + }, + dst: &syntaxv1.Message{ + Message: &syntaxv1.Message{ + String_: "dst", + Int64: 222, + }, + }, + }, + { + name: "maps", + src: &syntaxv1.Message{ + MapStringString: map[string]string{ + "src-key": "src-value", + }, + MapStringMessage: map[string]*syntaxv1.Message{ + "src-key": { + String_: "src-value", + }, + }, + }, + dst: &syntaxv1.Message{ + MapStringString: map[string]string{ + "dst-key": "dst-value", + }, + MapStringMessage: map[string]*syntaxv1.Message{ + "dst-key": { + String_: "dst-value", + }, + }, + }, + }, + { + name: "oneof: swap", + src: &syntaxv1.Message{ + Oneof: &syntaxv1.Message_OneofString{ + OneofString: "src", + }, + }, + dst: &syntaxv1.Message{ + Oneof: &syntaxv1.Message_OneofMessage2{ + OneofMessage2: &syntaxv1.Message{ + String_: "dst", + }, + }, + }, + }, + { + name: "oneof: message swap", + src: &syntaxv1.Message{ + Oneof: &syntaxv1.Message_OneofMessage1{ + OneofMessage1: &syntaxv1.Message{ + String_: "src", + }, + }, + }, + dst: &syntaxv1.Message{ + Oneof: &syntaxv1.Message_OneofMessage2{ + OneofMessage2: &syntaxv1.Message{ + String_: "dst", + }, + }, + }, + }, + } { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + srcClone := proto.Clone(tt.src) + Update(&fieldmaskpb.FieldMask{Paths: []string{"*"}}, tt.dst, tt.src) + assert.DeepEqual(t, srcClone, tt.dst, protocmp.Transform()) + }) + } + }) + t.Run("wire set fields", func(t *testing.T) { t.Parallel() for _, tt := range []struct { name string - mask *fieldmaskpb.FieldMask src proto.Message dst proto.Message expected proto.Message }{ { - name: "full replacement", - mask: &fieldmaskpb.FieldMask{ - Paths: []string{"*"}, + name: "scalars", + src: &syntaxv1.Message{ + Double: 111, + Float: 111, + }, + dst: &syntaxv1.Message{ + Double: 222, + Float: 222, + Bool: false, + String_: "222", + Bytes: []byte{222}, + }, + expected: &syntaxv1.Message{ + Double: 111, + Float: 111, + Bool: false, + String_: "222", + Bytes: []byte{222}, }, - src: &library.Book{ - Name: "src-name", - Author: "src-author", - Title: "src-title", - Read: false, + }, + { + name: "repeated", + src: &syntaxv1.Message{ + RepeatedDouble: []float64{111}, + }, + dst: &syntaxv1.Message{ + RepeatedDouble: []float64{222}, + RepeatedFloat: []float32{222}, + RepeatedBool: []bool{false}, + RepeatedString: []string{"222"}, + RepeatedBytes: [][]byte{{222}}, + }, + expected: &syntaxv1.Message{ + RepeatedDouble: []float64{111}, + RepeatedFloat: []float32{222}, + RepeatedBool: []bool{false}, + RepeatedString: []string{"222"}, + RepeatedBytes: [][]byte{{222}}, }, - dst: &library.Book{ - Name: "dst-name", - Author: "dst-author", - Title: "dst-title", - Read: true, + }, + { + name: "nested", + src: &syntaxv1.Message{ + Message: &syntaxv1.Message{String_: "src"}, + }, + dst: &syntaxv1.Message{ + Message: &syntaxv1.Message{ + String_: "dst", + Int64: 222, + }, }, - expected: &library.Book{ - Name: "src-name", - Author: "src-author", - Title: "src-title", - Read: false, + expected: &syntaxv1.Message{ + Message: &syntaxv1.Message{ + String_: "src", + Int64: 222, + }, }, }, { - name: "full replacement: nested", - mask: &fieldmaskpb.FieldMask{ - Paths: []string{"*"}, + name: "nested: dst nil", + src: &syntaxv1.Message{ + Message: &syntaxv1.Message{String_: "src"}, + }, + dst: &syntaxv1.Message{ + Message: nil, + }, + expected: &syntaxv1.Message{ + Message: &syntaxv1.Message{String_: "src"}, }, - src: &library.CreateBookRequest{ - Name: "src-name", - Book: &library.Book{ - Author: "src-author", - Read: false, + }, + { + name: "maps", + src: &syntaxv1.Message{ + MapStringString: map[string]string{ + "src-key": "src-value", + }, + MapStringMessage: map[string]*syntaxv1.Message{ + "src-key": {String_: "src-value"}, }, }, - dst: &library.CreateBookRequest{ - Name: "dst-name", - Book: &library.Book{ - Author: "dst-author", - Title: "dst-title", - Read: true, + dst: &syntaxv1.Message{ + MapStringString: map[string]string{ + "dst-key": "dst-value", + }, + MapStringMessage: map[string]*syntaxv1.Message{ + "dst-key": {String_: "dst-value"}, }, }, - expected: &library.CreateBookRequest{ - Name: "src-name", - Book: &library.Book{ - Author: "src-author", - Read: false, + expected: &syntaxv1.Message{ + MapStringString: map[string]string{ + "src-key": "src-value", + }, + MapStringMessage: map[string]*syntaxv1.Message{ + "src-key": {String_: "src-value"}, }, }, }, - { - name: "no field mask replaces non-zero fields", - mask: nil, - src: &library.Book{ - Name: "src-name", - Author: "src-author", + name: "maps: dst nil", + src: &syntaxv1.Message{ + MapStringString: map[string]string{ + "src-key": "src-value", + }, + MapStringMessage: map[string]*syntaxv1.Message{ + "src-key": {String_: "src-value"}, + }, }, - dst: &library.Book{ - Name: "dst-name", - Author: "dst-author", - Title: "dst-title", - Read: true, + dst: &syntaxv1.Message{ + MapStringString: nil, + MapStringMessage: nil, }, - expected: &library.Book{ - Name: "src-name", - Author: "src-author", - Title: "dst-title", - Read: true, + expected: &syntaxv1.Message{ + MapStringString: map[string]string{ + "src-key": "src-value", + }, + MapStringMessage: map[string]*syntaxv1.Message{ + "src-key": {String_: "src-value"}, + }, }, }, { - name: "no field mask replaces non-zero fields: nested", - mask: nil, - src: &library.CreateBookRequest{ - Book: &library.Book{ - Name: "src-name", - Author: "src-author", + name: "oneof", + src: &syntaxv1.Message{ + Oneof: &syntaxv1.Message_OneofMessage1{ + OneofMessage1: &syntaxv1.Message{String_: "src"}, }, }, - dst: &library.CreateBookRequest{ - Book: &library.Book{ - Name: "dst-name", - Author: "dst-author", - Title: "dst-title", - Read: true, + dst: &syntaxv1.Message{ + Oneof: &syntaxv1.Message_OneofMessage1{ + OneofMessage1: &syntaxv1.Message{ + String_: "dst", + Int64: 222, + }, }, }, - expected: &library.CreateBookRequest{ - Book: &library.Book{ - Name: "src-name", - Author: "src-author", - Title: "dst-title", - Read: true, + expected: &syntaxv1.Message{ + Oneof: &syntaxv1.Message_OneofMessage1{ + OneofMessage1: &syntaxv1.Message{ + String_: "src", + Int64: 222, + }, }, }, }, - { - name: "with partial field mask", - mask: &fieldmaskpb.FieldMask{ - Paths: []string{"name", "author", "read"}, + name: "oneof: kind swap", + src: &syntaxv1.Message{ + Oneof: &syntaxv1.Message_OneofString{ + OneofString: "src", + }, }, - src: &library.Book{ - Name: "src-name", - Author: "src-author", - Title: "src-title", - Read: false, + dst: &syntaxv1.Message{ + Oneof: &syntaxv1.Message_OneofMessage2{ + OneofMessage2: &syntaxv1.Message{ + String_: "dst", + }, + }, + }, + expected: &syntaxv1.Message{ + Oneof: &syntaxv1.Message_OneofString{ + OneofString: "src", + }, }, - dst: &library.Book{ - Name: "dst-name", - Author: "dst-author", - Title: "dst-title", - Read: true, + }, + { + name: "oneof: message swap", + src: &syntaxv1.Message{ + Oneof: &syntaxv1.Message_OneofMessage1{ + OneofMessage1: &syntaxv1.Message{ + String_: "src", + }, + }, + }, + dst: &syntaxv1.Message{ + Oneof: &syntaxv1.Message_OneofMessage2{ + OneofMessage2: &syntaxv1.Message{ + String_: "dst", + }, + }, + }, + expected: &syntaxv1.Message{ + Oneof: &syntaxv1.Message_OneofMessage1{ + OneofMessage1: &syntaxv1.Message{ + String_: "src", + }, + }, }, - expected: &library.Book{ - Name: "src-name", - Author: "src-author", - Title: "dst-title", - Read: false, + }, + } { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + Update(nil, tt.dst, tt.src) + assert.DeepEqual(t, tt.expected, tt.dst, protocmp.Transform()) + }) + } + }) + t.Run("paths", func(t *testing.T) { + t.Parallel() + for _, tt := range []struct { + name string + paths []string + src proto.Message + dst proto.Message + expected proto.Message + }{ + { + name: "scalars", + paths: []string{ + "double", + "bytes", + }, + src: &syntaxv1.Message{ + Double: 111, + Float: 111, + Bytes: []byte{111}, + }, + dst: &syntaxv1.Message{ + Double: 222, + Float: 222, + Bool: false, + String_: "222", + Bytes: []byte{222}, + }, + expected: &syntaxv1.Message{ + Double: 111, + Float: 222, + Bytes: []byte{111}, + Bool: false, + String_: "222", }, }, - { - name: "with non-top-level path", - mask: &fieldmaskpb.FieldMask{ - Paths: []string{"name", "book.name"}, + name: "repeated scalar", + paths: []string{ + "repeated_double", + "repeated_string", + }, + src: &syntaxv1.Message{ + RepeatedDouble: []float64{111}, + RepeatedFloat: []float32{111}, + }, + dst: &syntaxv1.Message{ + RepeatedDouble: []float64{222}, + RepeatedString: []string{"222"}, + RepeatedBytes: [][]byte{{222}}, }, - src: &library.CreateBookRequest{ - Name: "src-shelf", - Book: &library.Book{ - Name: "src-name", - Author: "src-author", - Title: "src-title", - Read: false, + expected: &syntaxv1.Message{ + RepeatedDouble: []float64{111}, + RepeatedBytes: [][]byte{{222}}, + }, + }, + { + name: "repeated message", + paths: []string{ + "repeated_message", + }, + src: &syntaxv1.Message{ + RepeatedMessage: []*syntaxv1.Message{ + {String_: "src"}, + {Int64: 111}, }, }, - dst: &library.CreateBookRequest{ - Name: "dst-shelf", - Book: &library.Book{ - Name: "dst-name", - Author: "dst-author", - Title: "dst-title", - Read: true, + dst: &syntaxv1.Message{ + RepeatedMessage: []*syntaxv1.Message{ + {Int64: 222}, + {String_: "dst"}, }, }, - expected: &library.CreateBookRequest{ - Name: "src-shelf", - Book: &library.Book{ - Name: "src-name", - Author: "dst-author", - Title: "dst-title", - Read: true, + expected: &syntaxv1.Message{ + RepeatedMessage: []*syntaxv1.Message{ + {String_: "src"}, + {Int64: 111}, }, }, }, - { - name: "repeated field", - mask: &fieldmaskpb.FieldMask{ - Paths: []string{"books"}, - }, - src: &library.ListBooksResponse{ - NextPageToken: "xxx", - Books: []*library.Book{ - { - Name: "src-name", - Author: "src-author", - Title: "src-title", - Read: false, - }, - { - Name: "src-name-2", - Author: "src-author-2", - Title: "src-title-2", - Read: false, + // can not update individual fields in a repeated message + name: "repeated message: deep", + paths: []string{ + "repeated_message.string", + }, + src: &syntaxv1.Message{ + RepeatedMessage: []*syntaxv1.Message{ + {String_: "src"}, + {Int64: 111}, + }, + }, + dst: &syntaxv1.Message{ + RepeatedMessage: []*syntaxv1.Message{ + {Int64: 222}, + {String_: "dst"}, + }, + }, + expected: &syntaxv1.Message{ + RepeatedMessage: []*syntaxv1.Message{ + {Int64: 222}, + {String_: "dst"}, + }, + }, + }, + { + name: "nested", + paths: []string{ + "message", + }, + src: &syntaxv1.Message{ + Message: &syntaxv1.Message{ + String_: "src", + }, + }, + dst: &syntaxv1.Message{ + Message: &syntaxv1.Message{ + String_: "dst", + Int64: 222, + }, + }, + expected: &syntaxv1.Message{ + Message: &syntaxv1.Message{ + String_: "src", + }, + }, + }, + { + name: "nested: deep", + paths: []string{ + "message.string", + }, + src: &syntaxv1.Message{ + Message: &syntaxv1.Message{ + String_: "src", + }, + }, + dst: &syntaxv1.Message{ + Message: &syntaxv1.Message{ + String_: "dst", + Int64: 222, + }, + }, + expected: &syntaxv1.Message{ + Message: &syntaxv1.Message{ + String_: "src", + Int64: 222, + }, + }, + }, + { + name: "nested: dst nil", + paths: []string{ + "message", + }, + src: &syntaxv1.Message{ + Message: &syntaxv1.Message{ + String_: "src", + }, + }, + dst: &syntaxv1.Message{ + Message: nil, + }, + expected: &syntaxv1.Message{ + Message: &syntaxv1.Message{ + String_: "src", + }, + }, + }, + { + name: "nested: deep, dst nil", + paths: []string{ + "message.string", + }, + src: &syntaxv1.Message{ + Message: &syntaxv1.Message{ + String_: "src", + }, + }, + dst: &syntaxv1.Message{ + Message: nil, + }, + expected: &syntaxv1.Message{ + Message: &syntaxv1.Message{ + String_: "src", + }, + }, + }, + { + name: "nested: deep, src nil", + paths: []string{ + "message.string", + }, + src: &syntaxv1.Message{ + Message: nil, + }, + dst: &syntaxv1.Message{ + Message: &syntaxv1.Message{ + String_: "src", + }, + }, + expected: &syntaxv1.Message{ + Message: &syntaxv1.Message{}, + }, + }, + { + name: "maps", + paths: []string{ + "map_string_string", + }, + src: &syntaxv1.Message{ + MapStringString: map[string]string{ + "src-key": "src-value", + }, + MapStringMessage: map[string]*syntaxv1.Message{ + "src-key": {String_: "src-value"}, + }, + }, + dst: &syntaxv1.Message{ + MapStringString: map[string]string{ + "dst-key": "dst-value", + }, + MapStringMessage: map[string]*syntaxv1.Message{ + "dst-key": {String_: "dst-value"}, + }, + }, + expected: &syntaxv1.Message{ + MapStringString: map[string]string{ + "src-key": "src-value", + }, + MapStringMessage: map[string]*syntaxv1.Message{ + "dst-key": {String_: "dst-value"}, + }, + }, + }, + { + // can not update individual entries in a map + name: "maps: deep", + paths: []string{ + "map_string_string.src1", + }, + src: &syntaxv1.Message{ + MapStringString: map[string]string{ + "src1": "src1-value", + "src2": "src2-value", + }, + }, + dst: &syntaxv1.Message{ + MapStringString: map[string]string{ + "dst-key": "dst-value", + }, + }, + expected: &syntaxv1.Message{ + MapStringString: map[string]string{ + "dst-key": "dst-value", + }, + }, + }, + { + name: "maps: dst nil", + paths: []string{ + "map_string_string", + }, + src: &syntaxv1.Message{ + MapStringString: map[string]string{ + "src-key": "src-value", + }, + MapStringMessage: map[string]*syntaxv1.Message{ + "src-key": {String_: "src-value"}, + }, + }, + dst: &syntaxv1.Message{}, + expected: &syntaxv1.Message{ + MapStringString: map[string]string{ + "src-key": "src-value", + }, + }, + }, + { + name: "maps: src nil", + paths: []string{ + "map_string_string", + }, + src: &syntaxv1.Message{}, + dst: &syntaxv1.Message{ + MapStringString: map[string]string{ + "dst-key": "dst-value", + }, + MapStringMessage: map[string]*syntaxv1.Message{ + "dst-key": {String_: "dst-value"}, + }, + }, + expected: &syntaxv1.Message{ + MapStringMessage: map[string]*syntaxv1.Message{ + "dst-key": {String_: "dst-value"}, + }, + }, + }, + { + name: "oneof", + paths: []string{ + "oneof_message1", + }, + src: &syntaxv1.Message{ + Oneof: &syntaxv1.Message_OneofMessage1{ + OneofMessage1: &syntaxv1.Message{ + String_: "src", }, }, }, - dst: &library.ListBooksResponse{ - NextPageToken: "yyy", - Books: []*library.Book{ - { - Name: "dst-name", - Author: "dst-author", - Title: "dst-title", - Read: false, + dst: &syntaxv1.Message{ + Oneof: &syntaxv1.Message_OneofMessage1{ + OneofMessage1: &syntaxv1.Message{ + String_: "dst", + Int64: 222, }, }, }, - expected: &library.ListBooksResponse{ - NextPageToken: "yyy", - Books: []*library.Book{ - { - Name: "src-name", - Author: "src-author", - Title: "src-title", - Read: false, + expected: &syntaxv1.Message{ + Oneof: &syntaxv1.Message_OneofMessage1{ + OneofMessage1: &syntaxv1.Message{ + String_: "src", }, - { - Name: "src-name-2", - Author: "src-author-2", - Title: "src-title-2", - Read: false, + }, + }, + }, + { + name: "oneof: kind swap", + paths: []string{ + "oneof_string", + }, + src: &syntaxv1.Message{ + Oneof: &syntaxv1.Message_OneofString{ + OneofString: "src", + }, + }, + dst: &syntaxv1.Message{ + Oneof: &syntaxv1.Message_OneofMessage2{ + OneofMessage2: &syntaxv1.Message{ + String_: "dst", }, }, }, + expected: &syntaxv1.Message{ + Oneof: &syntaxv1.Message_OneofString{ + OneofString: "src", + }, + }, }, - { - name: "repeated field nested path", - mask: &fieldmaskpb.FieldMask{ - Paths: []string{"books.name"}, - }, - src: &library.ListBooksResponse{ - NextPageToken: "xxx", - Books: []*library.Book{ - { - Name: "src-name", - Author: "src-author", - Title: "src-title", - Read: false, + name: "oneof: kind swap src nil", + paths: []string{ + "oneof_message2", + }, + src: &syntaxv1.Message{ + Oneof: &syntaxv1.Message_OneofString{ + OneofString: "src", + }, + }, + dst: &syntaxv1.Message{ + Oneof: &syntaxv1.Message_OneofMessage2{ + OneofMessage2: &syntaxv1.Message{ + String_: "dst", }, - { - Name: "src-name-2", - Author: "src-author-2", - Title: "src-title-2", - Read: false, + }, + }, + expected: &syntaxv1.Message{ + Oneof: &syntaxv1.Message_OneofMessage2{}, + }, + }, + { + name: "oneof: deep", + paths: []string{ + "oneof_message1.string", + }, + src: &syntaxv1.Message{ + Oneof: &syntaxv1.Message_OneofMessage1{ + OneofMessage1: &syntaxv1.Message{ + String_: "src", }, }, }, - dst: &library.ListBooksResponse{ - NextPageToken: "yyy", - Books: []*library.Book{ - { - Name: "dst-name", - Author: "dst-author", - Title: "dst-title", - Read: false, + dst: &syntaxv1.Message{ + Oneof: &syntaxv1.Message_OneofMessage2{ + OneofMessage2: &syntaxv1.Message{ + String_: "dst", }, }, }, - expected: &library.ListBooksResponse{ - NextPageToken: "yyy", - Books: []*library.Book{ - { - Name: "dst-name", - Author: "dst-author", - Title: "dst-title", - Read: false, + expected: &syntaxv1.Message{ + Oneof: &syntaxv1.Message_OneofMessage1{ + OneofMessage1: &syntaxv1.Message{ + String_: "src", }, }, }, }, - { - name: "with unknown path", - mask: &fieldmaskpb.FieldMask{ - Paths: []string{"name", "foo"}, - }, - src: &library.Book{ - Name: "src-name", - Author: "src-author", - Title: "src-title", - Read: false, + name: "oneof: deep src nil", + paths: []string{ + "oneof_message2.string", + }, + src: &syntaxv1.Message{ + Oneof: &syntaxv1.Message_OneofMessage1{ + OneofMessage1: &syntaxv1.Message{ + String_: "src", + }, + }, }, - dst: &library.Book{ - Name: "dst-name", - Author: "dst-author", - Title: "dst-title", - Read: true, + dst: &syntaxv1.Message{ + Oneof: &syntaxv1.Message_OneofMessage2{ + OneofMessage2: &syntaxv1.Message{ + String_: "dst", + }, + }, }, - expected: &library.Book{ - Name: "src-name", - Author: "dst-author", - Title: "dst-title", - Read: true, + expected: &syntaxv1.Message{ + Oneof: &syntaxv1.Message_OneofMessage2{ + OneofMessage2: &syntaxv1.Message{}, + }, }, }, } { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - Update(tt.mask, tt.dst, tt.src) + Update(&fieldmaskpb.FieldMask{Paths: tt.paths}, tt.dst, tt.src) assert.DeepEqual(t, tt.expected, tt.dst, protocmp.Transform()) }) }