go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/prjmanager/triager/cls_test.go (about) 1 // Copyright 2021 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package triager 16 17 import ( 18 "fmt" 19 "strings" 20 "testing" 21 "time" 22 23 "google.golang.org/protobuf/proto" 24 "google.golang.org/protobuf/types/known/timestamppb" 25 26 "go.chromium.org/luci/common/clock/testclock" 27 28 cfgpb "go.chromium.org/luci/cv/api/config/v2" 29 "go.chromium.org/luci/cv/internal/changelist" 30 "go.chromium.org/luci/cv/internal/common" 31 "go.chromium.org/luci/cv/internal/configs/prjcfg" 32 "go.chromium.org/luci/cv/internal/cvtesting" 33 "go.chromium.org/luci/cv/internal/prjmanager/prjpb" 34 "go.chromium.org/luci/cv/internal/run" 35 36 . "github.com/smartystreets/goconvey/convey" 37 . "go.chromium.org/luci/common/testing/assertions" 38 ) 39 40 func shouldResembleTriagedCL(actual any, expected ...any) string { 41 if len(expected) != 1 { 42 return fmt.Sprintf("expected 1 value, got %d", len(expected)) 43 } 44 exp := expected[0] // this may be nil 45 a, ok := actual.(*clInfo) 46 if !ok { 47 return fmt.Sprintf("Wrong actual type %T, must be %T", actual, a) 48 } 49 if err := ShouldHaveSameTypeAs(actual, exp); err != "" { 50 return err 51 } 52 b := exp.(*clInfo) 53 switch { 54 case a == b: 55 return "" 56 case a == nil: 57 return "actual is nil, but non-nil was expected" 58 case b == nil: 59 return "actual is not-nil, but nil was expected" 60 } 61 62 buf := strings.Builder{} 63 for _, err := range []string{ 64 ShouldResemble(a.cqReady, b.cqReady), 65 ShouldResemble(a.nprReady, b.nprReady), 66 ShouldResemble(a.runIndexes, b.runIndexes), 67 cvtesting.SafeShouldResemble(a.deps, b.deps), 68 ShouldResembleProto(a.pcl, b.pcl), 69 ShouldResembleProto(a.purgingCL, b.purgingCL), 70 ShouldResembleProto(a.purgeReasons, b.purgeReasons), 71 ShouldResembleProto(a.triggeringCLDeps, b.triggeringCLDeps), 72 } { 73 if err != "" { 74 buf.WriteRune(' ') 75 buf.WriteString(err) 76 } 77 } 78 return strings.TrimSpace(buf.String()) 79 } 80 81 func TestCLsTriage(t *testing.T) { 82 t.Parallel() 83 84 Convey("Component's PCL deps triage", t, func() { 85 ct := cvtesting.Test{} 86 ctx, cancel := ct.SetUp(t) 87 defer cancel() 88 89 // Truncate start time point s.t. easy to see diff in test failures. 90 epoch := testclock.TestRecentTimeUTC.Truncate(10000 * time.Second) 91 dryRun := func(t time.Time) *run.Trigger { 92 return &run.Trigger{Mode: string(run.DryRun), Time: timestamppb.New(t)} 93 } 94 fullRun := func(t time.Time) *run.Trigger { 95 return &run.Trigger{Mode: string(run.FullRun), Time: timestamppb.New(t)} 96 } 97 newPatchsetTrigger := func(t time.Time) *run.Trigger { 98 return &run.Trigger{Mode: string(run.NewPatchsetRun), Time: timestamppb.New(t)} 99 } 100 101 sup := &simplePMState{ 102 pb: &prjpb.PState{}, 103 cgs: []*prjcfg.ConfigGroup{ 104 {ID: "hash/singular", Content: &cfgpb.ConfigGroup{}}, 105 {ID: "hash/combinable", Content: &cfgpb.ConfigGroup{CombineCls: &cfgpb.CombineCLs{}}}, 106 {ID: "hash/another", Content: &cfgpb.ConfigGroup{}}, 107 {ID: "hash/npr", Content: &cfgpb.ConfigGroup{ 108 Verifiers: &cfgpb.Verifiers{ 109 Tryjob: &cfgpb.Verifiers_Tryjob{ 110 Builders: []*cfgpb.Verifiers_Tryjob_Builder{ 111 {Name: "nprBuilder", ModeAllowlist: []string{string(run.NewPatchsetRun)}}, 112 }, 113 }, 114 }, 115 }}, 116 }, 117 } 118 pm := pmState{sup} 119 const singIdx, combIdx, anotherIdx, nprIdx = 0, 1, 2, 3 120 121 do := func(c *prjpb.Component) map[int64]*clInfo { 122 sup.pb.Components = []*prjpb.Component{c} // include it in backup 123 backup := prjpb.PState{} 124 proto.Merge(&backup, sup.pb) 125 126 cls := triageCLs(ctx, c, pm) 127 So(sup.pb, ShouldResembleProto, &backup) // must not be modified 128 return cls 129 } 130 131 Convey("Typical 1 CL component without deps", func() { 132 sup.pb.Pcls = []*prjpb.PCL{{ 133 Clid: 1, 134 ConfigGroupIndexes: []int32{singIdx}, 135 Status: prjpb.PCL_OK, 136 Triggers: &run.Triggers{CqVoteTrigger: dryRun(epoch)}, 137 Submitted: false, 138 Deps: nil, 139 }} 140 141 Convey("Ready without runs", func() { 142 cls := do(&prjpb.Component{Clids: []int64{1}}) 143 So(cls, ShouldHaveLength, 1) 144 expected := &clInfo{ 145 pcl: pm.MustPCL(1), 146 runIndexes: nil, 147 purgingCL: nil, 148 149 triagedCL: triagedCL{ 150 purgeReasons: nil, 151 cqReady: true, 152 deps: &triagedDeps{}, 153 }, 154 } 155 So(cls[1], shouldResembleTriagedCL, expected) 156 157 Convey("ready may also be in 1+ Runs", func() { 158 cls := do(&prjpb.Component{ 159 Clids: []int64{1}, 160 Pruns: []*prjpb.PRun{{Id: "r1", Clids: []int64{1}, Mode: string(run.DryRun)}}, 161 }) 162 So(cls, ShouldHaveLength, 1) 163 expected.runIndexes = []int32{0} 164 So(cls[1], shouldResembleTriagedCL, expected) 165 }) 166 }) 167 168 Convey("CL already with Errors is not ready", func() { 169 sup.pb.Pcls[0].PurgeReasons = []*prjpb.PurgeReason{ 170 { 171 ClError: &changelist.CLError{ 172 Kind: &changelist.CLError_OwnerLacksEmail{OwnerLacksEmail: true}, 173 }, 174 ApplyTo: &prjpb.PurgeReason_AllActiveTriggers{AllActiveTriggers: true}, 175 }, 176 { 177 ClError: &changelist.CLError{ 178 Kind: &changelist.CLError_UnsupportedMode{UnsupportedMode: "CUSTOM_RUN"}, 179 }, 180 ApplyTo: &prjpb.PurgeReason_AllActiveTriggers{AllActiveTriggers: true}, 181 }, 182 } 183 cls := do(&prjpb.Component{Clids: []int64{1}}) 184 So(cls, ShouldHaveLength, 1) 185 expected := &clInfo{ 186 pcl: pm.MustPCL(1), 187 runIndexes: nil, 188 purgingCL: nil, 189 190 triagedCL: triagedCL{ 191 purgeReasons: sup.pb.Pcls[0].GetPurgeReasons(), 192 }, 193 } 194 So(cls[1], shouldResembleTriagedCL, expected) 195 }) 196 197 Convey("Already purged is never ready", func() { 198 sup.pb.PurgingCls = []*prjpb.PurgingCL{{ 199 Clid: 1, 200 ApplyTo: &prjpb.PurgingCL_AllActiveTriggers{AllActiveTriggers: true}, 201 }} 202 cls := do(&prjpb.Component{Clids: []int64{1}}) 203 So(cls, ShouldHaveLength, 1) 204 expected := &clInfo{ 205 pcl: pm.MustPCL(1), 206 runIndexes: nil, 207 purgingCL: pm.PurgingCL(1), 208 209 triagedCL: triagedCL{ 210 purgeReasons: nil, 211 cqReady: false, 212 deps: &triagedDeps{}, 213 }, 214 } 215 So(cls[1], shouldResembleTriagedCL, expected) 216 217 Convey("not even if inside 1+ Runs", func() { 218 cls := do(&prjpb.Component{ 219 Clids: []int64{1}, 220 Pruns: []*prjpb.PRun{{Id: "r1", Clids: []int64{1}, Mode: string(run.DryRun)}}, 221 }) 222 So(cls, ShouldHaveLength, 1) 223 expected.runIndexes = []int32{0} 224 So(cls[1], shouldResembleTriagedCL, expected) 225 }) 226 }) 227 228 Convey("CL matching several config groups is never ready", func() { 229 sup.PCL(1).ConfigGroupIndexes = []int32{singIdx, anotherIdx} 230 cls := do(&prjpb.Component{Clids: []int64{1}}) 231 So(cls, ShouldHaveLength, 1) 232 expected := &clInfo{ 233 pcl: pm.MustPCL(1), 234 runIndexes: nil, 235 purgingCL: nil, 236 237 triagedCL: triagedCL{ 238 purgeReasons: []*prjpb.PurgeReason{{ 239 ClError: &changelist.CLError{ 240 Kind: &changelist.CLError_WatchedByManyConfigGroups_{ 241 WatchedByManyConfigGroups: &changelist.CLError_WatchedByManyConfigGroups{ 242 ConfigGroups: []string{"singular", "another"}, 243 }, 244 }, 245 }, 246 ApplyTo: &prjpb.PurgeReason_Triggers{Triggers: &run.Triggers{CqVoteTrigger: dryRun(epoch)}}, 247 }}, 248 cqReady: false, 249 deps: nil, // not checked. 250 }, 251 } 252 So(cls[1], shouldResembleTriagedCL, expected) 253 254 Convey("not even if inside 1+ Runs, but Run protects from purging", func() { 255 cls := do(&prjpb.Component{ 256 Clids: []int64{1}, 257 Pruns: []*prjpb.PRun{{Id: "r1", Clids: []int64{1}, Mode: string(run.DryRun)}}, 258 }) 259 So(cls, ShouldHaveLength, 1) 260 expected.runIndexes = []int32{0} 261 expected.purgeReasons = nil 262 So(cls[1], shouldResembleTriagedCL, expected) 263 }) 264 }) 265 }) 266 267 Convey("Typical 1 CL component with new patchset run enabled", func() { 268 sup.pb.Pcls = []*prjpb.PCL{{ 269 Clid: 1, 270 ConfigGroupIndexes: []int32{nprIdx}, 271 Status: prjpb.PCL_OK, 272 Triggers: &run.Triggers{ 273 CqVoteTrigger: dryRun(epoch), 274 NewPatchsetRunTrigger: newPatchsetTrigger(epoch), 275 }, 276 Submitted: false, 277 Deps: nil, 278 }} 279 Convey("new patchset upload on CL with CQ vote run being purged", func() { 280 sup.pb.PurgingCls = append(sup.pb.PurgingCls, &prjpb.PurgingCL{ 281 Clid: 1, 282 ApplyTo: &prjpb.PurgingCL_Triggers{ 283 Triggers: &run.Triggers{ 284 CqVoteTrigger: dryRun(epoch), 285 }, 286 }, 287 }) 288 expected := &clInfo{ 289 pcl: pm.MustPCL(1), 290 runIndexes: nil, 291 purgingCL: &prjpb.PurgingCL{ 292 Clid: 1, 293 ApplyTo: &prjpb.PurgingCL_Triggers{ 294 Triggers: &run.Triggers{ 295 CqVoteTrigger: dryRun(epoch), 296 }, 297 }, 298 }, 299 300 triagedCL: triagedCL{ 301 purgeReasons: nil, 302 cqReady: false, 303 nprReady: true, 304 deps: &triagedDeps{}, 305 }, 306 } 307 308 cls := do(&prjpb.Component{Clids: []int64{1}}) 309 So(cls, ShouldHaveLength, 1) 310 So(cls[1], shouldResembleTriagedCL, expected) 311 }) 312 313 Convey("new patch upload on CL with NPR being purged", func() { 314 sup.pb.PurgingCls = append(sup.pb.PurgingCls, &prjpb.PurgingCL{ 315 Clid: 1, 316 ApplyTo: &prjpb.PurgingCL_Triggers{ 317 Triggers: &run.Triggers{ 318 NewPatchsetRunTrigger: newPatchsetTrigger(epoch), 319 }, 320 }, 321 }) 322 expected := &clInfo{ 323 pcl: pm.MustPCL(1), 324 runIndexes: nil, 325 purgingCL: &prjpb.PurgingCL{ 326 Clid: 1, 327 ApplyTo: &prjpb.PurgingCL_Triggers{ 328 Triggers: &run.Triggers{ 329 NewPatchsetRunTrigger: newPatchsetTrigger(epoch), 330 }, 331 }, 332 }, 333 triagedCL: triagedCL{ 334 purgeReasons: nil, 335 cqReady: true, 336 nprReady: false, 337 deps: &triagedDeps{}, 338 }, 339 } 340 cls := do(&prjpb.Component{Clids: []int64{1}}) 341 So(cls, ShouldHaveLength, 1) 342 So(cls[1], shouldResembleTriagedCL, expected) 343 344 }) 345 }) 346 Convey("Triage with ongoing New Pachset Run", func() { 347 sup.pb.Pcls = []*prjpb.PCL{{ 348 Clid: 1, 349 ConfigGroupIndexes: []int32{nprIdx}, 350 Status: prjpb.PCL_OK, 351 Triggers: &run.Triggers{ 352 NewPatchsetRunTrigger: newPatchsetTrigger(epoch), 353 }, 354 Submitted: false, 355 Deps: nil, 356 }} 357 expected := &clInfo{ 358 pcl: pm.MustPCL(1), 359 runIndexes: []int32{0}, 360 triagedCL: triagedCL{ 361 purgeReasons: nil, 362 nprReady: true, 363 }, 364 } 365 cls := do(&prjpb.Component{ 366 Clids: []int64{1}, 367 Pruns: []*prjpb.PRun{{Id: "r1", Clids: []int64{1}, Mode: string(run.NewPatchsetRun)}}, 368 }) 369 So(cls, ShouldHaveLength, 1) 370 So(cls[1], shouldResembleTriagedCL, expected) 371 }) 372 Convey("Single CL Runs: typical CL stack", func() { 373 // CL 3 depends on 2, which in turn depends 1. 374 // Start configuration is each one is Dry-run triggered. 375 sup.pb.Pcls = []*prjpb.PCL{ 376 { 377 Clid: 1, 378 ConfigGroupIndexes: []int32{singIdx}, 379 Status: prjpb.PCL_OK, 380 Triggers: &run.Triggers{CqVoteTrigger: dryRun(epoch)}, 381 Submitted: false, 382 Deps: nil, 383 }, 384 { 385 Clid: 2, 386 ConfigGroupIndexes: []int32{singIdx}, 387 Status: prjpb.PCL_OK, 388 Triggers: &run.Triggers{CqVoteTrigger: dryRun(epoch)}, 389 Submitted: false, 390 Deps: []*changelist.Dep{{Clid: 1, Kind: changelist.DepKind_HARD}}, 391 }, 392 { 393 Clid: 3, 394 ConfigGroupIndexes: []int32{singIdx}, 395 Status: prjpb.PCL_OK, 396 Triggers: &run.Triggers{CqVoteTrigger: dryRun(epoch)}, 397 Submitted: false, 398 Deps: []*changelist.Dep{{Clid: 1, Kind: changelist.DepKind_SOFT}, {Clid: 2, Kind: changelist.DepKind_HARD}}, 399 }, 400 } 401 402 Convey("Dry run everywhere is OK", func() { 403 cls := do(&prjpb.Component{Clids: []int64{1, 2, 3}}) 404 So(cls, ShouldHaveLength, 3) 405 for _, info := range cls { 406 So(info.cqReady, ShouldBeTrue) 407 So(info.deps.OK(), ShouldBeTrue) 408 So(info.lastCQVoteTriggered(), ShouldResemble, epoch) 409 } 410 }) 411 412 Convey("Full run at the bottom (CL1) and dry run elsewhere is also OK", func() { 413 sup.PCL(1).Triggers = &run.Triggers{CqVoteTrigger: fullRun(epoch)} 414 cls := do(&prjpb.Component{Clids: []int64{1, 2, 3}}) 415 So(cls, ShouldHaveLength, 3) 416 for _, info := range cls { 417 So(info.cqReady, ShouldBeTrue) 418 So(info.deps.OK(), ShouldBeTrue) 419 } 420 }) 421 422 Convey("Full Run on #3 is purged if its deps aren't submitted, but NPR is not affected", func() { 423 sup.PCL(1).Triggers = &run.Triggers{CqVoteTrigger: dryRun(epoch), NewPatchsetRunTrigger: newPatchsetTrigger(epoch)} 424 sup.PCL(2).Triggers = &run.Triggers{CqVoteTrigger: dryRun(epoch), NewPatchsetRunTrigger: newPatchsetTrigger(epoch)} 425 sup.PCL(3).Triggers = &run.Triggers{CqVoteTrigger: fullRun(epoch), NewPatchsetRunTrigger: newPatchsetTrigger(epoch)} 426 cls := do(&prjpb.Component{Clids: []int64{1, 2, 3}}) 427 So(cls[1].cqReady, ShouldBeTrue) 428 So(cls[2].cqReady, ShouldBeTrue) 429 So(cls[1].nprReady, ShouldBeTrue) 430 So(cls[2].nprReady, ShouldBeTrue) 431 So(cls[3].nprReady, ShouldBeTrue) 432 So(cls[3], shouldResembleTriagedCL, &clInfo{ 433 pcl: sup.PCL(3), 434 triagedCL: triagedCL{ 435 cqReady: false, 436 nprReady: true, 437 purgeReasons: []*prjpb.PurgeReason{{ 438 ClError: &changelist.CLError{ 439 Kind: &changelist.CLError_InvalidDeps_{ 440 InvalidDeps: &changelist.CLError_InvalidDeps{ 441 SingleFullDeps: sup.PCL(3).GetDeps(), 442 }, 443 }, 444 }, 445 ApplyTo: &prjpb.PurgeReason_Triggers{ 446 Triggers: &run.Triggers{ 447 CqVoteTrigger: fullRun(epoch), 448 }, 449 }, 450 }}, 451 deps: &triagedDeps{ 452 lastCQVoteTriggered: epoch, 453 invalidDeps: &changelist.CLError_InvalidDeps{ 454 SingleFullDeps: sup.PCL(3).GetDeps(), 455 }, 456 }, 457 }, 458 }) 459 }) 460 461 Convey("CL1 submitted but still with Run, CL2 CQ+1 is OK, CL3 CQ+2 is purged", func() { 462 sup.PCL(1).Triggers = nil 463 sup.PCL(1).Submitted = true 464 // PCL(2) is still not submitted. 465 sup.PCL(3).Triggers = &run.Triggers{CqVoteTrigger: fullRun(epoch)} 466 cls := do(&prjpb.Component{ 467 Clids: []int64{1, 2, 3}, 468 Pruns: []*prjpb.PRun{{Id: "r1", Clids: []int64{1}, Mode: string(run.FullRun)}}, 469 }) 470 So(cls[2].cqReady, ShouldBeTrue) 471 So(cls[2].deps, cvtesting.SafeShouldResemble, &triagedDeps{ 472 submitted: []*changelist.Dep{{Clid: 1, Kind: changelist.DepKind_HARD}}, 473 }) 474 So(cls[3], shouldResembleTriagedCL, &clInfo{ 475 pcl: sup.PCL(3), 476 triagedCL: triagedCL{ 477 cqReady: false, 478 purgeReasons: []*prjpb.PurgeReason{{ 479 ClError: &changelist.CLError{ 480 Kind: &changelist.CLError_InvalidDeps_{ 481 InvalidDeps: &changelist.CLError_InvalidDeps{ 482 SingleFullDeps: []*changelist.Dep{{Clid: 2, Kind: changelist.DepKind_HARD}}, 483 }, 484 }, 485 }, 486 ApplyTo: &prjpb.PurgeReason_Triggers{ 487 Triggers: &run.Triggers{ 488 CqVoteTrigger: fullRun(epoch), 489 }, 490 }, 491 }}, 492 deps: &triagedDeps{ 493 lastCQVoteTriggered: epoch.UTC(), 494 submitted: []*changelist.Dep{{Clid: 1, Kind: changelist.DepKind_SOFT}}, 495 invalidDeps: &changelist.CLError_InvalidDeps{ 496 SingleFullDeps: []*changelist.Dep{{Clid: 2, Kind: changelist.DepKind_HARD}}, 497 }, 498 }, 499 }, 500 }) 501 }) 502 }) 503 504 Convey("Multiple CL Runs: 1<->2 and 3 depending on both", func() { 505 // CL 3 depends on 1 and 2, while 1 and 2 depend on each other (e.g. via 506 // CQ-Depend). Start configuration is each one is Dry-run triggered. 507 sup.pb.Pcls = []*prjpb.PCL{ 508 { 509 Clid: 1, 510 ConfigGroupIndexes: []int32{combIdx}, 511 Status: prjpb.PCL_OK, 512 Triggers: &run.Triggers{CqVoteTrigger: dryRun(epoch)}, 513 Submitted: false, 514 Deps: []*changelist.Dep{{Clid: 2, Kind: changelist.DepKind_SOFT}}, 515 }, 516 { 517 Clid: 2, 518 ConfigGroupIndexes: []int32{combIdx}, 519 Status: prjpb.PCL_OK, 520 Triggers: &run.Triggers{CqVoteTrigger: dryRun(epoch)}, 521 Submitted: false, 522 Deps: []*changelist.Dep{{Clid: 1, Kind: changelist.DepKind_SOFT}}, 523 }, 524 { 525 Clid: 3, 526 ConfigGroupIndexes: []int32{combIdx}, 527 Status: prjpb.PCL_OK, 528 Triggers: &run.Triggers{CqVoteTrigger: dryRun(epoch)}, 529 Submitted: false, 530 Deps: []*changelist.Dep{{Clid: 1, Kind: changelist.DepKind_SOFT}, {Clid: 2, Kind: changelist.DepKind_SOFT}}, 531 }, 532 } 533 534 Convey("Happy case: all are ready", func() { 535 cls := do(&prjpb.Component{Clids: []int64{1, 2, 3}}) 536 So(cls, ShouldHaveLength, 3) 537 for _, info := range cls { 538 So(info.cqReady, ShouldBeTrue) 539 So(info.deps.OK(), ShouldBeTrue) 540 } 541 }) 542 543 Convey("Full Run on #1 and #2 can co-exist, but Dry run on #3 is purged", func() { 544 // This scenario documents current CQDaemon behavior. This isn't desired 545 // long term though. 546 sup.PCL(1).Triggers = &run.Triggers{CqVoteTrigger: fullRun(epoch)} 547 sup.PCL(2).Triggers = &run.Triggers{CqVoteTrigger: fullRun(epoch)} 548 cls := do(&prjpb.Component{Clids: []int64{1, 2, 3}}) 549 So(cls[1].cqReady, ShouldBeTrue) 550 So(cls[2].cqReady, ShouldBeTrue) 551 So(cls[3], shouldResembleTriagedCL, &clInfo{ 552 pcl: sup.PCL(3), 553 triagedCL: triagedCL{ 554 cqReady: false, 555 purgeReasons: []*prjpb.PurgeReason{{ 556 ClError: &changelist.CLError{ 557 Kind: &changelist.CLError_InvalidDeps_{ 558 InvalidDeps: &changelist.CLError_InvalidDeps{ 559 CombinableMismatchedMode: sup.PCL(3).GetDeps(), 560 }, 561 }, 562 }, 563 ApplyTo: &prjpb.PurgeReason_Triggers{ 564 Triggers: &run.Triggers{ 565 CqVoteTrigger: dryRun(epoch), 566 }, 567 }, 568 }}, 569 deps: &triagedDeps{ 570 lastCQVoteTriggered: epoch, 571 invalidDeps: &changelist.CLError_InvalidDeps{ 572 CombinableMismatchedMode: sup.PCL(3).GetDeps(), 573 }, 574 }, 575 }, 576 }) 577 }) 578 579 Convey("Dependencies in diff config groups are not allowed", func() { 580 sup.PCL(1).ConfigGroupIndexes = []int32{combIdx} // depends on 2 581 sup.PCL(2).ConfigGroupIndexes = []int32{anotherIdx} // depends on 1 582 sup.PCL(3).ConfigGroupIndexes = []int32{combIdx} // depends on 1(OK) and 2. 583 cls := do(&prjpb.Component{Clids: []int64{1, 2, 3}}) 584 for _, info := range cls { 585 So(info.cqReady, ShouldBeFalse) 586 So(info.purgeReasons, ShouldResembleProto, []*prjpb.PurgeReason{{ 587 ClError: &changelist.CLError{ 588 Kind: &changelist.CLError_InvalidDeps_{ 589 InvalidDeps: info.triagedCL.deps.invalidDeps, 590 }, 591 }, 592 ApplyTo: &prjpb.PurgeReason_Triggers{ 593 Triggers: &run.Triggers{ 594 CqVoteTrigger: dryRun(epoch), 595 }, 596 }, 597 }}) 598 } 599 600 Convey("unless dependency is already submitted", func() { 601 sup.PCL(2).Triggers = nil 602 sup.PCL(2).Submitted = true 603 604 cls := do(&prjpb.Component{Clids: []int64{1, 3}}) 605 for _, info := range cls { 606 So(info.cqReady, ShouldBeTrue) 607 So(info.purgeReasons, ShouldBeNil) 608 So(info.deps.submitted, ShouldResembleProto, []*changelist.Dep{{Clid: 2, Kind: changelist.DepKind_SOFT}}) 609 } 610 }) 611 }) 612 }) 613 614 Convey("Ready CLs can have not yet loaded dependencies", func() { 615 sup.pb.Pcls = []*prjpb.PCL{ 616 { 617 Clid: 1, 618 Status: prjpb.PCL_UNKNOWN, 619 }, 620 { 621 Clid: 2, 622 ConfigGroupIndexes: []int32{combIdx}, 623 Status: prjpb.PCL_OK, 624 Triggers: &run.Triggers{CqVoteTrigger: dryRun(epoch)}, 625 Deps: []*changelist.Dep{{Clid: 1, Kind: changelist.DepKind_SOFT}}, 626 }, 627 } 628 cls := do(&prjpb.Component{Clids: []int64{2}}) 629 So(cls[2], shouldResembleTriagedCL, &clInfo{ 630 pcl: sup.PCL(2), 631 triagedCL: triagedCL{ 632 cqReady: true, 633 deps: &triagedDeps{notYetLoaded: sup.PCL(2).GetDeps()}, 634 }, 635 }) 636 }) 637 638 Convey("Multiple CL Runs with chained CQ votes", func() { 639 const clid1, clid2, clid3, clid4 = 1, 2, 3, 4 640 641 newCL := func(clid int64, deps ...*changelist.Dep) *prjpb.PCL { 642 return &prjpb.PCL{ 643 Clid: clid, 644 ConfigGroupIndexes: []int32{singIdx}, 645 Status: prjpb.PCL_OK, 646 Submitted: false, 647 Deps: deps, 648 } 649 } 650 Dep := func(clid int64) *changelist.Dep { 651 return &changelist.Dep{Clid: clid, Kind: changelist.DepKind_HARD} 652 } 653 voter := "test@example.org" 654 ct.AddMember(voter, common.MCEDogfooderGroup) 655 sup.pb.Pcls = []*prjpb.PCL{ 656 newCL(clid1), 657 newCL(clid2, Dep(clid1)), 658 newCL(clid3, Dep(clid1), Dep(clid2)), 659 newCL(clid4, Dep(clid1), Dep(clid2), Dep(clid3)), 660 } 661 662 Convey("CQ vote on a child CL", func() { 663 // Trigger CQ on the CL 3 only. 664 sup.pb.Pcls[2].Triggers = &run.Triggers{CqVoteTrigger: fullRun(epoch)} 665 sup.pb.Pcls[2].Triggers.CqVoteTrigger.Email = voter 666 cls := do(&prjpb.Component{Clids: []int64{clid1, clid2, clid3, clid4}}) 667 So(cls, ShouldHaveLength, 4) 668 669 // - all CLs should be not-cq-ready. 670 So(cls[clid1].cqReady, ShouldBeFalse) 671 So(cls[clid2].cqReady, ShouldBeFalse) 672 So(cls[clid3].cqReady, ShouldBeFalse) 673 So(cls[clid4].cqReady, ShouldBeFalse) 674 // - CL3 should have CL1, and CL2 in needToTrigger, whereas 675 // the others shouldn't have any, because only CL3 has 676 // the CQ vote. Deps are not triaged, unless a given CL has 677 // a CQ vote. 678 So(cls[clid1].deps, ShouldBeNil) 679 So(cls[clid2].deps, ShouldBeNil) 680 So(cls[clid3].deps.needToTrigger, ShouldResembleProto, []*changelist.Dep{ 681 Dep(clid1), Dep(clid2), 682 }) 683 So(cls[clid4].deps, ShouldBeNil) 684 }) 685 686 Convey("CQ vote on multi CLs", func() { 687 // Trigger CQ on the CL 2 and 4. 688 sup.pb.Pcls[1].Triggers = &run.Triggers{CqVoteTrigger: fullRun(epoch)} 689 sup.pb.Pcls[1].Triggers.CqVoteTrigger.Email = voter 690 sup.pb.Pcls[3].Triggers = &run.Triggers{CqVoteTrigger: fullRun(epoch)} 691 sup.pb.Pcls[3].Triggers.CqVoteTrigger.Email = voter 692 cls := do(&prjpb.Component{Clids: []int64{clid1, clid2, clid3, clid4}}) 693 So(cls, ShouldHaveLength, 4) 694 695 // - all CLs should not be cq-ready. 696 So(cls[clid1].cqReady, ShouldBeFalse) 697 So(cls[clid2].cqReady, ShouldBeFalse) 698 So(cls[clid3].cqReady, ShouldBeFalse) 699 So(cls[clid4].cqReady, ShouldBeFalse) 700 // - CL3 should have CL1, and CL2 in needToTrigger, whereas 701 // the others shouldn't have any, because only CL3 has 702 // the CQ vote. Deps are not triaged, unless a given CL has 703 // a CQ vote. 704 So(cls[clid1].deps, ShouldBeNil) 705 So(cls[clid2].deps.needToTrigger, ShouldResembleProto, []*changelist.Dep{ 706 Dep(clid1), 707 }) 708 So(cls[clid3].deps, ShouldBeNil) 709 So(cls[clid4].deps.needToTrigger, ShouldResembleProto, []*changelist.Dep{ 710 // Should NOT have clid4 in needToTrigger, as it is already 711 // voted. 712 Dep(clid1), Dep(clid3), 713 }) 714 }) 715 716 Convey("CqReady if all voted", func() { 717 // Vote on all the CLs. 718 for i := 0; i < 4; i++ { 719 sup.pb.Pcls[i].Triggers = &run.Triggers{CqVoteTrigger: fullRun(epoch)} 720 sup.pb.Pcls[i].Triggers.CqVoteTrigger.Email = voter 721 } 722 cls := do(&prjpb.Component{Clids: []int64{clid1, clid2, clid3, clid4}}) 723 So(cls, ShouldHaveLength, 4) 724 725 // They all should be cq-ready. 726 So(cls[clid1].cqReady, ShouldBeTrue) 727 So(cls[clid2].cqReady, ShouldBeTrue) 728 So(cls[clid3].cqReady, ShouldBeTrue) 729 So(cls[clid4].cqReady, ShouldBeTrue) 730 731 Convey("unless there is an inflight TriggeringCLDeps{}", func() { 732 sup.pb.TriggeringClDeps, _ = sup.pb.COWTriggeringCLDeps(nil, []*prjpb.TriggeringCLDeps{ 733 {OperationId: "op-1", OriginClid: clid4, DepClids: []int64{1, 2, 3}}, 734 }) 735 cls := do(&prjpb.Component{Clids: []int64{clid1, clid2, clid3, clid4}}) 736 So(cls, ShouldHaveLength, 4) 737 738 // They all should not be cq-ready. 739 So(cls[clid1].cqReady, ShouldBeFalse) 740 So(cls[clid2].cqReady, ShouldBeFalse) 741 So(cls[clid3].cqReady, ShouldBeFalse) 742 So(cls[clid4].cqReady, ShouldBeFalse) 743 }) 744 }) 745 }) 746 }) 747 }