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  }