go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/run/impl/handler/submit_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  	"fmt"
    20  	"sort"
    21  	"strconv"
    22  	"testing"
    23  	"time"
    24  
    25  	"go.chromium.org/luci/common/clock"
    26  	gerritpb "go.chromium.org/luci/common/proto/gerrit"
    27  	"go.chromium.org/luci/gae/service/datastore"
    28  	"google.golang.org/protobuf/proto"
    29  	"google.golang.org/protobuf/types/known/timestamppb"
    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/common/tree"
    35  	"go.chromium.org/luci/cv/internal/configs/prjcfg"
    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/gerrit"
    39  	gf "go.chromium.org/luci/cv/internal/gerrit/gerritfake"
    40  	"go.chromium.org/luci/cv/internal/gerrit/trigger"
    41  	"go.chromium.org/luci/cv/internal/run"
    42  	"go.chromium.org/luci/cv/internal/run/eventpb"
    43  	"go.chromium.org/luci/cv/internal/run/impl/state"
    44  	"go.chromium.org/luci/cv/internal/run/impl/submit"
    45  	"go.chromium.org/luci/cv/internal/run/runtest"
    46  
    47  	. "github.com/smartystreets/goconvey/convey"
    48  	. "go.chromium.org/luci/common/testing/assertions"
    49  )
    50  
    51  func TestOnReadyForSubmission(t *testing.T) {
    52  	t.Parallel()
    53  
    54  	Convey("OnReadyForSubmission", t, func() {
    55  		ct := cvtesting.Test{}
    56  		ctx, cancel := ct.SetUp(t)
    57  		defer cancel()
    58  
    59  		const lProject = "l_project"
    60  		const gHost = "x-review.example.com"
    61  		rid := common.MakeRunID(lProject, ct.Clock.Now().Add(-2*time.Minute), 1, []byte("deadbeef"))
    62  		runCLs := common.CLIDs{1, 2}
    63  		r := run.Run{
    64  			ID:         rid,
    65  			Mode:       run.FullRun,
    66  			Status:     run.Status_RUNNING,
    67  			CreateTime: ct.Clock.Now().UTC().Add(-2 * time.Minute),
    68  			StartTime:  ct.Clock.Now().UTC().Add(-1 * time.Minute),
    69  			CLs:        runCLs,
    70  		}
    71  		cg := &cfgpb.Config{
    72  			ConfigGroups: []*cfgpb.ConfigGroup{
    73  				{
    74  					Name: "main",
    75  					Verifiers: &cfgpb.Verifiers{
    76  						TreeStatus: &cfgpb.Verifiers_TreeStatus{
    77  							Url: "tree.example.com",
    78  						},
    79  					},
    80  				},
    81  			},
    82  		}
    83  		prjcfgtest.Create(ctx, rid.LUCIProject(), cg)
    84  		meta, err := prjcfg.GetLatestMeta(ctx, rid.LUCIProject())
    85  		So(err, ShouldBeNil)
    86  		So(meta.ConfigGroupIDs, ShouldHaveLength, 1)
    87  		r.ConfigGroupID = meta.ConfigGroupIDs[0]
    88  
    89  		// 1 depends on 2
    90  		ci1 := gf.CI(
    91  			1111, gf.PS(2),
    92  			gf.CQ(2, ct.Clock.Now().Add(-2*time.Minute), gf.U("user-100")),
    93  			gf.Updated(clock.Now(ctx).Add(-1*time.Minute)))
    94  		ci2 := gf.CI(
    95  			2222, gf.PS(3),
    96  			gf.CQ(2, ct.Clock.Now().Add(-2*time.Minute), gf.U("user-100")),
    97  			gf.Updated(clock.Now(ctx).Add(-1*time.Minute)))
    98  		So(datastore.Put(ctx,
    99  			&run.RunCL{
   100  				ID:         1,
   101  				Run:        datastore.MakeKey(ctx, common.RunKind, string(rid)),
   102  				ExternalID: changelist.MustGobID(gHost, ci1.GetNumber()),
   103  				Detail: &changelist.Snapshot{
   104  					Kind: &changelist.Snapshot_Gerrit{
   105  						Gerrit: &changelist.Gerrit{
   106  							Host: gHost,
   107  							Info: proto.Clone(ci1).(*gerritpb.ChangeInfo),
   108  						},
   109  					},
   110  					Deps: []*changelist.Dep{
   111  						{Clid: 2, Kind: changelist.DepKind_HARD},
   112  					},
   113  				},
   114  			},
   115  			&run.RunCL{
   116  				ID:         2,
   117  				Run:        datastore.MakeKey(ctx, common.RunKind, string(rid)),
   118  				ExternalID: changelist.MustGobID(gHost, ci2.GetNumber()),
   119  				Detail: &changelist.Snapshot{
   120  					Kind: &changelist.Snapshot_Gerrit{
   121  						Gerrit: &changelist.Gerrit{
   122  							Host: gHost,
   123  							Info: proto.Clone(ci2).(*gerritpb.ChangeInfo),
   124  						},
   125  					},
   126  				},
   127  			},
   128  		), ShouldBeNil)
   129  
   130  		rs := &state.RunState{Run: r}
   131  
   132  		h, deps := makeTestHandler(&ct)
   133  
   134  		statuses := []run.Status{
   135  			run.Status_SUCCEEDED,
   136  			run.Status_FAILED,
   137  			run.Status_CANCELLED,
   138  		}
   139  		for _, status := range statuses {
   140  			Convey(fmt.Sprintf("Release submit queue when Run is %s", status), func() {
   141  				So(datastore.RunInTransaction(ctx, func(ctx context.Context) error {
   142  					waitlisted, err := submit.TryAcquire(ctx, deps.rm.NotifyReadyForSubmission, rs.ID, nil)
   143  					So(waitlisted, ShouldBeFalse)
   144  					return err
   145  				}, nil), ShouldBeNil)
   146  				rs.Status = status
   147  				res, err := h.OnReadyForSubmission(ctx, rs)
   148  				So(err, ShouldBeNil)
   149  				expectedState := &state.RunState{
   150  					Run: rs.Run,
   151  					LogEntries: []*run.LogEntry{
   152  						{
   153  							Time: timestamppb.New(clock.Now(ctx)),
   154  							Kind: &run.LogEntry_ReleasedSubmitQueue_{
   155  								ReleasedSubmitQueue: &run.LogEntry_ReleasedSubmitQueue{},
   156  							},
   157  						},
   158  					},
   159  				}
   160  				So(res.State, cvtesting.SafeShouldResemble, expectedState)
   161  				So(res.SideEffectFn, ShouldBeNil)
   162  				So(res.PreserveEvents, ShouldBeFalse)
   163  				So(res.PostProcessFn, ShouldBeNil)
   164  				current, waitlist, err := submit.LoadCurrentAndWaitlist(ctx, rs.ID)
   165  				So(err, ShouldBeNil)
   166  				So(current, ShouldBeEmpty)
   167  				So(waitlist, ShouldBeEmpty)
   168  			})
   169  		}
   170  
   171  		Convey("No-Op when status is SUBMITTING", func() {
   172  			rs.Status = run.Status_SUBMITTING
   173  			res, err := h.OnReadyForSubmission(ctx, rs)
   174  			So(err, ShouldBeNil)
   175  			So(res.State, ShouldEqual, rs)
   176  			So(res.SideEffectFn, ShouldBeNil)
   177  			So(res.PreserveEvents, ShouldBeFalse)
   178  			So(res.PostProcessFn, ShouldBeNil)
   179  		})
   180  
   181  		Convey("Do not submit if parent Run is not done yet.", func() {
   182  			const parentRun = common.RunID("parent/1-cow")
   183  			So(datastore.Put(ctx,
   184  				&run.Run{
   185  					ID:     parentRun,
   186  					Status: run.Status_RUNNING,
   187  					CLs:    common.CLIDs{13},
   188  				},
   189  				&run.RunCL{
   190  					ID:         13,
   191  					Run:        datastore.MakeKey(ctx, common.RunKind, string(parentRun)),
   192  					ExternalID: "gerrit/foo-review.googlesource.com/111",
   193  				},
   194  			), ShouldBeNil)
   195  			rs.Status = run.Status_WAITING_FOR_SUBMISSION
   196  			rs.DepRuns = common.RunIDs{parentRun}
   197  			res, err := h.OnReadyForSubmission(ctx, rs)
   198  			So(err, ShouldBeNil)
   199  			So(res.State.LogEntries, ShouldHaveLength, 1)
   200  			So(res.SideEffectFn, ShouldBeNil)
   201  			So(res.PreserveEvents, ShouldBeFalse)
   202  			So(res.PostProcessFn, ShouldBeNil)
   203  		})
   204  
   205  		for _, status := range []run.Status{run.Status_RUNNING, run.Status_WAITING_FOR_SUBMISSION} {
   206  			now := ct.Clock.Now().UTC()
   207  			ctx = context.WithValue(ctx, &fakeTaskIDKey, "task-foo")
   208  			Convey(fmt.Sprintf("When status is %s", status), func() {
   209  				rs.Status = status
   210  				Convey("Mark submitting if Submit Queue is acquired and tree is open", func() {
   211  					res, err := h.OnReadyForSubmission(ctx, rs)
   212  					So(err, ShouldBeNil)
   213  					So(res.State.Status, ShouldEqual, run.Status_SUBMITTING)
   214  					So(res.State.Submission, ShouldResembleProto, &run.Submission{
   215  						Deadline:          timestamppb.New(now.Add(defaultSubmissionDuration)),
   216  						Cls:               []int64{2, 1}, // in submission order
   217  						TaskId:            "task-foo",
   218  						TreeOpen:          true,
   219  						LastTreeCheckTime: timestamppb.New(now),
   220  					})
   221  					So(res.State.SubmissionScheduled, ShouldBeTrue)
   222  					So(res.SideEffectFn, ShouldBeNil)
   223  					So(res.PreserveEvents, ShouldBeFalse)
   224  					So(res.PostProcessFn, ShouldNotBeNil)
   225  					So(submit.MustCurrentRun(ctx, lProject), ShouldEqual, rid)
   226  					runtest.AssertReceivedReadyForSubmission(ctx, rid, now.Add(10*time.Second))
   227  					So(res.State.LogEntries, ShouldHaveLength, 2)
   228  					So(res.State.LogEntries[0].Kind, ShouldHaveSameTypeAs, &run.LogEntry_AcquiredSubmitQueue_{})
   229  					So(res.State.LogEntries[1].Kind.(*run.LogEntry_TreeChecked_).TreeChecked.Open, ShouldBeTrue)
   230  					// SubmitQueue not yet released.
   231  				})
   232  
   233  				Convey("Add Run to waitlist when Submit Queue is occupied", func() {
   234  					// another run has taken the current slot
   235  					anotherRunID := common.MakeRunID(lProject, now, 1, []byte("cafecafe"))
   236  					So(datastore.RunInTransaction(ctx, func(ctx context.Context) error {
   237  						_, err := submit.TryAcquire(ctx, deps.rm.NotifyReadyForSubmission, anotherRunID, nil)
   238  						So(err, ShouldBeNil)
   239  						return nil
   240  					}, nil), ShouldBeNil)
   241  					So(submit.MustCurrentRun(ctx, lProject), ShouldEqual, anotherRunID)
   242  					res, err := h.OnReadyForSubmission(ctx, rs)
   243  					So(err, ShouldBeNil)
   244  					So(res.State.Status, ShouldEqual, run.Status_WAITING_FOR_SUBMISSION)
   245  					So(res.SideEffectFn, ShouldBeNil)
   246  					So(res.PreserveEvents, ShouldBeFalse)
   247  					So(res.PostProcessFn, ShouldBeNil)
   248  					_, waitlist, err := submit.LoadCurrentAndWaitlist(ctx, rid)
   249  					So(err, ShouldBeNil)
   250  					So(waitlist.Index(rid), ShouldEqual, 0)
   251  					So(res.State.LogEntries, ShouldHaveLength, 1)
   252  					So(res.State.LogEntries[0].Kind, ShouldHaveSameTypeAs, &run.LogEntry_Waitlisted_{})
   253  				})
   254  
   255  				Convey("Revisit after 1 mintues if tree is closed", func() {
   256  					ct.TreeFake.ModifyState(ctx, tree.Closed)
   257  					res, err := h.OnReadyForSubmission(ctx, rs)
   258  					So(err, ShouldBeNil)
   259  					So(res.State.Status, ShouldEqual, run.Status_WAITING_FOR_SUBMISSION)
   260  					So(res.State.Submission, ShouldResembleProto, &run.Submission{
   261  						TreeOpen:          false,
   262  						LastTreeCheckTime: timestamppb.New(now),
   263  					})
   264  					So(res.SideEffectFn, ShouldBeNil)
   265  					So(res.PreserveEvents, ShouldBeFalse)
   266  					So(res.PostProcessFn, ShouldBeNil)
   267  					runtest.AssertReceivedPoke(ctx, rid, now.Add(1*time.Minute))
   268  					// The Run must not occupy the Submit Queue
   269  					So(submit.MustCurrentRun(ctx, lProject), ShouldNotEqual, rid)
   270  					So(res.State.LogEntries, ShouldHaveLength, 3)
   271  					So(res.State.LogEntries[0].Kind, ShouldHaveSameTypeAs, &run.LogEntry_AcquiredSubmitQueue_{})
   272  					So(res.State.LogEntries[1].Kind, ShouldHaveSameTypeAs, &run.LogEntry_TreeChecked_{})
   273  					So(res.State.LogEntries[2].Kind, ShouldHaveSameTypeAs, &run.LogEntry_ReleasedSubmitQueue_{})
   274  					So(res.State.LogEntries[1].Kind.(*run.LogEntry_TreeChecked_).TreeChecked.Open, ShouldBeFalse)
   275  				})
   276  
   277  				Convey("Set TreeErrorSince on first failure", func() {
   278  					ct.TreeFake.ModifyState(ctx, tree.StateUnknown)
   279  					ct.TreeFake.InjectErr(fmt.Errorf("error while fetching tree status"))
   280  					res, err := h.OnReadyForSubmission(ctx, rs)
   281  					So(err, ShouldBeNil)
   282  					So(res.State.Status, ShouldEqual, run.Status_WAITING_FOR_SUBMISSION)
   283  					So(res.State.Submission, ShouldResembleProto, &run.Submission{
   284  						TreeOpen:          false,
   285  						LastTreeCheckTime: timestamppb.New(now),
   286  						TreeErrorSince:    timestamppb.New(now),
   287  					})
   288  					So(res.SideEffectFn, ShouldBeNil)
   289  					So(res.PreserveEvents, ShouldBeFalse)
   290  					So(res.PostProcessFn, ShouldBeNil)
   291  					runtest.AssertReceivedPoke(ctx, rid, now.Add(1*time.Minute))
   292  					// The Run must not occupy the Submit Queue
   293  					So(submit.MustCurrentRun(ctx, lProject), ShouldNotEqual, rid)
   294  					So(res.State.LogEntries, ShouldHaveLength, 2)
   295  					So(res.State.LogEntries[0].Kind, ShouldHaveSameTypeAs, &run.LogEntry_AcquiredSubmitQueue_{})
   296  					So(res.State.LogEntries[1].Kind, ShouldHaveSameTypeAs, &run.LogEntry_ReleasedSubmitQueue_{})
   297  				})
   298  			})
   299  		}
   300  	})
   301  }
   302  
   303  func TestOnSubmissionCompleted(t *testing.T) {
   304  	t.Parallel()
   305  
   306  	Convey("OnSubmissionCompleted", t, func() {
   307  		ct := cvtesting.Test{}
   308  		ctx, cancel := ct.SetUp(t)
   309  		defer cancel()
   310  
   311  		const lProject = "infra"
   312  		const gHost = "x-review.example.com"
   313  		rid := common.MakeRunID(lProject, ct.Clock.Now().Add(-2*time.Minute), 1, []byte("deadbeef"))
   314  		runCLs := common.CLIDs{1, 2}
   315  		r := run.Run{
   316  			ID:         rid,
   317  			Mode:       run.FullRun,
   318  			Status:     run.Status_SUBMITTING,
   319  			CreateTime: ct.Clock.Now().UTC().Add(-2 * time.Minute),
   320  			StartTime:  ct.Clock.Now().UTC().Add(-1 * time.Minute),
   321  			CLs:        runCLs,
   322  		}
   323  		So(datastore.Put(ctx, &r), ShouldBeNil)
   324  		cg := &cfgpb.Config{
   325  			ConfigGroups: []*cfgpb.ConfigGroup{
   326  				{Name: "main"},
   327  			},
   328  		}
   329  		prjcfgtest.Create(ctx, rid.LUCIProject(), cg)
   330  		meta, err := prjcfg.GetLatestMeta(ctx, rid.LUCIProject())
   331  		So(err, ShouldBeNil)
   332  		So(meta.ConfigGroupIDs, ShouldHaveLength, 1)
   333  		r.ConfigGroupID = meta.ConfigGroupIDs[0]
   334  
   335  		genCL := func(clid common.CLID, change int, deps ...common.CLID) (*gerritpb.ChangeInfo, *changelist.CL, *run.RunCL) {
   336  			ci := gf.CI(
   337  				change, gf.PS(2),
   338  				gf.Owner("user-99"),
   339  				gf.CQ(1, ct.Clock.Now().Add(-5*time.Minute), gf.U("user-101")),
   340  				gf.CQ(2, ct.Clock.Now().Add(-2*time.Minute), gf.U("user-100")),
   341  				gf.Updated(clock.Now(ctx).Add(-1*time.Minute)))
   342  			triggers := trigger.Find(&trigger.FindInput{ChangeInfo: ci, ConfigGroup: cg.ConfigGroups[0]})
   343  			So(triggers.GetCqVoteTrigger(), ShouldResembleProto, &run.Trigger{
   344  				Time:            timestamppb.New(ct.Clock.Now().Add(-2 * time.Minute)),
   345  				Mode:            string(run.FullRun),
   346  				Email:           "user-100@example.com",
   347  				GerritAccountId: 100,
   348  			})
   349  			cl := &changelist.CL{
   350  				ID:         clid,
   351  				ExternalID: changelist.MustGobID(gHost, ci.GetNumber()),
   352  				EVersion:   10,
   353  				Snapshot: &changelist.Snapshot{
   354  					ExternalUpdateTime:    timestamppb.New(clock.Now(ctx).Add(-1 * time.Minute)),
   355  					LuciProject:           lProject,
   356  					Patchset:              2,
   357  					MinEquivalentPatchset: 1,
   358  					Kind: &changelist.Snapshot_Gerrit{
   359  						Gerrit: &changelist.Gerrit{
   360  							Host: gHost,
   361  							Info: proto.Clone(ci).(*gerritpb.ChangeInfo),
   362  						},
   363  					},
   364  				},
   365  			}
   366  			runCL := &run.RunCL{
   367  				ID:         clid,
   368  				Run:        datastore.MakeKey(ctx, common.RunKind, string(rid)),
   369  				ExternalID: changelist.MustGobID(gHost, ci.GetNumber()),
   370  				Detail: &changelist.Snapshot{
   371  					Kind: &changelist.Snapshot_Gerrit{
   372  						Gerrit: &changelist.Gerrit{
   373  							Host: gHost,
   374  							Info: proto.Clone(ci).(*gerritpb.ChangeInfo),
   375  						},
   376  					},
   377  				},
   378  				Trigger: triggers.GetCqVoteTrigger(),
   379  			}
   380  			if len(deps) > 0 {
   381  				cl.Snapshot.Deps = make([]*changelist.Dep, len(deps))
   382  				runCL.Detail.Deps = make([]*changelist.Dep, len(deps))
   383  				for i, dep := range deps {
   384  					cl.Snapshot.Deps[i] = &changelist.Dep{
   385  						Clid: int64(dep),
   386  						Kind: changelist.DepKind_HARD,
   387  					}
   388  					runCL.Detail.Deps[i] = &changelist.Dep{
   389  						Clid: int64(dep),
   390  						Kind: changelist.DepKind_HARD,
   391  					}
   392  				}
   393  			}
   394  			return ci, cl, runCL
   395  		}
   396  
   397  		ci1, cl1, runCL1 := genCL(1, 1111, 2)
   398  		ci2, cl2, runCL2 := genCL(2, 2222)
   399  		So(datastore.Put(ctx, cl1, cl2, runCL1, runCL2), ShouldBeNil)
   400  
   401  		ct.GFake.CreateChange(&gf.Change{
   402  			Host: gHost,
   403  			Info: proto.Clone(ci1).(*gerritpb.ChangeInfo),
   404  			ACLs: gf.ACLRestricted(lProject),
   405  		})
   406  		ct.GFake.CreateChange(&gf.Change{
   407  			Host: gHost,
   408  			Info: proto.Clone(ci2).(*gerritpb.ChangeInfo),
   409  			ACLs: gf.ACLRestricted(lProject),
   410  		})
   411  		ct.GFake.SetDependsOn(gHost, ci1, ci2)
   412  
   413  		rs := &state.RunState{Run: r}
   414  		h, deps := makeTestHandler(&ct)
   415  
   416  		statuses := []run.Status{
   417  			run.Status_SUCCEEDED,
   418  			run.Status_FAILED,
   419  			run.Status_CANCELLED,
   420  		}
   421  		for _, status := range statuses {
   422  			Convey(fmt.Sprintf("Release submit queue when Run is %s", status), func() {
   423  				So(datastore.RunInTransaction(ctx, func(ctx context.Context) error {
   424  					waitlisted, err := submit.TryAcquire(ctx, deps.rm.NotifyReadyForSubmission, rs.ID, nil)
   425  					So(waitlisted, ShouldBeFalse)
   426  					return err
   427  				}, nil), ShouldBeNil)
   428  				rs.Status = status
   429  				res, err := h.OnSubmissionCompleted(ctx, rs, nil)
   430  				So(err, ShouldBeNil)
   431  				expectedState := &state.RunState{
   432  					Run: rs.Run,
   433  					LogEntries: []*run.LogEntry{
   434  						{
   435  							Time: timestamppb.New(clock.Now(ctx)),
   436  							Kind: &run.LogEntry_ReleasedSubmitQueue_{
   437  								ReleasedSubmitQueue: &run.LogEntry_ReleasedSubmitQueue{},
   438  							},
   439  						},
   440  					},
   441  				}
   442  				So(res.State, cvtesting.SafeShouldResemble, expectedState)
   443  				So(res.SideEffectFn, ShouldBeNil)
   444  				So(res.PreserveEvents, ShouldBeFalse)
   445  				So(res.PostProcessFn, ShouldBeNil)
   446  				current, waitlist, err := submit.LoadCurrentAndWaitlist(ctx, rs.ID)
   447  				So(err, ShouldBeNil)
   448  				So(current, ShouldBeEmpty)
   449  				So(waitlist, ShouldBeEmpty)
   450  			})
   451  		}
   452  
   453  		ctx = context.WithValue(ctx, &fakeTaskIDKey, "task-foo")
   454  		Convey("Succeeded", func() {
   455  			sc := &eventpb.SubmissionCompleted{
   456  				Result: eventpb.SubmissionResult_SUCCEEDED,
   457  			}
   458  			res, err := h.OnSubmissionCompleted(ctx, rs, sc)
   459  			So(err, ShouldBeNil)
   460  			So(res.State.Status, ShouldEqual, run.Status_SUCCEEDED)
   461  			So(res.State.EndTime, ShouldEqual, ct.Clock.Now().UTC())
   462  			So(res.SideEffectFn, ShouldNotBeNil)
   463  			So(res.PreserveEvents, ShouldBeFalse)
   464  			So(res.PostProcessFn, ShouldBeNil)
   465  		})
   466  
   467  		selfSetReviewRequests := func() (ret []*gerritpb.SetReviewRequest) {
   468  			for _, req := range ct.GFake.Requests() {
   469  				switch r, ok := req.(*gerritpb.SetReviewRequest); {
   470  				case !ok:
   471  				case r.GetOnBehalfOf() != 0:
   472  				default:
   473  					ret = append(ret, r)
   474  				}
   475  			}
   476  			sort.SliceStable(ret, func(i, j int) bool {
   477  				return ret[i].Number < ret[j].Number
   478  			})
   479  			return
   480  		}
   481  		assertNotify := func(req *gerritpb.SetReviewRequest, accts ...int64) {
   482  			So(req, ShouldNotBeNil)
   483  			So(req.GetNotify(), ShouldEqual, gerritpb.Notify_NOTIFY_NONE)
   484  			So(req.GetNotifyDetails(), ShouldResembleProto, &gerritpb.NotifyDetails{
   485  				Recipients: []*gerritpb.NotifyDetails_Recipient{
   486  					{
   487  						RecipientType: gerritpb.NotifyDetails_RECIPIENT_TYPE_TO,
   488  						Info: &gerritpb.NotifyDetails_Info{
   489  							Accounts: accts,
   490  						},
   491  					},
   492  				},
   493  			})
   494  		}
   495  		assertAttentionSet := func(req *gerritpb.SetReviewRequest, reason string, accs ...int64) {
   496  			So(req, ShouldNotBeNil)
   497  			expected := []*gerritpb.AttentionSetInput{}
   498  			for _, a := range accs {
   499  				expected = append(
   500  					expected,
   501  					&gerritpb.AttentionSetInput{
   502  						User:   strconv.FormatInt(a, 10),
   503  						Reason: "ps#2: " + reason,
   504  					},
   505  				)
   506  			}
   507  			actual := req.GetAddToAttentionSet()
   508  			sort.SliceStable(actual, func(i, j int) bool {
   509  				lhs, _ := strconv.Atoi(actual[i].User)
   510  				rhs, _ := strconv.Atoi(actual[j].User)
   511  				return lhs < rhs
   512  			})
   513  			So(actual, ShouldResembleProto, expected)
   514  		}
   515  
   516  		Convey("Transient failure", func() {
   517  			sc := &eventpb.SubmissionCompleted{
   518  				Result: eventpb.SubmissionResult_FAILED_TRANSIENT,
   519  			}
   520  			Convey("When deadline is not exceeded", func() {
   521  				rs.Submission = &run.Submission{
   522  					Deadline: timestamppb.New(ct.Clock.Now().UTC().Add(10 * time.Minute)),
   523  				}
   524  
   525  				Convey("Resume submission if TaskID matches", func() {
   526  					rs.Submission.TaskId = "task-foo" // same task ID as the current task
   527  					res, err := h.OnSubmissionCompleted(ctx, rs, sc)
   528  					So(err, ShouldBeNil)
   529  					So(res.State.Status, ShouldEqual, run.Status_SUBMITTING)
   530  					So(res.State.Submission, ShouldResembleProto, &run.Submission{
   531  						Deadline: timestamppb.New(ct.Clock.Now().UTC().Add(10 * time.Minute)),
   532  						TaskId:   "task-foo",
   533  					}) // unchanged
   534  					So(res.State.SubmissionScheduled, ShouldBeTrue)
   535  					So(res.SideEffectFn, ShouldBeNil)
   536  					So(res.PreserveEvents, ShouldBeFalse)
   537  					So(res.PostProcessFn, ShouldNotBeNil)
   538  				})
   539  
   540  				Convey("Invoke RM at deadline if TaskID doesn't match", func() {
   541  					ctx, rmDispatcher := runtest.MockDispatch(ctx)
   542  					rs.Submission.TaskId = "another-task"
   543  					res, err := h.OnSubmissionCompleted(ctx, rs, sc)
   544  					So(err, ShouldBeNil)
   545  					expectedState := &state.RunState{
   546  						Run: rs.Run,
   547  						LogEntries: []*run.LogEntry{
   548  							{
   549  								Time: timestamppb.New(clock.Now(ctx)),
   550  								Kind: &run.LogEntry_SubmissionFailure_{
   551  									SubmissionFailure: &run.LogEntry_SubmissionFailure{
   552  										Event: &eventpb.SubmissionCompleted{Result: eventpb.SubmissionResult_FAILED_TRANSIENT},
   553  									},
   554  								},
   555  							},
   556  						},
   557  					}
   558  					So(res.State, cvtesting.SafeShouldResemble, expectedState)
   559  					So(res.SideEffectFn, ShouldBeNil)
   560  					So(res.PreserveEvents, ShouldBeTrue)
   561  					So(res.PostProcessFn, ShouldBeNil)
   562  					So(rmDispatcher.LatestETAof(string(rid)), ShouldHappenOnOrAfter, rs.Submission.Deadline.AsTime())
   563  				})
   564  			})
   565  
   566  			Convey("When deadline is exceeded", func() {
   567  				rs.Submission = &run.Submission{
   568  					Deadline: timestamppb.New(ct.Clock.Now().UTC().Add(-10 * time.Minute)),
   569  					TaskId:   "task-foo",
   570  				}
   571  				So(datastore.RunInTransaction(ctx, func(ctx context.Context) error {
   572  					waitlisted, err := submit.TryAcquire(ctx, deps.rm.NotifyReadyForSubmission, rid, nil)
   573  					So(waitlisted, ShouldBeFalse)
   574  					return err
   575  				}, nil), ShouldBeNil)
   576  				runAndVerify := func(expectedMsgs []struct {
   577  					clid int64
   578  					msg  string
   579  				}) {
   580  					res, err := h.OnSubmissionCompleted(ctx, rs, sc)
   581  					So(err, ShouldBeNil)
   582  					So(res.State.Status, ShouldEqual, run.Status_SUBMITTING)
   583  					for i, f := range sc.GetClFailures().GetFailures() {
   584  						So(res.State.Submission.GetFailedCls()[i], ShouldEqual, f.GetClid())
   585  					}
   586  					So(res.SideEffectFn, ShouldBeNil)
   587  					So(res.PreserveEvents, ShouldBeFalse)
   588  					So(res.PostProcessFn, ShouldBeNil)
   589  					So(res.State.OngoingLongOps.GetOps(), ShouldHaveLength, 1)
   590  					for i, f := range sc.GetClFailures().GetFailures() {
   591  						So(res.State.Submission.GetFailedCls()[i], ShouldEqual, f.GetClid())
   592  					}
   593  					for _, op := range res.State.OngoingLongOps.GetOps() {
   594  						So(op.GetResetTriggers(), ShouldNotBeNil)
   595  						expectedRequests := make([]*run.OngoingLongOps_Op_ResetTriggers_Request, len(expectedMsgs))
   596  						for i, expectedMsg := range expectedMsgs {
   597  							expectedRequests[i] = &run.OngoingLongOps_Op_ResetTriggers_Request{
   598  								Clid:    expectedMsg.clid,
   599  								Message: expectedMsg.msg,
   600  								Notify: gerrit.Whoms{
   601  									gerrit.Whom_OWNER,
   602  									gerrit.Whom_CQ_VOTERS,
   603  								},
   604  								AddToAttention: gerrit.Whoms{
   605  									gerrit.Whom_OWNER,
   606  									gerrit.Whom_CQ_VOTERS,
   607  								},
   608  								AddToAttentionReason: submissionFailureAttentionReason,
   609  							}
   610  						}
   611  						So(op.GetResetTriggers().GetRequests(), ShouldResembleProto, expectedRequests)
   612  						So(op.GetResetTriggers().GetRunStatusIfSucceeded(), ShouldEqual, run.Status_FAILED)
   613  					}
   614  					So(submit.MustCurrentRun(ctx, lProject), ShouldNotEqual, rs.ID)
   615  				}
   616  
   617  				Convey("Single CL Run", func() {
   618  					rs.Submission.Cls = []int64{2}
   619  					Convey("Not submitted", func() {
   620  						Convey("CL failure", func() {
   621  							sc.FailureReason = &eventpb.SubmissionCompleted_ClFailures{
   622  								ClFailures: &eventpb.SubmissionCompleted_CLSubmissionFailures{
   623  									Failures: []*eventpb.SubmissionCompleted_CLSubmissionFailure{
   624  										{Clid: 2, Message: "some transient failure"},
   625  									},
   626  								},
   627  							}
   628  							runAndVerify([]struct {
   629  								clid int64
   630  								msg  string
   631  							}{
   632  								{
   633  									clid: 2,
   634  									msg:  "CL failed to submit because of transient failure: some transient failure. However, submission is running out of time to retry.",
   635  								},
   636  							})
   637  						})
   638  						Convey("Unclassified failure", func() {
   639  							runAndVerify([]struct {
   640  								clid int64
   641  								msg  string
   642  							}{
   643  								{
   644  									clid: 2,
   645  									msg:  timeoutMsg,
   646  								},
   647  							})
   648  						})
   649  					})
   650  					Convey("Submitted", func() {
   651  						rs.Submission.SubmittedCls = []int64{2}
   652  						res, err := h.OnSubmissionCompleted(ctx, rs, sc)
   653  						So(err, ShouldBeNil)
   654  						So(res.State.Status, ShouldEqual, run.Status_SUCCEEDED)
   655  						So(res.State.EndTime, ShouldEqual, ct.Clock.Now())
   656  						for _, op := range res.State.OngoingLongOps.GetOps() {
   657  							if op.GetExecutePostAction() == nil {
   658  								SoMsg("should not contain any long op other than post action", op.GetWork(), ShouldBeNil)
   659  							}
   660  						}
   661  						So(res.SideEffectFn, ShouldNotBeNil)
   662  						So(res.PreserveEvents, ShouldBeFalse)
   663  						So(res.PostProcessFn, ShouldBeNil)
   664  						So(ct.GFake.GetChange(gHost, int(ci2.GetNumber())).Info, ShouldResembleProto, ci2) // unchanged
   665  						So(submit.MustCurrentRun(ctx, lProject), ShouldNotEqual, rs.ID)
   666  					})
   667  				})
   668  
   669  				Convey("Multi CLs Run", func() {
   670  					rs.Submission.Cls = []int64{2, 1}
   671  					Convey("None of the CLs are submitted", func() {
   672  						Convey("CL failure", func() {
   673  							sc.FailureReason = &eventpb.SubmissionCompleted_ClFailures{
   674  								ClFailures: &eventpb.SubmissionCompleted_CLSubmissionFailures{
   675  									Failures: []*eventpb.SubmissionCompleted_CLSubmissionFailure{
   676  										{Clid: 2, Message: "some transient failure"},
   677  									},
   678  								},
   679  							}
   680  							Convey("With root CL", func() {
   681  								rs.RootCL = 1
   682  								runAndVerify([]struct {
   683  									clid int64
   684  									msg  string
   685  								}{
   686  									{
   687  										clid: 1,
   688  										msg:  "Failed to submit the following CL(s):\n* https://x-review.example.com/c/2222: CL failed to submit because of transient failure: some transient failure. However, submission is running out of time to retry.\n\nNone of the CLs in the Run has been submitted. CLs:\n* https://x-review.example.com/c/2222\n* https://x-review.example.com/c/1111",
   689  									},
   690  								})
   691  							})
   692  							Convey("Without root CL", func() {
   693  								runAndVerify([]struct {
   694  									clid int64
   695  									msg  string
   696  								}{
   697  									{
   698  										clid: 1,
   699  										msg:  "This CL is not submitted because submission has failed for the following CL(s) which this CL depends on.\n* https://x-review.example.com/c/2222\n\nNone of the CLs in the Run has been submitted. CLs:\n* https://x-review.example.com/c/2222\n* https://x-review.example.com/c/1111",
   700  									},
   701  									{
   702  										clid: 2,
   703  										msg:  "CL failed to submit because of transient failure: some transient failure. However, submission is running out of time to retry.\n\nNone of the CLs in the Run has been submitted. CLs:\n* https://x-review.example.com/c/2222\n* https://x-review.example.com/c/1111",
   704  									},
   705  								})
   706  							})
   707  						})
   708  						Convey("Unclassified failure", func() {
   709  							Convey("With root CL", func() {
   710  								rs.RootCL = 1
   711  								runAndVerify([]struct {
   712  									clid int64
   713  									msg  string
   714  								}{
   715  									{
   716  										clid: 1,
   717  										msg:  timeoutMsg + "\n\nNone of the CLs in the Run has been submitted. CLs:\n* https://x-review.example.com/c/2222\n* https://x-review.example.com/c/1111",
   718  									},
   719  								})
   720  							})
   721  							Convey("Without root CL", func() {
   722  								runAndVerify([]struct {
   723  									clid int64
   724  									msg  string
   725  								}{
   726  									{
   727  										clid: 1,
   728  										msg:  timeoutMsg + "\n\nNone of the CLs in the Run has been submitted. CLs:\n* https://x-review.example.com/c/2222\n* https://x-review.example.com/c/1111",
   729  									},
   730  									{
   731  										clid: 2,
   732  										msg:  timeoutMsg + "\n\nNone of the CLs in the Run has been submitted. CLs:\n* https://x-review.example.com/c/2222\n* https://x-review.example.com/c/1111",
   733  									},
   734  								})
   735  							})
   736  						})
   737  					})
   738  
   739  					Convey("CLs partially submitted", func() {
   740  						rs.Submission.SubmittedCls = []int64{2}
   741  						ct.GFake.MutateChange(gHost, int(ci2.GetNumber()), func(c *gf.Change) {
   742  							gf.PS(int(ci2.GetRevisions()[ci2.GetCurrentRevision()].GetNumber()) + 1)(c.Info)
   743  							gf.Status(gerritpb.ChangeStatus_MERGED)(c.Info)
   744  						})
   745  
   746  						Convey("CL failure", func() {
   747  							sc.FailureReason = &eventpb.SubmissionCompleted_ClFailures{
   748  								ClFailures: &eventpb.SubmissionCompleted_CLSubmissionFailures{
   749  									Failures: []*eventpb.SubmissionCompleted_CLSubmissionFailure{
   750  										{Clid: 1, Message: "some transient failure"},
   751  									},
   752  								},
   753  							}
   754  							Convey("With root CL", func() {
   755  								rs.RootCL = 1
   756  								runAndVerify([]struct {
   757  									clid int64
   758  									msg  string
   759  								}{
   760  									{
   761  										clid: 1,
   762  										msg:  "CL failed to submit because of transient failure: some transient failure. However, submission is running out of time to retry.\n\nCLs in the Run have been submitted partially.\nNot submitted:\n* https://x-review.example.com/c/1111\nSubmitted:\n* https://x-review.example.com/c/2222\nPlease, use your judgement to determine if already submitted CLs have to be reverted, or if the remaining CLs could be manually submitted. If you think the partially submitted CLs may have broken the tip-of-tree of your project, consider notifying your infrastructure team/gardeners/sheriffs.",
   763  									},
   764  								})
   765  								// Not posting message to any other CLs at all.
   766  								reqs := selfSetReviewRequests()
   767  								So(reqs, ShouldBeEmpty)
   768  							})
   769  							Convey("Without root CL", func() {
   770  								runAndVerify([]struct {
   771  									clid int64
   772  									msg  string
   773  								}{
   774  									{
   775  										clid: 1,
   776  										msg:  "CL failed to submit because of transient failure: some transient failure. However, submission is running out of time to retry.\n\nCLs in the Run have been submitted partially.\nNot submitted:\n* https://x-review.example.com/c/1111\nSubmitted:\n* https://x-review.example.com/c/2222\nPlease, use your judgement to determine if already submitted CLs have to be reverted, or if the remaining CLs could be manually submitted. If you think the partially submitted CLs may have broken the tip-of-tree of your project, consider notifying your infrastructure team/gardeners/sheriffs.",
   777  									},
   778  								})
   779  								// Verify posting message to the submitted CL about the failure
   780  								// on the dependent CLs
   781  								reqs := selfSetReviewRequests()
   782  								So(reqs, ShouldHaveLength, 1)
   783  								So(reqs[0].GetNumber(), ShouldEqual, ci2.GetNumber())
   784  								assertNotify(reqs[0], 99, 100, 101)
   785  								assertAttentionSet(reqs[0], "failed to submit dependent CLs", 99, 100, 101)
   786  								So(reqs[0].Message, ShouldContainSubstring, "This CL is submitted. However, submission has failed for the following CL(s) which depend on this CL.")
   787  							})
   788  						})
   789  						Convey("Unclassified failure", func() {
   790  							Convey("With root CL", func() {
   791  								rs.RootCL = 1
   792  								runAndVerify([]struct {
   793  									clid int64
   794  									msg  string
   795  								}{
   796  									{
   797  										clid: 1,
   798  										msg:  timeoutMsg + "\n\nCLs in the Run have been submitted partially.\nNot submitted:\n* https://x-review.example.com/c/1111\nSubmitted:\n* https://x-review.example.com/c/2222\nPlease, use your judgement to determine if already submitted CLs have to be reverted, or if the remaining CLs could be manually submitted. If you think the partially submitted CLs may have broken the tip-of-tree of your project, consider notifying your infrastructure team/gardeners/sheriffs.",
   799  									},
   800  								})
   801  							})
   802  							Convey("Without root CL", func() {
   803  								runAndVerify([]struct {
   804  									clid int64
   805  									msg  string
   806  								}{
   807  									{
   808  										clid: 1,
   809  										msg:  timeoutMsg + "\n\nCLs in the Run have been submitted partially.\nNot submitted:\n* https://x-review.example.com/c/1111\nSubmitted:\n* https://x-review.example.com/c/2222\nPlease, use your judgement to determine if already submitted CLs have to be reverted, or if the remaining CLs could be manually submitted. If you think the partially submitted CLs may have broken the tip-of-tree of your project, consider notifying your infrastructure team/gardeners/sheriffs.",
   810  									},
   811  								})
   812  							})
   813  
   814  						})
   815  					})
   816  
   817  					Convey("CLs fully submitted", func() {
   818  						rs.Submission.SubmittedCls = []int64{2, 1}
   819  						res, err := h.OnSubmissionCompleted(ctx, rs, sc)
   820  						So(err, ShouldBeNil)
   821  						So(res.State.Status, ShouldEqual, run.Status_SUCCEEDED)
   822  						So(res.State.EndTime, ShouldEqual, ct.Clock.Now())
   823  						So(res.SideEffectFn, ShouldNotBeNil)
   824  						So(res.PreserveEvents, ShouldBeFalse)
   825  						So(res.PostProcessFn, ShouldBeNil)
   826  						So(submit.MustCurrentRun(ctx, lProject), ShouldNotEqual, rs.ID)
   827  					})
   828  				})
   829  			})
   830  		})
   831  
   832  		Convey("Permanent failure", func() {
   833  			sc := &eventpb.SubmissionCompleted{
   834  				Result: eventpb.SubmissionResult_FAILED_PERMANENT,
   835  			}
   836  			rs.Submission = &run.Submission{
   837  				Deadline: timestamppb.New(ct.Clock.Now().UTC().Add(10 * time.Minute)),
   838  				TaskId:   "task-foo",
   839  			}
   840  			runAndVerify := func(expectedMsgs []struct {
   841  				clid int64
   842  				msg  string
   843  			}) {
   844  				res, err := h.OnSubmissionCompleted(ctx, rs, sc)
   845  				So(err, ShouldBeNil)
   846  				So(res.State.Status, ShouldEqual, run.Status_SUBMITTING)
   847  				for i, f := range sc.GetClFailures().GetFailures() {
   848  					So(res.State.Submission.GetFailedCls()[i], ShouldEqual, f.GetClid())
   849  				}
   850  				So(res.SideEffectFn, ShouldBeNil)
   851  				So(res.PreserveEvents, ShouldBeFalse)
   852  				So(res.PostProcessFn, ShouldBeNil)
   853  				So(res.State.OngoingLongOps.GetOps(), ShouldHaveLength, 1)
   854  				for i, f := range sc.GetClFailures().GetFailures() {
   855  					So(res.State.Submission.GetFailedCls()[i], ShouldEqual, f.GetClid())
   856  				}
   857  				for _, op := range res.State.OngoingLongOps.GetOps() {
   858  					So(op.GetResetTriggers(), ShouldNotBeNil)
   859  					expectedRequests := make([]*run.OngoingLongOps_Op_ResetTriggers_Request, len(expectedMsgs))
   860  					for i, expectedMsg := range expectedMsgs {
   861  						expectedRequests[i] = &run.OngoingLongOps_Op_ResetTriggers_Request{
   862  							Clid:    expectedMsg.clid,
   863  							Message: expectedMsg.msg,
   864  							Notify: gerrit.Whoms{
   865  								gerrit.Whom_OWNER,
   866  								gerrit.Whom_CQ_VOTERS,
   867  							},
   868  							AddToAttention: gerrit.Whoms{
   869  								gerrit.Whom_OWNER,
   870  								gerrit.Whom_CQ_VOTERS,
   871  							},
   872  							AddToAttentionReason: submissionFailureAttentionReason,
   873  						}
   874  					}
   875  					So(op.GetResetTriggers().GetRequests(), ShouldResembleProto, expectedRequests)
   876  					So(op.GetResetTriggers().GetRunStatusIfSucceeded(), ShouldEqual, run.Status_FAILED)
   877  				}
   878  			}
   879  
   880  			Convey("Single CL Run", func() {
   881  				rs.Submission.Cls = []int64{2}
   882  				Convey("CL Submission failure", func() {
   883  					sc.FailureReason = &eventpb.SubmissionCompleted_ClFailures{
   884  						ClFailures: &eventpb.SubmissionCompleted_CLSubmissionFailures{
   885  							Failures: []*eventpb.SubmissionCompleted_CLSubmissionFailure{
   886  								{
   887  									Clid:    2,
   888  									Message: "CV failed to submit this CL because of merge conflict",
   889  								},
   890  							},
   891  						},
   892  					}
   893  					runAndVerify([]struct {
   894  						clid int64
   895  						msg  string
   896  					}{
   897  						{
   898  							clid: 2,
   899  							msg:  "CV failed to submit this CL because of merge conflict",
   900  						},
   901  					})
   902  				})
   903  
   904  				Convey("Unclassified failure", func() {
   905  					runAndVerify([]struct {
   906  						clid int64
   907  						msg  string
   908  					}{
   909  						{
   910  							clid: 2,
   911  							msg:  defaultMsg,
   912  						},
   913  					})
   914  				})
   915  			})
   916  
   917  			Convey("Multi CLs Run", func() {
   918  				rs.Submission.Cls = []int64{2, 1}
   919  				Convey("None of the CLs are submitted", func() {
   920  					Convey("CL Submission failure", func() {
   921  						sc.FailureReason = &eventpb.SubmissionCompleted_ClFailures{
   922  							ClFailures: &eventpb.SubmissionCompleted_CLSubmissionFailures{
   923  								Failures: []*eventpb.SubmissionCompleted_CLSubmissionFailure{
   924  									{
   925  										Clid:    2,
   926  										Message: "Failed to submit this CL because of merge conflict",
   927  									},
   928  								},
   929  							},
   930  						}
   931  						Convey("With root CL", func() {
   932  							rs.RootCL = 1
   933  							runAndVerify([]struct {
   934  								clid int64
   935  								msg  string
   936  							}{
   937  								{
   938  									clid: 1,
   939  									msg:  "Failed to submit the following CL(s):\n* https://x-review.example.com/c/2222: Failed to submit this CL because of merge conflict\n\nNone of the CLs in the Run has been submitted. CLs:\n* https://x-review.example.com/c/2222\n* https://x-review.example.com/c/1111",
   940  								},
   941  							})
   942  						})
   943  						Convey("Without root CL", func() {
   944  							runAndVerify([]struct {
   945  								clid int64
   946  								msg  string
   947  							}{
   948  								{
   949  									clid: 1,
   950  									msg:  "This CL is not submitted because submission has failed for the following CL(s) which this CL depends on.\n* https://x-review.example.com/c/2222\n\nNone of the CLs in the Run has been submitted. CLs:\n* https://x-review.example.com/c/2222\n* https://x-review.example.com/c/1111",
   951  								},
   952  								{
   953  									clid: 2,
   954  									msg:  "Failed to submit this CL because of merge conflict\n\nNone of the CLs in the Run has been submitted. CLs:\n* https://x-review.example.com/c/2222\n* https://x-review.example.com/c/1111",
   955  								},
   956  							})
   957  						})
   958  					})
   959  
   960  					Convey("Unclassified failure", func() {
   961  						Convey("With root CL", func() {
   962  							rs.RootCL = 1
   963  							runAndVerify([]struct {
   964  								clid int64
   965  								msg  string
   966  							}{
   967  								{
   968  									clid: 1,
   969  									msg:  defaultMsg + "\n\nNone of the CLs in the Run has been submitted. CLs:\n* https://x-review.example.com/c/2222\n* https://x-review.example.com/c/1111",
   970  								},
   971  							})
   972  						})
   973  						Convey("Without root CL", func() {
   974  							runAndVerify([]struct {
   975  								clid int64
   976  								msg  string
   977  							}{
   978  								{
   979  									clid: 1,
   980  									msg:  defaultMsg + "\n\nNone of the CLs in the Run has been submitted. CLs:\n* https://x-review.example.com/c/2222\n* https://x-review.example.com/c/1111",
   981  								},
   982  								{
   983  									clid: 2,
   984  									msg:  defaultMsg + "\n\nNone of the CLs in the Run has been submitted. CLs:\n* https://x-review.example.com/c/2222\n* https://x-review.example.com/c/1111",
   985  								},
   986  							})
   987  						})
   988  					})
   989  				})
   990  
   991  				Convey("CLs partially submitted", func() {
   992  					rs.Submission.SubmittedCls = []int64{2}
   993  					ct.GFake.MutateChange(gHost, int(ci2.GetNumber()), func(c *gf.Change) {
   994  						gf.PS(int(ci2.GetRevisions()[ci2.GetCurrentRevision()].GetNumber()) + 1)(c.Info)
   995  						gf.Status(gerritpb.ChangeStatus_MERGED)(c.Info)
   996  					})
   997  
   998  					Convey("CL Submission failure", func() {
   999  						sc.FailureReason = &eventpb.SubmissionCompleted_ClFailures{
  1000  							ClFailures: &eventpb.SubmissionCompleted_CLSubmissionFailures{
  1001  								Failures: []*eventpb.SubmissionCompleted_CLSubmissionFailure{
  1002  									{
  1003  										Clid:    1,
  1004  										Message: "Failed to submit this CL because of merge conflict",
  1005  									},
  1006  								},
  1007  							},
  1008  						}
  1009  						runAndVerify([]struct {
  1010  							clid int64
  1011  							msg  string
  1012  						}{
  1013  							{
  1014  								clid: 1,
  1015  								msg:  "Failed to submit this CL because of merge conflict\n\nCLs in the Run have been submitted partially.\nNot submitted:\n* https://x-review.example.com/c/1111\nSubmitted:\n* https://x-review.example.com/c/2222\nPlease, use your judgement to determine if already submitted CLs have to be reverted, or if the remaining CLs could be manually submitted. If you think the partially submitted CLs may have broken the tip-of-tree of your project, consider notifying your infrastructure team/gardeners/sheriffs.",
  1016  							},
  1017  						})
  1018  
  1019  						// Verify posting message to the submitted CL about the failure
  1020  						// on the dependent CLs
  1021  						reqs := selfSetReviewRequests()
  1022  						So(reqs, ShouldHaveLength, 1)
  1023  						So(reqs[0].GetNumber(), ShouldEqual, ci2.GetNumber())
  1024  						assertNotify(reqs[0], 99, 100, 101)
  1025  						assertAttentionSet(reqs[0], "failed to submit dependent CLs", 99, 100, 101)
  1026  						So(reqs[0].Message, ShouldContainSubstring, "This CL is submitted. However, submission has failed for the following CL(s) which depend on this CL.")
  1027  					})
  1028  
  1029  					Convey("don't attempt posting dependent failure message if posted already", func() {
  1030  						ct.GFake.MutateChange(gHost, int(ci2.GetNumber()), func(c *gf.Change) {
  1031  							msgs := c.Info.GetMessages()
  1032  							msgs = append(msgs, &gerritpb.ChangeMessageInfo{
  1033  								Message: partiallySubmittedMsgForSubmittedCLs,
  1034  							})
  1035  							gf.Messages(msgs...)(c.Info)
  1036  						})
  1037  						runAndVerify([]struct {
  1038  							clid int64
  1039  							msg  string
  1040  						}{
  1041  							{
  1042  								clid: 1,
  1043  								msg:  defaultMsg + "\n\nCLs in the Run have been submitted partially.\nNot submitted:\n* https://x-review.example.com/c/1111\nSubmitted:\n* https://x-review.example.com/c/2222\nPlease, use your judgement to determine if already submitted CLs have to be reverted, or if the remaining CLs could be manually submitted. If you think the partially submitted CLs may have broken the tip-of-tree of your project, consider notifying your infrastructure team/gardeners/sheriffs.",
  1044  							},
  1045  						})
  1046  						reqs := selfSetReviewRequests()
  1047  						So(reqs, ShouldBeEmpty) // no request to ci2
  1048  					})
  1049  
  1050  					Convey("Unclassified failure", func() {
  1051  						runAndVerify([]struct {
  1052  							clid int64
  1053  							msg  string
  1054  						}{
  1055  							{
  1056  								clid: 1,
  1057  								msg:  defaultMsg + "\n\nCLs in the Run have been submitted partially.\nNot submitted:\n* https://x-review.example.com/c/1111\nSubmitted:\n* https://x-review.example.com/c/2222\nPlease, use your judgement to determine if already submitted CLs have to be reverted, or if the remaining CLs could be manually submitted. If you think the partially submitted CLs may have broken the tip-of-tree of your project, consider notifying your infrastructure team/gardeners/sheriffs.",
  1058  							},
  1059  						})
  1060  					})
  1061  				})
  1062  			})
  1063  		})
  1064  	})
  1065  }
  1066  
  1067  func TestOnCLsSubmitted(t *testing.T) {
  1068  	t.Parallel()
  1069  
  1070  	Convey("OnCLsSubmitted", t, func() {
  1071  		ct := cvtesting.Test{}
  1072  		ctx, cancel := ct.SetUp(t)
  1073  		defer cancel()
  1074  		rid := common.MakeRunID("infra", ct.Clock.Now(), 1, []byte("deadbeef"))
  1075  		rs := &state.RunState{Run: run.Run{
  1076  			ID:         rid,
  1077  			Status:     run.Status_SUBMITTING,
  1078  			CreateTime: ct.Clock.Now().UTC().Add(-2 * time.Minute),
  1079  			StartTime:  ct.Clock.Now().UTC().Add(-1 * time.Minute),
  1080  			CLs:        common.CLIDs{1, 3, 5, 7},
  1081  			Submission: &run.Submission{
  1082  				Cls: []int64{3, 1, 7, 5}, // in submission order
  1083  			},
  1084  		}}
  1085  
  1086  		h, _ := makeTestHandler(&ct)
  1087  		Convey("Single", func() {
  1088  			res, err := h.OnCLsSubmitted(ctx, rs, common.CLIDs{3})
  1089  			So(err, ShouldBeNil)
  1090  			So(res.State.Submission.SubmittedCls, ShouldResemble, []int64{3})
  1091  
  1092  		})
  1093  		Convey("Duplicate", func() {
  1094  			res, err := h.OnCLsSubmitted(ctx, rs, common.CLIDs{3, 3, 3, 3, 1, 1, 1})
  1095  			So(err, ShouldBeNil)
  1096  			So(res.State.Submission.SubmittedCls, ShouldResemble, []int64{3, 1})
  1097  		})
  1098  		Convey("Obey Submission order", func() {
  1099  			res, err := h.OnCLsSubmitted(ctx, rs, common.CLIDs{1, 3, 5, 7})
  1100  			So(err, ShouldBeNil)
  1101  			So(res.State.Submission.SubmittedCls, ShouldResemble, []int64{3, 1, 7, 5})
  1102  		})
  1103  		Convey("Merge to existing", func() {
  1104  			rs.Submission.SubmittedCls = []int64{3, 1}
  1105  			// 1 should be deduped
  1106  			res, err := h.OnCLsSubmitted(ctx, rs, common.CLIDs{1, 7})
  1107  			So(err, ShouldBeNil)
  1108  			So(res.State.Submission.SubmittedCls, ShouldResemble, []int64{3, 1, 7})
  1109  		})
  1110  		Convey("Last cl arrives first", func() {
  1111  			res, err := h.OnCLsSubmitted(ctx, rs, common.CLIDs{5})
  1112  			So(err, ShouldBeNil)
  1113  			So(res.State.Submission.SubmittedCls, ShouldResemble, []int64{5})
  1114  			rs = res.State
  1115  			res, err = h.OnCLsSubmitted(ctx, rs, common.CLIDs{1, 3})
  1116  			So(err, ShouldBeNil)
  1117  			So(res.State.Submission.SubmittedCls, ShouldResemble, []int64{3, 1, 5})
  1118  			rs = res.State
  1119  			res, err = h.OnCLsSubmitted(ctx, rs, common.CLIDs{7})
  1120  			So(err, ShouldBeNil)
  1121  			So(res.State.Submission.SubmittedCls, ShouldResemble, []int64{3, 1, 7, 5})
  1122  		})
  1123  		Convey("Error for unknown CLs", func() {
  1124  			res, err := h.OnCLsSubmitted(ctx, rs, common.CLIDs{1, 3, 5, 7, 9, 11})
  1125  			So(err, ShouldErrLike, "received CLsSubmitted event for cls not belonging to this Run: [9 11]")
  1126  			So(res, ShouldBeNil)
  1127  		})
  1128  	})
  1129  }