Eventually an Consistently can now be stopped early with StopTrying(message) and StopTrying(message).Now()
Onsi Fakhouri
1 year, 6 months ago
475 | 475 | If `Consistently` is passed a `context.Context` it will exit if the context is cancelled - however it will always register the cancellation of the context as a failure. That is, the context is not used to control the duration of `Consistently` - that is always done by the `DURATION` parameter; instead, the context is used to allow `Consistently` to bail out early if it's time for the spec to finish up (e.g. a timeout has elapsed, or the user has sent an interrupt signal). |
476 | 476 | |
477 | 477 | > Developers often try to use `runtime.Gosched()` to nudge background goroutines to run. This can lead to flaky tests as it is not deterministic that a given goroutine will run during the `Gosched`. `Consistently` is particularly handy in these cases: it polls for 100ms which is typically more than enough time for all your Goroutines to run. Yes, this is basically like putting a time.Sleep() in your tests... Sometimes, when making negative assertions in a concurrent world, that's the best you can do! |
478 | ||
479 | ### Bailing Out Early | |
480 | ||
481 | There are cases where you need to signal to `Eventually` and `Consistently` that they should stop trying. Gomega provides`StopTrying(MESSAGE)` to allow you to send that signal. There are two ways to use `StopTrying`. | |
482 | ||
483 | First, you can return `StopTrying(MESSAGE)` as an error. Consider, for example, the case where `Eventually` is searching through a set of possible queries with a server: | |
484 | ||
485 | ```go | |
486 | playerIndex, numPlayers := 0, 11 | |
487 | Eventually(func() (string, error) { | |
488 | name := client.FetchPlayer(playerIndex) | |
489 | playerIndex += 1 | |
490 | if playerIndex == numPlayers { | |
491 | return name, StopTrying("No more players left") | |
492 | } else { | |
493 | return name, nil | |
494 | } | |
495 | }).Should(Equal("Patrick Mahomes")) | |
496 | ``` | |
497 | ||
498 | Here we return a `StopTrying(MESSAGE)` error to tell `Eventually` that we've looked through all possible players and that it should stop. Note that `Eventually` will check last name returned by this function and succeed if that name is the desired name. | |
499 | ||
500 | You can also call `StopTrying(MESSAGE).Now()` to immediately end execution of the function. Consider, for example, the case of a client communicating with a server that experiences an irrevocable error: | |
501 | ||
502 | ```go | |
503 | Eventually(func() []string { | |
504 | names, err := client.FetchAllPlayers() | |
505 | if err == client.IRRECOVERABLE_ERROR { | |
506 | StopTrying("An irrecoverable error occurred").Now() | |
507 | } | |
508 | return names | |
509 | }).Should(ContainElement("Patrick Mahomes")) | |
510 | ``` | |
511 | ||
512 | calling `.Now()` will trigger a panic that will signal to `Eventually` that the it should stop trying. | |
513 | ||
514 | You can also use both verison of `StopTrying()` with `Consistently`. Since `Consistently` is validating that something is _true_ consitently for the entire requested duration sending a `StopTrying()` signal is interpreted as success. Here's a somewhat contrived example: | |
515 | ||
516 | ```go | |
517 | go client.DoSomethingComplicated() | |
518 | Consistently(func() int { | |
519 | if client.Status() == client.DoneStatus { | |
520 | StopTrying("Client finished").Now() | |
521 | } | |
522 | return client.NumErrors() | |
523 | }).Should(Equal(0)) | |
524 | ``` | |
525 | ||
526 | here we succeed because no errors were identified while the client was working. | |
478 | 527 | |
479 | 528 | ### Modifying Default Intervals |
480 | 529 |
404 | 404 | return Default.ConsistentlyWithOffset(offset, actual, args...) |
405 | 405 | } |
406 | 406 | |
407 | /* | |
408 | StopTrying can be used to signal to Eventually and Consistently that the polled function will not change | |
409 | and that they should stop trying. In the case of Eventually, if a match does not occur in this, final, iteration then a failure will result. In the case of Consistently, as long as this last iteration satisfies the match, the assertion will be considered successful. | |
410 | ||
411 | You can send the StopTrying signal by either returning a StopTrying("message") messages as an error from your passed-in function _or_ by calling StopTrying("message").Now() to trigger a panic and end execution. | |
412 | ||
413 | Here are a couple of examples. This is how you might use StopTrying() as an error to signal that Eventually should stop: | |
414 | ||
415 | playerIndex, numPlayers := 0, 11 | |
416 | Eventually(func() (string, error) { | |
417 | name := client.FetchPlayer(playerIndex) | |
418 | playerIndex += 1 | |
419 | if playerIndex == numPlayers { | |
420 | return name, StopTrying("No more players left") | |
421 | } else { | |
422 | return name, nil | |
423 | } | |
424 | }).Should(Equal("Patrick Mahomes")) | |
425 | ||
426 | note that the final `name` returned alongside `StopTrying()` will be processed. | |
427 | ||
428 | And here's an example where `StopTrying().Now()` is called to halt execution immediately: | |
429 | ||
430 | Eventually(func() []string { | |
431 | names, err := client.FetchAllPlayers() | |
432 | if err == client.IRRECOVERABLE_ERROR { | |
433 | StopTrying("Irrecoverable error occurred").Now() | |
434 | } | |
435 | return names | |
436 | }).Should(ContainElement("Patrick Mahomes")) | |
437 | */ | |
438 | var StopTrying = internal.StopTrying | |
439 | ||
407 | 440 | // SetDefaultEventuallyTimeout sets the default timeout duration for Eventually. Eventually will repeatedly poll your condition until it succeeds, or until this timeout elapses. |
408 | 441 | func SetDefaultEventuallyTimeout(t time.Duration) { |
409 | 442 | Default.SetDefaultEventuallyTimeout(t) |
9 | 9 | |
10 | 10 | "github.com/onsi/gomega/types" |
11 | 11 | ) |
12 | ||
13 | type StopTryingError interface { | |
14 | error | |
15 | Now() | |
16 | wasViaPanic() bool | |
17 | } | |
18 | ||
19 | type stopTryingError struct { | |
20 | message string | |
21 | viaPanic bool | |
22 | } | |
23 | ||
24 | func (s *stopTryingError) Error() string { | |
25 | return s.message | |
26 | } | |
27 | ||
28 | func (s *stopTryingError) Now() { | |
29 | s.viaPanic = true | |
30 | panic(s) | |
31 | } | |
32 | ||
33 | func (s *stopTryingError) wasViaPanic() bool { | |
34 | return s.viaPanic | |
35 | } | |
36 | ||
37 | var stopTryingErrorType = reflect.TypeOf(&stopTryingError{}) | |
38 | ||
39 | var StopTrying = func(message string) StopTryingError { | |
40 | return &stopTryingError{message: message} | |
41 | } | |
12 | 42 | |
13 | 43 | type AsyncAssertionType uint |
14 | 44 | |
118 | 148 | return fmt.Sprintf(optionalDescription[0].(string), optionalDescription[1:]...) + "\n" |
119 | 149 | } |
120 | 150 | |
121 | func (assertion *AsyncAssertion) processReturnValues(values []reflect.Value) (interface{}, error) { | |
151 | func (assertion *AsyncAssertion) processReturnValues(values []reflect.Value) (interface{}, error, StopTryingError) { | |
152 | var err error | |
153 | var stopTrying StopTryingError | |
154 | ||
122 | 155 | if len(values) == 0 { |
123 | return nil, fmt.Errorf("No values were returned by the function passed to Gomega") | |
156 | return nil, fmt.Errorf("No values were returned by the function passed to Gomega"), stopTrying | |
124 | 157 | } |
125 | 158 | actual := values[0].Interface() |
159 | if actual != nil && reflect.TypeOf(actual) == stopTryingErrorType { | |
160 | stopTrying = actual.(StopTryingError) | |
161 | } | |
126 | 162 | for i, extraValue := range values[1:] { |
127 | 163 | extra := extraValue.Interface() |
128 | 164 | if extra == nil { |
129 | 165 | continue |
130 | 166 | } |
131 | zero := reflect.Zero(extraValue.Type()).Interface() | |
167 | extraType := reflect.TypeOf(extra) | |
168 | if extraType == stopTryingErrorType { | |
169 | stopTrying = extra.(StopTryingError) | |
170 | continue | |
171 | } | |
172 | zero := reflect.Zero(extraType).Interface() | |
132 | 173 | if reflect.DeepEqual(extra, zero) { |
133 | 174 | continue |
134 | 175 | } |
135 | return actual, fmt.Errorf("Unexpected non-nil/non-zero argument at index %d:\n\t<%T>: %#v", i+1, extra, extra) | |
136 | } | |
137 | return actual, nil | |
176 | if err == nil { | |
177 | err = fmt.Errorf("Unexpected non-nil/non-zero argument at index %d:\n\t<%T>: %#v", i+1, extra, extra) | |
178 | } | |
179 | } | |
180 | return actual, err, stopTrying | |
138 | 181 | } |
139 | 182 | |
140 | 183 | var gomegaType = reflect.TypeOf((*types.Gomega)(nil)).Elem() |
168 | 211 | `, assertion.asyncType, t, t.NumIn(), numProvided, have, assertion.asyncType) |
169 | 212 | } |
170 | 213 | |
171 | func (assertion *AsyncAssertion) buildActualPoller() (func() (interface{}, error), error) { | |
214 | func (assertion *AsyncAssertion) buildActualPoller() (func() (interface{}, error, StopTryingError), error) { | |
172 | 215 | if !assertion.actualIsFunc { |
173 | return func() (interface{}, error) { return assertion.actual, nil }, nil | |
216 | return func() (interface{}, error, StopTryingError) { return assertion.actual, nil, nil }, nil | |
174 | 217 | } |
175 | 218 | actualValue := reflect.ValueOf(assertion.actual) |
176 | 219 | actualType := reflect.TypeOf(assertion.actual) |
179 | 222 | if numIn == 0 && numOut == 0 { |
180 | 223 | return nil, assertion.invalidFunctionError(actualType) |
181 | 224 | } else if numIn == 0 { |
182 | return func() (interface{}, error) { return assertion.processReturnValues(actualValue.Call([]reflect.Value{})) }, nil | |
225 | return func() (actual interface{}, err error, stopTrying StopTryingError) { | |
226 | defer func() { | |
227 | if e := recover(); e != nil { | |
228 | if reflect.TypeOf(e) == stopTryingErrorType { | |
229 | stopTrying = e.(StopTryingError) | |
230 | } else { | |
231 | panic(e) | |
232 | } | |
233 | } | |
234 | }() | |
235 | ||
236 | actual, err, stopTrying = assertion.processReturnValues(actualValue.Call([]reflect.Value{})) | |
237 | return | |
238 | }, nil | |
183 | 239 | } |
184 | 240 | takesGomega, takesContext := actualType.In(0).Implements(gomegaType), actualType.In(0).Implements(contextType) |
185 | 241 | if takesGomega && numIn > 1 && actualType.In(1).Implements(contextType) { |
221 | 277 | return nil, assertion.argumentMismatchError(actualType, len(inValues)) |
222 | 278 | } |
223 | 279 | |
224 | return func() (actual interface{}, err error) { | |
280 | return func() (actual interface{}, err error, stopTrying StopTryingError) { | |
225 | 281 | var values []reflect.Value |
226 | 282 | assertionFailure = nil |
227 | 283 | defer func() { |
228 | 284 | if numOut == 0 { |
229 | 285 | actual = assertionFailure |
230 | 286 | } else { |
231 | actual, err = assertion.processReturnValues(values) | |
287 | actual, err, stopTrying = assertion.processReturnValues(values) | |
232 | 288 | if assertionFailure != nil { |
233 | 289 | err = assertionFailure |
234 | 290 | } |
235 | 291 | } |
236 | if e := recover(); e != nil && assertionFailure == nil { | |
237 | panic(e) | |
292 | if e := recover(); e != nil { | |
293 | if reflect.TypeOf(e) == stopTryingErrorType { | |
294 | stopTrying = e.(StopTryingError) | |
295 | } else if assertionFailure == nil { | |
296 | panic(e) | |
297 | } | |
238 | 298 | } |
239 | 299 | }() |
240 | 300 | values = actualValue.Call(inValues) |
242 | 302 | }, nil |
243 | 303 | } |
244 | 304 | |
245 | func (assertion *AsyncAssertion) matcherMayChange(matcher types.GomegaMatcher, value interface{}) bool { | |
246 | if assertion.actualIsFunc { | |
247 | return true | |
248 | } | |
249 | return types.MatchMayChangeInTheFuture(matcher, value) | |
305 | func (assertion *AsyncAssertion) matcherSaysStopTrying(matcher types.GomegaMatcher, value interface{}) StopTryingError { | |
306 | if assertion.actualIsFunc || types.MatchMayChangeInTheFuture(matcher, value) { | |
307 | return nil | |
308 | } | |
309 | return StopTrying("No future change is possible. Bailing out early") | |
250 | 310 | } |
251 | 311 | |
252 | 312 | type contextWithAttachProgressReporter interface { |
260 | 320 | |
261 | 321 | var matches bool |
262 | 322 | var err error |
263 | mayChange := true | |
264 | 323 | |
265 | 324 | assertion.g.THelper() |
266 | 325 | |
270 | 329 | return false |
271 | 330 | } |
272 | 331 | |
273 | value, err := pollActual() | |
332 | value, err, stopTrying := pollActual() | |
274 | 333 | if err == nil { |
275 | mayChange = assertion.matcherMayChange(matcher, value) | |
334 | if stopTrying == nil { | |
335 | stopTrying = assertion.matcherSaysStopTrying(matcher, value) | |
336 | } | |
276 | 337 | matches, err = matcher.Match(value) |
277 | 338 | } |
278 | 339 | |
315 | 376 | return true |
316 | 377 | } |
317 | 378 | |
318 | if !mayChange { | |
319 | fail("No future change is possible. Bailing out early") | |
379 | if stopTrying != nil { | |
380 | fail(stopTrying.Error() + " -") | |
320 | 381 | return false |
321 | 382 | } |
322 | 383 | |
323 | 384 | select { |
324 | 385 | case <-time.After(assertion.pollingInterval): |
325 | v, e := pollActual() | |
386 | v, e, st := pollActual() | |
387 | if st != nil && st.wasViaPanic() { | |
388 | // we were told to stop trying via panic - which means we dont' have reasonable new values | |
389 | // we should simply use the old values and exit now | |
390 | fail(st.Error() + " -") | |
391 | return false | |
392 | } | |
326 | 393 | lock.Lock() |
327 | value, err = v, e | |
394 | value, err, stopTrying = v, e, st | |
328 | 395 | lock.Unlock() |
329 | 396 | if err == nil { |
330 | mayChange = assertion.matcherMayChange(matcher, value) | |
397 | if stopTrying == nil { | |
398 | stopTrying = assertion.matcherSaysStopTrying(matcher, value) | |
399 | } | |
331 | 400 | matches, e = matcher.Match(value) |
332 | 401 | lock.Lock() |
333 | 402 | err = e |
348 | 417 | return false |
349 | 418 | } |
350 | 419 | |
351 | if !mayChange { | |
420 | if stopTrying != nil { | |
352 | 421 | return true |
353 | 422 | } |
354 | 423 | |
355 | 424 | select { |
356 | 425 | case <-time.After(assertion.pollingInterval): |
357 | v, e := pollActual() | |
426 | v, e, st := pollActual() | |
427 | if st != nil && st.wasViaPanic() { | |
428 | // we were told to stop trying via panic - which means we made it this far and should return successfully | |
429 | return true | |
430 | } | |
358 | 431 | lock.Lock() |
359 | value, err = v, e | |
432 | value, err, stopTrying = v, e, st | |
360 | 433 | lock.Unlock() |
361 | 434 | if err == nil { |
362 | mayChange = assertion.matcherMayChange(matcher, value) | |
435 | if stopTrying == nil { | |
436 | stopTrying = assertion.matcherSaysStopTrying(matcher, value) | |
437 | } | |
363 | 438 | matches, e = matcher.Match(value) |
364 | 439 | lock.Lock() |
365 | 440 | err = e |
906 | 906 | }) |
907 | 907 | }) |
908 | 908 | |
909 | Describe("when using OracleMatchers", func() { | |
910 | It("stops and gives up with an appropriate failure message if the OracleMatcher says things can't change", func() { | |
911 | c := make(chan bool) | |
912 | close(c) | |
913 | ||
914 | t := time.Now() | |
915 | ig.G.Eventually(c).WithTimeout(100*time.Millisecond).WithPolling(10*time.Millisecond).Should(Receive(), "Receive is an OracleMatcher that gives up if the channel is closed") | |
916 | Ω(time.Since(t)).Should(BeNumerically("<", 90*time.Millisecond)) | |
917 | Ω(ig.FailureMessage).Should(ContainSubstring("No future change is possible.")) | |
918 | Ω(ig.FailureMessage).Should(ContainSubstring("The channel is closed.")) | |
919 | }) | |
920 | ||
921 | It("never gives up if actual is a function", func() { | |
922 | c := make(chan bool) | |
923 | close(c) | |
924 | ||
925 | t := time.Now() | |
926 | ig.G.Eventually(func() chan bool { return c }).WithTimeout(100*time.Millisecond).WithPolling(10*time.Millisecond).Should(Receive(), "Receive is an OracleMatcher that gives up if the channel is closed") | |
927 | Ω(time.Since(t)).Should(BeNumerically(">=", 90*time.Millisecond)) | |
928 | Ω(ig.FailureMessage).ShouldNot(ContainSubstring("No future change is possible.")) | |
929 | Ω(ig.FailureMessage).Should(ContainSubstring("Timed out after")) | |
909 | Describe("Stopping Early", func() { | |
910 | Describe("when using OracleMatchers", func() { | |
911 | It("stops and gives up with an appropriate failure message if the OracleMatcher says things can't change", func() { | |
912 | c := make(chan bool) | |
913 | close(c) | |
914 | ||
915 | t := time.Now() | |
916 | ig.G.Eventually(c).WithTimeout(100*time.Millisecond).WithPolling(10*time.Millisecond).Should(Receive(), "Receive is an OracleMatcher that gives up if the channel is closed") | |
917 | Ω(time.Since(t)).Should(BeNumerically("<", 90*time.Millisecond)) | |
918 | Ω(ig.FailureMessage).Should(ContainSubstring("No future change is possible.")) | |
919 | Ω(ig.FailureMessage).Should(ContainSubstring("The channel is closed.")) | |
920 | }) | |
921 | ||
922 | It("never gives up if actual is a function", func() { | |
923 | c := make(chan bool) | |
924 | close(c) | |
925 | ||
926 | t := time.Now() | |
927 | ig.G.Eventually(func() chan bool { return c }).WithTimeout(100*time.Millisecond).WithPolling(10*time.Millisecond).Should(Receive(), "Receive is an OracleMatcher that gives up if the channel is closed") | |
928 | Ω(time.Since(t)).Should(BeNumerically(">=", 90*time.Millisecond)) | |
929 | Ω(ig.FailureMessage).ShouldNot(ContainSubstring("No future change is possible.")) | |
930 | Ω(ig.FailureMessage).Should(ContainSubstring("Timed out after")) | |
931 | }) | |
932 | }) | |
933 | ||
934 | Describe("The StopTrying signal", func() { | |
935 | Context("when success occurs on the last iteration", func() { | |
936 | It("succeeds and stops when the signal is returned", func() { | |
937 | possibilities := []string{"A", "B", "C"} | |
938 | i := 0 | |
939 | Eventually(func() (string, error) { | |
940 | possibility := possibilities[i] | |
941 | i += 1 | |
942 | if i == len(possibilities) { | |
943 | return possibility, StopTrying("Reached the end") | |
944 | } else { | |
945 | return possibility, nil | |
946 | } | |
947 | }).Should(Equal("C")) | |
948 | Ω(i).Should(Equal(3)) | |
949 | }) | |
950 | ||
951 | It("counts as success for consistently", func() { | |
952 | i := 0 | |
953 | Consistently(func() (int, error) { | |
954 | i += 1 | |
955 | if i >= 10 { | |
956 | return i, StopTrying("Reached the end") | |
957 | } | |
958 | return i, nil | |
959 | }).Should(BeNumerically("<=", 10)) | |
960 | ||
961 | i = 0 | |
962 | Consistently(func() int { | |
963 | i += 1 | |
964 | if i >= 10 { | |
965 | StopTrying("Reached the end").Now() | |
966 | } | |
967 | return i | |
968 | }).Should(BeNumerically("<=", 10)) | |
969 | }) | |
970 | }) | |
971 | ||
972 | Context("when success does not occur", func() { | |
973 | It("fails and stops trying early", func() { | |
974 | possibilities := []string{"A", "B", "C"} | |
975 | i := 0 | |
976 | ig.G.Eventually(func() (string, error) { | |
977 | possibility := possibilities[i] | |
978 | i += 1 | |
979 | if i == len(possibilities) { | |
980 | return possibility, StopTrying("Reached the end") | |
981 | } else { | |
982 | return possibility, nil | |
983 | } | |
984 | }).Should(Equal("D")) | |
985 | Ω(i).Should(Equal(3)) | |
986 | Ω(ig.FailureMessage).Should(ContainSubstring("Reached the end - after")) | |
987 | Ω(ig.FailureMessage).Should(ContainSubstring("Expected\n <string>: C\nto equal\n <string>: D")) | |
988 | }) | |
989 | }) | |
990 | ||
991 | Context("when StopTrying().Now() is called", func() { | |
992 | It("halts execution, stops trying, and emits the last failure", func() { | |
993 | possibilities := []string{"A", "B", "C"} | |
994 | i := -1 | |
995 | ig.G.Eventually(func() string { | |
996 | i += 1 | |
997 | if i < len(possibilities) { | |
998 | return possibilities[i] | |
999 | } else { | |
1000 | StopTrying("Out of tries").Now() | |
1001 | panic("welp") | |
1002 | } | |
1003 | }).Should(Equal("D")) | |
1004 | Ω(i).Should(Equal(3)) | |
1005 | Ω(ig.FailureMessage).Should(ContainSubstring("Out of tries - after")) | |
1006 | Ω(ig.FailureMessage).Should(ContainSubstring("Expected\n <string>: C\nto equal\n <string>: D")) | |
1007 | }) | |
1008 | }) | |
1009 | ||
1010 | It("still allows regular panics to get through", func() { | |
1011 | defer func() { | |
1012 | e := recover() | |
1013 | Ω(e).Should(Equal("welp")) | |
1014 | }() | |
1015 | Eventually(func() string { | |
1016 | panic("welp") | |
1017 | return "A" | |
1018 | }).Should(Equal("A")) | |
1019 | }) | |
1020 | ||
1021 | Context("when used in conjunction wihth a Gomega and/or Context", func() { | |
1022 | It("correctly catches the StopTrying signal", func() { | |
1023 | i := 0 | |
1024 | ctx := context.WithValue(context.Background(), "key", "A") | |
1025 | ig.G.Eventually(func(g Gomega, ctx context.Context, expected string) { | |
1026 | i += 1 | |
1027 | if i >= 3 { | |
1028 | StopTrying("Out of tries").Now() | |
1029 | } | |
1030 | g.Expect(ctx.Value("key")).To(Equal(expected)) | |
1031 | }).WithContext(ctx).WithArguments("B").Should(Succeed()) | |
1032 | Ω(i).Should(Equal(3)) | |
1033 | Ω(ig.FailureMessage).Should(ContainSubstring("Out of tries - after")) | |
1034 | Ω(ig.FailureMessage).Should(ContainSubstring("Assertion in callback at")) | |
1035 | Ω(ig.FailureMessage).Should(ContainSubstring("<string>: A")) | |
1036 | }) | |
1037 | }) | |
930 | 1038 | }) |
931 | 1039 | }) |
932 | 1040 |