github.com/pf-qiu/concourse/v6@v6.7.3-0.20201207032516-1f455d73275f/atc/exec/in_parallel_test.go (about) 1 package exec_test 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "sync" 8 "time" 9 10 "github.com/pf-qiu/concourse/v6/atc/exec" 11 . "github.com/pf-qiu/concourse/v6/atc/exec" 12 "github.com/pf-qiu/concourse/v6/atc/exec/build" 13 "github.com/pf-qiu/concourse/v6/atc/exec/execfakes" 14 . "github.com/onsi/ginkgo" 15 . "github.com/onsi/gomega" 16 ) 17 18 var _ = Describe("Parallel", func() { 19 var ( 20 ctx context.Context 21 cancel func() 22 23 fakeStepA *execfakes.FakeStep 24 fakeStepB *execfakes.FakeStep 25 fakeSteps []Step 26 27 repo *build.Repository 28 state *execfakes.FakeRunState 29 30 step Step 31 stepOk bool 32 stepErr error 33 ) 34 35 BeforeEach(func() { 36 ctx, cancel = context.WithCancel(context.Background()) 37 38 fakeStepA = new(execfakes.FakeStep) 39 fakeStepB = new(execfakes.FakeStep) 40 fakeSteps = []Step{fakeStepA, fakeStepB} 41 42 step = InParallel(fakeSteps, len(fakeSteps), false) 43 44 repo = build.NewRepository() 45 state = new(execfakes.FakeRunState) 46 state.ArtifactRepositoryReturns(repo) 47 }) 48 49 AfterEach(func() { 50 cancel() 51 }) 52 53 JustBeforeEach(func() { 54 stepOk, stepErr = step.Run(ctx, state) 55 }) 56 57 It("succeeds", func() { 58 Expect(stepErr).ToNot(HaveOccurred()) 59 }) 60 61 It("passes the artifact repo to all steps", func() { 62 Expect(fakeStepA.RunCallCount()).To(Equal(1)) 63 _, repo := fakeStepA.RunArgsForCall(0) 64 Expect(repo).To(Equal(repo)) 65 66 Expect(fakeStepB.RunCallCount()).To(Equal(1)) 67 _, repo = fakeStepB.RunArgsForCall(0) 68 Expect(repo).To(Equal(repo)) 69 }) 70 71 Describe("executing each step", func() { 72 Context("when not constrained by parallel limit", func() { 73 BeforeEach(func() { 74 wg := new(sync.WaitGroup) 75 wg.Add(2) 76 77 fakeStepA.RunStub = func(context.Context, RunState) (bool, error) { 78 wg.Done() 79 wg.Wait() 80 return true, nil 81 } 82 83 fakeStepB.RunStub = func(context.Context, RunState) (bool, error) { 84 wg.Done() 85 wg.Wait() 86 return true, nil 87 } 88 }) 89 90 It("happens concurrently", func() { 91 Expect(fakeStepA.RunCallCount()).To(Equal(1)) 92 Expect(fakeStepB.RunCallCount()).To(Equal(1)) 93 }) 94 }) 95 96 Context("when parallel limit is 1", func() { 97 BeforeEach(func() { 98 step = InParallel(fakeSteps, 1, false) 99 ch := make(chan struct{}, 1) 100 101 fakeStepA.RunStub = func(context.Context, RunState) (bool, error) { 102 time.Sleep(10 * time.Millisecond) 103 ch <- struct{}{} 104 return true, nil 105 } 106 107 fakeStepB.RunStub = func(context.Context, RunState) (bool, error) { 108 defer GinkgoRecover() 109 110 select { 111 case <-ch: 112 default: 113 Fail("step B started before step A could complete") 114 } 115 return true, nil 116 } 117 }) 118 119 It("happens sequentially", func() { 120 Expect(fakeStepA.RunCallCount()).To(Equal(1)) 121 Expect(fakeStepB.RunCallCount()).To(Equal(1)) 122 }) 123 }) 124 }) 125 126 Describe("canceling", func() { 127 BeforeEach(func() { 128 wg := new(sync.WaitGroup) 129 wg.Add(2) 130 131 fakeStepA.RunStub = func(context.Context, RunState) (bool, error) { 132 wg.Done() 133 return true, nil 134 } 135 136 fakeStepB.RunStub = func(context.Context, RunState) (bool, error) { 137 wg.Done() 138 wg.Wait() 139 cancel() 140 return true, nil 141 } 142 }) 143 144 It("cancels each substep", func() { 145 ctx, _ := fakeStepA.RunArgsForCall(0) 146 Expect(ctx.Err()).To(Equal(context.Canceled)) 147 ctx, _ = fakeStepB.RunArgsForCall(0) 148 Expect(ctx.Err()).To(Equal(context.Canceled)) 149 }) 150 151 It("returns ctx.Err()", func() { 152 Expect(stepErr).To(Equal(context.Canceled)) 153 }) 154 155 Context("when there are steps pending execution", func() { 156 BeforeEach(func() { 157 step = InParallel(fakeSteps, 1, false) 158 159 fakeStepA.RunStub = func(context.Context, RunState) (bool, error) { 160 cancel() 161 return true, nil 162 } 163 164 fakeStepB.RunStub = func(context.Context, RunState) (bool, error) { 165 return true, nil 166 } 167 }) 168 169 It("returns ctx.Err()", func() { 170 Expect(stepErr).To(Equal(context.Canceled)) 171 }) 172 173 It("does not execute the remaining steps", func() { 174 ctx, _ := fakeStepA.RunArgsForCall(0) 175 Expect(ctx.Err()).To(Equal(context.Canceled)) 176 Expect(fakeStepB.RunCallCount()).To(Equal(0)) 177 }) 178 179 }) 180 }) 181 182 Context("when steps fail", func() { 183 Context("with normal error", func() { 184 disasterA := errors.New("nope A") 185 disasterB := errors.New("nope B") 186 187 BeforeEach(func() { 188 fakeStepA.RunReturns(false, disasterA) 189 fakeStepB.RunReturns(false, disasterB) 190 }) 191 192 Context("and fail fast is false", func() { 193 BeforeEach(func() { 194 step = InParallel(fakeSteps, 1, false) 195 }) 196 It("lets all steps finish before exiting", func() { 197 Expect(fakeStepA.RunCallCount()).To(Equal(1)) 198 Expect(fakeStepB.RunCallCount()).To(Equal(1)) 199 }) 200 It("exits with an error including the original message", func() { 201 Expect(stepErr.Error()).To(ContainSubstring("nope A")) 202 Expect(stepErr.Error()).To(ContainSubstring("nope B")) 203 }) 204 }) 205 206 Context("and fail fast is true", func() { 207 BeforeEach(func() { 208 step = InParallel(fakeSteps, 1, true) 209 }) 210 It("it cancels remaining steps", func() { 211 Expect(fakeStepA.RunCallCount()).To(Equal(1)) 212 Expect(fakeStepB.RunCallCount()).To(Equal(0)) 213 }) 214 It("exits with an error including the message from the failed steps", func() { 215 Expect(stepErr.Error()).To(ContainSubstring("nope A")) 216 Expect(stepErr.Error()).NotTo(ContainSubstring("nope B")) 217 }) 218 }) 219 }) 220 221 Context("with context canceled error", func() { 222 // error might be wrapped. For example we pass context from in_parallel step 223 // -> task step -> ... -> baggageclaim StreamOut() -> http request. When context 224 // got canceled in in_parallel step, the http client sending the request will 225 // wrap the context.Canceled error into Url.Error 226 disasterB := fmt.Errorf("some thing failed by %w", context.Canceled) 227 228 BeforeEach(func() { 229 fakeStepB.RunReturns(false, disasterB) 230 }) 231 232 It("exits with no error", func() { 233 Expect(stepErr).ToNot(HaveOccurred()) 234 }) 235 }) 236 }) 237 238 Context("when all steps are successful", func() { 239 BeforeEach(func() { 240 fakeStepA.RunReturns(true, nil) 241 fakeStepB.RunReturns(true, nil) 242 }) 243 244 It("succeeds", func() { 245 Expect(stepOk).To(BeTrue()) 246 }) 247 }) 248 249 Context("and some steps are not successful", func() { 250 BeforeEach(func() { 251 fakeStepA.RunReturns(true, nil) 252 fakeStepB.RunReturns(false, nil) 253 }) 254 255 It("fails", func() { 256 Expect(stepOk).To(BeFalse()) 257 }) 258 }) 259 260 Context("when no steps indicate success", func() { 261 BeforeEach(func() { 262 fakeStepA.RunReturns(false, nil) 263 fakeStepB.RunReturns(false, nil) 264 }) 265 266 It("fails", func() { 267 Expect(stepOk).To(BeFalse()) 268 }) 269 }) 270 271 Context("when there are no steps", func() { 272 BeforeEach(func() { 273 step = InParallelStep{} 274 }) 275 276 It("succeeds", func() { 277 Expect(stepOk).To(BeTrue()) 278 }) 279 }) 280 281 Describe("Panic", func() { 282 Context("when one step panics", func() { 283 BeforeEach(func() { 284 fakeStepA.RunReturns(false, nil) 285 fakeStepB.RunStub = func(context.Context, exec.RunState) (bool, error) { 286 panic("something went wrong") 287 } 288 }) 289 290 It("returns an error", func() { 291 Expect(stepErr.Error()).To(ContainSubstring("something went wrong")) 292 }) 293 }) 294 }) 295 })