Skip to content

Commit a2dc7c3

Browse files
committed
Gomega supports passing arguments to functions via WithArguments()
1 parent e2091c5 commit a2dc7c3

File tree

5 files changed

+204
-43
lines changed

5 files changed

+204
-43
lines changed

docs/index.md

+50-17
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ In both cases you should always pass `Eventually` a function that, when polled,
308308

309309
#### Category 2: Making `Eventually` assertions on functions
310310

311-
`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.
311+
`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.
312312

313313
For example:
314314

@@ -322,7 +322,7 @@ will repeatedly poll `client.FetchCount` until the `BeNumerically` matcher is sa
322322

323323
> Note that this example could have been written as `Eventually(client.FetchCount).Should(BeNumerically(">=", 17))`
324324
325-
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.
325+
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.
326326

327327
For example, consider a method that returns a value and an error:
328328

@@ -338,38 +338,58 @@ Eventually(FetchFromDB).Should(Equal("got it"))
338338

339339
will pass only if and when the returned error is `nil` *and* the returned string satisfies the matcher.
340340

341+
342+
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:
343+
344+
```go
345+
func FetchFullName(userId int) (string, error)
346+
```
347+
348+
You can poll this function like so:
349+
350+
```go
351+
Eventually(FetchFullName).WithArguments(1138).Should(Equal("Wookie"))
352+
```
353+
354+
`WithArguments()` supports multiple arugments as well as variadic arguments.
355+
341356
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:
342357

343358
```go
344359
It("fetches the correct count", func(ctx SpecContext) {
345360
Eventually(func() int {
346-
return client.FetchCount(ctx)
361+
return client.FetchCount(ctx, "/users")
347362
}, ctx).Should(BeNumerically(">=", 17))
348363
}, SpecTimeout(time.Second))
349364
```
350365

351-
now when the spec times out both the `client.FetchCount` function and `Eventually` will be signaled and told to exit.
366+
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.
367+
368+
369+
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:
370+
371+
```go
372+
It("fetches the correct count", func(ctx SpecContext) {
373+
Eventually(client.FetchCount).WithContext(ctx).WithArguments("/users").Should(BeNumerically(">=", 17))
374+
}, SpecTimeout(time.Second))
375+
```
376+
377+
now the `ctx` `SpecContext` is used both by `Eventually` and `client.FetchCount` and the `"/users"` argument is passed in after the `ctx` argument.
352378

353379
The use of a context also allows you to specify a single timeout across a collection of `Eventually` assertions:
354380

355381
```go
356382
It("adds a few books and checks the count", func(ctx SpecContext) {
357-
intialCount := client.FetchCount(ctx)
383+
intialCount := client.FetchCount(ctx, "/items")
358384
client.AddItem(ctx, "foo")
359385
client.AddItem(ctx, "bar")
360-
Eventually(func() {
361-
return client.FetchCount(ctx)
362-
}).WithContext(ctx).Should(BeNumerically(">=", 17))
363-
Eventually(func() {
364-
return client.FetchItems(ctx)
365-
}).WithContext(ctx).Should(ContainElement("foo"))
366-
Eventually(func() {
367-
return client.FetchItems(ctx)
368-
}).WithContext(ctx).Should(ContainElement("bar"))
386+
Eventually(client.FetchCount).WithContext(ctx).WithArguments("/items").Should(BeNumerically("==", initialCount + 2))
387+
Eventually(client.FetchItems).WithContext(ctx).Should(ContainElement("foo"))
388+
Eventually(client.FetchItems).WithContext(ctx).Should(ContainElement("foo"))
369389
}, SpecTimeout(time.Second * 5))
370390
```
371391

372-
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.
392+
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.
373393

374394
#### Category 3: Making assertions _in_ the function passed into `Eventually`
375395

