Skip to content
14 changes: 14 additions & 0 deletions assert/assertion_format.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,10 @@ func ErrorIsf(t TestingT, err error, target error, msg string, args ...interface
// Eventuallyf asserts that given condition will be met in waitFor time,
// periodically checking target function each tick.
//
// If the condition does not return normally, but instead calls [runtime.Goexit],
// the assertion fails immediately. This usually means that the condition called
// t.FailNow() on the outer 't'.
//
// assert.Eventuallyf(t, func() bool { return true; }, time.Second, 10*time.Millisecond, "error message %s", "formatted")
func Eventuallyf(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) bool {
if h, ok := t.(tHelper); ok {
Expand All @@ -185,6 +189,12 @@ func Eventuallyf(t TestingT, condition func() bool, waitFor time.Duration, tick
// If the condition is not met before waitFor, the collected errors of
// the last tick are copied to t.
//
// If the condition does not return normally, but instead calls [runtime.Goexit],
// and the exit was not via 'collect.FailNow()', the assertion fails immediately.
// This usually means that the condition called t.FailNow() on the outer 't'.
// Use [CollectT.FailNow] or 'require' functions on the provided 'collect' to
// only fail the current tick.
//
// externalValue := false
// go func() {
// time.Sleep(8*time.Second)
Expand Down Expand Up @@ -525,6 +535,10 @@ func Negativef(t TestingT, e interface{}, msg string, args ...interface{}) bool
// Neverf asserts that the given condition doesn't satisfy in waitFor time,
// periodically checking the target function each tick.
//
// If the condition does not return normally, but instead calls [runtime.Goexit],
// the assertion fails immediately. This usually means that the condition called
// t.FailNow() on the outer 't'.
//
// assert.Neverf(t, func() bool { return false; }, time.Second, 10*time.Millisecond, "error message %s", "formatted")
func Neverf(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) bool {
if h, ok := t.(tHelper); ok {
Expand Down
28 changes: 28 additions & 0 deletions assert/assertion_forward.go
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,10 @@ func (a *Assertions) Errorf(err error, msg string, args ...interface{}) bool {
// Eventually asserts that given condition will be met in waitFor time,
// periodically checking target function each tick.
//
// If the condition does not return normally, but instead calls [runtime.Goexit],
// the assertion fails immediately. This usually means that the condition called
// t.FailNow() on the outer 't'.
//
// a.Eventually(func() bool { return true; }, time.Second, 10*time.Millisecond)
func (a *Assertions) Eventually(condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
Expand All @@ -342,6 +346,12 @@ func (a *Assertions) Eventually(condition func() bool, waitFor time.Duration, ti
// If the condition is not met before waitFor, the collected errors of
// the last tick are copied to t.
//
// If the condition does not return normally, but instead calls [runtime.Goexit],
// and the exit was not via 'collect.FailNow()', the assertion fails immediately.
// This usually means that the condition called t.FailNow() on the outer 't'.
// Use [CollectT.FailNow] or 'require' functions on the provided 'collect' to
// only fail the current tick.
//
// externalValue := false
// go func() {
// time.Sleep(8*time.Second)
Expand All @@ -367,6 +377,12 @@ func (a *Assertions) EventuallyWithT(condition func(collect *CollectT), waitFor
// If the condition is not met before waitFor, the collected errors of
// the last tick are copied to t.
//
// If the condition does not return normally, but instead calls [runtime.Goexit],
// and the exit was not via 'collect.FailNow()', the assertion fails immediately.
// This usually means that the condition called t.FailNow() on the outer 't'.
// Use [CollectT.FailNow] or 'require' functions on the provided 'collect' to
// only fail the current tick.
//
// externalValue := false
// go func() {
// time.Sleep(8*time.Second)
Expand All @@ -386,6 +402,10 @@ func (a *Assertions) EventuallyWithTf(condition func(collect *CollectT), waitFor
// Eventuallyf asserts that given condition will be met in waitFor time,
// periodically checking target function each tick.
//
// If the condition does not return normally, but instead calls [runtime.Goexit],
// the assertion fails immediately. This usually means that the condition called
// t.FailNow() on the outer 't'.
//
// a.Eventuallyf(func() bool { return true; }, time.Second, 10*time.Millisecond, "error message %s", "formatted")
func (a *Assertions) Eventuallyf(condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
Expand Down Expand Up @@ -1039,6 +1059,10 @@ func (a *Assertions) Negativef(e interface{}, msg string, args ...interface{}) b
// Never asserts that the given condition doesn't satisfy in waitFor time,
// periodically checking the target function each tick.
//
// If the condition does not return normally, but instead calls [runtime.Goexit],
// the assertion fails immediately. This usually means that the condition called
// t.FailNow() on the outer 't'.
//
// a.Never(func() bool { return false; }, time.Second, 10*time.Millisecond)
func (a *Assertions) Never(condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
Expand All @@ -1050,6 +1074,10 @@ func (a *Assertions) Never(condition func() bool, waitFor time.Duration, tick ti
// Neverf asserts that the given condition doesn't satisfy in waitFor time,
// periodically checking the target function each tick.
//
// If the condition does not return normally, but instead calls [runtime.Goexit],
// the assertion fails immediately. This usually means that the condition called
// t.FailNow() on the outer 't'.
//
// a.Neverf(func() bool { return false; }, time.Second, 10*time.Millisecond, "error message %s", "formatted")
func (a *Assertions) Neverf(condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
Expand Down
154 changes: 133 additions & 21 deletions assert/assertions.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"runtime"
"runtime/debug"
"strings"
"sync/atomic"
"time"
"unicode"
"unicode/utf8"
Expand Down Expand Up @@ -2003,14 +2004,34 @@ type tHelper = interface {
// Eventually asserts that given condition will be met in waitFor time,
// periodically checking target function each tick.
//
// If the condition does not return normally, but instead calls [runtime.Goexit],
// the assertion fails immediately. This usually means that the condition called
// t.FailNow() on the outer 't'.
//
// assert.Eventually(t, func() bool { return true; }, time.Second, 10*time.Millisecond)
func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}

ch := make(chan bool, 1)
checkCond := func() { ch <- condition() }
const (
conditionExitedUnexpectedly = iota
conditionReturnedTrue
conditionReturnedFalse
)

ch := make(chan int, 1)
checkCond := func() {
result := conditionExitedUnexpectedly
defer func() {
ch <- result
}()
if condition() {
result = conditionReturnedTrue
} else {
result = conditionReturnedFalse
}
}

timer := time.NewTimer(waitFor)
defer timer.Stop()
Expand All @@ -2030,11 +2051,20 @@ func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick t
case <-tickC:
tickC = nil
go checkCond()
case v := <-ch:
if v {
case result := <-ch:
switch result {
case conditionExitedUnexpectedly:
// Condition exited via [runtime.Goexit]. This usually means
// that the condition called t.FailNow() on the outer 't'.
return Fail(t, "Condition exited unexpectedly", msgAndArgs...)
case conditionReturnedTrue:
return true
case conditionReturnedFalse:
// All good, continue checking.
fallthrough
default:
tickC = ticker.C
}
tickC = ticker.C
}
}
}
Expand All @@ -2045,6 +2075,10 @@ type CollectT struct {
// If it's non-nil but len(c.errors) == 0, this is also a failure
// obtained by direct c.FailNow() call.
errors []error

// exited is set to true if FailNow was called to indicate that the test
// exited correctly via runtime.Goexit.
exited bool
}

// Helper is like [testing.T.Helper] but does nothing.
Expand All @@ -2058,6 +2092,7 @@ func (c *CollectT) Errorf(format string, args ...interface{}) {
// FailNow stops execution by calling runtime.Goexit.
func (c *CollectT) FailNow() {
c.fail()
c.exited = true
runtime.Goexit()
}

Expand Down Expand Up @@ -2090,6 +2125,12 @@ func (c *CollectT) failed() bool {
// If the condition is not met before waitFor, the collected errors of
// the last tick are copied to t.
//
// If the condition does not return normally, but instead calls [runtime.Goexit],
// and the exit was not via 'collect.FailNow()', the assertion fails immediately.
// This usually means that the condition called t.FailNow() on the outer 't'.
// Use [CollectT.FailNow] or 'require' functions on the provided 'collect' to
// only fail the current tick.
//
// externalValue := false
// go func() {
// time.Sleep(8*time.Second)
Expand All @@ -2104,15 +2145,51 @@ func EventuallyWithT(t TestingT, condition func(collect *CollectT), waitFor time
h.Helper()
}

var lastFinishedTickErrs []error
ch := make(chan *CollectT, 1)
const (
conditionExitedUnexpectedly = iota
conditionFailed
conditionSucceeded
)

var lastFinishedTickErrs atomic.Value // of []error
ch := make(chan int, 1)

checkCond := func() {
result := conditionExitedUnexpectedly
collect := new(CollectT)
defer func() {
ch <- collect
// At this point, the condition has returned or exited. It is safe
// to check collect.errors and collect.exited, unless the user has
// created additional goroutines that access 'collect', which would
// be a misuse and is not supported.
if collect.exited {
// Condition exited via [CollectT.FailNow], which is a regular
// way to fail the condition early and exit the goroutine.
result = conditionFailed
}
// Keep the collected tick errors, so that they can be copied to 't'
// when timeout is reached or there is an unexpected exit.
// Always store the actual value of collect.errors, even if nil
lastFinishedTickErrs.Store(collect.errors)

ch <- result
}()
condition(collect)
if collect.failed() {
result = conditionFailed
} else {
result = conditionSucceeded
}
}

copyLastFinishedTickErrs := func() {
errs, ok := lastFinishedTickErrs.Load().([]error)
if !ok {
return
}
for _, err := range errs {
t.Errorf("%v", err)
}
}

timer := time.NewTimer(waitFor)
Expand All @@ -2129,35 +2206,61 @@ func EventuallyWithT(t TestingT, condition func(collect *CollectT), waitFor time
for {
select {
case <-timer.C:
for _, err := range lastFinishedTickErrs {
t.Errorf("%v", err)
}
copyLastFinishedTickErrs()
return Fail(t, "Condition never satisfied", msgAndArgs...)
case <-tickC:
tickC = nil
go checkCond()
case collect := <-ch:
if !collect.failed() {
case result := <-ch:
switch result {
case conditionExitedUnexpectedly:
// Condition exited via [runtime.Goexit]. This usually means
// that the condition called t.FailNow() on the outer 't'.
copyLastFinishedTickErrs()
return Fail(t, "Condition exited unexpectedly", msgAndArgs...)
case conditionSucceeded:
return true
case conditionFailed:
// All good, continue checking.
fallthrough
default:
tickC = ticker.C
}
// Keep the errors from the last ended condition, so that they can be copied to t if timeout is reached.
lastFinishedTickErrs = collect.errors
tickC = ticker.C
}
}
}

// Never asserts that the given condition doesn't satisfy in waitFor time,
// periodically checking the target function each tick.
//
// If the condition does not return normally, but instead calls [runtime.Goexit],
// the assertion fails immediately. This usually means that the condition called
// t.FailNow() on the outer 't'.
//
// assert.Never(t, func() bool { return false; }, time.Second, 10*time.Millisecond)
func Never(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}

ch := make(chan bool, 1)
checkCond := func() { ch <- condition() }
const (
conditionExitedUnexpectedly = iota
conditionReturnedTrue
conditionReturnedFalse
)

ch := make(chan int, 1)
checkCond := func() {
result := conditionExitedUnexpectedly
defer func() {
ch <- result
}()
if condition() {
result = conditionReturnedTrue
} else {
result = conditionReturnedFalse
}
}

timer := time.NewTimer(waitFor)
defer timer.Stop()
Expand All @@ -2177,11 +2280,20 @@ func Never(t TestingT, condition func() bool, waitFor time.Duration, tick time.D
case <-tickC:
tickC = nil
go checkCond()
case v := <-ch:
if v {
case result := <-ch:
switch result {
case conditionExitedUnexpectedly:
// Condition exited via [runtime.Goexit]. This usually means
// that the condition called t.FailNow() on the outer 't'.
return Fail(t, "Condition exited unexpectedly", msgAndArgs...)
case conditionReturnedTrue:
return Fail(t, "Condition satisfied", msgAndArgs...)
case conditionReturnedFalse:
// All good, continue checking.
fallthrough
default:
tickC = ticker.C
}
tickC = ticker.C
}
}
}
Expand Down
Loading
Loading