go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/testing/assertions/proto_tests.go (about) 1 // Copyright 2018 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package assertions 16 17 import ( 18 "fmt" 19 "reflect" 20 21 "google.golang.org/protobuf/encoding/protojson" 22 "google.golang.org/protobuf/encoding/prototext" 23 "google.golang.org/protobuf/proto" 24 "google.golang.org/protobuf/testing/protocmp" 25 26 protoLegacy "github.com/golang/protobuf/proto" 27 28 "github.com/google/go-cmp/cmp" 29 ) 30 31 // ShouldResembleProto determines if two values are deeply equal, using 32 // the following rules: 33 // - proto equality is defined by proto.Equal, 34 // - if a type has an .Equal method, equality is defined by 35 // that method, 36 // - in the absence of the above, deep equality is defined 37 // by recursing over structs, maps and slices in the usual way. 38 // 39 // See github.com/google/go-cmp/cmp#Equal for more details. 40 // 41 // This method is similar to goconvey's ShouldResemble, except that 42 // supports proto messages at the top-level or nested inside 43 // other types. 44 func ShouldResembleProto(actual any, expected ...any) string { 45 if len(expected) != 1 { 46 return fmt.Sprintf("ShouldResembleProto expects 1 value, got %d", len(expected)) 47 } 48 exp := expected[0] 49 50 // Compare all unexported fields. 51 exportAll := cmp.Exporter(func(t reflect.Type) bool { 52 return true 53 }) 54 diff := cmp.Diff(exp, actual, protocmp.Transform(), exportAll) 55 if diff != "" { 56 return fmt.Sprintf("Unexpected difference (-want +got):\n%s", diff) 57 } 58 59 return "" // Success 60 } 61 62 // ShouldResembleProtoText is like ShouldResembleProto, but expected 63 // is protobuf text. 64 // actual must be a message. A slice of messages is not supported. 65 func ShouldResembleProtoText(actual any, expected ...any) string { 66 return shouldResembleProtoUnmarshal( 67 func(s string, m proto.Message) error { 68 return prototext.Unmarshal([]byte(s), m) 69 }, 70 actual, 71 expected...) 72 } 73 74 // ShouldResembleProtoJSON is like ShouldResembleProto, but expected 75 // is protobuf text. 76 // actual must be a message. A slice of messages is not supported. 77 func ShouldResembleProtoJSON(actual any, expected ...any) string { 78 return shouldResembleProtoUnmarshal( 79 func(s string, m proto.Message) error { 80 return protojson.Unmarshal([]byte(s), m) 81 }, 82 actual, 83 expected...) 84 } 85 86 func shouldResembleProtoUnmarshal(unmarshal func(string, proto.Message) error, actual any, expected ...any) string { 87 if _, ok := protoMessage(actual); !ok { 88 return fmt.Sprintf("ShouldResembleProtoText expects a proto message, got %T", actual) 89 } 90 91 if len(expected) != 1 { 92 return fmt.Sprintf("ShouldResembleProtoText expects 1 value, got %d", len(expected)) 93 } 94 expText, ok := expected[0].(string) 95 if !ok { 96 return fmt.Sprintf("ShouldResembleProtoText expects a string value, got %T", expected[0]) 97 } 98 99 expMsg := reflect.New(reflect.TypeOf(actual).Elem()).Interface().(proto.Message) 100 101 if err := unmarshal(expText, expMsg); err != nil { 102 return err.Error() 103 } 104 return ShouldResembleProto(actual, expMsg) 105 } 106 107 var textPBMultiline = prototext.MarshalOptions{ 108 Multiline: true, 109 } 110 111 // protoMessage returns V2 proto message, converting v1 on the fly. 112 func protoMessage(a any) (proto.Message, bool) { 113 if m, ok := a.(proto.Message); ok { 114 return m, true 115 } 116 if m, ok := a.(protoLegacy.Message); ok { 117 return protoLegacy.MessageV2(m), true 118 } 119 return nil, false 120 }