k8s.io/kubernetes@v1.29.3/test/e2e/framework/expect.go (about) 1 /* 2 Copyright 2014 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 framework 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 "strings" 24 "time" 25 26 ginkgotypes "github.com/onsi/ginkgo/v2/types" 27 "github.com/onsi/gomega" 28 "github.com/onsi/gomega/format" 29 "github.com/onsi/gomega/types" 30 ) 31 32 // MakeMatcher builds a gomega.Matcher based on a single callback function. 33 // That function is passed the actual value that is to be checked. 34 // There are three possible outcomes of the check: 35 // - An error is returned, which then is converted into a failure 36 // by Gomega. 37 // - A non-nil failure function is returned, which then is called 38 // by Gomega once a failure string is needed. This is useful 39 // to avoid unnecessarily preparing a failure string for intermediate 40 // failures in Eventually or Consistently. 41 // - Both function and error are nil, which means that the check 42 // succeeded. 43 func MakeMatcher[T interface{}](match func(actual T) (failure func() string, err error)) types.GomegaMatcher { 44 return &matcher[T]{ 45 match: match, 46 } 47 } 48 49 type matcher[T interface{}] struct { 50 match func(actual T) (func() string, error) 51 failure func() string 52 } 53 54 func (m *matcher[T]) Match(actual interface{}) (success bool, err error) { 55 if actual, ok := actual.(T); ok { 56 failure, err := m.match(actual) 57 if err != nil { 58 return false, err 59 } 60 m.failure = failure 61 if failure != nil { 62 return false, nil 63 } 64 return true, nil 65 } 66 var empty T 67 return false, gomega.StopTrying(fmt.Sprintf("internal error: expected %T, got:\n%s", empty, format.Object(actual, 1))) 68 } 69 70 func (m *matcher[T]) FailureMessage(actual interface{}) string { 71 return m.failure() 72 } 73 74 func (m matcher[T]) NegatedFailureMessage(actual interface{}) string { 75 return m.failure() 76 } 77 78 var _ types.GomegaMatcher = &matcher[string]{} 79 80 // Gomega returns an interface that can be used like gomega to express 81 // assertions. The difference is that failed assertions are returned as an 82 // error: 83 // 84 // if err := Gomega().Expect(pod.Status.Phase).To(gomega.Equal(v1.Running)); err != nil { 85 // return fmt.Errorf("test pod not running: %w", err) 86 // } 87 // 88 // This error can get wrapped to provide additional context for the 89 // failure. The test then should use ExpectNoError to turn a non-nil error into 90 // a failure. 91 // 92 // When using this approach, there is no need for call offsets and extra 93 // descriptions for the Expect call because the call stack will be dumped when 94 // ExpectNoError is called and the additional description(s) can be added by 95 // wrapping the error. 96 // 97 // Asynchronous assertions use the framework's Poll interval and PodStart timeout 98 // by default. 99 func Gomega() GomegaInstance { 100 return gomegaInstance{} 101 } 102 103 type GomegaInstance interface { 104 Expect(actual interface{}) Assertion 105 Eventually(ctx context.Context, args ...interface{}) AsyncAssertion 106 Consistently(ctx context.Context, args ...interface{}) AsyncAssertion 107 } 108 109 type Assertion interface { 110 Should(matcher types.GomegaMatcher) error 111 ShouldNot(matcher types.GomegaMatcher) error 112 To(matcher types.GomegaMatcher) error 113 ToNot(matcher types.GomegaMatcher) error 114 NotTo(matcher types.GomegaMatcher) error 115 } 116 117 type AsyncAssertion interface { 118 Should(matcher types.GomegaMatcher) error 119 ShouldNot(matcher types.GomegaMatcher) error 120 121 WithTimeout(interval time.Duration) AsyncAssertion 122 WithPolling(interval time.Duration) AsyncAssertion 123 } 124 125 type gomegaInstance struct{} 126 127 var _ GomegaInstance = gomegaInstance{} 128 129 func (g gomegaInstance) Expect(actual interface{}) Assertion { 130 return assertion{actual: actual} 131 } 132 133 func (g gomegaInstance) Eventually(ctx context.Context, args ...interface{}) AsyncAssertion { 134 return newAsyncAssertion(ctx, args, false) 135 } 136 137 func (g gomegaInstance) Consistently(ctx context.Context, args ...interface{}) AsyncAssertion { 138 return newAsyncAssertion(ctx, args, true) 139 } 140 141 func newG() (*FailureError, gomega.Gomega) { 142 var failure FailureError 143 g := gomega.NewGomega(func(msg string, callerSkip ...int) { 144 failure = FailureError{ 145 msg: msg, 146 } 147 }) 148 149 return &failure, g 150 } 151 152 type assertion struct { 153 actual interface{} 154 } 155 156 func (a assertion) Should(matcher types.GomegaMatcher) error { 157 err, g := newG() 158 if !g.Expect(a.actual).Should(matcher) { 159 err.backtrace() 160 return *err 161 } 162 return nil 163 } 164 165 func (a assertion) ShouldNot(matcher types.GomegaMatcher) error { 166 err, g := newG() 167 if !g.Expect(a.actual).ShouldNot(matcher) { 168 err.backtrace() 169 return *err 170 } 171 return nil 172 } 173 174 func (a assertion) To(matcher types.GomegaMatcher) error { 175 err, g := newG() 176 if !g.Expect(a.actual).To(matcher) { 177 err.backtrace() 178 return *err 179 } 180 return nil 181 } 182 183 func (a assertion) ToNot(matcher types.GomegaMatcher) error { 184 err, g := newG() 185 if !g.Expect(a.actual).ToNot(matcher) { 186 err.backtrace() 187 return *err 188 } 189 return nil 190 } 191 192 func (a assertion) NotTo(matcher types.GomegaMatcher) error { 193 err, g := newG() 194 if !g.Expect(a.actual).NotTo(matcher) { 195 err.backtrace() 196 return *err 197 } 198 return nil 199 } 200 201 type asyncAssertion struct { 202 ctx context.Context 203 args []interface{} 204 timeout time.Duration 205 interval time.Duration 206 consistently bool 207 } 208 209 func newAsyncAssertion(ctx context.Context, args []interface{}, consistently bool) asyncAssertion { 210 return asyncAssertion{ 211 ctx: ctx, 212 args: args, 213 // PodStart is used as default because waiting for a pod is the 214 // most common operation. 215 timeout: TestContext.timeouts.PodStart, 216 interval: TestContext.timeouts.Poll, 217 consistently: consistently, 218 } 219 } 220 221 func (a asyncAssertion) newAsync() (*FailureError, gomega.AsyncAssertion) { 222 err, g := newG() 223 var assertion gomega.AsyncAssertion 224 if a.consistently { 225 assertion = g.Consistently(a.ctx, a.args...) 226 } else { 227 assertion = g.Eventually(a.ctx, a.args...) 228 } 229 assertion = assertion.WithTimeout(a.timeout).WithPolling(a.interval) 230 return err, assertion 231 } 232 233 func (a asyncAssertion) Should(matcher types.GomegaMatcher) error { 234 err, assertion := a.newAsync() 235 if !assertion.Should(matcher) { 236 err.backtrace() 237 return *err 238 } 239 return nil 240 } 241 242 func (a asyncAssertion) ShouldNot(matcher types.GomegaMatcher) error { 243 err, assertion := a.newAsync() 244 if !assertion.ShouldNot(matcher) { 245 err.backtrace() 246 return *err 247 } 248 return nil 249 } 250 251 func (a asyncAssertion) WithTimeout(timeout time.Duration) AsyncAssertion { 252 a.timeout = timeout 253 return a 254 } 255 256 func (a asyncAssertion) WithPolling(interval time.Duration) AsyncAssertion { 257 a.interval = interval 258 return a 259 } 260 261 // FailureError is an error where the error string is meant to be passed to 262 // ginkgo.Fail directly, i.e. adding some prefix like "unexpected error" is not 263 // necessary. It is also not necessary to dump the error struct. 264 type FailureError struct { 265 msg string 266 fullStackTrace string 267 } 268 269 func (f FailureError) Error() string { 270 return f.msg 271 } 272 273 func (f FailureError) Backtrace() string { 274 return f.fullStackTrace 275 } 276 277 func (f FailureError) Is(target error) bool { 278 return target == ErrFailure 279 } 280 281 func (f *FailureError) backtrace() { 282 f.fullStackTrace = ginkgotypes.NewCodeLocationWithStackTrace(2).FullStackTrace 283 } 284 285 // ErrFailure is an empty error that can be wrapped to indicate that an error 286 // is a FailureError. It can also be used to test for a FailureError:. 287 // 288 // return fmt.Errorf("some problem%w", ErrFailure) 289 // ... 290 // err := someOperation() 291 // if errors.Is(err, ErrFailure) { 292 // ... 293 // } 294 var ErrFailure error = FailureError{} 295 296 // ExpectNotEqual expects the specified two are not the same, otherwise an exception raises 297 // 298 // Deprecated: use gomega.Expect().ToNot(gomega.Equal()) 299 func ExpectNotEqual(actual interface{}, extra interface{}, explain ...interface{}) { 300 gomega.ExpectWithOffset(1, actual).NotTo(gomega.Equal(extra), explain...) 301 } 302 303 // ExpectError expects an error happens, otherwise an exception raises 304 // 305 // Deprecated: use gomega.Expect().To(gomega.HaveOccurred()) or (better!) check 306 // specifically for the error that is expected with 307 // gomega.Expect().To(gomega.MatchError(gomega.ContainSubstring())) 308 func ExpectError(err error, explain ...interface{}) { 309 gomega.ExpectWithOffset(1, err).To(gomega.HaveOccurred(), explain...) 310 } 311 312 // ExpectNoError checks if "err" is set, and if so, fails assertion while logging the error. 313 func ExpectNoError(err error, explain ...interface{}) { 314 ExpectNoErrorWithOffset(1, err, explain...) 315 } 316 317 // ExpectNoErrorWithOffset checks if "err" is set, and if so, fails assertion while logging the error at "offset" levels above its caller 318 // (for example, for call chain f -> g -> ExpectNoErrorWithOffset(1, ...) error would be logged for "f"). 319 func ExpectNoErrorWithOffset(offset int, err error, explain ...interface{}) { 320 if err == nil { 321 return 322 } 323 324 // Errors usually contain unexported fields. We have to use 325 // a formatter here which can print those. 326 prefix := "" 327 if len(explain) > 0 { 328 if str, ok := explain[0].(string); ok { 329 prefix = fmt.Sprintf(str, explain[1:]...) + ": " 330 } else { 331 prefix = fmt.Sprintf("unexpected explain arguments, need format string: %v", explain) 332 } 333 } 334 335 // This intentionally doesn't use gomega.Expect. Instead we take 336 // full control over what information is presented where: 337 // - The complete error object is logged because it may contain 338 // additional information that isn't included in its error 339 // string. 340 // - It is not included in the failure message because 341 // it might make the failure message very large and/or 342 // cause error aggregation to work less well: two 343 // failures at the same code line might not be matched in 344 // https://go.k8s.io/triage because the error details are too 345 // different. 346 // 347 // Some errors include all relevant information in the Error 348 // string. For those we can skip the redundant log message. 349 // For our own failures we only log the additional stack backtrace 350 // because it is not included in the failure message. 351 var failure FailureError 352 if errors.As(err, &failure) && failure.Backtrace() != "" { 353 Logf("Failed inside E2E framework:\n %s", strings.ReplaceAll(failure.Backtrace(), "\n", "\n ")) 354 } else if !errors.Is(err, ErrFailure) { 355 Logf("Unexpected error: %s\n%s", prefix, format.Object(err, 1)) 356 } 357 Fail(prefix+err.Error(), 1+offset) 358 }