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:         &quotaManagerMock{},
   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  }