go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/prjmanager/triager/deps_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 "testing" 20 "time" 21 22 "google.golang.org/protobuf/proto" 23 "google.golang.org/protobuf/types/known/timestamppb" 24 25 "go.chromium.org/luci/common/clock/testclock" 26 27 cfgpb "go.chromium.org/luci/cv/api/config/v2" 28 "go.chromium.org/luci/cv/internal/changelist" 29 "go.chromium.org/luci/cv/internal/common" 30 "go.chromium.org/luci/cv/internal/configs/prjcfg" 31 "go.chromium.org/luci/cv/internal/cvtesting" 32 "go.chromium.org/luci/cv/internal/prjmanager/prjpb" 33 "go.chromium.org/luci/cv/internal/run" 34 35 . "github.com/smartystreets/goconvey/convey" 36 . "go.chromium.org/luci/common/testing/assertions" 37 ) 38 39 func TestDepsTriage(t *testing.T) { 40 t.Parallel() 41 42 Convey("Component's PCL deps triage", t, func() { 43 ct := cvtesting.Test{} 44 ctx, cancel := ct.SetUp(t) 45 defer cancel() 46 47 // Truncate start time point s.t. easy to see diff in test failures. 48 epoch := testclock.TestRecentTimeUTC.Truncate(10000 * time.Second) 49 dryRun := func(t time.Time) *run.Triggers { 50 return &run.Triggers{CqVoteTrigger: &run.Trigger{Mode: string(run.DryRun), Time: timestamppb.New(t)}} 51 } 52 fullRun := func(t time.Time) *run.Triggers { 53 return &run.Triggers{CqVoteTrigger: &run.Trigger{Mode: string(run.FullRun), Time: timestamppb.New(t)}} 54 } 55 56 sup := &simplePMState{ 57 pb: &prjpb.PState{}, 58 cgs: []*prjcfg.ConfigGroup{ 59 {ID: "hash/singular", Content: &cfgpb.ConfigGroup{}}, 60 {ID: "hash/combinable", Content: &cfgpb.ConfigGroup{CombineCls: &cfgpb.CombineCLs{}}}, 61 {ID: "hash/another", Content: &cfgpb.ConfigGroup{}}, 62 }, 63 } 64 const singIdx, combIdx, anotherIdx = 0, 1, 2 65 66 do := func(pcl *prjpb.PCL, cgIdx int32) *triagedDeps { 67 backup := prjpb.PState{} 68 proto.Merge(&backup, sup.pb) 69 70 // Actual component doesn't matter in this test. 71 td := triageDeps(ctx, pcl, cgIdx, pmState{sup}) 72 So(sup.pb, ShouldResembleProto, &backup) // must not be modified 73 return td 74 } 75 76 Convey("Singluar and Combinable behave the same", func() { 77 sameTests := func(name string, cgIdx int32) { 78 Convey(name, func() { 79 Convey("no deps", func() { 80 sup.pb.Pcls = []*prjpb.PCL{ 81 {Clid: 33, ConfigGroupIndexes: []int32{cgIdx}}, 82 } 83 td := do(sup.pb.Pcls[0], cgIdx) 84 So(td, cvtesting.SafeShouldResemble, &triagedDeps{}) 85 So(td.OK(), ShouldBeTrue) 86 }) 87 88 Convey("Valid CL stack CQ+1", func() { 89 sup.pb.Pcls = []*prjpb.PCL{ 90 {Clid: 31, ConfigGroupIndexes: []int32{cgIdx}, Triggers: dryRun(epoch.Add(3 * time.Second))}, 91 {Clid: 32, ConfigGroupIndexes: []int32{cgIdx}, Triggers: dryRun(epoch.Add(2 * time.Second))}, 92 {Clid: 33, ConfigGroupIndexes: []int32{cgIdx}, Triggers: dryRun(epoch.Add(1 * time.Second)), 93 Deps: []*changelist.Dep{ 94 {Clid: 31, Kind: changelist.DepKind_SOFT}, 95 {Clid: 32, Kind: changelist.DepKind_HARD}, 96 }}, 97 } 98 td := do(sup.PCL(33), cgIdx) 99 So(td, cvtesting.SafeShouldResemble, &triagedDeps{ 100 lastCQVoteTriggered: epoch.Add(3 * time.Second), 101 }) 102 So(td.OK(), ShouldBeTrue) 103 }) 104 105 Convey("Not yet loaded deps", func() { 106 sup.pb.Pcls = []*prjpb.PCL{ 107 // 31 isn't in PCLs yet 108 {Clid: 32, Status: prjpb.PCL_UNKNOWN}, 109 {Clid: 33, ConfigGroupIndexes: []int32{cgIdx}, Triggers: dryRun(epoch.Add(1 * time.Second)), 110 Deps: []*changelist.Dep{ 111 {Clid: 31, Kind: changelist.DepKind_SOFT}, 112 {Clid: 32, Kind: changelist.DepKind_HARD}, 113 }}, 114 } 115 pcl33 := sup.PCL(33) 116 td := do(pcl33, cgIdx) 117 So(td, cvtesting.SafeShouldResemble, &triagedDeps{notYetLoaded: pcl33.GetDeps()}) 118 So(td.OK(), ShouldBeTrue) 119 }) 120 121 Convey("Unwatched", func() { 122 sup.pb.Pcls = []*prjpb.PCL{ 123 {Clid: 31, Status: prjpb.PCL_UNWATCHED}, 124 {Clid: 32, Status: prjpb.PCL_DELETED}, 125 {Clid: 33, ConfigGroupIndexes: []int32{cgIdx}, Triggers: dryRun(epoch.Add(1 * time.Second)), 126 Deps: []*changelist.Dep{ 127 {Clid: 31, Kind: changelist.DepKind_SOFT}, 128 {Clid: 32, Kind: changelist.DepKind_HARD}, 129 }}, 130 } 131 pcl33 := sup.PCL(33) 132 td := do(pcl33, cgIdx) 133 So(td, cvtesting.SafeShouldResemble, &triagedDeps{ 134 invalidDeps: &changelist.CLError_InvalidDeps{ 135 Unwatched: pcl33.GetDeps(), 136 }, 137 }) 138 So(td.OK(), ShouldBeFalse) 139 }) 140 141 Convey("Submitted can be in any config group and they are OK deps", func() { 142 sup.pb.Pcls = []*prjpb.PCL{ 143 {Clid: 32, ConfigGroupIndexes: []int32{anotherIdx}, Submitted: true}, 144 {Clid: 33, ConfigGroupIndexes: []int32{cgIdx}, Triggers: dryRun(epoch.Add(1 * time.Second)), 145 Deps: []*changelist.Dep{{Clid: 32, Kind: changelist.DepKind_HARD}}}, 146 } 147 pcl33 := sup.PCL(33) 148 td := do(pcl33, cgIdx) 149 So(td, cvtesting.SafeShouldResemble, &triagedDeps{submitted: pcl33.GetDeps()}) 150 So(td.OK(), ShouldBeTrue) 151 }) 152 153 Convey("Wrong config group", func() { 154 sup.pb.Pcls = []*prjpb.PCL{ 155 {Clid: 31, Triggers: dryRun(epoch.Add(3 * time.Second)), ConfigGroupIndexes: []int32{anotherIdx}}, 156 {Clid: 32, Triggers: dryRun(epoch.Add(2 * time.Second)), ConfigGroupIndexes: []int32{anotherIdx, cgIdx}}, 157 {Clid: 33, Triggers: dryRun(epoch.Add(1 * time.Second)), ConfigGroupIndexes: []int32{cgIdx}, 158 Deps: []*changelist.Dep{ 159 {Clid: 31, Kind: changelist.DepKind_SOFT}, 160 {Clid: 32, Kind: changelist.DepKind_HARD}, 161 }}, 162 } 163 pcl33 := sup.PCL(33) 164 td := do(pcl33, cgIdx) 165 So(td, cvtesting.SafeShouldResemble, &triagedDeps{ 166 lastCQVoteTriggered: epoch.Add(3 * time.Second), 167 invalidDeps: &changelist.CLError_InvalidDeps{ 168 WrongConfigGroup: pcl33.GetDeps(), 169 }, 170 }) 171 So(td.OK(), ShouldBeFalse) 172 }) 173 174 Convey("Too many deps", func() { 175 // Create maxAllowedDeps+1 deps. 176 sup.pb.Pcls = make([]*prjpb.PCL, 0, maxAllowedDeps+2) 177 deps := make([]*changelist.Dep, 0, maxAllowedDeps+1) 178 for i := 1; i <= maxAllowedDeps+1; i++ { 179 sup.pb.Pcls = append(sup.pb.Pcls, &prjpb.PCL{ 180 Clid: int64(1000 + i), 181 ConfigGroupIndexes: []int32{cgIdx}, 182 Triggers: dryRun(epoch.Add(time.Second)), 183 }) 184 deps = append(deps, &changelist.Dep{Clid: int64(1000 + i), Kind: changelist.DepKind_SOFT}) 185 } 186 // Add the PCL with the above deps. 187 sup.pb.Pcls = append(sup.pb.Pcls, &prjpb.PCL{ 188 Clid: 2000, 189 ConfigGroupIndexes: []int32{cgIdx}, 190 Triggers: dryRun(epoch.Add(time.Second)), 191 Deps: deps, 192 }) 193 td := do(sup.PCL(2000), cgIdx) 194 So(td, cvtesting.SafeShouldResemble, &triagedDeps{ 195 lastCQVoteTriggered: epoch.Add(time.Second), 196 invalidDeps: &changelist.CLError_InvalidDeps{ 197 TooMany: &changelist.CLError_InvalidDeps_TooMany{ 198 Actual: maxAllowedDeps + 1, 199 MaxAllowed: maxAllowedDeps, 200 }, 201 }, 202 }) 203 So(td.OK(), ShouldBeFalse) 204 }) 205 }) 206 } 207 sameTests("singular", singIdx) 208 sameTests("combinable", combIdx) 209 }) 210 211 Convey("Singular speciality", func() { 212 sup.pb.Pcls = []*prjpb.PCL{ 213 { 214 Clid: 31, ConfigGroupIndexes: []int32{singIdx}, 215 Triggers: dryRun(epoch.Add(3 * time.Second)), 216 }, 217 { 218 Clid: 32, ConfigGroupIndexes: []int32{singIdx}, 219 Triggers: fullRun(epoch.Add(2 * time.Second)), // not happy about its dep. 220 Deps: []*changelist.Dep{{Clid: 31, Kind: changelist.DepKind_HARD}}, 221 }, 222 { 223 Clid: 33, ConfigGroupIndexes: []int32{singIdx}, 224 Triggers: dryRun(epoch.Add(3 * time.Second)), // doesn't care about deps. 225 Deps: []*changelist.Dep{ 226 {Clid: 31, Kind: changelist.DepKind_SOFT}, 227 {Clid: 32, Kind: changelist.DepKind_HARD}, 228 }, 229 }, 230 } 231 Convey("dry run doesn't care about deps' triggers", func() { 232 pcl33 := sup.PCL(33) 233 td := do(pcl33, singIdx) 234 So(td, cvtesting.SafeShouldResemble, &triagedDeps{ 235 lastCQVoteTriggered: epoch.Add(3 * time.Second), 236 }) 237 }) 238 Convey("full run doesn't allow any dep by default", func() { 239 pcl32 := sup.PCL(32) 240 td := do(pcl32, singIdx) 241 So(td, cvtesting.SafeShouldResemble, &triagedDeps{ 242 lastCQVoteTriggered: epoch.Add(3 * time.Second), 243 invalidDeps: &changelist.CLError_InvalidDeps{ 244 SingleFullDeps: pcl32.GetDeps(), 245 }, 246 }) 247 So(td.OK(), ShouldBeFalse) 248 249 Convey("unless allow_submit_with_open_deps is true", func() { 250 sup.cgs[singIdx].Content.Verifiers = &cfgpb.Verifiers{ 251 GerritCqAbility: &cfgpb.Verifiers_GerritCQAbility{ 252 AllowSubmitWithOpenDeps: true, 253 }, 254 } 255 td := do(pcl32, singIdx) 256 So(td, cvtesting.SafeShouldResemble, &triagedDeps{ 257 lastCQVoteTriggered: epoch.Add(3 * time.Second), 258 }) 259 So(td.OK(), ShouldBeTrue) 260 261 Convey("but not if dep is soft", func() { 262 // Soft dependency (ie via Cq-Depend) won't be submitted as part a 263 // single Submit gerrit RPC, so it can't be allowed. 264 pcl32.GetDeps()[0].Kind = changelist.DepKind_SOFT 265 td := do(pcl32, singIdx) 266 So(td, cvtesting.SafeShouldResemble, &triagedDeps{ 267 lastCQVoteTriggered: epoch.Add(3 * time.Second), 268 invalidDeps: &changelist.CLError_InvalidDeps{ 269 SingleFullDeps: pcl32.GetDeps(), 270 }, 271 }) 272 So(td.OK(), ShouldBeFalse) 273 }) 274 }) 275 276 Convey("unless the user is an MCE dogfooder and deps are HARD", func() { 277 // TODO(crbug/1470341) remove this test if chained cq votes 278 // is enabled by default. 279 pcl31 := sup.PCL(31) 280 pcl31.Triggers = nil 281 pcl32 := sup.PCL(32) 282 pcl32.Triggers.CqVoteTrigger.Email = "test@example.org" 283 ct.AddMember("test@example.org", common.MCEDogfooderGroup) 284 285 // triage with HARD dep. It should be good. 286 td := do(pcl32, singIdx) 287 So(td.OK(), ShouldBeTrue) 288 // triage with a SOFT dep. This should fail, as chained cq 289 // votes only support HARD deps. 290 pcl32.GetDeps()[0].Kind = changelist.DepKind_SOFT 291 td = do(pcl32, singIdx) 292 So(td, cvtesting.SafeShouldResemble, &triagedDeps{ 293 invalidDeps: &changelist.CLError_InvalidDeps{ 294 SingleFullDeps: pcl32.GetDeps(), 295 }, 296 }) 297 So(td.OK(), ShouldBeFalse) 298 }) 299 }) 300 }) 301 302 Convey("Full run with chained CQ votes", func() { 303 voter := "test@example.org" 304 ct.AddMember(voter, common.MCEDogfooderGroup) 305 sup.pb.Pcls = []*prjpb.PCL{ 306 { 307 Clid: 31, ConfigGroupIndexes: []int32{singIdx}, 308 }, 309 { 310 Clid: 32, ConfigGroupIndexes: []int32{singIdx}, 311 Deps: []*changelist.Dep{ 312 {Clid: 31, Kind: changelist.DepKind_HARD}, 313 }, 314 }, 315 { 316 Clid: 33, ConfigGroupIndexes: []int32{singIdx}, 317 Deps: []*changelist.Dep{ 318 {Clid: 31, Kind: changelist.DepKind_HARD}, 319 {Clid: 32, Kind: changelist.DepKind_HARD}, 320 }, 321 }, 322 } 323 324 Convey("Single vote on the topmost CL", func() { 325 pcl33 := sup.PCL(33) 326 pcl33.Triggers = fullRun(epoch) 327 pcl33.Triggers.CqVoteTrigger.Email = voter 328 td := do(pcl33, singIdx) 329 330 // The triage dep result should be OK(), but have 331 // the not-yet-voted deps in needToTrigger 332 So(td.OK(), ShouldBeTrue) 333 So(td.needToTrigger, ShouldResembleProto, []*changelist.Dep{ 334 {Clid: 31, Kind: changelist.DepKind_HARD}, 335 {Clid: 32, Kind: changelist.DepKind_HARD}, 336 }) 337 }) 338 Convey("a dep already has CQ+2", func() { 339 pcl31 := sup.PCL(31) 340 pcl31.Triggers = fullRun(epoch) 341 pcl31.Triggers.CqVoteTrigger.Email = voter 342 pcl33 := sup.PCL(33) 343 pcl33.Triggers = fullRun(epoch) 344 pcl33.Triggers.CqVoteTrigger.Email = voter 345 td := do(pcl33, singIdx) 346 347 So(td.OK(), ShouldBeTrue) 348 So(td.needToTrigger, ShouldResembleProto, []*changelist.Dep{ 349 {Clid: 32, Kind: changelist.DepKind_HARD}, 350 }) 351 }) 352 Convey("a dep has CQ+1", func() { 353 pcl31 := sup.PCL(31) 354 pcl31.Triggers = dryRun(epoch) 355 pcl31.Triggers.CqVoteTrigger.Email = voter 356 pcl33 := sup.PCL(33) 357 pcl33.Triggers = fullRun(epoch) 358 pcl33.Triggers.CqVoteTrigger.Email = voter 359 360 // triageDep should still put the dep with CQ+1 in 361 // needToTrigger, so that PM will schedule a TQ task to override 362 // the CQ vote with CQ+2. 363 td := do(pcl33, singIdx) 364 So(td.OK(), ShouldBeTrue) 365 So(td.needToTrigger, ShouldResembleProto, []*changelist.Dep{ 366 {Clid: 31, Kind: changelist.DepKind_HARD}, 367 {Clid: 32, Kind: changelist.DepKind_HARD}, 368 }) 369 }) 370 }) 371 Convey("Combinable speciality", func() { 372 // Setup valid deps; sub-tests wll mutate this to become invalid. 373 sup.pb.Pcls = []*prjpb.PCL{ 374 { 375 Clid: 31, ConfigGroupIndexes: []int32{combIdx}, 376 Triggers: dryRun(epoch.Add(3 * time.Second)), 377 }, 378 { 379 Clid: 32, ConfigGroupIndexes: []int32{combIdx}, 380 Triggers: dryRun(epoch.Add(2 * time.Second)), 381 Deps: []*changelist.Dep{{Clid: 31, Kind: changelist.DepKind_HARD}}, 382 }, 383 { 384 Clid: 33, ConfigGroupIndexes: []int32{combIdx}, 385 Triggers: dryRun(epoch.Add(1 * time.Second)), 386 Deps: []*changelist.Dep{ 387 {Clid: 31, Kind: changelist.DepKind_SOFT}, 388 {Clid: 32, Kind: changelist.DepKind_HARD}, 389 }, 390 }, 391 } 392 Convey("dry run expects all deps to be dry", func() { 393 pcl32 := sup.PCL(32) 394 Convey("ok", func() { 395 td := do(pcl32, combIdx) 396 So(td, cvtesting.SafeShouldResemble, &triagedDeps{lastCQVoteTriggered: epoch.Add(3 * time.Second)}) 397 }) 398 399 Convey("... not full runs", func() { 400 // TODO(tandrii): this can and should be supported. 401 sup.PCL(31).Triggers.CqVoteTrigger.Mode = string(run.FullRun) 402 td := do(pcl32, combIdx) 403 So(td, cvtesting.SafeShouldResemble, &triagedDeps{ 404 lastCQVoteTriggered: epoch.Add(3 * time.Second), 405 invalidDeps: &changelist.CLError_InvalidDeps{ 406 CombinableMismatchedMode: pcl32.GetDeps(), 407 }, 408 }) 409 }) 410 }) 411 Convey("full run considers any dep incompatible", func() { 412 pcl33 := sup.PCL(33) 413 Convey("ok", func() { 414 for _, pcl := range sup.pb.GetPcls() { 415 pcl.Triggers.CqVoteTrigger.Mode = string(run.FullRun) 416 } 417 td := do(pcl33, combIdx) 418 So(td, cvtesting.SafeShouldResemble, &triagedDeps{lastCQVoteTriggered: epoch.Add(3 * time.Second)}) 419 }) 420 Convey("... not dry runs", func() { 421 sup.PCL(32).Triggers.CqVoteTrigger.Mode = string(run.FullRun) 422 td := do(pcl33, combIdx) 423 So(td, cvtesting.SafeShouldResemble, &triagedDeps{ 424 lastCQVoteTriggered: epoch.Add(3 * time.Second), 425 invalidDeps: &changelist.CLError_InvalidDeps{ 426 CombinableMismatchedMode: []*changelist.Dep{{Clid: 32, Kind: changelist.DepKind_HARD}}, 427 }, 428 }) 429 So(td.OK(), ShouldBeFalse) 430 }) 431 }) 432 }) 433 434 Convey("iterateNotSubmitted works", func() { 435 d1 := &changelist.Dep{Clid: 1} 436 d2 := &changelist.Dep{Clid: 2} 437 d3 := &changelist.Dep{Clid: 3} 438 pcl := &prjpb.PCL{} 439 td := &triagedDeps{} 440 441 iterate := func() (out []*changelist.Dep) { 442 td.iterateNotSubmitted(pcl, func(dep *changelist.Dep) { out = append(out, dep) }) 443 return 444 } 445 446 Convey("no deps", func() { 447 So(iterate(), ShouldBeEmpty) 448 }) 449 Convey("only submitted", func() { 450 td.submitted = []*changelist.Dep{d3, d1, d2} 451 pcl.Deps = []*changelist.Dep{d3, d1, d2} // order must be the same 452 So(iterate(), ShouldBeEmpty) 453 }) 454 Convey("some submitted", func() { 455 pcl.Deps = []*changelist.Dep{d3, d1, d2} 456 td.submitted = []*changelist.Dep{d3} 457 So(iterate(), ShouldResembleProto, []*changelist.Dep{d1, d2}) 458 td.submitted = []*changelist.Dep{d1} 459 So(iterate(), ShouldResembleProto, []*changelist.Dep{d3, d2}) 460 td.submitted = []*changelist.Dep{d2} 461 So(iterate(), ShouldResembleProto, []*changelist.Dep{d3, d1}) 462 }) 463 Convey("none submitted", func() { 464 pcl.Deps = []*changelist.Dep{d3, d1, d2} 465 So(iterate(), ShouldResembleProto, []*changelist.Dep{d3, d1, d2}) 466 }) 467 Convey("notYetLoaded deps are iterated over, too", func() { 468 pcl.Deps = []*changelist.Dep{d3, d1, d2} 469 td.notYetLoaded = []*changelist.Dep{d3} 470 td.submitted = []*changelist.Dep{d2} 471 So(iterate(), ShouldResembleProto, []*changelist.Dep{d3, d1}) 472 }) 473 Convey("panic on invalid usage", func() { 474 Convey("wrong PCL", func() { 475 pcl.Deps = []*changelist.Dep{d3, d1, d2} 476 td.submitted = []*changelist.Dep{d1, d2, d3} // wrong order 477 So(func() { iterate() }, ShouldPanicLike, fmt.Errorf("(wrong PCL?)")) 478 }) 479 }) 480 }) 481 }) 482 }