Gomega supports passing arguments to functions via WithArguments()
Onsi Fakhouri
1 year, 7 months ago
307 | 307 | |
308 | 308 | #### Category 2: Making `Eventually` assertions on functions |
309 | 309 | |
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. | |
311 | 311 | |
312 | 312 | For example: |
313 | 313 | |
321 | 321 | |
322 | 322 | > Note that this example could have been written as `Eventually(client.FetchCount).Should(BeNumerically(">=", 17))` |
323 | 323 | |
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. | |
325 | 325 | |
326 | 326 | For example, consider a method that returns a value and an error: |
327 | 327 | |
336 | 336 | ``` |
337 | 337 | |
338 | 338 | 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. | |
339 | 354 | |
340 | 355 | 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: |
341 | 356 | |
342 | 357 | ```go |
343 | 358 | It("fetches the correct count", func(ctx SpecContext) { |
344 | 359 | Eventually(func() int { |
345 | return client.FetchCount(ctx) | |
360 | return client.FetchCount(ctx, "/users") | |
346 | 361 | }, ctx).Should(BeNumerically(">=", 17)) |
347 | 362 | }, SpecTimeout(time.Second)) |
348 | 363 | ``` |
349 | 364 | |
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. | |
351 | 377 | |
352 | 378 | The use of a context also allows you to specify a single timeout across a collection of `Eventually` assertions: |
353 | 379 | |
354 | 380 | ```go |
355 | 381 | It("adds a few books and checks the count", func(ctx SpecContext) { |
356 | intialCount := client.FetchCount(ctx) | |
382 | intialCount := client.FetchCount(ctx, "/items") | |
357 | 383 | client.AddItem(ctx, "foo") |
358 | 384 | 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")) | |
368 | 388 | }, SpecTimeout(time.Second * 5)) |
369 | 389 | ``` |
370 | 390 | |
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. | |
372 | 392 | |
373 | 393 | #### Category 3: Making assertions _in_ the function passed into `Eventually` |
374 | 394 | |
403 | 423 | |
404 | 424 | will rerun the function until all assertions pass. |
405 | 425 | |
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 | ||
406 | 439 | ### Consistently |
407 | 440 | |
408 | 441 | `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. |
423 | 456 | |
424 | 457 | 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. |
425 | 458 | |
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) | |
430 | 463 | ``` |
431 | 464 | |
432 | 465 | `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: |
265 | 265 | |
266 | 266 | **Category 2: Make Eventually assertions on functions** |
267 | 267 | |
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. | |
269 | 269 | |
270 | 270 | For example: |
271 | 271 | |
285 | 285 | |
286 | 286 | will pass only if and when the returned error is nil *and* the returned string satisfies the matcher. |
287 | 287 | |
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 | ||
288 | 294 | 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: |
289 | 295 | |
290 | 296 | It("fetches the correct count", func(ctx SpecContext) { |
291 | 297 | Eventually(func() int { |
292 | return client.FetchCount(ctx) | |
298 | return client.FetchCount(ctx, "/users") | |
293 | 299 | }, ctx).Should(BeNumerically(">=", 17)) |
294 | 300 | }, SpecTimeout(time.Second)) |
295 | 301 | |
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. | |
297 | 309 | |
298 | 310 | **Category 3: Making assertions _in_ the function passed into Eventually** |
299 | 311 | |
323 | 335 | |
324 | 336 | will rerun the function until all assertions pass. |
325 | 337 | |
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 | ||
326 | 349 | Finally, in addition to passing timeouts and a context to Eventually you can be more explicit with Eventually's chaining configuration methods: |
327 | 350 | |
328 | 351 | Eventually(..., "1s", "2s", ctx).Should(...) |
30 | 30 | type AsyncAssertion struct { |
31 | 31 | asyncType AsyncAssertionType |
32 | 32 | |
33 | actualIsFunc bool | |
34 | actual interface{} | |
33 | actualIsFunc bool | |
34 | actual interface{} | |
35 | argsToForward []interface{} | |
35 | 36 | |
36 | 37 | timeoutInterval time.Duration |
37 | 38 | pollingInterval time.Duration |
88 | 89 | return assertion |
89 | 90 | } |
90 | 91 | |
92 | func (assertion *AsyncAssertion) WithArguments(argsToForward ...interface{}) types.AsyncAssertion { | |
93 | assertion.argsToForward = argsToForward | |
94 | return assertion | |
95 | } | |
96 | ||
91 | 97 | func (assertion *AsyncAssertion) Should(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool { |
92 | 98 | assertion.g.THelper() |
93 | 99 | vetOptionalDescription("Asynchronous assertion", optionalDescription...) |
144 | 150 | `, assertion.asyncType, t, assertion.asyncType) |
145 | 151 | } |
146 | 152 | |
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(). | |
149 | 155 | |
150 | 156 | 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) | |
152 | 169 | } |
153 | 170 | |
154 | 171 | func (assertion *AsyncAssertion) buildActualPoller() (func() (interface{}, error), error) { |
157 | 174 | } |
158 | 175 | actualValue := reflect.ValueOf(assertion.actual) |
159 | 176 | actualType := reflect.TypeOf(assertion.actual) |
160 | numIn, numOut := actualType.NumIn(), actualType.NumOut() | |
177 | numIn, numOut, isVariadic := actualType.NumIn(), actualType.NumOut(), actualType.IsVariadic() | |
161 | 178 | |
162 | 179 | if numIn == 0 && numOut == 0 { |
163 | 180 | return nil, assertion.invalidFunctionError(actualType) |
168 | 185 | if takesGomega && numIn > 1 && actualType.In(1).Implements(contextType) { |
169 | 186 | takesContext = true |
170 | 187 | } |
188 | if takesContext && len(assertion.argsToForward) > 0 && reflect.TypeOf(assertion.argsToForward[0]).Implements(contextType) { | |
189 | takesContext = false | |
190 | } | |
171 | 191 | if !takesGomega && numOut == 0 { |
172 | 192 | return nil, assertion.invalidFunctionError(actualType) |
173 | 193 | } |
174 | 194 | 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() | |
186 | 196 | } |
187 | 197 | |
188 | 198 | var assertionFailure error |
200 | 210 | } |
201 | 211 | if takesContext { |
202 | 212 | 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)) | |
203 | 222 | } |
204 | 223 | |
205 | 224 | return func() (actual interface{}, err error) { |
1 | 1 | |
2 | 2 | import ( |
3 | 3 | "errors" |
4 | "fmt" | |
4 | 5 | "reflect" |
5 | 6 | "runtime" |
7 | "strings" | |
6 | 8 | "time" |
7 | 9 | |
8 | 10 | . "github.com/onsi/ginkgo/v2" |
788 | 790 | ig.G.Eventually(func(ctx context.Context) string { |
789 | 791 | return ctx.Value("key").(string) |
790 | 792 | }).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().")) | |
792 | 794 | Ω(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 | ||
793 | 883 | }) |
794 | 884 | }) |
795 | 885 | }) |
802 | 892 | |
803 | 893 | ig.G.Consistently(func(ctx context.Context) {}).Should(Equal("foo")) |
804 | 894 | Ω(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)")) | |
810 | 895 | Ω(ig.FailureSkip).Should(Equal([]int{2})) |
811 | 896 | |
812 | 897 | ig.G.Eventually(func(ctx context.Context, g Gomega) {}).Should(Equal("foo")) |
73 | 73 | Within(timeout time.Duration) AsyncAssertion |
74 | 74 | ProbeEvery(interval time.Duration) AsyncAssertion |
75 | 75 | WithContext(ctx context.Context) AsyncAssertion |
76 | WithArguments(argsToForward ...interface{}) AsyncAssertion | |
76 | 77 | } |
77 | 78 | |
78 | 79 | // Assertions are returned by Ω and Expect and enable assertions against Gomega matchers |