go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/run/impl/handler/update_config_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 handler
    16  
    17  import (
    18  	"fmt"
    19  	"testing"
    20  	"time"
    21  
    22  	"google.golang.org/protobuf/proto"
    23  	"google.golang.org/protobuf/types/known/timestamppb"
    24  
    25  	bbpb "go.chromium.org/luci/buildbucket/proto"
    26  	bbutil "go.chromium.org/luci/buildbucket/protoutil"
    27  	"go.chromium.org/luci/common/clock"
    28  	gerritpb "go.chromium.org/luci/common/proto/gerrit"
    29  	"go.chromium.org/luci/gae/service/datastore"
    30  
    31  	cfgpb "go.chromium.org/luci/cv/api/config/v2"
    32  	"go.chromium.org/luci/cv/internal/changelist"
    33  	"go.chromium.org/luci/cv/internal/common"
    34  	"go.chromium.org/luci/cv/internal/configs/prjcfg/prjcfgtest"
    35  	"go.chromium.org/luci/cv/internal/cvtesting"
    36  	gf "go.chromium.org/luci/cv/internal/gerrit/gerritfake"
    37  	"go.chromium.org/luci/cv/internal/gerrit/trigger"
    38  	"go.chromium.org/luci/cv/internal/run"
    39  	"go.chromium.org/luci/cv/internal/run/impl/state"
    40  	"go.chromium.org/luci/cv/internal/tryjob/requirement"
    41  
    42  	. "github.com/smartystreets/goconvey/convey"
    43  )
    44  
    45  func TestUpdateConfig(t *testing.T) {
    46  	Convey("OnCLUpdated", t, func() {
    47  		ct := cvtesting.Test{}
    48  		ctx, cancel := ct.SetUp(t)
    49  		defer cancel()
    50  
    51  		const (
    52  			lProject    = "chromium"
    53  			gHost       = "x-review.example.com"
    54  			gRepoFirst  = "repo/first"
    55  			gRepoSecond = "repo/second"
    56  			gRef        = "refs/heads/main"
    57  		)
    58  		runID := common.MakeRunID(lProject, ct.Clock.Now(), 1, []byte("deadbeef"))
    59  		builder := &bbpb.BuilderID{
    60  			Project: lProject,
    61  			Bucket:  "bucket",
    62  			Builder: "some-builder",
    63  		}
    64  
    65  		putRunCL := func(ci *gerritpb.ChangeInfo, cg *cfgpb.ConfigGroup) {
    66  			triggers := trigger.Find(&trigger.FindInput{ChangeInfo: ci, ConfigGroup: cg})
    67  			So(triggers.GetCqVoteTrigger(), ShouldNotBeNil)
    68  			rcl := run.RunCL{
    69  				Run:        datastore.MakeKey(ctx, common.RunKind, string(runID)),
    70  				ID:         common.CLID(ci.GetNumber()),
    71  				ExternalID: changelist.MustGobID(gHost, ci.GetNumber()),
    72  				Trigger:    triggers.GetCqVoteTrigger(),
    73  				Detail: &changelist.Snapshot{
    74  					Patchset: ci.GetRevisions()[ci.GetCurrentRevision()].GetNumber(),
    75  					Kind: &changelist.Snapshot_Gerrit{
    76  						Gerrit: &changelist.Gerrit{
    77  							Info: ci,
    78  							Host: gHost,
    79  						},
    80  					},
    81  				},
    82  			}
    83  			So(datastore.Put(ctx, &rcl), ShouldBeNil)
    84  		}
    85  
    86  		// Seed project with one version of prior config.
    87  		prjcfgtest.Create(ctx, lProject, &cfgpb.Config{ConfigGroups: []*cfgpb.ConfigGroup{{Name: "ev1"}}})
    88  		metaBefore := prjcfgtest.MustExist(ctx, lProject)
    89  		So(metaBefore.EVersion, ShouldEqual, 1)
    90  		// Set up initial Run state.
    91  		cfgCurrent := &cfgpb.Config{
    92  			ConfigGroups: []*cfgpb.ConfigGroup{
    93  				{
    94  					Name: "main",
    95  					Gerrit: []*cfgpb.ConfigGroup_Gerrit{{
    96  						Url: "https://" + gHost,
    97  						Projects: []*cfgpb.ConfigGroup_Gerrit_Project{
    98  							{Name: gRepoFirst, RefRegexp: []string{"refs/heads/.+"}},
    99  							{Name: gRepoSecond, RefRegexp: []string{"refs/heads/.+"}},
   100  						},
   101  					}},
   102  					Verifiers: &cfgpb.Verifiers{
   103  						Tryjob: &cfgpb.Verifiers_Tryjob{
   104  							Builders: []*cfgpb.Verifiers_Tryjob_Builder{
   105  								{Name: bbutil.FormatBuilderID(builder)},
   106  							},
   107  						},
   108  					},
   109  					AdditionalModes: []*cfgpb.Mode{{
   110  						Name:            "CUSTOM_RUN",
   111  						CqLabelValue:    1,
   112  						TriggeringValue: 1,
   113  						TriggeringLabel: "Will-Be-Changed-In-Tests-Below",
   114  					}},
   115  				},
   116  				{
   117  					Name: "special",
   118  					Gerrit: []*cfgpb.ConfigGroup_Gerrit{{
   119  						Url: "https://" + gHost,
   120  						Projects: []*cfgpb.ConfigGroup_Gerrit_Project{{
   121  							Name:      "repo/will-be-replaced-in-tests-below",
   122  							RefRegexp: []string{"refs/heads/.+"},
   123  						}},
   124  					}},
   125  				},
   126  			},
   127  		}
   128  		prjcfgtest.Update(ctx, lProject, cfgCurrent)
   129  		metaCurrent := prjcfgtest.MustExist(ctx, lProject)
   130  		triggerTime := clock.Now(ctx).UTC()
   131  		cgMain := cfgCurrent.GetConfigGroups()[0]
   132  		putRunCL(gf.CI(1, gf.Project(gRepoFirst), gf.CQ(+1, triggerTime, gf.U("user-1"))), cgMain)
   133  		putRunCL(gf.CI(
   134  			2, gf.Project(gRepoSecond),
   135  			gf.CQ(+1, triggerTime, gf.U("user-1")),
   136  			// Custom+1 has no effect as AdditionalModes above is misconfigured.
   137  			gf.Vote("Custom", +1, triggerTime, gf.U("user-1")),
   138  		), cgMain)
   139  		rs := &state.RunState{
   140  			Run: run.Run{
   141  				ID:            runID,
   142  				CLs:           common.MakeCLIDs(1, 2),
   143  				CreateTime:    triggerTime,
   144  				StartTime:     triggerTime.Add(1 * time.Minute),
   145  				Status:        run.Status_RUNNING,
   146  				ConfigGroupID: prjcfgtest.MustExist(ctx, lProject).ConfigGroupIDs[0], // main
   147  				Tryjobs: &run.Tryjobs{
   148  					RequirementVersion:    1,
   149  					RequirementComputedAt: timestamppb.New(triggerTime.Add(1 * time.Minute)),
   150  				},
   151  				Mode: run.DryRun,
   152  			},
   153  		}
   154  		runCLs, err := run.LoadRunCLs(ctx, rs.ID, rs.CLs)
   155  		So(err, ShouldBeNil)
   156  		initialReqmt, err := requirement.Compute(ctx, requirement.Input{
   157  			ConfigGroup: cgMain,
   158  			RunOwner:    rs.Owner,
   159  			CLs:         runCLs,
   160  			RunOptions:  rs.Options,
   161  			RunMode:     rs.Mode,
   162  		})
   163  		So(err, ShouldBeNil)
   164  		So(initialReqmt.OK(), ShouldBeTrue)
   165  		rs.Tryjobs.Requirement = initialReqmt.Requirement
   166  		// Prepare new config as a copy of existing one. Add extra ConfigGroup to it
   167  		// to ensure its hash will always differ.
   168  		cfgNew := proto.Clone(cfgCurrent).(*cfgpb.Config)
   169  		cfgNew.ConfigGroups = append(cfgNew.ConfigGroups, &cfgpb.ConfigGroup{Name: "foo"})
   170  
   171  		h, _ := makeTestHandler(&ct)
   172  
   173  		updateConfig := func() *Result {
   174  			prjcfgtest.Update(ctx, lProject, cfgNew)
   175  			metaNew := prjcfgtest.MustExist(ctx, lProject)
   176  			res, err := h.UpdateConfig(ctx, rs, metaNew.Hash())
   177  			So(err, ShouldBeNil)
   178  			return res
   179  		}
   180  
   181  		Convey("Noop", func() {
   182  			ensureNoop := func(res *Result) {
   183  				So(res.State, ShouldEqual, rs)
   184  				So(res.SideEffectFn, ShouldBeNil)
   185  				So(res.PreserveEvents, ShouldBeFalse)
   186  			}
   187  
   188  			for _, status := range []run.Status{
   189  				run.Status_SUCCEEDED,
   190  				run.Status_FAILED,
   191  				run.Status_CANCELLED,
   192  			} {
   193  				Convey(fmt.Sprintf("When Run is %s", status), func() {
   194  					rs.Status = status
   195  					ensureNoop(updateConfig())
   196  				})
   197  			}
   198  			Convey("When given config hash isn't new", func() {
   199  				Convey("but is the same as current", func() {
   200  					res, err := h.UpdateConfig(ctx, rs, metaCurrent.Hash())
   201  					So(err, ShouldBeNil)
   202  					ensureNoop(res)
   203  				})
   204  				Convey("but is older than current", func() {
   205  					res, err := h.UpdateConfig(ctx, rs, metaBefore.Hash())
   206  					So(err, ShouldBeNil)
   207  					ensureNoop(res)
   208  				})
   209  			})
   210  		})
   211  
   212  		Convey("Preserve events for SUBMITTING Run", func() {
   213  			rs.Status = run.Status_SUBMITTING
   214  			res := updateConfig()
   215  			So(res.State, ShouldEqual, rs)
   216  			So(res.SideEffectFn, ShouldBeNil)
   217  			So(res.PreserveEvents, ShouldBeTrue)
   218  		})
   219  
   220  		Convey("Upgrades to newer config version when", func() {
   221  			ensureUpdated := func(expectedGroupName string) *Result {
   222  				res := updateConfig()
   223  				So(res.State.ConfigGroupID.Hash(), ShouldNotEqual, metaCurrent.Hash())
   224  				So(res.State.ConfigGroupID.Name(), ShouldEqual, expectedGroupName)
   225  				So(res.State.Status, ShouldEqual, run.Status_RUNNING)
   226  				So(res.State.LogEntries, ShouldHaveLength, 1)
   227  				So(res.State.LogEntries[0].GetConfigChanged(), ShouldNotBeNil)
   228  				So(res.SideEffectFn, ShouldBeNil)
   229  				So(res.PreserveEvents, ShouldBeFalse)
   230  				return res
   231  			}
   232  			Convey("ConfigGroup is same", func() {
   233  				ensureUpdated("main")
   234  			})
   235  			Convey("ConfigGroup renamed", func() {
   236  				cfgNew.ConfigGroups[0].Name = "blah"
   237  				ensureUpdated("blah")
   238  			})
   239  			Convey("ConfigGroup re-ordered and renamed", func() {
   240  				cfgNew.ConfigGroups[0].Name = "blah"
   241  				cfgNew.ConfigGroups[0], cfgNew.ConfigGroups[1] = cfgNew.ConfigGroups[1], cfgNew.ConfigGroups[0]
   242  				ensureUpdated("blah")
   243  			})
   244  			Convey("Verifier config changed", func() {
   245  				cfgNew.ConfigGroups[0].Verifiers.TreeStatus = &cfgpb.Verifiers_TreeStatus{Url: "https://whatever.example.com"}
   246  				res := ensureUpdated("main")
   247  				So(res.State.Tryjobs.GetRequirementVersion(), ShouldEqual, rs.Tryjobs.GetRequirementVersion())
   248  			})
   249  			Convey("Watched refs changed", func() {
   250  				cfgNew.ConfigGroups[0].Gerrit[0].Projects[0].RefRegexpExclude = []string{"refs/heads/exclude"}
   251  				ensureUpdated("main")
   252  			})
   253  			Convey("Tryjob requirement changed", func() {
   254  				tryjobVerifier := cfgNew.ConfigGroups[0].Verifiers.Tryjob
   255  				tryjobVerifier.Builders = append(tryjobVerifier.Builders,
   256  					&cfgpb.Verifiers_Tryjob_Builder{
   257  						Name: fmt.Sprintf("%s/another-bucket/another-builder", lProject),
   258  					})
   259  				res := ensureUpdated("main")
   260  				So(proto.Equal(res.State.Tryjobs.GetRequirement(), rs.Tryjobs.GetRequirement()), ShouldBeFalse)
   261  				So(res.State.Tryjobs.GetRequirementVersion(), ShouldEqual, rs.Tryjobs.GetRequirementVersion()+1)
   262  				So(res.State.Tryjobs.GetRequirementComputedAt().AsTime(), ShouldEqual, ct.Clock.Now().UTC())
   263  			})
   264  		})
   265  
   266  		Convey("Cancel Run when", func() {
   267  			ensureCancelled := func() {
   268  				res := updateConfig()
   269  				// Applicable ConfigGroupID should remain the same.
   270  				So(res.State.ConfigGroupID, ShouldEqual, rs.ConfigGroupID)
   271  				So(res.State.Status, ShouldEqual, run.Status_CANCELLED)
   272  				So(res.SideEffectFn, ShouldNotBeNil)
   273  				So(res.PreserveEvents, ShouldBeFalse)
   274  			}
   275  			Convey("a CL is no longer watched", func() {
   276  				cfgNew.ConfigGroups[0].Gerrit[0].Projects[1].Name = "repo/different"
   277  				ensureCancelled()
   278  			})
   279  			Convey("a CL is watched by >1 ConfigGroup", func() {
   280  				cfgNew.ConfigGroups[1].Gerrit[0].Projects[0].Name = gRepoFirst
   281  				ensureCancelled()
   282  			})
   283  			Convey("CLs are watched by different ConfigGroups", func() {
   284  				cfgNew.ConfigGroups[0].Gerrit[0].Projects[0].Name = "repo/different"
   285  				cfgNew.ConfigGroups[1].Gerrit[0].Projects[0].Name = gRepoFirst
   286  				ensureCancelled()
   287  			})
   288  			Convey("CLs trigger has changed", func() {
   289  				cfgNew.ConfigGroups[0].AdditionalModes[0].TriggeringLabel = "Custom"
   290  				ensureCancelled()
   291  			})
   292  		})
   293  	})
   294  }