1 /* 2 Copyright 2024 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package ktesting 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 "strings" 24 25 "github.com/onsi/gomega" 26 "github.com/onsi/gomega/format" 27 ) 28 29 // FailureError is an error where the error string is meant to be passed to 30 // [TContext.Fatal] directly, i.e. adding some prefix like "unexpected error" is not 31 // necessary. It is also not necessary to dump the error struct. 32 type FailureError struct { 33 Msg string 34 FullStackTrace string 35 } 36 37 func (f FailureError) Error() string { 38 return f.Msg 39 } 40 41 func (f FailureError) Backtrace() string { 42 return f.FullStackTrace 43 } 44 45 func (f FailureError) Is(target error) bool { 46 return target == ErrFailure 47 } 48 49 // ErrFailure is an empty error that can be wrapped to indicate that an error 50 // is a FailureError. It can also be used to test for a FailureError:. 51 // 52 // return fmt.Errorf("some problem%w", ErrFailure) 53 // ... 54 // err := someOperation() 55 // if errors.Is(err, ErrFailure) { 56 // ... 57 // } 58 var ErrFailure error = FailureError{} 59 60 func expect(tCtx TContext, actual interface{}, extra ...interface{}) gomega.Assertion { 61 tCtx.Helper() 62 return gomega.NewWithT(tCtx).Expect(actual, extra...) 63 } 64 65 func expectNoError(tCtx TContext, err error, explain ...interface{}) { 66 if err == nil { 67 return 68 } 69 70 tCtx.Helper() 71 72 description := buildDescription(explain...) 73 74 if errors.Is(err, ErrFailure) { 75 var failure FailureError 76 if errors.As(err, &failure) { 77 if backtrace := failure.Backtrace(); backtrace != "" { 78 if description != "" { 79 tCtx.Log(description) 80 } 81 tCtx.Logf("Failed at:\n %s", strings.ReplaceAll(backtrace, "\n", "\n ")) 82 } 83 } 84 if description != "" { 85 tCtx.Fatalf("%s: %s", description, err.Error()) 86 } 87 tCtx.Fatal(err.Error()) 88 } 89 90 if description == "" { 91 description = "Unexpected error" 92 } 93 tCtx.Logf("%s:\n%s", description, format.Object(err, 1)) 94 tCtx.Fatalf("%s: %v", description, err.Error()) 95 } 96 97 func buildDescription(explain ...interface{}) string { 98 switch len(explain) { 99 case 0: 100 return "" 101 case 1: 102 if describe, ok := explain[0].(func() string); ok { 103 return describe() 104 } 105 } 106 return fmt.Sprintf(explain[0].(string), explain[1:]...) 107 } 108 109 // Eventually wraps [gomega.Eventually] such that a failure will be reported via 110 // TContext.Fatal. 111 // 112 // In contrast to [gomega.Eventually], the parameter is strongly typed. It must 113 // accept a TContext as first argument and return one value, the one which is 114 // then checked with the matcher. 115 // 116 // In contrast to direct usage of [gomega.Eventually], make additional 117 // assertions inside the callback is okay as long as they use the TContext that 118 // is passed in. For example, errors can be checked with ExpectNoError: 119 // 120 // cb := func(func(tCtx ktesting.TContext) int { 121 // value, err := doSomething(...) 122 // tCtx.ExpectNoError(err, "something failed") 123 // assert(tCtx, 42, value, "the answer") 124 // return value 125 // } 126 // tCtx.Eventually(cb).Should(gomega.Equal(42), "should be the answer to everything") 127 // 128 // If there is no value, then an error can be returned: 129 // 130 // cb := func(func(tCtx ktesting.TContext) error { 131 // err := doSomething(...) 132 // return err 133 // } 134 // tCtx.Eventually(cb).Should(gomega.Succeed(), "foobar should succeed") 135 // 136 // The default Gomega poll interval and timeout are used. Setting a specific 137 // timeout may be useful: 138 // 139 // tCtx.Eventually(cb).Timeout(5 * time.Second).Should(gomega.Succeed(), "foobar should succeed") 140 // 141 // Canceling the context in the callback only affects code in the callback. The 142 // context passed to Eventually is not getting canceled. To abort polling 143 // immediately because the expected condition is known to not be reached 144 // anymore, use [gomega.StopTrying]: 145 // 146 // cb := func(func(tCtx ktesting.TContext) int { 147 // value, err := doSomething(...) 148 // if errors.Is(err, SomeFinalErr) { 149 // // This message completely replaces the normal 150 // // failure message and thus should include all 151 // // relevant information. 152 // // 153 // // github.com/onsi/gomega/format is a good way 154 // // to format arbitrary data. It uses indention 155 // // and falls back to YAML for Kubernetes API 156 // // structs for readability. 157 // gomega.StopTrying("permanent failure, last value:\n%s", format.Object(value, 1 /* indent one level */)). 158 // Wrap(err).Now() 159 // } 160 // ktesting.ExpectNoError(tCtx, err, "something failed") 161 // return value 162 // } 163 // tCtx.Eventually(cb).Should(gomega.Equal(42), "should be the answer to everything") 164 // 165 // To poll again after some specific timeout, use [gomega.TryAgainAfter]. This is 166 // particularly useful in [Consistently] to ignore some intermittent error. 167 // 168 // cb := func(func(tCtx ktesting.TContext) int { 169 // value, err := doSomething(...) 170 // var intermittentErr SomeIntermittentError 171 // if errors.As(err, &intermittentErr) { 172 // gomega.TryAgainAfter(intermittentErr.RetryPeriod).Wrap(err).Now() 173 // } 174 // ktesting.ExpectNoError(tCtx, err, "something failed") 175 // return value 176 // } 177 // tCtx.Eventually(cb).Should(gomega.Equal(42), "should be the answer to everything") 178 func Eventually[T any](tCtx TContext, cb func(TContext) T) gomega.AsyncAssertion { 179 tCtx.Helper() 180 return gomega.NewWithT(tCtx).Eventually(tCtx, func(ctx context.Context) (val T, err error) { 181 tCtx := WithContext(tCtx, ctx) 182 tCtx, finalize := WithError(tCtx, &err) 183 defer finalize() 184 tCtx = WithCancel(tCtx) 185 return cb(tCtx), nil 186 }) 187 } 188 189 // Consistently wraps [gomega.Consistently] the same way as [Eventually] wraps 190 // [gomega.Eventually]. 191 func Consistently[T any](tCtx TContext, cb func(TContext) T) gomega.AsyncAssertion { 192 tCtx.Helper() 193 return gomega.NewWithT(tCtx).Consistently(tCtx, func(ctx context.Context) (val T, err error) { 194 tCtx := WithContext(tCtx, ctx) 195 tCtx, finalize := WithError(tCtx, &err) 196 defer finalize() 197 return cb(tCtx), nil 198 }) 199 } 200