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 }