go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/prjmanager/state/components_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 "testing" 22 "time" 23 24 "google.golang.org/protobuf/types/known/timestamppb" 25 26 "go.chromium.org/luci/auth/identity" 27 "go.chromium.org/luci/common/clock/testclock" 28 "go.chromium.org/luci/common/errors" 29 "go.chromium.org/luci/gae/service/datastore" 30 31 cfgpb "go.chromium.org/luci/cv/api/config/v2" 32 "go.chromium.org/luci/cv/internal/changelist" 33 "go.chromium.org/luci/cv/internal/common" 34 "go.chromium.org/luci/cv/internal/configs/prjcfg/prjcfgtest" 35 "go.chromium.org/luci/cv/internal/cvtesting" 36 gf "go.chromium.org/luci/cv/internal/gerrit/gerritfake" 37 "go.chromium.org/luci/cv/internal/gerrit/trigger" 38 "go.chromium.org/luci/cv/internal/prjmanager" 39 "go.chromium.org/luci/cv/internal/prjmanager/itriager" 40 "go.chromium.org/luci/cv/internal/prjmanager/pmtest" 41 "go.chromium.org/luci/cv/internal/prjmanager/prjpb" 42 "go.chromium.org/luci/cv/internal/run" 43 "go.chromium.org/luci/cv/internal/run/runcreator" 44 "go.chromium.org/luci/cv/internal/run/runquery" 45 "go.chromium.org/luci/cv/internal/tryjob" 46 47 . "github.com/smartystreets/goconvey/convey" 48 . "go.chromium.org/luci/common/testing/assertions" 49 ) 50 51 func TestEarliestDecisionTime(t *testing.T) { 52 t.Parallel() 53 54 Convey("earliestDecisionTime works", t, func() { 55 now := testclock.TestRecentTimeUTC 56 t0 := now.Add(time.Hour) 57 58 earliest := func(cs []*prjpb.Component) time.Time { 59 t, tPB, asap := earliestDecisionTime(cs) 60 if asap { 61 return now 62 } 63 if t.IsZero() { 64 So(tPB, ShouldBeNil) 65 } else { 66 So(tPB.AsTime(), ShouldResemble, t) 67 } 68 return t 69 } 70 71 cs := []*prjpb.Component{ 72 {DecisionTime: nil}, 73 } 74 So(earliest(cs), ShouldResemble, time.Time{}) 75 76 cs = append(cs, &prjpb.Component{DecisionTime: timestamppb.New(t0.Add(time.Second))}) 77 So(earliest(cs), ShouldResemble, t0.Add(time.Second)) 78 79 cs = append(cs, &prjpb.Component{}) 80 So(earliest(cs), ShouldResemble, t0.Add(time.Second)) 81 82 cs = append(cs, &prjpb.Component{DecisionTime: timestamppb.New(t0.Add(time.Hour))}) 83 So(earliest(cs), ShouldResemble, t0.Add(time.Second)) 84 85 cs = append(cs, &prjpb.Component{DecisionTime: timestamppb.New(t0)}) 86 So(earliest(cs), ShouldResemble, t0) 87 88 cs = append(cs, &prjpb.Component{ 89 TriageRequired: true, 90 // DecisionTime in this case doesn't matter. 91 DecisionTime: timestamppb.New(t0.Add(10 * time.Hour)), 92 }) 93 So(earliest(cs), ShouldResemble, now) 94 }) 95 } 96 97 func TestComponentsActions(t *testing.T) { 98 t.Parallel() 99 100 Convey("Component actions logic work in the abstract", t, func() { 101 ct := cvtesting.Test{} 102 ctx, cancel := ct.SetUp(t) 103 defer cancel() 104 now := ct.Clock.Now() 105 106 const lProject = "luci-project" 107 108 prjcfgtest.Create(ctx, lProject, &cfgpb.Config{ConfigGroups: []*cfgpb.ConfigGroup{{Name: "main"}}}) 109 meta := prjcfgtest.MustExist(ctx, lProject) 110 pmNotifier := prjmanager.NewNotifier(ct.TQDispatcher) 111 runNotifier := run.NewNotifier(ct.TQDispatcher) 112 tjNotifier := tryjob.NewNotifier(ct.TQDispatcher) 113 h := Handler{ 114 PMNotifier: pmNotifier, 115 RunNotifier: runNotifier, 116 CLMutator: changelist.NewMutator(ct.TQDispatcher, pmNotifier, runNotifier, tjNotifier), 117 } 118 state := &State{ 119 PB: &prjpb.PState{ 120 LuciProject: lProject, 121 Status: prjpb.Status_STARTED, 122 ConfigHash: meta.Hash(), 123 Pcls: []*prjpb.PCL{ 124 {Clid: 1}, 125 {Clid: 2}, 126 {Clid: 3}, 127 {Clid: 999}, 128 }, 129 Components: []*prjpb.Component{ 130 {Clids: []int64{999}}, // never sees any action. 131 {Clids: []int64{1}, DecisionTime: timestamppb.New(now.Add(1 * time.Minute))}, 132 {Clids: []int64{2}, DecisionTime: timestamppb.New(now.Add(2 * time.Minute))}, 133 {Clids: []int64{3}, DecisionTime: timestamppb.New(now.Add(3 * time.Minute))}, 134 }, 135 NextEvalTime: timestamppb.New(now.Add(1 * time.Minute)), 136 }, 137 } 138 139 pb := backupPB(state) 140 141 markComponentsForTriage := func(indexes ...int) { 142 for _, i := range indexes { 143 state.PB.GetComponents()[i].TriageRequired = true 144 } 145 pb = backupPB(state) 146 } 147 148 markTriaged := func(c *prjpb.Component) *prjpb.Component { 149 if !c.GetTriageRequired() { 150 panic(fmt.Errorf("must required triage")) 151 } 152 o := c.CloneShallow() 153 o.TriageRequired = false 154 return o 155 } 156 157 calledOn := make(chan *prjpb.Component, len(state.PB.Components)) 158 collectCalledOn := func() []int { 159 var out []int 160 loop: 161 for { 162 select { 163 case c := <-calledOn: 164 out = append(out, int(c.GetClids()[0])) 165 default: 166 break loop 167 } 168 } 169 sort.Ints(out) 170 return out 171 } 172 173 Convey("noop at triage", func() { 174 h.ComponentTriage = func(_ context.Context, c *prjpb.Component, _ itriager.PMState) (itriager.Result, error) { 175 calledOn <- c 176 return itriager.Result{}, nil 177 } 178 actions, saveForDebug, err := h.triageComponents(ctx, state) 179 So(err, ShouldBeNil) 180 So(saveForDebug, ShouldBeFalse) 181 So(actions, ShouldBeNil) 182 So(state.PB, ShouldResembleProto, pb) 183 So(collectCalledOn(), ShouldBeEmpty) 184 185 Convey("ExecDeferred", func() { 186 state2, sideEffect, err := h.ExecDeferred(ctx, state) 187 So(err, ShouldBeNil) 188 So(state.PB, ShouldResembleProto, pb) 189 So(state2, ShouldEqual, state) // pointer comparison 190 So(sideEffect, ShouldBeNil) 191 // Always creates new task iff there is NextEvalTime. 192 So(pmtest.ETAsOF(ct.TQ.Tasks(), lProject), ShouldNotBeEmpty) 193 }) 194 }) 195 196 Convey("triage called on TriageRequired components or when decision time is <= now", func() { 197 ct.Clock.Set(state.PB.Components[1].DecisionTime.AsTime()) 198 c1next := state.PB.Components[1].DecisionTime.AsTime().Add(time.Hour) 199 markComponentsForTriage(3) 200 h.ComponentTriage = func(_ context.Context, c *prjpb.Component, _ itriager.PMState) (itriager.Result, error) { 201 calledOn <- c 202 switch c.GetClids()[0] { 203 case 1: 204 c = c.CloneShallow() 205 c.DecisionTime = timestamppb.New(c1next) 206 return itriager.Result{NewValue: c}, nil 207 case 3: 208 return itriager.Result{NewValue: markTriaged(c)}, nil 209 } 210 panic("unreachable") 211 } 212 actions, saveForDebug, err := h.triageComponents(ctx, state) 213 So(err, ShouldBeNil) 214 So(saveForDebug, ShouldBeFalse) 215 So(actions, ShouldHaveLength, 2) 216 So(collectCalledOn(), ShouldResemble, []int{1, 3}) 217 218 Convey("ExecDeferred", func() { 219 state2, sideEffect, err := h.ExecDeferred(ctx, state) 220 So(err, ShouldBeNil) 221 So(sideEffect, ShouldBeNil) 222 pb.NextEvalTime = timestamppb.New(now.Add(2 * time.Minute)) 223 pb.Components[1].DecisionTime = timestamppb.New(c1next) 224 pb.Components[3].TriageRequired = false 225 So(state2.PB, ShouldResembleProto, pb) 226 So(pmtest.ETAsWithin(ct.TQ.Tasks(), lProject, time.Second, now.Add(2*time.Minute)), ShouldNotBeEmpty) 227 }) 228 }) 229 230 Convey("purges CLs", func() { 231 markComponentsForTriage(1, 2, 3) 232 h.ComponentTriage = func(_ context.Context, c *prjpb.Component, _ itriager.PMState) (itriager.Result, error) { 233 switch clid := c.GetClids()[0]; clid { 234 case 1, 3: 235 return itriager.Result{CLsToPurge: []*prjpb.PurgeCLTask{{ 236 PurgingCl: &prjpb.PurgingCL{Clid: clid, 237 ApplyTo: &prjpb.PurgingCL_AllActiveTriggers{AllActiveTriggers: true}, 238 }, 239 PurgeReasons: []*prjpb.PurgeReason{{ 240 ClError: &changelist.CLError{ 241 Kind: &changelist.CLError_OwnerLacksEmail{ 242 OwnerLacksEmail: true, 243 }, 244 }, 245 ApplyTo: &prjpb.PurgeReason_AllActiveTriggers{AllActiveTriggers: true}, 246 }}, 247 }}}, nil 248 case 2: 249 return itriager.Result{}, nil 250 } 251 panic("unreachable") 252 } 253 actions, saveForDebug, err := h.triageComponents(ctx, state) 254 So(err, ShouldBeNil) 255 So(saveForDebug, ShouldBeFalse) 256 So(actions, ShouldHaveLength, 3) 257 So(state.PB, ShouldResembleProto, pb) 258 259 Convey("ExecDeferred", func() { 260 state2, sideEffects, err := h.ExecDeferred(ctx, state) 261 So(err, ShouldBeNil) 262 expectedDeadline := timestamppb.New(now.Add(maxPurgingCLDuration)) 263 So(state2.PB.GetPurgingCls(), ShouldResembleProto, []*prjpb.PurgingCL{ 264 {Clid: 1, OperationId: "1580640000-1", Deadline: expectedDeadline, 265 ApplyTo: &prjpb.PurgingCL_AllActiveTriggers{AllActiveTriggers: true}, 266 }, 267 {Clid: 3, OperationId: "1580640000-3", Deadline: expectedDeadline, 268 ApplyTo: &prjpb.PurgingCL_AllActiveTriggers{AllActiveTriggers: true}, 269 }, 270 }) 271 272 sideEffect := sideEffects.(*SideEffects).items[0] 273 So(sideEffect, ShouldHaveSameTypeAs, &TriggerPurgeCLTasks{}) 274 ps := sideEffect.(*TriggerPurgeCLTasks).payloads 275 So(ps, ShouldHaveLength, 2) 276 // Unlike PB.PurgingCls, the tasks aren't necessarily sorted. 277 sort.Slice(ps, func(i, j int) bool { return ps[i].GetPurgingCl().GetClid() < ps[j].GetPurgingCl().GetClid() }) 278 So(ps[0].GetPurgingCl(), ShouldResembleProto, state2.PB.GetPurgingCls()[0]) // CL#1 279 So(ps[0].GetLuciProject(), ShouldEqual, lProject) 280 So(ps[1].GetPurgingCl(), ShouldResembleProto, state2.PB.GetPurgingCls()[1]) // CL#3 281 }) 282 }) 283 284 Convey("trigger CL Deps", func() { 285 markComponentsForTriage(1, 2, 3) 286 h.ComponentTriage = func(_ context.Context, c *prjpb.Component, _ itriager.PMState) (itriager.Result, error) { 287 switch clid := c.GetClids()[0]; clid { 288 case 3: 289 return itriager.Result{CLsToTriggerDeps: []*prjpb.TriggeringCLDeps{{ 290 OriginClid: 3, 291 DepClids: []int64{1, 2}, 292 }}}, nil 293 case 1, 2: 294 return itriager.Result{}, nil 295 } 296 panic("unreachable") 297 } 298 actions, saveForDebug, err := h.triageComponents(ctx, state) 299 So(err, ShouldBeNil) 300 So(saveForDebug, ShouldBeFalse) 301 So(actions, ShouldHaveLength, 3) 302 So(state.PB, ShouldResembleProto, pb) 303 304 Convey("ExecDeferred", func() { 305 state2, sideEffects, err := h.ExecDeferred(ctx, state) 306 So(err, ShouldBeNil) 307 expectedDeadline := timestamppb.New(now.Add(prjpb.MaxTriggeringCLDepsDuration)) 308 So(state2.PB.GetTriggeringClDeps(), ShouldHaveLength, 1) 309 So(state2.PB.GetTriggeringClDeps()[0], ShouldResembleProto, &prjpb.TriggeringCLDeps{ 310 OriginClid: 3, 311 DepClids: []int64{1, 2}, 312 OperationId: fmt.Sprintf("%d-3", expectedDeadline.AsTime().Unix()), 313 Deadline: expectedDeadline, 314 }) 315 316 sideEffect := sideEffects.(*SideEffects).items[0] 317 So(sideEffect, ShouldHaveSameTypeAs, &ScheduleTriggeringCLDepsTasks{}) 318 ts := sideEffect.(*ScheduleTriggeringCLDepsTasks).payloads 319 So(ts, ShouldHaveLength, 1) 320 // Sort tasks. Tasks aren't necessarily sorted. 321 sort.Slice(ts, func(i, j int) bool { 322 lhs := ts[i].GetTriggeringClDeps().GetOriginClid() 323 rhs := ts[j].GetTriggeringClDeps().GetOriginClid() 324 return lhs < rhs 325 }) 326 So(ts, ShouldHaveLength, 1) 327 So(ts[0].GetLuciProject(), ShouldEqual, lProject) 328 So(ts[0].GetTriggeringClDeps(), ShouldResembleProto, 329 state2.PB.GetTriggeringClDeps()[0]) 330 }) 331 }) 332 333 Convey("partial failure in triage", func() { 334 markComponentsForTriage(1, 2, 3) 335 h.ComponentTriage = func(_ context.Context, c *prjpb.Component, _ itriager.PMState) (itriager.Result, error) { 336 switch c.GetClids()[0] { 337 case 1: 338 return itriager.Result{}, errors.New("oops1") 339 case 2, 3: 340 return itriager.Result{NewValue: markTriaged(c)}, nil 341 } 342 panic("unreachable") 343 } 344 actions, saveForDebug, err := h.triageComponents(ctx, state) 345 So(err, ShouldBeNil) 346 So(saveForDebug, ShouldBeFalse) 347 So(actions, ShouldHaveLength, 2) 348 So(state.PB, ShouldResembleProto, pb) 349 350 Convey("ExecDeferred", func() { 351 // Execute slightly after #1 component decision time. 352 ct.Clock.Set(pb.Components[1].DecisionTime.AsTime().Add(time.Microsecond)) 353 state2, sideEffect, err := h.ExecDeferred(ctx, state) 354 So(err, ShouldBeNil) 355 So(sideEffect, ShouldBeNil) 356 pb.Components[2].TriageRequired = false 357 pb.Components[3].TriageRequired = false 358 pb.NextEvalTime = timestamppb.New(ct.Clock.Now()) // re-triage ASAP. 359 So(state2.PB, ShouldResembleProto, pb) 360 // Self-poke task must be scheduled for earliest possible from now. 361 So(pmtest.ETAsWithin(ct.TQ.Tasks(), lProject, time.Second, ct.Clock.Now().Add(prjpb.PMTaskInterval)), ShouldNotBeEmpty) 362 }) 363 }) 364 365 Convey("outdated PMState detected during triage", func() { 366 markComponentsForTriage(1, 2, 3) 367 h.ComponentTriage = func(_ context.Context, c *prjpb.Component, _ itriager.PMState) (itriager.Result, error) { 368 switch c.GetClids()[0] { 369 case 1: 370 return itriager.Result{}, errors.Annotate(itriager.ErrOutdatedPMState, "smth changed").Err() 371 case 2, 3: 372 return itriager.Result{NewValue: markTriaged(c)}, nil 373 } 374 panic("unreachable") 375 } 376 actions, saveForDebug, err := h.triageComponents(ctx, state) 377 So(err, ShouldBeNil) 378 So(saveForDebug, ShouldBeFalse) 379 So(actions, ShouldHaveLength, 2) 380 So(state.PB, ShouldResembleProto, pb) 381 382 Convey("ExecDeferred", func() { 383 state2, sideEffect, err := h.ExecDeferred(ctx, state) 384 So(err, ShouldBeNil) 385 So(sideEffect, ShouldBeNil) 386 pb.Components[2].TriageRequired = false 387 pb.Components[3].TriageRequired = false 388 pb.NextEvalTime = timestamppb.New(ct.Clock.Now()) // re-triage ASAP. 389 So(state2.PB, ShouldResembleProto, pb) 390 // Self-poke task must be scheduled for earliest possible from now. 391 So(pmtest.ETAsWithin(ct.TQ.Tasks(), lProject, time.Second, ct.Clock.Now().Add(prjpb.PMTaskInterval)), ShouldNotBeEmpty) 392 }) 393 }) 394 395 Convey("100% failure in triage", func() { 396 markComponentsForTriage(1, 2) 397 h.ComponentTriage = func(_ context.Context, _ *prjpb.Component, _ itriager.PMState) (itriager.Result, error) { 398 return itriager.Result{}, errors.New("oops") 399 } 400 _, _, err := h.triageComponents(ctx, state) 401 So(err, ShouldErrLike, "failed to triage 2 components") 402 So(state.PB, ShouldResembleProto, pb) 403 404 Convey("ExecDeferred", func() { 405 state2, sideEffect, err := h.ExecDeferred(ctx, state) 406 So(err, ShouldNotBeNil) 407 So(sideEffect, ShouldBeNil) 408 So(state2, ShouldBeNil) 409 }) 410 }) 411 412 Convey("Catches panic in triage", func() { 413 markComponentsForTriage(1) 414 h.ComponentTriage = func(_ context.Context, _ *prjpb.Component, _ itriager.PMState) (itriager.Result, error) { 415 panic(errors.New("oops")) 416 } 417 _, _, err := h.ExecDeferred(ctx, state) 418 So(err, ShouldErrLike, errCaughtPanic) 419 So(state.PB, ShouldResembleProto, pb) 420 }) 421 422 Convey("With Run Creation", func() { 423 // Run creation requires ProjectStateOffload entity to exist. 424 So(datastore.Put(ctx, &prjmanager.ProjectStateOffload{ 425 ConfigHash: prjcfgtest.MustExist(ctx, lProject).ConfigGroupIDs[0].Hash(), 426 Project: datastore.MakeKey(ctx, prjmanager.ProjectKind, lProject), 427 Status: prjpb.Status_STARTED, 428 }), ShouldBeNil) 429 430 makeRunCreator := func(clid int64, fail bool) *runcreator.Creator { 431 cfgGroups, err := prjcfgtest.MustExist(ctx, lProject).GetConfigGroups(ctx) 432 if err != nil { 433 panic(err) 434 } 435 ci := gf.CI(int(clid), gf.PS(1), gf.Owner("user-1"), gf.CQ(+1, ct.Clock.Now(), gf.U("user-2"))) 436 cl := &changelist.CL{ 437 ID: common.CLID(clid), 438 EVersion: 1, 439 Snapshot: &changelist.Snapshot{Kind: &changelist.Snapshot_Gerrit{Gerrit: &changelist.Gerrit{ 440 Host: "gerrit-review.example.com", 441 Info: ci, 442 }}}, 443 } 444 if fail { 445 // Simulate EVersion mismatch to fail run creation. 446 cl.EVersion = 2 447 } 448 err = datastore.Put(ctx, cl) 449 if err != nil { 450 panic(err) 451 } 452 cl.EVersion = 1 453 return &runcreator.Creator{ 454 LUCIProject: lProject, 455 ConfigGroupID: cfgGroups[0].ID, 456 Mode: run.DryRun, 457 OperationID: fmt.Sprintf("op-%d-%t", clid, fail), 458 Owner: identity.Identity("user:user-1@example.com"), 459 CreatedBy: identity.Identity("user:user-2@example.com"), 460 BilledTo: identity.Identity("user:user-2@example.com"), 461 Options: &run.Options{}, 462 InputCLs: []runcreator.CL{{ 463 ID: common.CLID(clid), 464 ExpectedEVersion: 1, 465 Snapshot: cl.Snapshot, 466 TriggerInfo: trigger.Find(&trigger.FindInput{ 467 ChangeInfo: ci, 468 ConfigGroup: cfgGroups[0].Content, 469 }).GetCqVoteTrigger(), 470 }}, 471 } 472 } 473 474 findRunOf := func(clid int) *run.Run { 475 switch runs, _, err := (runquery.CLQueryBuilder{CLID: common.CLID(clid)}).LoadRuns(ctx); { 476 case err != nil: 477 panic(err) 478 case len(runs) == 0: 479 return nil 480 case len(runs) > 1: 481 panic(fmt.Errorf("%d Runs for given CL", len(runs))) 482 default: 483 return runs[0] 484 } 485 } 486 487 Convey("100% success", func() { 488 markComponentsForTriage(1) 489 h.ComponentTriage = func(_ context.Context, c *prjpb.Component, _ itriager.PMState) (itriager.Result, error) { 490 rc := makeRunCreator(1, false /* succeed */) 491 return itriager.Result{NewValue: markTriaged(c), RunsToCreate: []*runcreator.Creator{rc}}, nil 492 } 493 494 state2, sideEffect, err := h.ExecDeferred(ctx, state) 495 So(err, ShouldBeNil) 496 So(sideEffect, ShouldBeNil) 497 pb.Components[1].TriageRequired = false // must be saved, since Run Creation succeeded. 498 So(state2.PB, ShouldResembleProto, pb) 499 So(findRunOf(1), ShouldNotBeNil) 500 }) 501 502 Convey("100% failure", func() { 503 markComponentsForTriage(1) 504 h.ComponentTriage = func(_ context.Context, c *prjpb.Component, _ itriager.PMState) (itriager.Result, error) { 505 rc := makeRunCreator(1, true /* fail */) 506 return itriager.Result{NewValue: markTriaged(c), RunsToCreate: []*runcreator.Creator{rc}}, nil 507 } 508 509 _, sideEffect, err := h.ExecDeferred(ctx, state) 510 So(err, ShouldErrLike, "failed to actOnComponents") 511 So(sideEffect, ShouldBeNil) 512 So(findRunOf(1), ShouldBeNil) 513 }) 514 515 Convey("Partial failure", func() { 516 markComponentsForTriage(1, 2, 3) 517 h.ComponentTriage = func(_ context.Context, c *prjpb.Component, _ itriager.PMState) (itriager.Result, error) { 518 clid := c.GetClids()[0] 519 // Set up each component trying to create a Run, 520 // and #2 and #3 additionally purging a CL, 521 // but #2 failing to create a Run. 522 failIf := clid == 2 523 rc := makeRunCreator(clid, failIf) 524 res := itriager.Result{NewValue: markTriaged(c), RunsToCreate: []*runcreator.Creator{rc}} 525 if clid != 1 { 526 // Contrived example, since in practice purging a CL concurrently 527 // with Run creation in the same component ought to happen only iff 528 // there are several CLs and presumably on different CLs. 529 res.CLsToPurge = []*prjpb.PurgeCLTask{ 530 { 531 PurgingCl: &prjpb.PurgingCL{ 532 Clid: clid, 533 ApplyTo: &prjpb.PurgingCL_AllActiveTriggers{AllActiveTriggers: true}, 534 }, 535 PurgeReasons: []*prjpb.PurgeReason{{ 536 ClError: &changelist.CLError{ 537 Kind: &changelist.CLError_OwnerLacksEmail{OwnerLacksEmail: true}, 538 }, 539 ApplyTo: &prjpb.PurgeReason_AllActiveTriggers{AllActiveTriggers: true}, 540 }}, 541 }, 542 } 543 } 544 return res, nil 545 } 546 547 state2, sideEffects, err := h.ExecDeferred(ctx, state) 548 So(err, ShouldBeNil) 549 // Only #3 component purge must be a SideEffect. 550 sideEffect := sideEffects.(*SideEffects).items[0] 551 So(sideEffect, ShouldHaveSameTypeAs, &TriggerPurgeCLTasks{}) 552 ps := sideEffect.(*TriggerPurgeCLTasks).payloads 553 So(ps, ShouldHaveLength, 1) 554 So(ps[0].GetPurgingCl().GetClid(), ShouldEqual, 3) 555 556 So(findRunOf(1), ShouldNotBeNil) 557 pb.Components[1].TriageRequired = false 558 // Component #2 must remain unchanged. 559 So(findRunOf(3), ShouldNotBeNil) 560 pb.Components[3].TriageRequired = false 561 pb.PurgingCls = []*prjpb.PurgingCL{ 562 { 563 Clid: 3, OperationId: "1580640000-3", 564 Deadline: timestamppb.New(ct.Clock.Now().Add(maxPurgingCLDuration)), 565 ApplyTo: &prjpb.PurgingCL_AllActiveTriggers{AllActiveTriggers: true}, 566 }, 567 } 568 pb.NextEvalTime = timestamppb.New(ct.Clock.Now()) // re-triage ASAP. 569 So(state2.PB, ShouldResembleProto, pb) 570 }) 571 572 Convey("Catches panic", func() { 573 markComponentsForTriage(1) 574 h.ComponentTriage = func(_ context.Context, c *prjpb.Component, _ itriager.PMState) (itriager.Result, error) { 575 rc := makeRunCreator(1, false) 576 rc.LUCIProject = "" // causes panic because of incorrect usage. 577 return itriager.Result{NewValue: markTriaged(c), RunsToCreate: []*runcreator.Creator{rc}}, nil 578 } 579 580 _, _, err := h.ExecDeferred(ctx, state) 581 So(err, ShouldErrLike, errCaughtPanic) 582 So(state.PB, ShouldResembleProto, pb) 583 }) 584 }) 585 }) 586 }