go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/run/impl/longops/reset_triggers_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 longops 16 17 import ( 18 "fmt" 19 "testing" 20 "time" 21 22 "google.golang.org/protobuf/types/known/timestamppb" 23 24 "go.chromium.org/luci/common/clock" 25 gerritpb "go.chromium.org/luci/common/proto/gerrit" 26 "go.chromium.org/luci/gae/service/datastore" 27 28 cfgpb "go.chromium.org/luci/cv/api/config/v2" 29 "go.chromium.org/luci/cv/internal/changelist" 30 "go.chromium.org/luci/cv/internal/common" 31 "go.chromium.org/luci/cv/internal/common/lease" 32 "go.chromium.org/luci/cv/internal/configs/prjcfg/prjcfgtest" 33 "go.chromium.org/luci/cv/internal/cvtesting" 34 "go.chromium.org/luci/cv/internal/gerrit" 35 gf "go.chromium.org/luci/cv/internal/gerrit/gerritfake" 36 "go.chromium.org/luci/cv/internal/gerrit/trigger" 37 "go.chromium.org/luci/cv/internal/metrics" 38 "go.chromium.org/luci/cv/internal/run" 39 "go.chromium.org/luci/cv/internal/run/eventpb" 40 41 . "github.com/smartystreets/goconvey/convey" 42 ) 43 44 func TestResetTriggers(t *testing.T) { 45 t.Parallel() 46 47 Convey("ResetTriggers works", t, func() { 48 ct := cvtesting.Test{} 49 ctx, cancel := ct.SetUp(t) 50 defer cancel() 51 mutator := changelist.NewMutator(ct.TQDispatcher, nil, nil, nil) 52 53 const ( 54 lProject = "infra" 55 gHost = "g-review.example.com" 56 ) 57 runCreateTime := clock.Now(ctx) 58 runID := common.MakeRunID(lProject, runCreateTime, 1, []byte("deadbeef")) 59 60 cfg := cfgpb.Config{ 61 ConfigGroups: []*cfgpb.ConfigGroup{ 62 {Name: "test"}, 63 }, 64 } 65 prjcfgtest.Create(ctx, lProject, &cfg) 66 67 initRunAndCLs := func(cis []*gerritpb.ChangeInfo) (*run.Run, common.CLIDs) { 68 clids := make(common.CLIDs, len(cis)) 69 cls := make([]*changelist.CL, len(cis)) 70 runCLs := make([]*run.RunCL, len(cis)) 71 for i, ci := range cis { 72 So(ci.GetNumber(), ShouldBeGreaterThan, 0) 73 So(ci.GetNumber(), ShouldBeLessThan, 1000) 74 triggers := trigger.Find(&trigger.FindInput{ChangeInfo: ci, ConfigGroup: cfg.GetConfigGroups()[0]}) 75 So(triggers.GetCqVoteTrigger(), ShouldNotBeNil) 76 So(ct.GFake.Has(gHost, int(ci.GetNumber())), ShouldBeFalse) 77 ct.GFake.AddFrom(gf.WithCIs(gHost, gf.ACLRestricted(lProject), ci)) 78 cl := changelist.MustGobID(gHost, ci.GetNumber()).MustCreateIfNotExists(ctx) 79 cl.Snapshot = &changelist.Snapshot{ 80 Kind: &changelist.Snapshot_Gerrit{Gerrit: &changelist.Gerrit{ 81 Host: gHost, 82 Info: ci, 83 }}, 84 LuciProject: lProject, 85 ExternalUpdateTime: timestamppb.New(runCreateTime), 86 } 87 cl.EVersion++ 88 clids[i] = cl.ID 89 runCLs[i] = &run.RunCL{ 90 ID: cl.ID, 91 ExternalID: cl.ExternalID, 92 IndexedID: cl.ID, 93 Trigger: triggers.GetCqVoteTrigger(), 94 Run: datastore.MakeKey(ctx, common.RunKind, string(runID)), 95 Detail: cl.Snapshot, 96 } 97 cls[i] = cl 98 } 99 r := &run.Run{ 100 ID: runID, 101 Status: run.Status_RUNNING, 102 CLs: clids, 103 Mode: run.DryRun, 104 ConfigGroupID: prjcfgtest.MustExist(ctx, lProject).ConfigGroupIDs[0], 105 } 106 So(datastore.Put(ctx, r, cls, runCLs), ShouldBeNil) 107 return r, clids 108 } 109 110 makeOp := func(r *run.Run) *ResetTriggersOp { 111 reqs := make([]*run.OngoingLongOps_Op_ResetTriggers_Request, len(r.CLs)) 112 for i, clid := range r.CLs { 113 reqs[i] = &run.OngoingLongOps_Op_ResetTriggers_Request{ 114 Clid: int64(clid), 115 Message: fmt.Sprintf("reset message for CL %d", clid), 116 Notify: gerrit.Whoms{ 117 gerrit.Whom_OWNER, 118 gerrit.Whom_REVIEWERS, 119 }, 120 AddToAttention: gerrit.Whoms{ 121 gerrit.Whom_OWNER, 122 gerrit.Whom_CQ_VOTERS, 123 }, 124 AddToAttentionReason: fmt.Sprintf("attention reason for CL %d", clid), 125 } 126 } 127 128 return &ResetTriggersOp{ 129 Base: &Base{ 130 Op: &run.OngoingLongOps_Op{ 131 Deadline: timestamppb.New(clock.Now(ctx).Add(10000 * time.Hour)), // infinite 132 CancelRequested: false, 133 Work: &run.OngoingLongOps_Op_ResetTriggers_{ 134 ResetTriggers: &run.OngoingLongOps_Op_ResetTriggers{ 135 Requests: reqs, 136 }, 137 }, 138 }, 139 IsCancelRequested: func() bool { return false }, 140 Run: r, 141 }, 142 GFactory: ct.GFactory(), 143 CLMutator: mutator, 144 } 145 } 146 147 assertTriggerRemoved := func(eid changelist.ExternalID) { 148 host, changeID, err := changelist.ExternalID(eid).ParseGobID() 149 So(err, ShouldBeNil) 150 So(host, ShouldEqual, gHost) 151 changeInfo := ct.GFake.GetChange(gHost, int(changeID)).Info 152 So(trigger.Find(&trigger.FindInput{ChangeInfo: changeInfo, ConfigGroup: cfg.GetConfigGroups()[0]}), ShouldBeNil) 153 } 154 155 testHappyPath := func(prefix string, clCount, concurrency int) { 156 Convey(fmt.Sprintf("%s [%d CLs with concurrency %d]", prefix, clCount, concurrency), func() { 157 cis := make([]*gerritpb.ChangeInfo, clCount) 158 for i := range cis { 159 cis[i] = gf.CI(i+1, gf.CQ(+1), gf.Updated(runCreateTime.Add(-1*time.Minute))) 160 } 161 r, _ := initRunAndCLs(cis) 162 startTime := clock.Now(ctx) 163 op := makeOp(r) 164 op.Concurrency = concurrency 165 res, err := op.Do(ctx) 166 So(err, ShouldBeNil) 167 So(res.GetStatus(), ShouldEqual, eventpb.LongOpCompleted_SUCCEEDED) 168 results := res.GetResetTriggers().GetResults() 169 So(results, ShouldHaveLength, clCount) 170 processedCLIDs := make(common.CLIDsSet, clCount) 171 for _, result := range results { 172 So(processedCLIDs.HasI64(result.Id), ShouldBeFalse) // duplicate processing 173 processedCLIDs.AddI64(result.Id) 174 assertTriggerRemoved(changelist.ExternalID(result.ExternalId)) 175 So(result.GetSuccessInfo().GetResetAt().AsTime(), ShouldHappenOnOrAfter, startTime) 176 } 177 So(ct.TSMonSentValue(ctx, metrics.Internal.RunResetTriggerAttempted, lProject, "test", string(run.DryRun), true, "GERRIT_ERROR_NONE"), ShouldEqual, clCount) 178 }) 179 } 180 181 testHappyPath("single", 1, 1) 182 testHappyPath("serial", 4, 1) 183 testHappyPath("concurrent", 80, 8) 184 185 // TODO(crbug/1297723): re-enable this test after fixing the flake. 186 SkipConvey("Retry on alreadyInLease failure", func() { 187 // Creating changes from 1 to `clCount`, lease the CL with duration == 188 // change number * time.Minute. 189 clCount := 6 190 cis := make([]*gerritpb.ChangeInfo, clCount) 191 for i := 1; i <= clCount; i++ { 192 cis[i-1] = gf.CI(i, gf.CQ(+1), gf.Updated(runCreateTime.Add(-1*time.Minute))) 193 } 194 r, clids := initRunAndCLs(cis) 195 for i, clid := range clids { 196 _, _, err := lease.ApplyOnCL(ctx, clid, time.Duration(cis[i].GetNumber())*time.Minute, "FooBar") 197 So(err, ShouldBeNil) 198 } 199 startTime := clock.Now(ctx) 200 op := makeOp(r) 201 op.Concurrency = clCount 202 op.testAfterTryResetFn = func() { 203 // Advance the clock by 1 minute + 1 second so that the lease will 204 // be guaranteed to expire in the next attempt. 205 ct.Clock.Add(1*time.Minute + 1*time.Second) 206 } 207 res, err := op.Do(ctx) 208 So(err, ShouldBeNil) 209 So(res.GetStatus(), ShouldEqual, eventpb.LongOpCompleted_SUCCEEDED) 210 results := res.GetResetTriggers().GetResults() 211 So(results, ShouldHaveLength, len(cis)) 212 for i, result := range results { 213 So(result.Id, ShouldEqual, clids[i]) 214 So(result.GetSuccessInfo().GetResetAt().AsTime(), ShouldHappenAfter, startTime.Add(time.Duration(cis[i].GetNumber())*time.Minute)) 215 assertTriggerRemoved(changelist.ExternalID(result.ExternalId)) 216 } 217 }) 218 219 // TODO(crbug/1199880): test can retry transient failure once Gerrit fake 220 // gain the flakiness mode. 221 222 Convey("Failed permanently for non-transient error", func() { 223 cis := []*gerritpb.ChangeInfo{ 224 gf.CI(1, gf.CQ(+1), gf.Updated(runCreateTime.Add(-1*time.Minute))), 225 gf.CI(2, gf.CQ(+1), gf.Updated(runCreateTime.Add(-1*time.Minute))), 226 } 227 r, clids := initRunAndCLs(cis) 228 ct.GFake.MutateChange(gHost, 2, func(c *gf.Change) { 229 c.ACLs = gf.ACLReadOnly(lProject) // can't mutate 230 }) 231 op := makeOp(r) 232 startTime := clock.Now(ctx) 233 res, err := op.Do(ctx) 234 So(err, ShouldNotBeNil) 235 So(res.GetStatus(), ShouldEqual, eventpb.LongOpCompleted_FAILED) 236 results := res.GetResetTriggers().GetResults() 237 So(results, ShouldHaveLength, len(cis)) 238 for _, result := range results { 239 switch common.CLID(result.Id) { 240 case clids[0]: // Change 1 241 So(result.GetSuccessInfo().GetResetAt().AsTime(), ShouldHappenAfter, startTime) 242 case clids[1]: // Change 2 243 So(result.GetFailureInfo().GetFailureMessage(), ShouldNotBeEmpty) 244 } 245 So(result.ExternalId, ShouldNotBeEmpty) 246 } 247 So(ct.TSMonSentValue(ctx, metrics.Internal.RunResetTriggerAttempted, lProject, "test", string(run.DryRun), true, "GERRIT_ERROR_NONE"), ShouldEqual, 1) 248 So(ct.TSMonSentValue(ctx, metrics.Internal.RunResetTriggerAttempted, lProject, "test", string(run.DryRun), false, "PERMISSION_DENIED"), ShouldEqual, 1) 249 }) 250 251 Convey("Doesn't obey long op cancellation", func() { 252 ci := gf.CI(1, gf.CQ(+1), gf.Updated(runCreateTime.Add(-1*time.Minute))) 253 cis := []*gerritpb.ChangeInfo{ci} 254 r, clids := initRunAndCLs(cis) 255 op := makeOp(r) 256 op.IsCancelRequested = func() bool { return true } 257 res, err := op.Do(ctx) 258 So(err, ShouldBeNil) 259 So(res.GetStatus(), ShouldEqual, eventpb.LongOpCompleted_SUCCEEDED) 260 results := res.GetResetTriggers().GetResults() 261 So(results, ShouldHaveLength, len(cis)) 262 for i, result := range results { 263 So(result.Id, ShouldEqual, clids[i]) 264 assertTriggerRemoved(changelist.ExternalID(result.ExternalId)) 265 So(result.GetSuccessInfo().GetResetAt(), ShouldNotBeNil) 266 } 267 }) 268 }) 269 }