github.com/swisspost/terratest@v0.0.0-20230214120104-7ec6de2e1ae0/modules/retry/retry_test.go (about) 1 package retry 2 3 import ( 4 "fmt" 5 "testing" 6 "time" 7 8 "github.com/stretchr/testify/assert" 9 ) 10 11 func TestDoWithRetry(t *testing.T) { 12 t.Parallel() 13 14 expectedOutput := "expected" 15 expectedError := fmt.Errorf("expected error") 16 17 actionAlwaysReturnsExpected := func() (string, error) { return expectedOutput, nil } 18 actionAlwaysReturnsError := func() (string, error) { return expectedOutput, expectedError } 19 20 createActionThatReturnsExpectedAfterFiveRetries := func() func() (string, error) { 21 count := 0 22 return func() (string, error) { 23 count++ 24 if count > 5 { 25 return expectedOutput, nil 26 } 27 return expectedOutput, expectedError 28 } 29 } 30 31 testCases := []struct { 32 description string 33 maxRetries int 34 expectedError error 35 action func() (string, error) 36 }{ 37 {"Return value on first try", 10, nil, actionAlwaysReturnsExpected}, 38 {"Return error on all retries", 10, MaxRetriesExceeded{Description: "Return error on all retries", MaxRetries: 10}, actionAlwaysReturnsError}, 39 {"Return value after 5 retries", 10, nil, createActionThatReturnsExpectedAfterFiveRetries()}, 40 {"Return value after 5 retries, but only do 4 retries", 4, MaxRetriesExceeded{Description: "Return value after 5 retries, but only do 4 retries", MaxRetries: 4}, createActionThatReturnsExpectedAfterFiveRetries()}, 41 } 42 43 for _, testCase := range testCases { 44 testCase := testCase // capture range variable for each test case 45 46 t.Run(testCase.description, func(t *testing.T) { 47 t.Parallel() 48 49 actualOutput, err := DoWithRetryE(t, testCase.description, testCase.maxRetries, 1*time.Millisecond, testCase.action) 50 assert.Equal(t, expectedOutput, actualOutput) 51 if testCase.expectedError != nil { 52 assert.Equal(t, testCase.expectedError, err) 53 } else { 54 assert.NoError(t, err) 55 assert.Equal(t, expectedOutput, actualOutput) 56 } 57 }) 58 } 59 } 60 61 func TestDoWithTimeout(t *testing.T) { 62 t.Parallel() 63 64 expectedOutput := "expected" 65 expectedError := fmt.Errorf("expected error") 66 67 actionReturnsValueImmediately := func() (string, error) { return expectedOutput, nil } 68 actionReturnsErrorImmediately := func() (string, error) { return "", expectedError } 69 70 createActionThatReturnsValueAfterDelay := func(delay time.Duration) func() (string, error) { 71 return func() (string, error) { 72 time.Sleep(delay) 73 return expectedOutput, nil 74 } 75 } 76 77 createActionThatReturnsErrorAfterDelay := func(delay time.Duration) func() (string, error) { 78 return func() (string, error) { 79 time.Sleep(delay) 80 return "", expectedError 81 } 82 } 83 84 testCases := []struct { 85 description string 86 timeout time.Duration 87 expectedError error 88 action func() (string, error) 89 }{ 90 {"Returns value immediately", 5 * time.Second, nil, actionReturnsValueImmediately}, 91 {"Returns error immediately", 5 * time.Second, expectedError, actionReturnsErrorImmediately}, 92 {"Returns value after 2 seconds", 5 * time.Second, nil, createActionThatReturnsValueAfterDelay(2 * time.Second)}, 93 {"Returns error after 2 seconds", 5 * time.Second, expectedError, createActionThatReturnsErrorAfterDelay(2 * time.Second)}, 94 {"Returns value after timeout exceeded", 5 * time.Second, TimeoutExceeded{Description: "Returns value after timeout exceeded", Timeout: 5 * time.Second}, createActionThatReturnsValueAfterDelay(10 * time.Second)}, 95 {"Returns error after timeout exceeded", 5 * time.Second, TimeoutExceeded{Description: "Returns error after timeout exceeded", Timeout: 5 * time.Second}, createActionThatReturnsErrorAfterDelay(10 * time.Second)}, 96 } 97 98 for _, testCase := range testCases { 99 testCase := testCase // capture range variable for each test case 100 101 t.Run(testCase.description, func(t *testing.T) { 102 t.Parallel() 103 104 actualOutput, err := DoWithTimeoutE(t, testCase.description, testCase.timeout, testCase.action) 105 if testCase.expectedError != nil { 106 assert.Equal(t, testCase.expectedError, err) 107 } else { 108 assert.NoError(t, err) 109 assert.Equal(t, expectedOutput, actualOutput) 110 } 111 }) 112 } 113 } 114 115 func TestDoInBackgroundUntilStopped(t *testing.T) { 116 t.Parallel() 117 118 sleepBetweenRetries := 5 * time.Second 119 counter := 0 120 121 stop := DoInBackgroundUntilStopped(t, t.Name(), sleepBetweenRetries, func() { 122 counter++ 123 }) 124 125 time.Sleep(sleepBetweenRetries * 3) 126 stop.Done() 127 128 assert.Equal(t, 3, counter) 129 130 time.Sleep(sleepBetweenRetries * 3) 131 assert.Equal(t, 3, counter) 132 } 133 134 func TestDoWithRetryableErrors(t *testing.T) { 135 t.Parallel() 136 137 expectedOutput := "this is the expected output" 138 expectedError := fmt.Errorf("expected error") 139 unexpectedError := fmt.Errorf("some other error") 140 141 actionAlwaysReturnsExpected := func() (string, error) { return expectedOutput, nil } 142 actionAlwaysReturnsExpectedError := func() (string, error) { return expectedOutput, expectedError } 143 actionAlwaysReturnsUnexpectedError := func() (string, error) { return expectedOutput, unexpectedError } 144 145 createActionThatReturnsExpectedAfterFiveRetriesOfExpectedErrors := func() func() (string, error) { 146 count := 0 147 return func() (string, error) { 148 count++ 149 if count > 5 { 150 return expectedOutput, nil 151 } 152 return expectedOutput, expectedError 153 } 154 } 155 156 createActionThatReturnsExpectedAfterFiveRetriesOfUnexpectedErrors := func() func() (string, error) { 157 count := 0 158 return func() (string, error) { 159 count++ 160 if count > 5 { 161 return expectedOutput, nil 162 } 163 return expectedOutput, unexpectedError 164 } 165 } 166 167 createActionThatReturnsErrorCounterAfterFiveRetriesOfExpectedErrors := func() func() (string, error) { 168 count := 0 169 return func() (string, error) { 170 count++ 171 if count > 5 { 172 return expectedOutput, ErrorCounter(count) 173 } 174 return expectedOutput, expectedError 175 } 176 } 177 178 matchAllRegexp := ".*" 179 matchExpectedErrorExactRegexp := expectedError.Error() 180 matchExpectedErrorRegexp := "^expected.*$" 181 matchNothingRegexp1 := "this won't match any of our errors" 182 matchNothingRegexp2 := "this also won't match any of our errors" 183 matchStdoutExactlyRegexp := expectedOutput 184 matchStdoutRegexp := "this.*output" 185 186 noRetryableErrors := map[string]string{} 187 retryOnAllErrors := map[string]string{ 188 matchAllRegexp: "match all errors", 189 } 190 retryOnExpectedErrorExactMatch := map[string]string{ 191 matchExpectedErrorExactRegexp: "match expected error exactly", 192 } 193 retryOnExpectedErrorRegexpMatch := map[string]string{ 194 matchExpectedErrorRegexp: "match expected error using a regex", 195 } 196 retryOnExpectedErrorRegexpMatchWithOthers := map[string]string{ 197 matchNothingRegexp1: "unrelated regex that shouldn't match anything", 198 matchExpectedErrorRegexp: "match expected error using a regex", 199 matchNothingRegexp2: "another unrelated regex that shouldn't match anything", 200 } 201 retryOnErrorsThatWontMatch := map[string]string{ 202 matchNothingRegexp1: "unrelated regex that shouldn't match anything", 203 matchNothingRegexp2: "another unrelated regex that shouldn't match anything", 204 } 205 retryOnExpectedStdoutExactMatch := map[string]string{ 206 matchStdoutExactlyRegexp: "match expected stdout exactly", 207 } 208 retryOnExpectedStdoutRegex := map[string]string{ 209 matchStdoutRegexp: "match expected stdout using a regex", 210 } 211 212 testCases := []struct { 213 description string 214 retryableErrors map[string]string 215 maxRetries int 216 expectedError error 217 action func() (string, error) 218 }{ 219 {"Return value on first try", noRetryableErrors, 10, nil, actionAlwaysReturnsExpected}, 220 {"Return expected error, but no retryable errors requested", noRetryableErrors, 10, FatalError{Underlying: expectedError}, actionAlwaysReturnsExpectedError}, 221 {"Return expected error, but retryable errors do not match", retryOnErrorsThatWontMatch, 10, FatalError{Underlying: expectedError}, actionAlwaysReturnsExpectedError}, 222 {"Return expected error on all retries, use match all regex", retryOnAllErrors, 10, MaxRetriesExceeded{Description: "Return expected error on all retries, use match all regex", MaxRetries: 10}, actionAlwaysReturnsExpectedError}, 223 {"Return expected error on all retries, use match exactly regex", retryOnExpectedErrorExactMatch, 3, MaxRetriesExceeded{Description: "Return expected error on all retries, use match exactly regex", MaxRetries: 3}, actionAlwaysReturnsExpectedError}, 224 {"Return expected error on all retries, use regex", retryOnExpectedErrorRegexpMatch, 1, MaxRetriesExceeded{Description: "Return expected error on all retries, use regex", MaxRetries: 1}, actionAlwaysReturnsExpectedError}, 225 {"Return expected error on all retries, use regex amidst others", retryOnExpectedErrorRegexpMatchWithOthers, 1, MaxRetriesExceeded{Description: "Return expected error on all retries, use regex amidst others", MaxRetries: 1}, actionAlwaysReturnsExpectedError}, 226 {"Return unexpected error on all retries, but match stdout exactly", retryOnExpectedStdoutExactMatch, 10, MaxRetriesExceeded{Description: "Return unexpected error on all retries, but match stdout exactly", MaxRetries: 10}, actionAlwaysReturnsUnexpectedError}, 227 {"Return unexpected error on all retries, but match stdout with regex", retryOnExpectedStdoutRegex, 3, MaxRetriesExceeded{Description: "Return unexpected error on all retries, but match stdout with regex", MaxRetries: 3}, actionAlwaysReturnsUnexpectedError}, 228 {"Return value after 5 retries with expected error, match all", retryOnAllErrors, 10, nil, createActionThatReturnsExpectedAfterFiveRetriesOfExpectedErrors()}, 229 {"Return value after 5 retries with expected error, match exactly", retryOnExpectedErrorExactMatch, 10, nil, createActionThatReturnsExpectedAfterFiveRetriesOfExpectedErrors()}, 230 {"Return value after 5 retries with expected error, match regex", retryOnExpectedErrorRegexpMatch, 10, nil, createActionThatReturnsExpectedAfterFiveRetriesOfExpectedErrors()}, 231 {"Return value after 5 retries with expected error, match multiple regex", retryOnExpectedErrorRegexpMatchWithOthers, 10, nil, createActionThatReturnsExpectedAfterFiveRetriesOfExpectedErrors()}, 232 {"Return value after 5 retries with expected error, match stdout exactly", retryOnExpectedStdoutExactMatch, 10, nil, createActionThatReturnsExpectedAfterFiveRetriesOfUnexpectedErrors()}, 233 {"Return value after 5 retries with expected error, match stdout with regex", retryOnExpectedStdoutRegex, 10, nil, createActionThatReturnsExpectedAfterFiveRetriesOfUnexpectedErrors()}, 234 {"Return value after 5 retries with expected error, match exactly, but only do 4 retries", retryOnExpectedErrorExactMatch, 4, MaxRetriesExceeded{Description: "Return value after 5 retries with expected error, match exactly, but only do 4 retries", MaxRetries: 4}, createActionThatReturnsExpectedAfterFiveRetriesOfExpectedErrors()}, 235 {"Return unexpected error after 5 retries with expected error, match exactly", retryOnExpectedErrorExactMatch, 10, FatalError{Underlying: ErrorCounter(6)}, createActionThatReturnsErrorCounterAfterFiveRetriesOfExpectedErrors()}, 236 {"Return unexpected error after 5 retries with expected error, match regex", retryOnExpectedErrorRegexpMatch, 10, FatalError{Underlying: ErrorCounter(6)}, createActionThatReturnsErrorCounterAfterFiveRetriesOfExpectedErrors()}, 237 {"Return unexpected error after 5 retries with expected error, match multiple regex", retryOnExpectedErrorRegexpMatchWithOthers, 10, FatalError{Underlying: ErrorCounter(6)}, createActionThatReturnsErrorCounterAfterFiveRetriesOfExpectedErrors()}, 238 {"Return unexpected error after 5 retries with expected error, match all", retryOnAllErrors, 10, MaxRetriesExceeded{Description: "Return unexpected error after 5 retries with expected error, match all", MaxRetries: 10}, createActionThatReturnsErrorCounterAfterFiveRetriesOfExpectedErrors()}, 239 } 240 241 for _, testCase := range testCases { 242 testCase := testCase // capture range variable for each test case 243 244 t.Run(testCase.description, func(t *testing.T) { 245 t.Parallel() 246 247 actualOutput, err := DoWithRetryableErrorsE(t, testCase.description, testCase.retryableErrors, testCase.maxRetries, 1*time.Millisecond, testCase.action) 248 assert.Equal(t, expectedOutput, actualOutput) 249 if testCase.expectedError != nil { 250 assert.Equal(t, testCase.expectedError, err) 251 } else { 252 assert.NoError(t, err) 253 assert.Equal(t, expectedOutput, actualOutput) 254 } 255 }) 256 } 257 } 258 259 type ErrorCounter int 260 261 func (count ErrorCounter) Error() string { 262 return fmt.Sprintf("%d", int(count)) 263 }