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  }