go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/run/impl/manager_test.go (about) 1 // Copyright 2020 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 impl 16 17 import ( 18 "context" 19 "fmt" 20 "math/rand" 21 "testing" 22 "time" 23 24 "google.golang.org/protobuf/types/known/timestamppb" 25 26 "go.chromium.org/luci/common/clock" 27 "go.chromium.org/luci/common/logging" 28 "go.chromium.org/luci/common/logging/memlogger" 29 "go.chromium.org/luci/gae/service/datastore" 30 "go.chromium.org/luci/server/tq/tqtesting" 31 32 cfgpb "go.chromium.org/luci/cv/api/config/v2" 33 "go.chromium.org/luci/cv/internal/changelist" 34 "go.chromium.org/luci/cv/internal/common" 35 "go.chromium.org/luci/cv/internal/common/eventbox" 36 "go.chromium.org/luci/cv/internal/configs/prjcfg/prjcfgtest" 37 "go.chromium.org/luci/cv/internal/cvtesting" 38 "go.chromium.org/luci/cv/internal/prjmanager" 39 "go.chromium.org/luci/cv/internal/quota" 40 "go.chromium.org/luci/cv/internal/run" 41 "go.chromium.org/luci/cv/internal/run/eventpb" 42 "go.chromium.org/luci/cv/internal/run/impl/handler" 43 "go.chromium.org/luci/cv/internal/run/impl/state" 44 "go.chromium.org/luci/cv/internal/run/rdb" 45 "go.chromium.org/luci/cv/internal/run/runtest" 46 "go.chromium.org/luci/cv/internal/tryjob" 47 48 . "github.com/smartystreets/goconvey/convey" 49 . "go.chromium.org/luci/common/testing/assertions" 50 ) 51 52 func TestRunManager(t *testing.T) { 53 t.Parallel() 54 55 Convey("RunManager", t, func() { 56 ct := cvtesting.Test{} 57 ctx, cancel := ct.SetUp(t) 58 defer cancel() 59 const runID = "chromium/222-1-deadbeef" 60 const initialEVersion = 10 61 So(datastore.Put(ctx, &run.Run{ 62 ID: runID, 63 Status: run.Status_RUNNING, 64 EVersion: initialEVersion, 65 }), ShouldBeNil) 66 67 currentRun := func(ctx context.Context) *run.Run { 68 ret := &run.Run{ID: runID} 69 So(datastore.Get(ctx, ret), ShouldBeNil) 70 return ret 71 } 72 73 notifier := run.NewNotifier(ct.TQDispatcher) 74 pm := prjmanager.NewNotifier(ct.TQDispatcher) 75 tjNotifier := tryjob.NewNotifier(ct.TQDispatcher) 76 clMutator := changelist.NewMutator(ct.TQDispatcher, pm, notifier, tjNotifier) 77 clUpdater := changelist.NewUpdater(ct.TQDispatcher, clMutator) 78 cf := rdb.NewMockRecorderClientFactory(ct.GoMockCtl) 79 qm := quota.NewManager() 80 _ = New(notifier, pm, tjNotifier, clMutator, clUpdater, ct.GFactory(), ct.BuildbucketFake.NewClientFactory(), ct.TreeFake.Client(), ct.BQFake, cf, qm, ct.Env) 81 82 // sorted by the order of execution. 83 eventTestcases := []struct { 84 event *eventpb.Event 85 sendFn func(context.Context) error 86 invokedHandlerMethod string 87 }{ 88 { 89 &eventpb.Event{ 90 Event: &eventpb.Event_LongOpCompleted{ 91 LongOpCompleted: &eventpb.LongOpCompleted{ 92 OperationId: "1-1", 93 }, 94 }, 95 }, 96 func(ctx context.Context) error { 97 return notifier.SendNow(ctx, runID, &eventpb.Event{ 98 Event: &eventpb.Event_LongOpCompleted{ 99 LongOpCompleted: &eventpb.LongOpCompleted{ 100 OperationId: "1-1", 101 }, 102 }, 103 }) 104 }, 105 "OnLongOpCompleted", 106 }, 107 { 108 &eventpb.Event{ 109 Event: &eventpb.Event_Cancel{ 110 Cancel: &eventpb.Cancel{ 111 Reason: "user request", 112 }, 113 }, 114 }, 115 func(ctx context.Context) error { 116 return notifier.Cancel(ctx, runID, "user request") 117 }, 118 "Cancel", 119 }, 120 { 121 &eventpb.Event{ 122 Event: &eventpb.Event_Start{ 123 Start: &eventpb.Start{}, 124 }, 125 }, 126 func(ctx context.Context) error { 127 return notifier.Start(ctx, runID) 128 }, 129 "Start", 130 }, 131 { 132 &eventpb.Event{ 133 Event: &eventpb.Event_NewConfig{ 134 NewConfig: &eventpb.NewConfig{ 135 Hash: "deadbeef", 136 Eversion: 2, 137 }, 138 }, 139 }, 140 func(ctx context.Context) error { 141 return notifier.UpdateConfig(ctx, runID, "deadbeef", 2) 142 }, 143 "UpdateConfig", 144 }, 145 { 146 &eventpb.Event{ 147 Event: &eventpb.Event_ClsUpdated{ 148 ClsUpdated: &changelist.CLUpdatedEvents{ 149 Events: []*changelist.CLUpdatedEvent{ 150 { 151 Clid: int64(1), 152 Eversion: int64(2), 153 }, 154 }, 155 }, 156 }, 157 }, 158 func(ctx context.Context) error { 159 return notifier.NotifyCLsUpdated(ctx, runID, &changelist.CLUpdatedEvents{ 160 Events: []*changelist.CLUpdatedEvent{ 161 { 162 Clid: int64(1), 163 Eversion: int64(2), 164 }, 165 }, 166 }) 167 }, 168 "OnCLsUpdated", 169 }, 170 { 171 &eventpb.Event{ 172 Event: &eventpb.Event_TryjobsUpdated{ 173 TryjobsUpdated: &tryjob.TryjobUpdatedEvents{ 174 Events: []*tryjob.TryjobUpdatedEvent{ 175 {TryjobId: 10}, 176 }, 177 }, 178 }, 179 }, 180 func(ctx context.Context) error { 181 return notifier.SendNow(ctx, runID, &eventpb.Event{ 182 Event: &eventpb.Event_TryjobsUpdated{ 183 TryjobsUpdated: &tryjob.TryjobUpdatedEvents{ 184 Events: []*tryjob.TryjobUpdatedEvent{ 185 {TryjobId: 10}, 186 }, 187 }, 188 }, 189 }) 190 }, 191 "OnTryjobsUpdated", 192 }, 193 { 194 &eventpb.Event{ 195 Event: &eventpb.Event_ClsSubmitted{ 196 ClsSubmitted: &eventpb.CLsSubmitted{ 197 Clids: []int64{1, 2}, 198 }, 199 }, 200 }, 201 func(ctx context.Context) error { 202 return notifier.SendNow(ctx, runID, &eventpb.Event{ 203 Event: &eventpb.Event_ClsSubmitted{ 204 ClsSubmitted: &eventpb.CLsSubmitted{ 205 Clids: []int64{1, 2}, 206 }, 207 }, 208 }) 209 }, 210 "OnCLsSubmitted", 211 }, 212 { 213 &eventpb.Event{ 214 Event: &eventpb.Event_SubmissionCompleted{ 215 SubmissionCompleted: &eventpb.SubmissionCompleted{ 216 Result: eventpb.SubmissionResult_SUCCEEDED, 217 }, 218 }, 219 }, 220 func(ctx context.Context) error { 221 return notifier.SendNow(ctx, runID, &eventpb.Event{ 222 Event: &eventpb.Event_SubmissionCompleted{ 223 SubmissionCompleted: &eventpb.SubmissionCompleted{ 224 Result: eventpb.SubmissionResult_SUCCEEDED, 225 }, 226 }, 227 }) 228 }, 229 "OnSubmissionCompleted", 230 }, 231 { 232 &eventpb.Event{ 233 Event: &eventpb.Event_ReadyForSubmission{ 234 ReadyForSubmission: &eventpb.ReadyForSubmission{}, 235 }, 236 }, 237 func(ctx context.Context) error { 238 return notifier.SendNow(ctx, runID, &eventpb.Event{ 239 Event: &eventpb.Event_ReadyForSubmission{ 240 ReadyForSubmission: &eventpb.ReadyForSubmission{}, 241 }, 242 }) 243 }, 244 "OnReadyForSubmission", 245 }, 246 { 247 &eventpb.Event{ 248 Event: &eventpb.Event_Poke{ 249 Poke: &eventpb.Poke{}, 250 }, 251 }, 252 func(ctx context.Context) error { 253 return notifier.PokeNow(ctx, runID) 254 }, 255 "Poke", 256 }, 257 { 258 &eventpb.Event{ 259 Event: &eventpb.Event_ParentRunCompleted{ 260 ParentRunCompleted: &eventpb.ParentRunCompleted{}, 261 }, 262 }, 263 func(ctx context.Context) error { 264 return notifier.SendNow(ctx, runID, &eventpb.Event{ 265 Event: &eventpb.Event_ParentRunCompleted{ 266 ParentRunCompleted: &eventpb.ParentRunCompleted{}, 267 }, 268 }) 269 }, 270 "OnParentRunCompleted", 271 }, 272 } 273 for _, et := range eventTestcases { 274 Convey(fmt.Sprintf("Can process Event %T", et.event.GetEvent()), func() { 275 fh := &fakeHandler{} 276 ctx = context.WithValue(ctx, &fakeHandlerKey, fh) 277 So(et.sendFn(ctx), ShouldBeNil) 278 runtest.AssertInEventbox(ctx, runID, et.event) 279 So(runtest.Runs(ct.TQ.Tasks()), ShouldResemble, common.RunIDs{runID}) 280 ct.TQ.Run(ctx, tqtesting.StopAfterTask(eventpb.ManageRunTaskClass)) 281 So(fh.invocations[0], ShouldEqual, et.invokedHandlerMethod) 282 So(currentRun(ctx).EVersion, ShouldEqual, initialEVersion+1) 283 runtest.AssertNotInEventbox(ctx, runID, et.event) // consumed 284 }) 285 } 286 287 Convey("Process Events in order", func() { 288 fh := &fakeHandler{} 289 ctx = context.WithValue(ctx, &fakeHandlerKey, fh) 290 291 var expectInvokedMethods []string 292 for _, et := range eventTestcases { 293 // skipping Cancel because when Start and Cancel are both present. 294 // only Cancel will execute. See next test 295 if et.event.GetCancel() == nil { 296 expectInvokedMethods = append(expectInvokedMethods, et.invokedHandlerMethod) 297 } 298 } 299 rand.Shuffle(len(eventTestcases), func(i, j int) { 300 eventTestcases[i], eventTestcases[j] = eventTestcases[j], eventTestcases[i] 301 }) 302 for _, etc := range eventTestcases { 303 if etc.event.GetCancel() == nil { 304 So(etc.sendFn(ctx), ShouldBeNil) 305 } 306 } 307 ct.TQ.Run(ctx, tqtesting.StopAfterTask(eventpb.ManageRunTaskClass)) 308 expectInvokedMethods = append(expectInvokedMethods, "TryResumeSubmission") // always invoked 309 So(fh.invocations, ShouldResemble, expectInvokedMethods) 310 So(currentRun(ctx).EVersion, ShouldEqual, initialEVersion+1) 311 }) 312 313 Convey("Don't Start if received both Cancel and Start Event", func() { 314 fh := &fakeHandler{} 315 ctx = context.WithValue(ctx, &fakeHandlerKey, fh) 316 notifier.Start(ctx, runID) 317 notifier.Cancel(ctx, runID, "user request") 318 ct.TQ.Run(ctx, tqtesting.StopAfterTask(eventpb.ManageRunTaskClass)) 319 So(fh.invocations[0], ShouldEqual, "Cancel") 320 for _, inv := range fh.invocations[1:] { 321 So(inv, ShouldNotEqual, "Start") 322 } 323 So(currentRun(ctx).EVersion, ShouldEqual, initialEVersion+1) 324 runtest.AssertNotInEventbox(ctx, runID, &eventpb.Event{ 325 Event: &eventpb.Event_Cancel{ 326 Cancel: &eventpb.Cancel{ 327 Reason: "user request", 328 }, 329 }, 330 }, 331 &eventpb.Event{ 332 Event: &eventpb.Event_Start{ 333 Start: &eventpb.Start{}, 334 }, 335 }, 336 ) 337 }) 338 339 Convey("Can Preserve events", func() { 340 fh := &fakeHandler{preserveEvents: true} 341 ctx = context.WithValue(ctx, &fakeHandlerKey, fh) 342 So(notifier.Start(ctx, runID), ShouldBeNil) 343 ct.TQ.Run(ctx, tqtesting.StopAfterTask(eventpb.ManageRunTaskClass)) 344 So(currentRun(ctx).EVersion, ShouldEqual, initialEVersion+1) 345 runtest.AssertInEventbox(ctx, runID, 346 &eventpb.Event{ 347 Event: &eventpb.Event_Start{ 348 Start: &eventpb.Start{}, 349 }, 350 }, 351 ) 352 }) 353 354 Convey("Can save RunLog", func() { 355 fh := &fakeHandler{startAddsLogEntries: []*run.LogEntry{ 356 { 357 Time: timestamppb.New(clock.Now(ctx)), 358 Kind: &run.LogEntry_Created_{Created: &run.LogEntry_Created{ 359 ConfigGroupId: "deadbeef/main", 360 }}, 361 }, 362 }} 363 ctx = context.WithValue(ctx, &fakeHandlerKey, fh) 364 So(notifier.Start(ctx, runID), ShouldBeNil) 365 ct.TQ.Run(ctx, tqtesting.StopAfterTask(eventpb.ManageRunTaskClass)) 366 So(currentRun(ctx).EVersion, ShouldEqual, initialEVersion+1) 367 entries, err := run.LoadRunLogEntries(ctx, runID) 368 So(err, ShouldBeNil) 369 So(entries, ShouldResembleProto, fh.startAddsLogEntries) 370 }) 371 372 Convey("Can run PostProcessFn", func() { 373 var postProcessFnExecuted bool 374 fh := &fakeHandler{ 375 postProcessFn: func(c context.Context) error { 376 postProcessFnExecuted = true 377 return nil 378 }, 379 } 380 ctx = context.WithValue(ctx, &fakeHandlerKey, fh) 381 So(notifier.Start(ctx, runID), ShouldBeNil) 382 ct.TQ.Run(ctx, tqtesting.StopAfterTask(eventpb.ManageRunTaskClass)) 383 So(postProcessFnExecuted, ShouldBeTrue) 384 }) 385 }) 386 387 Convey("Poke", t, func() { 388 ct := cvtesting.Test{} 389 ctx, cancel := ct.SetUp(t) 390 defer cancel() 391 const ( 392 lProject = "chromium" 393 dryRunners = "dry-runner-group" 394 runID = lProject + "/222-1-deadbeef" 395 ) 396 cfg := &cfgpb.Config{ 397 ConfigGroups: []*cfgpb.ConfigGroup{ 398 { 399 Name: "main", 400 Verifiers: &cfgpb.Verifiers{ 401 GerritCqAbility: &cfgpb.Verifiers_GerritCQAbility{ 402 DryRunAccessList: []string{dryRunners}, 403 }, 404 }, 405 }, 406 }, 407 } 408 prjcfgtest.Create(ctx, lProject, cfg) 409 410 tCreate := ct.Clock.Now().UTC().Add(-2 * time.Minute) 411 So(datastore.Put(ctx, &run.Run{ 412 ID: runID, 413 Status: run.Status_RUNNING, 414 CreateTime: tCreate, 415 StartTime: tCreate.Add(1 * time.Minute), 416 EVersion: 10, 417 ConfigGroupID: prjcfgtest.MustExist(ctx, lProject).ConfigGroupIDs[0], 418 }), ShouldBeNil) 419 420 notifier := run.NewNotifier(ct.TQDispatcher) 421 pm := prjmanager.NewNotifier(ct.TQDispatcher) 422 tjNotifier := tryjob.NewNotifier(ct.TQDispatcher) 423 clMutator := changelist.NewMutator(ct.TQDispatcher, pm, notifier, tjNotifier) 424 clUpdater := changelist.NewUpdater(ct.TQDispatcher, clMutator) 425 cf := rdb.NewMockRecorderClientFactory(ct.GoMockCtl) 426 qm := quota.NewManager() 427 _ = New(notifier, pm, tjNotifier, clMutator, clUpdater, ct.GFactory(), ct.BuildbucketFake.NewClientFactory(), ct.TreeFake.Client(), ct.BQFake, cf, qm, ct.Env) 428 429 Convey("Recursive", func() { 430 So(notifier.PokeNow(ctx, runID), ShouldBeNil) 431 So(runtest.Runs(ct.TQ.Tasks()), ShouldResemble, common.RunIDs{runID}) 432 ct.TQ.Run(ctx, tqtesting.StopAfterTask(eventpb.ManageRunTaskClass)) 433 for i := 0; i < 10; i++ { 434 now := clock.Now(ctx) 435 runtest.AssertInEventbox(ctx, runID, &eventpb.Event{ 436 Event: &eventpb.Event_Poke{ 437 Poke: &eventpb.Poke{}, 438 }, 439 ProcessAfter: timestamppb.New(now.Add(pokeInterval)), 440 }) 441 ct.TQ.Run(ctx, tqtesting.StopAfterTask(eventpb.ManageRunTaskClass)) 442 } 443 444 Convey("Stops after Run is finalized", func() { 445 So(datastore.Put(ctx, &run.Run{ 446 ID: runID, 447 Status: run.Status_CANCELLED, 448 CreateTime: tCreate, 449 StartTime: tCreate.Add(1 * time.Minute), 450 EndTime: ct.Clock.Now().UTC(), 451 EVersion: 11, 452 }), ShouldBeNil) 453 ct.TQ.Run(ctx, tqtesting.StopAfterTask(eventpb.ManageRunTaskClass)) 454 runtest.AssertEventboxEmpty(ctx, runID) 455 }) 456 }) 457 458 Convey("Existing event due during the interval", func() { 459 So(notifier.PokeNow(ctx, runID), ShouldBeNil) 460 So(notifier.PokeAfter(ctx, runID, 30*time.Second), ShouldBeNil) 461 ct.TQ.Run(ctx, tqtesting.StopAfterTask(eventpb.ManageRunTaskClass)) 462 463 runtest.AssertNotInEventbox(ctx, runID, &eventpb.Event{ 464 Event: &eventpb.Event_Poke{ 465 Poke: &eventpb.Poke{}, 466 }, 467 ProcessAfter: timestamppb.New(clock.Now(ctx).Add(pokeInterval)), 468 }) 469 So(runtest.Tasks(ct.TQ.Tasks()), ShouldHaveLength, 1) 470 task := runtest.Tasks(ct.TQ.Tasks())[0] 471 So(task.ETA, ShouldResemble, clock.Now(ctx).UTC().Add(30*time.Second)) 472 So(task.Payload, ShouldResembleProto, &eventpb.ManageRunTask{RunId: string(runID)}) 473 }) 474 475 Convey("Run is missing", func() { 476 So(datastore.Delete(ctx, &run.Run{ID: runID}), ShouldBeNil) 477 So(notifier.PokeNow(ctx, runID), ShouldBeNil) 478 ctx = memlogger.Use(ctx) 479 log := logging.Get(ctx).(*memlogger.MemLogger) 480 ct.TQ.Run(ctx, tqtesting.StopAfterTask(eventpb.ManageRunTaskClass)) 481 So(log, memlogger.ShouldHaveLog, logging.Error, fmt.Sprintf("run %s is missing from datastore but got manage-run task", runID)) 482 }) 483 }) 484 } 485 486 type fakeHandler struct { 487 invocations []string 488 preserveEvents bool 489 postProcessFn eventbox.PostProcessFn 490 startAddsLogEntries []*run.LogEntry 491 } 492 493 var _ handler.Handler = &fakeHandler{} 494 495 func (fh *fakeHandler) Start(ctx context.Context, rs *state.RunState) (*handler.Result, error) { 496 fh.addInvocation("Start") 497 rs = rs.ShallowCopy() 498 if len(fh.startAddsLogEntries) > 0 { 499 rs.LogEntries = append(rs.LogEntries, fh.startAddsLogEntries...) 500 } 501 return &handler.Result{ 502 State: rs, 503 PreserveEvents: fh.preserveEvents, 504 PostProcessFn: fh.postProcessFn, 505 }, nil 506 } 507 508 func (fh *fakeHandler) Cancel(ctx context.Context, rs *state.RunState, reasons []string) (*handler.Result, error) { 509 fh.addInvocation("Cancel") 510 return &handler.Result{ 511 State: rs.ShallowCopy(), 512 PreserveEvents: fh.preserveEvents, 513 PostProcessFn: fh.postProcessFn, 514 }, nil 515 } 516 517 func (fh *fakeHandler) OnCLsUpdated(ctx context.Context, rs *state.RunState, _ common.CLIDs) (*handler.Result, error) { 518 fh.addInvocation("OnCLsUpdated") 519 return &handler.Result{ 520 State: rs.ShallowCopy(), 521 PreserveEvents: fh.preserveEvents, 522 PostProcessFn: fh.postProcessFn, 523 }, nil 524 } 525 526 func (fh *fakeHandler) OnReadyForSubmission(ctx context.Context, rs *state.RunState) (*handler.Result, error) { 527 fh.addInvocation("OnReadyForSubmission") 528 return &handler.Result{ 529 State: rs.ShallowCopy(), 530 PreserveEvents: fh.preserveEvents, 531 PostProcessFn: fh.postProcessFn, 532 }, nil 533 } 534 535 // OnCLsSubmitted records provided CLs have been submitted. 536 func (fh *fakeHandler) OnCLsSubmitted(ctx context.Context, rs *state.RunState, clids common.CLIDs) (*handler.Result, error) { 537 fh.addInvocation("OnCLsSubmitted") 538 return &handler.Result{ 539 State: rs.ShallowCopy(), 540 PreserveEvents: fh.preserveEvents, 541 PostProcessFn: fh.postProcessFn, 542 }, nil 543 } 544 545 func (fh *fakeHandler) OnSubmissionCompleted(ctx context.Context, rs *state.RunState, sc *eventpb.SubmissionCompleted) (*handler.Result, error) { 546 fh.addInvocation("OnSubmissionCompleted") 547 return &handler.Result{ 548 State: rs.ShallowCopy(), 549 PreserveEvents: fh.preserveEvents, 550 PostProcessFn: fh.postProcessFn, 551 }, nil 552 } 553 554 func (fh *fakeHandler) OnLongOpCompleted(ctx context.Context, rs *state.RunState, result *eventpb.LongOpCompleted) (*handler.Result, error) { 555 fh.addInvocation("OnLongOpCompleted") 556 return &handler.Result{ 557 State: rs.ShallowCopy(), 558 PreserveEvents: fh.preserveEvents, 559 PostProcessFn: fh.postProcessFn, 560 }, nil 561 } 562 563 func (fh *fakeHandler) OnTryjobsUpdated(ctx context.Context, rs *state.RunState, tryjobs common.TryjobIDs) (*handler.Result, error) { 564 fh.addInvocation("OnTryjobsUpdated") 565 return &handler.Result{ 566 State: rs.ShallowCopy(), 567 PreserveEvents: fh.preserveEvents, 568 PostProcessFn: fh.postProcessFn, 569 }, nil 570 } 571 572 func (fh *fakeHandler) TryResumeSubmission(ctx context.Context, rs *state.RunState) (*handler.Result, error) { 573 fh.addInvocation("TryResumeSubmission") 574 return &handler.Result{ 575 State: rs.ShallowCopy(), 576 PreserveEvents: fh.preserveEvents, 577 PostProcessFn: fh.postProcessFn, 578 }, nil 579 } 580 581 func (fh *fakeHandler) Poke(ctx context.Context, rs *state.RunState) (*handler.Result, error) { 582 fh.addInvocation("Poke") 583 return &handler.Result{ 584 State: rs.ShallowCopy(), 585 PreserveEvents: fh.preserveEvents, 586 PostProcessFn: fh.postProcessFn, 587 }, nil 588 } 589 590 func (fh *fakeHandler) UpdateConfig(ctx context.Context, rs *state.RunState, hash string) (*handler.Result, error) { 591 fh.addInvocation("UpdateConfig") 592 return &handler.Result{ 593 State: rs.ShallowCopy(), 594 PreserveEvents: fh.preserveEvents, 595 PostProcessFn: fh.postProcessFn, 596 }, nil 597 } 598 599 func (fh *fakeHandler) addInvocation(method string) { 600 fh.invocations = append(fh.invocations, method) 601 } 602 603 func (fh *fakeHandler) OnParentRunCompleted(ctx context.Context, rs *state.RunState) (*handler.Result, error) { 604 fh.addInvocation("OnParentRunCompleted") 605 return &handler.Result{ 606 State: rs.ShallowCopy(), 607 PreserveEvents: fh.preserveEvents, 608 PostProcessFn: fh.postProcessFn, 609 }, nil 610 }