1package waiter
2
3import (
4	mathrand "math/rand"
5	"strings"
6	"testing"
7	"time"
8
9	"github.com/aws/smithy-go/rand"
10)
11
12func TestComputeDelay(t *testing.T) {
13	cases := map[string]struct {
14		totalAttempts       int64
15		minDelay            time.Duration
16		maxDelay            time.Duration
17		maxWaitTime         time.Duration
18		expectedMaxDelays   []time.Duration
19		expectedError       string
20		expectedMinAttempts int
21	}{
22		"standard": {
23			totalAttempts:       8,
24			minDelay:            2 * time.Second,
25			maxDelay:            120 * time.Second,
26			maxWaitTime:         300 * time.Second,
27			expectedMaxDelays:   []time.Duration{2, 4, 8, 16, 32, 64, 120, 120},
28			expectedMinAttempts: 8,
29		},
30		"zero minDelay": {
31			totalAttempts: 3,
32			minDelay:      0,
33			maxDelay:      120 * time.Second,
34			maxWaitTime:   300 * time.Second,
35			expectedError: "minDelay must be greater than zero",
36		},
37		"zero maxDelay": {
38			totalAttempts: 3,
39			minDelay:      10 * time.Second,
40			maxDelay:      0,
41			maxWaitTime:   300 * time.Second,
42			expectedError: "maxDelay must be greater than zero",
43		},
44		"zero remaining time": {
45			totalAttempts:       3,
46			minDelay:            10 * time.Second,
47			maxDelay:            20 * time.Second,
48			maxWaitTime:         0,
49			expectedMaxDelays:   []time.Duration{0},
50			expectedMinAttempts: 1,
51		},
52		"max wait time is less than min delay": {
53			totalAttempts:       3,
54			minDelay:            10 * time.Second,
55			maxDelay:            20 * time.Second,
56			maxWaitTime:         5 * time.Second,
57			expectedMaxDelays:   []time.Duration{0},
58			expectedMinAttempts: 1,
59		},
60		"large minDelay": {
61			totalAttempts:       80,
62			minDelay:            150 * time.Minute,
63			maxDelay:            200 * time.Minute,
64			maxWaitTime:         250 * time.Minute,
65			expectedMinAttempts: 1,
66		},
67		"large maxDelay": {
68			totalAttempts:       80,
69			minDelay:            15 * time.Minute,
70			maxDelay:            2000 * time.Minute,
71			maxWaitTime:         250 * time.Minute,
72			expectedMinAttempts: 5,
73		},
74	}
75
76	for name, c := range cases {
77		t.Run(name, func(t *testing.T) {
78			// mock smithy-go rand/#Reader
79			r := rand.Reader
80			defer func() {
81				rand.Reader = r
82			}()
83			rand.Reader = mathrand.New(mathrand.NewSource(1))
84
85			// mock waiter call
86			delays, err := mockwait(c.totalAttempts, c.minDelay, c.maxDelay, c.maxWaitTime)
87
88			if len(c.expectedError) != 0 {
89				if err == nil {
90					t.Fatalf("expected error, got none")
91				}
92				if e, a := c.expectedError, err.Error(); !strings.Contains(a, e) {
93					t.Fatalf("expected error %v, got %v instead", e, a)
94				}
95			} else if err != nil {
96				t.Fatalf("expected no error, got %v", err)
97			}
98
99			if e, a := c.expectedMinAttempts, len(delays); e > a {
100				t.Logf("%v", delays)
101				t.Fatalf("expected minimum attempts to be %v, got %v", e, a)
102			}
103
104			for i, expectedDelay := range c.expectedMaxDelays {
105				if e, a := expectedDelay*time.Second, delays[i]; e < a {
106					t.Fatalf("attempt %d : expected delay to be less than %v, got %v", i+1, e, a)
107				}
108
109				if e, a := c.minDelay, delays[i]; e > a && c.maxWaitTime > c.minDelay {
110					t.Fatalf("attempt %d : expected delay to be more than %v, got %v", i+1, e, a)
111				}
112			}
113			t.Logf("delays : %v", delays)
114		})
115	}
116}
117
118func mockwait(maxAttempts int64, minDelay, maxDelay, maxWaitTime time.Duration) ([]time.Duration, error) {
119	delays := make([]time.Duration, 0)
120	remainingTime := maxWaitTime
121	var attempt int64
122
123	for {
124		attempt++
125
126		if maxAttempts < attempt {
127			break
128		}
129
130		delay, err := ComputeDelay(attempt, minDelay, maxDelay, remainingTime)
131		if err != nil {
132			return delays, err
133		}
134
135		delays = append(delays, delay)
136
137		remainingTime -= delay
138		if remainingTime < minDelay {
139			break
140		}
141	}
142
143	return delays, nil
144}
145