istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/test/util/assert/assert.go (about) 1 // Copyright Istio 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 assert 16 17 import ( 18 "fmt" 19 "reflect" 20 "strings" 21 "time" 22 23 "github.com/google/go-cmp/cmp" 24 "github.com/google/go-cmp/cmp/cmpopts" 25 "google.golang.org/protobuf/testing/protocmp" 26 27 "istio.io/istio/pkg/ptr" 28 "istio.io/istio/pkg/test" 29 "istio.io/istio/pkg/test/util/retry" 30 ) 31 32 var compareErrors = cmp.Comparer(func(x, y error) bool { 33 switch { 34 case x == nil && y == nil: 35 return true 36 case x != nil && y == nil: 37 return false 38 case x == nil && y != nil: 39 return false 40 case x != nil && y != nil: 41 return x.Error() == y.Error() 42 default: 43 panic("unreachable") 44 } 45 }) 46 47 // cmpOptioner can be implemented to provide custom options that should be used when comparing a type. 48 // Warning: this is no recursive, unfortunately. So a type `A{B}` cannot rely on `B` implementing this to customize comparing `B`. 49 type cmpOptioner interface { 50 CmpOpts() []cmp.Option 51 } 52 53 // opts gets the comparison opts for a type. This includes some defaults, but allows each type to explicitly append their own. 54 func opts[T any](a T) []cmp.Option { 55 if o, ok := any(a).(cmpOptioner); ok { 56 opts := append([]cmp.Option{}, cmpOpts...) 57 opts = append(opts, o.CmpOpts()...) 58 return opts 59 } 60 // if T is actually a slice (ex: []A), check that and get the opts for the element type (A). 61 t := reflect.TypeOf(a) 62 if t != nil && t.Kind() == reflect.Slice { 63 v := reflect.New(t.Elem()).Elem().Interface() 64 if o, ok := v.(cmpOptioner); ok { 65 opts := append([]cmp.Option{}, cmpOpts...) 66 opts = append(opts, o.CmpOpts()...) 67 return opts 68 } 69 } 70 return cmpOpts 71 } 72 73 var cmpOpts = []cmp.Option{protocmp.Transform(), cmpopts.EquateEmpty(), compareErrors} 74 75 // Compare compares two objects and returns and error if they are not the same. 76 func Compare[T any](a, b T) error { 77 if !cmp.Equal(a, b, opts(a)...) { 78 return fmt.Errorf("found diff: %v\nLeft: %v\nRight: %v", cmp.Diff(a, b, opts(a)...), a, b) 79 } 80 return nil 81 } 82 83 // Equal compares two objects and fails if they are not the same. 84 func Equal[T any](t test.Failer, a, b T, context ...string) { 85 t.Helper() 86 if !cmp.Equal(a, b, opts(a)...) { 87 cs := "" 88 if len(context) > 0 { 89 cs = " " + strings.Join(context, ", ") + ":" 90 } 91 t.Fatalf("found diff:%s %v\nLeft: %v\nRight: %v", cs, cmp.Diff(a, b, opts(a)...), a, b) 92 } 93 } 94 95 // EventuallyEqual compares repeatedly calls the fetch function until the result matches the expectation. 96 func EventuallyEqual[T any](t test.Failer, fetch func() T, expected T, retryOpts ...retry.Option) { 97 t.Helper() 98 var a T 99 // Unit tests typically need shorter default; opts can override though 100 ro := []retry.Option{retry.Timeout(time.Second * 2), retry.BackoffDelay(time.Millisecond * 2)} 101 ro = append(ro, retryOpts...) 102 err := retry.UntilSuccess(func() error { 103 a = fetch() 104 if !cmp.Equal(a, expected, opts(expected)...) { 105 return fmt.Errorf("not equal") 106 } 107 return nil 108 }, ro...) 109 if err != nil { 110 t.Fatalf("found diff: %v\nGot: %v\nWant: %v", cmp.Diff(a, expected, opts(expected)...), a, expected) 111 } 112 } 113 114 // Error asserts the provided err is non-nil 115 func Error(t test.Failer, err error) { 116 t.Helper() 117 if err == nil { 118 t.Fatal("expected error but got nil") 119 } 120 } 121 122 // NoError asserts the provided err is nil 123 func NoError(t test.Failer, err error) { 124 t.Helper() 125 if err != nil { 126 t.Fatalf("expected no error but got: %v", err) 127 } 128 } 129 130 // ChannelHasItem asserts a channel has an element within 5s and returns the element 131 func ChannelHasItem[T any](t test.Failer, c <-chan T) T { 132 t.Helper() 133 select { 134 case r := <-c: 135 return r 136 case <-time.After(time.Second * 5): 137 t.Fatalf("failed to receive event after 5s") 138 } 139 // Not reachable 140 return ptr.Empty[T]() 141 } 142 143 // ChannelIsEmpty asserts a channel is empty for at least 20ms 144 func ChannelIsEmpty[T any](t test.Failer, c <-chan T) { 145 t.Helper() 146 select { 147 case r := <-c: 148 t.Fatalf("channel had element, expected empty: %v", r) 149 case <-time.After(time.Millisecond * 20): 150 } 151 }