Skip to content

Commit bdb5ec7

Browse files
author
Axel von Bertoldi
committed
Unit tests for retryCommand
1 parent 00ac872 commit bdb5ec7

1 file changed

Lines changed: 158 additions & 0 deletions

File tree

magefiles/pulp/push_test.go

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@ package pulp
55
import (
66
"fmt"
77
"io"
8+
"regexp"
89
"strings"
910
"testing"
11+
"time"
1012

13+
"github.com/jpillora/backoff"
1114
"github.com/stretchr/testify/require"
1215
)
1316

@@ -193,6 +196,161 @@ Description : GitLab Runner
193196
}
194197
}
195198

199+
func TestRetryCommandRun(t *testing.T) {
200+
tests := map[string]struct {
201+
execBehavior func(attempt int) (bool, string, error) // returns (success, stderr, error )
202+
retryableErrs []*regexp.Regexp
203+
expectedError bool
204+
errorContains string
205+
expectedAttempt int
206+
}{
207+
"successful on first attempt": {
208+
execBehavior: func(attempt int) (bool, string, error) {
209+
return true, "", nil
210+
},
211+
retryableErrs: []*regexp.Regexp{},
212+
expectedError: false,
213+
expectedAttempt: 1,
214+
},
215+
"successful on second attempt with retryable error": {
216+
execBehavior: func(attempt int) (bool, string, error) {
217+
if attempt == 1 {
218+
return false, "Artifact with checksum of 'abc123' already exists.", fmt.Errorf("artifact error")
219+
}
220+
return true, "", nil
221+
},
222+
retryableErrs: []*regexp.Regexp{
223+
regexp.MustCompile(`Artifact with checksum of '.*' already exists\.`),
224+
},
225+
expectedError: false,
226+
expectedAttempt: 2,
227+
},
228+
"successful on third attempt with retryable error": {
229+
execBehavior: func(attempt int) (bool, string, error) {
230+
if attempt <= 2 {
231+
return false, "Artifact with checksum of 'xyz789' already exists.", fmt.Errorf("artifact error")
232+
}
233+
return true, "", nil
234+
},
235+
retryableErrs: []*regexp.Regexp{
236+
regexp.MustCompile(`Artifact with checksum of '.*' already exists\.`),
237+
},
238+
expectedError: false,
239+
expectedAttempt: 3,
240+
},
241+
"fails with non-retryable error on first attempt": {
242+
execBehavior: func(attempt int) (bool, string, error) {
243+
return false, "Permission denied: cannot access repository", fmt.Errorf("permission denied")
244+
},
245+
retryableErrs: []*regexp.Regexp{
246+
regexp.MustCompile(`Artifact with checksum of '.*' already exists\.`),
247+
},
248+
expectedError: true,
249+
errorContains: "Permission denied",
250+
expectedAttempt: 1,
251+
},
252+
"fails after max retries with retryable error": {
253+
execBehavior: func(attempt int) (bool, string, error) {
254+
return false, "Artifact with checksum of 'def456' already exists.", fmt.Errorf("artifact error")
255+
},
256+
retryableErrs: []*regexp.Regexp{
257+
regexp.MustCompile(`Artifact with checksum of '.*' already exists\.`),
258+
},
259+
expectedError: true,
260+
errorContains: "failed after 5 retries",
261+
expectedAttempt: 5,
262+
},
263+
"multiple retryable error patterns": {
264+
execBehavior: func(attempt int) (bool, string, error) {
265+
if attempt == 1 {
266+
return false, "Connection timeout: server not responding", fmt.Errorf("timeout")
267+
}
268+
if attempt == 2 {
269+
return false, "Artifact with checksum of 'ghi012' already exists.", fmt.Errorf("artifact error")
270+
}
271+
return true, "", nil
272+
},
273+
retryableErrs: []*regexp.Regexp{
274+
regexp.MustCompile(`Artifact with checksum of '.*' already exists\.`),
275+
regexp.MustCompile(`Connection timeout:.*`),
276+
},
277+
expectedError: false,
278+
expectedAttempt: 3,
279+
},
280+
"retryable error on last attempt succeeds": {
281+
execBehavior: func(attempt int) (bool, string, error) {
282+
if attempt < 5 {
283+
return false, "Artifact with checksum of 'jkl345' already exists.", fmt.Errorf("artifact error")
284+
}
285+
return true, "", nil
286+
},
287+
retryableErrs: []*regexp.Regexp{
288+
regexp.MustCompile(`Artifact with checksum of '.*' already exists\.`),
289+
},
290+
expectedError: false,
291+
expectedAttempt: 5,
292+
},
293+
"no retryable errors configured": {
294+
execBehavior: func(attempt int) (bool, string, error) {
295+
return false, "Some error message", fmt.Errorf("some error")
296+
},
297+
retryableErrs: []*regexp.Regexp{},
298+
expectedError: true,
299+
errorContains: "Some error message",
300+
expectedAttempt: 1,
301+
},
302+
"empty stderr with error": {
303+
execBehavior: func(attempt int) (bool, string, error) {
304+
return false, "", fmt.Errorf("command failed")
305+
},
306+
retryableErrs: []*regexp.Regexp{
307+
regexp.MustCompile(`Artifact with checksum of '.*' already exists\.`),
308+
},
309+
expectedError: true,
310+
errorContains: "execution of command",
311+
expectedAttempt: 1,
312+
},
313+
}
314+
315+
for tn, tt := range tests {
316+
t.Run(tn, func(t *testing.T) {
317+
attempt := 0
318+
319+
// Create mock exec function that tracks attempts
320+
execMock := func(env map[string]string, out io.Writer, stderr io.Writer, cmd string, args ...string) (bool, error) {
321+
attempt++
322+
success, stderrMsg, err := tt.execBehavior(attempt)
323+
324+
if stderrMsg != "" {
325+
_, _ = io.WriteString(stderr, stderrMsg)
326+
}
327+
328+
return success, err
329+
}
330+
331+
// Create retryCommand with mocked exec
332+
cmd := newRetryCommand("test-cmd", []string{"arg1", "arg2"}, tt.retryableErrs, io.Discard, execMock)
333+
// make it a bit faster
334+
cmd.backoff = backoff.Backoff{Min: 10 * time.Millisecond, Max: 50 * time.Millisecond}
335+
336+
// Run the command
337+
err := cmd.run()
338+
339+
// Verify results
340+
if tt.expectedError {
341+
require.Error(t, err)
342+
if tt.errorContains != "" {
343+
require.Contains(t, err.Error(), tt.errorContains)
344+
}
345+
} else {
346+
require.NoError(t, err)
347+
}
348+
349+
require.Equal(t, tt.expectedAttempt, attempt, "expected %d attempts, got %d", tt.expectedAttempt, attempt)
350+
})
351+
}
352+
}
353+
196354
func TestRpmPusherPush(t *testing.T) {
197355
tests := map[string]struct {
198356
releases []string

0 commit comments

Comments
 (0)