blob: 29c5b40e187fdbbe5d9f68b9de855a83ccbbdf79 [file] [log] [blame]
/*Package poll provides tools for testing asynchronous code.
package poll // import ""
import (
// TestingT is the subset of testing.T used by WaitOn
type TestingT interface {
Fatalf(format string, args ...interface{})
// LogT is a logging interface that is passed to the WaitOn check function
type LogT interface {
Log(args ...interface{})
Logf(format string, args ...interface{})
type helperT interface {
// Settings are used to configure the behaviour of WaitOn
type Settings struct {
// Timeout is the maximum time to wait for the condition. Defaults to 10s.
Timeout time.Duration
// Delay is the time to sleep between checking the condition. Defaults to
// 100ms.
Delay time.Duration
func defaultConfig() *Settings {
return &Settings{Timeout: 10 * time.Second, Delay: 100 * time.Millisecond}
// SettingOp is a function which accepts and modifies Settings
type SettingOp func(config *Settings)
// WithDelay sets the delay to wait between polls
func WithDelay(delay time.Duration) SettingOp {
return func(config *Settings) {
config.Delay = delay
// WithTimeout sets the timeout
func WithTimeout(timeout time.Duration) SettingOp {
return func(config *Settings) {
config.Timeout = timeout
// Result of a check performed by WaitOn
type Result interface {
// Error indicates that the check failed and polling should stop, and the
// the has failed
Error() error
// Done indicates that polling should stop, and the test should proceed
Done() bool
// Message provides the most recent state when polling has not completed
Message() string
type result struct {
done bool
message string
err error
func (r result) Done() bool {
return r.done
func (r result) Message() string {
return r.message
func (r result) Error() error {
return r.err
// Continue returns a Result that indicates to WaitOn that it should continue
// polling. The message text will be used as the failure message if the timeout
// is reached.
func Continue(message string, args ...interface{}) Result {
return result{message: fmt.Sprintf(message, args...)}
// Success returns a Result where Done() returns true, which indicates to WaitOn
// that it should stop polling and exit without an error.
func Success() Result {
return result{done: true}
// Error returns a Result that indicates to WaitOn that it should fail the test
// and stop polling.
func Error(err error) Result {
return result{err: err}
// WaitOn a condition or until a timeout. Poll by calling check and exit when
// check returns a done Result. To fail a test and exit polling with an error
// return a error result.
func WaitOn(t TestingT, check Check, pollOps ...SettingOp) {
if ht, ok := t.(helperT); ok {
config := defaultConfig()
for _, pollOp := range pollOps {
var lastMessage string
after := time.After(config.Timeout)
chResult := make(chan Result)
for {
go func() {
chResult <- check(t)
select {
case <-after:
if lastMessage == "" {
lastMessage = "first check never completed"
t.Fatalf("timeout hit after %s: %s", config.Timeout, lastMessage)
case result := <-chResult:
switch {
case result.Error() != nil:
t.Fatalf("polling check failed: %s", result.Error())
case result.Done():
lastMessage = result.Message()
// Compare values using the cmp.Comparison. If the comparison fails return a
// result which indicates to WaitOn that it should continue waiting.
// If the comparison is successful then WaitOn stops polling.
func Compare(compare cmp.Comparison) Result {
buf := new(logBuffer)
if assert.RunComparison(buf, assert.ArgsAtZeroIndex, compare) {
return Success()
return Continue(buf.String())
type logBuffer struct {
log [][]interface{}
func (c *logBuffer) Log(args ...interface{}) {
c.log = append(c.log, args)
func (c *logBuffer) String() string {
b := new(strings.Builder)
for _, item := range c.log {
b.WriteString(fmt.Sprint(item...) + " ")
return b.String()