Codebase list golang-gomega / 52976bb
Eventually an Consistently can now be stopped early with StopTrying(message) and StopTrying(message).Now() Onsi Fakhouri 1 year, 6 months ago
4 changed file(s) with 316 addition(s) and 51 deletion(s). Raw diff Collapse all Expand all
475475 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).
476476
477477 > 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.
478527
479528 ### Modifying Default Intervals
480529
404404 return Default.ConsistentlyWithOffset(offset, actual, args...)
405405 }
406406
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
407440 // SetDefaultEventuallyTimeout sets the default timeout duration for Eventually. Eventually will repeatedly poll your condition until it succeeds, or until this timeout elapses.
408441 func SetDefaultEventuallyTimeout(t time.Duration) {
409442 Default.SetDefaultEventuallyTimeout(t)
99
1010 "github.com/onsi/gomega/types"
1111 )
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 }
1242
1343 type AsyncAssertionType uint
1444
118148 return fmt.Sprintf(optionalDescription[0].(string), optionalDescription[1:]...) + "\n"
119149 }
120150
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
122155 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
124157 }
125158 actual := values[0].Interface()
159 if actual != nil && reflect.TypeOf(actual) == stopTryingErrorType {
160 stopTrying = actual.(StopTryingError)
161 }
126162 for i, extraValue := range values[1:] {
127163 extra := extraValue.Interface()
128164 if extra == nil {
129165 continue
130166 }
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()
132173 if reflect.DeepEqual(extra, zero) {
133174 continue
134175 }
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
138181 }
139182
140183 var gomegaType = reflect.TypeOf((*types.Gomega)(nil)).Elem()
168211 `, assertion.asyncType, t, t.NumIn(), numProvided, have, assertion.asyncType)
169212 }
170213
171 func (assertion *AsyncAssertion) buildActualPoller() (func() (interface{}, error), error) {
214 func (assertion *AsyncAssertion) buildActualPoller() (func() (interface{}, error, StopTryingError), error) {
172215 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
174217 }
175218 actualValue := reflect.ValueOf(assertion.actual)
176219 actualType := reflect.TypeOf(assertion.actual)
179222 if numIn == 0 && numOut == 0 {
180223 return nil, assertion.invalidFunctionError(actualType)
181224 } 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
183239 }
184240 takesGomega, takesContext := actualType.In(0).Implements(gomegaType), actualType.In(0).Implements(contextType)
185241 if takesGomega && numIn > 1 && actualType.In(1).Implements(contextType) {
221277 return nil, assertion.argumentMismatchError(actualType, len(inValues))
222278 }
223279
224 return func() (actual interface{}, err error) {
280 return func() (actual interface{}, err error, stopTrying StopTryingError) {
225281 var values []reflect.Value
226282 assertionFailure = nil
227283 defer func() {
228284 if numOut == 0 {
229285 actual = assertionFailure
230286 } else {
231 actual, err = assertion.processReturnValues(values)
287 actual, err, stopTrying = assertion.processReturnValues(values)
232288 if assertionFailure != nil {
233289 err = assertionFailure
234290 }
235291 }
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 }
238298 }
239299 }()
240300 values = actualValue.Call(inValues)
242302 }, nil
243303 }
244304
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")
250310 }
251311
252312 type contextWithAttachProgressReporter interface {
260320
261321 var matches bool
262322 var err error
263 mayChange := true
264323
265324 assertion.g.THelper()
266325
270329 return false
271330 }
272331
273 value, err := pollActual()
332 value, err, stopTrying := pollActual()
274333 if err == nil {
275 mayChange = assertion.matcherMayChange(matcher, value)
334 if stopTrying == nil {
335 stopTrying = assertion.matcherSaysStopTrying(matcher, value)
336 }
276337 matches, err = matcher.Match(value)
277338 }
278339
315376 return true
316377 }
317378
318 if !mayChange {
319 fail("No future change is possible. Bailing out early")
379 if stopTrying != nil {
380 fail(stopTrying.Error() + " -")
320381 return false
321382 }
322383
323384 select {
324385 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 }
326393 lock.Lock()
327 value, err = v, e
394 value, err, stopTrying = v, e, st
328395 lock.Unlock()
329396 if err == nil {
330 mayChange = assertion.matcherMayChange(matcher, value)
397 if stopTrying == nil {
398 stopTrying = assertion.matcherSaysStopTrying(matcher, value)
399 }
331400 matches, e = matcher.Match(value)
332401 lock.Lock()
333402 err = e
348417 return false
349418 }
350419
351 if !mayChange {
420 if stopTrying != nil {
352421 return true
353422 }
354423
355424 select {
356425 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 }
358431 lock.Lock()
359 value, err = v, e
432 value, err, stopTrying = v, e, st
360433 lock.Unlock()
361434 if err == nil {
362 mayChange = assertion.matcherMayChange(matcher, value)
435 if stopTrying == nil {
436 stopTrying = assertion.matcherSaysStopTrying(matcher, value)
437 }
363438 matches, e = matcher.Match(value)
364439 lock.Lock()
365440 err = e
906906 })
907907 })
908908
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 })
9301038 })
9311039 })
9321040