go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/prjmanager/state/categorize_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 state 16 17 import ( 18 "strings" 19 "testing" 20 "time" 21 22 "google.golang.org/protobuf/encoding/prototext" 23 "google.golang.org/protobuf/types/known/timestamppb" 24 25 "go.chromium.org/luci/common/logging" 26 gerritpb "go.chromium.org/luci/common/proto/gerrit" 27 "go.chromium.org/luci/gae/service/datastore" 28 29 cfgpb "go.chromium.org/luci/cv/api/config/v2" 30 "go.chromium.org/luci/cv/internal/changelist" 31 "go.chromium.org/luci/cv/internal/common" 32 "go.chromium.org/luci/cv/internal/configs/prjcfg/prjcfgtest" 33 gf "go.chromium.org/luci/cv/internal/gerrit/gerritfake" 34 "go.chromium.org/luci/cv/internal/gerrit/gobmap/gobmaptest" 35 "go.chromium.org/luci/cv/internal/gerrit/trigger" 36 "go.chromium.org/luci/cv/internal/prjmanager/prjpb" 37 "go.chromium.org/luci/cv/internal/run" 38 39 . "github.com/smartystreets/goconvey/convey" 40 . "go.chromium.org/luci/common/testing/assertions" 41 ) 42 43 func TestCategorizeAndLoadActiveIntoPCLs(t *testing.T) { 44 t.Parallel() 45 46 Convey("loadActiveIntoPCLs and categorizeCLs work", t, func() { 47 ct := ctest{ 48 lProject: "test", 49 gHost: "c-review.example.com", 50 } 51 ctx, cancel := ct.SetUp(t) 52 defer cancel() 53 54 cfg := &cfgpb.Config{} 55 So(prototext.Unmarshal([]byte(cfgText1), cfg), ShouldBeNil) 56 prjcfgtest.Create(ctx, ct.lProject, cfg) 57 meta := prjcfgtest.MustExist(ctx, ct.lProject) 58 gobmaptest.Update(ctx, ct.lProject) 59 60 // Simulate existence of "test-b" project watching the same Gerrit host but 61 // diff repo. 62 const lProjectB = "test-b" 63 cfgTextB := strings.ReplaceAll(cfgText1, "repo/a", "repo/b") 64 cfgB := &cfgpb.Config{} 65 So(prototext.Unmarshal([]byte(cfgTextB), cfgB), ShouldBeNil) 66 prjcfgtest.Create(ctx, lProjectB, cfgB) 67 gobmaptest.Update(ctx, lProjectB) 68 69 cis := make(map[int]*gerritpb.ChangeInfo, 20) 70 makeCI := func(i int, project string, cq int, extra ...gf.CIModifier) { 71 mods := []gf.CIModifier{ 72 gf.Ref("refs/heads/main"), 73 gf.Project(project), 74 gf.Updated(ct.Clock.Now()), 75 } 76 if cq > 0 { 77 mods = append(mods, gf.CQ(cq, ct.Clock.Now(), gf.U("user-1"))) 78 } 79 mods = append(mods, extra...) 80 cis[i] = gf.CI(i, mods...) 81 ct.GFake.CreateChange(&gf.Change{Host: ct.gHost, ACLs: gf.ACLPublic(), Info: cis[i]}) 82 } 83 makeStack := func(ids []int, project string, cq int) { 84 for i, child := range ids { 85 makeCI(child, project, cq) 86 for _, parent := range ids[:i] { 87 ct.GFake.SetDependsOn(ct.gHost, cis[child], cis[parent]) 88 } 89 } 90 } 91 // Simulate the following CLs state in Gerrit: 92 // In this project: 93 // CQ+1 94 // 1 <- 2 form a stack (2 depends on 1) 95 // 3 depends on 2 via Cq-Depend. 96 // CQ+2 97 // 4 standalone 98 // 5 <- 6 form a stack (6 depends on 5) 99 // 7 <- 8 <- 9 form a stack (9 depends on 7,8) 100 // 13 CQ-Depend on 11 (diff project) and 12 (not existing). 101 // In another project: 102 // CQ+1 103 // 10 <- 11 form a stack (11 depends on 10) 104 makeStack([]int{1, 2}, "repo/a", +1) 105 makeCI(3, "repo/a", +1, gf.Desc("T\n\nCq-Depend: 2")) 106 makeStack([]int{7, 8, 9}, "repo/a", +2) 107 makeStack([]int{5, 6}, "repo/a", +2) 108 makeCI(4, "repo/a", +2) 109 makeCI(13, "repo/a", +2, gf.Desc("T\n\nCq-Depend: 11,12")) 110 makeStack([]int{10, 11}, "repo/b", +1) 111 112 // Import into DS all CLs in their respective LUCI projects. 113 // Do this in-order such that they match auto-assigned CLIDs by fake 114 // Datastore as this helps test readability. Note that importing CL 13 would 115 // create CL entity for dep #12 before creating CL 13th own entity. 116 cls := make(map[int]*changelist.CL, 20) 117 for i := 1; i < 14; i++ { 118 if i == 12 { 119 continue // skipped. will be done after 13 120 } 121 pr := ct.lProject 122 if i == 10 || i == 11 { 123 pr = lProjectB 124 } 125 cls[i] = ct.runCLUpdaterAs(ctx, int64(i), pr) 126 } 127 // This will get 404 from Gerrit. 128 cls[12] = ct.runCLUpdater(ctx, 12) 129 130 for i := 1; i < 14; i++ { 131 // On in-memory DS fake, auto-generated IDs are 1,2, ..., 132 // so by construction the following would hold: 133 // cls[i].ID == i 134 // On real DS, emit mapping to assist in test debug. 135 if cls[i].ID != common.CLID(i) { 136 logging.Debugf(ctx, "cls[%d].ID = %d", i, cls[i].ID) 137 } 138 } 139 140 run4 := &run.Run{ 141 ID: common.RunID(ct.lProject + "/1-a"), 142 CLs: common.CLIDs{cls[4].ID}, 143 } 144 run56 := &run.Run{ 145 ID: common.RunID(ct.lProject + "/56-bb"), 146 CLs: common.CLIDs{cls[5].ID, cls[6].ID}, 147 } 148 run789 := &run.Run{ 149 ID: common.RunID(ct.lProject + "/789-ccc"), 150 CLs: common.CLIDs{cls[9].ID, cls[7].ID, cls[8].ID}, 151 } 152 So(datastore.Put(ctx, run4, run56, run789), ShouldBeNil) 153 154 state := &State{PB: &prjpb.PState{ 155 LuciProject: ct.lProject, 156 Status: prjpb.Status_STARTED, 157 ConfigHash: meta.Hash(), 158 ConfigGroupNames: []string{"g0", "g1"}, 159 RepartitionRequired: true, 160 }} 161 162 Convey("just categorization", func() { 163 state.PB.Pcls = sortPCLs([]*prjpb.PCL{ 164 defaultPCL(cls[5]), 165 defaultPCL(cls[6]), 166 defaultPCL(cls[7]), 167 defaultPCL(cls[8]), 168 defaultPCL(cls[9]), 169 {Clid: int64(cls[12].ID), Eversion: 1, Status: prjpb.PCL_UNKNOWN}, 170 }) 171 state.PB.Components = []*prjpb.Component{ 172 { 173 Clids: i64sorted(cls[5].ID, cls[6].ID), 174 Pruns: []*prjpb.PRun{prjpb.MakePRun(run56)}, 175 }, 176 // Simulate 9 previously not depending on 7, 8. 177 {Clids: i64sorted(cls[7].ID, cls[8].ID)}, 178 {Clids: i64s(cls[9].ID)}, 179 } 180 // 789 doesn't match any 1 component, even though 7,8,9 CLs are in PCLs. 181 state.PB.CreatedPruns = []*prjpb.PRun{prjpb.MakePRun(run789)} 182 pbBefore := backupPB(state) 183 184 cat := state.categorizeCLs(ctx) 185 So(state.loadActiveIntoPCLs(ctx, cat), ShouldBeNil) 186 So(cat, ShouldResemble, &categorizedCLs{ 187 active: mkClidsSet(cls, 5, 6, 7, 8, 9), 188 deps: common.CLIDsSet{}, 189 unused: mkClidsSet(cls, 12), 190 unloaded: common.CLIDsSet{}, 191 }) 192 So(state.PB, ShouldResembleProto, pbBefore) 193 }) 194 195 Convey("loads unloaded dependencies and active CLs without recursion", func() { 196 state.PB.Pcls = []*prjpb.PCL{ 197 defaultPCL(cls[3]), // depends on 2, which in turns depends on 1. 198 } 199 state.PB.CreatedPruns = []*prjpb.PRun{prjpb.MakePRun(run56)} 200 pb := backupPB(state) 201 202 cat := state.categorizeCLs(ctx) 203 So(cat, ShouldResemble, &categorizedCLs{ 204 active: mkClidsSet(cls, 3, 5, 6), 205 deps: mkClidsSet(cls, 2), 206 unused: common.CLIDsSet{}, 207 unloaded: mkClidsSet(cls, 2, 5, 6), 208 }) 209 So(state.loadActiveIntoPCLs(ctx, cat), ShouldBeNil) 210 So(cat, ShouldResemble, &categorizedCLs{ 211 active: mkClidsSet(cls, 3, 2, 5, 6), 212 deps: mkClidsSet(cls, 1), 213 unused: common.CLIDsSet{}, 214 unloaded: mkClidsSet(cls, 1), 215 }) 216 pb.Pcls = sortPCLs([]*prjpb.PCL{ 217 defaultPCL(cls[2]), 218 defaultPCL(cls[3]), 219 defaultPCL(cls[5]), 220 defaultPCL(cls[6]), 221 }) 222 So(state.PB, ShouldResembleProto, pb) 223 }) 224 225 Convey("loads incomplete Run with unloaded deps", func() { 226 // This case shouldn't normally happen in practice. This case simulates a 227 // runStale created a while ago of just (11, 13), presumably when current 228 // project had CL #11 in scope. 229 // Now, 11 and 13 depend on 10 and 12, respectively, and 10 and 11 are no 230 // longer watched by current project. 231 runStale := &run.Run{ 232 ID: common.RunID(ct.lProject + "/111-s"), 233 CLs: common.CLIDs{cls[13].ID, cls[11].ID}, 234 } 235 So(datastore.Put(ctx, runStale), ShouldBeNil) 236 state.PB.CreatedPruns = []*prjpb.PRun{prjpb.MakePRun(runStale)} 237 pb := backupPB(state) 238 239 cat := state.categorizeCLs(ctx) 240 So(cat, ShouldResemble, &categorizedCLs{ 241 active: mkClidsSet(cls, 11, 13), 242 deps: common.CLIDsSet{}, 243 unused: common.CLIDsSet{}, 244 unloaded: mkClidsSet(cls, 11, 13), 245 }) 246 So(state.loadActiveIntoPCLs(ctx, cat), ShouldBeNil) 247 So(cat, ShouldResemble, &categorizedCLs{ 248 active: mkClidsSet(cls, 11, 13), 249 // 10 isn't in deps because this project has no visibility into CL 11. 250 deps: mkClidsSet(cls, 12), 251 unused: common.CLIDsSet{}, 252 unloaded: mkClidsSet(cls, 12), 253 }) 254 pb.Pcls = sortPCLs([]*prjpb.PCL{ 255 defaultPCL(cls[13]), 256 { 257 Clid: int64(cls[11].ID), 258 Eversion: cls[11].EVersion, 259 Status: prjpb.PCL_UNWATCHED, 260 Deps: nil, // not visible to this project 261 }, 262 }) 263 So(state.PB, ShouldResembleProto, pb) 264 }) 265 266 Convey("loads incomplete Run with non-existent CLs", func() { 267 // This case shouldn't happen in practice, but it can't be ruled out. 268 // In order to incorporate just added .CreatedRun into State, 269 // Run's CLs must have PCL entries. 270 runStale := &run.Run{ 271 ID: common.RunID(ct.lProject + "/404-s"), 272 CLs: common.CLIDs{cls[4].ID, 404}, 273 } 274 So(datastore.Put(ctx, runStale), ShouldBeNil) 275 state.PB.CreatedPruns = []*prjpb.PRun{prjpb.MakePRun(runStale)} 276 pb := backupPB(state) 277 278 cat := state.categorizeCLs(ctx) 279 So(cat, ShouldResemble, &categorizedCLs{ 280 active: common.CLIDsSet{cls[4].ID: struct{}{}, 404: struct{}{}}, 281 deps: common.CLIDsSet{}, 282 unused: common.CLIDsSet{}, 283 unloaded: common.CLIDsSet{cls[4].ID: struct{}{}, 404: struct{}{}}, 284 }) 285 So(state.loadActiveIntoPCLs(ctx, cat), ShouldBeNil) 286 So(cat, ShouldResemble, &categorizedCLs{ 287 active: common.CLIDsSet{cls[4].ID: struct{}{}, 404: struct{}{}}, 288 deps: common.CLIDsSet{}, 289 unused: common.CLIDsSet{}, 290 unloaded: common.CLIDsSet{}, 291 }) 292 pb.Pcls = sortPCLs([]*prjpb.PCL{ 293 defaultPCL(cls[4]), 294 { 295 Clid: 404, 296 Eversion: 0, 297 Status: prjpb.PCL_DELETED, 298 }, 299 }) 300 So(state.PB, ShouldResembleProto, pb) 301 }) 302 303 Convey("identifies submitted PCLs as unused if possible", func() { 304 // Modify 1<-2 stack to have #1 submitted. 305 ct.Clock.Add(time.Minute) 306 cls[1] = ct.submitCL(ctx, 1) 307 cis[1] = cls[1].Snapshot.GetGerrit().GetInfo() 308 So(cis[1].Status, ShouldEqual, gerritpb.ChangeStatus_MERGED) 309 310 state.PB.Pcls = []*prjpb.PCL{ 311 { 312 Clid: int64(cls[1].ID), 313 Eversion: cls[1].EVersion, 314 Status: prjpb.PCL_OK, 315 Submitted: true, 316 }, 317 } 318 Convey("standalone submitted CL without a Run is unused", func() { 319 cat := state.categorizeCLs(ctx) 320 exp := &categorizedCLs{ 321 active: common.CLIDsSet{}, 322 deps: common.CLIDsSet{}, 323 unloaded: common.CLIDsSet{}, 324 unused: mkClidsSet(cls, 1), 325 } 326 So(cat, ShouldResemble, exp) 327 So(state.loadActiveIntoPCLs(ctx, cat), ShouldBeNil) 328 So(cat, ShouldResemble, exp) 329 }) 330 331 Convey("standalone submitted CL with a Run is active", func() { 332 state.PB.Components = []*prjpb.Component{ 333 { 334 Clids: i64s(cls[1].ID), 335 Pruns: []*prjpb.PRun{ 336 {Clids: i64s(cls[1].ID), Id: "run1"}, 337 }, 338 }, 339 } 340 cat := state.categorizeCLs(ctx) 341 exp := &categorizedCLs{ 342 active: mkClidsSet(cls, 1), 343 deps: common.CLIDsSet{}, 344 unloaded: common.CLIDsSet{}, 345 unused: common.CLIDsSet{}, 346 } 347 So(cat, ShouldResemble, exp) 348 So(state.loadActiveIntoPCLs(ctx, cat), ShouldBeNil) 349 So(cat, ShouldResemble, exp) 350 }) 351 352 Convey("submitted dependent is neither active nor unused, but a dep", func() { 353 triggers := trigger.Find(&trigger.FindInput{ChangeInfo: cis[2], ConfigGroup: cfg.ConfigGroups[0]}) 354 So(triggers.GetCqVoteTrigger(), ShouldNotBeNil) 355 state.PB.Pcls = sortPCLs(append(state.PB.Pcls, 356 &prjpb.PCL{ 357 Clid: int64(cls[2].ID), 358 Eversion: cls[2].EVersion, 359 Status: prjpb.PCL_OK, 360 Triggers: triggers, 361 ConfigGroupIndexes: []int32{0}, 362 Deps: cls[2].Snapshot.GetDeps(), 363 }, 364 )) 365 cat := state.categorizeCLs(ctx) 366 exp := &categorizedCLs{ 367 active: mkClidsSet(cls, 2), 368 deps: mkClidsSet(cls, 1), 369 unloaded: common.CLIDsSet{}, 370 unused: common.CLIDsSet{}, 371 } 372 So(cat, ShouldResemble, exp) 373 So(state.loadActiveIntoPCLs(ctx, cat), ShouldBeNil) 374 So(cat, ShouldResemble, exp) 375 }) 376 }) 377 378 Convey("prunes PCLs with expired triggers", func() { 379 makePCL := func(i int, t time.Time, deps ...*changelist.Dep) *prjpb.PCL { 380 return &prjpb.PCL{ 381 Clid: int64(cls[i].ID), 382 Eversion: 1, 383 Status: prjpb.PCL_OK, 384 Deps: deps, 385 Triggers: &run.Triggers{CqVoteTrigger: &run.Trigger{ 386 GerritAccountId: 1, 387 Mode: string(run.DryRun), 388 Time: timestamppb.New(t), 389 }}, 390 } 391 } 392 state.PB.Pcls = []*prjpb.PCL{ 393 makePCL(1, ct.Clock.Now().Add(-time.Minute), &changelist.Dep{Clid: int64(cls[4].ID)}), 394 makePCL(2, ct.Clock.Now().Add(-common.MaxTriggerAge+time.Second)), 395 makePCL(3, ct.Clock.Now().Add(-common.MaxTriggerAge)), 396 } 397 cat := state.categorizeCLs(ctx) 398 So(cat, ShouldResemble, &categorizedCLs{ 399 active: mkClidsSet(cls, 1, 2), 400 deps: mkClidsSet(cls, 4), 401 unloaded: mkClidsSet(cls, 4), 402 unused: mkClidsSet(cls, 3), 403 }) 404 405 Convey("and doesn't promote unloaded to active if trigger has expired", func() { 406 // Keep CQ+2 vote, but make it timestamp really old. 407 infoRef := cls[4].Snapshot.GetGerrit().GetInfo() 408 infoRef.Labels = nil 409 gf.CQ(2, ct.Clock.Now().Add(-common.MaxTriggerAge), gf.U("user-1"))(infoRef) 410 So(datastore.Put(ctx, cls[4]), ShouldBeNil) 411 412 So(state.loadActiveIntoPCLs(ctx, cat), ShouldBeNil) 413 So(cat, ShouldResemble, &categorizedCLs{ 414 active: mkClidsSet(cls, 1, 2), 415 deps: mkClidsSet(cls, 4), 416 unloaded: common.CLIDsSet{}, 417 unused: mkClidsSet(cls, 3), 418 }) 419 }) 420 }) 421 422 Convey("noop", func() { 423 cat := state.categorizeCLs(ctx) 424 So(state.loadActiveIntoPCLs(ctx, cat), ShouldBeNil) 425 So(cat, ShouldResemble, &categorizedCLs{ 426 active: common.CLIDsSet{}, 427 deps: common.CLIDsSet{}, 428 unused: common.CLIDsSet{}, 429 unloaded: common.CLIDsSet{}, 430 }) 431 }) 432 }) 433 }