...

Source file src/gotest.tools/v3/poll/poll.go

Documentation: gotest.tools/v3/poll

     1  /*Package poll provides tools for testing asynchronous code.
     2   */
     3  package poll // import "gotest.tools/v3/poll"
     4  
     5  import (
     6  	"fmt"
     7  	"strings"
     8  	"time"
     9  
    10  	"gotest.tools/v3/assert/cmp"
    11  	"gotest.tools/v3/internal/assert"
    12  	"gotest.tools/v3/internal/format"
    13  )
    14  
    15  // TestingT is the subset of [testing.T] used by [WaitOn]
    16  type TestingT interface {
    17  	LogT
    18  	Fatalf(format string, args ...interface{})
    19  }
    20  
    21  // LogT is a logging interface that is passed to the [WaitOn] check function
    22  type LogT interface {
    23  	Log(args ...interface{})
    24  	Logf(format string, args ...interface{})
    25  }
    26  
    27  type helperT interface {
    28  	Helper()
    29  }
    30  
    31  // Settings are used to configure the behaviour of [WaitOn]
    32  type Settings struct {
    33  	// Timeout is the maximum time to wait for the condition. Defaults to 10s.
    34  	Timeout time.Duration
    35  	// Delay is the time to sleep between checking the condition. Defaults to
    36  	// 100ms.
    37  	Delay time.Duration
    38  }
    39  
    40  func defaultConfig() *Settings {
    41  	return &Settings{Timeout: 10 * time.Second, Delay: 100 * time.Millisecond}
    42  }
    43  
    44  // SettingOp is a function which accepts and modifies Settings
    45  type SettingOp func(config *Settings)
    46  
    47  // WithDelay sets the delay to wait between polls
    48  func WithDelay(delay time.Duration) SettingOp {
    49  	return func(config *Settings) {
    50  		config.Delay = delay
    51  	}
    52  }
    53  
    54  // WithTimeout sets the timeout
    55  func WithTimeout(timeout time.Duration) SettingOp {
    56  	return func(config *Settings) {
    57  		config.Timeout = timeout
    58  	}
    59  }
    60  
    61  // Result of a check performed by [WaitOn]
    62  type Result interface {
    63  	// Error indicates that the check failed and polling should stop, and the
    64  	// the has failed
    65  	Error() error
    66  	// Done indicates that polling should stop, and the test should proceed
    67  	Done() bool
    68  	// Message provides the most recent state when polling has not completed
    69  	Message() string
    70  }
    71  
    72  type result struct {
    73  	done    bool
    74  	message string
    75  	err     error
    76  }
    77  
    78  func (r result) Done() bool {
    79  	return r.done
    80  }
    81  
    82  func (r result) Message() string {
    83  	return r.message
    84  }
    85  
    86  func (r result) Error() error {
    87  	return r.err
    88  }
    89  
    90  // Continue returns a [Result] that indicates to [WaitOn] that it should continue
    91  // polling. The message text will be used as the failure message if the timeout
    92  // is reached.
    93  func Continue(message string, args ...interface{}) Result {
    94  	return result{message: format.Message(append([]interface{}{message}, args...)...)}
    95  }
    96  
    97  // Success returns a [Result] where Done() returns true, which indicates to [WaitOn]
    98  // that it should stop polling and exit without an error.
    99  func Success() Result {
   100  	return result{done: true}
   101  }
   102  
   103  // Error returns a [Result] that indicates to [WaitOn] that it should fail the test
   104  // and stop polling.
   105  func Error(err error) Result {
   106  	return result{err: err}
   107  }
   108  
   109  // WaitOn a condition or until a timeout. Poll by calling check and exit when
   110  // check returns a done Result. To fail a test and exit polling with an error
   111  // return a error result.
   112  func WaitOn(t TestingT, check Check, pollOps ...SettingOp) {
   113  	if ht, ok := t.(helperT); ok {
   114  		ht.Helper()
   115  	}
   116  	config := defaultConfig()
   117  	for _, pollOp := range pollOps {
   118  		pollOp(config)
   119  	}
   120  
   121  	var lastMessage string
   122  	after := time.After(config.Timeout)
   123  	chResult := make(chan Result)
   124  	for {
   125  		go func() {
   126  			chResult <- check(t)
   127  		}()
   128  		select {
   129  		case <-after:
   130  			if lastMessage == "" {
   131  				lastMessage = "first check never completed"
   132  			}
   133  			t.Fatalf("timeout hit after %s: %s", config.Timeout, lastMessage)
   134  		case result := <-chResult:
   135  			switch {
   136  			case result.Error() != nil:
   137  				t.Fatalf("polling check failed: %s", result.Error())
   138  			case result.Done():
   139  				return
   140  			}
   141  			time.Sleep(config.Delay)
   142  			lastMessage = result.Message()
   143  		}
   144  	}
   145  }
   146  
   147  // Compare values using the [cmp.Comparison]. If the comparison fails return a
   148  // result which indicates to WaitOn that it should continue waiting.
   149  // If the comparison is successful then [WaitOn] stops polling.
   150  func Compare(compare cmp.Comparison) Result {
   151  	buf := new(logBuffer)
   152  	if assert.RunComparison(buf, assert.ArgsAtZeroIndex, compare) {
   153  		return Success()
   154  	}
   155  	return Continue("%v", buf.String())
   156  }
   157  
   158  type logBuffer struct {
   159  	log [][]interface{}
   160  }
   161  
   162  func (c *logBuffer) Log(args ...interface{}) {
   163  	c.log = append(c.log, args)
   164  }
   165  
   166  func (c *logBuffer) String() string {
   167  	b := new(strings.Builder)
   168  	for _, item := range c.log {
   169  		b.WriteString(fmt.Sprint(item...) + " ")
   170  	}
   171  	return b.String()
   172  }
   173  

View as plain text