go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/cvtesting/saferesemble_test.go (about) 1 // Copyright 2021 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 cvtesting 16 17 import ( 18 "fmt" 19 "reflect" 20 "strings" 21 "testing" 22 "time" 23 24 "google.golang.org/protobuf/proto" 25 "google.golang.org/protobuf/types/known/durationpb" 26 "google.golang.org/protobuf/types/known/timestamppb" 27 28 "go.chromium.org/luci/common/clock/testclock" 29 "go.chromium.org/luci/common/testing/assertions" 30 31 "go.chromium.org/luci/cv/internal/cvtesting/saferesembletest" 32 33 . "github.com/smartystreets/goconvey/convey" 34 ) 35 36 func TestSafeShouldResemble(t *testing.T) { 37 t.Parallel() 38 39 // pbShuffle returns unmarshal(marshal(t)). 40 // 41 // This exists to test the case on which the original Go Convey's 42 // ShouldResemble (and its friends) hang, e.g. the following hangs: 43 // ShouldResemble(t, pbShuffle(t)) 44 pbShuffle := func(t proto.Message) proto.Message { 45 out, err := proto.Marshal(t) 46 if err != nil { 47 panic(err) 48 } 49 ret := reflect.New(reflect.TypeOf(t).Elem()).Interface().(proto.Message) 50 if err = proto.Unmarshal(out, ret); err != nil { 51 panic(err) 52 } 53 return ret 54 } 55 56 var ( 57 ts66 = ×tamppb.Timestamp{Seconds: 6, Nanos: 6} 58 ts53 = ×tamppb.Timestamp{Seconds: 5, Nanos: 3} 59 ts53duped = ×tamppb.Timestamp{Seconds: 5, Nanos: 3} 60 ts53decoded = pbShuffle(ts53).(*timestamppb.Timestamp) 61 62 d22 = &durationpb.Duration{Seconds: 2, Nanos: 2} 63 d43 = &durationpb.Duration{Seconds: 5, Nanos: 3} 64 d43cloned = proto.Clone(d43).(*durationpb.Duration) 65 d43decoded = pbShuffle(d43).(*durationpb.Duration) 66 ) 67 68 cases := []struct { 69 name string 70 a, e any 71 diffsContains []string 72 shouldPanicLike string 73 }{ 74 // Nil special cases. 75 { 76 name: "nils are equal", 77 a: (*tNoProtos)(nil), 78 e: (*tNoProtos)(nil), 79 }, 80 { 81 name: "nil vs non-nil", 82 a: (*tNoProtos)(nil), 83 e: &tNoProtos{}, 84 diffsContains: []string{"actual is nil, but not nil is expected"}, 85 }, 86 { 87 name: "non-nil vs nil", 88 a: &tNoProtos{}, 89 e: (*tNoProtos)(nil), 90 diffsContains: []string{"actual is not nil, but nil is expected"}, 91 }, 92 93 // Equality of zero values. 94 { 95 name: "equal zero value without protos", 96 a: tNoProtos{}, 97 e: tNoProtos{}, 98 }, 99 { 100 name: "equal zero value with protos", 101 a: tWith2Protos{}, 102 e: tWith2Protos{}, 103 }, 104 { 105 name: "equal ptr to zero value without protos", 106 a: &tNoProtos{}, 107 e: &tNoProtos{}, 108 }, 109 { 110 name: "equal ptr to zero value with protos", 111 a: &tWith2Protos{}, 112 e: &tWith2Protos{}, 113 }, 114 115 // Equality with populated fields. 116 { 117 name: "equal without protos", 118 a: tNoProtos{a: 1, A: testclock.TestRecentTimeUTC}, 119 e: tNoProtos{a: 1, A: testclock.TestRecentTimeUTC}, 120 }, 121 { 122 name: "equal with proto as same ptr", 123 a: tWithProto{a: 1, PB: ts53}, 124 e: tWithProto{a: 1, PB: ts53}, 125 }, 126 { 127 name: "equal with proto same value", 128 a: tWithProto{a: 1, PB: ts53}, 129 e: tWithProto{a: 1, PB: ts53duped}, 130 }, 131 { 132 name: "equal with proto with IO", 133 a: tWithProto{a: 1, PB: ts53}, 134 e: tWithProto{a: 1, PB: ts53decoded}, 135 }, 136 { 137 name: "equal with proto cloned", 138 a: tWith2Protos{A: 1, b: "b", T: ts53, D: d43}, 139 e: tWith2Protos{A: 1, b: "b", T: ts53, D: d43cloned}, 140 }, 141 { 142 name: "equal with private fields and no protos", 143 a: saferesembletest.NewNoProtos(1, testclock.TestRecentTimeUTC), 144 e: saferesembletest.NewNoProtos(1, testclock.TestRecentTimeUTC), 145 }, 146 { 147 name: "equal with private fields and private protos", 148 a: saferesembletest.NewWithPrivateProto(1, "b", ts53), 149 e: saferesembletest.NewWithPrivateProto(1, "b", ts53decoded), 150 }, 151 { 152 name: "equal with inner struct fields and no protos", 153 a: saferesembletest.NewWithInnerStructNoProto(2, "y", saferesembletest.NewNoProtos(1, testclock.TestRecentTimeUTC)), 154 e: saferesembletest.NewWithInnerStructNoProto(2, "y", saferesembletest.NewNoProtos(1, testclock.TestRecentTimeUTC)), 155 }, 156 { 157 name: "equal with inner struct fields and has proto", 158 a: saferesembletest.NewWithInnerStructHasProto(2, "y", saferesembletest.NewWithPrivateProto(1, "b", ts53)), 159 e: saferesembletest.NewWithInnerStructHasProto(2, "y", saferesembletest.NewWithPrivateProto(1, "b", ts53decoded)), 160 }, 161 { 162 name: "equal with private inner struct fields and no protos", 163 a: saferesembletest.NewWithPrivateInnerStructNoProto(2, "y", saferesembletest.NewNoProtos(1, testclock.TestRecentTimeUTC)), 164 e: saferesembletest.NewWithPrivateInnerStructNoProto(2, "y", saferesembletest.NewNoProtos(1, testclock.TestRecentTimeUTC)), 165 }, 166 { 167 name: "equal with private inner struct fields and has proto", 168 a: saferesembletest.NewWithPrivateStructHasProto(2, "y", saferesembletest.NewWithPrivateProto(1, "b", ts53)), 169 e: saferesembletest.NewWithPrivateStructHasProto(2, "y", saferesembletest.NewWithPrivateProto(1, "b", ts53decoded)), 170 }, 171 { 172 name: "equal with inner struct slice field and no protos", 173 a: saferesembletest.NewWithInnerStructSliceNoProto(2, "y", []saferesembletest.NoProtos{ 174 saferesembletest.NewNoProtos(1, testclock.TestRecentTimeUTC), 175 }), 176 e: saferesembletest.NewWithInnerStructSliceNoProto(2, "y", []saferesembletest.NoProtos{ 177 saferesembletest.NewNoProtos(1, testclock.TestRecentTimeUTC), 178 }), 179 }, 180 { 181 name: "equal with inner struct slice field and has proto", 182 a: saferesembletest.NewWithInnerStructSliceHasProto(2, "y", []saferesembletest.WithPrivateProto{ 183 saferesembletest.NewWithPrivateProto(1, "b", ts53), 184 }), 185 e: saferesembletest.NewWithInnerStructSliceHasProto(2, "y", []saferesembletest.WithPrivateProto{ 186 saferesembletest.NewWithPrivateProto(1, "b", ts53decoded), 187 }), 188 }, 189 { 190 name: "equal with private inner struct slice fields and no protos", 191 a: saferesembletest.NewWithPrivateInnerStructSliceNoProto(2, "y", []saferesembletest.NoProtos{ 192 saferesembletest.NewNoProtos(1, testclock.TestRecentTimeUTC), 193 }), 194 e: saferesembletest.NewWithPrivateInnerStructSliceNoProto(2, "y", []saferesembletest.NoProtos{ 195 saferesembletest.NewNoProtos(1, testclock.TestRecentTimeUTC), 196 }), 197 }, 198 { 199 name: "equal with private inner struct fields slice and has proto", 200 a: saferesembletest.NewWithPrivateInnerStructSliceHasProto(2, "y", []saferesembletest.WithPrivateProto{ 201 saferesembletest.NewWithPrivateProto(1, "b", ts53), 202 }), 203 e: saferesembletest.NewWithPrivateInnerStructSliceHasProto(2, "y", []saferesembletest.WithPrivateProto{ 204 saferesembletest.NewWithPrivateProto(1, "b", ts53decoded), 205 }), 206 }, 207 208 // Diff. 209 { 210 name: "diff without no protos", 211 a: &tNoProtos{a: 1}, 212 e: &tNoProtos{a: 2}, 213 diffsContains: []string{"non-proto fields differ:", "(Should equal)!"}, 214 }, 215 { 216 name: "diff, but not in protos", 217 a: &tWith2Protos{A: 123, T: ts53, D: d43}, 218 e: &tWith2Protos{b: "B", T: ts53, D: d43decoded}, 219 diffsContains: []string{"non-proto fields differ:", "123", "B"}, 220 }, 221 { 222 name: "diff only in 1 proto", 223 a: &tWith2Protos{b: "B", T: ts53, D: d43}, 224 e: &tWith2Protos{b: "B", T: ts53, D: d22}, 225 diffsContains: []string{"field .D differs:"}, 226 }, 227 { 228 name: "diff only in 2 protos", 229 a: &tWith2Protos{b: "B", T: ts66, D: d43}, 230 e: &tWith2Protos{b: "B", T: ts53, D: d22}, 231 diffsContains: []string{"field .T differs:", "field .D differs:"}, 232 }, 233 { 234 name: "diff everywhere", 235 a: &tWith2Protos{A: 111, b: "bbb", T: ts66, D: d43}, 236 e: &tWith2Protos{A: 222, b: "BBB", T: ts53, D: d22}, 237 diffsContains: []string{"111", "BBB", "field .T differs:", "field .D differs:"}, 238 }, 239 { 240 name: "diff everywhere with private fields & protos", 241 a: saferesembletest.NewWithPrivateProto(111, "bbb", ts53), 242 e: saferesembletest.NewWithPrivateProto(222, "BBB", ts66), 243 diffsContains: []string{"111", "BBB", "field .t differs:", "non-proto fields differ:"}, 244 }, 245 { 246 name: "diff in 1 proto element of a slice", 247 a: &tWithProtoSlice{TS: []*timestamppb.Timestamp{ts53duped, ts53}}, 248 e: &tWithProtoSlice{TS: []*timestamppb.Timestamp{ts53decoded, ts66}}, 249 diffsContains: []string{"field .TS differs:"}, 250 }, 251 { 252 name: "diff inner struct no protos", 253 a: saferesembletest.NewWithInnerStructNoProto(2, "y", saferesembletest.NewNoProtos(1, testclock.TestRecentTimeUTC)), 254 e: saferesembletest.NewWithInnerStructNoProto(2, "y", saferesembletest.NewNoProtos(1, testclock.TestRecentTimeUTC.Add(1*time.Second))), 255 diffsContains: []string{"non-proto fields differ:", "(Should equal)!"}, 256 }, 257 { 258 name: "diff inner struct has proto", 259 a: saferesembletest.NewWithInnerStructHasProto(2, "y", saferesembletest.NewWithPrivateProto(1, "b", ts66)), 260 e: saferesembletest.NewWithInnerStructHasProto(2, "y", saferesembletest.NewWithPrivateProto(1, "b", ts53)), 261 diffsContains: []string{"field .I.t differs:"}, 262 }, 263 { 264 name: "diff inner struct slice no protos", 265 a: saferesembletest.NewWithInnerStructSliceNoProto(2, "y", []saferesembletest.NoProtos{ 266 saferesembletest.NewNoProtos(1, testclock.TestRecentTimeUTC), 267 }), 268 e: saferesembletest.NewWithInnerStructSliceNoProto(2, "y", []saferesembletest.NoProtos{ 269 saferesembletest.NewNoProtos(1, testclock.TestRecentTimeUTC.Add(1*time.Second)), 270 }), 271 diffsContains: []string{"non-proto fields differ:", "(Should equal)!"}, 272 }, 273 { 274 name: "diff inner struct slice has proto", 275 a: saferesembletest.NewWithInnerStructSliceHasProto(2, "y", []saferesembletest.WithPrivateProto{ 276 saferesembletest.NewWithPrivateProto(1, "b", ts66), 277 }), 278 e: saferesembletest.NewWithInnerStructSliceHasProto(2, "y", []saferesembletest.WithPrivateProto{ 279 saferesembletest.NewWithPrivateProto(1, "b", ts53), 280 }), 281 diffsContains: []string{"field .Is[0].t differs:"}, 282 }, 283 { 284 name: "diff in struct slice length of a slice", 285 a: saferesembletest.NewWithInnerStructSliceHasProto(2, "y", []saferesembletest.WithPrivateProto{ 286 saferesembletest.NewWithPrivateProto(1, "b", ts53), 287 saferesembletest.NewWithPrivateProto(10, "bb", ts53), 288 }), 289 e: saferesembletest.NewWithInnerStructSliceHasProto(2, "y", []saferesembletest.WithPrivateProto{ 290 saferesembletest.NewWithPrivateProto(1, "b", ts53), 291 }), 292 diffsContains: []string{"field .Is differs in length:"}, 293 }, 294 } 295 296 for i, c := range cases { 297 i, tCase := i, c 298 name := fmt.Sprintf("%2d: %s", i, tCase.name) 299 Convey(name, t, func() { 300 if tCase.shouldPanicLike != "" { 301 So(func() { SafeShouldResemble(tCase.a, tCase.e) }, assertions.ShouldPanicLike, tCase.shouldPanicLike) 302 return 303 } 304 305 diff := SafeShouldResemble(tCase.a, tCase.e) 306 if len(tCase.diffsContains) == 0 { 307 So(diff, ShouldEqual, "") 308 return 309 } 310 _, _ = Printf("\n\n===== diff emitted for %s =====\n%s\n%s\n", name, diff, strings.Repeat("=", 80)) 311 for _, sub := range tCase.diffsContains { 312 So(diff, ShouldContainSubstring, sub) 313 } 314 }) 315 } 316 } 317 318 type tNoProtos struct { 319 a int 320 A time.Time 321 } 322 323 type tWithProto struct { 324 a int 325 PB *timestamppb.Timestamp 326 } 327 328 type tWith2Protos struct { 329 A int 330 b string 331 T *timestamppb.Timestamp 332 D *durationpb.Duration 333 } 334 335 type tWithProtoSlice struct { 336 TS []*timestamppb.Timestamp 337 }