github.com/outbrain/consul@v1.4.5/testutil/retry/retry.go (about)

     1  // Package retry provides support for repeating operations in tests.
     2  //
     3  // A sample retry operation looks like this:
     4  //
     5  //   func TestX(t *testing.T) {
     6  //       retry.Run(t, func(r *retry.R) {
     7  //           if err := foo(); err != nil {
     8  //               r.Fatal("f: ", err)
     9  //           }
    10  //       })
    11  //   }
    12  //
    13  package retry
    14  
    15  import (
    16  	"bytes"
    17  	"fmt"
    18  	"runtime"
    19  	"strings"
    20  	"sync"
    21  	"time"
    22  )
    23  
    24  // Failer is an interface compatible with testing.T.
    25  type Failer interface {
    26  	// Log is called for the final test output
    27  	Log(args ...interface{})
    28  
    29  	// FailNow is called when the retrying is abandoned.
    30  	FailNow()
    31  }
    32  
    33  // R provides context for the retryer.
    34  type R struct {
    35  	fail   bool
    36  	output []string
    37  }
    38  
    39  func (r *R) FailNow() {
    40  	r.fail = true
    41  	runtime.Goexit()
    42  }
    43  
    44  func (r *R) Fatal(args ...interface{}) {
    45  	r.log(fmt.Sprint(args...))
    46  	r.FailNow()
    47  }
    48  
    49  func (r *R) Fatalf(format string, args ...interface{}) {
    50  	r.log(fmt.Sprintf(format, args...))
    51  	r.FailNow()
    52  }
    53  
    54  func (r *R) Error(args ...interface{}) {
    55  	r.log(fmt.Sprint(args...))
    56  	r.fail = true
    57  }
    58  
    59  func (r *R) Errorf(format string, args ...interface{}) {
    60  	r.log(fmt.Sprintf(format, args...))
    61  	r.fail = true
    62  }
    63  
    64  func (r *R) Check(err error) {
    65  	if err != nil {
    66  		r.log(err.Error())
    67  		r.FailNow()
    68  	}
    69  }
    70  
    71  func (r *R) log(s string) {
    72  	r.output = append(r.output, decorate(s))
    73  }
    74  
    75  func decorate(s string) string {
    76  	_, file, line, ok := runtime.Caller(3)
    77  	if ok {
    78  		n := strings.LastIndex(file, "/")
    79  		if n >= 0 {
    80  			file = file[n+1:]
    81  		}
    82  	} else {
    83  		file = "???"
    84  		line = 1
    85  	}
    86  	return fmt.Sprintf("%s:%d: %s", file, line, s)
    87  }
    88  
    89  func Run(t Failer, f func(r *R)) {
    90  	run(DefaultFailer(), t, f)
    91  }
    92  
    93  func RunWith(r Retryer, t Failer, f func(r *R)) {
    94  	run(r, t, f)
    95  }
    96  
    97  func dedup(a []string) string {
    98  	if len(a) == 0 {
    99  		return ""
   100  	}
   101  	m := map[string]int{}
   102  	for _, s := range a {
   103  		m[s] = m[s] + 1
   104  	}
   105  	var b bytes.Buffer
   106  	for _, s := range a {
   107  		if _, ok := m[s]; ok {
   108  			b.WriteString(s)
   109  			b.WriteRune('\n')
   110  			delete(m, s)
   111  		}
   112  	}
   113  	return string(b.Bytes())
   114  }
   115  
   116  func run(r Retryer, t Failer, f func(r *R)) {
   117  	rr := &R{}
   118  	fail := func() {
   119  		out := dedup(rr.output)
   120  		if out != "" {
   121  			t.Log(out)
   122  		}
   123  		t.FailNow()
   124  	}
   125  	for r.NextOr(fail) {
   126  		var wg sync.WaitGroup
   127  		wg.Add(1)
   128  		go func() {
   129  			defer wg.Done()
   130  			f(rr)
   131  		}()
   132  		wg.Wait()
   133  		if rr.fail {
   134  			rr.fail = false
   135  			continue
   136  		}
   137  		break
   138  	}
   139  }
   140  
   141  // DefaultFailer provides default retry.Run() behavior for unit tests.
   142  func DefaultFailer() *Timer {
   143  	return &Timer{Timeout: 7 * time.Second, Wait: 25 * time.Millisecond}
   144  }
   145  
   146  // TwoSeconds repeats an operation for two seconds and waits 25ms in between.
   147  func TwoSeconds() *Timer {
   148  	return &Timer{Timeout: 2 * time.Second, Wait: 25 * time.Millisecond}
   149  }
   150  
   151  // ThreeTimes repeats an operation three times and waits 25ms in between.
   152  func ThreeTimes() *Counter {
   153  	return &Counter{Count: 3, Wait: 25 * time.Millisecond}
   154  }
   155  
   156  // Retryer provides an interface for repeating operations
   157  // until they succeed or an exit condition is met.
   158  type Retryer interface {
   159  	// NextOr returns true if the operation should be repeated.
   160  	// Otherwise, it calls fail and returns false.
   161  	NextOr(fail func()) bool
   162  }
   163  
   164  // Counter repeats an operation a given number of
   165  // times and waits between subsequent operations.
   166  type Counter struct {
   167  	Count int
   168  	Wait  time.Duration
   169  
   170  	count int
   171  }
   172  
   173  func (r *Counter) NextOr(fail func()) bool {
   174  	if r.count == r.Count {
   175  		fail()
   176  		return false
   177  	}
   178  	if r.count > 0 {
   179  		time.Sleep(r.Wait)
   180  	}
   181  	r.count++
   182  	return true
   183  }
   184  
   185  // Timer repeats an operation for a given amount
   186  // of time and waits between subsequent operations.
   187  type Timer struct {
   188  	Timeout time.Duration
   189  	Wait    time.Duration
   190  
   191  	// stop is the timeout deadline.
   192  	// Set on the first invocation of Next().
   193  	stop time.Time
   194  }
   195  
   196  func (r *Timer) NextOr(fail func()) bool {
   197  	if r.stop.IsZero() {
   198  		r.stop = time.Now().Add(r.Timeout)
   199  		return true
   200  	}
   201  	if time.Now().After(r.stop) {
   202  		fail()
   203  		return false
   204  	}
   205  	time.Sleep(r.Wait)
   206  	return true
   207  }