1package exec_test
2
3import (
4	"context"
5	"errors"
6
7	. "github.com/concourse/concourse/atc/exec"
8	"github.com/concourse/concourse/atc/exec/build"
9	"github.com/concourse/concourse/atc/exec/execfakes"
10	. "github.com/onsi/ginkgo"
11	. "github.com/onsi/gomega"
12)
13
14var _ = Describe("Retry Step", func() {
15	var (
16		ctx    context.Context
17		cancel func()
18
19		attempt1 *execfakes.FakeStep
20		attempt2 *execfakes.FakeStep
21		attempt3 *execfakes.FakeStep
22
23		repo  *build.Repository
24		state *execfakes.FakeRunState
25
26		step Step
27	)
28
29	BeforeEach(func() {
30		ctx, cancel = context.WithCancel(context.Background())
31
32		attempt1 = new(execfakes.FakeStep)
33		attempt2 = new(execfakes.FakeStep)
34		attempt3 = new(execfakes.FakeStep)
35
36		repo = build.NewRepository()
37		state = new(execfakes.FakeRunState)
38		state.ArtifactRepositoryReturns(repo)
39
40		step = Retry(attempt1, attempt2, attempt3)
41	})
42
43	Context("when calling succeeded before running", func() {
44		Context("when the RetryStep is given no attempts", func() {
45			BeforeEach(func() {
46				step = Retry()
47			})
48
49			Describe("Succeeded", func() {
50				It("should return false", func() {
51					Expect(step.Succeeded()).To(BeFalse())
52				})
53			})
54		})
55
56		Context("when the RetryStep is given attempts", func() {
57			Describe("Succeeded", func() {
58				It("should return false", func() {
59					Expect(step.Succeeded()).To(BeFalse())
60				})
61			})
62		})
63	})
64	Context("when attempt 1 succeeds", func() {
65		BeforeEach(func() {
66			attempt1.SucceededReturns(true)
67		})
68
69		Describe("Run", func() {
70			var stepErr error
71
72			JustBeforeEach(func() {
73				stepErr = step.Run(ctx, state)
74			})
75
76			It("returns nil having only run the first attempt", func() {
77				Expect(stepErr).ToNot(HaveOccurred())
78
79				Expect(attempt1.RunCallCount()).To(Equal(1))
80				Expect(attempt2.RunCallCount()).To(Equal(0))
81				Expect(attempt3.RunCallCount()).To(Equal(0))
82			})
83
84			Describe("Succeeded", func() {
85				It("delegates to attempt 1", func() {
86					// internal check for success within retry loop
87					Expect(attempt1.SucceededCallCount()).To(Equal(1))
88
89					attempt1.SucceededReturns(true)
90
91					Expect(step.Succeeded()).To(BeTrue())
92
93					Expect(attempt1.SucceededCallCount()).To(Equal(2))
94				})
95			})
96		})
97	})
98
99	Context("when attempt 1 fails, and attempt 2 succeeds", func() {
100		BeforeEach(func() {
101			attempt1.SucceededReturns(false)
102			attempt2.SucceededReturns(true)
103		})
104
105		Describe("Run", func() {
106			var stepErr error
107
108			JustBeforeEach(func() {
109				stepErr = step.Run(ctx, state)
110			})
111
112			It("returns nil having only run the first and second attempts", func() {
113				Expect(stepErr).ToNot(HaveOccurred())
114
115				Expect(attempt1.RunCallCount()).To(Equal(1))
116				Expect(attempt2.RunCallCount()).To(Equal(1))
117				Expect(attempt3.RunCallCount()).To(Equal(0))
118			})
119
120			Describe("Succeeded", func() {
121				It("delegates to attempt 2", func() {
122					// internal check for success within retry loop
123					Expect(attempt2.SucceededCallCount()).To(Equal(1))
124
125					attempt2.SucceededReturns(true)
126
127					Expect(step.Succeeded()).To(BeTrue())
128
129					Expect(attempt2.SucceededCallCount()).To(Equal(2))
130				})
131			})
132		})
133	})
134
135	Context("when attempt 1 errors, and attempt 2 succeeds", func() {
136		BeforeEach(func() {
137			attempt1.RunReturns(errors.New("nope"))
138			attempt2.SucceededReturns(true)
139		})
140
141		Describe("Run", func() {
142			var stepErr error
143
144			JustBeforeEach(func() {
145				stepErr = step.Run(ctx, state)
146			})
147
148			It("returns nil having only run the first and second attempts", func() {
149				Expect(stepErr).ToNot(HaveOccurred())
150
151				Expect(attempt1.RunCallCount()).To(Equal(1))
152				Expect(attempt2.RunCallCount()).To(Equal(1))
153				Expect(attempt3.RunCallCount()).To(Equal(0))
154			})
155
156			Describe("Succeeded", func() {
157				It("delegates to attempt 2", func() {
158					// internal check for success within retry loop
159					Expect(attempt2.SucceededCallCount()).To(Equal(1))
160
161					attempt2.SucceededReturns(true)
162
163					Expect(step.Succeeded()).To(BeTrue())
164
165					Expect(attempt2.SucceededCallCount()).To(Equal(2))
166				})
167			})
168		})
169	})
170
171	Context("when attempt 1 errors, and attempt 2 is interrupted", func() {
172		BeforeEach(func() {
173			attempt1.RunReturns(errors.New("nope"))
174			attempt2.RunStub = func(c context.Context, r RunState) error {
175				cancel()
176				return c.Err()
177			}
178		})
179
180		Describe("Run", func() {
181			var stepErr error
182
183			JustBeforeEach(func() {
184				stepErr = step.Run(ctx, state)
185			})
186
187			It("returns the context error having only run the first and second attempts", func() {
188				Expect(stepErr).To(Equal(context.Canceled))
189
190				Expect(attempt1.RunCallCount()).To(Equal(1))
191				Expect(attempt2.RunCallCount()).To(Equal(1))
192				Expect(attempt3.RunCallCount()).To(Equal(0))
193			})
194
195			Describe("Succeeded", func() {
196				It("delegates to attempt 2", func() {
197					// internal check for success within retry loop
198					Expect(attempt2.SucceededCallCount()).To(Equal(0))
199
200					attempt2.SucceededReturns(true)
201
202					Expect(step.Succeeded()).To(BeTrue())
203
204					Expect(attempt2.SucceededCallCount()).To(Equal(1))
205				})
206			})
207		})
208	})
209
210	Context("when attempt 1 errors, attempt 2 times out, and attempt 3 succeeds", func() {
211		BeforeEach(func() {
212			attempt1.RunReturns(errors.New("nope"))
213			attempt2.RunStub = func(c context.Context, r RunState) error {
214				timeout, subCancel := context.WithTimeout(c, 0)
215				defer subCancel()
216				<-timeout.Done()
217				return timeout.Err()
218			}
219		})
220
221		Describe("Run", func() {
222			var stepErr error
223
224			JustBeforeEach(func() {
225				stepErr = step.Run(ctx, state)
226			})
227
228			It("returns nil after running all 3 steps", func() {
229				Expect(stepErr).ToNot(HaveOccurred())
230
231				Expect(attempt1.RunCallCount()).To(Equal(1))
232				Expect(attempt2.RunCallCount()).To(Equal(1))
233				Expect(attempt3.RunCallCount()).To(Equal(1))
234			})
235
236			Describe("Succeeded", func() {
237				It("delegates to attempt 3", func() {
238					// internal check for success within retry loop
239					Expect(attempt3.SucceededCallCount()).To(Equal(1))
240
241					attempt3.SucceededReturns(true)
242
243					Expect(step.Succeeded()).To(BeTrue())
244
245					Expect(attempt3.SucceededCallCount()).To(Equal(2))
246				})
247			})
248		})
249	})
250
251	Context("when attempt 1 fails, attempt 2 fails, and attempt 3 succeeds", func() {
252		BeforeEach(func() {
253			attempt1.SucceededReturns(false)
254			attempt2.SucceededReturns(false)
255			attempt3.SucceededReturns(true)
256		})
257
258		Describe("Run", func() {
259			var stepErr error
260
261			JustBeforeEach(func() {
262				stepErr = step.Run(ctx, state)
263			})
264
265			It("returns nil after running all 3 steps", func() {
266				Expect(stepErr).ToNot(HaveOccurred())
267
268				Expect(attempt1.RunCallCount()).To(Equal(1))
269				Expect(attempt2.RunCallCount()).To(Equal(1))
270				Expect(attempt3.RunCallCount()).To(Equal(1))
271			})
272
273			Describe("Succeeded", func() {
274				It("delegates to attempt 3", func() {
275					// internal check for success within retry loop
276					Expect(attempt3.SucceededCallCount()).To(Equal(1))
277
278					attempt3.SucceededReturns(true)
279
280					Expect(step.Succeeded()).To(BeTrue())
281
282					Expect(attempt3.SucceededCallCount()).To(Equal(2))
283				})
284			})
285		})
286	})
287
288	Context("when attempt 1 fails, attempt 2 fails, and attempt 3 errors", func() {
289		disaster := errors.New("nope")
290
291		BeforeEach(func() {
292			attempt1.SucceededReturns(false)
293			attempt2.SucceededReturns(false)
294			attempt3.RunReturns(disaster)
295		})
296
297		Describe("Run", func() {
298			var stepErr error
299
300			JustBeforeEach(func() {
301				stepErr = step.Run(ctx, state)
302			})
303
304			It("returns the error", func() {
305				Expect(stepErr).To(Equal(disaster))
306
307				Expect(attempt1.RunCallCount()).To(Equal(1))
308				Expect(attempt2.RunCallCount()).To(Equal(1))
309				Expect(attempt3.RunCallCount()).To(Equal(1))
310			})
311
312			Describe("Succeeded", func() {
313				It("delegates to attempt 3", func() {
314					// no internal check for success within retry loop, since it errored
315					Expect(attempt3.SucceededCallCount()).To(Equal(0))
316
317					attempt3.SucceededReturns(true)
318
319					Expect(step.Succeeded()).To(BeTrue())
320
321					Expect(attempt3.SucceededCallCount()).To(Equal(1))
322				})
323			})
324		})
325	})
326
327	Context("when attempt 1 fails, attempt 2 fails, and attempt 3 fails", func() {
328		BeforeEach(func() {
329			attempt1.SucceededReturns(false)
330			attempt2.SucceededReturns(false)
331			attempt3.SucceededReturns(true)
332		})
333
334		Describe("Run", func() {
335			var stepErr error
336
337			JustBeforeEach(func() {
338				stepErr = step.Run(ctx, state)
339			})
340
341			It("returns nil having only run the first and second attempts", func() {
342				Expect(stepErr).ToNot(HaveOccurred())
343
344				Expect(attempt1.RunCallCount()).To(Equal(1))
345				Expect(attempt2.RunCallCount()).To(Equal(1))
346				Expect(attempt3.RunCallCount()).To(Equal(1))
347			})
348
349			Describe("Succeeded", func() {
350				It("delegates to attempt 3", func() {
351					// internal check for success within retry loop
352					Expect(attempt3.SucceededCallCount()).To(Equal(1))
353
354					attempt3.SucceededReturns(true)
355
356					Expect(step.Succeeded()).To(BeTrue())
357
358					Expect(attempt3.SucceededCallCount()).To(Equal(2))
359				})
360			})
361		})
362	})
363})
364