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        = &timestamppb.Timestamp{Seconds: 6, Nanos: 6}
    58  		ts53        = &timestamppb.Timestamp{Seconds: 5, Nanos: 3}
    59  		ts53duped   = &timestamppb.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  }