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 }