go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/prjmanager/state/state_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 "context" 19 "fmt" 20 "sort" 21 "strings" 22 "testing" 23 "time" 24 25 "google.golang.org/protobuf/encoding/prototext" 26 "google.golang.org/protobuf/proto" 27 "google.golang.org/protobuf/types/known/timestamppb" 28 29 "go.chromium.org/luci/common/clock/testclock" 30 gerritpb "go.chromium.org/luci/common/proto/gerrit" 31 "go.chromium.org/luci/gae/service/datastore" 32 33 cfgpb "go.chromium.org/luci/cv/api/config/v2" 34 "go.chromium.org/luci/cv/internal/changelist" 35 "go.chromium.org/luci/cv/internal/common" 36 "go.chromium.org/luci/cv/internal/configs/prjcfg" 37 "go.chromium.org/luci/cv/internal/configs/prjcfg/prjcfgtest" 38 "go.chromium.org/luci/cv/internal/cvtesting" 39 "go.chromium.org/luci/cv/internal/gerrit/cfgmatcher" 40 gf "go.chromium.org/luci/cv/internal/gerrit/gerritfake" 41 "go.chromium.org/luci/cv/internal/gerrit/gobmap/gobmaptest" 42 "go.chromium.org/luci/cv/internal/gerrit/poller" 43 "go.chromium.org/luci/cv/internal/gerrit/trigger" 44 gerritupdater "go.chromium.org/luci/cv/internal/gerrit/updater" 45 "go.chromium.org/luci/cv/internal/prjmanager" 46 "go.chromium.org/luci/cv/internal/prjmanager/clpurger" 47 "go.chromium.org/luci/cv/internal/prjmanager/prjpb" 48 "go.chromium.org/luci/cv/internal/run" 49 "go.chromium.org/luci/cv/internal/tryjob" 50 51 . "github.com/smartystreets/goconvey/convey" 52 . "go.chromium.org/luci/common/testing/assertions" 53 ) 54 55 type ctest struct { 56 cvtesting.Test 57 58 lProject string 59 gHost string 60 pm *prjmanager.Notifier 61 clUpdater *changelist.Updater 62 } 63 64 func (ct *ctest) SetUp(testingT *testing.T) (context.Context, func()) { 65 ctx, cancel := ct.Test.SetUp(testingT) 66 ct.pm = prjmanager.NewNotifier(ct.TQDispatcher) 67 ct.clUpdater = changelist.NewUpdater(ct.TQDispatcher, changelist.NewMutator(ct.TQDispatcher, ct.pm, nil, tryjob.NewNotifier(ct.TQDispatcher))) 68 gerritupdater.RegisterUpdater(ct.clUpdater, ct.GFactory()) 69 return ctx, cancel 70 } 71 72 func (ct ctest) runCLUpdater(ctx context.Context, change int64) *changelist.CL { 73 return ct.runCLUpdaterAs(ctx, change, ct.lProject) 74 } 75 76 func (ct ctest) runCLUpdaterAs(ctx context.Context, change int64, lProject string) *changelist.CL { 77 So(ct.clUpdater.TestingForceUpdate(ctx, &changelist.UpdateCLTask{ 78 LuciProject: lProject, 79 ExternalId: string(changelist.MustGobID(ct.gHost, change)), 80 Requester: changelist.UpdateCLTask_RUN_POKE, 81 }), ShouldBeNil) 82 eid, err := changelist.GobID(ct.gHost, change) 83 So(err, ShouldBeNil) 84 cl, err := eid.Load(ctx) 85 So(err, ShouldBeNil) 86 So(cl, ShouldNotBeNil) 87 return cl 88 } 89 90 func (ct ctest) submitCL(ctx context.Context, change int64) *changelist.CL { 91 ct.GFake.MutateChange(ct.gHost, int(change), func(c *gf.Change) { 92 gf.Status(gerritpb.ChangeStatus_MERGED)(c.Info) 93 gf.Updated(ct.Clock.Now())(c.Info) 94 }) 95 cl := ct.runCLUpdater(ctx, change) 96 97 // If this fails, you forgot to change fake time. 98 So(cl.Snapshot.GetGerrit().GetInfo().GetStatus(), ShouldEqual, gerritpb.ChangeStatus_MERGED) 99 return cl 100 } 101 102 const cfgText1 = ` 103 config_groups { 104 name: "g0" 105 gerrit { 106 url: "https://c-review.example.com" # Must match gHost. 107 projects { 108 name: "repo/a" 109 ref_regexp: "refs/heads/main" 110 } 111 } 112 } 113 config_groups { 114 name: "g1" 115 fallback: YES 116 gerrit { 117 url: "https://c-review.example.com" # Must match gHost. 118 projects { 119 name: "repo/a" 120 ref_regexp: "refs/heads/.+" 121 } 122 } 123 } 124 ` 125 126 func updateConfigToNoFallabck(ctx context.Context, ct *ctest) prjcfg.Meta { 127 cfgText2 := strings.ReplaceAll(cfgText1, "fallback: YES", "fallback: NO") 128 cfg2 := &cfgpb.Config{} 129 So(prototext.Unmarshal([]byte(cfgText2), cfg2), ShouldBeNil) 130 prjcfgtest.Update(ctx, ct.lProject, cfg2) 131 gobmaptest.Update(ctx, ct.lProject) 132 return prjcfgtest.MustExist(ctx, ct.lProject) 133 } 134 135 func updateConfigRenameG1toG11(ctx context.Context, ct *ctest) prjcfg.Meta { 136 cfgText2 := strings.ReplaceAll(cfgText1, `"g1"`, `"g11"`) 137 cfg2 := &cfgpb.Config{} 138 So(prototext.Unmarshal([]byte(cfgText2), cfg2), ShouldBeNil) 139 prjcfgtest.Update(ctx, ct.lProject, cfg2) 140 gobmaptest.Update(ctx, ct.lProject) 141 return prjcfgtest.MustExist(ctx, ct.lProject) 142 } 143 144 func TestUpdateConfig(t *testing.T) { 145 t.Parallel() 146 147 Convey("updateConfig works", t, func() { 148 ct := ctest{ 149 lProject: "test", 150 gHost: "c-review.example.com", 151 Test: cvtesting.Test{}, 152 } 153 ctx, cancel := ct.SetUp(t) 154 defer cancel() 155 156 cfg1 := &cfgpb.Config{} 157 So(prototext.Unmarshal([]byte(cfgText1), cfg1), ShouldBeNil) 158 159 prjcfgtest.Create(ctx, ct.lProject, cfg1) 160 meta := prjcfgtest.MustExist(ctx, ct.lProject) 161 gobmaptest.Update(ctx, ct.lProject) 162 163 clPoller := poller.New(ct.TQDispatcher, nil, nil, nil) 164 h := Handler{CLPoller: clPoller} 165 166 Convey("initializes newly started project", func() { 167 // Newly started project doesn't have any CLs, yet, regardless of what CL 168 // snapshots are stored in Datastore. 169 s0 := &State{PB: &prjpb.PState{LuciProject: ct.lProject}} 170 pb0 := backupPB(s0) 171 s1, sideEffect, err := h.UpdateConfig(ctx, s0) 172 So(err, ShouldBeNil) 173 So(s0.PB, ShouldResembleProto, pb0) // s0 must not change. 174 So(sideEffect, ShouldResemble, &UpdateIncompleteRunsConfig{ 175 Hash: meta.Hash(), 176 EVersion: meta.EVersion, 177 RunIDs: nil, 178 }) 179 So(s1.PB, ShouldResembleProto, &prjpb.PState{ 180 LuciProject: ct.lProject, 181 Status: prjpb.Status_STARTED, 182 ConfigHash: meta.Hash(), 183 ConfigGroupNames: []string{"g0", "g1"}, 184 Components: nil, 185 Pcls: nil, 186 RepartitionRequired: false, 187 }) 188 So(s1.LogReasons, ShouldResemble, []prjpb.LogReason{prjpb.LogReason_CONFIG_CHANGED, prjpb.LogReason_STATUS_CHANGED}) 189 }) 190 191 // Add 3 CLs: 101 standalone and 202<-203 as a stack. 192 triggerTS := timestamppb.New(ct.Clock.Now()) 193 ci101 := gf.CI( 194 101, gf.PS(1), gf.Ref("refs/heads/main"), gf.Project("repo/a"), 195 gf.CQ(+2, ct.Clock.Now(), gf.U("user-1")), gf.Updated(ct.Clock.Now()), 196 ) 197 ci202 := gf.CI( 198 202, gf.PS(3), gf.Ref("refs/heads/other"), gf.Project("repo/a"), gf.AllRevs(), 199 gf.CQ(+1, ct.Clock.Now(), gf.U("user-2")), gf.Updated(ct.Clock.Now()), 200 ) 201 ci203 := gf.CI( 202 203, gf.PS(3), gf.Ref("refs/heads/other"), gf.Project("repo/a"), gf.AllRevs(), 203 gf.CQ(+1, ct.Clock.Now(), gf.U("user-2")), gf.Updated(ct.Clock.Now()), 204 ) 205 ct.GFake.CreateChange(&gf.Change{Host: ct.gHost, ACLs: gf.ACLPublic(), Info: ci101}) 206 ct.GFake.CreateChange(&gf.Change{Host: ct.gHost, ACLs: gf.ACLPublic(), Info: ci202}) 207 ct.GFake.CreateChange(&gf.Change{Host: ct.gHost, ACLs: gf.ACLPublic(), Info: ci203}) 208 ct.GFake.SetDependsOn(ct.gHost, "203_3" /* child */, "202_2" /*parent*/) 209 cl101 := ct.runCLUpdater(ctx, 101) 210 cl202 := ct.runCLUpdater(ctx, 202) 211 cl203 := ct.runCLUpdater(ctx, 203) 212 213 s1 := &State{ 214 PB: &prjpb.PState{ 215 LuciProject: ct.lProject, 216 Status: prjpb.Status_STARTED, 217 ConfigHash: meta.Hash(), 218 ConfigGroupNames: []string{"g0", "g1"}, 219 Pcls: []*prjpb.PCL{ 220 { 221 Clid: int64(cl101.ID), 222 Eversion: 1, 223 ConfigGroupIndexes: []int32{0}, // g0 224 Status: prjpb.PCL_OK, 225 Triggers: &run.Triggers{CqVoteTrigger: &run.Trigger{ 226 Mode: string(run.FullRun), 227 Time: triggerTS, 228 Email: gf.U("user-1").GetEmail(), 229 GerritAccountId: gf.U("user-1").GetAccountId(), 230 }}, 231 }, 232 { 233 Clid: int64(cl202.ID), 234 Eversion: 1, 235 ConfigGroupIndexes: []int32{1}, // g1 236 Status: prjpb.PCL_OK, 237 Triggers: &run.Triggers{CqVoteTrigger: &run.Trigger{ 238 Mode: string(run.DryRun), 239 Time: triggerTS, 240 Email: gf.U("user-2").GetEmail(), 241 GerritAccountId: gf.U("user-2").GetAccountId(), 242 }}, 243 }, 244 { 245 Clid: int64(cl203.ID), 246 Eversion: 1, 247 ConfigGroupIndexes: []int32{1}, // g1 248 Status: prjpb.PCL_OK, 249 Triggers: &run.Triggers{CqVoteTrigger: &run.Trigger{ 250 Mode: string(run.DryRun), 251 Time: triggerTS, 252 Email: gf.U("user-2").GetEmail(), 253 GerritAccountId: gf.U("user-2").GetAccountId(), 254 }}, 255 Deps: []*changelist.Dep{{Clid: int64(cl202.ID), Kind: changelist.DepKind_HARD}}, 256 }, 257 }, 258 Components: []*prjpb.Component{ 259 { 260 Clids: []int64{int64(cl101.ID)}, 261 Pruns: []*prjpb.PRun{ 262 { 263 Id: ct.lProject + "/" + "1111-v1-beef", 264 Clids: []int64{int64(cl101.ID)}, 265 }, 266 }, 267 }, 268 { 269 Clids: []int64{404}, 270 }, 271 }, 272 }, 273 } 274 pb1 := backupPB(s1) 275 276 Convey("noop update is quick", func() { 277 s2, sideEffect, err := h.UpdateConfig(ctx, s1) 278 So(err, ShouldBeNil) 279 So(s2, ShouldEqual, s1) // pointer comparison only. 280 So(sideEffect, ShouldBeNil) 281 }) 282 283 Convey("existing project", func() { 284 Convey("updated without touching components", func() { 285 meta2 := updateConfigToNoFallabck(ctx, &ct) 286 s2, sideEffect, err := h.UpdateConfig(ctx, s1) 287 So(err, ShouldBeNil) 288 So(s1.PB, ShouldResembleProto, pb1) // s1 must not change. 289 So(sideEffect, ShouldResemble, &UpdateIncompleteRunsConfig{ 290 Hash: meta2.Hash(), 291 EVersion: meta2.EVersion, 292 RunIDs: common.MakeRunIDs(ct.lProject + "/" + "1111-v1-beef"), 293 }) 294 So(s2.PB, ShouldResembleProto, &prjpb.PState{ 295 LuciProject: ct.lProject, 296 Status: prjpb.Status_STARTED, 297 ConfigHash: meta2.Hash(), // changed 298 ConfigGroupNames: []string{"g0", "g1"}, 299 Pcls: []*prjpb.PCL{ 300 { 301 Clid: int64(cl101.ID), 302 Eversion: 1, 303 ConfigGroupIndexes: []int32{0, 1}, // +g1, because g1 is no longer "fallback: YES" 304 Status: prjpb.PCL_OK, 305 Triggers: &run.Triggers{CqVoteTrigger: &run.Trigger{ 306 Mode: string(run.FullRun), 307 Time: triggerTS, 308 Email: gf.U("user-1").GetEmail(), 309 GerritAccountId: gf.U("user-1").GetAccountId(), 310 }}, 311 }, 312 pb1.Pcls[1], // #202 didn't change. 313 pb1.Pcls[2], // #203 didn't change. 314 }, 315 Components: markForTriage(pb1.Components), 316 RepartitionRequired: true, 317 }) 318 So(s2.LogReasons, ShouldResemble, []prjpb.LogReason{prjpb.LogReason_CONFIG_CHANGED}) 319 }) 320 321 Convey("If PCLs stay same, RepartitionRequired must be false", func() { 322 meta2 := updateConfigRenameG1toG11(ctx, &ct) 323 s2, sideEffect, err := h.UpdateConfig(ctx, s1) 324 So(err, ShouldBeNil) 325 So(s1.PB, ShouldResembleProto, pb1) // s1 must not change. 326 So(sideEffect, ShouldResemble, &UpdateIncompleteRunsConfig{ 327 Hash: meta2.Hash(), 328 EVersion: meta2.EVersion, 329 RunIDs: common.MakeRunIDs(ct.lProject + "/" + "1111-v1-beef"), 330 }) 331 So(s2.PB, ShouldResembleProto, &prjpb.PState{ 332 LuciProject: ct.lProject, 333 Status: prjpb.Status_STARTED, 334 ConfigHash: meta2.Hash(), 335 ConfigGroupNames: []string{"g0", "g11"}, // g1 -> g11. 336 Pcls: pb1.GetPcls(), 337 Components: markForTriage(pb1.Components), 338 RepartitionRequired: false, 339 }) 340 }) 341 }) 342 343 Convey("disabled project updated with long ago deleted CL", func() { 344 s1.PB.Status = prjpb.Status_STOPPED 345 for _, c := range s1.PB.GetComponents() { 346 c.Pruns = nil // disabled projects don't have incomplete runs. 347 } 348 pb1 = backupPB(s1) 349 changelist.Delete(ctx, cl101.ID) 350 351 meta2 := updateConfigToNoFallabck(ctx, &ct) 352 s2, sideEffect, err := h.UpdateConfig(ctx, s1) 353 So(err, ShouldBeNil) 354 So(s1.PB, ShouldResembleProto, pb1) // s1 must not change. 355 So(sideEffect, ShouldResemble, &UpdateIncompleteRunsConfig{ 356 Hash: meta2.Hash(), 357 EVersion: meta2.EVersion, 358 // No runs to notify. 359 }) 360 So(s2.PB, ShouldResembleProto, &prjpb.PState{ 361 LuciProject: ct.lProject, 362 Status: prjpb.Status_STARTED, 363 ConfigHash: meta2.Hash(), // changed 364 ConfigGroupNames: []string{"g0", "g1"}, 365 Pcls: []*prjpb.PCL{ 366 { 367 Clid: int64(cl101.ID), 368 Eversion: 1, 369 Status: prjpb.PCL_DELETED, 370 }, 371 pb1.Pcls[1], // #202 didn't change. 372 pb1.Pcls[2], // #203 didn't change. 373 }, 374 Components: markForTriage(pb1.Components), 375 RepartitionRequired: true, 376 }) 377 So(s2.LogReasons, ShouldResemble, []prjpb.LogReason{prjpb.LogReason_CONFIG_CHANGED, prjpb.LogReason_STATUS_CHANGED}) 378 }) 379 380 Convey("disabled project waits for incomplete Runs", func() { 381 prjcfgtest.Disable(ctx, ct.lProject) 382 s2, sideEffect, err := h.UpdateConfig(ctx, s1) 383 So(err, ShouldBeNil) 384 pb := backupPB(s1) 385 pb.Status = prjpb.Status_STOPPING 386 So(s2.PB, ShouldResembleProto, pb) 387 So(sideEffect, ShouldResemble, &CancelIncompleteRuns{ 388 RunIDs: common.MakeRunIDs(ct.lProject + "/" + "1111-v1-beef"), 389 }) 390 So(s2.LogReasons, ShouldResemble, []prjpb.LogReason{prjpb.LogReason_STATUS_CHANGED}) 391 }) 392 393 Convey("disabled project stops iff there are no incomplete Runs", func() { 394 for _, c := range s1.PB.GetComponents() { 395 c.Pruns = nil 396 } 397 prjcfgtest.Disable(ctx, ct.lProject) 398 s2, sideEffect, err := h.UpdateConfig(ctx, s1) 399 So(err, ShouldBeNil) 400 So(sideEffect, ShouldBeNil) 401 pb := backupPB(s1) 402 pb.Status = prjpb.Status_STOPPED 403 So(s2.PB, ShouldResembleProto, pb) 404 So(prjpb.SortAndDedupeLogReasons(s2.LogReasons), ShouldResemble, []prjpb.LogReason{prjpb.LogReason_STATUS_CHANGED}) 405 }) 406 407 // The rest of the test coverage of UpdateConfig is achieved by testing code 408 // of makePCL. 409 410 Convey("makePCL with full snapshot works", func() { 411 var err error 412 s1.configGroups, err = meta.GetConfigGroups(ctx) 413 So(err, ShouldBeNil) 414 s1.cfgMatcher = cfgmatcher.LoadMatcherFromConfigGroups(ctx, s1.configGroups, &meta) 415 416 Convey("Status == OK", func() { 417 expected := &prjpb.PCL{ 418 Clid: int64(cl101.ID), 419 Eversion: cl101.EVersion, 420 ConfigGroupIndexes: []int32{0}, // g0 421 Triggers: &run.Triggers{CqVoteTrigger: &run.Trigger{ 422 Mode: string(run.FullRun), 423 Time: triggerTS, 424 Email: gf.U("user-1").GetEmail(), 425 GerritAccountId: gf.U("user-1").GetAccountId(), 426 }}, 427 } 428 Convey("CL snapshotted with current config", func() { 429 So(s1.makePCL(ctx, cl101), ShouldResembleProto, expected) 430 }) 431 Convey("CL snapshotted with an older config", func() { 432 cl101.ApplicableConfig.GetProjects()[0].ConfigGroupIds = []string{"oldhash/g0"} 433 So(s1.makePCL(ctx, cl101), ShouldResembleProto, expected) 434 }) 435 Convey("not triggered CL", func() { 436 delete(cl101.Snapshot.GetGerrit().GetInfo().GetLabels(), trigger.CQLabelName) 437 expected.Triggers = nil 438 So(s1.makePCL(ctx, cl101), ShouldResembleProto, expected) 439 }) 440 Convey("abandoned CL is not triggered even if it has CQ vote", func() { 441 cl101.Snapshot.GetGerrit().GetInfo().Status = gerritpb.ChangeStatus_ABANDONED 442 expected.Triggers = nil 443 So(s1.makePCL(ctx, cl101), ShouldResembleProto, expected) 444 }) 445 Convey("Submitted CL is also not triggered even if it has CQ vote", func() { 446 cl101.Snapshot.GetGerrit().GetInfo().Status = gerritpb.ChangeStatus_MERGED 447 expected.Triggers = nil 448 expected.Submitted = true 449 So(s1.makePCL(ctx, cl101), ShouldResembleProto, expected) 450 }) 451 }) 452 453 Convey("outdated snapshot requires waiting", func() { 454 cl101.Snapshot.Outdated = &changelist.Snapshot_Outdated{} 455 So(s1.makePCL(ctx, cl101), ShouldResembleProto, &prjpb.PCL{ 456 Clid: int64(cl101.ID), 457 Eversion: cl101.EVersion, 458 Status: prjpb.PCL_UNKNOWN, 459 Outdated: &changelist.Snapshot_Outdated{}, 460 }) 461 }) 462 463 Convey("snapshot from diff project requires waiting", func() { 464 cl101.Snapshot.LuciProject = "another" 465 So(s1.makePCL(ctx, cl101), ShouldResembleProto, &prjpb.PCL{ 466 Clid: int64(cl101.ID), 467 Eversion: cl101.EVersion, 468 Status: prjpb.PCL_UNKNOWN, 469 }) 470 }) 471 472 Convey("CL from diff project is unwatched", func() { 473 s1.PB.LuciProject = "another" 474 So(s1.makePCL(ctx, cl101), ShouldResembleProto, &prjpb.PCL{ 475 Clid: int64(cl101.ID), 476 Eversion: cl101.EVersion, 477 Status: prjpb.PCL_UNWATCHED, 478 }) 479 }) 480 481 Convey("CL watched by several projects is unwatched but with an error", func() { 482 cl101.ApplicableConfig.Projects = append( 483 cl101.ApplicableConfig.GetProjects(), 484 &changelist.ApplicableConfig_Project{ 485 ConfigGroupIds: []string{"g"}, 486 Name: "another", 487 }) 488 So(s1.makePCL(ctx, cl101), ShouldResembleProto, &prjpb.PCL{ 489 Clid: int64(cl101.ID), 490 Eversion: cl101.EVersion, 491 Status: prjpb.PCL_OK, 492 ConfigGroupIndexes: []int32{0}, // g0 493 Triggers: &run.Triggers{CqVoteTrigger: &run.Trigger{ 494 Mode: string(run.FullRun), 495 Time: triggerTS, 496 Email: gf.U("user-1").GetEmail(), 497 GerritAccountId: gf.U("user-1").GetAccountId(), 498 }}, 499 PurgeReasons: []*prjpb.PurgeReason{{ 500 ClError: &changelist.CLError{ 501 Kind: &changelist.CLError_WatchedByManyProjects_{ 502 WatchedByManyProjects: &changelist.CLError_WatchedByManyProjects{ 503 Projects: []string{s1.PB.GetLuciProject(), "another"}, 504 }, 505 }, 506 }, 507 ApplyTo: &prjpb.PurgeReason_AllActiveTriggers{AllActiveTriggers: true}, 508 }}, 509 Errors: []*changelist.CLError{{Kind: &changelist.CLError_WatchedByManyProjects_{ 510 WatchedByManyProjects: &changelist.CLError_WatchedByManyProjects{ 511 Projects: []string{s1.PB.GetLuciProject(), "another"}, 512 }, 513 }}}, 514 }) 515 }) 516 517 Convey("CL with Commit: false footer has an error", func() { 518 cl101.Snapshot.Metadata = []*changelist.StringPair{{Key: "Commit", Value: "false"}} 519 So(s1.makePCL(ctx, cl101).GetPurgeReasons(), ShouldResembleProto, []*prjpb.PurgeReason{ 520 { 521 ClError: &changelist.CLError{ 522 Kind: &changelist.CLError_CommitBlocked{CommitBlocked: true}, 523 }, 524 ApplyTo: &prjpb.PurgeReason_Triggers{Triggers: &run.Triggers{ 525 CqVoteTrigger: &run.Trigger{ 526 Mode: string(run.FullRun), 527 Time: triggerTS, 528 Email: gf.U("user-1").GetEmail(), 529 GerritAccountId: gf.U("user-1").GetAccountId(), 530 }, 531 }}, 532 }, 533 }) 534 }) 535 536 Convey("'Commit: false' footer works with different capitalization", func() { 537 cl101.Snapshot.Metadata = []*changelist.StringPair{{Key: "COMMIT", Value: "FALSE"}} 538 So(s1.makePCL(ctx, cl101).GetPurgeReasons(), ShouldResembleProto, []*prjpb.PurgeReason{{ 539 ClError: &changelist.CLError{ 540 Kind: &changelist.CLError_CommitBlocked{CommitBlocked: true}, 541 }, 542 ApplyTo: &prjpb.PurgeReason_Triggers{Triggers: &run.Triggers{ 543 CqVoteTrigger: &run.Trigger{ 544 Mode: string(run.FullRun), 545 Time: triggerTS, 546 Email: gf.U("user-1").GetEmail(), 547 GerritAccountId: gf.U("user-1").GetAccountId(), 548 }, 549 }}, 550 }}) 551 }) 552 553 Convey("'Commit: false' has no effect for dry run CL", func() { 554 // cl202 is set up for dry run, unlike cl101. 555 cl202.Snapshot.Metadata = []*changelist.StringPair{{Key: "Commit", Value: "false"}} 556 So(s1.makePCL(ctx, cl202).GetPurgeReasons(), ShouldBeEmpty) 557 }) 558 }) 559 }) 560 } 561 562 func TestOnCLsUpdated(t *testing.T) { 563 t.Parallel() 564 565 Convey("OnCLsUpdated works", t, func() { 566 ct := ctest{ 567 lProject: "test", 568 gHost: "c-review.example.com", 569 } 570 ctx, cancel := ct.SetUp(t) 571 defer cancel() 572 573 cfg1 := &cfgpb.Config{} 574 So(prototext.Unmarshal([]byte(cfgText1), cfg1), ShouldBeNil) 575 576 prjcfgtest.Create(ctx, ct.lProject, cfg1) 577 meta := prjcfgtest.MustExist(ctx, ct.lProject) 578 gobmaptest.Update(ctx, ct.lProject) 579 580 // Add 3 CLs: 101 standalone and 202<-203 as a stack. 581 triggerTS := timestamppb.New(ct.Clock.Now()) 582 ci101 := gf.CI( 583 101, gf.PS(1), gf.Ref("refs/heads/main"), gf.Project("repo/a"), 584 gf.CQ(+2, ct.Clock.Now(), gf.U("user-1")), gf.Updated(ct.Clock.Now()), 585 ) 586 ci202 := gf.CI( 587 202, gf.PS(3), gf.Ref("refs/heads/other"), gf.Project("repo/a"), gf.AllRevs(), 588 gf.CQ(+1, ct.Clock.Now(), gf.U("user-2")), gf.Updated(ct.Clock.Now()), 589 ) 590 ci203 := gf.CI( 591 203, gf.PS(3), gf.Ref("refs/heads/other"), gf.Project("repo/a"), gf.AllRevs(), 592 gf.CQ(+1, ct.Clock.Now(), gf.U("user-2")), gf.Updated(ct.Clock.Now()), 593 ) 594 ct.GFake.CreateChange(&gf.Change{Host: ct.gHost, ACLs: gf.ACLPublic(), Info: ci101}) 595 ct.GFake.CreateChange(&gf.Change{Host: ct.gHost, ACLs: gf.ACLPublic(), Info: ci202}) 596 ct.GFake.CreateChange(&gf.Change{Host: ct.gHost, ACLs: gf.ACLPublic(), Info: ci203}) 597 ct.GFake.SetDependsOn(ct.gHost, "203_3" /* child */, "202_2" /*parent*/) 598 cl101 := ct.runCLUpdater(ctx, 101) 599 cl202 := ct.runCLUpdater(ctx, 202) 600 cl203 := ct.runCLUpdater(ctx, 203) 601 602 h := Handler{} 603 s0 := &State{PB: &prjpb.PState{ 604 LuciProject: ct.lProject, 605 Status: prjpb.Status_STARTED, 606 ConfigHash: meta.Hash(), 607 ConfigGroupNames: []string{"g0", "g1"}, 608 }} 609 pb0 := backupPB(s0) 610 611 // NOTE: conversion of individual CL to PCL is in TestUpdateConfig. 612 613 Convey("One simple CL", func() { 614 s1, sideEffect, err := h.OnCLsUpdated(ctx, s0, map[int64]int64{ 615 int64(cl101.ID): cl101.EVersion, 616 }) 617 So(err, ShouldBeNil) 618 So(s0.PB, ShouldResembleProto, pb0) 619 So(sideEffect, ShouldBeNil) 620 So(s1.PB.Pcls, ShouldResembleProto, []*prjpb.PCL{ 621 { 622 Clid: int64(cl101.ID), 623 Eversion: 1, 624 ConfigGroupIndexes: []int32{0}, // g0 625 Status: prjpb.PCL_OK, 626 Triggers: &run.Triggers{CqVoteTrigger: &run.Trigger{ 627 Mode: string(run.FullRun), 628 Time: triggerTS, 629 Email: gf.U("user-1").GetEmail(), 630 GerritAccountId: gf.U("user-1").GetAccountId(), 631 }}, 632 }, 633 }) 634 So(s1.PB.RepartitionRequired, ShouldBeTrue) 635 636 Convey("Noop based on EVersion", func() { 637 s2, sideEffect, err := h.OnCLsUpdated(ctx, s1, map[int64]int64{ 638 int64(cl101.ID): 1, // already known 639 }) 640 So(err, ShouldBeNil) 641 So(sideEffect, ShouldBeNil) 642 So(s1.PB.GetPcls(), ShouldEqual, s2.PB.GetPcls()) // pointer comparison only. 643 }) 644 645 Convey("Marks affected components for triage", func() { 646 cl101.EVersion++ 647 So(datastore.Put(ctx, cl101), ShouldBeNil) 648 // Add 2 components, one of which references cl101. 649 s1.PB.Components = []*prjpb.Component{ 650 {Clids: []int64{int64(cl101.ID)}}, 651 {Clids: []int64{int64(cl101.ID + 111111)}}, 652 } 653 pb := backupPB(s1) 654 s2, sideEffect, err := h.OnCLsUpdated(ctx, s1, map[int64]int64{ 655 int64(cl101.ID): cl101.EVersion, 656 }) 657 So(s1.PB, ShouldResembleProto, pb) 658 So(err, ShouldBeNil) 659 So(sideEffect, ShouldBeNil) 660 // The only expected changes are: 661 pb.Components[0].TriageRequired = true 662 pb.Pcls[0].Eversion = cl101.EVersion 663 So(s2.PB, ShouldResembleProto, pb) 664 }) 665 }) 666 667 Convey("One CL with a yet unknown dep", func() { 668 s1, sideEffect, err := h.OnCLsUpdated(ctx, s0, map[int64]int64{ 669 int64(cl203.ID): 1, 670 }) 671 So(err, ShouldBeNil) 672 So(s0.PB, ShouldResembleProto, pb0) 673 So(sideEffect, ShouldBeNil) 674 So(s1.PB, ShouldResembleProto, &prjpb.PState{ 675 LuciProject: ct.lProject, 676 Status: prjpb.Status_STARTED, 677 ConfigHash: meta.Hash(), 678 ConfigGroupNames: []string{"g0", "g1"}, 679 Pcls: []*prjpb.PCL{ 680 { 681 Clid: int64(cl203.ID), 682 Eversion: 1, 683 ConfigGroupIndexes: []int32{1}, // g1 684 Status: prjpb.PCL_OK, 685 Triggers: &run.Triggers{CqVoteTrigger: &run.Trigger{ 686 Mode: string(run.DryRun), 687 Time: triggerTS, 688 Email: gf.U("user-2").GetEmail(), 689 GerritAccountId: gf.U("user-2").GetAccountId(), 690 }}, 691 Deps: []*changelist.Dep{{Clid: int64(cl202.ID), Kind: changelist.DepKind_HARD}}, 692 }, 693 }, 694 RepartitionRequired: true, 695 }) 696 Convey("unknown dep becomes known and marks a component for triage", func() { 697 // Add a component which has only 203. 698 s1.PB.Components = []*prjpb.Component{ 699 {Clids: []int64{int64(cl203.ID)}}, 700 } 701 pb := backupPB(s1) 702 s2, sideEffect, err := h.OnCLsUpdated(ctx, s1, map[int64]int64{ 703 int64(cl202.ID): cl202.EVersion, 704 }) 705 So(s1.PB, ShouldResembleProto, pb) 706 So(err, ShouldBeNil) 707 So(sideEffect, ShouldBeNil) 708 So(s2.PB.Components[0].TriageRequired, ShouldBeTrue) 709 }) 710 }) 711 712 Convey("PCLs must remain sorted", func() { 713 pcl101 := &prjpb.PCL{ 714 Clid: int64(cl101.ID), 715 Eversion: 1, 716 ConfigGroupIndexes: []int32{0}, // g0 717 Status: prjpb.PCL_OK, 718 Triggers: &run.Triggers{CqVoteTrigger: &run.Trigger{ 719 Mode: string(run.FullRun), 720 Time: triggerTS, 721 }}, 722 } 723 s1 := &State{PB: &prjpb.PState{ 724 LuciProject: ct.lProject, 725 Status: prjpb.Status_STARTED, 726 ConfigHash: meta.Hash(), 727 ConfigGroupNames: []string{"g0", "g1"}, 728 Pcls: sortPCLs([]*prjpb.PCL{ 729 pcl101, 730 { 731 Clid: int64(cl203.ID), 732 Eversion: 1, 733 ConfigGroupIndexes: []int32{1}, // g1 734 Status: prjpb.PCL_OK, 735 Triggers: &run.Triggers{CqVoteTrigger: &run.Trigger{ 736 Mode: string(run.DryRun), 737 Time: triggerTS, 738 }}, 739 Deps: []*changelist.Dep{{Clid: int64(cl202.ID), Kind: changelist.DepKind_HARD}}, 740 }, 741 }), 742 }} 743 pb1 := backupPB(s1) 744 bumpEVersion(ctx, cl203, 3) 745 s2, sideEffect, err := h.OnCLsUpdated(ctx, s1, map[int64]int64{ 746 404: 404, // doesn't even exist 747 int64(cl202.ID): cl202.EVersion, // new 748 int64(cl101.ID): cl101.EVersion, // unchanged 749 int64(cl203.ID): 3, // updated 750 }) 751 So(err, ShouldBeNil) 752 So(s1.PB, ShouldResembleProto, pb1) 753 So(sideEffect, ShouldBeNil) 754 So(s2.PB, ShouldResembleProto, &prjpb.PState{ 755 LuciProject: ct.lProject, 756 Status: prjpb.Status_STARTED, 757 ConfigHash: meta.Hash(), 758 ConfigGroupNames: []string{"g0", "g1"}, 759 Pcls: sortPCLs([]*prjpb.PCL{ 760 { 761 Clid: 404, 762 Eversion: 0, 763 Status: prjpb.PCL_DELETED, 764 }, 765 pcl101, // 101 is unchanged 766 { // new 767 Clid: int64(cl202.ID), 768 Eversion: 1, 769 ConfigGroupIndexes: []int32{1}, // g1 770 Status: prjpb.PCL_OK, 771 Triggers: &run.Triggers{CqVoteTrigger: &run.Trigger{ 772 Mode: string(run.DryRun), 773 Time: triggerTS, 774 Email: gf.U("user-2").GetEmail(), 775 GerritAccountId: gf.U("user-2").GetAccountId(), 776 }}, 777 }, 778 { // updated 779 Clid: int64(cl203.ID), 780 Eversion: 3, 781 ConfigGroupIndexes: []int32{1}, // g1 782 Status: prjpb.PCL_OK, 783 Triggers: &run.Triggers{CqVoteTrigger: &run.Trigger{ 784 Mode: string(run.DryRun), 785 Time: triggerTS, 786 Email: gf.U("user-2").GetEmail(), 787 GerritAccountId: gf.U("user-2").GetAccountId(), 788 }}, 789 Deps: []*changelist.Dep{{Clid: int64(cl202.ID), Kind: changelist.DepKind_HARD}}, 790 }, 791 }), 792 RepartitionRequired: true, 793 }) 794 }) 795 796 Convey("Invalid dep of some other CL must be marked as unwatched", func() { 797 // For example, if user made a typo in `CQ-Depend`, e.g.: 798 // `CQ-Depend: chromiAm:123` 799 // then CL Updater will create an entity for such CL anyway, 800 // but eventually fill it with DependentMeta stating that this LUCI 801 // project has no access to it. 802 // Note that such typos may be malicious, so PM must treat such CLs as not 803 // found regardless of whether they actually exist in Gerrit. 804 cl404 := ct.runCLUpdater(ctx, 404) 805 So(cl404.Snapshot, ShouldBeNil) 806 So(cl404.ApplicableConfig, ShouldBeNil) 807 So(cl404.Access.GetByProject(), ShouldContainKey, ct.lProject) 808 s1, sideEffect, err := h.OnCLsUpdated(ctx, s0, map[int64]int64{ 809 int64(cl404.ID): 1, 810 }) 811 So(err, ShouldBeNil) 812 So(s0.PB, ShouldResembleProto, pb0) 813 So(sideEffect, ShouldBeNil) 814 pb1 := proto.Clone(pb0).(*prjpb.PState) 815 pb1.Pcls = append(pb0.Pcls, &prjpb.PCL{ 816 Clid: int64(cl404.ID), 817 Eversion: 1, 818 ConfigGroupIndexes: []int32{}, 819 Status: prjpb.PCL_UNWATCHED, 820 }) 821 pb1.RepartitionRequired = true 822 So(s1.PB, ShouldResembleProto, pb1) 823 }) 824 825 Convey("non-STARTED project ignores all CL events", func() { 826 s0.PB.Status = prjpb.Status_STOPPING 827 s1, sideEffect, err := h.OnCLsUpdated(ctx, s0, map[int64]int64{ 828 int64(cl101.ID): cl101.EVersion, 829 }) 830 So(err, ShouldBeNil) 831 So(sideEffect, ShouldBeNil) 832 So(s0, ShouldEqual, s1) // pointer comparison only. 833 }) 834 }) 835 } 836 837 func TestRunsCreatedAndFinished(t *testing.T) { 838 t.Parallel() 839 840 Convey("OnRunsCreated and OnRunsFinished works", t, func() { 841 ct := ctest{ 842 lProject: "test", 843 gHost: "c-review.example.com", 844 } 845 ctx, cancel := ct.SetUp(t) 846 defer cancel() 847 848 cfg1 := &cfgpb.Config{} 849 So(prototext.Unmarshal([]byte(cfgText1), cfg1), ShouldBeNil) 850 prjcfgtest.Create(ctx, ct.lProject, cfg1) 851 meta := prjcfgtest.MustExist(ctx, ct.lProject) 852 853 run1 := &run.Run{ID: common.RunID(ct.lProject + "/101-new"), CLs: common.CLIDs{101}} 854 run789 := &run.Run{ID: common.RunID(ct.lProject + "/789-efg"), CLs: common.CLIDs{709, 707, 708}} 855 run1finished := &run.Run{ID: common.RunID(ct.lProject + "/101-done"), CLs: common.CLIDs{101}, Status: run.Status_FAILED} 856 So(datastore.Put(ctx, run1finished, run1, run789), ShouldBeNil) 857 So(run.IsEnded(run1finished.Status), ShouldBeTrue) 858 859 h := Handler{} 860 s1 := &State{PB: &prjpb.PState{ 861 LuciProject: ct.lProject, 862 Status: prjpb.Status_STARTED, 863 ConfigHash: meta.Hash(), 864 ConfigGroupNames: []string{"g0", "g1"}, 865 // For OnRunsFinished / OnRunsCreated PCLs don't matter, so omit them from 866 // the test for brevity, even though valid State must have PCLs covering 867 // all components. 868 Pcls: nil, 869 Components: []*prjpb.Component{ 870 { 871 Clids: []int64{101}, 872 Pruns: []*prjpb.PRun{{Id: ct.lProject + "/101-aaa", Clids: []int64{101}}}, 873 }, 874 { 875 Clids: []int64{202, 203, 204}, 876 }, 877 }, 878 CreatedPruns: []*prjpb.PRun{ 879 {Id: ct.lProject + "/789-efg", Clids: []int64{707, 708, 709}}, 880 }, 881 }} 882 var err error 883 s1.configGroups, err = meta.GetConfigGroups(ctx) 884 So(err, ShouldBeNil) 885 pb1 := backupPB(s1) 886 887 Convey("Noops", func() { 888 finished := make(map[common.RunID]run.Status) 889 Convey("OnRunsFinished on not tracked Run", func() { 890 finished[run1finished.ID] = run.Status_SUCCEEDED 891 s2, sideEffect, err := h.OnRunsFinished(ctx, s1, finished) 892 So(err, ShouldBeNil) 893 So(sideEffect, ShouldBeNil) 894 // although s2 is cloned, it must be exact same as s1. 895 So(s2.PB, ShouldResembleProto, pb1) 896 }) 897 Convey("OnRunsCreated on already finished run", func() { 898 s2, sideEffect, err := h.OnRunsCreated(ctx, s1, common.RunIDs{run1finished.ID}) 899 So(err, ShouldBeNil) 900 So(sideEffect, ShouldBeNil) 901 // although s2 is cloned, it must be exact same as s1. 902 So(s2.PB, ShouldResembleProto, pb1) 903 }) 904 Convey("OnRunsCreated on already tracked Run", func() { 905 s2, sideEffect, err := h.OnRunsCreated(ctx, s1, common.MakeRunIDs(ct.lProject+"/101-aaa")) 906 So(err, ShouldBeNil) 907 So(sideEffect, ShouldBeNil) 908 So(s2, ShouldEqual, s1) 909 So(pb1, ShouldResembleProto, s1.PB) 910 }) 911 Convey("OnRunsCreated on somehow already deleted run", func() { 912 s2, sideEffect, err := h.OnRunsCreated(ctx, s1, common.MakeRunIDs(ct.lProject+"/404-nnn")) 913 So(err, ShouldBeNil) 914 So(sideEffect, ShouldBeNil) 915 // although s2 is cloned, it must be exact same as s1. 916 So(s2.PB, ShouldResembleProto, pb1) 917 }) 918 }) 919 920 Convey("OnRunsCreated", func() { 921 Convey("when PM is started", func() { 922 runX := &run.Run{ // Run involving all of CLs and more. 923 ID: common.RunID(ct.lProject + "/000-xxx"), 924 // The order doesn't have to and is intentionally not sorted here. 925 CLs: common.CLIDs{404, 101, 202, 204, 203}, 926 } 927 run2 := &run.Run{ID: common.RunID(ct.lProject + "/202-bbb"), CLs: common.CLIDs{202}} 928 run3 := &run.Run{ID: common.RunID(ct.lProject + "/203-ccc"), CLs: common.CLIDs{203}} 929 run23 := &run.Run{ID: common.RunID(ct.lProject + "/232-bcb"), CLs: common.CLIDs{203, 202}} 930 run234 := &run.Run{ID: common.RunID(ct.lProject + "/234-bcd"), CLs: common.CLIDs{203, 204, 202}} 931 So(datastore.Put(ctx, run2, run3, run23, run234, runX), ShouldBeNil) 932 933 s2, sideEffect, err := h.OnRunsCreated(ctx, s1, common.RunIDs{ 934 run2.ID, run3.ID, run23.ID, run234.ID, runX.ID, 935 // non-existing Run shouldn't derail others. 936 common.RunID(ct.lProject + "/404-nnn"), 937 }) 938 So(err, ShouldBeNil) 939 So(pb1, ShouldResembleProto, s1.PB) 940 So(sideEffect, ShouldBeNil) 941 So(s2.PB, ShouldResembleProto, &prjpb.PState{ 942 LuciProject: ct.lProject, 943 Status: prjpb.Status_STARTED, 944 ConfigHash: meta.Hash(), 945 ConfigGroupNames: []string{"g0", "g1"}, 946 Components: []*prjpb.Component{ 947 s1.PB.GetComponents()[0], // 101 is unchanged 948 { 949 Clids: []int64{202, 203, 204}, 950 Pruns: []*prjpb.PRun{ 951 // Runs & CLs must be sorted by their respective IDs. 952 {Id: string(run2.ID), Clids: []int64{202}}, 953 {Id: string(run3.ID), Clids: []int64{203}}, 954 {Id: string(run23.ID), Clids: []int64{202, 203}}, 955 {Id: string(run234.ID), Clids: []int64{202, 203, 204}}, 956 }, 957 TriageRequired: true, 958 }, 959 }, 960 RepartitionRequired: true, 961 CreatedPruns: []*prjpb.PRun{ 962 {Id: string(runX.ID), Clids: []int64{101, 202, 203, 204, 404}}, 963 {Id: ct.lProject + "/789-efg", Clids: []int64{707, 708, 709}}, // unchanged 964 }, 965 }) 966 }) 967 Convey("when PM is stopping", func() { 968 s1.PB.Status = prjpb.Status_STOPPING 969 pb1 := backupPB(s1) 970 Convey("cancels incomplete Runs", func() { 971 s2, sideEffect, err := h.OnRunsCreated(ctx, s1, common.RunIDs{run1.ID, run1finished.ID}) 972 So(err, ShouldBeNil) 973 So(pb1, ShouldResembleProto, s1.PB) 974 So(sideEffect, ShouldResemble, &CancelIncompleteRuns{ 975 RunIDs: common.RunIDs{run1.ID}, 976 }) 977 So(s2, ShouldEqual, s1) 978 }) 979 }) 980 }) 981 982 Convey("OnRunsFinished", func() { 983 s1.PB.Status = prjpb.Status_STOPPING 984 pb1 := backupPB(s1) 985 finished := make(map[common.RunID]run.Status) 986 987 Convey("deletes from Components", func() { 988 pb1 := backupPB(s1) 989 runIDs := common.MakeRunIDs(ct.lProject + "/101-aaa") 990 finished[runIDs[0]] = run.Status_CANCELLED 991 s2, sideEffect, err := h.OnRunsFinished(ctx, s1, finished) 992 So(err, ShouldBeNil) 993 So(pb1, ShouldResembleProto, s1.PB) 994 So(sideEffect, ShouldBeNil) 995 So(s2.PB, ShouldResembleProto, &prjpb.PState{ 996 LuciProject: ct.lProject, 997 Status: prjpb.Status_STOPPING, 998 ConfigHash: meta.Hash(), 999 ConfigGroupNames: []string{"g0", "g1"}, 1000 Components: []*prjpb.Component{ 1001 { 1002 Clids: []int64{101}, 1003 Pruns: nil, // removed 1004 TriageRequired: true, 1005 }, 1006 s1.PB.GetComponents()[1], // unchanged 1007 }, 1008 CreatedPruns: s1.PB.GetCreatedPruns(), // unchanged 1009 RepartitionRequired: true, 1010 }) 1011 }) 1012 1013 Convey("deletes from CreatedPruns", func() { 1014 runIDs := common.MakeRunIDs(ct.lProject + "/789-efg") 1015 finished[runIDs[0]] = run.Status_CANCELLED 1016 s2, sideEffect, err := h.OnRunsFinished(ctx, s1, finished) 1017 So(err, ShouldBeNil) 1018 So(pb1, ShouldResembleProto, s1.PB) 1019 So(sideEffect, ShouldBeNil) 1020 So(s2.PB, ShouldResembleProto, &prjpb.PState{ 1021 LuciProject: ct.lProject, 1022 Status: prjpb.Status_STOPPING, 1023 ConfigHash: meta.Hash(), 1024 ConfigGroupNames: []string{"g0", "g1"}, 1025 Components: s1.PB.Components, // unchanged 1026 CreatedPruns: nil, // removed 1027 }) 1028 }) 1029 1030 Convey("stops PM iff all runs finished", func() { 1031 runIDs := common.MakeRunIDs( 1032 ct.lProject+"/101-aaa", 1033 ct.lProject+"/789-efg", 1034 ) 1035 finished[runIDs[0]] = run.Status_SUCCEEDED 1036 finished[runIDs[1]] = run.Status_SUCCEEDED 1037 s2, sideEffect, err := h.OnRunsFinished(ctx, s1, finished) 1038 So(err, ShouldBeNil) 1039 So(pb1, ShouldResembleProto, s1.PB) 1040 So(sideEffect, ShouldBeNil) 1041 So(s2.PB, ShouldResembleProto, &prjpb.PState{ 1042 LuciProject: ct.lProject, 1043 Status: prjpb.Status_STOPPED, 1044 ConfigHash: meta.Hash(), 1045 ConfigGroupNames: []string{"g0", "g1"}, 1046 Pcls: s1.PB.GetPcls(), 1047 Components: []*prjpb.Component{ 1048 {Clids: []int64{101}, TriageRequired: true}, 1049 s1.PB.GetComponents()[1], // unchanged. 1050 }, 1051 CreatedPruns: nil, // removed 1052 RepartitionRequired: true, 1053 }) 1054 So(s2.LogReasons, ShouldResemble, []prjpb.LogReason{prjpb.LogReason_STATUS_CHANGED}) 1055 }) 1056 1057 Convey("purges triggers of the child CLs", func() { 1058 // Emulate an MCE run. 1059 now := testclock.TestRecentTimeUTC 1060 mceRun := &prjpb.PRun{ 1061 Id: "202-deef", 1062 Mode: string(run.FullRun), 1063 Clids: []int64{202}, 1064 } 1065 s1.PB.Components = []*prjpb.Component{ 1066 { 1067 Clids: []int64{202, 203, 204}, 1068 Pruns: []*prjpb.PRun{mceRun}, 1069 }, 1070 } 1071 s1.PB.Pcls = []*prjpb.PCL{ 1072 { 1073 Clid: int64(202), 1074 Eversion: 1, 1075 Status: prjpb.PCL_OK, 1076 ConfigGroupIndexes: []int32{0}, 1077 }, 1078 { 1079 Clid: int64(203), 1080 Eversion: 1, 1081 Status: prjpb.PCL_OK, 1082 Deps: []*changelist.Dep{ 1083 {Clid: 202, Kind: changelist.DepKind_HARD}, 1084 }, 1085 ConfigGroupIndexes: []int32{0}, 1086 Triggers: &run.Triggers{CqVoteTrigger: &run.Trigger{ 1087 Mode: string(run.FullRun), 1088 Time: timestamppb.New(now.Add(-10 * time.Minute)), 1089 }}, 1090 }, 1091 { 1092 Clid: int64(204), 1093 Eversion: 1, 1094 Status: prjpb.PCL_OK, 1095 Deps: []*changelist.Dep{ 1096 {Clid: 202, Kind: changelist.DepKind_HARD}, 1097 {Clid: 203, Kind: changelist.DepKind_HARD}, 1098 }, 1099 ConfigGroupIndexes: []int32{0}, 1100 Triggers: &run.Triggers{CqVoteTrigger: &run.Trigger{ 1101 Mode: string(run.FullRun), 1102 Time: timestamppb.New(now.Add(-10 * time.Minute)), 1103 }}, 1104 }, 1105 } 1106 checkPurgeTask := func(task *prjpb.PurgeCLTask, clToPurge, depRunCL int64) { 1107 So(task.PurgingCl.Clid, ShouldEqual, clToPurge) 1108 So(task.PurgeReasons, ShouldResembleProto, []*prjpb.PurgeReason{ 1109 { 1110 ClError: &changelist.CLError{ 1111 Kind: &changelist.CLError_DepRunFailed{ 1112 DepRunFailed: depRunCL, 1113 }, 1114 }, 1115 ApplyTo: &prjpb.PurgeReason_Triggers{ 1116 Triggers: &run.Triggers{ 1117 CqVoteTrigger: &run.Trigger{ 1118 Mode: string(run.FullRun), 1119 Time: s1.PB.GetPCL(clToPurge).GetTriggers().GetCqVoteTrigger().GetTime(), 1120 }, 1121 }, 1122 }, 1123 }, 1124 }) 1125 } 1126 1127 Convey("if they have CQ votes", func() { 1128 finished[common.RunID(mceRun.Id)] = run.Status_FAILED 1129 _, sideEffect, err := h.OnRunsFinished(ctx, s1, finished) 1130 So(err, ShouldBeNil) 1131 So(sideEffect, ShouldNotBeNil) 1132 tasks := sideEffect.(*TriggerPurgeCLTasks) 1133 1134 // Should purge the vote on both 203 and 204. 1135 So(tasks.payloads, ShouldHaveLength, 2) 1136 checkPurgeTask(tasks.payloads[0], 203, 202) 1137 checkPurgeTask(tasks.payloads[1], 204, 202) 1138 1139 // Only the top CL should be configured to send an email. 1140 So(tasks.payloads[0].PurgingCl.Notification, ShouldResembleProto, clpurger.NoNotification) 1141 So(tasks.payloads[1].PurgingCl.Notification, ShouldBeNil) 1142 }) 1143 Convey("unless the finished Run is failed", func() { 1144 finished[common.RunID(mceRun.Id)] = run.Status_SUCCEEDED 1145 _, sideEffect, err := h.OnRunsFinished(ctx, s1, finished) 1146 So(err, ShouldBeNil) 1147 So(sideEffect, ShouldBeNil) 1148 }) 1149 Convey("unless they have ongoing Runs", func() { 1150 finished[common.RunID(mceRun.Id)] = run.Status_FAILED 1151 // create a run for the middle CL, not the top CL. 1152 middleRun := &prjpb.PRun{ 1153 Id: "203-deef", 1154 Mode: string(run.FullRun), 1155 Clids: []int64{203}, 1156 } 1157 s1.PB.Components[0].Pruns = append(s1.PB.Components[0].Pruns, middleRun) 1158 _, sideEffect, err := h.OnRunsFinished(ctx, s1, finished) 1159 So(err, ShouldBeNil) 1160 So(sideEffect, ShouldNotBeNil) 1161 tasks := sideEffect.(*TriggerPurgeCLTasks) 1162 1163 // Should purge the vote on 204 only 1164 So(tasks.payloads, ShouldHaveLength, 1) 1165 checkPurgeTask(tasks.payloads[0], 204, 202) 1166 So(tasks.payloads[0].PurgingCl.Notification, ShouldBeNil) 1167 }) 1168 }) 1169 }) 1170 }) 1171 } 1172 1173 func TestOnPurgesCompleted(t *testing.T) { 1174 t.Parallel() 1175 1176 Convey("OnPurgesCompleted works", t, func() { 1177 ct := ctest{ 1178 lProject: "test", 1179 gHost: "c-review.example.com", 1180 Test: cvtesting.Test{}, 1181 } 1182 ctx, cancel := ct.SetUp(t) 1183 defer cancel() 1184 1185 cfg1 := &cfgpb.Config{} 1186 So(prototext.Unmarshal([]byte(cfgText1), cfg1), ShouldBeNil) 1187 1188 prjcfgtest.Create(ctx, ct.lProject, cfg1) 1189 meta := prjcfgtest.MustExist(ctx, ct.lProject) 1190 gobmaptest.Update(ctx, ct.lProject) 1191 1192 h := Handler{} 1193 triggerTS := timestamppb.New(ct.Clock.Now()) 1194 ci101 := gf.CI( 1195 101, gf.PS(1), gf.Ref("refs/heads/main"), gf.Project("repo/a"), 1196 gf.CQ(+2, ct.Clock.Now(), gf.U("user-1")), gf.Updated(ct.Clock.Now()), 1197 ) 1198 ci202 := gf.CI( 1199 202, gf.PS(3), gf.Ref("refs/heads/other"), gf.Project("repo/a"), gf.AllRevs(), 1200 gf.CQ(+1, ct.Clock.Now(), gf.U("user-2")), gf.Updated(ct.Clock.Now()), 1201 ) 1202 ci203 := gf.CI( 1203 203, gf.PS(3), gf.Ref("refs/heads/other"), gf.Project("repo/a"), gf.AllRevs(), 1204 gf.CQ(+1, ct.Clock.Now(), gf.U("user-2")), gf.Updated(ct.Clock.Now()), 1205 ) 1206 ci209 := gf.CI( 1207 209, gf.PS(3), gf.Ref("refs/heads/other"), gf.Project("repo/a"), gf.AllRevs(), 1208 gf.CQ(+1, ct.Clock.Now(), gf.U("user-2")), gf.Updated(ct.Clock.Now()), 1209 ) 1210 1211 ct.GFake.CreateChange(&gf.Change{Host: ct.gHost, ACLs: gf.ACLPublic(), Info: ci101}) 1212 ct.GFake.CreateChange(&gf.Change{Host: ct.gHost, ACLs: gf.ACLPublic(), Info: ci202}) 1213 ct.GFake.CreateChange(&gf.Change{Host: ct.gHost, ACLs: gf.ACLPublic(), Info: ci203}) 1214 ct.GFake.CreateChange(&gf.Change{Host: ct.gHost, ACLs: gf.ACLPublic(), Info: ci209}) 1215 cl101 := ct.runCLUpdater(ctx, 101) 1216 cl202 := ct.runCLUpdater(ctx, 202) 1217 cl203 := ct.runCLUpdater(ctx, 203) 1218 cl209 := ct.runCLUpdater(ctx, 209) 1219 1220 Convey("Empty", func() { 1221 s1 := &State{PB: &prjpb.PState{}} 1222 s2, sideEffect, evsToConsume, err := h.OnPurgesCompleted(ctx, s1, nil) 1223 So(err, ShouldBeNil) 1224 So(sideEffect, ShouldBeNil) 1225 So(s1, ShouldEqual, s2) 1226 So(evsToConsume, ShouldHaveLength, 0) 1227 }) 1228 1229 Convey("With existing", func() { 1230 now := testclock.TestRecentTimeUTC 1231 ctx, _ := testclock.UseTime(ctx, now) 1232 s1 := &State{PB: &prjpb.PState{ 1233 LuciProject: ct.lProject, 1234 PurgingCls: []*prjpb.PurgingCL{ 1235 // expires later 1236 { 1237 Clid: int64(cl101.ID), 1238 OperationId: "1", 1239 Deadline: timestamppb.New(now.Add(time.Minute)), 1240 ApplyTo: &prjpb.PurgingCL_AllActiveTriggers{AllActiveTriggers: true}, 1241 }, 1242 // expires now, but due to grace period it'll stay here. 1243 { 1244 Clid: int64(cl202.ID), 1245 OperationId: "2", 1246 Deadline: timestamppb.New(now), 1247 ApplyTo: &prjpb.PurgingCL_AllActiveTriggers{AllActiveTriggers: true}, 1248 }, 1249 // definitely expired. 1250 { 1251 Clid: int64(cl203.ID), 1252 OperationId: "3", 1253 Deadline: timestamppb.New(now.Add(-time.Hour)), 1254 ApplyTo: &prjpb.PurgingCL_AllActiveTriggers{AllActiveTriggers: true}, 1255 }, 1256 }, 1257 // Components require PCLs, but in this test it doesn't matter. 1258 Components: []*prjpb.Component{ 1259 {Clids: []int64{int64(cl209.ID)}}, // for unconfusing indexes below. 1260 {Clids: []int64{int64(cl101.ID)}}, 1261 {Clids: []int64{int64(cl202.ID)}, TriageRequired: true}, 1262 {Clids: []int64{int64(cl203.ID)}}, 1263 }, 1264 // PCLs are supposed to be sorted. 1265 Pcls: []*prjpb.PCL{ 1266 { 1267 Clid: int64(cl101.ID), 1268 Eversion: cl101.EVersion, 1269 Status: prjpb.PCL_OK, 1270 ConfigGroupIndexes: []int32{0}, 1271 Triggers: &run.Triggers{CqVoteTrigger: &run.Trigger{ 1272 Mode: string(run.FullRun), 1273 Time: triggerTS, 1274 Email: gf.U("user-1").GetEmail(), 1275 GerritAccountId: gf.U("user-1").GetAccountId(), 1276 }}, 1277 }, 1278 { 1279 Clid: int64(cl202.ID), 1280 Eversion: 1, 1281 Status: prjpb.PCL_OK, 1282 ConfigGroupIndexes: []int32{1}, 1283 Triggers: &run.Triggers{CqVoteTrigger: &run.Trigger{ 1284 Mode: string(run.DryRun), 1285 Time: triggerTS, 1286 Email: gf.U("user-2").GetEmail(), 1287 GerritAccountId: gf.U("user-2").GetAccountId(), 1288 }}, 1289 }, 1290 { 1291 Clid: int64(cl203.ID), 1292 Eversion: 1, 1293 Status: prjpb.PCL_OK, 1294 ConfigGroupIndexes: []int32{1}, 1295 Triggers: &run.Triggers{CqVoteTrigger: &run.Trigger{ 1296 Mode: string(run.DryRun), 1297 Time: triggerTS, 1298 Email: gf.U("user-2").GetEmail(), 1299 GerritAccountId: gf.U("user-2").GetAccountId(), 1300 }}, 1301 }, 1302 { 1303 Clid: int64(cl209.ID), 1304 Eversion: 1, 1305 Status: prjpb.PCL_OK, 1306 ConfigGroupIndexes: []int32{1}, 1307 Triggers: &run.Triggers{CqVoteTrigger: &run.Trigger{ 1308 Mode: string(run.DryRun), 1309 Time: triggerTS, 1310 Email: gf.U("user-2").GetEmail(), 1311 GerritAccountId: gf.U("user-2").GetAccountId(), 1312 }}, 1313 }, 1314 }, 1315 ConfigGroupNames: []string{"g0", "g1"}, 1316 ConfigHash: meta.Hash(), 1317 }} 1318 pb := backupPB(s1) 1319 1320 Convey("Expires and removed", func() { 1321 s2, sideEffect, evsToConsume, err := h.OnPurgesCompleted(ctx, s1, []*prjpb.PurgeCompleted{{OperationId: "1", Clid: int64(cl101.ID)}}) 1322 So(err, ShouldBeNil) 1323 So(sideEffect, ShouldBeNil) 1324 So(s1.PB, ShouldResembleProto, pb) 1325 So(evsToConsume, ShouldEqual, []int{0}) 1326 1327 pb.PurgingCls = []*prjpb.PurgingCL{ 1328 { 1329 Clid: int64(cl202.ID), OperationId: "2", Deadline: timestamppb.New(now), 1330 ApplyTo: &prjpb.PurgingCL_AllActiveTriggers{AllActiveTriggers: true}, 1331 }, 1332 } 1333 pb.Components = []*prjpb.Component{ 1334 pb.Components[0], 1335 {Clids: []int64{int64(cl101.ID)}, TriageRequired: true}, 1336 pb.Components[2], 1337 {Clids: []int64{int64(cl203.ID)}, TriageRequired: true}, 1338 } 1339 So(s2.PB, ShouldResembleProto, pb) 1340 }) 1341 1342 Convey("All removed", func() { 1343 s2, sideEffect, evsToConsume, err := h.OnPurgesCompleted(ctx, s1, []*prjpb.PurgeCompleted{ 1344 {OperationId: "3", Clid: int64(cl203.ID)}, 1345 {OperationId: "1", Clid: int64(cl101.ID)}, 1346 {OperationId: "5", Clid: int64(cl209.ID)}, 1347 {OperationId: "2", Clid: int64(cl202.ID)}, 1348 }) 1349 So(err, ShouldBeNil) 1350 So(sideEffect, ShouldBeNil) 1351 So(s1.PB, ShouldResembleProto, pb) 1352 So(evsToConsume, ShouldEqual, []int{0, 1, 2, 3}) 1353 pb.PurgingCls = nil 1354 pb.Components = []*prjpb.Component{ 1355 pb.Components[0], 1356 {Clids: []int64{int64(cl101.ID)}, TriageRequired: true}, 1357 pb.Components[2], // it was waiting for triage already 1358 {Clids: []int64{int64(cl203.ID)}, TriageRequired: true}, 1359 } 1360 So(s2.PB, ShouldResembleProto, pb) 1361 }) 1362 1363 Convey("Outdated", func() { 1364 cl101.Snapshot.Outdated = &changelist.Snapshot_Outdated{} 1365 So(datastore.Put(ctx, cl101), ShouldBeNil) 1366 s2, sideEffect, evsToConsume, err := h.OnPurgesCompleted(ctx, s1, []*prjpb.PurgeCompleted{ 1367 {OperationId: "1", Clid: int64(cl101.ID)}, 1368 }) 1369 So(err, ShouldBeNil) 1370 So(sideEffect, ShouldBeNil) 1371 So(s2.PB.GetPurgingCL(int64(cl101.ID)), ShouldNotBeNil) 1372 So(evsToConsume, ShouldBeNil) 1373 }) 1374 1375 Convey("Doesn't modify components if they are due re-repartition anyway", func() { 1376 s1.PB.RepartitionRequired = true 1377 pb := backupPB(s1) 1378 s2, sideEffect, evsToConsume, err := h.OnPurgesCompleted(ctx, s1, []*prjpb.PurgeCompleted{ 1379 {OperationId: "1", Clid: int64(cl101.ID)}, 1380 {OperationId: "2", Clid: int64(cl202.ID)}, 1381 {OperationId: "3", Clid: int64(cl203.ID)}, 1382 }) 1383 So(err, ShouldBeNil) 1384 So(sideEffect, ShouldBeNil) 1385 So(s1.PB, ShouldResembleProto, pb) 1386 1387 pb.PurgingCls = nil 1388 So(s2.PB, ShouldResembleProto, pb) 1389 So(evsToConsume, ShouldEqual, []int{0, 1, 2}) 1390 }) 1391 }) 1392 }) 1393 } 1394 1395 func TestOnTriggeringCLDepsCompleted(t *testing.T) { 1396 t.Parallel() 1397 1398 Convey("OnTriggeringCLDepsCompleted", t, func() { 1399 ct := ctest{ 1400 lProject: "test", 1401 gHost: "c-review.example.com", 1402 Test: cvtesting.Test{}, 1403 } 1404 ctx, cancel := ct.SetUp(t) 1405 defer cancel() 1406 1407 cfg1 := &cfgpb.Config{} 1408 So(prototext.Unmarshal([]byte(cfgText1), cfg1), ShouldBeNil) 1409 1410 prjcfgtest.Create(ctx, ct.lProject, cfg1) 1411 meta := prjcfgtest.MustExist(ctx, ct.lProject) 1412 gobmaptest.Update(ctx, ct.lProject) 1413 1414 clPoller := poller.New(ct.TQDispatcher, nil, nil, nil) 1415 h := Handler{CLPoller: clPoller} 1416 1417 // mock CLs 1418 now := ct.Clock.Now() 1419 ci101 := gf.CI( 1420 101, gf.PS(1), gf.Ref("refs/heads/main"), gf.Project("repo/a"), 1421 gf.CQ(+2, now, gf.U("user-1")), gf.Updated(now), 1422 ) 1423 ci102 := gf.CI( 1424 102, gf.PS(3), gf.Ref("refs/heads/main"), gf.Project("repo/a"), gf.AllRevs(), 1425 gf.CQ(+1, now, gf.U("user-1")), gf.Updated(now), 1426 ) 1427 ci103 := gf.CI( 1428 103, gf.PS(3), gf.Ref("refs/heads/main"), gf.Project("repo/a"), gf.AllRevs(), 1429 gf.CQ(+1, now, gf.U("user-1")), gf.Updated(now), 1430 ) 1431 ct.GFake.CreateChange(&gf.Change{Host: ct.gHost, ACLs: gf.ACLPublic(), Info: ci101}) 1432 ct.GFake.CreateChange(&gf.Change{Host: ct.gHost, ACLs: gf.ACLPublic(), Info: ci102}) 1433 ct.GFake.CreateChange(&gf.Change{Host: ct.gHost, ACLs: gf.ACLPublic(), Info: ci103}) 1434 ct.GFake.SetDependsOn(ct.gHost, "103_3", "102_3", "101_1") 1435 cl101 := ct.runCLUpdater(ctx, 101) 1436 cl102 := ct.runCLUpdater(ctx, 102) 1437 cl103 := ct.runCLUpdater(ctx, 103) 1438 1439 s1 := &State{PB: &prjpb.PState{ 1440 LuciProject: ct.lProject, 1441 Pcls: []*prjpb.PCL{ 1442 { 1443 Clid: int64(cl101.ID), 1444 Eversion: 1, 1445 Status: prjpb.PCL_OK, 1446 Triggers: &run.Triggers{CqVoteTrigger: &run.Trigger{ 1447 Mode: string(run.FullRun), 1448 Time: timestamppb.New(now.Add(-10 * time.Minute)), 1449 }}, 1450 ConfigGroupIndexes: []int32{0}, 1451 }, 1452 { 1453 Clid: int64(cl102.ID), 1454 Eversion: 1, 1455 Status: prjpb.PCL_OK, 1456 Triggers: &run.Triggers{CqVoteTrigger: &run.Trigger{ 1457 Mode: string(run.FullRun), 1458 Time: timestamppb.New(now.Add(-10 * time.Minute)), 1459 }}, 1460 ConfigGroupIndexes: []int32{0}, 1461 }, 1462 { 1463 Clid: int64(cl103.ID), 1464 Eversion: 1, 1465 Status: prjpb.PCL_OK, 1466 ConfigGroupIndexes: []int32{0}, 1467 }, 1468 }, 1469 // Components require PCLs, but in this test it doesn't matter. 1470 Components: []*prjpb.Component{ 1471 {Clids: []int64{int64(cl101.ID)}}, // for unconfusing indexes below. 1472 {Clids: []int64{int64(cl102.ID)}}, 1473 {Clids: []int64{int64(cl103.ID)}}, 1474 }, 1475 ConfigGroupNames: []string{"g0"}, 1476 ConfigHash: meta.Hash(), 1477 }} 1478 addTriggeringCLDeps := func(s *State, deadline time.Time, origin *changelist.CL, deps ...*changelist.CL) *prjpb.TriggeringCLDeps { 1479 var clids []int64 1480 for _, dep := range deps { 1481 clids = append(clids, int64(dep.ID)) 1482 } 1483 op := &prjpb.TriggeringCLDeps{ 1484 OriginClid: int64(origin.ID), 1485 DepClids: clids, 1486 OperationId: fmt.Sprintf("op-%d", origin.ID), 1487 Deadline: timestamppb.New(deadline), 1488 Trigger: &run.Trigger{Mode: string(run.FullRun)}, 1489 } 1490 s.PB.TriggeringClDeps, _ = s.PB.COWTriggeringCLDeps(nil, []*prjpb.TriggeringCLDeps{op}) 1491 return op 1492 } 1493 TriggeringCLDeps := func(s *State, cl *changelist.CL) *prjpb.TriggeringCLDeps { 1494 return s.PB.GetTriggeringCLDeps(int64(cl.ID)) 1495 } 1496 1497 Convey("effectively noop if empty", func() { 1498 s2, se, evIndexes, err := h.OnTriggeringCLDepsCompleted(ctx, s1, nil) 1499 So(err, ShouldBeNil) 1500 So(se, ShouldBeNil) 1501 So(evIndexes, ShouldBeNil) 1502 // OnTriggeringCLDepsCompleted() always makes a shallow clone for 1503 // PCL evaluations. There shouldn't be any changes other than that. 1504 s2.alreadyCloned = true 1505 So(s1, ShouldEqual, s2) 1506 }) 1507 Convey("removes an expired op", func() { 1508 addTriggeringCLDeps(s1, now.Add(-time.Hour), cl103, cl101, cl102) 1509 s2, se, evIndexes, err := h.OnTriggeringCLDepsCompleted(ctx, s1, nil) 1510 So(err, ShouldBeNil) 1511 So(TriggeringCLDeps(s2, cl103), ShouldBeNil) 1512 So(se, ShouldBeNil) 1513 So(evIndexes, ShouldBeNil) 1514 }) 1515 Convey("with succeeeded ops", func() { 1516 op := addTriggeringCLDeps(s1, now.Add(time.Minute), cl103, cl101, cl102) 1517 events := []*prjpb.TriggeringCLDepsCompleted{ 1518 { 1519 OperationId: op.GetOperationId(), 1520 Origin: int64(cl103.ID), 1521 Succeeded: []int64{int64(cl101.ID), int64(cl102.ID)}, 1522 }, 1523 } 1524 Convey("removes the op", func() { 1525 s2, se, evIndexes, err := h.OnTriggeringCLDepsCompleted(ctx, s1, events) 1526 So(err, ShouldBeNil) 1527 So(TriggeringCLDeps(s2, cl103), ShouldBeNil) 1528 So(se, ShouldBeNil) 1529 So(evIndexes, ShouldEqual, []int{0}) 1530 }) 1531 Convey("keeps the op, if any dep PCL is outdated", func() { 1532 cl102.Snapshot.Outdated = &changelist.Snapshot_Outdated{} 1533 So(datastore.Put(ctx, cl102 /* dep */), ShouldBeNil) 1534 s2, se, evIndexes, err := h.OnTriggeringCLDepsCompleted(ctx, s1, events) 1535 So(err, ShouldBeNil) 1536 So(TriggeringCLDeps(s2, cl103 /* origin */), ShouldNotBeNil) 1537 So(se, ShouldBeNil) 1538 So(evIndexes, ShouldBeNil) 1539 }) 1540 }) 1541 Convey("enqueues PurgeCLTasks for the origin and dep CLs, if an Op has fails", func() { 1542 op := addTriggeringCLDeps(s1, now.Add(time.Minute), cl103, cl101, cl102) 1543 events := []*prjpb.TriggeringCLDepsCompleted{ 1544 { 1545 OperationId: op.GetOperationId(), 1546 Origin: int64(cl103.ID), 1547 Succeeded: []int64{int64(cl101.ID)}, 1548 Failed: []*changelist.CLError_TriggerDeps{{ 1549 PermissionDenied: []*changelist.CLError_TriggerDeps_PermissionDenied{{ 1550 Clid: int64(cl102.ID), 1551 Email: "foo@example.org", 1552 }}, 1553 }}, 1554 }, 1555 } 1556 s2, se, evIndexes, err := h.OnTriggeringCLDepsCompleted(ctx, s1, events) 1557 So(err, ShouldBeNil) 1558 So(evIndexes, ShouldEqual, []int{0}) 1559 1560 // remove the TriggeringCLDeps, but schedule PurgingCL(s). 1561 So(TriggeringCLDeps(s2, cl103), ShouldBeNil) 1562 So(s2.PB.GetPurgingCL(int64(cl101.ID)), ShouldNotBeNil) 1563 So(s2.PB.GetPurgingCL(int64(cl102.ID)), ShouldBeNil) 1564 1565 // verify the PurginCL payload. 1566 tasks := se.(*TriggerPurgeCLTasks) 1567 dl := timestamppb.New(now.Add(maxPurgingCLDuration)) 1568 opID := dl.AsTime().Unix() 1569 So(tasks.payloads, ShouldHaveLength, 2) 1570 tr := &run.Triggers{ 1571 CqVoteTrigger: &run.Trigger{ 1572 Mode: string(run.FullRun), 1573 }, 1574 } 1575 oriPT, depPT := tasks.payloads[0], tasks.payloads[1] 1576 if oriPT.GetPurgingCl().GetClid() != int64(cl103.ID) { 1577 oriPT, depPT = depPT, oriPT 1578 } 1579 expectedPurgeReasons := []*prjpb.PurgeReason{ 1580 { 1581 ClError: &changelist.CLError{ 1582 Kind: &changelist.CLError_TriggerDeps_{ 1583 TriggerDeps: &changelist.CLError_TriggerDeps{ 1584 PermissionDenied: []*changelist.CLError_TriggerDeps_PermissionDenied{{ 1585 Clid: int64(cl102.ID), 1586 Email: "foo@example.org", 1587 }}, 1588 }, 1589 }, 1590 }, 1591 ApplyTo: &prjpb.PurgeReason_Triggers{Triggers: tr}, 1592 }, 1593 } 1594 So(oriPT.GetPurgeReasons(), ShouldResembleProto, expectedPurgeReasons) 1595 So(oriPT.GetPurgingCl(), ShouldResembleProto, &prjpb.PurgingCL{ 1596 Clid: int64(cl103.ID), 1597 Deadline: dl, 1598 // Must be nil for the default notifications. 1599 Notification: nil, 1600 OperationId: fmt.Sprintf("%d-%d", opID, cl103.ID), 1601 ApplyTo: &prjpb.PurgingCL_Triggers{Triggers: tr}, 1602 }) 1603 So(depPT.GetPurgeReasons(), ShouldResembleProto, expectedPurgeReasons) 1604 So(depPT.GetPurgingCl(), ShouldResembleProto, &prjpb.PurgingCL{ 1605 Clid: int64(cl101.ID), 1606 Deadline: dl, 1607 Notification: clpurger.NoNotification, 1608 OperationId: fmt.Sprintf("%d-%d", opID, cl101.ID), 1609 ApplyTo: &prjpb.PurgingCL_Triggers{Triggers: tr}, 1610 }) 1611 }) 1612 }) 1613 } 1614 1615 // backupPB returns a deep copy of State.PB for future assertion that State 1616 // wasn't modified. 1617 func backupPB(s *State) *prjpb.PState { 1618 ret := &prjpb.PState{} 1619 proto.Merge(ret, s.PB) 1620 return ret 1621 } 1622 1623 func bumpEVersion(ctx context.Context, cl *changelist.CL, desired int64) { 1624 if cl.EVersion >= desired { 1625 panic(fmt.Errorf("can't go %d to %d", cl.EVersion, desired)) 1626 } 1627 cl.EVersion = desired 1628 So(datastore.Put(ctx, cl), ShouldBeNil) 1629 } 1630 1631 func defaultPCL(cl *changelist.CL) *prjpb.PCL { 1632 p := &prjpb.PCL{ 1633 Clid: int64(cl.ID), 1634 Eversion: cl.EVersion, 1635 ConfigGroupIndexes: []int32{0}, 1636 Status: prjpb.PCL_OK, 1637 Deps: cl.Snapshot.GetDeps(), 1638 } 1639 ci := cl.Snapshot.GetGerrit().GetInfo() 1640 if ci != nil { 1641 p.Triggers = trigger.Find(&trigger.FindInput{ChangeInfo: ci, ConfigGroup: &cfgpb.ConfigGroup{}}) 1642 } 1643 return p 1644 } 1645 1646 func i64s(vs ...any) []int64 { 1647 res := make([]int64, len(vs)) 1648 for i, v := range vs { 1649 switch x := v.(type) { 1650 case int64: 1651 res[i] = x 1652 case common.CLID: 1653 res[i] = int64(x) 1654 case int: 1655 res[i] = int64(x) 1656 default: 1657 panic(fmt.Errorf("unknown type: %T %v", v, v)) 1658 } 1659 } 1660 return res 1661 } 1662 1663 func i64sorted(vs ...any) []int64 { 1664 res := i64s(vs...) 1665 sort.Slice(res, func(i, j int) bool { return res[i] < res[j] }) 1666 return res 1667 } 1668 1669 func sortPCLs(vs []*prjpb.PCL) []*prjpb.PCL { 1670 sort.Slice(vs, func(i, j int) bool { return vs[i].GetClid() < vs[j].GetClid() }) 1671 return vs 1672 } 1673 1674 func mkClidsSet(cls map[int]*changelist.CL, ids ...int) common.CLIDsSet { 1675 res := make(common.CLIDsSet, len(ids)) 1676 for _, id := range ids { 1677 res[cls[id].ID] = struct{}{} 1678 } 1679 return res 1680 } 1681 1682 func sortByFirstCL(cs []*prjpb.Component) []*prjpb.Component { 1683 sort.Slice(cs, func(i, j int) bool { return cs[i].GetClids()[0] < cs[j].GetClids()[0] }) 1684 return cs 1685 }