@@ -2004,14 +2004,34 @@ type tHelper = interface {
20042004// Eventually asserts that given condition will be met in waitFor time,
20052005// periodically checking target function each tick.
20062006//
2007+ // If the condition does not return normally, but instead calls [runtime.Goexit],
2008+ // the assertion fails immediately. This usually means that the condition called
2009+ // t.FailNow() on the outer 't'.
2010+ //
20072011// assert.Eventually(t, func() bool { return true; }, time.Second, 10*time.Millisecond)
20082012func Eventually (t TestingT , condition func () bool , waitFor time.Duration , tick time.Duration , msgAndArgs ... interface {}) bool {
20092013 if h , ok := t .(tHelper ); ok {
20102014 h .Helper ()
20112015 }
20122016
2013- ch := make (chan bool , 1 )
2014- checkCond := func () { ch <- condition () }
2017+ const (
2018+ conditionExitedUnexpectedly = iota
2019+ conditionReturnedTrue
2020+ conditionReturnedFalse
2021+ )
2022+
2023+ resultCh := make (chan int , 1 )
2024+ checkCond := func () {
2025+ result := conditionExitedUnexpectedly
2026+ defer func () {
2027+ resultCh <- result
2028+ }()
2029+ if condition () {
2030+ result = conditionReturnedTrue
2031+ } else {
2032+ result = conditionReturnedFalse
2033+ }
2034+ }
20152035
20162036 timer := time .NewTimer (waitFor )
20172037 defer timer .Stop ()
@@ -2031,11 +2051,20 @@ func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick t
20312051 case <- tickC :
20322052 tickC = nil
20332053 go checkCond ()
2034- case v := <- ch :
2035- if v {
2054+ case result := <- resultCh :
2055+ switch result {
2056+ case conditionExitedUnexpectedly :
2057+ // Condition exited via [runtime.Goexit]. This usually means
2058+ // that the condition called t.FailNow() on the outer 't'.
2059+ return Fail (t , "Condition exited unexpectedly" , msgAndArgs ... )
2060+ case conditionReturnedTrue :
20362061 return true
2062+ case conditionReturnedFalse :
2063+ // All good, continue checking.
2064+ fallthrough
2065+ default :
2066+ tickC = ticker .C
20372067 }
2038- tickC = ticker .C
20392068 }
20402069 }
20412070}
@@ -2046,6 +2075,10 @@ type CollectT struct {
20462075 // If it's non-nil but len(c.errors) == 0, this is also a failure
20472076 // obtained by direct c.FailNow() call.
20482077 errors []error
2078+
2079+ // exited is set to true if FailNow was called to indicate that the test
2080+ // exited correctly via runtime.Goexit.
2081+ exited bool
20492082}
20502083
20512084// Helper is like [testing.T.Helper] but does nothing.
@@ -2059,6 +2092,7 @@ func (c *CollectT) Errorf(format string, args ...interface{}) {
20592092// FailNow stops execution by calling runtime.Goexit.
20602093func (c * CollectT ) FailNow () {
20612094 c .fail ()
2095+ c .exited = true
20622096 runtime .Goexit ()
20632097}
20642098
@@ -2091,6 +2125,12 @@ func (c *CollectT) failed() bool {
20912125// If the condition is not met before waitFor, the collected errors of
20922126// the last tick are copied to t.
20932127//
2128+ // If the condition does not return normally, but instead calls [runtime.Goexit],
2129+ // and the exit was not via 'collect.FailNow()', the assertion fails immediately.
2130+ // This usually means that the condition called t.FailNow() on the outer 't'.
2131+ // Use [CollectT.FailNow] or 'require' functions on the provided 'collect' to
2132+ // only fail the current tick.
2133+ //
20942134// externalValue := false
20952135// go func() {
20962136// time.Sleep(8*time.Second)
@@ -2105,15 +2145,35 @@ func EventuallyWithT(t TestingT, condition func(collect *CollectT), waitFor time
21052145 h .Helper ()
21062146 }
21072147
2148+ const (
2149+ conditionExitedUnexpectedly = iota
2150+ conditionFailed
2151+ conditionSucceeded
2152+ )
2153+
21082154 var lastFinishedTickErrs []error
2109- ch := make (chan * CollectT , 1 )
2155+ ch := make (chan int , 1 )
21102156
21112157 checkCond := func () {
2158+ result := conditionExitedUnexpectedly
21122159 collect := new (CollectT )
21132160 defer func () {
2114- ch <- collect
2161+ if collect .exited {
2162+ // Condition exited via [CollectT.FailNow], which is a regular
2163+ // way to fail the condition early and exit the goroutine.
2164+ result = conditionFailed
2165+ }
2166+ // Keep the collected tick errors, so that they can be copied to 't'
2167+ // when timeout is reached or there is an unexpected exit.
2168+ lastFinishedTickErrs = collect .errors
2169+ ch <- result
21152170 }()
21162171 condition (collect )
2172+ if collect .failed () {
2173+ result = conditionFailed
2174+ } else {
2175+ result = conditionSucceeded
2176+ }
21172177 }
21182178
21192179 timer := time .NewTimer (waitFor )
@@ -2137,28 +2197,56 @@ func EventuallyWithT(t TestingT, condition func(collect *CollectT), waitFor time
21372197 case <- tickC :
21382198 tickC = nil
21392199 go checkCond ()
2140- case collect := <- ch :
2141- if ! collect .failed () {
2200+ case result := <- ch :
2201+ switch result {
2202+ case conditionExitedUnexpectedly :
2203+ for _ , err := range lastFinishedTickErrs {
2204+ t .Errorf ("%v" , err )
2205+ }
2206+ return Fail (t , "Condition exited unexpectedly" , msgAndArgs ... )
2207+ case conditionSucceeded :
21422208 return true
2209+ case conditionFailed :
2210+ // All good, continue checking.
2211+ fallthrough
2212+ default :
2213+ tickC = ticker .C
21432214 }
2144- // Keep the errors from the last ended condition, so that they can be copied to t if timeout is reached.
2145- lastFinishedTickErrs = collect .errors
2146- tickC = ticker .C
21472215 }
21482216 }
21492217}
21502218
21512219// Never asserts that the given condition doesn't satisfy in waitFor time,
21522220// periodically checking the target function each tick.
21532221//
2222+ // If the condition does not return normally, but instead calls [runtime.Goexit],
2223+ // the assertion fails immediately. This usually means that the condition called
2224+ // t.FailNow() on the outer 't'.
2225+ //
21542226// assert.Never(t, func() bool { return false; }, time.Second, 10*time.Millisecond)
21552227func Never (t TestingT , condition func () bool , waitFor time.Duration , tick time.Duration , msgAndArgs ... interface {}) bool {
21562228 if h , ok := t .(tHelper ); ok {
21572229 h .Helper ()
21582230 }
21592231
2160- ch := make (chan bool , 1 )
2161- checkCond := func () { ch <- condition () }
2232+ const (
2233+ conditionExitedUnexpectedly = iota
2234+ conditionReturnedTrue
2235+ conditionReturnedFalse
2236+ )
2237+
2238+ ch := make (chan int , 1 )
2239+ checkCond := func () {
2240+ result := conditionExitedUnexpectedly
2241+ defer func () {
2242+ ch <- result
2243+ }()
2244+ if condition () {
2245+ result = conditionReturnedTrue
2246+ } else {
2247+ result = conditionReturnedFalse
2248+ }
2249+ }
21622250
21632251 timer := time .NewTimer (waitFor )
21642252 defer timer .Stop ()
@@ -2178,11 +2266,20 @@ func Never(t TestingT, condition func() bool, waitFor time.Duration, tick time.D
21782266 case <- tickC :
21792267 tickC = nil
21802268 go checkCond ()
2181- case v := <- ch :
2182- if v {
2269+ case result := <- ch :
2270+ switch result {
2271+ case conditionExitedUnexpectedly :
2272+ // Condition exited via [runtime.Goexit]. This usually means
2273+ // that the condition called t.FailNow() on the outer 't'.
2274+ return Fail (t , "Condition exited unexpectedly" , msgAndArgs ... )
2275+ case conditionReturnedTrue :
21832276 return Fail (t , "Condition satisfied" , msgAndArgs ... )
2277+ case conditionReturnedFalse :
2278+ // All good, continue checking.
2279+ fallthrough
2280+ default :
2281+ tickC = ticker .C
21842282 }
2185- tickC = ticker .C
21862283 }
21872284 }
21882285}
0 commit comments