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  }