Skip to content

Commit e29de39

Browse files
author
Uwe Jugel
committed
catch unexpected exit and fail fast with new error
1 parent 429ee0b commit e29de39

1 file changed

Lines changed: 114 additions & 17 deletions

File tree

assert/assertions.go

Lines changed: 114 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -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)
20082012
func 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.
20602093
func (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)
21552227
func 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

Comments
 (0)