go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/prjmanager/triager/runs_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 triager 16 17 import ( 18 "testing" 19 20 "go.chromium.org/luci/auth/identity" 21 gerritpb "go.chromium.org/luci/common/proto/gerrit" 22 23 cfgpb "go.chromium.org/luci/cv/api/config/v2" 24 "go.chromium.org/luci/cv/internal/changelist" 25 "go.chromium.org/luci/cv/internal/cvtesting" 26 gf "go.chromium.org/luci/cv/internal/gerrit/gerritfake" 27 "go.chromium.org/luci/cv/internal/gerrit/trigger" 28 "go.chromium.org/luci/cv/internal/prjmanager/prjpb" 29 "go.chromium.org/luci/cv/internal/run" 30 31 . "github.com/smartystreets/goconvey/convey" 32 . "go.chromium.org/luci/common/testing/assertions" 33 ) 34 35 func TestFindImmediateHardDeps(t *testing.T) { 36 t.Parallel() 37 38 Convey("findImmediateHardDeps", t, func() { 39 ct := cvtesting.Test{} 40 _, cancel := ct.SetUp(t) 41 defer cancel() 42 43 cls := make(map[int64]*clInfo) 44 nextCLID := int64(1) 45 spm := &simplePMState{ 46 pb: &prjpb.PState{}, 47 } 48 newCL := func() *testCLInfo { 49 defer func() { nextCLID++ }() 50 tci := &testCLInfo{ 51 pcl: &prjpb.PCL{ 52 Clid: nextCLID, 53 }, 54 } 55 cls[nextCLID] = (*clInfo)(tci) 56 spm.pb.Pcls = append(spm.pb.Pcls, tci.pcl) 57 return tci 58 } 59 rs := &runStage{ 60 pm: pmState{spm}, 61 cls: cls, 62 } 63 CLIDs := func(pcls ...*prjpb.PCL) []int64 { 64 var ret []int64 65 for _, pcl := range pcls { 66 ret = append(ret, pcl.GetClid()) 67 } 68 return ret 69 } 70 71 Convey("without deps", func() { 72 cl1 := newCL() 73 cl2 := newCL() 74 So(rs.findImmediateHardDeps(cl1.pcl), ShouldEqual, CLIDs()) 75 So(rs.findImmediateHardDeps(cl2.pcl), ShouldEqual, CLIDs()) 76 }) 77 78 Convey("with HARD deps", func() { 79 Convey("one immediate parent", func() { 80 cl1 := newCL() 81 cl2 := newCL().Deps(cl1) 82 cl3 := newCL().Deps(cl1, cl2) 83 84 So(rs.findImmediateHardDeps(cl1.pcl), ShouldEqual, CLIDs()) 85 So(rs.findImmediateHardDeps(cl2.pcl), ShouldEqual, CLIDs(cl1.pcl)) 86 So(rs.findImmediateHardDeps(cl3.pcl), ShouldEqual, CLIDs(cl2.pcl)) 87 }) 88 89 Convey("more than one immediate parents", func() { 90 // stack 1 91 s1cl1 := newCL() 92 s1cl2 := newCL().Deps(s1cl1) 93 So(rs.findImmediateHardDeps(s1cl1.pcl), ShouldEqual, CLIDs()) 94 So(rs.findImmediateHardDeps(s1cl2.pcl), ShouldEqual, CLIDs(s1cl1.pcl)) 95 // stack 2 96 s2cl1 := newCL() 97 s2cl2 := newCL().Deps(s2cl1) 98 So(rs.findImmediateHardDeps(s2cl1.pcl), ShouldEqual, CLIDs()) 99 So(rs.findImmediateHardDeps(s2cl2.pcl), ShouldEqual, CLIDs(s2cl1.pcl)) 100 101 // cl3 belongs to both stack 1 and 2. 102 // both s1cl2 and s2cl2 should be its immediate parents. 103 cl3 := newCL().Deps(s1cl1, s1cl2, s2cl1, s2cl2) 104 So(rs.findImmediateHardDeps(cl3.pcl), ShouldEqual, CLIDs(s1cl2.pcl, s2cl2.pcl)) 105 }) 106 }) 107 108 Convey("with SOFT deps", func() { 109 cl1 := newCL() 110 cl2 := newCL().SoftDeps(cl1) 111 cl3 := newCL().SoftDeps(cl1, cl2) 112 113 So(rs.findImmediateHardDeps(cl1.pcl), ShouldEqual, CLIDs()) 114 So(rs.findImmediateHardDeps(cl2.pcl), ShouldEqual, CLIDs()) 115 So(rs.findImmediateHardDeps(cl3.pcl), ShouldEqual, CLIDs()) 116 }) 117 118 Convey("with a mix of HARD and SOFT deps", func() { 119 cl1 := newCL() 120 clA := newCL() 121 cl2 := newCL().Deps(cl1) 122 cl3 := newCL().Deps(cl1, cl2).SoftDeps(clA) 123 cl4 := newCL().Deps(cl1, cl2, cl3) 124 125 So(rs.findImmediateHardDeps(cl1.pcl), ShouldEqual, CLIDs()) 126 So(rs.findImmediateHardDeps(cl2.pcl), ShouldEqual, CLIDs(cl1.pcl)) 127 So(rs.findImmediateHardDeps(cl3.pcl), ShouldEqual, CLIDs(cl2.pcl)) 128 So(rs.findImmediateHardDeps(cl4.pcl), ShouldEqual, CLIDs(cl3.pcl)) 129 }) 130 }) 131 } 132 133 func TestQuotaPayer(t *testing.T) { 134 t.Parallel() 135 136 Convey("quotaPayer", t, func() { 137 cl := &changelist.CL{ 138 ID: 1234, 139 Snapshot: &changelist.Snapshot{ 140 Kind: &changelist.Snapshot_Gerrit{ 141 Gerrit: &changelist.Gerrit{ 142 Info: &gerritpb.ChangeInfo{ 143 Labels: make(map[string]*gerritpb.LabelInfo), 144 }, 145 }, 146 }, 147 }, 148 } 149 owner := identity.Identity("user:user-1@example.com") 150 trigg := identity.Identity("user:user-2@example.com") 151 tr := &run.Trigger{} 152 153 setAutoSubmit := func(cl *changelist.CL, val bool) { 154 labels := cl.Snapshot.GetGerrit().GetInfo().Labels 155 if !val { 156 delete(labels, trigger.AutoSubmitLabelName) 157 } else { 158 labels[trigger.AutoSubmitLabelName] = &gerritpb.LabelInfo{ 159 All: []*gerritpb.ApprovalInfo{{ 160 User: gf.U(owner.Value()), 161 Value: 1, 162 }}, 163 } 164 } 165 } 166 167 Convey("panics", func() { 168 var expected string 169 Convey("if owner is empty", func() { 170 owner = "" 171 expected = "empty owner" 172 }) 173 Convey("if owner is annoymous", func() { 174 owner = identity.AnonymousIdentity 175 expected = "the CL owner is anonymous" 176 }) 177 So(func() { quotaPayer(cl, owner, trigg, tr) }, ShouldPanicLike, expected) 178 }) 179 180 Convey("with NewPatchsetRun", func() { 181 // must be the owner always. 182 tr.Mode = string(run.NewPatchsetRun) 183 // the owner is the triggerer in NPR. 184 trigg = owner 185 186 setAutoSubmit(cl, true) 187 So(quotaPayer(cl, owner, trigg, tr), ShouldEqual, trigg) 188 setAutoSubmit(cl, false) 189 So(quotaPayer(cl, owner, trigg, tr), ShouldEqual, trigg) 190 }) 191 192 Convey("with Dry-Run", func() { 193 Convey("by the StandardMode with AS", func() { 194 tr.Mode = string(run.DryRun) 195 setAutoSubmit(cl, true) 196 }) 197 Convey("by the StandardMode without AS", func() { 198 tr.Mode = string(run.DryRun) 199 setAutoSubmit(cl, false) 200 }) 201 Convey("by the CustomMode with AS", func() { 202 tr.Mode = "custom-mode" 203 tr.ModeDefinition = &cfgpb.Mode{ 204 Name: "custom-mode", 205 CqLabelValue: trigger.CQVoteByMode(run.DryRun), 206 TriggeringLabel: "custom-label", 207 } 208 setAutoSubmit(cl, true) 209 }) 210 Convey("by the CustomMode without AS", func() { 211 tr.Mode = "custom-mode" 212 tr.ModeDefinition = &cfgpb.Mode{ 213 Name: "custom-mode", 214 CqLabelValue: trigger.CQVoteByMode(run.DryRun), 215 TriggeringLabel: "custom-label", 216 } 217 setAutoSubmit(cl, false) 218 }) 219 // Must be the triggerer always. 220 So(quotaPayer(cl, owner, trigg, tr), ShouldEqual, trigg) 221 }) 222 Convey("with Full-Run", func() { 223 var expected identity.Identity 224 Convey("by the StandardMode with AS", func() { 225 tr.Mode = string(run.FullRun) 226 setAutoSubmit(cl, true) 227 expected = owner 228 }) 229 Convey("by the StandardMode without AS", func() { 230 tr.Mode = string(run.FullRun) 231 setAutoSubmit(cl, false) 232 expected = trigg 233 }) 234 Convey("by the CustomMode with AS", func() { 235 tr.Mode = "custom-mode" 236 tr.ModeDefinition = &cfgpb.Mode{ 237 Name: "custom-mode", 238 CqLabelValue: trigger.CQVoteByMode(run.FullRun), 239 TriggeringLabel: "custom-label", 240 } 241 setAutoSubmit(cl, true) 242 expected = owner 243 }) 244 Convey("by the CustomMode without AS", func() { 245 tr.Mode = "custom-mode" 246 tr.ModeDefinition = &cfgpb.Mode{ 247 Name: "custom-mode", 248 CqLabelValue: trigger.CQVoteByMode(run.FullRun), 249 TriggeringLabel: "custom-label", 250 } 251 setAutoSubmit(cl, false) 252 expected = trigg 253 }) 254 So(quotaPayer(cl, owner, trigg, tr), ShouldEqual, expected) 255 }) 256 }) 257 }