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 }