go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/prjmanager/triager/trigger_test.go (about) 1 // Copyright 2023 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 21 cfgpb "go.chromium.org/luci/cv/api/config/v2" 22 "go.chromium.org/luci/cv/internal/changelist" 23 "go.chromium.org/luci/cv/internal/configs/prjcfg" 24 "go.chromium.org/luci/cv/internal/cvtesting" 25 "go.chromium.org/luci/cv/internal/prjmanager/prjpb" 26 "go.chromium.org/luci/cv/internal/run" 27 28 . "github.com/smartystreets/goconvey/convey" 29 . "go.chromium.org/luci/common/testing/assertions" 30 ) 31 32 type testCLInfo clInfo 33 34 func (ci *testCLInfo) Deps(deps ...*testCLInfo) *testCLInfo { 35 for _, dep := range deps { 36 ci.pcl.Deps = append(ci.pcl.Deps, &changelist.Dep{ 37 Clid: dep.Clid(), 38 Kind: changelist.DepKind_HARD, 39 }) 40 } 41 return ci 42 } 43 44 func (ci *testCLInfo) SoftDeps(deps ...*testCLInfo) *testCLInfo { 45 for _, dep := range deps { 46 ci.pcl.Deps = append(ci.pcl.Deps, &changelist.Dep{ 47 Clid: dep.Clid(), 48 Kind: changelist.DepKind_SOFT, 49 }) 50 } 51 return ci 52 } 53 54 func (ci *testCLInfo) CQ(val int) *testCLInfo { 55 switch val { 56 case 0: 57 ci.pcl.Triggers = nil 58 case 1: 59 ci.pcl.Triggers = &run.Triggers{ 60 CqVoteTrigger: &run.Trigger{ 61 Mode: string(run.DryRun), 62 }, 63 } 64 case 2: 65 ci.pcl.Triggers = &run.Triggers{ 66 CqVoteTrigger: &run.Trigger{ 67 Mode: string(run.FullRun), 68 }, 69 } 70 default: 71 panic(fmt.Errorf("unsupported CQ value")) 72 } 73 return ci 74 } 75 76 func (ci *testCLInfo) triageDeps(cls map[int64]*clInfo) { 77 mode := ci.pcl.GetTriggers().GetCqVoteTrigger().GetMode() 78 if mode != string(run.FullRun) { 79 return 80 } 81 ci.deps.needToTrigger = ci.deps.needToTrigger[:0] 82 for _, dep := range ci.pcl.GetDeps() { 83 dci, ok := cls[dep.GetClid()] 84 if !ok { 85 ci.deps.notYetLoaded = append(ci.deps.notYetLoaded, &changelist.Dep{ 86 Clid: dep.GetClid(), 87 Kind: changelist.DepKind_HARD, 88 }) 89 continue 90 } 91 depMode := dci.pcl.GetTriggers().GetCqVoteTrigger().GetMode() 92 if mode != depMode { 93 ci.deps.needToTrigger = append(ci.deps.needToTrigger, &changelist.Dep{ 94 Clid: dep.GetClid(), 95 Kind: changelist.DepKind_HARD, 96 }) 97 } 98 } 99 } 100 101 func (ci *testCLInfo) Clid() int64 { 102 return ci.pcl.GetClid() 103 } 104 105 func (ci *testCLInfo) NeedToTrigger() []int64 { 106 var ret []int64 107 if ci.deps == nil { 108 return ret 109 } 110 for _, dep := range ci.deps.needToTrigger { 111 ret = append(ret, dep.GetClid()) 112 } 113 return ret 114 } 115 116 func (ci *testCLInfo) SetPurgingCL() *testCLInfo { 117 ci.purgingCL = &prjpb.PurgingCL{} 118 return ci 119 } 120 121 func (ci *testCLInfo) SetPurgeReasons() *testCLInfo { 122 ci.purgeReasons = []*prjpb.PurgeReason{{}} 123 return ci 124 } 125 126 func (ci *testCLInfo) SetIncompleteRun(m run.Mode) *testCLInfo { 127 ci.runIndexes = []int32{1} 128 ci.runCountByMode[m]++ 129 return ci 130 } 131 132 func (ci *testCLInfo) SetTriggeringCLDeps() *testCLInfo { 133 ci.triggeringCLDeps = &prjpb.TriggeringCLDeps{} 134 return ci 135 } 136 137 func (ci *testCLInfo) Outdated() *testCLInfo { 138 ci.pcl.Outdated = &changelist.Snapshot_Outdated{} 139 return ci 140 } 141 142 func TestStageTriggerCLDeps(t *testing.T) { 143 t.Parallel() 144 145 Convey("stargeTriggerCLDeps", t, func() { 146 ct := cvtesting.Test{} 147 ctx, cancel := ct.SetUp(t) 148 defer cancel() 149 150 cq2 := &run.Trigger{Mode: string(run.FullRun)} 151 cls := make(map[int64]*clInfo) 152 nextCLID := int64(1) 153 newCL := func() *testCLInfo { 154 defer func() { nextCLID++ }() 155 tci := &testCLInfo{ 156 pcl: &prjpb.PCL{ 157 Clid: nextCLID, 158 ConfigGroupIndexes: []int32{0}, 159 }, 160 triagedCL: triagedCL{ 161 deps: &triagedDeps{}, 162 }, 163 runCountByMode: make(map[run.Mode]int), 164 } 165 cls[nextCLID] = (*clInfo)(tci) 166 return tci 167 } 168 triageDeps := func(cis ...*testCLInfo) { 169 for _, ci := range cis { 170 ci.triageDeps(cls) 171 } 172 } 173 sup := &simplePMState{ 174 pb: &prjpb.PState{}, 175 cgs: []*prjcfg.ConfigGroup{ 176 {ID: "hash/cg1", Content: &cfgpb.ConfigGroup{}}, 177 }, 178 } 179 pm := pmState{sup} 180 181 Convey("CLs without deps", func() { 182 cl1 := newCL().CQ(0) 183 cl2 := newCL().CQ(+2) 184 triageDeps(cl1, cl2) 185 So(cl1.NeedToTrigger(), ShouldBeNil) 186 So(cl2.NeedToTrigger(), ShouldBeNil) 187 So(stageTriggerCLDeps(ctx, cls, pm), ShouldHaveLength, 0) 188 }) 189 190 Convey("CL with deps", func() { 191 cl1 := newCL() 192 cl2 := newCL().Deps(cl1) 193 cl3 := newCL().Deps(cl1, cl2) 194 195 Convey("no deps have CQ vote", func() { 196 cl3 = cl3.CQ(+2) 197 triageDeps(cl1, cl2, cl3) 198 So(cl3.NeedToTrigger(), ShouldEqual, []int64{cl1.Clid(), cl2.Clid()}) 199 So(stageTriggerCLDeps(ctx, cls, pm), ShouldResembleProto, []*prjpb.TriggeringCLDeps{ 200 { 201 OriginClid: cl3.Clid(), 202 DepClids: []int64{cl1.Clid(), cl2.Clid()}, 203 Trigger: cq2, 204 ConfigGroupName: "cg1", 205 }, 206 }) 207 208 Convey("unless outdated", func() { 209 Convey("the origin CL", func() { 210 cl3 = cl3.Outdated() 211 }) 212 Convey("a dep CL", func() { 213 cl2 = cl2.Outdated() 214 }) 215 triageDeps(cl1, cl2, cl3) 216 So(cl3.NeedToTrigger(), ShouldEqual, []int64{cl1.Clid(), cl2.Clid()}) 217 So(stageTriggerCLDeps(ctx, cls, pm), ShouldBeNil) 218 }) 219 220 Convey("retriaging it should be noop", func() { 221 // Now, cl3 has TriggeringCLDeps, created by the previous 222 // stageTriggerCLDeps(), and let's say that cl2 has voted. 223 cl2 = cl2.CQ(+2) 224 cl3 = cl3.SetTriggeringCLDeps() 225 triageDeps(cl1, cl2, cl3) 226 227 // Now, triageDeps declares that both cl2 and cl3 have 228 // unvoted deps, but none of them should schedule a new 229 // task. 230 So(cl2.NeedToTrigger(), ShouldEqual, []int64{cl1.Clid()}) 231 So(cl3.NeedToTrigger(), ShouldEqual, []int64{cl1.Clid()}) 232 So(stageTriggerCLDeps(ctx, cls, pm), ShouldBeNil) 233 }) 234 235 Convey("unless a dep was not loaded yet", func() { 236 delete(cls, cl1.pcl.GetClid()) 237 triageDeps(cl1, cl2, cl3) 238 So(stageTriggerCLDeps(ctx, cls, pm), ShouldHaveLength, 0) 239 }) 240 }) 241 Convey("all deps have CQ vote", func() { 242 cl1 = cl1.CQ(+2) 243 cl2 = cl2.CQ(+2) 244 cl3 = cl3.CQ(+2) 245 triageDeps(cl1, cl2, cl3) 246 So(cl3.NeedToTrigger(), ShouldBeNil) 247 So(stageTriggerCLDeps(ctx, cls, pm), ShouldHaveLength, 0) 248 }) 249 Convey("some deps have and some others don't have CQ votes", func() { 250 cl2 = cl2.CQ(+2) 251 cl3 = cl3.CQ(+2) 252 triageDeps(cl1, cl2, cl3) 253 // Both cl2.deps and cl3.deps have cl1 in needToTrigger, but 254 // TriggerCLDeps{} should be created for cl3 only. 255 So(cl2.NeedToTrigger(), ShouldEqual, []int64{cl1.Clid()}) 256 So(cl3.NeedToTrigger(), ShouldEqual, []int64{cl1.Clid()}) 257 So(stageTriggerCLDeps(ctx, cls, pm), ShouldResembleProto, []*prjpb.TriggeringCLDeps{ 258 { 259 OriginClid: cl3.Clid(), 260 DepClids: []int64{cl1.Clid()}, 261 Trigger: cq2, 262 ConfigGroupName: "cg1", 263 }, 264 }) 265 }) 266 }) 267 268 Convey("with inflight purges", func() { 269 cl1 := newCL() 270 cl2 := newCL().Deps(cl1) 271 cl3 := newCL().Deps(cl1, cl2) 272 273 Convey("PurgingCL on the originating CL", func() { 274 cl3 = cl3.CQ(+2).SetPurgingCL() 275 triageDeps(cl1, cl2, cl3) 276 So(cl2.NeedToTrigger(), ShouldBeNil) 277 So(cl3.NeedToTrigger(), ShouldEqual, []int64{cl1.Clid(), cl2.Clid()}) 278 }) 279 Convey("PurgingCL on a parent CL", func() { 280 cl2 = cl2.CQ(+2).SetPurgingCL() 281 cl3 = cl3.CQ(+2) 282 triageDeps(cl1, cl2, cl3) 283 So(cl2.NeedToTrigger(), ShouldEqual, []int64{cl1.Clid()}) 284 So(cl3.NeedToTrigger(), ShouldEqual, []int64{cl1.Clid()}) 285 }) 286 Convey("purgeReasons on the originating CL", func() { 287 cl3 = cl3.CQ(+2).SetPurgeReasons() 288 triageDeps(cl1, cl2, cl3) 289 So(cl2.NeedToTrigger(), ShouldBeNil) 290 So(cl3.NeedToTrigger(), ShouldEqual, []int64{cl1.Clid(), cl2.Clid()}) 291 }) 292 Convey("purgeReasons on a parent CL", func() { 293 cl2 = cl2.CQ(+2).SetPurgeReasons() 294 cl3 = cl3.CQ(+2) 295 triageDeps(cl1, cl2, cl3) 296 So(cl2.NeedToTrigger(), ShouldEqual, []int64{cl1.Clid()}) 297 So(cl3.NeedToTrigger(), ShouldEqual, []int64{cl1.Clid()}) 298 }) 299 So(stageTriggerCLDeps(ctx, cls, pm), ShouldHaveLength, 0) 300 }) 301 302 Convey("with inflight TriggeringCLDeps", func() { 303 cl1 := newCL() 304 cl2 := newCL().Deps(cl1) 305 cl3 := newCL().Deps(cl1, cl2) 306 307 Convey("TriggeringCLDeps on the originating CL", func() { 308 cl3 = cl3.CQ(+2).SetTriggeringCLDeps() 309 triageDeps(cl1, cl2, cl3) 310 So(cl2.NeedToTrigger(), ShouldBeNil) 311 So(cl3.NeedToTrigger(), ShouldEqual, []int64{cl1.Clid(), cl2.Clid()}) 312 }) 313 Convey("TriggeringCLDeps on a parent CL", func() { 314 cl2 = cl2.CQ(+2).SetTriggeringCLDeps() 315 cl3 = cl3.CQ(+2) 316 triageDeps(cl1, cl2, cl3) 317 So(cl2.NeedToTrigger(), ShouldEqual, []int64{cl1.Clid()}) 318 So(cl3.NeedToTrigger(), ShouldEqual, []int64{cl1.Clid()}) 319 }) 320 So(stageTriggerCLDeps(ctx, cls, pm), ShouldHaveLength, 0) 321 }) 322 323 Convey("with incomplete run", func() { 324 cl1 := newCL() 325 cl2 := newCL().Deps(cl1) 326 cl3 := newCL().Deps(cl1, cl2) 327 cl4 := newCL().Deps(cl1, cl2, cl3) 328 329 Convey("incomplete run with the same CQ vote in all the CLs", func() { 330 cl1 = cl1.CQ(+2).SetIncompleteRun(run.FullRun) 331 cl2 = cl2.CQ(+2).SetIncompleteRun(run.FullRun) 332 cl3 = cl3.CQ(+2).SetIncompleteRun(run.FullRun) 333 334 triageDeps(cl1, cl2, cl3, cl4) 335 So(cl1.NeedToTrigger(), ShouldBeNil) 336 So(cl2.NeedToTrigger(), ShouldBeNil) 337 So(cl3.NeedToTrigger(), ShouldBeNil) 338 So(cl4.NeedToTrigger(), ShouldBeNil) 339 So(stageTriggerCLDeps(ctx, cls, pm), ShouldHaveLength, 0) 340 }) 341 342 Convey("incomplete run with different CQVotes in deps", func() { 343 cl1 = cl1.CQ(+0).SetIncompleteRun(run.NewPatchsetRun) 344 cl2 = cl2.CQ(+1).SetIncompleteRun(run.DryRun) 345 cl3 = cl3.CQ(+2).SetIncompleteRun(run.FullRun) 346 347 triageDeps(cl1, cl2, cl3, cl4) 348 So(cl1.NeedToTrigger(), ShouldBeNil) 349 So(cl2.NeedToTrigger(), ShouldBeNil) 350 So(cl3.NeedToTrigger(), ShouldEqual, []int64{cl1.pcl.GetClid(), cl2.pcl.GetClid()}) 351 So(cl4.NeedToTrigger(), ShouldBeNil) 352 So(stageTriggerCLDeps(ctx, cls, pm), ShouldHaveLength, 0) 353 }) 354 355 Convey("incomplete run on parent CLs", func() { 356 // This happen, where a child CL receives CQ+2, while its 357 // parents are running. 358 cl1 = cl1.CQ(+2).SetIncompleteRun(run.FullRun) 359 cl2 = cl2.CQ(+2).SetIncompleteRun(run.FullRun) 360 cl3 = cl3.CQ(+2) 361 cl4 = cl3.CQ(0) 362 363 triageDeps(cl1, cl2, cl3, cl4) 364 So(cl1.NeedToTrigger(), ShouldBeNil) 365 So(cl2.NeedToTrigger(), ShouldBeNil) 366 So(cl3.NeedToTrigger(), ShouldBeNil) 367 So(cl4.NeedToTrigger(), ShouldBeNil) 368 So(stageTriggerCLDeps(ctx, cls, pm), ShouldHaveLength, 0) 369 }) 370 371 Convey("MCE over MCE", func() { 372 // Similar to "incomplete run on parent CLs", but with another 373 // CL between. 374 cl1 = cl1.CQ(+2).SetIncompleteRun(run.FullRun) 375 cl2 = cl2.CQ(+2).SetIncompleteRun(run.FullRun) 376 cl3 = cl3.CQ(0) 377 cl4 = cl4.CQ(+2) 378 379 triageDeps(cl1, cl2, cl3, cl4) 380 So(cl1.NeedToTrigger(), ShouldBeNil) 381 So(cl2.NeedToTrigger(), ShouldBeNil) 382 So(cl3.NeedToTrigger(), ShouldBeNil) 383 So(cl4.NeedToTrigger(), ShouldEqual, []int64{cl3.Clid()}) 384 So(stageTriggerCLDeps(ctx, cls, pm), ShouldResembleProto, []*prjpb.TriggeringCLDeps{ 385 { 386 OriginClid: cl4.Clid(), 387 DepClids: []int64{cl3.Clid()}, 388 Trigger: cq2, 389 ConfigGroupName: "cg1", 390 }, 391 }) 392 }) 393 394 Convey("MCE over MCE with a mix of incomplete and complete runs", func() { 395 // Similar to "incomplete run on parent CLs", but with another 396 // CL between. 397 cl1 = cl1.CQ(+2) 398 cl2 = cl2.CQ(+2).SetIncompleteRun(run.FullRun) 399 cl3 = cl3.CQ(0) 400 cl4 = cl4.CQ(+2) 401 402 triageDeps(cl1, cl2, cl3, cl4) 403 So(cl1.NeedToTrigger(), ShouldBeNil) 404 So(cl2.NeedToTrigger(), ShouldBeNil) 405 So(cl3.NeedToTrigger(), ShouldBeNil) 406 So(cl4.NeedToTrigger(), ShouldEqual, []int64{cl3.Clid()}) 407 So(stageTriggerCLDeps(ctx, cls, pm), ShouldResembleProto, []*prjpb.TriggeringCLDeps{ 408 { 409 OriginClid: cl4.Clid(), 410 DepClids: []int64{cl3.Clid()}, 411 Trigger: cq2, 412 ConfigGroupName: "cg1", 413 }, 414 }) 415 }) 416 }) 417 }) 418 }