blob: 8929608c1b38635a10322febd0dd432cd596caba [file] [log] [blame]
// Copyright 2021 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cancel
import (
"fmt"
"strconv"
"testing"
"time"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"
"go.chromium.org/luci/common/clock"
gerritpb "go.chromium.org/luci/common/proto/gerrit"
"go.chromium.org/luci/common/retry/transient"
"go.chromium.org/luci/gae/service/datastore"
cfgpb "go.chromium.org/luci/cv/api/config/v2"
"go.chromium.org/luci/cv/internal/changelist"
"go.chromium.org/luci/cv/internal/configs/prjcfg"
"go.chromium.org/luci/cv/internal/cvtesting"
"go.chromium.org/luci/cv/internal/gerrit"
gf "go.chromium.org/luci/cv/internal/gerrit/gerritfake"
"go.chromium.org/luci/cv/internal/gerrit/trigger"
"go.chromium.org/luci/cv/internal/run"
"go.chromium.org/luci/cv/internal/usertext"
. "github.com/smartystreets/goconvey/convey"
. "go.chromium.org/luci/common/testing/assertions"
)
func TestCancel(t *testing.T) {
t.Parallel()
Convey("Cancel", t, func() {
ct := cvtesting.Test{}
ctx, cancel := ct.SetUp()
defer cancel()
const ownerID int64 = 5
const reviewerID int64 = 50
const triggererID int64 = 100
triggerer := gf.U(fmt.Sprintf("user-%d", triggererID))
const gHost = "x-review.example.com"
const lProject = "lProject"
const changeNum = 10001
triggerTime := ct.Clock.Now().Add(-2 * time.Minute)
ci := gf.CI(
10001, gf.PS(2),
gf.Owner(fmt.Sprintf("user-%d", ownerID)),
gf.CQ(2, triggerTime, triggerer),
gf.Updated(clock.Now(ctx).Add(-1*time.Minute)),
gf.Reviewer(gf.U(fmt.Sprintf("user-%d", reviewerID))),
)
triggers := trigger.Find(&trigger.FindInput{ChangeInfo: ci, ConfigGroup: &cfgpb.ConfigGroup{}})
So(triggers.GetCqVoteTrigger(), ShouldResembleProto, &run.Trigger{
Time: timestamppb.New(triggerTime),
Mode: string(run.FullRun),
Email: fmt.Sprintf("user-%d@example.com", triggererID),
GerritAccountId: triggererID,
})
So(triggers.GetCqVoteTrigger().GerritAccountId, ShouldEqual, 100)
cl := &changelist.CL{
ID: 99999,
ExternalID: changelist.MustGobID(gHost, int64(changeNum)),
EVersion: 2,
Snapshot: &changelist.Snapshot{
ExternalUpdateTime: timestamppb.New(clock.Now(ctx).Add(-3 * time.Minute)),
LuciProject: lProject,
Patchset: 2,
MinEquivalentPatchset: 1,
Kind: &changelist.Snapshot_Gerrit{
Gerrit: &changelist.Gerrit{
Host: gHost,
Info: proto.Clone(ci).(*gerritpb.ChangeInfo),
},
},
},
TriggerNewPatchsetRunAfterPS: 1,
}
So(datastore.Put(ctx, cl), ShouldBeNil)
ct.GFake.CreateChange(&gf.Change{
Host: gHost,
Info: proto.Clone(ci).(*gerritpb.ChangeInfo),
ACLs: gf.ACLGrant(gf.OpRead, codes.PermissionDenied, lProject).Or(
gf.ACLGrant(gf.OpReview, codes.PermissionDenied, lProject),
gf.ACLGrant(gf.OpAlterVotesOfOthers, codes.PermissionDenied, lProject),
),
})
input := Input{
CL: cl,
ConfigGroups: []*prjcfg.ConfigGroup{{
Content: &cfgpb.ConfigGroup{
Verifiers: &cfgpb.Verifiers{Tryjob: &cfgpb.Verifiers_Tryjob{
Builders: []*cfgpb.Verifiers_Tryjob_Builder{{
Name: "new patchset upload builder",
ModeAllowlist: []string{string(run.NewPatchsetRun)},
}},
}},
},
}},
LUCIProject: lProject,
Message: "Full Run has passed",
Requester: "test",
Notify: gerrit.Whoms{gerrit.Owner, gerrit.CQVoters},
AddToAttentionSet: gerrit.Whoms{gerrit.Reviewers},
AttentionReason: usertext.StoppedRun,
LeaseDuration: 30 * time.Second,
RunCLExternalIDs: []changelist.ExternalID{
changelist.MustGobID(gHost, int64(10002)),
changelist.MustGobID(gHost, int64(10003)),
},
CLMutator: changelist.NewMutator(ct.TQDispatcher, nil, nil, nil),
GFactory: ct.GFactory(),
}
findTriggers := func(resultCI *gerritpb.ChangeInfo) *run.Triggers {
for _, cg := range input.ConfigGroups {
if ts := trigger.Find(&trigger.FindInput{ChangeInfo: resultCI, ConfigGroup: cg.Content}); ts != nil {
return ts
}
}
return nil
}
ts := findTriggers(ci)
cqTrigger := ts.GetCqVoteTrigger()
nprTrigger := ts.GetNewPatchsetRunTrigger()
input.Triggers = &run.Triggers{}
Convey("Fails PreCondition if CL is AccessDenied from code review site", func() {
Convey("For CQ-Label trigger", func() {
input.Triggers.CqVoteTrigger = cqTrigger
})
Convey("For NewPatchset trigger", func() {
input.Triggers.NewPatchsetRunTrigger = nprTrigger
})
noAccessTime := ct.Clock.Now().UTC().Add(1 * time.Minute)
cl.Access = &changelist.Access{
ByProject: map[string]*changelist.Access_Project{
lProject: {
UpdateTime: timestamppb.New(noAccessTime),
NoAccessTime: timestamppb.New(noAccessTime),
},
},
}
err := Cancel(ctx, input)
So(err, ShouldErrLike, "failed to cancel trigger because CV lost access to this CL")
So(ErrPreconditionFailedTag.In(err), ShouldBeTrue)
})
Convey("Fails PreCondition if CL has newer PS in datastore", func() {
input.Triggers.CqVoteTrigger = cqTrigger
newCI := proto.Clone(ci).(*gerritpb.ChangeInfo)
gf.PS(3)(newCI)
newCL := &changelist.CL{
ID: 99999,
ExternalID: changelist.MustGobID(gHost, int64(changeNum)),
EVersion: 3,
Snapshot: &changelist.Snapshot{
ExternalUpdateTime: timestamppb.New(clock.Now(ctx).Add(-1 * time.Minute)),
LuciProject: lProject,
Patchset: 3,
MinEquivalentPatchset: 3,
Kind: &changelist.Snapshot_Gerrit{
Gerrit: &changelist.Gerrit{
Host: gHost,
Info: newCI,
},
},
},
}
So(datastore.Put(ctx, newCL), ShouldBeNil)
err := Cancel(ctx, input)
So(err, ShouldErrLike, "failed to cancel because ps 2 is not current for cl(99999)")
So(ErrPreconditionFailedTag.In(err), ShouldBeTrue)
})
Convey("Fails PreCondition if CL has newer PS in Gerrit", func() {
input.Triggers.CqVoteTrigger = cqTrigger
ct.GFake.MutateChange(gHost, int(ci.GetNumber()), func(c *gf.Change) {
gf.PS(3)(c.Info)
})
err := Cancel(ctx, input)
So(err, ShouldErrLike, "failed to cancel because ps 2 is not current for x-review.example.com/10001")
So(ErrPreconditionFailedTag.In(err), ShouldBeTrue)
})
Convey("Cancelling CQ Vote fails if receive stale data from gerrit", func() {
input.Triggers.CqVoteTrigger = cqTrigger
ct.GFake.MutateChange(gHost, int(ci.GetNumber()), func(c *gf.Change) {
gf.Updated(clock.Now(ctx).Add(-3 * time.Minute))(c.Info)
})
err := Cancel(ctx, input)
So(err, ShouldErrLike, gerrit.ErrStaleData)
So(transient.Tag.In(err), ShouldBeTrue)
})
Convey("Cancelling NewPatchsetRun", func() {
input.Triggers.NewPatchsetRunTrigger = nprTrigger
input.Message = "cancelling new patchset run trigger"
cl := &changelist.CL{ID: input.CL.ID}
So(datastore.Get(ctx, cl), ShouldBeNil)
originalValue := cl.TriggerNewPatchsetRunAfterPS
So(Cancel(ctx, input), ShouldBeNil)
cl = &changelist.CL{ID: input.CL.ID}
So(datastore.Get(ctx, cl), ShouldBeNil)
So(cl.TriggerNewPatchsetRunAfterPS, ShouldNotEqual, originalValue)
So(cl.TriggerNewPatchsetRunAfterPS, ShouldEqual, input.CL.Snapshot.Patchset)
change := ct.GFake.GetChange(input.CL.Snapshot.GetGerrit().GetHost(), int(input.CL.Snapshot.GetGerrit().GetInfo().GetNumber()))
So(change.Info.GetMessages()[len(change.Info.GetMessages())-1].Message, ShouldEqual, input.Message)
})
splitSetReviewRequests := func() (onBehalf, asSelf []*gerritpb.SetReviewRequest) {
for _, req := range ct.GFake.Requests() {
switch r, ok := req.(*gerritpb.SetReviewRequest); {
case !ok:
case r.GetOnBehalfOf() != 0:
// OnBehalfOf removes votes and must happen before any asSelf.
So(asSelf, ShouldBeEmpty)
onBehalf = append(onBehalf, r)
default:
asSelf = append(asSelf, r)
}
}
return onBehalf, asSelf
}
Convey("cancel new patchset run and cq vote run at the same time", func() {
input.Triggers.CqVoteTrigger = cqTrigger
input.Triggers.NewPatchsetRunTrigger = nprTrigger
cl := &changelist.CL{ID: input.CL.ID}
So(datastore.Get(ctx, cl), ShouldBeNil)
originalValue := cl.TriggerNewPatchsetRunAfterPS
err := Cancel(ctx, input)
So(err, ShouldBeNil)
resultCI := ct.GFake.GetChange(gHost, int(ci.GetNumber()))
So(resultCI.Info.GetMessages(), ShouldHaveLength, 1)
So(resultCI.Info.GetMessages()[0].GetMessage(), ShouldEqual, input.Message)
So(gf.NonZeroVotes(resultCI.Info, trigger.CQLabelName), ShouldBeEmpty)
onBehalfs, asSelf := splitSetReviewRequests()
So(onBehalfs, ShouldHaveLength, 1)
So(onBehalfs[0].GetOnBehalfOf(), ShouldEqual, triggererID)
So(onBehalfs[0].GetNotifyDetails(), ShouldBeNil)
So(asSelf, ShouldHaveLength, 1)
So(asSelf[0].GetNotify(), ShouldEqual, gerritpb.Notify_NOTIFY_NONE)
So(asSelf[0].GetNotifyDetails(), ShouldResembleProto,
&gerritpb.NotifyDetails{
Recipients: []*gerritpb.NotifyDetails_Recipient{
{
RecipientType: gerritpb.NotifyDetails_RECIPIENT_TYPE_TO,
Info: &gerritpb.NotifyDetails_Info{
Accounts: []int64{ownerID, triggererID},
},
},
},
})
So(asSelf[0].GetAddToAttentionSet(), ShouldResembleProto, []*gerritpb.AttentionSetInput{
{User: strconv.FormatInt(reviewerID, 10), Reason: "ps#2: " + usertext.StoppedRun},
})
cl = &changelist.CL{ID: input.CL.ID}
So(datastore.Get(ctx, cl), ShouldBeNil)
So(cl.TriggerNewPatchsetRunAfterPS, ShouldNotEqual, originalValue)
So(cl.TriggerNewPatchsetRunAfterPS, ShouldEqual, input.CL.Snapshot.Patchset)
})
Convey("Remove single vote", func() {
input.Triggers.CqVoteTrigger = cqTrigger
err := Cancel(ctx, input)
So(err, ShouldBeNil)
resultCI := ct.GFake.GetChange(gHost, int(ci.GetNumber()))
So(resultCI.Info.GetMessages(), ShouldHaveLength, 1)
So(resultCI.Info.GetMessages()[0].GetMessage(), ShouldEqual, input.Message)
So(gf.NonZeroVotes(resultCI.Info, trigger.CQLabelName), ShouldBeEmpty)
onBehalfs, asSelf := splitSetReviewRequests()
So(onBehalfs, ShouldHaveLength, 1)
So(onBehalfs[0].GetOnBehalfOf(), ShouldEqual, triggererID)
So(onBehalfs[0].GetNotifyDetails(), ShouldBeNil)
So(asSelf, ShouldHaveLength, 1)
So(asSelf[0].GetNotify(), ShouldEqual, gerritpb.Notify_NOTIFY_NONE)
So(asSelf[0].GetNotifyDetails(), ShouldResembleProto,
&gerritpb.NotifyDetails{
Recipients: []*gerritpb.NotifyDetails_Recipient{
{
RecipientType: gerritpb.NotifyDetails_RECIPIENT_TYPE_TO,
Info: &gerritpb.NotifyDetails_Info{
Accounts: []int64{ownerID, triggererID},
},
},
},
})
So(asSelf[0].GetAddToAttentionSet(), ShouldResembleProto, []*gerritpb.AttentionSetInput{
{User: strconv.FormatInt(reviewerID, 10), Reason: "ps#2: " + usertext.StoppedRun},
})
})
Convey("Remove multiple votes", func() {
input.Triggers.CqVoteTrigger = cqTrigger
ct.GFake.MutateChange(gHost, int(ci.GetNumber()), func(c *gf.Change) {
gf.CQ(1, clock.Now(ctx).Add(-130*time.Second), gf.U("user-1"))(c.Info)
gf.CQ(2, clock.Now(ctx).Add(-110*time.Second), gf.U("user-70"))(c.Info)
gf.CQ(1, clock.Now(ctx).Add(-100*time.Second), gf.U("user-1000"))(c.Info)
})
Convey("Success", func() {
err := Cancel(ctx, input)
So(err, ShouldBeNil)
resultCI := ct.GFake.GetChange(gHost, int(ci.GetNumber()))
So(resultCI.Info.GetMessages(), ShouldHaveLength, 1)
So(resultCI.Info.GetMessages()[0].GetMessage(), ShouldEqual, input.Message)
So(gf.NonZeroVotes(resultCI.Info, trigger.CQLabelName), ShouldBeEmpty)
onBehalfs, asSelf := splitSetReviewRequests()
for _, r := range onBehalfs {
So(r.GetNotify(), ShouldEqual, gerritpb.Notify_NOTIFY_NONE)
So(r.GetNotifyDetails(), ShouldBeNil)
}
// The triggering vote(s) must have been removed last, the order of
// removals for the rest doesn't matter so long as it does the job.
So(onBehalfs[len(onBehalfs)-1].GetOnBehalfOf(), ShouldEqual, 100)
So(asSelf, ShouldHaveLength, 1)
So(asSelf[0].GetNotify(), ShouldEqual, gerritpb.Notify_NOTIFY_NONE)
So(asSelf[0].GetNotifyDetails(), ShouldResembleProto,
&gerritpb.NotifyDetails{
Recipients: []*gerritpb.NotifyDetails_Recipient{
{
RecipientType: gerritpb.NotifyDetails_RECIPIENT_TYPE_TO,
Info: &gerritpb.NotifyDetails_Info{
Accounts: []int64{1, ownerID, 70, triggererID, 1000},
},
},
},
})
So(asSelf[0].GetAddToAttentionSet(), ShouldResembleProto, []*gerritpb.AttentionSetInput{
{User: strconv.FormatInt(reviewerID, 10), Reason: "ps#2: " + usertext.StoppedRun},
})
})
Convey("Removing non-triggering votes fails", func() {
ct.GFake.MutateChange(gHost, int(ci.GetNumber()), func(c *gf.Change) {
c.ACLs = gf.ACLGrant(gf.OpRead, codes.PermissionDenied, lProject).Or(
gf.ACLGrant(gf.OpReview, codes.PermissionDenied, lProject),
) // no permission to vote on behalf of others
})
err := Cancel(ctx, input)
So(err, ShouldBeNil)
onBehalfs, _ := splitSetReviewRequests()
So(onBehalfs, ShouldHaveLength, 3) // all non-triggering votes
for _, r := range onBehalfs {
switch r.GetOnBehalfOf() {
case triggererID:
// CV shouldn't remove triggering votes if removal of non-triggering
// votes fails.
So(r.GetOnBehalfOf(), ShouldNotEqual, triggererID)
case 1, 70, 1000:
default:
panic(fmt.Errorf("unknown on_behalf_of %d", r.GetOnBehalfOf()))
}
}
})
})
Convey("Removing votes from non-CQ labels used in additional modes", func() {
const uLabel = "Ultra-Quick-Label"
const qLabel = "Quick-Label"
cqTrigger.AdditionalLabel = uLabel
input.Triggers.CqVoteTrigger = cqTrigger
input.ConfigGroups = []*prjcfg.ConfigGroup{
{
Content: &cfgpb.ConfigGroup{
AdditionalModes: []*cfgpb.Mode{
{
Name: "ULTRA_QUICK_RUN",
CqLabelValue: 1,
TriggeringLabel: uLabel,
TriggeringValue: 1,
},
{
Name: "QUICK_RUN",
CqLabelValue: 1,
TriggeringLabel: qLabel,
TriggeringValue: 1,
},
},
},
},
}
ultraQuick := func(value int, timeAndUser ...interface{}) gf.CIModifier {
return gf.Vote(uLabel, value, timeAndUser...)
}
quick := func(value int, timeAndUser ...interface{}) gf.CIModifier {
return gf.Vote(qLabel, value, timeAndUser...)
}
// Exact timestamps don't matter in this test, but in practice they affect
// computation of the triggerign vote.
ct.GFake.MutateChange(gHost, int(ci.GetNumber()), func(c *gf.Change) {
// user-99 forgot to vote CQ+1.
quick(1, clock.Now(ctx).Add(-300*time.Second), gf.U("user-99"))(c.Info)
ultraQuick(1, clock.Now(ctx).Add(-200*time.Second), gf.U("user-99"))(c.Info)
// user-100 actually triggered an ULTRA_QUICK_RUN.
gf.CQ(1, clock.Now(ctx).Add(-150*time.Second), gf.U("user-100"))(c.Info)
ultraQuick(1, clock.Now(ctx).Add(-150*time.Second), gf.U("user-100"))(c.Info)
quick(1, clock.Now(ctx).Add(-150*time.Second), gf.U("user-100"))(c.Info)
// user-101 CQ+1 was a noop.
gf.CQ(1, clock.Now(ctx).Add(-120*time.Second), gf.U("user-101"))(c.Info)
// user-102 votes for a QUICK_RUN is a noop, but should be removed as
// as well.
gf.CQ(1, clock.Now(ctx).Add(-110*time.Second), gf.U("user-101"))(c.Info)
quick(1, clock.Now(ctx).Add(-110*time.Second), gf.U("user-102"))(c.Info)
// user-103 votes is a noop, though weird, yet still must be removed.
ultraQuick(3, clock.Now(ctx).Add(-100*time.Second), gf.U("user-104"))(c.Info)
// user-104 votes is 0, and doesn't need a reset.
ultraQuick(0, clock.Now(ctx).Add(-90*time.Second), gf.U("user-104"))(c.Info)
})
err := Cancel(ctx, input)
So(err, ShouldBeNil)
resultCI := ct.GFake.GetChange(gHost, int(ci.GetNumber()))
So(gf.NonZeroVotes(resultCI.Info, trigger.CQLabelName), ShouldBeEmpty)
So(gf.NonZeroVotes(resultCI.Info, qLabel), ShouldBeEmpty)
So(gf.NonZeroVotes(resultCI.Info, uLabel), ShouldBeEmpty)
onBehalfs, _ := splitSetReviewRequests()
// The last request must be for account 100.
So(onBehalfs[len(onBehalfs)-1].GetOnBehalfOf(), ShouldEqual, 100)
So(onBehalfs[len(onBehalfs)-1].GetLabels(), ShouldResemble, map[string]int32{
trigger.CQLabelName: 0,
qLabel: 0,
uLabel: 0,
})
})
Convey("Skips zero votes", func() {
input.Triggers.CqVoteTrigger = cqTrigger
ct.GFake.MutateChange(gHost, int(ci.GetNumber()), func(c *gf.Change) {
gf.CQ(0, clock.Now(ctx).Add(-90*time.Second), gf.U("user-101"))(c.Info)
gf.CQ(0, clock.Now(ctx).Add(-100*time.Second), gf.U("user-102"))(c.Info)
gf.CQ(0, clock.Now(ctx).Add(-110*time.Second), gf.U("user-103"))(c.Info)
})
err := Cancel(ctx, input)
So(err, ShouldBeNil)
resultCI := ct.GFake.GetChange(gHost, int(ci.GetNumber()))
So(resultCI.Info.GetMessages(), ShouldHaveLength, 1)
So(resultCI.Info.GetMessages()[0].GetMessage(), ShouldEqual, input.Message)
So(gf.NonZeroVotes(resultCI.Info, trigger.CQLabelName), ShouldBeEmpty)
onBehalfs, _ := splitSetReviewRequests()
So(onBehalfs, ShouldHaveLength, 1)
So(onBehalfs[0].GetOnBehalfOf(), ShouldEqual, triggererID)
})
Convey("Post Message even if triggering votes has been removed already", func() {
input.Triggers.CqVoteTrigger = cqTrigger
ct.GFake.MutateChange(gHost, int(ci.GetNumber()), func(c *gf.Change) {
gf.CQ(0, clock.Now(ctx), triggerer)(c.Info)
})
err := Cancel(ctx, input)
So(err, ShouldBeNil)
resultCI := ct.GFake.GetChange(gHost, int(ci.GetNumber()))
So(resultCI.Info.GetMessages(), ShouldHaveLength, 1)
So(resultCI.Info.GetMessages()[0].GetMessage(), ShouldEqual, input.Message)
})
Convey("Post Message if CV has no permission to vote", func() {
input.Triggers.CqVoteTrigger = cqTrigger
ct.GFake.MutateChange(gHost, int(ci.GetNumber()), func(c *gf.Change) {
c.ACLs = gf.ACLGrant(gf.OpRead, codes.PermissionDenied, lProject).Or(
// Needed to post comments
gf.ACLGrant(gf.OpReview, codes.PermissionDenied, lProject),
)
})
So(Cancel(ctx, input), ShouldBeNil)
resultCI := ct.GFake.GetChange(gHost, int(ci.GetNumber())).Info
// CQ+2 vote remains.
So(gf.NonZeroVotes(resultCI, trigger.CQLabelName), ShouldResembleProto, []*gerritpb.ApprovalInfo{
{
User: triggerer,
Value: 2,
Date: timestamppb.New(triggerTime),
},
})
// But CL is no longer triggered.
So(findTriggers(resultCI).GetCqVoteTrigger(), ShouldBeNil)
// Still, user should know what happened.
expectedMsg := input.Message + `
CV failed to unset the Commit-Queue label on your behalf. Please unvote and revote on the Commit-Queue label to retry.
Bot data: {"action":"cancel","triggered_at":"2020-02-02T10:28:00Z","revision":"rev-010001-002","cls":["x-review.example.com:10002","x-review.example.com:10003"]}`
So(resultCI.GetMessages()[0].GetMessage(), ShouldEqual, expectedMsg)
})
Convey("Post Message if change is in bad state", func() {
input.Triggers.CqVoteTrigger = cqTrigger
ct.GFake.MutateChange(gHost, int(ci.GetNumber()), func(c *gf.Change) {
gf.Status(gerritpb.ChangeStatus_ABANDONED)(c.Info)
c.ACLs = func(op gf.Operation, _ string) *status.Status {
if op == gf.OpAlterVotesOfOthers {
return status.New(codes.FailedPrecondition, "change abandoned, no vote removals allowed")
}
return status.New(codes.OK, "")
}
})
err := Cancel(ctx, input)
So(err, ShouldBeNil)
resultCI := ct.GFake.GetChange(gHost, int(ci.GetNumber())).Info
// CQ+2 vote remains.
So(gf.NonZeroVotes(resultCI, trigger.CQLabelName), ShouldResembleProto, []*gerritpb.ApprovalInfo{
{
User: triggerer,
Value: 2,
Date: timestamppb.New(triggerTime),
},
})
// But CL is no longer triggered.
So(findTriggers(resultCI).GetCqVoteTrigger(), ShouldBeNil)
// Still, user should know what happened.
So(resultCI.GetMessages(), ShouldHaveLength, 1)
So(resultCI.GetMessages()[0].GetMessage(), ShouldContainSubstring, "CV failed to unset the Commit-Queue label on your behalf")
})
Convey("Post Message also fails", func() {
input.Triggers.CqVoteTrigger = cqTrigger
ct.GFake.MutateChange(gHost, int(ci.GetNumber()), func(c *gf.Change) {
c.ACLs = gf.ACLGrant(gf.OpRead, codes.PermissionDenied, lProject)
})
err := Cancel(ctx, input)
So(err, ShouldErrLike, "no permission to remove vote x-review.example.com/10001")
So(ErrPermanentTag.In(err), ShouldBeTrue)
resultCI := ct.GFake.GetChange(gHost, int(ci.GetNumber())).Info
So(gf.NonZeroVotes(resultCI, trigger.CQLabelName), ShouldResembleProto, []*gerritpb.ApprovalInfo{
{
User: triggerer,
Value: 2,
Date: timestamppb.New(triggerTime),
},
})
So(resultCI.GetMessages(), ShouldBeEmpty)
})
})
}