@@ -404,6 +424,19 @@ Eventually(func(g Gomega) {
404424

405425
will rerun the function until all assertions pass.
406426

427+
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:
428+
429+
```go
430+
Eventually(func(g Gomega, ctx context.Context, path string, expected ...string){
431+
tok, err := client.GetToken(ctx)
432+
g.Expect(err).NotTo(HaveOccurred())
433+
434+
elements, err := client.Fetch(ctx, tok, path)
435+
g.Expect(err).NotTo(HaveOccurred())
436+
g.Expect(elements).To(ConsistOf(expected))
437+
}).WithContext(ctx).WithArguments("/names", "Joe", "Jane", "Sam").Should(Succeed())
438+
```
439+
407440
### Consistently
408441

409442
`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.
@@ -424,10 +457,10 @@ Consistently(ACTUAL, (DURATION), (POLLING_INTERVAL), (context.Context)).Should(M
424457

425458
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.
426459

427-
Also as with `Eventually`, `Consistently` supports chaining `WithTimeout` and `WithPolling` and `WithContext` in the form of:
460+
Also as with `Eventually`, `Consistently` supports chaining `WithTimeout`, `WithPolling`, `WithContext` and `WithArguments` in the form of:
428461

429462
```go
430-
Consistently(ACTUAL).WithTimeout(DURATION).WithPolling(POLLING_INTERVAL).WithContext(ctx).Should(MATCHER)
463+
Consistently(ACTUAL).WithTimeout(DURATION).WithPolling(POLLING_INTERVAL).WithContext(ctx).WithArguments(...).Should(MATCHER)
431464
```
432465

433466
`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:

gomega_dsl.go

+26-3
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ this will trigger Go's race detector as the goroutine polling via Eventually wil
266266
267267
**Category 2: Make Eventually assertions on functions**
268268
269-
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.
269+
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.
270270
271271
For example:
272272
@@ -286,15 +286,27 @@ Then
286286
287287
will pass only if and when the returned error is nil *and* the returned string satisfies the matcher.
288288
289+
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:
290+
func FetchFullName(userId int) (string, error)
291+
292+
You can poll this function like so:
293+
Eventually(FetchFullName).WithArguments(1138).Should(Equal("Wookie"))
294+
289295
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:
290296
291297
It("fetches the correct count", func(ctx SpecContext) {
292298
Eventually(func() int {
293-
return client.FetchCount(ctx)
299+
return client.FetchCount(ctx, "/users")
294300
}, ctx).Should(BeNumerically(">=", 17))
295301
}, SpecTimeout(time.Second))
296302
297-
now, when Ginkgo cancels the context both the FetchCount client and Gomega will be informed and can exit.
303+
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:
304+
305+
It("fetches the correct count", func(ctx SpecContext) {
306+
Eventually(client.FetchCount).WithContext(ctx).WithArguments("/users").Should(BeNumerically(">=", 17))
307+
}, SpecTimeout(time.Second))
308+
309+
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.
298310
299311
**Category 3: Making assertions _in_ the function passed into Eventually**
300312
@@ -324,6 +336,17 @@ For example:
324336
325337
will rerun the function until all assertions pass.
326338
339+
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:
340+
341+
Eventually(func(g Gomega, ctx context.Context, path string, expected ...string){
342+
tok, err := client.GetToken(ctx)
343+
g.Expect(err).NotTo(HaveOccurred())
344+
345+
elements, err := client.Fetch(ctx, tok, path)
346+
g.Expect(err).NotTo(HaveOccurred())
347+
g.Expect(elements).To(ConsistOf(expected))
348+
}).WithContext(ctx).WithArguments("/names", "Joe", "Jane", "Sam").Should(Succeed())
349+
327350
Finally, in addition to passing timeouts and a context to Eventually you can be more explicit with Eventually's chaining configuration methods:
328351
329352
Eventually(..., "1s", "2s", ctx).Should(...)

internal/async_assertion.go

+36-17
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,9 @@ func (at AsyncAssertionType) String() string {
3131
type AsyncAssertion struct {
3232
asyncType AsyncAssertionType
3333

34-
actualIsFunc bool
35-
actual interface{}
34+
actualIsFunc bool
35+
actual interface{}
36+
argsToForward []interface{}
3637

3738
timeoutInterval time.Duration
3839
pollingInterval time.Duration
@@ -89,6 +90,11 @@ func (assertion *AsyncAssertion) WithContext(ctx context.Context) types.AsyncAss
8990
return assertion
9091
}
9192

93+
func (assertion *AsyncAssertion) WithArguments(argsToForward ...interface{}) types.AsyncAssertion {
94+
assertion.argsToForward = argsToForward
95+
return assertion
96+
}
97+
9298
func (assertion *AsyncAssertion) Should(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool {
9399
assertion.g.THelper()
94100
vetOptionalDescription("Asynchronous assertion", optionalDescription...)
@@ -145,11 +151,22 @@ You can learn more at https://onsi.github.io/gomega/#eventually
145151
`, assertion.asyncType, t, assertion.asyncType)
146152
}
147153

148-
func (assertion *AsyncAssertion) noConfiguredContextForFunctionError(t reflect.Type) error {
149-
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().
154+
func (assertion *AsyncAssertion) noConfiguredContextForFunctionError() error {
155+
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().
150156
151157
You can learn more at https://onsi.github.io/gomega/#eventually
152-
`, assertion.asyncType, t, assertion.asyncType)
158+
`, assertion.asyncType, assertion.asyncType)
159+
}
160+
161+
func (assertion *AsyncAssertion) argumentMismatchError(t reflect.Type, numProvided int) error {
162+
have := "have"
163+
if numProvided == 1 {
164+
have = "has"
165+
}
166+
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.
167+
168+
You can learn more at https://onsi.github.io/gomega/#eventually
169+
`, assertion.asyncType, t, t.NumIn(), numProvided, have, assertion.asyncType)
153170
}
154171

155172
func (assertion *AsyncAssertion) buildActualPoller() (func() (interface{}, error), error) {
@@ -158,7 +175,7 @@ func (assertion *AsyncAssertion) buildActualPoller() (func() (interface{}, error
158175
}
159176
actualValue := reflect.ValueOf(assertion.actual)
160177
actualType := reflect.TypeOf(assertion.actual)
161-
numIn, numOut := actualType.NumIn(), actualType.NumOut()
178+
numIn, numOut, isVariadic := actualType.NumIn(), actualType.NumOut(), actualType.IsVariadic()
162179

163180
if numIn == 0 && numOut == 0 {
164181
return nil, assertion.invalidFunctionError(actualType)
@@ -169,21 +186,14 @@ func (assertion *AsyncAssertion) buildActualPoller() (func() (interface{}, error
169186
if takesGomega && numIn > 1 && actualType.In(1).Implements(contextType) {
170187
takesContext = true
171188
}
189+
if takesContext && len(assertion.argsToForward) > 0 && reflect.TypeOf(assertion.argsToForward[0]).Implements(contextType) {
190+
takesContext = false
191+
}
172192
if !takesGomega && numOut == 0 {
173193
return nil, assertion.invalidFunctionError(actualType)
174194
}
175195
if takesContext && assertion.ctx == nil {
176-
return nil, assertion.noConfiguredContextForFunctionError(actualType)
177-
}
178-
remainingIn := numIn
179-
if takesGomega {
180-
remainingIn -= 1
181-
}
182-
if takesContext {
183-
remainingIn -= 1
184-
}
185-
if remainingIn > 0 {
186-
return nil, assertion.invalidFunctionError(actualType)
196+
return nil, assertion.noConfiguredContextForFunctionError()
187197
}
188198

189199
var assertionFailure error
@@ -202,6 +212,15 @@ func (assertion *AsyncAssertion) buildActualPoller() (func() (interface{}, error
202212
if takesContext {
203213
inValues = append(inValues, reflect.ValueOf(assertion.ctx))
204214
}
215+
for _, arg := range assertion.argsToForward {
216+
inValues = append(inValues, reflect.ValueOf(arg))
217+
}
218+
219+
if !isVariadic && numIn != len(inValues) {
220+
return nil, assertion.argumentMismatchError(actualType, len(inValues))
221+
} else if isVariadic && len(inValues) < numIn-1 {
222+
return nil, assertion.argumentMismatchError(actualType, len(inValues))
223+
}
205224

206225
return func() (actual interface{}, err error) {
207226
var values []reflect.Value

0 commit comments

Comments
 (0)