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  }