go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/gerrit/trigger/reset_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 trigger 16 17 import ( 18 "fmt" 19 "strconv" 20 "testing" 21 "time" 22 23 "google.golang.org/grpc/codes" 24 "google.golang.org/grpc/status" 25 "google.golang.org/protobuf/proto" 26 "google.golang.org/protobuf/types/known/timestamppb" 27 28 "go.chromium.org/luci/common/clock" 29 gerritpb "go.chromium.org/luci/common/proto/gerrit" 30 "go.chromium.org/luci/common/retry/transient" 31 "go.chromium.org/luci/gae/service/datastore" 32 33 cfgpb "go.chromium.org/luci/cv/api/config/v2" 34 "go.chromium.org/luci/cv/internal/changelist" 35 "go.chromium.org/luci/cv/internal/configs/prjcfg" 36 "go.chromium.org/luci/cv/internal/cvtesting" 37 "go.chromium.org/luci/cv/internal/gerrit" 38 gf "go.chromium.org/luci/cv/internal/gerrit/gerritfake" 39 "go.chromium.org/luci/cv/internal/run" 40 "go.chromium.org/luci/cv/internal/usertext" 41 42 // Convey package also exports `Reset` method that conflicts with the function 43 // in this package. This is a workaround since we can not dot import the convey 44 // package. 45 // 46 // WARNING: importing the below packages as "." will make So() and 47 // assertions, like ShouldErrLike, silently ignore the assertion results. 48 // e.g., So(1, ShouldBeNil) will pass. 49 c "github.com/smartystreets/goconvey/convey" 50 la "go.chromium.org/luci/common/testing/assertions" 51 ) 52 53 func TestReset(t *testing.T) { 54 t.Parallel() 55 56 c.Convey("Reset", t, func() { 57 ct := cvtesting.Test{} 58 ctx, cancel := ct.SetUp(t) 59 defer cancel() 60 61 const ownerID int64 = 5 62 const reviewerID int64 = 50 63 const triggererID int64 = 100 64 triggerer := gf.U(fmt.Sprintf("user-%d", triggererID)) 65 const gHost = "x-review.example.com" 66 const lProject = "lProject" 67 const changeNum = 10001 68 triggerTime := ct.Clock.Now().Add(-2 * time.Minute) 69 ci := gf.CI( 70 10001, gf.PS(2), 71 gf.Owner(fmt.Sprintf("user-%d", ownerID)), 72 gf.CQ(2, triggerTime, triggerer), 73 gf.Updated(clock.Now(ctx).Add(-1*time.Minute)), 74 gf.Reviewer(gf.U(fmt.Sprintf("user-%d", reviewerID))), 75 ) 76 triggers := Find(&FindInput{ChangeInfo: ci, ConfigGroup: &cfgpb.ConfigGroup{}}) 77 c.So(triggers.GetCqVoteTrigger(), la.ShouldResembleProto, &run.Trigger{ 78 Time: timestamppb.New(triggerTime), 79 Mode: string(run.FullRun), 80 Email: fmt.Sprintf("user-%d@example.com", triggererID), 81 GerritAccountId: triggererID, 82 }) 83 c.So(triggers.GetCqVoteTrigger().GerritAccountId, c.ShouldEqual, 100) 84 cl := &changelist.CL{ 85 ID: 99999, 86 ExternalID: changelist.MustGobID(gHost, int64(changeNum)), 87 EVersion: 2, 88 Snapshot: &changelist.Snapshot{ 89 ExternalUpdateTime: timestamppb.New(clock.Now(ctx).Add(-3 * time.Minute)), 90 LuciProject: lProject, 91 Patchset: 2, 92 MinEquivalentPatchset: 1, 93 Kind: &changelist.Snapshot_Gerrit{ 94 Gerrit: &changelist.Gerrit{ 95 Host: gHost, 96 Info: proto.Clone(ci).(*gerritpb.ChangeInfo), 97 }, 98 }, 99 }, 100 TriggerNewPatchsetRunAfterPS: 1, 101 } 102 c.So(datastore.Put(ctx, cl), c.ShouldBeNil) 103 ct.GFake.CreateChange(&gf.Change{ 104 Host: gHost, 105 Info: proto.Clone(ci).(*gerritpb.ChangeInfo), 106 ACLs: gf.ACLGrant(gf.OpRead, codes.PermissionDenied, lProject).Or( 107 gf.ACLGrant(gf.OpReview, codes.PermissionDenied, lProject), 108 gf.ACLGrant(gf.OpAlterVotesOfOthers, codes.PermissionDenied, lProject), 109 ), 110 }) 111 112 input := ResetInput{ 113 CL: cl, 114 ConfigGroups: []*prjcfg.ConfigGroup{{ 115 Content: &cfgpb.ConfigGroup{ 116 Verifiers: &cfgpb.Verifiers{Tryjob: &cfgpb.Verifiers_Tryjob{ 117 Builders: []*cfgpb.Verifiers_Tryjob_Builder{{ 118 Name: "new patchset upload builder", 119 ModeAllowlist: []string{string(run.NewPatchsetRun)}, 120 }}, 121 }}, 122 }, 123 }}, 124 LUCIProject: lProject, 125 Message: "Full Run has passed", 126 Requester: "test", 127 Notify: gerrit.Whoms{gerrit.Whom_OWNER, gerrit.Whom_CQ_VOTERS}, 128 AddToAttentionSet: gerrit.Whoms{gerrit.Whom_REVIEWERS}, 129 AttentionReason: usertext.StoppedRun, 130 LeaseDuration: 30 * time.Second, 131 CLMutator: changelist.NewMutator(ct.TQDispatcher, nil, nil, nil), 132 GFactory: ct.GFactory(), 133 } 134 findTriggers := func(resultCI *gerritpb.ChangeInfo) *run.Triggers { 135 for _, cg := range input.ConfigGroups { 136 if ts := Find(&FindInput{ChangeInfo: resultCI, ConfigGroup: cg.Content}); ts != nil { 137 return ts 138 } 139 } 140 return nil 141 } 142 ts := findTriggers(ci) 143 cqTrigger := ts.GetCqVoteTrigger() 144 nprTrigger := ts.GetNewPatchsetRunTrigger() 145 input.Triggers = &run.Triggers{} 146 147 c.Convey("Fails PreCondition if CL is AccessDenied from code review site", func() { 148 c.Convey("For CQ-Label trigger", func() { 149 input.Triggers.CqVoteTrigger = cqTrigger 150 }) 151 c.Convey("For NewPatchset trigger", func() { 152 input.Triggers.NewPatchsetRunTrigger = nprTrigger 153 }) 154 noAccessTime := ct.Clock.Now().UTC().Add(1 * time.Minute) 155 cl.Access = &changelist.Access{ 156 ByProject: map[string]*changelist.Access_Project{ 157 lProject: { 158 UpdateTime: timestamppb.New(noAccessTime), 159 NoAccessTime: timestamppb.New(noAccessTime), 160 }, 161 }, 162 } 163 err := Reset(ctx, input) 164 c.So(err, la.ShouldErrLike, "failed to reset trigger because CV lost access to this CL") 165 c.So(ErrResetPreconditionFailedTag.In(err), c.ShouldBeTrue) 166 }) 167 isOutdated := func(cl *changelist.CL) bool { 168 e := &changelist.CL{ID: cl.ID} 169 c.So(datastore.Get(ctx, e), c.ShouldBeNil) 170 return e.Snapshot.GetOutdated() != nil 171 } 172 173 c.Convey("Fails PreCondition if CL has newer PS in datastore", func() { 174 input.Triggers.CqVoteTrigger = cqTrigger 175 newCI := proto.Clone(ci).(*gerritpb.ChangeInfo) 176 gf.PS(3)(newCI) 177 newCL := &changelist.CL{ 178 ID: 99999, 179 ExternalID: changelist.MustGobID(gHost, int64(changeNum)), 180 EVersion: 3, 181 Snapshot: &changelist.Snapshot{ 182 ExternalUpdateTime: timestamppb.New(clock.Now(ctx).Add(-1 * time.Minute)), 183 LuciProject: lProject, 184 Patchset: 3, 185 MinEquivalentPatchset: 3, 186 Kind: &changelist.Snapshot_Gerrit{ 187 Gerrit: &changelist.Gerrit{ 188 Host: gHost, 189 Info: newCI, 190 }, 191 }, 192 }, 193 } 194 c.So(datastore.Put(ctx, newCL), c.ShouldBeNil) 195 err := Reset(ctx, input) 196 c.So(err, la.ShouldErrLike, "failed to reset because ps 2 is not current for cl(99999)") 197 c.So(ErrResetPreconditionFailedTag.In(err), c.ShouldBeTrue) 198 c.So(isOutdated(cl), c.ShouldBeFalse) 199 }) 200 201 c.Convey("Fails PreCondition if CL has newer PS in Gerrit", func() { 202 input.Triggers.CqVoteTrigger = cqTrigger 203 ct.GFake.MutateChange(gHost, int(ci.GetNumber()), func(c *gf.Change) { 204 gf.PS(3)(c.Info) 205 }) 206 err := Reset(ctx, input) 207 c.So(err, la.ShouldErrLike, "failed to reset because ps 2 is not current for x-review.example.com/10001") 208 c.So(ErrResetPreconditionFailedTag.In(err), c.ShouldBeTrue) 209 c.So(isOutdated(cl), c.ShouldBeFalse) 210 }) 211 212 c.Convey("Cancelling CQ Vote fails if receive stale data from gerrit", func() { 213 input.Triggers.CqVoteTrigger = cqTrigger 214 ct.GFake.MutateChange(gHost, int(ci.GetNumber()), func(c *gf.Change) { 215 gf.Updated(clock.Now(ctx).Add(-3 * time.Minute))(c.Info) 216 }) 217 err := Reset(ctx, input) 218 c.So(err, la.ShouldErrLike, gerrit.ErrStaleData) 219 c.So(transient.Tag.In(err), c.ShouldBeTrue) 220 c.So(isOutdated(cl), c.ShouldBeFalse) 221 }) 222 223 c.Convey("Cancelling NewPatchsetRun", func() { 224 input.Triggers.NewPatchsetRunTrigger = nprTrigger 225 input.Message = "reset new patchset run trigger" 226 227 cl := &changelist.CL{ID: input.CL.ID} 228 c.So(datastore.Get(ctx, cl), c.ShouldBeNil) 229 originalValue := cl.TriggerNewPatchsetRunAfterPS 230 231 c.So(Reset(ctx, input), c.ShouldBeNil) 232 // cancelling a new patchset run doesn't mark the snapshot 233 // as outdated. 234 c.So(isOutdated(cl), c.ShouldBeFalse) 235 236 cl = &changelist.CL{ID: input.CL.ID} 237 c.So(datastore.Get(ctx, cl), c.ShouldBeNil) 238 c.So(cl.TriggerNewPatchsetRunAfterPS, c.ShouldNotEqual, originalValue) 239 c.So(cl.TriggerNewPatchsetRunAfterPS, c.ShouldEqual, input.CL.Snapshot.Patchset) 240 change := ct.GFake.GetChange(input.CL.Snapshot.GetGerrit().GetHost(), int(input.CL.Snapshot.GetGerrit().GetInfo().GetNumber())) 241 c.So(change.Info.GetMessages()[len(change.Info.GetMessages())-1].Message, c.ShouldEqual, input.Message) 242 }) 243 244 splitSetReviewRequests := func() (onBehalf, asSelf []*gerritpb.SetReviewRequest) { 245 for _, req := range ct.GFake.Requests() { 246 switch r, ok := req.(*gerritpb.SetReviewRequest); { 247 case !ok: 248 case r.GetOnBehalfOf() != 0: 249 // OnBehalfOf removes votes and must happen before any asSelf. 250 c.So(asSelf, c.ShouldBeEmpty) 251 onBehalf = append(onBehalf, r) 252 default: 253 asSelf = append(asSelf, r) 254 } 255 } 256 return onBehalf, asSelf 257 } 258 c.Convey("cancel new patchset run and cq vote run at the same time", func() { 259 input.Triggers.CqVoteTrigger = cqTrigger 260 input.Triggers.NewPatchsetRunTrigger = nprTrigger 261 cl := &changelist.CL{ID: input.CL.ID} 262 c.So(datastore.Get(ctx, cl), c.ShouldBeNil) 263 originalValue := cl.TriggerNewPatchsetRunAfterPS 264 265 err := Reset(ctx, input) 266 c.So(err, c.ShouldBeNil) 267 c.So(isOutdated(cl), c.ShouldBeTrue) // snapshot is outdated 268 resultCI := ct.GFake.GetChange(gHost, int(ci.GetNumber())) 269 c.So(resultCI.Info.GetMessages(), c.ShouldHaveLength, 1) 270 c.So(resultCI.Info.GetMessages()[0].GetMessage(), c.ShouldEqual, input.Message) 271 c.So(gf.NonZeroVotes(resultCI.Info, CQLabelName), c.ShouldBeEmpty) 272 273 onBehalfs, asSelf := splitSetReviewRequests() 274 c.So(onBehalfs, c.ShouldHaveLength, 1) 275 c.So(onBehalfs[0].GetOnBehalfOf(), c.ShouldEqual, triggererID) 276 c.So(onBehalfs[0].GetNotifyDetails(), c.ShouldBeNil) 277 c.So(asSelf, c.ShouldHaveLength, 1) 278 c.So(asSelf[0].GetNotify(), c.ShouldEqual, gerritpb.Notify_NOTIFY_NONE) 279 c.So(asSelf[0].GetNotifyDetails(), la.ShouldResembleProto, 280 &gerritpb.NotifyDetails{ 281 Recipients: []*gerritpb.NotifyDetails_Recipient{ 282 { 283 RecipientType: gerritpb.NotifyDetails_RECIPIENT_TYPE_TO, 284 Info: &gerritpb.NotifyDetails_Info{ 285 Accounts: []int64{ownerID, triggererID}, 286 }, 287 }, 288 }, 289 }) 290 c.So(asSelf[0].GetAddToAttentionSet(), la.ShouldResembleProto, []*gerritpb.AttentionSetInput{ 291 {User: strconv.FormatInt(reviewerID, 10), Reason: "ps#2: " + usertext.StoppedRun}, 292 }) 293 cl = &changelist.CL{ID: input.CL.ID} 294 c.So(datastore.Get(ctx, cl), c.ShouldBeNil) 295 c.So(cl.TriggerNewPatchsetRunAfterPS, c.ShouldNotEqual, originalValue) 296 c.So(cl.TriggerNewPatchsetRunAfterPS, c.ShouldEqual, input.CL.Snapshot.Patchset) 297 }) 298 c.Convey("Remove single vote", func() { 299 input.Triggers.CqVoteTrigger = cqTrigger 300 err := Reset(ctx, input) 301 c.So(err, c.ShouldBeNil) 302 c.So(isOutdated(cl), c.ShouldBeTrue) // snapshot is outdated 303 resultCI := ct.GFake.GetChange(gHost, int(ci.GetNumber())) 304 c.So(resultCI.Info.GetMessages(), c.ShouldHaveLength, 1) 305 c.So(resultCI.Info.GetMessages()[0].GetMessage(), c.ShouldEqual, input.Message) 306 c.So(gf.NonZeroVotes(resultCI.Info, CQLabelName), c.ShouldBeEmpty) 307 308 onBehalfs, asSelf := splitSetReviewRequests() 309 c.So(onBehalfs, c.ShouldHaveLength, 1) 310 c.So(onBehalfs[0].GetOnBehalfOf(), c.ShouldEqual, triggererID) 311 c.So(onBehalfs[0].GetNotifyDetails(), c.ShouldBeNil) 312 c.So(asSelf, c.ShouldHaveLength, 1) 313 c.So(asSelf[0].GetNotify(), c.ShouldEqual, gerritpb.Notify_NOTIFY_NONE) 314 c.So(asSelf[0].GetNotifyDetails(), la.ShouldResembleProto, 315 &gerritpb.NotifyDetails{ 316 Recipients: []*gerritpb.NotifyDetails_Recipient{ 317 { 318 RecipientType: gerritpb.NotifyDetails_RECIPIENT_TYPE_TO, 319 Info: &gerritpb.NotifyDetails_Info{ 320 Accounts: []int64{ownerID, triggererID}, 321 }, 322 }, 323 }, 324 }) 325 c.So(asSelf[0].GetAddToAttentionSet(), la.ShouldResembleProto, []*gerritpb.AttentionSetInput{ 326 {User: strconv.FormatInt(reviewerID, 10), Reason: "ps#2: " + usertext.StoppedRun}, 327 }) 328 c.So(asSelf[0].GetTag(), c.ShouldResemble, fmt.Sprintf("autogenerated:cq:full-run:%d", triggerTime.Unix())) 329 }) 330 331 c.Convey("Remove multiple votes", func() { 332 input.Triggers.CqVoteTrigger = cqTrigger 333 ct.GFake.MutateChange(gHost, int(ci.GetNumber()), func(c *gf.Change) { 334 gf.CQ(1, clock.Now(ctx).Add(-130*time.Second), gf.U("user-1"))(c.Info) 335 gf.CQ(2, clock.Now(ctx).Add(-110*time.Second), gf.U("user-70"))(c.Info) 336 gf.CQ(1, clock.Now(ctx).Add(-100*time.Second), gf.U("user-1000"))(c.Info) 337 }) 338 339 c.Convey("Success", func() { 340 err := Reset(ctx, input) 341 c.So(err, c.ShouldBeNil) 342 c.So(isOutdated(cl), c.ShouldBeTrue) // snapshot is outdated 343 resultCI := ct.GFake.GetChange(gHost, int(ci.GetNumber())) 344 c.So(resultCI.Info.GetMessages(), c.ShouldHaveLength, 1) 345 c.So(resultCI.Info.GetMessages()[0].GetMessage(), c.ShouldEqual, input.Message) 346 c.So(gf.NonZeroVotes(resultCI.Info, CQLabelName), c.ShouldBeEmpty) 347 348 onBehalfs, asSelf := splitSetReviewRequests() 349 for _, r := range onBehalfs { 350 c.So(r.GetNotify(), c.ShouldEqual, gerritpb.Notify_NOTIFY_NONE) 351 c.So(r.GetNotifyDetails(), c.ShouldBeNil) 352 } 353 // The triggering vote(s) must have been removed last, the order of 354 // removals for the rest doesn't matter so long as it does the job. 355 c.So(onBehalfs[len(onBehalfs)-1].GetOnBehalfOf(), c.ShouldEqual, 100) 356 c.So(asSelf, c.ShouldHaveLength, 1) 357 c.So(asSelf[0].GetNotify(), c.ShouldEqual, gerritpb.Notify_NOTIFY_NONE) 358 c.So(asSelf[0].GetNotifyDetails(), la.ShouldResembleProto, 359 &gerritpb.NotifyDetails{ 360 Recipients: []*gerritpb.NotifyDetails_Recipient{ 361 { 362 RecipientType: gerritpb.NotifyDetails_RECIPIENT_TYPE_TO, 363 Info: &gerritpb.NotifyDetails_Info{ 364 Accounts: []int64{1, ownerID, 70, triggererID, 1000}, 365 }, 366 }, 367 }, 368 }) 369 c.So(asSelf[0].GetAddToAttentionSet(), la.ShouldResembleProto, []*gerritpb.AttentionSetInput{ 370 {User: strconv.FormatInt(reviewerID, 10), Reason: "ps#2: " + usertext.StoppedRun}, 371 }) 372 }) 373 374 c.Convey("Removing non-triggering votes fails", func() { 375 ct.GFake.MutateChange(gHost, int(ci.GetNumber()), func(c *gf.Change) { 376 c.ACLs = gf.ACLGrant(gf.OpRead, codes.PermissionDenied, lProject).Or( 377 gf.ACLGrant(gf.OpReview, codes.PermissionDenied, lProject), 378 ) // no permission to vote on behalf of others 379 }) 380 err := Reset(ctx, input) 381 c.So(err, c.ShouldBeNil) 382 c.So(isOutdated(cl), c.ShouldBeFalse) 383 onBehalfs, _ := splitSetReviewRequests() 384 c.So(onBehalfs, c.ShouldHaveLength, 3) // all non-triggering votes 385 for _, r := range onBehalfs { 386 switch r.GetOnBehalfOf() { 387 case triggererID: 388 // CV shouldn't remove triggering votes if removal of non-triggering 389 // votes fails. 390 c.So(r.GetOnBehalfOf(), c.ShouldNotEqual, triggererID) 391 case 1, 70, 1000: 392 default: 393 panic(fmt.Errorf("unknown on_behalf_of %d", r.GetOnBehalfOf())) 394 } 395 } 396 }) 397 }) 398 399 c.Convey("Removing votes from non-CQ labels used in additional modes", func() { 400 const uLabel = "Ultra-Quick-Label" 401 const qLabel = "Quick-Label" 402 input.Triggers.CqVoteTrigger = cqTrigger 403 input.ConfigGroups = []*prjcfg.ConfigGroup{ 404 { 405 Content: &cfgpb.ConfigGroup{ 406 AdditionalModes: []*cfgpb.Mode{ 407 { 408 Name: "ULTRA_QUICK_RUN", 409 CqLabelValue: 1, 410 TriggeringLabel: uLabel, 411 TriggeringValue: 1, 412 }, 413 { 414 Name: "QUICK_RUN", 415 CqLabelValue: 1, 416 TriggeringLabel: qLabel, 417 TriggeringValue: 1, 418 }, 419 }, 420 }, 421 }, 422 } 423 424 ultraQuick := func(value int, timeAndUser ...any) gf.CIModifier { 425 return gf.Vote(uLabel, value, timeAndUser...) 426 } 427 quick := func(value int, timeAndUser ...any) gf.CIModifier { 428 return gf.Vote(qLabel, value, timeAndUser...) 429 } 430 // Exact timestamps don't matter in this test, but in practice they affect 431 // computation of the triggering vote. 432 ct.GFake.MutateChange(gHost, int(ci.GetNumber()), func(c *gf.Change) { 433 // user-99 forgot to vote CQ+1. 434 quick(1, clock.Now(ctx).Add(-300*time.Second), gf.U("user-99"))(c.Info) 435 ultraQuick(1, clock.Now(ctx).Add(-200*time.Second), gf.U("user-99"))(c.Info) 436 437 // user-100 actually triggered an ULTRA_QUICK_RUN. 438 gf.CQ(1, clock.Now(ctx).Add(-150*time.Second), gf.U("user-100"))(c.Info) 439 ultraQuick(1, clock.Now(ctx).Add(-150*time.Second), gf.U("user-100"))(c.Info) 440 quick(1, clock.Now(ctx).Add(-150*time.Second), gf.U("user-100"))(c.Info) 441 442 // user-101 CQ+1 was a noop. 443 gf.CQ(1, clock.Now(ctx).Add(-120*time.Second), gf.U("user-101"))(c.Info) 444 445 // user-102 votes for a QUICK_RUN is a noop, but should be removed as 446 // as well. 447 gf.CQ(1, clock.Now(ctx).Add(-110*time.Second), gf.U("user-101"))(c.Info) 448 quick(1, clock.Now(ctx).Add(-110*time.Second), gf.U("user-102"))(c.Info) 449 450 // user-103 votes is a noop, though weird, yet still must be removed. 451 ultraQuick(3, clock.Now(ctx).Add(-100*time.Second), gf.U("user-104"))(c.Info) 452 453 // user-104 votes is 0, and doesn't need a reset. 454 ultraQuick(0, clock.Now(ctx).Add(-90*time.Second), gf.U("user-104"))(c.Info) 455 }) 456 err := Reset(ctx, input) 457 c.So(err, c.ShouldBeNil) 458 c.So(isOutdated(cl), c.ShouldBeTrue) // snapshot is outdated 459 460 resultCI := ct.GFake.GetChange(gHost, int(ci.GetNumber())) 461 c.So(gf.NonZeroVotes(resultCI.Info, CQLabelName), c.ShouldBeEmpty) 462 c.So(gf.NonZeroVotes(resultCI.Info, qLabel), c.ShouldBeEmpty) 463 c.So(gf.NonZeroVotes(resultCI.Info, uLabel), c.ShouldBeEmpty) 464 465 onBehalfs, _ := splitSetReviewRequests() 466 // The last request must be for account 100. 467 c.So(onBehalfs[len(onBehalfs)-1].GetOnBehalfOf(), c.ShouldEqual, 100) 468 c.So(onBehalfs[len(onBehalfs)-1].GetLabels(), c.ShouldResemble, map[string]int32{ 469 CQLabelName: 0, 470 qLabel: 0, 471 uLabel: 0, 472 }) 473 }) 474 475 c.Convey("Skips zero votes", func() { 476 input.Triggers.CqVoteTrigger = cqTrigger 477 ct.GFake.MutateChange(gHost, int(ci.GetNumber()), func(c *gf.Change) { 478 gf.CQ(0, clock.Now(ctx).Add(-90*time.Second), gf.U("user-101"))(c.Info) 479 gf.CQ(0, clock.Now(ctx).Add(-100*time.Second), gf.U("user-102"))(c.Info) 480 gf.CQ(0, clock.Now(ctx).Add(-110*time.Second), gf.U("user-103"))(c.Info) 481 }) 482 483 err := Reset(ctx, input) 484 c.So(err, c.ShouldBeNil) 485 c.So(isOutdated(cl), c.ShouldBeTrue) // snapshot is outdated 486 resultCI := ct.GFake.GetChange(gHost, int(ci.GetNumber())) 487 c.So(resultCI.Info.GetMessages(), c.ShouldHaveLength, 1) 488 c.So(resultCI.Info.GetMessages()[0].GetMessage(), c.ShouldEqual, input.Message) 489 c.So(gf.NonZeroVotes(resultCI.Info, CQLabelName), c.ShouldBeEmpty) 490 onBehalfs, _ := splitSetReviewRequests() 491 c.So(onBehalfs, c.ShouldHaveLength, 1) 492 c.So(onBehalfs[0].GetOnBehalfOf(), c.ShouldEqual, triggererID) 493 }) 494 495 c.Convey("Post Message even if triggering votes has been removed already", func() { 496 input.Triggers.CqVoteTrigger = cqTrigger 497 ct.GFake.MutateChange(gHost, int(ci.GetNumber()), func(c *gf.Change) { 498 gf.CQ(0, clock.Now(ctx), triggerer)(c.Info) 499 }) 500 err := Reset(ctx, input) 501 c.So(err, c.ShouldBeNil) 502 c.So(isOutdated(cl), c.ShouldBeTrue) // snapshot is outdated 503 resultCI := ct.GFake.GetChange(gHost, int(ci.GetNumber())) 504 c.So(resultCI.Info.GetMessages(), c.ShouldHaveLength, 1) 505 c.So(resultCI.Info.GetMessages()[0].GetMessage(), c.ShouldEqual, input.Message) 506 }) 507 508 c.Convey("Post Message if CV has no permission to vote", func() { 509 input.Triggers.CqVoteTrigger = cqTrigger 510 ct.GFake.MutateChange(gHost, int(ci.GetNumber()), func(c *gf.Change) { 511 c.ACLs = gf.ACLGrant(gf.OpRead, codes.PermissionDenied, lProject).Or( 512 // Needed to post comments 513 gf.ACLGrant(gf.OpReview, codes.PermissionDenied, lProject), 514 ) 515 }) 516 c.So(Reset(ctx, input), c.ShouldBeNil) 517 c.So(isOutdated(cl), c.ShouldBeFalse) 518 resultCI := ct.GFake.GetChange(gHost, int(ci.GetNumber())).Info 519 // CQ+2 vote remains. 520 c.So(gf.NonZeroVotes(resultCI, CQLabelName), la.ShouldResembleProto, []*gerritpb.ApprovalInfo{ 521 { 522 User: triggerer, 523 Value: 2, 524 Date: timestamppb.New(triggerTime), 525 }, 526 }) 527 // But CL is no longer triggered. 528 c.So(findTriggers(resultCI).GetCqVoteTrigger(), c.ShouldBeNil) 529 // Still, user should know what happened. 530 expectedMsg := input.Message + ` 531 532 CV failed to unset the Commit-Queue label on your behalf. Please unvote and revote on the Commit-Queue label to retry. 533 534 Bot data: {"action":"cancel","triggered_at":"2020-02-02T10:28:00Z","revision":"rev-010001-002"}` 535 c.So(resultCI.GetMessages()[0].GetMessage(), c.ShouldEqual, expectedMsg) 536 }) 537 538 c.Convey("Post Message if change is in bad state", func() { 539 input.Triggers.CqVoteTrigger = cqTrigger 540 ct.GFake.MutateChange(gHost, int(ci.GetNumber()), func(c *gf.Change) { 541 gf.Status(gerritpb.ChangeStatus_ABANDONED)(c.Info) 542 c.ACLs = func(op gf.Operation, _ string) *status.Status { 543 if op == gf.OpAlterVotesOfOthers { 544 return status.New(codes.FailedPrecondition, "change abandoned, no vote removals allowed") 545 } 546 return status.New(codes.OK, "") 547 } 548 }) 549 err := Reset(ctx, input) 550 c.So(err, c.ShouldBeNil) 551 c.So(isOutdated(cl), c.ShouldBeFalse) 552 resultCI := ct.GFake.GetChange(gHost, int(ci.GetNumber())).Info 553 // CQ+2 vote remains. 554 c.So(gf.NonZeroVotes(resultCI, CQLabelName), la.ShouldResembleProto, []*gerritpb.ApprovalInfo{ 555 { 556 User: triggerer, 557 Value: 2, 558 Date: timestamppb.New(triggerTime), 559 }, 560 }) 561 // But CL is no longer triggered. 562 c.So(findTriggers(resultCI).GetCqVoteTrigger(), c.ShouldBeNil) 563 // Still, user should know what happened. 564 c.So(resultCI.GetMessages(), c.ShouldHaveLength, 1) 565 c.So(resultCI.GetMessages()[0].GetMessage(), c.ShouldContainSubstring, "CV failed to unset the Commit-Queue label on your behalf") 566 }) 567 568 c.Convey("Post Message also fails", func() { 569 input.Triggers.CqVoteTrigger = cqTrigger 570 ct.GFake.MutateChange(gHost, int(ci.GetNumber()), func(c *gf.Change) { 571 c.ACLs = gf.ACLGrant(gf.OpRead, codes.PermissionDenied, lProject) 572 }) 573 err := Reset(ctx, input) 574 c.So(err, la.ShouldErrLike, "no permission to remove vote x-review.example.com/10001") 575 c.So(isOutdated(cl), c.ShouldBeFalse) 576 c.So(ErrResetPermanentTag.In(err), c.ShouldBeTrue) 577 resultCI := ct.GFake.GetChange(gHost, int(ci.GetNumber())).Info 578 c.So(gf.NonZeroVotes(resultCI, CQLabelName), la.ShouldResembleProto, []*gerritpb.ApprovalInfo{ 579 { 580 User: triggerer, 581 Value: 2, 582 Date: timestamppb.New(triggerTime), 583 }, 584 }) 585 c.So(resultCI.GetMessages(), c.ShouldBeEmpty) 586 }) 587 }) 588 }