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  })