go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/run/impl/handler/common_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 handler 16 17 import ( 18 "context" 19 "sort" 20 "strings" 21 "sync" 22 "testing" 23 "time" 24 25 "google.golang.org/protobuf/types/known/timestamppb" 26 27 "go.chromium.org/luci/gae/service/datastore" 28 "go.chromium.org/luci/server/quota/quotapb" 29 "go.chromium.org/luci/server/tq/tqtesting" 30 31 "go.chromium.org/luci/common/errors" 32 cfgpb "go.chromium.org/luci/cv/api/config/v2" 33 apipb "go.chromium.org/luci/cv/api/v1" 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" 40 gf "go.chromium.org/luci/cv/internal/gerrit/gerritfake" 41 "go.chromium.org/luci/cv/internal/gerrit/trigger" 42 "go.chromium.org/luci/cv/internal/metrics" 43 "go.chromium.org/luci/cv/internal/prjmanager" 44 "go.chromium.org/luci/cv/internal/prjmanager/pmtest" 45 "go.chromium.org/luci/cv/internal/run" 46 "go.chromium.org/luci/cv/internal/run/bq" 47 "go.chromium.org/luci/cv/internal/run/eventpb" 48 "go.chromium.org/luci/cv/internal/run/impl/state" 49 "go.chromium.org/luci/cv/internal/run/postaction" 50 "go.chromium.org/luci/cv/internal/run/pubsub" 51 "go.chromium.org/luci/cv/internal/run/rdb" 52 "go.chromium.org/luci/cv/internal/run/runtest" 53 "go.chromium.org/luci/cv/internal/tryjob" 54 55 . "github.com/smartystreets/goconvey/convey" 56 . "go.chromium.org/luci/common/testing/assertions" 57 ) 58 59 func TestEndRun(t *testing.T) { 60 t.Parallel() 61 62 Convey("EndRun", t, func() { 63 ct := cvtesting.Test{} 64 ctx, cancel := ct.SetUp(t) 65 defer cancel() 66 67 const ( 68 clid = 1 69 lProject = "infra" 70 ) 71 prjcfgtest.Create(ctx, lProject, &cfgpb.Config{ 72 ConfigGroups: []*cfgpb.ConfigGroup{ 73 { 74 Name: "main", 75 PostActions: []*cfgpb.ConfigGroup_PostAction{ 76 { 77 Name: "run-verification-label", 78 Conditions: []*cfgpb.ConfigGroup_PostAction_TriggeringCondition{ 79 { 80 Mode: string(run.DryRun), 81 Statuses: []apipb.Run_Status{apipb.Run_FAILED}, 82 }, 83 }, 84 }, 85 }, 86 }, 87 }, 88 }) 89 cgs, err := prjcfgtest.MustExist(ctx, lProject).GetConfigGroups(ctx) 90 So(err, ShouldBeNil) 91 cg := cgs[0] 92 93 // mock a CL with two onoging Runs. 94 rids := common.RunIDs{ 95 common.MakeRunID(lProject, ct.Clock.Now(), 1, []byte("deadbeef")), 96 common.MakeRunID(lProject, ct.Clock.Now(), 1, []byte("cafecafe")), 97 } 98 sort.Sort(rids) 99 cl := changelist.CL{ 100 ID: clid, 101 EVersion: 3, 102 IncompleteRuns: rids, 103 UpdateTime: ct.Clock.Now().UTC(), 104 } 105 So(datastore.Put(ctx, &cl), ShouldBeNil) 106 107 // mock some child runs of rids[0] 108 childRunID := common.MakeRunID(lProject, ct.Clock.Now(), 1, []byte("child")) 109 childRun := run.Run{ 110 ID: childRunID, 111 DepRuns: common.RunIDs{rids[0]}, 112 } 113 finChildRunID := common.MakeRunID(lProject, ct.Clock.Now(), 1, []byte("finchild")) 114 finChildRun := run.Run{ 115 ID: finChildRunID, 116 DepRuns: common.RunIDs{rids[0]}, 117 Status: run.Status_FAILED, 118 } 119 So(datastore.Put(ctx, &childRun, &finChildRun), ShouldBeNil) 120 121 rs := &state.RunState{ 122 Run: run.Run{ 123 ID: rids[0], 124 Status: run.Status_RUNNING, 125 ConfigGroupID: cg.ID, 126 CreateTime: ct.Clock.Now().Add(-2 * time.Minute), 127 StartTime: ct.Clock.Now().Add(-1 * time.Minute), 128 Mode: run.DryRun, 129 CLs: common.CLIDs{1}, 130 OngoingLongOps: &run.OngoingLongOps{ 131 Ops: map[string]*run.OngoingLongOps_Op{ 132 "11-22": { 133 CancelRequested: false, 134 Work: &run.OngoingLongOps_Op_PostStartMessage{PostStartMessage: true}, 135 }, 136 }, 137 }, 138 }, 139 } 140 141 impl, deps := makeImpl(&ct) 142 se := impl.endRun(ctx, rs, run.Status_FAILED, cg, []*run.Run{&childRun, &finChildRun}) 143 So(rs.Status, ShouldEqual, run.Status_FAILED) 144 So(rs.EndTime, ShouldEqual, ct.Clock.Now()) 145 So(datastore.RunInTransaction(ctx, se, nil), ShouldBeNil) 146 147 Convey("removeRunFromCLs", func() { 148 // fetch the updated CL entity. 149 cl = changelist.CL{ID: clid} 150 So(datastore.Get(ctx, &cl), ShouldBeNil) 151 152 // it should have removed the ended Run, but not the other 153 // ongoing Run from the CL entity. 154 So(cl.IncompleteRuns, ShouldResemble, common.RunIDs{rids[1]}) 155 Convey("schedule CLUpdate for the removed Run", func() { 156 ct.TQ.Run(ctx, tqtesting.StopAfterTask(changelist.BatchOnCLUpdatedTaskClass)) 157 pmtest.AssertReceivedRunFinished(ctx, rids[0], rs.Status) 158 pmtest.AssertReceivedCLsNotified(ctx, rids[0].LUCIProject(), []*changelist.CL{&cl}) 159 So(deps.clUpdater.refreshedCLs, ShouldResemble, common.MakeCLIDs(clid)) 160 }) 161 }) 162 163 Convey("child runs get ParentRunCompleted events.", func() { 164 runtest.AssertReceivedParentRunCompleted(ctx, childRunID) 165 runtest.AssertNotReceivedParentRunCompleted(ctx, finChildRunID) 166 }) 167 168 Convey("cancel ongoing LongOps", func() { 169 So(rs.OngoingLongOps.GetOps()["11-22"].GetCancelRequested(), ShouldBeTrue) 170 }) 171 172 Convey("populate metrics for run events", func() { 173 fset1 := []any{ 174 lProject, "main", string(run.DryRun), 175 apipb.Run_FAILED.String(), true, 176 } 177 fset2 := fset1[0 : len(fset1)-1] 178 So(ct.TSMonSentValue(ctx, metrics.Public.RunEnded, fset1...), ShouldEqual, 1) 179 So(ct.TSMonSentDistr(ctx, metrics.Public.RunDuration, fset2...).Sum(), 180 ShouldAlmostEqual, (1 * time.Minute).Seconds()) 181 So(ct.TSMonSentDistr(ctx, metrics.Public.RunTotalDuration, fset1...).Sum(), 182 ShouldAlmostEqual, (2 * time.Minute).Seconds()) 183 }) 184 185 Convey("publish RunEnded event", func() { 186 var task *pubsub.PublishRunEndedTask 187 for _, t := range ct.TQ.Tasks() { 188 if p, ok := t.Payload.(*pubsub.PublishRunEndedTask); ok { 189 task = p 190 break 191 } 192 } 193 So(task, ShouldResembleProto, &pubsub.PublishRunEndedTask{ 194 PublicId: rs.ID.PublicID(), 195 LuciProject: rs.ID.LUCIProject(), 196 Status: rs.Status, 197 Eversion: int64(rs.EVersion + 1), 198 }) 199 }) 200 201 Convey("enqueue long-ops for PostAction", func() { 202 postActions := make([]*run.OngoingLongOps_Op_ExecutePostActionPayload, 0, len(rs.OngoingLongOps.GetOps())) 203 for _, op := range rs.OngoingLongOps.GetOps() { 204 if act := op.GetExecutePostAction(); act != nil { 205 d := timestamppb.New(ct.Clock.Now().UTC().Add(maxPostActionExecutionDuration)) 206 So(op.GetDeadline(), ShouldResembleProto, d) 207 So(op.GetCancelRequested(), ShouldBeFalse) 208 postActions = append(postActions, act) 209 } 210 } 211 sort.Slice(postActions, func(i, j int) bool { 212 return strings.Compare(postActions[i].GetName(), postActions[j].GetName()) < 0 213 }) 214 215 So(postActions, ShouldResembleProto, []*run.OngoingLongOps_Op_ExecutePostActionPayload{ 216 { 217 Name: postaction.CreditRunQuotaPostActionName, 218 Kind: &run.OngoingLongOps_Op_ExecutePostActionPayload_CreditRunQuota_{ 219 CreditRunQuota: &run.OngoingLongOps_Op_ExecutePostActionPayload_CreditRunQuota{}, 220 }, 221 }, 222 { 223 Name: "run-verification-label", 224 Kind: &run.OngoingLongOps_Op_ExecutePostActionPayload_ConfigAction{ 225 ConfigAction: &cfgpb.ConfigGroup_PostAction{ 226 Name: "run-verification-label", 227 Conditions: []*cfgpb.ConfigGroup_PostAction_TriggeringCondition{ 228 { 229 Mode: string(run.DryRun), 230 Statuses: []apipb.Run_Status{apipb.Run_FAILED}, 231 }, 232 }, 233 }, 234 }, 235 }, 236 }) 237 }) 238 }) 239 } 240 241 func TestCheckRunCreate(t *testing.T) { 242 t.Parallel() 243 Convey("CheckRunCreate", t, func() { 244 ct := &cvtesting.Test{} 245 ctx, cancel := ct.SetUp(t) 246 defer cancel() 247 const clid1 = 1 248 const clid2 = 2 249 const gHost = "x-review.example.com" 250 const gRepo = "luci-go" 251 const gChange1 = 123 252 const gChange2 = 234 253 const lProject = "infra" 254 255 prjcfgtest.Create(ctx, lProject, &cfgpb.Config{ 256 ConfigGroups: []*cfgpb.ConfigGroup{ 257 { 258 Name: "main", 259 Gerrit: []*cfgpb.ConfigGroup_Gerrit{{ 260 Url: "https://" + gHost, 261 Projects: []*cfgpb.ConfigGroup_Gerrit_Project{ 262 {Name: gRepo, RefRegexp: []string{"refs/heads/.+"}}, 263 }, 264 }}, 265 Verifiers: &cfgpb.Verifiers{ 266 GerritCqAbility: &cfgpb.Verifiers_GerritCQAbility{ 267 CommitterList: []string{"committer-group"}, 268 }, 269 }, 270 }, 271 }, 272 }) 273 cgs, err := prjcfgtest.MustExist(ctx, lProject).GetConfigGroups(ctx) 274 So(err, ShouldBeNil) 275 276 cg := cgs[0] 277 278 rid := common.MakeRunID("infra", ct.Clock.Now(), 1, []byte("deadbeef")) 279 rs := &state.RunState{ 280 Run: run.Run{ 281 ID: rid, 282 Status: run.Status_RUNNING, 283 ConfigGroupID: prjcfg.MakeConfigGroupID("deadbeef", "main"), 284 CreateTime: ct.Clock.Now().Add(-2 * time.Minute), 285 StartTime: ct.Clock.Now().Add(-1 * time.Minute), 286 CLs: common.CLIDs{clid1, clid2}, 287 }, 288 } 289 cls := []*changelist.CL{ 290 { 291 ID: clid1, 292 ExternalID: changelist.MustGobID(gHost, gChange1), 293 IncompleteRuns: common.RunIDs{rid}, 294 EVersion: 3, 295 UpdateTime: ct.Clock.Now().UTC(), 296 Snapshot: &changelist.Snapshot{ 297 Kind: &changelist.Snapshot_Gerrit{Gerrit: &changelist.Gerrit{ 298 Host: gHost, 299 Info: gf.CI(gChange1, 300 gf.Owner("user-1"), 301 gf.Approve(), 302 gf.CQ(+2, rs.CreateTime, "user-1"), 303 ), 304 }}, 305 LuciProject: lProject, 306 ExternalUpdateTime: timestamppb.New(ct.Clock.Now()), 307 }, 308 }, 309 { 310 ID: clid2, 311 ExternalID: changelist.MustGobID(gHost, gChange2), 312 IncompleteRuns: common.RunIDs{rid}, 313 EVersion: 5, 314 UpdateTime: ct.Clock.Now().UTC(), 315 Snapshot: &changelist.Snapshot{ 316 Kind: &changelist.Snapshot_Gerrit{Gerrit: &changelist.Gerrit{ 317 Host: gHost, 318 Info: gf.CI(gChange2, 319 gf.Owner("user-1"), 320 gf.Approve(), 321 gf.CQ(+2, rs.CreateTime, "user-1"), 322 ), 323 }}, 324 LuciProject: lProject, 325 ExternalUpdateTime: timestamppb.New(ct.Clock.Now()), 326 }, 327 }, 328 } 329 rcls := []*run.RunCL{ 330 { 331 ID: clid1, 332 Run: datastore.MakeKey(ctx, common.RunKind, string(rid)), 333 ExternalID: cls[0].ExternalID, 334 Detail: cls[0].Snapshot, 335 Trigger: trigger.Find(&trigger.FindInput{ 336 ChangeInfo: cls[0].Snapshot.GetGerrit().GetInfo(), 337 ConfigGroup: cg.Content, 338 }).GetCqVoteTrigger(), 339 }, 340 { 341 ID: clid2, 342 Run: datastore.MakeKey(ctx, common.RunKind, string(rid)), 343 ExternalID: cls[1].ExternalID, 344 Detail: cls[1].Snapshot, 345 Trigger: trigger.Find(&trigger.FindInput{ 346 ChangeInfo: cls[1].Snapshot.GetGerrit().GetInfo(), 347 ConfigGroup: cg.Content, 348 }).GetCqVoteTrigger(), 349 }, 350 } 351 So(datastore.Put(ctx, cls, rcls), ShouldBeNil) 352 353 Convey("Returns empty metas for new patchset run", func() { 354 rs.Mode = run.NewPatchsetRun 355 ok, err := checkRunCreate(ctx, rs, cg, rcls, cls) 356 So(err, ShouldBeNil) 357 So(ok, ShouldBeFalse) 358 So(rs.OngoingLongOps.Ops, ShouldHaveLength, 1) 359 for _, op := range rs.OngoingLongOps.Ops { 360 reqs := op.GetResetTriggers().GetRequests() 361 So(reqs, ShouldHaveLength, 2) 362 So(reqs[0].Clid, ShouldEqual, clid1) 363 So(reqs[0].Message, ShouldEqual, "") 364 So(reqs[0].AddToAttention, ShouldBeEmpty) 365 So(reqs[1].Clid, ShouldEqual, clid2) 366 So(reqs[1].Message, ShouldEqual, "") 367 So(reqs[1].AddToAttention, ShouldBeEmpty) 368 } 369 }) 370 Convey("Populates metas for other modes", func() { 371 rs.Mode = run.FullRun 372 ok, err := checkRunCreate(ctx, rs, cg, rcls, cls) 373 So(err, ShouldBeNil) 374 So(ok, ShouldBeFalse) 375 So(rs.OngoingLongOps.Ops, ShouldHaveLength, 1) 376 for _, op := range rs.OngoingLongOps.Ops { 377 reqs := op.GetResetTriggers().GetRequests() 378 So(reqs, ShouldHaveLength, 2) 379 So(reqs[0].Clid, ShouldEqual, clid1) 380 So(reqs[0].Message, ShouldEqual, "CV cannot start a Run for `user-1@example.com` because the user is not a committer.") 381 So(reqs[0].AddToAttention, ShouldResemble, []gerrit.Whom{ 382 gerrit.Whom_OWNER, 383 gerrit.Whom_CQ_VOTERS}) 384 So(reqs[1].Clid, ShouldEqual, clid2) 385 So(reqs[1].Message, ShouldEqual, "CV cannot start a Run for `user-1@example.com` because the user is not a committer.") 386 So(reqs[1].AddToAttention, ShouldResemble, []gerrit.Whom{ 387 gerrit.Whom_OWNER, 388 gerrit.Whom_CQ_VOTERS}) 389 } 390 }) 391 392 Convey("Populates metas if run has root CL", func() { 393 rs.Mode = run.FullRun 394 rs.RootCL = clid1 395 // make rootCL not submittable 396 cls[0].Snapshot.GetGerrit().Info = gf.CI(gChange1, 397 gf.Owner("user-1"), 398 gf.Disapprove(), 399 gf.CQ(+2, rs.CreateTime, "user-1"), 400 ) 401 // Remove the trigger on second CL 402 cls[1].Snapshot.GetGerrit().Info = gf.CI(gChange2, 403 gf.Owner("user-1"), 404 gf.Approve(), 405 ) 406 rcls[0].Detail = cls[0].Snapshot 407 rcls[1].Detail = cls[1].Snapshot 408 So(datastore.Put(ctx, cls, rcls), ShouldBeNil) 409 410 Convey("Only root CL fails the ACL check", func() { 411 ct.AddMember("user-1", "committer-group") // can create a full run on cl2 now 412 ok, err := checkRunCreate(ctx, rs, cg, rcls, cls) 413 So(err, ShouldBeNil) 414 So(ok, ShouldBeFalse) 415 So(rs.OngoingLongOps.Ops, ShouldHaveLength, 1) 416 for _, op := range rs.OngoingLongOps.Ops { 417 reqs := op.GetResetTriggers().GetRequests() 418 So(reqs, ShouldHaveLength, 1) 419 So(reqs[0].Clid, ShouldEqual, clid1) 420 So(reqs[0].Message, ShouldContainSubstring, "CV cannot start a Run because this CL is not submittable") 421 So(reqs[0].AddToAttention, ShouldResemble, []gerrit.Whom{ 422 gerrit.Whom_OWNER, 423 gerrit.Whom_CQ_VOTERS}) 424 } 425 }) 426 427 Convey("Non root CL also fails the ACL check", func() { 428 ok, err := checkRunCreate(ctx, rs, cg, rcls, cls) 429 So(err, ShouldBeNil) 430 So(ok, ShouldBeFalse) 431 So(rs.OngoingLongOps.Ops, ShouldHaveLength, 1) 432 for _, op := range rs.OngoingLongOps.Ops { 433 reqs := op.GetResetTriggers().GetRequests() 434 So(reqs, ShouldHaveLength, 1) 435 So(reqs[0].Clid, ShouldEqual, clid1) 436 So(reqs[0].Message, ShouldContainSubstring, "can not start the Run due to following errors") 437 So(reqs[0].AddToAttention, ShouldResemble, []gerrit.Whom{ 438 gerrit.Whom_OWNER, 439 gerrit.Whom_CQ_VOTERS}) 440 } 441 }) 442 }) 443 }) 444 } 445 446 type dependencies struct { 447 pm *prjmanager.Notifier 448 rm *run.Notifier 449 qm *quotaManagerMock 450 tjNotifier *tryjobNotifierMock 451 clUpdater *clUpdaterMock 452 } 453 454 type testHandler struct { 455 inner Handler 456 } 457 458 func validateStateMutation(passed, initialCopy, result *state.RunState) { 459 switch { 460 case cvtesting.SafeShouldResemble(result, initialCopy) == "": 461 // No state change; doesn't matter whether shallow copy is created or not. 462 return 463 case passed == result: 464 So(errors.New("handler mutated the input state but doesn't create a shallow copy before mutation"), ShouldBeNil) 465 case cvtesting.SafeShouldResemble(initialCopy, passed) != "": 466 So(errors.New("handler created a shallow copy but modified addressable property in place; forgot to clone a proto?"), ShouldBeNil) 467 } 468 } 469 470 func (t *testHandler) Start(ctx context.Context, rs *state.RunState) (*Result, error) { 471 initialCopy := rs.DeepCopy() 472 res, err := t.inner.Start(ctx, rs) 473 if err != nil { 474 return nil, err 475 } 476 validateStateMutation(rs, initialCopy, res.State) 477 return res, err 478 } 479 480 func (t *testHandler) Cancel(ctx context.Context, rs *state.RunState, reasons []string) (*Result, error) { 481 initialCopy := rs.DeepCopy() 482 res, err := t.inner.Cancel(ctx, rs, reasons) 483 if err != nil { 484 return nil, err 485 } 486 validateStateMutation(rs, initialCopy, res.State) 487 return res, err 488 } 489 490 func (t *testHandler) OnCLsUpdated(ctx context.Context, rs *state.RunState, cls common.CLIDs) (*Result, error) { 491 initialCopy := rs.DeepCopy() 492 res, err := t.inner.OnCLsUpdated(ctx, rs, cls) 493 if err != nil { 494 return nil, err 495 } 496 validateStateMutation(rs, initialCopy, res.State) 497 return res, err 498 } 499 500 func (t *testHandler) UpdateConfig(ctx context.Context, rs *state.RunState, ver string) (*Result, error) { 501 initialCopy := rs.DeepCopy() 502 res, err := t.inner.UpdateConfig(ctx, rs, ver) 503 if err != nil { 504 return nil, err 505 } 506 validateStateMutation(rs, initialCopy, res.State) 507 return res, err 508 } 509 510 func (t *testHandler) OnReadyForSubmission(ctx context.Context, rs *state.RunState) (*Result, error) { 511 initialCopy := rs.DeepCopy() 512 res, err := t.inner.OnReadyForSubmission(ctx, rs) 513 if err != nil { 514 return nil, err 515 } 516 validateStateMutation(rs, initialCopy, res.State) 517 return res, err 518 } 519 520 func (t *testHandler) OnCLsSubmitted(ctx context.Context, rs *state.RunState, cls common.CLIDs) (*Result, error) { 521 initialCopy := rs.DeepCopy() 522 res, err := t.inner.OnCLsSubmitted(ctx, rs, cls) 523 if err != nil { 524 return nil, err 525 } 526 validateStateMutation(rs, initialCopy, res.State) 527 return res, err 528 } 529 530 func (t *testHandler) OnSubmissionCompleted(ctx context.Context, rs *state.RunState, sc *eventpb.SubmissionCompleted) (*Result, error) { 531 initialCopy := rs.DeepCopy() 532 res, err := t.inner.OnSubmissionCompleted(ctx, rs, sc) 533 if err != nil { 534 return nil, err 535 } 536 validateStateMutation(rs, initialCopy, res.State) 537 return res, err 538 } 539 540 func (t *testHandler) OnLongOpCompleted(ctx context.Context, rs *state.RunState, result *eventpb.LongOpCompleted) (*Result, error) { 541 initialCopy := rs.DeepCopy() 542 res, err := t.inner.OnLongOpCompleted(ctx, rs, result) 543 if err != nil { 544 return nil, err 545 } 546 validateStateMutation(rs, initialCopy, res.State) 547 return res, err 548 } 549 550 func (t *testHandler) OnTryjobsUpdated(ctx context.Context, rs *state.RunState, tryjobs common.TryjobIDs) (*Result, error) { 551 initialCopy := rs.DeepCopy() 552 res, err := t.inner.OnTryjobsUpdated(ctx, rs, tryjobs) 553 if err != nil { 554 return nil, err 555 } 556 validateStateMutation(rs, initialCopy, res.State) 557 return res, err 558 } 559 560 func (t *testHandler) TryResumeSubmission(ctx context.Context, rs *state.RunState) (*Result, error) { 561 initialCopy := rs.DeepCopy() 562 res, err := t.inner.TryResumeSubmission(ctx, rs) 563 if err != nil { 564 return nil, err 565 } 566 validateStateMutation(rs, initialCopy, res.State) 567 return res, err 568 } 569 570 func (t *testHandler) Poke(ctx context.Context, rs *state.RunState) (*Result, error) { 571 initialCopy := rs.DeepCopy() 572 res, err := t.inner.Poke(ctx, rs) 573 if err != nil { 574 return nil, err 575 } 576 validateStateMutation(rs, initialCopy, res.State) 577 return res, err 578 } 579 580 func (t *testHandler) OnParentRunCompleted(ctx context.Context, rs *state.RunState) (*Result, error) { 581 initialCopy := rs.DeepCopy() 582 res, err := t.inner.OnParentRunCompleted(ctx, rs) 583 if err != nil { 584 return nil, err 585 } 586 validateStateMutation(rs, initialCopy, res.State) 587 return res, err 588 } 589 590 func makeTestHandler(ct *cvtesting.Test) (Handler, dependencies) { 591 handler, dependencies := makeImpl(ct) 592 return &testHandler{inner: handler}, dependencies 593 } 594 595 // makeImpl should only be used to test common functions. For testing handler, 596 // please use makeTestHandler instead. 597 func makeImpl(ct *cvtesting.Test) (*Impl, dependencies) { 598 deps := dependencies{ 599 pm: prjmanager.NewNotifier(ct.TQDispatcher), 600 rm: run.NewNotifier(ct.TQDispatcher), 601 qm: "aManagerMock{}, 602 tjNotifier: &tryjobNotifierMock{}, 603 clUpdater: &clUpdaterMock{}, 604 } 605 cf := rdb.NewMockRecorderClientFactory(ct.GoMockCtl) 606 impl := &Impl{ 607 PM: deps.pm, 608 RM: deps.rm, 609 TN: deps.tjNotifier, 610 CLMutator: changelist.NewMutator(ct.TQDispatcher, deps.pm, deps.rm, nil), 611 CLUpdater: deps.clUpdater, 612 TreeClient: ct.TreeFake.Client(), 613 GFactory: ct.GFactory(), 614 BQExporter: bq.NewExporter(ct.TQDispatcher, ct.BQFake, ct.Env), 615 RdbNotifier: rdb.NewNotifier(ct.TQDispatcher, cf), 616 Publisher: pubsub.NewPublisher(ct.TQDispatcher, ct.Env), 617 QM: deps.qm, 618 Env: ct.Env, 619 } 620 return impl, deps 621 } 622 623 type clUpdaterMock struct { 624 m sync.Mutex 625 refreshedCLs common.CLIDs 626 } 627 628 func (c *clUpdaterMock) ScheduleBatch(ctx context.Context, luciProject string, cls []*changelist.CL, requester changelist.UpdateCLTask_Requester) error { 629 c.m.Lock() 630 for _, cl := range cls { 631 c.refreshedCLs = append(c.refreshedCLs, cl.ID) 632 } 633 c.m.Unlock() 634 return nil 635 } 636 637 type tryjobNotifierMock struct { 638 m sync.Mutex 639 updateScheduled common.TryjobIDs 640 } 641 642 func (t *tryjobNotifierMock) ScheduleUpdate(ctx context.Context, id common.TryjobID, _ tryjob.ExternalID) error { 643 t.m.Lock() 644 t.updateScheduled = append(t.updateScheduled, id) 645 t.m.Unlock() 646 return nil 647 } 648 649 type quotaManagerMock struct { 650 runQuotaOp *quotapb.OpResult 651 userLimit *cfgpb.UserLimit 652 runQuotaErr error 653 654 debitRunQuotaCalls int 655 } 656 657 func (qm *quotaManagerMock) DebitRunQuota(ctx context.Context, r *run.Run) (*quotapb.OpResult, *cfgpb.UserLimit, error) { 658 qm.debitRunQuotaCalls++ 659 return qm.runQuotaOp, qm.userLimit, qm.runQuotaErr 660 }