github.com/pf-qiu/concourse/v6@v6.7.3-0.20201207032516-1f455d73275f/atc/scheduler/algorithm/firstoccurrence_test.go (about)

     1  package algorithm_test
     2  
     3  import (
     4  	"context"
     5  	"crypto/md5"
     6  	"database/sql"
     7  	"encoding/hex"
     8  	"encoding/json"
     9  	"strconv"
    10  	"time"
    11  
    12  	sq "github.com/Masterminds/squirrel"
    13  	"github.com/pf-qiu/concourse/v6/atc"
    14  	"github.com/pf-qiu/concourse/v6/atc/db"
    15  	"github.com/pf-qiu/concourse/v6/atc/scheduler/algorithm"
    16  	. "github.com/onsi/ginkgo"
    17  	. "github.com/onsi/gomega"
    18  	gocache "github.com/patrickmn/go-cache"
    19  )
    20  
    21  var _ = Describe("Resolve", func() {
    22  	type buildInput struct {
    23  		BuildID      int
    24  		JobName      string
    25  		InputName    string
    26  		Version      string
    27  		ResourceName string
    28  		CheckOrder   int
    29  		RerunBuildID int
    30  	}
    31  
    32  	type buildOutput struct {
    33  		BuildID      int
    34  		JobName      string
    35  		Version      string
    36  		ResourceName string
    37  		CheckOrder   int
    38  	}
    39  
    40  	var (
    41  		inputMapping db.InputMapping
    42  		buildInputs  []buildInput
    43  		buildOutputs []buildOutput
    44  	)
    45  
    46  	BeforeEach(func() {
    47  		buildInputs = []buildInput{}
    48  		buildOutputs = []buildOutput{}
    49  	})
    50  
    51  	JustBeforeEach(func() {
    52  		setup := setupDB{
    53  			teamID:      1,
    54  			pipelineID:  1,
    55  			psql:        sq.StatementBuilder.PlaceholderFormat(sq.Dollar).RunWith(dbConn),
    56  			jobIDs:      StringMapping{},
    57  			resourceIDs: StringMapping{},
    58  			versionIDs:  StringMapping{},
    59  		}
    60  
    61  		// setup team 1 and pipeline 1
    62  		team, err := teamFactory.CreateTeam(atc.Team{Name: "algorithm"})
    63  		Expect(err).NotTo(HaveOccurred())
    64  
    65  		pipeline, _, err := team.SavePipeline(atc.PipelineRef{Name: "algorithm"}, atc.Config{
    66  			Resources: atc.ResourceConfigs{
    67  				{
    68  					Name: "r1",
    69  					Type: "r1-type",
    70  				},
    71  			},
    72  			Jobs: atc.JobConfigs{
    73  				{
    74  					Name: "j1",
    75  					PlanSequence: []atc.Step{
    76  						{
    77  							Config: &atc.GetStep{
    78  								Name:     "some-input",
    79  								Resource: "r1",
    80  							},
    81  						},
    82  					},
    83  				},
    84  			},
    85  		}, db.ConfigVersion(0), false)
    86  		Expect(err).NotTo(HaveOccurred())
    87  
    88  		setupTx, err := dbConn.Begin()
    89  		Expect(err).ToNot(HaveOccurred())
    90  
    91  		brt := db.BaseResourceType{
    92  			Name: "some-base-type",
    93  		}
    94  
    95  		_, err = brt.FindOrCreate(setupTx, false)
    96  		Expect(err).NotTo(HaveOccurred())
    97  		Expect(setupTx.Commit()).To(Succeed())
    98  
    99  		resources := map[string]atc.ResourceConfig{}
   100  
   101  		// insert two jobs
   102  		setup.insertJob("j1")
   103  		setup.insertJob("j2")
   104  
   105  		// insert resource and two resource versions
   106  		setup.insertRowVersion(resources, DBRow{
   107  			Resource:   "r1",
   108  			Version:    "v1",
   109  			CheckOrder: 1,
   110  			Disabled:   false,
   111  		})
   112  		setup.insertRowVersion(resources, DBRow{
   113  			Resource:   "r1",
   114  			Version:    "v2",
   115  			CheckOrder: 2,
   116  			Disabled:   false,
   117  		})
   118  
   119  		succeessfulBuildOutputs := map[int]map[string][]string{}
   120  		buildToJobID := map[int]int{}
   121  		buildToRerunOf := map[int]int{}
   122  		// Set up build outputs
   123  		for _, buildOutput := range buildOutputs {
   124  			setup.insertRowBuild(DBRow{
   125  				Job:     buildOutput.JobName,
   126  				BuildID: buildOutput.BuildID,
   127  			}, false)
   128  
   129  			setup.insertRowVersion(resources, DBRow{
   130  				Resource:   buildOutput.ResourceName,
   131  				Version:    buildOutput.Version,
   132  				CheckOrder: buildOutput.CheckOrder,
   133  				Disabled:   false,
   134  			})
   135  
   136  			versionJSON, err := json.Marshal(atc.Version{"ver": buildOutput.Version})
   137  			Expect(err).ToNot(HaveOccurred())
   138  
   139  			resourceID := setup.resourceIDs.ID(buildOutput.ResourceName)
   140  			_, err = setup.psql.Insert("build_resource_config_version_outputs").
   141  				Columns("build_id", "resource_id", "version_md5", "name").
   142  				Values(buildOutput.BuildID, resourceID, sq.Expr("md5(?)", versionJSON), buildOutput.ResourceName).
   143  				Exec()
   144  			Expect(err).ToNot(HaveOccurred())
   145  
   146  			outputs, ok := succeessfulBuildOutputs[buildOutput.BuildID]
   147  			if !ok {
   148  				outputs = map[string][]string{}
   149  				succeessfulBuildOutputs[buildOutput.BuildID] = outputs
   150  			}
   151  
   152  			key := strconv.Itoa(resourceID)
   153  
   154  			outputs[key] = append(outputs[key], convertToMD5(buildOutput.Version))
   155  			buildToJobID[buildOutput.BuildID] = setup.jobIDs.ID(buildOutput.JobName)
   156  		}
   157  
   158  		// Set up build inputs
   159  		for _, buildInput := range buildInputs {
   160  			setup.insertRowBuild(DBRow{
   161  				Job:            buildInput.JobName,
   162  				BuildID:        buildInput.BuildID,
   163  				RerunOfBuildID: buildInput.RerunBuildID,
   164  			}, false)
   165  
   166  			setup.insertRowVersion(resources, DBRow{
   167  				Resource:   buildInput.ResourceName,
   168  				Version:    buildInput.Version,
   169  				CheckOrder: buildInput.CheckOrder,
   170  				Disabled:   false,
   171  			})
   172  
   173  			versionJSON, err := json.Marshal(atc.Version{"ver": buildInput.Version})
   174  			Expect(err).ToNot(HaveOccurred())
   175  
   176  			resourceID := setup.resourceIDs.ID(buildInput.ResourceName)
   177  			_, err = setup.psql.Insert("build_resource_config_version_inputs").
   178  				Columns("build_id", "resource_id", "version_md5", "name", "first_occurrence").
   179  				Values(buildInput.BuildID, resourceID, sq.Expr("md5(?)", versionJSON), buildInput.InputName, false).
   180  				Exec()
   181  			Expect(err).ToNot(HaveOccurred())
   182  
   183  			outputs, ok := succeessfulBuildOutputs[buildInput.BuildID]
   184  			if !ok {
   185  				outputs = map[string][]string{}
   186  				succeessfulBuildOutputs[buildInput.BuildID] = outputs
   187  			}
   188  
   189  			key := strconv.Itoa(resourceID)
   190  
   191  			outputs[key] = append(outputs[key], convertToMD5(buildInput.Version))
   192  			buildToJobID[buildInput.BuildID] = setup.jobIDs.ID(buildInput.JobName)
   193  
   194  			if buildInput.RerunBuildID != 0 {
   195  				buildToRerunOf[buildInput.BuildID] = buildInput.RerunBuildID
   196  			}
   197  		}
   198  
   199  		for buildID, outputs := range succeessfulBuildOutputs {
   200  			outputsJSON, err := json.Marshal(outputs)
   201  			Expect(err).ToNot(HaveOccurred())
   202  
   203  			var rerunOf sql.NullInt64
   204  			if buildToRerunOf[buildID] != 0 {
   205  				rerunOf.Int64 = int64(buildToRerunOf[buildID])
   206  			}
   207  
   208  			_, err = setup.psql.Insert("successful_build_outputs").
   209  				Columns("build_id", "job_id", "rerun_of", "outputs").
   210  				Values(buildID, buildToJobID[buildID], rerunOf, outputsJSON).
   211  				Suffix("ON CONFLICT DO NOTHING").
   212  				Exec()
   213  			Expect(err).ToNot(HaveOccurred())
   214  		}
   215  
   216  		versionsDB := db.NewVersionsDB(dbConn, 2, gocache.New(10*time.Second, 10*time.Second))
   217  
   218  		job, found, err := pipeline.Job("j1")
   219  		Expect(err).ToNot(HaveOccurred())
   220  		Expect(found).To(BeTrue())
   221  
   222  		r1, found, err := pipeline.Resource("r1")
   223  		Expect(err).ToNot(HaveOccurred())
   224  		Expect(found).To(BeTrue())
   225  
   226  		jobInputs := db.InputConfigs{
   227  			{
   228  				Name:       "some-input",
   229  				ResourceID: r1.ID(),
   230  				JobID:      job.ID(),
   231  			},
   232  		}
   233  
   234  		algorithm := algorithm.New(versionsDB)
   235  
   236  		var ok bool
   237  		inputMapping, ok, _, err = algorithm.Compute(context.Background(), job, jobInputs)
   238  		Expect(err).ToNot(HaveOccurred())
   239  		Expect(ok).To(BeTrue())
   240  	})
   241  
   242  	// All these contexts are under the assumption that the algorithm is being
   243  	// run for job "j1" that has a resource called "r1". In the database, there
   244  	// are two jobs ("j1" and j2"), one resource ("r1") and two versions ("v1",
   245  	// "v2") for that resource.
   246  
   247  	// The build inputs that is being set in the contexts are the build inputs
   248  	// that the algorithm will see and use to determine whether the computed set
   249  	// of inputs for this job ("r1" with version "v2" because v2 is the latest
   250  	// version) will be first occurrence.
   251  
   252  	Context("when the version computed from the algorithm was the same version and resource as an existing input with the same job and the same name", func() {
   253  		BeforeEach(func() {
   254  			buildInputs = []buildInput{
   255  				{
   256  					Version:      "v2",
   257  					ResourceName: "r1",
   258  					CheckOrder:   2,
   259  					BuildID:      31,
   260  					JobName:      "j1",
   261  					InputName:    "some-input",
   262  				},
   263  				{
   264  					Version:      "v2",
   265  					ResourceName: "r1",
   266  					CheckOrder:   2,
   267  					BuildID:      31,
   268  					JobName:      "j1",
   269  					InputName:    "some-other-input",
   270  				},
   271  				{
   272  					Version:      "v2",
   273  					ResourceName: "r1",
   274  					CheckOrder:   2,
   275  					BuildID:      32,
   276  					JobName:      "j2",
   277  					InputName:    "some-input",
   278  				},
   279  			}
   280  		})
   281  
   282  		It("sets FirstOccurrence to false", func() {
   283  			Expect(inputMapping).To(Equal(db.InputMapping{
   284  				"some-input": db.InputResult{
   285  					Input: &db.AlgorithmInput{
   286  						AlgorithmVersion: db.AlgorithmVersion{
   287  							Version:    db.ResourceVersion(convertToMD5("v2")),
   288  							ResourceID: 1,
   289  						},
   290  						FirstOccurrence: false,
   291  					},
   292  					PassedBuildIDs: []int{},
   293  				},
   294  			}))
   295  		})
   296  	})
   297  
   298  	Context("when the version computed from the algorithm was the same version and resource as an existing input with the same job but a different name", func() {
   299  		BeforeEach(func() {
   300  			buildInputs = []buildInput{
   301  				{
   302  					Version:      "v2",
   303  					ResourceName: "r1",
   304  					CheckOrder:   2,
   305  					BuildID:      31,
   306  					JobName:      "j1",
   307  					InputName:    "some-other-input",
   308  				},
   309  			}
   310  		})
   311  
   312  		It("sets FirstOccurrence to true", func() {
   313  			Expect(inputMapping).To(Equal(db.InputMapping{
   314  				"some-input": db.InputResult{
   315  					Input: &db.AlgorithmInput{
   316  						AlgorithmVersion: db.AlgorithmVersion{
   317  							Version:    db.ResourceVersion(convertToMD5("v2")),
   318  							ResourceID: 1},
   319  						FirstOccurrence: true,
   320  					},
   321  					PassedBuildIDs: []int{},
   322  				},
   323  			}))
   324  		})
   325  	})
   326  
   327  	Context("when the version computed from the algorithm was the same version and resource as existing input but with a different job with the same name", func() {
   328  		BeforeEach(func() {
   329  			buildInputs = []buildInput{
   330  				{
   331  					Version:      "v2",
   332  					ResourceName: "r1",
   333  					CheckOrder:   2,
   334  					BuildID:      32,
   335  					JobName:      "j2",
   336  					InputName:    "some-input",
   337  				},
   338  			}
   339  		})
   340  
   341  		It("sets FirstOccurrence to true", func() {
   342  			Expect(inputMapping).To(Equal(db.InputMapping{
   343  				"some-input": db.InputResult{
   344  					Input: &db.AlgorithmInput{
   345  						AlgorithmVersion: db.AlgorithmVersion{
   346  							Version:    db.ResourceVersion(convertToMD5("v2")),
   347  							ResourceID: 1},
   348  						FirstOccurrence: true,
   349  					},
   350  					PassedBuildIDs: []int{},
   351  				},
   352  			}))
   353  		})
   354  	})
   355  
   356  	Context("when the version computed from the algorithm was a different version was an existing input of the same job with the same name", func() {
   357  		BeforeEach(func() {
   358  			buildInputs = []buildInput{
   359  				{
   360  					Version:      "v1",
   361  					ResourceName: "r1",
   362  					CheckOrder:   1,
   363  					BuildID:      31,
   364  					JobName:      "j1",
   365  					InputName:    "some-input",
   366  				},
   367  			}
   368  		})
   369  
   370  		It("sets FirstOccurrence to true", func() {
   371  			Expect(inputMapping).To(Equal(db.InputMapping{
   372  				"some-input": db.InputResult{
   373  					Input: &db.AlgorithmInput{
   374  						AlgorithmVersion: db.AlgorithmVersion{
   375  							Version:    db.ResourceVersion(convertToMD5("v2")),
   376  							ResourceID: 1},
   377  						FirstOccurrence: true,
   378  					},
   379  					PassedBuildIDs: []int{},
   380  				},
   381  			}))
   382  		})
   383  	})
   384  
   385  	Context("when the version computed from the algorithm was not the same version as an existing output of the same job", func() {
   386  		BeforeEach(func() {
   387  			buildOutputs = []buildOutput{
   388  				{
   389  					Version:      "v1",
   390  					ResourceName: "r1",
   391  					CheckOrder:   1,
   392  					BuildID:      31,
   393  					JobName:      "j1",
   394  				},
   395  			}
   396  		})
   397  
   398  		It("sets FirstOccurrence to false", func() {
   399  			Expect(inputMapping).To(Equal(db.InputMapping{
   400  				"some-input": db.InputResult{
   401  					Input: &db.AlgorithmInput{
   402  						AlgorithmVersion: db.AlgorithmVersion{
   403  							Version:    db.ResourceVersion(convertToMD5("v2")),
   404  							ResourceID: 1},
   405  						FirstOccurrence: true,
   406  					},
   407  					PassedBuildIDs: []int{},
   408  				},
   409  			}))
   410  		})
   411  	})
   412  
   413  	Context("when a version computed is equal to an existing input from a rerun build", func() {
   414  		BeforeEach(func() {
   415  			buildInputs = []buildInput{
   416  				{
   417  					Version:      "v1",
   418  					ResourceName: "r1",
   419  					CheckOrder:   1,
   420  					BuildID:      30,
   421  					JobName:      "j1",
   422  					InputName:    "some-input",
   423  				},
   424  				{
   425  					Version:      "v2",
   426  					ResourceName: "r1",
   427  					CheckOrder:   1,
   428  					BuildID:      31,
   429  					JobName:      "j1",
   430  					InputName:    "some-input",
   431  				},
   432  				{
   433  					Version:      "v1",
   434  					ResourceName: "r1",
   435  					CheckOrder:   1,
   436  					BuildID:      32,
   437  					JobName:      "j1",
   438  					InputName:    "some-input",
   439  					RerunBuildID: 30,
   440  				},
   441  			}
   442  		})
   443  
   444  		It("sets FirstOccurrence to false", func() {
   445  			Expect(inputMapping).To(Equal(db.InputMapping{
   446  				"some-input": db.InputResult{
   447  					Input: &db.AlgorithmInput{
   448  						AlgorithmVersion: db.AlgorithmVersion{
   449  							Version:    db.ResourceVersion(convertToMD5("v2")),
   450  							ResourceID: 1},
   451  						FirstOccurrence: false,
   452  					},
   453  					PassedBuildIDs: []int{},
   454  				},
   455  			}))
   456  		})
   457  	})
   458  
   459  	Context("when the version computed from the algorithm was the same version and resource of an old build", func() {
   460  		BeforeEach(func() {
   461  			buildInputs = []buildInput{
   462  				{
   463  					Version:      "v2",
   464  					ResourceName: "r1",
   465  					CheckOrder:   2,
   466  					BuildID:      30,
   467  					JobName:      "j1",
   468  					InputName:    "some-input",
   469  				},
   470  				{
   471  					Version:      "v3",
   472  					ResourceName: "r1",
   473  					CheckOrder:   3,
   474  					BuildID:      31,
   475  					JobName:      "j1",
   476  					InputName:    "some-input",
   477  				},
   478  				{
   479  					Version:      "v2",
   480  					ResourceName: "r1",
   481  					CheckOrder:   2,
   482  					BuildID:      32,
   483  					JobName:      "j1",
   484  					InputName:    "some-input",
   485  				},
   486  			}
   487  		})
   488  
   489  		It("sets FirstOccurrence to false", func() {
   490  			Expect(inputMapping).To(Equal(db.InputMapping{
   491  				"some-input": db.InputResult{
   492  					Input: &db.AlgorithmInput{
   493  						AlgorithmVersion: db.AlgorithmVersion{
   494  							Version:    db.ResourceVersion(convertToMD5("v3")),
   495  							ResourceID: 1,
   496  						},
   497  						FirstOccurrence: false,
   498  					},
   499  					PassedBuildIDs: []int{},
   500  				},
   501  			}))
   502  		})
   503  	})
   504  })
   505  
   506  func convertToMD5(version string) string {
   507  	versionJSON, err := json.Marshal(atc.Version{"ver": version})
   508  	Expect(err).ToNot(HaveOccurred())
   509  
   510  	hasher := md5.New()
   511  	hasher.Write([]byte(versionJSON))
   512  	return hex.EncodeToString(hasher.Sum(nil))
   513  }