Codebase list golang-gomega / a2dc7c3
Gomega supports passing arguments to functions via WithArguments() Onsi Fakhouri 1 year, 7 months ago
5 changed file(s) with 206 addition(s) and 45 deletion(s). Raw diff Collapse all Expand all
307307
308308 #### Category 2: Making `Eventually` assertions on functions
309309
310 `Eventually` can be passed functions that **take no arguments** and **return at least one value**. When configured this way, `Eventually` will poll the function repeatedly and pass the first returned value to the matcher.
310 `Eventually` can be passed functions that **return at least one value**. When configured this way, `Eventually` will poll the function repeatedly and pass the first returned value to the matcher.
311311
312312 For example:
313313
321321
322322 > Note that this example could have been written as `Eventually(client.FetchCount).Should(BeNumerically(">=", 17))`
323323
324 If multple values are returned by the function, `Eventually` will pass the first value to the matcher and require that all others are zero-valued. This allows you to pass `Eventually` a function that returns a value and an error - a common pattern in Go.
324 If multiple values are returned by the function, `Eventually` will pass the first value to the matcher and require that all others are zero-valued. This allows you to pass `Eventually` a function that returns a value and an error - a common pattern in Go.
325325
326326 For example, consider a method that returns a value and an error:
327327
336336 ```
337337
338338 will pass only if and when the returned error is `nil` *and* the returned string satisfies the matcher.
339
340
341 Eventually can also accept functions that take arguments, however you must provide those arguments using `Eventually().WithArguments()`. For example, consider a function that takes a user-id and makes a network request to fetch a full name:
342
343 ```go
344 func FetchFullName(userId int) (string, error)
345 ```
346
347 You can poll this function like so:
348
349 ```go
350 Eventually(FetchFullName).WithArguments(1138).Should(Equal("Wookie"))
351 ```
352
353 `WithArguments()` supports multiple arugments as well as variadic arguments.
339354
340355 It is important to note that the function passed into Eventually is invoked **synchronously** when polled. `Eventually` does not (in fact, it cannot) kill the function if it takes longer to return than `Eventually`'s configured timeout. This is where using a `context.Context` can be helpful. Here is an example that leverages Gingko's support for interruptible nodes and spec timeouts:
341356
342357 ```go
343358 It("fetches the correct count", func(ctx SpecContext) {
344359 Eventually(func() int {
345 return client.FetchCount(ctx)
360 return client.FetchCount(ctx, "/users")
346361 }, ctx).Should(BeNumerically(">=", 17))
347362 }, SpecTimeout(time.Second))
348363 ```
349364
350 now when the spec times out both the `client.FetchCount` function and `Eventually` will be signaled and told to exit.
365 now when the spec times out both the `client.FetchCount` function and `Eventually` will be signaled and told to exit. you an also use `Eventually().WithContext(ctx)` to provide the context.
366
367
368 Since functions that take a context.Context as a first-argument are common in Go, `Eventually` supports automatically injecting the provided context into the function. This plays nicely with `WithArguments()` as well. You can rewrite the above example as:
369
370 ```go
371 It("fetches the correct count", func(ctx SpecContext) {
372 Eventually(client.FetchCount).WithContext(ctx).WithArguments("/users").Should(BeNumerically(">=", 17))
373 }, SpecTimeout(time.Second))
374 ```
375
376 now the `ctx` `SpecContext` is used both by `Eventually` and `client.FetchCount` and the `"/users"` argument is passed in after the `ctx` argument.
351377
352378 The use of a context also allows you to specify a single timeout across a collection of `Eventually` assertions:
353379
354380 ```go
355381 It("adds a few books and checks the count", func(ctx SpecContext) {
356 intialCount := client.FetchCount(ctx)
382 intialCount := client.FetchCount(ctx, "/items")
357383 client.AddItem(ctx, "foo")
358384 client.AddItem(ctx, "bar")
359 Eventually(func() {
360 return client.FetchCount(ctx)
361 }).WithContext(ctx).Should(BeNumerically(">=", 17))
362 Eventually(func() {
363 return client.FetchItems(ctx)
364 }).WithContext(ctx).Should(ContainElement("foo"))
365 Eventually(func() {
366 return client.FetchItems(ctx)
367 }).WithContext(ctx).Should(ContainElement("bar"))
385 Eventually(client.FetchCount).WithContext(ctx).WithArguments("/items").Should(BeNumerically("==", initialCount + 2))
386 Eventually(client.FetchItems).WithContext(ctx).Should(ContainElement("foo"))
387 Eventually(client.FetchItems).WithContext(ctx).Should(ContainElement("foo"))
368388 }, SpecTimeout(time.Second * 5))
369389 ```
370390
371 In addition, Gingko's `SpecContext` allows Goemga to tell Ginkgo about the status of a currently running `Eventually` whenever a Progress Report is generated. So, if a spec times out while running an `Eventually` Ginkgo will not only show you which `Eventually` was running when the timeout occured, but will also include the failure the `Eventually` was hitting when the timeout occurred.
391 In addition, Gingko's `SpecContext` allows Gomega to tell Ginkgo about the status of a currently running `Eventually` whenever a Progress Report is generated. So, if a spec times out while running an `Eventually` Ginkgo will not only show you which `Eventually` was running when the timeout occured, but will also include the failure the `Eventually` was hitting when the timeout occurred.
372392
373393 #### Category 3: Making assertions _in_ the function passed into `Eventually`
374394
403423
404424 will rerun the function until all assertions pass.
405425
426 You can also pass additional arugments to functions that take a Gomega. The only rule is that the Gomega argument must be first. If you also want to pass the context attached to `Eventually` you must ensure that is the second argument. For example:
427
428 ```go
429 Eventually(func(g Gomega, ctx context.Context, path string, expected ...string){
430 tok, err := client.GetToken(ctx)
431 g.Expect(err).NotTo(HaveOccurred())
432
433 elements, err := client.Fetch(ctx, tok, path)
434 g.Expect(err).NotTo(HaveOccurred())
435 g.Expect(elements).To(ConsistOf(expected))
436 }).WithContext(ctx).WithArguments("/names", "Joe", "Jane", "Sam").Should(Succeed())
437 ```
438
406439 ### Consistently
407440
408441 `Consistently` checks that an assertion passes for a period of time. It does this by polling its argument repeatedly during the period. It fails if the matcher ever fails during that period.
423456
424457 As with `Eventually`, the duration parameters can be `time.Duration`s, string representations of a `time.Duration` (e.g. `"200ms"`) or `float64`s that are interpreted as seconds.
425458
426 Also as with `Eventually`, `Consistently` supports chaining `WithTimeout` and `WithPolling` and `WithContext` in the form of:
427
428 ```go
429 Consistently(ACTUAL).WithTimeout(DURATION).WithPolling(POLLING_INTERVAL).WithContext(ctx).Should(MATCHER)
459 Also as with `Eventually`, `Consistently` supports chaining `WithTimeout`, `WithPolling`, `WithContext` and `WithArguments` in the form of:
460
461 ```go
462 Consistently(ACTUAL).WithTimeout(DURATION).WithPolling(POLLING_INTERVAL).WithContext(ctx).WithArguments(...).Should(MATCHER)
430463 ```
431464
432465 `Consistently` tries to capture the notion that something "does not eventually" happen. A common use-case is to assert that no goroutine writes to a channel for a period of time. If you pass `Consistently` an argument that is not a function, it simply passes that argument to the matcher. So we can assert that:
265265
266266 **Category 2: Make Eventually assertions on functions**
267267
268 Eventually can be passed functions that **take no arguments** and **return at least one value**. When configured this way, Eventually will poll the function repeatedly and pass the first returned value to the matcher.
268 Eventually can be passed functions that **return at least one value**. When configured this way, Eventually will poll the function repeatedly and pass the first returned value to the matcher.
269269
270270 For example:
271271
285285
286286 will pass only if and when the returned error is nil *and* the returned string satisfies the matcher.
287287
288 Eventually can also accept functions that take arguments, however you must provide those arguments using .WithArguments(). For example, consider a function that takes a user-id and makes a network request to fetch a full name:
289 func FetchFullName(userId int) (string, error)
290
291 You can poll this function like so:
292 Eventually(FetchFullName).WithArguments(1138).Should(Equal("Wookie"))
293
288294 It is important to note that the function passed into Eventually is invoked *synchronously* when polled. Eventually does not (in fact, it cannot) kill the function if it takes longer to return than Eventually's configured timeout. A common practice here is to use a context. Here's an example that combines Ginkgo's spec timeout support with Eventually:
289295
290296 It("fetches the correct count", func(ctx SpecContext) {
291297 Eventually(func() int {
292 return client.FetchCount(ctx)
298 return client.FetchCount(ctx, "/users")
293299 }, ctx).Should(BeNumerically(">=", 17))
294300 }, SpecTimeout(time.Second))
295301
296 now, when Ginkgo cancels the context both the FetchCount client and Gomega will be informed and can exit.
302 you an also use Eventually().WithContext(ctx) to pass in the context. Passed-in contexts play nicely with paseed-in arguments as long as the context appears first. You can rewrite the above example as:
303
304 It("fetches the correct count", func(ctx SpecContext) {
305 Eventually(client.FetchCount).WithContext(ctx).WithArguments("/users").Should(BeNumerically(">=", 17))
306 }, SpecTimeout(time.Second))
307
308 Either way the context passd to Eventually is also passed to the underlying funciton. Now, when Ginkgo cancels the context both the FetchCount client and Gomega will be informed and can exit.
297309
298310 **Category 3: Making assertions _in_ the function passed into Eventually**
299311
323335
324336 will rerun the function until all assertions pass.
325337
338 You can also pass additional arugments to functions that take a Gomega. The only rule is that the Gomega argument must be first. If you also want to pass the context attached to Eventually you must ensure that is the second argument. For example:
339
340 Eventually(func(g Gomega, ctx context.Context, path string, expected ...string){
341 tok, err := client.GetToken(ctx)
342 g.Expect(err).NotTo(HaveOccurred())
343
344 elements, err := client.Fetch(ctx, tok, path)
345 g.Expect(err).NotTo(HaveOccurred())
346 g.Expect(elements).To(ConsistOf(expected))
347 }).WithContext(ctx).WithArguments("/names", "Joe", "Jane", "Sam").Should(Succeed())
348
326349 Finally, in addition to passing timeouts and a context to Eventually you can be more explicit with Eventually's chaining configuration methods:
327350
328351 Eventually(..., "1s", "2s", ctx).Should(...)
3030 type AsyncAssertion struct {
3131 asyncType AsyncAssertionType
3232
33 actualIsFunc bool
34 actual interface{}
33 actualIsFunc bool
34 actual interface{}
35 argsToForward []interface{}
3536
3637 timeoutInterval time.Duration
3738 pollingInterval time.Duration
8889 return assertion
8990 }
9091
92 func (assertion *AsyncAssertion) WithArguments(argsToForward ...interface{}) types.AsyncAssertion {
93 assertion.argsToForward = argsToForward
94 return assertion
95 }
96
9197 func (assertion *AsyncAssertion) Should(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool {
9298 assertion.g.THelper()
9399 vetOptionalDescription("Asynchronous assertion", optionalDescription...)
144150 `, assertion.asyncType, t, assertion.asyncType)
145151 }
146152
147 func (assertion *AsyncAssertion) noConfiguredContextForFunctionError(t reflect.Type) error {
148 return fmt.Errorf(`The function passed to %s requested a context.Context, but no context has been provided to %s. Please pass one in using %s().WithContext().
153 func (assertion *AsyncAssertion) noConfiguredContextForFunctionError() error {
154 return fmt.Errorf(`The function passed to %s requested a context.Context, but no context has been provided. Please pass one in using %s().WithContext().
149155
150156 You can learn more at https://onsi.github.io/gomega/#eventually
151 `, assertion.asyncType, t, assertion.asyncType)
157 `, assertion.asyncType, assertion.asyncType)
158 }
159
160 func (assertion *AsyncAssertion) argumentMismatchError(t reflect.Type, numProvided int) error {
161 have := "have"
162 if numProvided == 1 {
163 have = "has"
164 }
165 return fmt.Errorf(`The function passed to %s has signature %s takes %d arguments but %d %s been provided. Please use %s().WithArguments() to pass the corect set of arguments.
166
167 You can learn more at https://onsi.github.io/gomega/#eventually
168 `, assertion.asyncType, t, t.NumIn(), numProvided, have, assertion.asyncType)
152169 }
153170
154171 func (assertion *AsyncAssertion) buildActualPoller() (func() (interface{}, error), error) {
157174 }
158175 actualValue := reflect.ValueOf(assertion.actual)
159176 actualType := reflect.TypeOf(assertion.actual)
160 numIn, numOut := actualType.NumIn(), actualType.NumOut()
177 numIn, numOut, isVariadic := actualType.NumIn(), actualType.NumOut(), actualType.IsVariadic()
161178
162179 if numIn == 0 && numOut == 0 {
163180 return nil, assertion.invalidFunctionError(actualType)
168185 if takesGomega && numIn > 1 && actualType.In(1).Implements(contextType) {
169186 takesContext = true
170187 }
188 if takesContext && len(assertion.argsToForward) > 0 && reflect.TypeOf(assertion.argsToForward[0]).Implements(contextType) {
189 takesContext = false
190 }
171191 if !takesGomega && numOut == 0 {
172192 return nil, assertion.invalidFunctionError(actualType)
173193 }
174194 if takesContext && assertion.ctx == nil {
175 return nil, assertion.noConfiguredContextForFunctionError(actualType)
176 }
177 remainingIn := numIn
178 if takesGomega {
179 remainingIn -= 1
180 }
181 if takesContext {
182 remainingIn -= 1
183 }
184 if remainingIn > 0 {
185 return nil, assertion.invalidFunctionError(actualType)
195 return nil, assertion.noConfiguredContextForFunctionError()
186196 }
187197
188198 var assertionFailure error
200210 }
201211 if takesContext {
202212 inValues = append(inValues, reflect.ValueOf(assertion.ctx))
213 }
214 for _, arg := range assertion.argsToForward {
215 inValues = append(inValues, reflect.ValueOf(arg))
216 }
217
218 if !isVariadic && numIn != len(inValues) {
219 return nil, assertion.argumentMismatchError(actualType, len(inValues))
220 } else if isVariadic && len(inValues) < numIn-1 {
221 return nil, assertion.argumentMismatchError(actualType, len(inValues))
203222 }
204223
205224 return func() (actual interface{}, err error) {
11
22 import (
33 "errors"
4 "fmt"
45 "reflect"
56 "runtime"
7 "strings"
68 "time"
79
810 . "github.com/onsi/ginkgo/v2"
788790 ig.G.Eventually(func(ctx context.Context) string {
789791 return ctx.Value("key").(string)
790792 }).Should(Equal("value"))
791 Ω(ig.FailureMessage).Should(ContainSubstring("The function passed to Eventually requested a context.Context, but no context has been provided to func(context.Context) string. Please pass one in using Eventually().WithContext()."))
793 Ω(ig.FailureMessage).Should(ContainSubstring("The function passed to Eventually requested a context.Context, but no context has been provided. Please pass one in using Eventually().WithContext()."))
792794 Ω(ig.FailureSkip).Should(Equal([]int{2}))
795 })
796 })
797 })
798
799 Context("when passed a function that takes additional arguments", func() {
800 Context("with just arguments", func() {
801 It("forwards those arguments along", func() {
802 Eventually(func(a int, b string) string {
803 return fmt.Sprintf("%d - %s", a, b)
804 }).WithArguments(10, "four").Should(Equal("10 - four"))
805
806 Eventually(func(a int, b string, c ...int) string {
807 return fmt.Sprintf("%d - %s (%d%d%d)", a, b, c[0], c[1], c[2])
808 }).WithArguments(10, "four", 5, 1, 0).Should(Equal("10 - four (510)"))
809 })
810 })
811
812 Context("with a Gomega arugment as well", func() {
813 It("can also forward arguments alongside a Gomega", func() {
814 Eventually(func(g Gomega, a int, b int) {
815 g.Expect(a).To(Equal(b))
816 }).WithArguments(10, 3).ShouldNot(Succeed())
817 Eventually(func(g Gomega, a int, b int) {
818 g.Expect(a).To(Equal(b))
819 }).WithArguments(3, 3).Should(Succeed())
820 })
821 })
822
823 Context("with a context arugment as well", func() {
824 It("can also forward arguments alongside a context", func() {
825 ctx := context.WithValue(context.Background(), "key", "value")
826 Eventually(func(ctx context.Context, animal string) string {
827 return ctx.Value("key").(string) + " " + animal
828 }).WithArguments("pony").WithContext(ctx).Should(Equal("value pony"))
829 })
830 })
831
832 Context("with Gomega and context arugments", func() {
833 It("forwards arguments alongside both", func() {
834 ctx := context.WithValue(context.Background(), "key", "I have")
835 f := func(g Gomega, ctx context.Context, count int, zoo ...string) {
836 sentence := fmt.Sprintf("%s %d animals: %s", ctx.Value("key"), count, strings.Join(zoo, ", "))
837 g.Expect(sentence).To(Equal("I have 3 animals: dog, cat, pony"))
838 }
839
840 Eventually(f).WithArguments(3, "dog", "cat", "pony").WithContext(ctx).Should(Succeed())
841 Eventually(f).WithArguments(2, "dog", "cat").WithContext(ctx).Should(MatchError(ContainSubstring("Expected\n <string>: I have 2 animals: dog, cat\nto equal\n <string>: I have 3 animals: dog, cat, pony")))
842 })
843 })
844
845 Context("with a context that is in the argument list", func() {
846 It("does not forward the configured context", func() {
847 ctxA := context.WithValue(context.Background(), "key", "A")
848 ctxB := context.WithValue(context.Background(), "key", "B")
849
850 Eventually(func(ctx context.Context, a string) string {
851 return ctx.Value("key").(string) + " " + a
852 }).WithContext(ctxA).WithArguments(ctxB, "C").Should(Equal("B C"))
853 })
854 })
855
856 Context("and an incorrect number of arguments is provided", func() {
857 It("errors", func() {
858 ig.G.Eventually(func(a int) string {
859 return ""
860 }).Should(Equal("foo"))
861 Ω(ig.FailureMessage).Should(ContainSubstring("The function passed to Eventually has signature func(int) string takes 1 arguments but 0 have been provided. Please use Eventually().WithArguments() to pass the corect set of arguments."))
862
863 ig.G.Eventually(func(a int, b int) string {
864 return ""
865 }).WithArguments(1).Should(Equal("foo"))
866 Ω(ig.FailureMessage).Should(ContainSubstring("The function passed to Eventually has signature func(int, int) string takes 2 arguments but 1 has been provided. Please use Eventually().WithArguments() to pass the corect set of arguments."))
867
868 ig.G.Eventually(func(a int, b int) string {
869 return ""
870 }).WithArguments(1, 2, 3).Should(Equal("foo"))
871 Ω(ig.FailureMessage).Should(ContainSubstring("The function passed to Eventually has signature func(int, int) string takes 2 arguments but 3 have been provided. Please use Eventually().WithArguments() to pass the corect set of arguments."))
872
873 ig.G.Eventually(func(g Gomega, a int, b int) string {
874 return ""
875 }).WithArguments(1, 2, 3).Should(Equal("foo"))
876 Ω(ig.FailureMessage).Should(ContainSubstring("The function passed to Eventually has signature func(types.Gomega, int, int) string takes 3 arguments but 4 have been provided. Please use Eventually().WithArguments() to pass the corect set of arguments."))
877
878 ig.G.Eventually(func(a int, b int, c ...int) string {
879 return ""
880 }).WithArguments(1).Should(Equal("foo"))
881 Ω(ig.FailureMessage).Should(ContainSubstring("The function passed to Eventually has signature func(int, int, ...int) string takes 3 arguments but 1 has been provided. Please use Eventually().WithArguments() to pass the corect set of arguments."))
882
793883 })
794884 })
795885 })
802892
803893 ig.G.Consistently(func(ctx context.Context) {}).Should(Equal("foo"))
804894 Ω(ig.FailureMessage).Should(ContainSubstring("The function passed to Consistently had an invalid signature of func(context.Context)"))
805 Ω(ig.FailureSkip).Should(Equal([]int{2}))
806
807 ig = NewInstrumentedGomega()
808 ig.G.Eventually(func(g Gomega, foo string) {}).Should(Equal("foo"))
809 Ω(ig.FailureMessage).Should(ContainSubstring("The function passed to Eventually had an invalid signature of func(types.Gomega, string)"))
810895 Ω(ig.FailureSkip).Should(Equal([]int{2}))
811896
812897 ig.G.Eventually(func(ctx context.Context, g Gomega) {}).Should(Equal("foo"))
7373 Within(timeout time.Duration) AsyncAssertion
7474 ProbeEvery(interval time.Duration) AsyncAssertion
7575 WithContext(ctx context.Context) AsyncAssertion
76 WithArguments(argsToForward ...interface{}) AsyncAssertion
7677 }
7778
7879 // Assertions are returned by Ω and Expect and enable assertions against Gomega matchers