Codebase list golang-gomega / e5105cf
When passed a context and no explicit timeout, Eventually will only timeout when the context is cancelled This enables using the same context across a series of Eventually's without worrying about specifying a specific timeout for each one. If an explicit timeout _is_ set, then that timeout is used alognside the context. Onsi Fakhouri 1 year, 6 months ago
4 changed file(s) with 179 addition(s) and 73 deletion(s). Raw diff Collapse all Expand all
263263 ```go
264264 Eventually(ACTUAL).WithTimeout(TIMEOUT).WithPolling(POLLING_INTERVAL).WithContext(ctx).Should(MATCHER)
265265 ```
266
267 When no explicit timeout is provided, `Eventually` will use the default timeout. However if no explicit timeout is provided _and_ a context is provided, `Eventually` will not apply a timeout but will instead keep trying until the context is cancelled. If both a context and a timeout are provided, `Eventually` will keep trying until either the context is cancelled or time runs out, whichever comes first.
266268
267269 Eventually works with any Gomega compatible matcher and supports making assertions against three categories of `ACTUAL` value:
268270
473475 As with `Eventually`, you can also pass `Consistently` a function. In fact, `Consistently` works with the three categories of `ACTUAL` value outlined for `Eventually` in the section above.
474476
475477 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).
478
479 When no explicit duration is provided, `Consistently` will use the default duration. Unlike `Eventually`, this behavior holds whether or not a context is provided.
476480
477481 > 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!
478482
11
22 import (
33 "context"
4 "errors"
45 "fmt"
56 "reflect"
67 "runtime"
78 "sync"
89 "time"
9 "errors"
1010
1111 "github.com/onsi/gomega/types"
1212 )
1616 Now()
1717 wasViaPanic() bool
1818 }
19
2019
2120 func asStopTryingError(actual interface{}) (StopTryingError, bool) {
2221 if actual == nil {
172171 return nil, fmt.Errorf("No values were returned by the function passed to Gomega"), stopTrying
173172 }
174173 actual := values[0].Interface()
175 if stopTryingErr, ok := asStopTryingError(actual); ok{
174 if stopTryingErr, ok := asStopTryingError(actual); ok {
176175 stopTrying = stopTryingErr
177176 }
178177 for i, extraValue := range values[1:] {
180179 if extra == nil {
181180 continue
182181 }
183 if stopTryingErr, ok := asStopTryingError(extra); ok{
182 if stopTryingErr, ok := asStopTryingError(extra); ok {
184183 stopTrying = stopTryingErr
185184 continue
186185 }
324323 return StopTrying("No future change is possible. Bailing out early")
325324 }
326325
326 func (assertion *AsyncAssertion) afterTimeout() <-chan time.Time {
327 if assertion.timeoutInterval >= 0 {
328 return time.After(assertion.timeoutInterval)
329 }
330
331 if assertion.asyncType == AsyncAssertionTypeConsistently {
332 return time.After(assertion.g.DurationBundle.ConsistentlyDuration)
333 } else {
334 if assertion.ctx == nil {
335 return time.After(assertion.g.DurationBundle.EventuallyTimeout)
336 } else {
337 return nil
338 }
339 }
340 }
341
342 func (assertion *AsyncAssertion) afterPolling() <-chan time.Time {
343 if assertion.pollingInterval >= 0 {
344 return time.After(assertion.pollingInterval)
345 }
346 if assertion.asyncType == AsyncAssertionTypeConsistently {
347 return time.After(assertion.g.DurationBundle.ConsistentlyPollingInterval)
348 } else {
349 return time.After(assertion.g.DurationBundle.EventuallyPollingInterval)
350 }
351 }
352
327353 type contextWithAttachProgressReporter interface {
328354 AttachProgressReporter(func() string) func()
329355 }
330356
331357 func (assertion *AsyncAssertion) match(matcher types.GomegaMatcher, desiredMatch bool, optionalDescription ...interface{}) bool {
332358 timer := time.Now()
333 timeout := time.After(assertion.timeoutInterval)
359 timeout := assertion.afterTimeout()
334360 lock := sync.Mutex{}
335361
336362 var matches bool
397423 }
398424
399425 select {
400 case <-time.After(assertion.pollingInterval):
426 case <-assertion.afterPolling():
401427 v, e, st := pollActual()
402428 if st != nil && st.wasViaPanic() {
403429 // we were told to stop trying via panic - which means we dont' have reasonable new values
437463 }
438464
439465 select {
440 case <-time.After(assertion.pollingInterval):
466 case <-assertion.afterPolling():
441467 v, e, st := pollActual()
442468 if st != nil && st.wasViaPanic() {
443469 // we were told to stop trying via panic - which means we made it this far and should return successfully
169169 })
170170 })
171171
172 Context("when the passed-in context is cancelled", func() {
173 It("stops and returns a failure", func() {
174 ctx, cancel := context.WithCancel(context.Background())
175 counter := 0
176 ig.G.Eventually(func() string {
177 counter++
178 if counter == 2 {
179 cancel()
180 } else if counter == 10 {
172 Context("with a passed-in context", func() {
173 Context("when the passed-in context is cancelled", func() {
174 It("stops and returns a failure", func() {
175 ctx, cancel := context.WithCancel(context.Background())
176 counter := 0
177 ig.G.Eventually(func() string {
178 counter++
179 if counter == 2 {
180 cancel()
181 } else if counter == 10 {
182 return MATCH
183 }
184 return NO_MATCH
185 }, time.Hour, ctx).Should(SpecMatch())
186 Ω(ig.FailureMessage).Should(ContainSubstring("Context was cancelled after"))
187 Ω(ig.FailureMessage).Should(ContainSubstring("positive: no match"))
188 })
189
190 It("can also be configured via WithContext()", func() {
191 ctx, cancel := context.WithCancel(context.Background())
192 counter := 0
193 ig.G.Eventually(func() string {
194 counter++
195 if counter == 2 {
196 cancel()
197 } else if counter == 10 {
198 return MATCH
199 }
200 return NO_MATCH
201 }, time.Hour).WithContext(ctx).Should(SpecMatch())
202 Ω(ig.FailureMessage).Should(ContainSubstring("Context was cancelled after"))
203 Ω(ig.FailureMessage).Should(ContainSubstring("positive: no match"))
204 })
205
206 It("counts as a failure for Consistently", func() {
207 ctx, cancel := context.WithCancel(context.Background())
208 counter := 0
209 ig.G.Consistently(func() string {
210 counter++
211 if counter == 2 {
212 cancel()
213 } else if counter == 10 {
214 return NO_MATCH
215 }
181216 return MATCH
182 }
183 return NO_MATCH
184 }, time.Hour, ctx).Should(SpecMatch())
185 Ω(ig.FailureMessage).Should(ContainSubstring("Context was cancelled after"))
186 Ω(ig.FailureMessage).Should(ContainSubstring("positive: no match"))
187 })
188
189 It("can also be configured via WithContext()", func() {
190 ctx, cancel := context.WithCancel(context.Background())
191 counter := 0
192 ig.G.Eventually(func() string {
193 counter++
194 if counter == 2 {
195 cancel()
196 } else if counter == 10 {
197 return MATCH
198 }
199 return NO_MATCH
200 }, time.Hour).WithContext(ctx).Should(SpecMatch())
201 Ω(ig.FailureMessage).Should(ContainSubstring("Context was cancelled after"))
202 Ω(ig.FailureMessage).Should(ContainSubstring("positive: no match"))
203 })
204
205 It("counts as a failure for Consistently", func() {
206 ctx, cancel := context.WithCancel(context.Background())
207 counter := 0
208 ig.G.Consistently(func() string {
209 counter++
210 if counter == 2 {
211 cancel()
212 } else if counter == 10 {
217 }, time.Hour).WithContext(ctx).Should(SpecMatch())
218 Ω(ig.FailureMessage).Should(ContainSubstring("Context was cancelled after"))
219 Ω(ig.FailureMessage).Should(ContainSubstring("positive: match"))
220 })
221 })
222
223 Context("when the passed-in context is a Ginkgo SpecContext that can take a progress reporter attachment", func() {
224 It("attaches a progress reporter context that allows it to report on demand", func() {
225 fakeSpecContext := &FakeGinkgoSpecContext{}
226 var message string
227 ctx := context.WithValue(context.Background(), "GINKGO_SPEC_CONTEXT", fakeSpecContext)
228 ig.G.Eventually(func() string {
229 if fakeSpecContext.Attached != nil {
230 message = fakeSpecContext.Attached()
231 }
213232 return NO_MATCH
214 }
215 return MATCH
216 }, time.Hour).WithContext(ctx).Should(SpecMatch())
217 Ω(ig.FailureMessage).Should(ContainSubstring("Context was cancelled after"))
218 Ω(ig.FailureMessage).Should(ContainSubstring("positive: match"))
219 })
220 })
221
222 Context("when the passed-in context is a Ginkgo SpecContext that can take a progress reporter attachment", func() {
223 It("attaches a progress reporter context that allows it to report on demand", func() {
224 fakeSpecContext := &FakeGinkgoSpecContext{}
225 var message string
226 ctx := context.WithValue(context.Background(), "GINKGO_SPEC_CONTEXT", fakeSpecContext)
227 ig.G.Eventually(func() string {
228 if fakeSpecContext.Attached != nil {
229 message = fakeSpecContext.Attached()
230 }
231 return NO_MATCH
232 }).WithTimeout(time.Millisecond * 20).WithContext(ctx).Should(Equal(MATCH))
233
234 Ω(message).Should(Equal("Expected\n <string>: no match\nto equal\n <string>: match"))
235 Ω(fakeSpecContext.Cancelled).Should(BeTrue())
233 }).WithTimeout(time.Millisecond * 20).WithContext(ctx).Should(Equal(MATCH))
234
235 Ω(message).Should(Equal("Expected\n <string>: no match\nto equal\n <string>: match"))
236 Ω(fakeSpecContext.Cancelled).Should(BeTrue())
237 })
238 })
239
240 Describe("the interaction between the context and the timeout", func() {
241 It("only relies on context cancellation when no explicit timeout is specified", func() {
242 ig.G.SetDefaultEventuallyTimeout(time.Millisecond * 10)
243 ig.G.SetDefaultEventuallyPollingInterval(time.Millisecond * 40)
244 t := time.Now()
245 ctx, cancel := context.WithCancel(context.Background())
246 iterations := 0
247 ig.G.Eventually(func() string {
248 iterations += 1
249 if time.Since(t) > time.Millisecond*200 {
250 cancel()
251 }
252 return "A"
253 }).WithContext(ctx).Should(Equal("B"))
254 Ω(time.Since(t)).Should(BeNumerically("~", time.Millisecond*200, time.Millisecond*100))
255 Ω(iterations).Should(BeNumerically("~", 200/40, 2))
256 Ω(ig.FailureMessage).Should(ContainSubstring("Context was cancelled after"))
257 })
258
259 It("uses the explicit timeout when it is provided", func() {
260 t := time.Now()
261 ctx, cancel := context.WithCancel(context.Background())
262 iterations := 0
263 ig.G.Eventually(func() string {
264 iterations += 1
265 if time.Since(t) > time.Millisecond*200 {
266 cancel()
267 }
268 return "A"
269 }).WithContext(ctx).WithTimeout(time.Millisecond * 80).ProbeEvery(time.Millisecond * 40).Should(Equal("B"))
270 Ω(time.Since(t)).Should(BeNumerically("~", time.Millisecond*80, time.Millisecond*40))
271 Ω(iterations).Should(BeNumerically("~", 80/40, 2))
272 Ω(ig.FailureMessage).Should(ContainSubstring("Timed out after"))
273 })
236274 })
237275 })
238276 })
349387 It("calls the optional description if it is a function", func() {
350388 ig.G.Consistently(NO_MATCH).Should(SpecMatch(), func() string { return "boop" })
351389 Ω(ig.FailureMessage).Should(ContainSubstring("boop"))
390 })
391 })
392
393 Context("with a passed-in context", func() {
394 Context("when the passed-in context is cancelled", func() {
395 It("counts as a failure for Consistently", func() {
396 ctx, cancel := context.WithCancel(context.Background())
397 counter := 0
398 ig.G.Consistently(func() string {
399 counter++
400 if counter == 2 {
401 cancel()
402 } else if counter == 10 {
403 return NO_MATCH
404 }
405 return MATCH
406 }, time.Hour).WithContext(ctx).Should(SpecMatch())
407 Ω(ig.FailureMessage).Should(ContainSubstring("Context was cancelled after"))
408 Ω(ig.FailureMessage).Should(ContainSubstring("positive: match"))
409 })
410 })
411
412 Describe("the interaction between the context and the timeout", func() {
413 It("only always uses the default interval even if not explicit duration is provided", func() {
414 ig.G.SetDefaultConsistentlyDuration(time.Millisecond * 200)
415 ig.G.SetDefaultConsistentlyPollingInterval(time.Millisecond * 40)
416 t := time.Now()
417 ctx, cancel := context.WithCancel(context.Background())
418 defer cancel()
419 iterations := 0
420 ig.G.Consistently(func() string {
421 iterations += 1
422 return "A"
423 }).WithContext(ctx).Should(Equal("A"))
424 Ω(time.Since(t)).Should(BeNumerically("~", time.Millisecond*200, time.Millisecond*100))
425 Ω(iterations).Should(BeNumerically("~", 200/40, 2))
426 Ω(ig.FailureMessage).Should(BeZero())
427 })
352428 })
353429 })
354430 })
5656 }
5757
5858 func (g *Gomega) EventuallyWithOffset(offset int, actual interface{}, args ...interface{}) types.AsyncAssertion {
59 timeoutInterval := g.DurationBundle.EventuallyTimeout
60 pollingInterval := g.DurationBundle.EventuallyPollingInterval
59 timeoutInterval := -time.Duration(1)
60 pollingInterval := -time.Duration(1)
6161 intervals := []interface{}{}
6262 var ctx context.Context
6363 for _, arg := range args {
8383 }
8484
8585 func (g *Gomega) ConsistentlyWithOffset(offset int, actual interface{}, args ...interface{}) types.AsyncAssertion {
86 timeoutInterval := g.DurationBundle.ConsistentlyDuration
87 pollingInterval := g.DurationBundle.ConsistentlyPollingInterval
86 timeoutInterval := -time.Duration(1)
87 pollingInterval := -time.Duration(1)
8888 intervals := []interface{}{}
8989 var ctx context.Context
9090 for _, arg := range args {