go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/run/impl/longops/postgerritmessage_test.go (about) 1 // Copyright 2023 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 . "github.com/smartystreets/goconvey/convey" 23 "google.golang.org/grpc/codes" 24 "google.golang.org/grpc/status" 25 "google.golang.org/protobuf/types/known/timestamppb" 26 27 "go.chromium.org/luci/common/clock" 28 gerritpb "go.chromium.org/luci/common/proto/gerrit" 29 cfgpb "go.chromium.org/luci/cv/api/config/v2" 30 "go.chromium.org/luci/cv/internal/changelist" 31 "go.chromium.org/luci/cv/internal/common" 32 "go.chromium.org/luci/cv/internal/configs/prjcfg/prjcfgtest" 33 "go.chromium.org/luci/cv/internal/configs/validation" 34 "go.chromium.org/luci/cv/internal/cvtesting" 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/run" 38 "go.chromium.org/luci/cv/internal/run/eventpb" 39 "go.chromium.org/luci/cv/internal/run/impl/util" 40 "go.chromium.org/luci/gae/service/datastore" 41 ) 42 43 func TestPostGerritMessage(t *testing.T) { 44 t.Parallel() 45 46 Convey("PostGerritMessageOp works", t, func() { 47 ct := cvtesting.Test{} 48 ctx, cancel := ct.SetUp(t) 49 defer cancel() 50 51 const ( 52 lProject = "chromeos" 53 runID = lProject + "/777-1-deadbeef" 54 gHost = "g-review.example.com" 55 gChange1 = 111 56 gChange2 = 222 57 ) 58 59 cfg := cfgpb.Config{ 60 CqStatusHost: validation.CQStatusHostPublic, 61 ConfigGroups: []*cfgpb.ConfigGroup{ 62 {Name: "test"}, 63 }, 64 } 65 prjcfgtest.Create(ctx, lProject, &cfg) 66 67 ensureCL := func(ci *gerritpb.ChangeInfo) (*changelist.CL, *run.RunCL) { 68 triggers := trigger.Find(&trigger.FindInput{ChangeInfo: ci, ConfigGroup: cfg.GetConfigGroups()[0]}) 69 So(triggers.GetCqVoteTrigger(), ShouldNotBeNil) 70 71 if ct.GFake.Has(gHost, int(ci.GetNumber())) { 72 ct.GFake.MutateChange(gHost, int(ci.GetNumber()), func(c *gf.Change) { 73 c.Info = ci 74 }) 75 } else { 76 ct.GFake.AddFrom(gf.WithCIs(gHost, gf.ACLRestricted(lProject), ci)) 77 } 78 79 cl := changelist.MustGobID(gHost, ci.GetNumber()).MustCreateIfNotExists(ctx) 80 rcl := &run.RunCL{ 81 ID: cl.ID, 82 ExternalID: cl.ExternalID, 83 IndexedID: cl.ID, 84 Trigger: triggers.GetCqVoteTrigger(), 85 Run: datastore.MakeKey(ctx, common.RunKind, string(runID)), 86 Detail: &changelist.Snapshot{ 87 Kind: &changelist.Snapshot_Gerrit{Gerrit: &changelist.Gerrit{ 88 Host: gHost, 89 Info: ci, 90 }}, 91 ExternalUpdateTime: timestamppb.New(ct.Clock.Now()), 92 }, 93 } 94 cl.Snapshot = rcl.Detail 95 cl.EVersion++ 96 So(datastore.Put(ctx, cl, rcl), ShouldBeNil) 97 return cl, rcl 98 } 99 100 makeRunWithCLs := func(r *run.Run, cis ...*gerritpb.ChangeInfo) *run.Run { 101 if len(cis) == 0 { 102 panic(fmt.Errorf("at least one CL required")) 103 } 104 if r == nil { 105 r = &run.Run{} 106 } 107 r.ID = runID 108 r.Status = run.Status_RUNNING 109 for _, ci := range cis { 110 _, rcl := ensureCL(ci) 111 r.CLs = append(r.CLs, rcl.ID) 112 } 113 if r.Mode == "" { 114 r.Mode = run.FullRun 115 } 116 if r.ConfigGroupID == "" { 117 r.ConfigGroupID = prjcfgtest.MustExist(ctx, lProject).ConfigGroupIDs[0] 118 } 119 So(datastore.Put(ctx, r), ShouldBeNil) 120 return r 121 } 122 123 testMsg := "This is a test message." 124 makeOp := func(r *run.Run, testMsg string) *PostGerritMessageOp { 125 return &PostGerritMessageOp{ 126 Base: &Base{ 127 Op: &run.OngoingLongOps_Op{ 128 Deadline: timestamppb.New(ct.Clock.Now().Add(10000 * time.Hour)), 129 CancelRequested: false, 130 Work: &run.OngoingLongOps_Op_PostGerritMessage_{ 131 PostGerritMessage: &run.OngoingLongOps_Op_PostGerritMessage{ 132 Message: testMsg, 133 }, 134 }, 135 }, 136 IsCancelRequested: func() bool { return false }, 137 Run: r, 138 }, 139 Env: ct.Env, 140 GFactory: ct.GFactory(), 141 } 142 } 143 144 Convey("Happy path", func() { 145 op := makeOp(makeRunWithCLs(nil, gf.CI(gChange1, gf.CQ(+2))), testMsg) 146 res, err := op.Do(ctx) 147 148 So(err, ShouldBeNil) 149 So(res.GetStatus(), ShouldEqual, eventpb.LongOpCompleted_SUCCEEDED) 150 So(ct.GFake.GetChange(gHost, gChange1).Info, gf.ShouldLastMessageContain, "This is a test message.") 151 }) 152 153 Convey("Happy path with multiple CLs", func() { 154 op := makeOp(makeRunWithCLs( 155 &run.Run{Mode: run.DryRun}, 156 gf.CI(gChange1, gf.CQ(+1)), 157 gf.CI(gChange2, gf.CQ(+1)), 158 ), testMsg) 159 res, err := op.Do(ctx) 160 So(err, ShouldBeNil) 161 So(res.GetStatus(), ShouldEqual, eventpb.LongOpCompleted_SUCCEEDED) 162 163 for _, gChange := range []int{gChange1, gChange2} { 164 ci := ct.GFake.GetChange(gHost, gChange).Info 165 So(ci, gf.ShouldLastMessageContain, "This is a test message.") 166 // Should post exactly one message. 167 So(ci.GetMessages(), ShouldHaveLength, 1) 168 169 // Recorded timestamp must be approximately correct since both CLs are 170 // posted at around the same time. 171 So(res.GetPostGerritMessage().GetTime().AsTime(), ShouldHappenWithin, time.Second, ci.GetMessages()[0].GetDate().AsTime()) 172 } 173 }) 174 175 Convey("Best effort avoidance of duplicated messages", func() { 176 // Make two same PostGerritMessageOp objects, since they are single-use 177 // only. 178 opFirst := makeOp(makeRunWithCLs(nil, gf.CI(gChange1, gf.CQ(+2))), testMsg) 179 opRetry := makeOp(makeRunWithCLs(nil, gf.CI(gChange1, gf.CQ(+2))), testMsg) 180 181 // For test simplicity, this retry would have a substring of the 182 // originally posted testMsg. This simulates gerrit appending 183 // metadata such as the patchset name to the message. 184 opRetrySubstring := makeOp(makeRunWithCLs(nil, gf.CI(gChange1, gf.CQ(+2))), "test message") 185 186 // Simulate first try updating Gerrit, but somehow crashing before getting 187 // response from Gerrit. 188 _, err := opFirst.Do(ctx) 189 So(err, ShouldBeNil) 190 ci := ct.GFake.GetChange(gHost, gChange1).Info 191 So(ci, gf.ShouldLastMessageContain, "This is a test message.") 192 So(ci.GetMessages(), ShouldHaveLength, 1) 193 194 Convey("very quick retry leads to dups", func() { 195 ct.Clock.Add(time.Second) 196 res, err := opRetry.Do(ctx) 197 So(err, ShouldBeNil) 198 So(res.GetStatus(), ShouldEqual, eventpb.LongOpCompleted_SUCCEEDED) 199 So(ct.GFake.GetChange(gHost, gChange1).Info.GetMessages(), ShouldHaveLength, 2) 200 // And the timestamp isn't entirely right, but that's fine. 201 So(res.GetPostGerritMessage().GetTime().AsTime(), ShouldResemble, ct.Clock.Now().UTC().Truncate(time.Second)) 202 }) 203 204 Convey("later retry", func() { 205 ct.Clock.Add(util.StaleCLAgeThreshold) 206 res, err := opRetry.Do(ctx) 207 So(err, ShouldBeNil) 208 So(res.GetStatus(), ShouldEqual, eventpb.LongOpCompleted_SUCCEEDED) 209 // There should still be exactly 1 message. 210 ci := ct.GFake.GetChange(gHost, gChange1).Info 211 So(ci.GetMessages(), ShouldHaveLength, 1) 212 // and the timestamp must be exactly correct. 213 So(res.GetPostGerritMessage().GetTime().AsTime(), ShouldResemble, ci.GetMessages()[0].GetDate().AsTime()) 214 }) 215 216 Convey("later retry avoids reposting msg even when gerrit appends metadata", func() { 217 ct.Clock.Add(util.StaleCLAgeThreshold) 218 res, err := opRetrySubstring.Do(ctx) 219 So(err, ShouldBeNil) 220 So(res.GetStatus(), ShouldEqual, eventpb.LongOpCompleted_SUCCEEDED) 221 // There should still be exactly 1 message. 222 ci := ct.GFake.GetChange(gHost, gChange1).Info 223 So(ci.GetMessages(), ShouldHaveLength, 1) 224 // and the timestamp must be exactly correct. 225 So(res.GetPostGerritMessage().GetTime().AsTime(), ShouldResemble, ci.GetMessages()[0].GetDate().AsTime()) 226 }) 227 }) 228 229 Convey("Failures", func() { 230 op := makeOp(makeRunWithCLs( 231 &run.Run{Mode: run.DryRun}, 232 gf.CI(gChange1, gf.CQ(+1)), 233 ), testMsg) 234 ctx, cancel := clock.WithDeadline(ctx, op.Op.Deadline.AsTime()) 235 defer cancel() 236 ct.Clock.Set(op.Op.Deadline.AsTime().Add(-8 * time.Minute)) 237 238 Convey("With a non transient failure", func() { 239 ct.GFake.MutateChange(gHost, gChange1, func(c *gf.Change) { 240 c.ACLs = func(_ gf.Operation, _ string) *status.Status { 241 return status.New(codes.PermissionDenied, "admin-is-angry-today") 242 } 243 }) 244 }) 245 Convey("With a transient failure", func() { 246 ct.GFake.MutateChange(gHost, gChange1, func(c *gf.Change) { 247 c.ACLs = func(_ gf.Operation, _ string) *status.Status { 248 return status.New(codes.Internal, "oops, temp error") 249 } 250 }) 251 }) 252 res, err := op.Do(ctx) 253 // Given any failure, the status should be set to FAILED, 254 // but the returned error is nil to prevent the TQ retry. 255 So(err, ShouldBeNil) 256 So(res.GetStatus(), ShouldEqual, eventpb.LongOpCompleted_FAILED) 257 So(res.GetPostGerritMessage().GetTime(), ShouldBeNil) 258 So(ct.GFake.GetChange(gHost, gChange1).Info.GetMessages(), ShouldHaveLength, 0) 259 }) 260 }) 261 }