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 }