github.com/pf-qiu/concourse/v6@v6.7.3-0.20201207032516-1f455d73275f/atc/gc/build_log_collector_test.go (about)

     1  package gc_test
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"time"
     8  
     9  	"github.com/pf-qiu/concourse/v6/atc"
    10  	"github.com/pf-qiu/concourse/v6/atc/db"
    11  	"github.com/pf-qiu/concourse/v6/atc/db/dbfakes"
    12  	. "github.com/pf-qiu/concourse/v6/atc/gc"
    13  
    14  	. "github.com/onsi/ginkgo"
    15  	. "github.com/onsi/gomega"
    16  )
    17  
    18  var _ = Describe("BuildLogCollector", func() {
    19  	var (
    20  		buildLogCollector     GcCollector
    21  		fakePipelineFactory   *dbfakes.FakePipelineFactory
    22  		fakePipelineLifecycle *dbfakes.FakePipelineLifecycle
    23  		batchSize             int
    24  		buildLogRetainCalc    BuildLogRetentionCalculator
    25  	)
    26  
    27  	BeforeEach(func() {
    28  		fakePipelineFactory = new(dbfakes.FakePipelineFactory)
    29  		fakePipelineLifecycle = new(dbfakes.FakePipelineLifecycle)
    30  		batchSize = 5
    31  		buildLogRetainCalc = NewBuildLogRetentionCalculator(0, 0, 0, 0)
    32  	})
    33  
    34  	JustBeforeEach(func() {
    35  		buildLogCollector = NewBuildLogCollector(
    36  			fakePipelineFactory,
    37  			fakePipelineLifecycle,
    38  			batchSize,
    39  			buildLogRetainCalc,
    40  			false,
    41  		)
    42  	})
    43  
    44  	It("removes build events from deleted pipelines", func() {
    45  		err := buildLogCollector.Run(context.TODO())
    46  		Expect(err).ToNot(HaveOccurred())
    47  		Expect(fakePipelineLifecycle.RemoveBuildEventsForDeletedPipelinesCallCount()).To(Equal(1))
    48  	})
    49  
    50  	Context("when removing build events from deleted pipelines fails", func() {
    51  		BeforeEach(func() {
    52  			fakePipelineLifecycle.RemoveBuildEventsForDeletedPipelinesReturns(errors.New("error"))
    53  		})
    54  
    55  		It("errors", func() {
    56  			err := buildLogCollector.Run(context.TODO())
    57  			Expect(err).To(HaveOccurred())
    58  		})
    59  	})
    60  
    61  	Context("when there is a pipeline", func() {
    62  		var fakePipeline *dbfakes.FakePipeline
    63  
    64  		BeforeEach(func() {
    65  			fakePipeline = new(dbfakes.FakePipeline)
    66  			fakePipeline.IDReturns(42)
    67  
    68  			fakePipelineFactory.AllPipelinesReturns([]db.Pipeline{fakePipeline}, nil)
    69  		})
    70  
    71  		Context("when getting the dashboard fails", func() {
    72  			var disaster error
    73  
    74  			BeforeEach(func() {
    75  				disaster = errors.New("sorry pal")
    76  				fakePipeline.JobsReturns(nil, disaster)
    77  			})
    78  
    79  			It("returns the error", func() {
    80  				err := buildLogCollector.Run(context.TODO())
    81  				Expect(err).To(Equal(disaster))
    82  			})
    83  		})
    84  
    85  		Context("when the dashboard has a job", func() {
    86  			var fakeJob *dbfakes.FakeJob
    87  
    88  			BeforeEach(func() {
    89  				fakeJob = new(dbfakes.FakeJob)
    90  				fakeJob.NameReturns("job-1")
    91  				fakeJob.FirstLoggedBuildIDReturns(5)
    92  				fakeJob.ConfigReturns(atc.JobConfig{
    93  					BuildLogsToRetain: 2,
    94  				}, nil)
    95  
    96  				fakePipeline.JobsReturns([]db.Job{fakeJob}, nil)
    97  			})
    98  
    99  			Context("drain handling", func() {
   100  				JustBeforeEach(func() {
   101  					buildLogCollector = NewBuildLogCollector(
   102  						fakePipelineFactory,
   103  						fakePipelineLifecycle,
   104  						batchSize,
   105  						buildLogRetainCalc,
   106  						true,
   107  					)
   108  				})
   109  				BeforeEach(func() {
   110  					fakeJob.BuildsStub = func(page db.Page) ([]db.Build, db.Pagination, error) {
   111  						if *page.From == 5 {
   112  							return []db.Build{sbDrained(9, false), sbDrained(8, false), sbDrained(7, true), sbDrained(6, false), sbDrained(5, true)}, db.Pagination{Newer: &db.Page{From: db.NewIntPtr(10), Limit: 5}}, nil
   113  						} else if *page.From == 10 {
   114  							return []db.Build{sbDrained(11, true), sbDrained(10, true)}, db.Pagination{}, nil
   115  						}
   116  						Fail(fmt.Sprintf("Builds called with unexpected argument: page=%#v", page))
   117  						return []db.Build{}, db.Pagination{}, nil
   118  					}
   119  
   120  					fakePipeline.DeleteBuildEventsByBuildIDsReturns(nil)
   121  					fakeJob.UpdateFirstLoggedBuildIDReturns(nil)
   122  				})
   123  
   124  				JustBeforeEach(func() {
   125  					err := buildLogCollector.Run(context.TODO())
   126  					Expect(err).NotTo(HaveOccurred())
   127  				})
   128  
   129  				It("should not reap builds which have not been drained", func() {
   130  					Expect(fakePipeline.DeleteBuildEventsByBuildIDsCallCount()).To(Equal(1))
   131  
   132  					Expect(fakePipeline.DeleteBuildEventsByBuildIDsArgsForCall(0)).Should(Not(ContainElement(6)))
   133  					Expect(fakePipeline.DeleteBuildEventsByBuildIDsArgsForCall(0)).Should(Not(ContainElement(8)))
   134  					Expect(fakePipeline.DeleteBuildEventsByBuildIDsArgsForCall(0)).Should(Not(ContainElement(9)))
   135  				})
   136  
   137  				It("should reap builds which have been drained", func() {
   138  					Expect(fakePipeline.DeleteBuildEventsByBuildIDsCallCount()).To(Equal(1))
   139  
   140  					Expect(fakePipeline.DeleteBuildEventsByBuildIDsArgsForCall(0)).To(ConsistOf(7, 5))
   141  				})
   142  
   143  				It("should update first logged build id to the earliest non-drained build", func() {
   144  					Expect(fakePipeline.DeleteBuildEventsByBuildIDsCallCount()).To(Equal(1))
   145  
   146  					Expect(fakeJob.UpdateFirstLoggedBuildIDCallCount()).To(Equal(1))
   147  					actualNewFirstLoggedBuildID := fakeJob.UpdateFirstLoggedBuildIDArgsForCall(0)
   148  					Expect(actualNewFirstLoggedBuildID).To(Equal(6))
   149  				})
   150  			})
   151  
   152  			Context("when drain has not been configured", func() {
   153  				BeforeEach(func() {
   154  					buildLogCollector = NewBuildLogCollector(
   155  						fakePipelineFactory,
   156  						fakePipelineLifecycle,
   157  						batchSize,
   158  						buildLogRetainCalc,
   159  						false,
   160  					)
   161  					fakeJob.BuildsStub = func(page db.Page) ([]db.Build, db.Pagination, error) {
   162  						if *page.From == 5 {
   163  							return []db.Build{sbDrained(9, true), sbDrained(8, false), sbDrained(7, false), sbDrained(6, true), sbDrained(5, false)}, db.Pagination{Newer: &db.Page{From: db.NewIntPtr(10), Limit: 5}}, nil
   164  						} else if *page.From == 10 {
   165  							return []db.Build{sbDrained(10, true)}, db.Pagination{}, nil
   166  						}
   167  						Fail(fmt.Sprintf("Builds called with unexpected argument: page=%#v", page))
   168  						return []db.Build{}, db.Pagination{}, nil
   169  					}
   170  
   171  					fakePipeline.DeleteBuildEventsByBuildIDsReturns(nil)
   172  					fakeJob.UpdateFirstLoggedBuildIDReturns(nil)
   173  				})
   174  				It("should reap builds if draining is not configured", func() {
   175  					err := buildLogCollector.Run(context.TODO())
   176  					Expect(err).NotTo(HaveOccurred())
   177  					Expect(fakePipeline.DeleteBuildEventsByBuildIDsCallCount()).To(Equal(1))
   178  
   179  					Expect(fakePipeline.DeleteBuildEventsByBuildIDsArgsForCall(0)).To(ConsistOf(5, 6, 7, 8))
   180  
   181  					Expect(fakeJob.UpdateFirstLoggedBuildIDCallCount()).To(Equal(1))
   182  					actualNewFirstLoggedBuildID := fakeJob.UpdateFirstLoggedBuildIDArgsForCall(0)
   183  					Expect(actualNewFirstLoggedBuildID).To(Equal(9))
   184  				})
   185  			})
   186  
   187  			Context("when deleting build events fails", func() {
   188  				var disaster error
   189  
   190  				BeforeEach(func() {
   191  					fakeJob.BuildsStub = func(page db.Page) ([]db.Build, db.Pagination, error) {
   192  						if *page.From == 5 {
   193  							return []db.Build{sbDrained(8, false), sbDrained(7, true), sbDrained(6, false), sbDrained(5, false)}, db.Pagination{}, nil
   194  						}
   195  						Fail(fmt.Sprintf("Builds called with unexpected argument: page=%#v", page))
   196  						return []db.Build{}, db.Pagination{}, nil
   197  					}
   198  
   199  					disaster = errors.New("major malfunction")
   200  
   201  					fakePipeline.DeleteBuildEventsByBuildIDsReturns(disaster)
   202  				})
   203  
   204  				It("returns the error", func() {
   205  					err := buildLogCollector.Run(context.TODO())
   206  					Expect(err).To(Equal(disaster))
   207  				})
   208  
   209  				It("does not update first logged build id", func() {
   210  					buildLogCollector.Run(context.TODO())
   211  
   212  					Expect(fakeJob.UpdateFirstLoggedBuildIDCallCount()).To(BeZero())
   213  				})
   214  			})
   215  
   216  			Context("when updating first logged build id fails", func() {
   217  				var disaster error
   218  
   219  				BeforeEach(func() {
   220  					fakeJob.BuildsStub = func(page db.Page) ([]db.Build, db.Pagination, error) {
   221  						if *page.From == 5 {
   222  							return []db.Build{sbDrained(8, false), sbDrained(7, true), sbDrained(6, false), sbDrained(5, false)}, db.Pagination{}, nil
   223  						}
   224  						Fail(fmt.Sprintf("Builds called with unexpected argument: page=%#v", page))
   225  						return []db.Build{}, db.Pagination{}, nil
   226  					}
   227  
   228  					disaster = errors.New("major malfunction")
   229  
   230  					fakeJob.UpdateFirstLoggedBuildIDReturns(disaster)
   231  				})
   232  
   233  				It("returns the error", func() {
   234  					err := buildLogCollector.Run(context.TODO())
   235  					Expect(err).To(Equal(disaster))
   236  				})
   237  			})
   238  
   239  			Context("when the builds we want to reap are still running", func() {
   240  				BeforeEach(func() {
   241  					fakeJob.ConfigReturns(atc.JobConfig{
   242  						BuildLogsToRetain: 3,
   243  					}, nil)
   244  					fakeJob.BuildsStub = func(page db.Page) ([]db.Build, db.Pagination, error) {
   245  						if *page.From == 5 {
   246  							return []db.Build{
   247  								runningBuild(9),
   248  								runningBuild(8),
   249  								sb(7),
   250  								sb(6),
   251  								sb(5),
   252  							}, db.Pagination{Newer: &db.Page{From: db.NewIntPtr(10), Limit: 5}}, nil
   253  						} else if *page.From == 10 {
   254  							return []db.Build{sb(10)}, db.Pagination{}, nil
   255  						} else {
   256  							Fail(fmt.Sprintf("Builds called with unexpected argument: page=%#v", page))
   257  						}
   258  						return nil, db.Pagination{}, nil
   259  					}
   260  
   261  					fakePipeline.DeleteBuildEventsByBuildIDsReturns(nil)
   262  
   263  					fakeJob.UpdateFirstLoggedBuildIDReturns(nil)
   264  				})
   265  
   266  				JustBeforeEach(func() {
   267  					err := buildLogCollector.Run(context.TODO())
   268  					Expect(err).NotTo(HaveOccurred())
   269  				})
   270  
   271  				It("reaps only not-running builds", func() {
   272  					Expect(fakePipeline.DeleteBuildEventsByBuildIDsCallCount()).To(Equal(1))
   273  					actualBuildIDs := fakePipeline.DeleteBuildEventsByBuildIDsArgsForCall(0)
   274  					Expect(actualBuildIDs).To(ConsistOf(5))
   275  				})
   276  
   277  				It("updates FirstLoggedBuildID to earliest non-reaped build", func() {
   278  					Expect(fakeJob.UpdateFirstLoggedBuildIDCallCount()).To(Equal(1))
   279  					actualNewFirstLoggedBuildID := fakeJob.UpdateFirstLoggedBuildIDArgsForCall(0)
   280  					Expect(actualNewFirstLoggedBuildID).To(Equal(6))
   281  				})
   282  			})
   283  
   284  			Context("when no builds need to be reaped", func() {
   285  				BeforeEach(func() {
   286  					fakeJob.BuildsStub = func(page db.Page) ([]db.Build, db.Pagination, error) {
   287  						if *page.From == 5 {
   288  							return []db.Build{runningBuild(5)}, db.Pagination{}, nil
   289  						} else {
   290  							Fail(fmt.Sprintf("Builds called with unexpected argument: page=%#v", page))
   291  						}
   292  						return nil, db.Pagination{}, nil
   293  					}
   294  
   295  					fakePipeline.DeleteBuildEventsByBuildIDsReturns(nil)
   296  
   297  					fakeJob.UpdateFirstLoggedBuildIDReturns(nil)
   298  				})
   299  
   300  				JustBeforeEach(func() {
   301  					err := buildLogCollector.Run(context.TODO())
   302  					Expect(err).NotTo(HaveOccurred())
   303  				})
   304  
   305  				It("doesn't reap any builds", func() {
   306  					Expect(fakePipeline.DeleteBuildEventsByBuildIDsCallCount()).To(BeZero())
   307  				})
   308  
   309  				It("doesn't update FirstLoggedBuildID", func() {
   310  					Expect(fakeJob.UpdateFirstLoggedBuildIDCallCount()).To(BeZero())
   311  				})
   312  			})
   313  
   314  			Context("when no builds exist", func() {
   315  				BeforeEach(func() {
   316  					fakeJob.BuildsReturns(nil, db.Pagination{}, nil)
   317  
   318  					fakePipeline.DeleteBuildEventsByBuildIDsReturns(nil)
   319  
   320  					fakeJob.UpdateFirstLoggedBuildIDReturns(nil)
   321  				})
   322  
   323  				It("doesn't reap any builds", func() {
   324  					err := buildLogCollector.Run(context.TODO())
   325  					Expect(err).NotTo(HaveOccurred())
   326  
   327  					Expect(fakePipeline.DeleteBuildEventsByBuildIDsCallCount()).To(BeZero())
   328  					Expect(fakeJob.UpdateFirstLoggedBuildIDCallCount()).To(BeZero())
   329  				})
   330  			})
   331  
   332  			Context("when getting the job builds fails", func() {
   333  				var disaster error
   334  
   335  				BeforeEach(func() {
   336  					disaster = errors.New("major malfunction")
   337  
   338  					fakeJob.BuildsReturns(nil, db.Pagination{}, disaster)
   339  				})
   340  
   341  				It("returns the error", func() {
   342  					err := buildLogCollector.Run(context.TODO())
   343  					Expect(err).To(Equal(disaster))
   344  				})
   345  			})
   346  
   347  			Context("when only count is set", func() {
   348  				BeforeEach(func() {
   349  					fakeJob.BuildsStub = func(page db.Page) ([]db.Build, db.Pagination, error) {
   350  						if *page.From == 5 {
   351  							return []db.Build{sbTime(6, time.Now().Add(-23*time.Hour)), sbTime(5, time.Now().Add(-49*time.Hour))}, db.Pagination{}, nil
   352  						}
   353  						Fail(fmt.Sprintf("Builds called with unexpected argument: page=%#v", page))
   354  						return nil, db.Pagination{}, nil
   355  					}
   356  
   357  					fakeJob.ConfigReturns(atc.JobConfig{
   358  						BuildLogRetention: &atc.BuildLogRetention{
   359  							Builds: 1,
   360  							Days:   0,
   361  						},
   362  					}, nil)
   363  
   364  					fakePipeline.DeleteBuildEventsByBuildIDsReturns(nil)
   365  					fakeJob.UpdateFirstLoggedBuildIDReturns(nil)
   366  				})
   367  
   368  				It("should delete 1 build event", func() {
   369  					err := buildLogCollector.Run(context.TODO())
   370  					Expect(err).NotTo(HaveOccurred())
   371  
   372  					Expect(fakePipeline.DeleteBuildEventsByBuildIDsCallCount()).To(Equal(1))
   373  					actualBuildIDs := fakePipeline.DeleteBuildEventsByBuildIDsArgsForCall(0)
   374  					Expect(actualBuildIDs).To(ConsistOf(5))
   375  				})
   376  			})
   377  
   378  			Context("when only date is set", func() {
   379  				BeforeEach(func() {
   380  					fakeJob.BuildsStub = func(page db.Page) ([]db.Build, db.Pagination, error) {
   381  						if *page.From == 5 {
   382  							return []db.Build{sbTime(6, time.Now().Add(-23*time.Hour)), sbTime(5, time.Now().Add(-49*time.Hour))}, db.Pagination{}, nil
   383  						}
   384  						Fail(fmt.Sprintf("Builds called with unexpected argument: page=%#v", page))
   385  						return nil, db.Pagination{}, nil
   386  					}
   387  
   388  					fakeJob.ConfigReturns(atc.JobConfig{
   389  						BuildLogRetention: &atc.BuildLogRetention{
   390  							Builds: 0,
   391  							Days:   3,
   392  						},
   393  					}, nil)
   394  
   395  					fakePipeline.DeleteBuildEventsByBuildIDsReturns(nil)
   396  					fakeJob.UpdateFirstLoggedBuildIDReturns(nil)
   397  				})
   398  
   399  				It("should delete nothing, because of the date retention", func() {
   400  					err := buildLogCollector.Run(context.TODO())
   401  					Expect(err).NotTo(HaveOccurred())
   402  
   403  					Expect(fakePipeline.DeleteBuildEventsByBuildIDsCallCount()).To(Equal(0))
   404  				})
   405  			})
   406  
   407  			Context("when count and date are set > 0", func() {
   408  				BeforeEach(func() {
   409  					fakeJob.BuildsStub = func(page db.Page) ([]db.Build, db.Pagination, error) {
   410  						if *page.From == 5 {
   411  							return []db.Build{sbTime(6, time.Now().Add(-23*time.Hour)), sbTime(5, time.Now().Add(-49*time.Hour))}, db.Pagination{}, nil
   412  						}
   413  						Fail(fmt.Sprintf("Builds called with unexpected argument: page=%#v", page))
   414  						return nil, db.Pagination{}, nil
   415  					}
   416  
   417  					fakeJob.ConfigReturns(atc.JobConfig{
   418  						BuildLogRetention: &atc.BuildLogRetention{
   419  							Builds: 1,
   420  							Days:   3,
   421  						},
   422  					}, nil)
   423  
   424  					fakePipeline.DeleteBuildEventsByBuildIDsReturns(nil)
   425  					fakeJob.UpdateFirstLoggedBuildIDReturns(nil)
   426  				})
   427  
   428  				It("should delete 1 build, because of the builds retention", func() {
   429  					err := buildLogCollector.Run(context.TODO())
   430  					Expect(err).NotTo(HaveOccurred())
   431  
   432  					Expect(fakePipeline.DeleteBuildEventsByBuildIDsCallCount()).To(Equal(1))
   433  					actualBuildIDs := fakePipeline.DeleteBuildEventsByBuildIDsArgsForCall(0)
   434  					Expect(actualBuildIDs).To(ConsistOf(5))
   435  				})
   436  			})
   437  
   438  			Context("when only date is set", func() {
   439  				BeforeEach(func() {
   440  					fakeJob.BuildsStub = func(page db.Page) ([]db.Build, db.Pagination, error) {
   441  						if *page.From == 5 {
   442  							return []db.Build{sbTime(6, time.Now().Add(-23*time.Hour)), sbTime(5, time.Now().Add(-49*time.Hour))}, db.Pagination{}, nil
   443  						}
   444  						Fail(fmt.Sprintf("Builds called with unexpected argument: page=%#v", page))
   445  						return nil, db.Pagination{}, nil
   446  					}
   447  
   448  					fakeJob.ConfigReturns(atc.JobConfig{
   449  						BuildLogRetention: &atc.BuildLogRetention{
   450  							Builds: 0,
   451  							Days:   1,
   452  						},
   453  					}, nil)
   454  
   455  					fakePipeline.DeleteBuildEventsByBuildIDsReturns(nil)
   456  					fakeJob.UpdateFirstLoggedBuildIDReturns(nil)
   457  				})
   458  
   459  				It("should delete before that", func() {
   460  					err := buildLogCollector.Run(context.TODO())
   461  					Expect(err).NotTo(HaveOccurred())
   462  
   463  					Expect(fakePipeline.DeleteBuildEventsByBuildIDsCallCount()).To(Equal(1))
   464  					actualBuildIDs := fakePipeline.DeleteBuildEventsByBuildIDsArgsForCall(0)
   465  					Expect(actualBuildIDs).To(ConsistOf(5))
   466  				})
   467  			})
   468  
   469  			Context("when min_success_build is set", func() {
   470  				BeforeEach(func() {
   471  					fakeJob.ConfigReturns(atc.JobConfig{
   472  						BuildLogRetention: &atc.BuildLogRetention{
   473  							Builds:                 5,
   474  							Days:                   0,
   475  							MinimumSucceededBuilds: 2,
   476  						},
   477  					}, nil)
   478  
   479  					page1 := db.Page{From: db.NewIntPtr(5), Limit: 5}
   480  					page2 := db.Page{From: db.NewIntPtr(10), Limit: 5}
   481  					page3 := db.Page{From: db.NewIntPtr(15), Limit: 5}
   482  					fakeJob.BuildsStub = func(page db.Page) ([]db.Build, db.Pagination, error) {
   483  						if *page.From == *page1.From {
   484  							return []db.Build{sb(9), successBuild(8), sb(7), reapedBuild(6), reapedBuild(5)}, db.Pagination{Newer: &page2}, nil
   485  						} else if *page.From == *page2.From {
   486  							return []db.Build{sb(14), successBuild(13), sb(12), sb(11), sb(10)}, db.Pagination{Newer: &page3}, nil
   487  						} else if *page.From == *page3.From {
   488  							return []db.Build{sb(18), sb(17), sb(16), sb(15)}, db.Pagination{}, nil
   489  						}
   490  						Fail(fmt.Sprintf("Builds called with unexpected argument: page=%#v", page))
   491  						return nil, db.Pagination{}, nil
   492  					}
   493  				})
   494  
   495  				JustBeforeEach(func() {
   496  					err := buildLogCollector.Run(context.TODO())
   497  					Expect(err).NotTo(HaveOccurred())
   498  				})
   499  
   500  				It("should reap non success builds", func() {
   501  					Expect(fakePipeline.DeleteBuildEventsByBuildIDsCallCount()).To(Equal(1))
   502  					actualBuildIDs := fakePipeline.DeleteBuildEventsByBuildIDsArgsForCall(0)
   503  					Expect(actualBuildIDs).To(ConsistOf(7, 9, 10, 11, 12, 14, 15))
   504  
   505  					Expect(fakePipeline.DeleteBuildEventsByBuildIDsArgsForCall(0)).Should(Not(ContainElement(5)))
   506  					Expect(fakePipeline.DeleteBuildEventsByBuildIDsArgsForCall(0)).Should(Not(ContainElement(6)))
   507  				})
   508  
   509  				It("should keep at least n success builds, n=MinSuccessBuilds, n=2 ", func() {
   510  					Expect(fakePipeline.DeleteBuildEventsByBuildIDsArgsForCall(0)).Should(Not(ContainElement(8)))
   511  					Expect(fakePipeline.DeleteBuildEventsByBuildIDsArgsForCall(0)).Should(Not(ContainElement(13)))
   512  				})
   513  
   514  				It("should update first logged build id to the earliest success build", func() {
   515  					Expect(fakeJob.UpdateFirstLoggedBuildIDCallCount()).To(Equal(1))
   516  					actualNewFirstLoggedBuildID := fakeJob.UpdateFirstLoggedBuildIDArgsForCall(0)
   517  					Expect(actualNewFirstLoggedBuildID).To(Equal(8))
   518  				})
   519  			})
   520  
   521  			Context("when min_success_build equals builds", func() {
   522  				BeforeEach(func() {
   523  					fakeJob.ConfigReturns(atc.JobConfig{
   524  						BuildLogRetention: &atc.BuildLogRetention{
   525  							Builds:                 5,
   526  							Days:                   0,
   527  							MinimumSucceededBuilds: 5,
   528  						},
   529  					}, nil)
   530  
   531  					page1 := db.Page{From: db.NewIntPtr(5), Limit: 5}
   532  					page2 := db.Page{From: db.NewIntPtr(10), Limit: 5}
   533  					page3 := db.Page{From: db.NewIntPtr(15), Limit: 5}
   534  					fakeJob.BuildsStub = func(page db.Page) ([]db.Build, db.Pagination, error) {
   535  						if *page.From == *page1.From {
   536  							return []db.Build{sb(9), successBuild(8), sb(7), reapedBuild(6), reapedBuild(5)}, db.Pagination{Newer: &page2}, nil
   537  						} else if *page.From == *page2.From {
   538  							return []db.Build{sb(14), successBuild(13), successBuild(12), sb(11), successBuild(10)}, db.Pagination{Newer: &page3}, nil
   539  						} else if *page.From == *page3.From {
   540  							return []db.Build{successBuild(18), sb(17), sb(16), successBuild(15)}, db.Pagination{}, nil
   541  						}
   542  						Fail(fmt.Sprintf("Builds called with unexpected argument: page=%#v", page))
   543  						return nil, db.Pagination{}, nil
   544  					}
   545  				})
   546  
   547  				JustBeforeEach(func() {
   548  					err := buildLogCollector.Run(context.TODO())
   549  					Expect(err).NotTo(HaveOccurred())
   550  				})
   551  
   552  				It("should reap non success builds and success builds that exceeds min success build retained number", func() {
   553  					Expect(fakePipeline.DeleteBuildEventsByBuildIDsCallCount()).To(Equal(1))
   554  					actualBuildIDs := fakePipeline.DeleteBuildEventsByBuildIDsArgsForCall(0)
   555  					Expect(actualBuildIDs).To(ConsistOf(7, 8, 9, 11, 14, 16, 17))
   556  
   557  					Expect(fakePipeline.DeleteBuildEventsByBuildIDsArgsForCall(0)).Should(Not(ContainElement(5)))
   558  					Expect(fakePipeline.DeleteBuildEventsByBuildIDsArgsForCall(0)).Should(Not(ContainElement(6)))
   559  				})
   560  
   561  				It("should keep at least n success builds, n=MinSuccessBuilds, n=5", func() {
   562  					Expect(fakePipeline.DeleteBuildEventsByBuildIDsArgsForCall(0)).Should(Not(ContainElement(10)))
   563  					Expect(fakePipeline.DeleteBuildEventsByBuildIDsArgsForCall(0)).Should(Not(ContainElement(12)))
   564  					Expect(fakePipeline.DeleteBuildEventsByBuildIDsArgsForCall(0)).Should(Not(ContainElement(13)))
   565  					Expect(fakePipeline.DeleteBuildEventsByBuildIDsArgsForCall(0)).Should(Not(ContainElement(15)))
   566  					Expect(fakePipeline.DeleteBuildEventsByBuildIDsArgsForCall(0)).Should(Not(ContainElement(18)))
   567  				})
   568  
   569  				It("should update first logged build id to the earliest success build", func() {
   570  					Expect(fakeJob.UpdateFirstLoggedBuildIDCallCount()).To(Equal(1))
   571  					actualNewFirstLoggedBuildID := fakeJob.UpdateFirstLoggedBuildIDArgsForCall(0)
   572  					Expect(actualNewFirstLoggedBuildID).To(Equal(10))
   573  				})
   574  			})
   575  		})
   576  
   577  		Context("when the FirstLoggedBuildID has an value", func() {
   578  			Context("when all the logs get reaped", func() {
   579  				var fakeJob *dbfakes.FakeJob
   580  
   581  				BeforeEach(func() {
   582  					fakeJob = new(dbfakes.FakeJob)
   583  					fakeJob.NameReturns("job-1")
   584  					fakeJob.FirstLoggedBuildIDReturns(5)
   585  					fakeJob.ConfigReturns(atc.JobConfig{
   586  						BuildLogRetention: &atc.BuildLogRetention{
   587  							Days: 1,
   588  						},
   589  					}, nil)
   590  
   591  					fakePipeline.JobsReturns([]db.Job{fakeJob}, nil)
   592  
   593  					yesterday := time.Now().Add(-30 * time.Hour)
   594  
   595  					fakeJob.BuildsReturns([]db.Build{sbTime(9, yesterday), sbTime(8, yesterday), sbTime(7, yesterday), sbTime(6, yesterday), sbTime(5, yesterday)}, db.Pagination{}, nil)
   596  				})
   597  
   598  				It("FirstLoggedBuildID doesn't get reset to 0", func() {
   599  					Expect(buildLogCollector.Run(context.TODO())).NotTo(HaveOccurred())
   600  					Expect(fakePipeline.DeleteBuildEventsByBuildIDsCallCount()).To(Equal(1))
   601  					Expect(fakePipeline.DeleteBuildEventsByBuildIDsArgsForCall(0)).To(ConsistOf(9, 8, 7, 6, 5))
   602  					Expect(fakeJob.UpdateFirstLoggedBuildIDCallCount()).To(Equal(0))
   603  				})
   604  			})
   605  
   606  			Context("when FirstLoggedBuildID == 1", func() {
   607  				var fakeJob *dbfakes.FakeJob
   608  
   609  				BeforeEach(func() {
   610  					fakeJob = new(dbfakes.FakeJob)
   611  					fakeJob.NameReturns("job-1")
   612  					fakeJob.FirstLoggedBuildIDReturns(1)
   613  					fakeJob.ConfigReturns(atc.JobConfig{
   614  						BuildLogsToRetain: 10,
   615  					}, nil)
   616  
   617  					fakePipeline.JobsReturns([]db.Job{fakeJob}, nil)
   618  				})
   619  
   620  				Context("when we install a custom build log retention calculator", func() {
   621  					BeforeEach(func() {
   622  						buildLogRetainCalc = NewBuildLogRetentionCalculator(3, 3, 0, 0)
   623  
   624  						fakeJob.BuildsStub = func(page db.Page) ([]db.Build, db.Pagination, error) {
   625  							if *page.From == 1 {
   626  								return []db.Build{sb(4), sb(3), sb(2), sb(1)}, db.Pagination{}, nil
   627  							}
   628  
   629  							Fail(fmt.Sprintf("Builds called with unexpected argument: page=%#v", page))
   630  							return nil, db.Pagination{}, nil
   631  						}
   632  
   633  						fakePipeline.DeleteBuildEventsByBuildIDsReturns(nil)
   634  						fakeJob.UpdateFirstLoggedBuildIDReturns(nil)
   635  					})
   636  
   637  					It("uses build log calculator", func() {
   638  						Expect(buildLogCollector.Run(context.TODO())).NotTo(HaveOccurred())
   639  						Expect(fakePipeline.DeleteBuildEventsByBuildIDsCallCount()).To(Equal(1))
   640  						Expect(fakePipeline.DeleteBuildEventsByBuildIDsArgsForCall(0)).To(ConsistOf(1))
   641  					})
   642  				})
   643  
   644  				Context("when getting the job builds fails", func() {
   645  					var disaster error
   646  
   647  					BeforeEach(func() {
   648  						disaster = errors.New("major malfunction")
   649  
   650  						fakeJob.BuildsReturns(nil, db.Pagination{}, disaster)
   651  					})
   652  
   653  					It("returns the error", func() {
   654  						err := buildLogCollector.Run(context.TODO())
   655  						Expect(err).To(Equal(disaster))
   656  					})
   657  				})
   658  			})
   659  
   660  		})
   661  
   662  		Context("when the job says retain 0 builds", func() {
   663  			var fakeJob *dbfakes.FakeJob
   664  
   665  			BeforeEach(func() {
   666  				fakeJob = new(dbfakes.FakeJob)
   667  				fakeJob.NameReturns("job-1")
   668  				fakeJob.FirstLoggedBuildIDReturns(6)
   669  				fakeJob.ConfigReturns(atc.JobConfig{
   670  					BuildLogsToRetain: 0,
   671  				}, nil)
   672  				fakeJob.TagsReturns([]string{})
   673  
   674  				fakePipeline.JobsReturns([]db.Job{fakeJob}, nil)
   675  			})
   676  
   677  			It("skips the reaping step for that job", func() {
   678  				err := buildLogCollector.Run(context.TODO())
   679  				Expect(err).NotTo(HaveOccurred())
   680  
   681  				Expect(fakeJob.BuildsCallCount()).To(BeZero())
   682  				Expect(fakePipeline.DeleteBuildEventsByBuildIDsCallCount()).To(BeZero())
   683  				Expect(fakeJob.UpdateFirstLoggedBuildIDCallCount()).To(BeZero())
   684  			})
   685  		})
   686  	})
   687  
   688  	Context("when there is a paused pipeline", func() {
   689  		var fakePipeline *dbfakes.FakePipeline
   690  
   691  		BeforeEach(func() {
   692  			fakePipeline = new(dbfakes.FakePipeline)
   693  			fakePipeline.IDReturns(42)
   694  			fakePipeline.PausedReturns(true)
   695  
   696  			fakePipelineFactory.AllPipelinesReturns([]db.Pipeline{fakePipeline}, nil)
   697  		})
   698  
   699  		It("skips the reaping step for that pipeline", func() {
   700  			err := buildLogCollector.Run(context.TODO())
   701  			Expect(err).NotTo(HaveOccurred())
   702  
   703  			Expect(fakePipeline.DeleteBuildEventsByBuildIDsCallCount()).To(BeZero())
   704  		})
   705  	})
   706  
   707  	Context("when getting the pipelines fails", func() {
   708  		var disaster error
   709  
   710  		BeforeEach(func() {
   711  			disaster = errors.New("major malfunction")
   712  
   713  			fakePipelineFactory.AllPipelinesReturns(nil, disaster)
   714  		})
   715  
   716  		It("returns the error", func() {
   717  			err := buildLogCollector.Run(context.TODO())
   718  			Expect(err).To(Equal(disaster))
   719  		})
   720  	})
   721  
   722  })
   723  
   724  func sb(id int) db.Build {
   725  	build := new(dbfakes.FakeBuild)
   726  	build.IDReturns(id)
   727  	build.IsRunningReturns(false)
   728  	return build
   729  }
   730  
   731  func sbTime(id int, end time.Time) db.Build {
   732  	build := new(dbfakes.FakeBuild)
   733  	build.IDReturns(id)
   734  	build.EndTimeReturns(end)
   735  	build.IsRunningReturns(false)
   736  	return build
   737  }
   738  
   739  func sbDrained(id int, drained bool) db.Build {
   740  	build := new(dbfakes.FakeBuild)
   741  	build.IsDrainedReturns(drained)
   742  	build.IDReturns(id)
   743  	build.IsRunningReturns(false)
   744  	return build
   745  }
   746  
   747  func runningBuild(id int) db.Build {
   748  	build := new(dbfakes.FakeBuild)
   749  	build.IDReturns(id)
   750  	build.IsRunningReturns(true)
   751  	return build
   752  }
   753  
   754  func reapedBuild(id int) db.Build {
   755  	build := new(dbfakes.FakeBuild)
   756  	build.IDReturns(id)
   757  	build.ReapTimeReturns(time.Now())
   758  	return build
   759  }
   760  
   761  func successBuild(id int) db.Build {
   762  	build := new(dbfakes.FakeBuild)
   763  	build.IDReturns(id)
   764  	build.StatusReturns(db.BuildStatusSucceeded)
   765  	return build
   766  }