go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/gerrit/trigger/trigger_test.go (about) 1 // Copyright 2020 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 trigger 16 17 import ( 18 "testing" 19 "time" 20 21 "google.golang.org/protobuf/types/known/timestamppb" 22 23 "go.chromium.org/luci/common/clock/testclock" 24 gerritpb "go.chromium.org/luci/common/proto/gerrit" 25 26 cfgpb "go.chromium.org/luci/cv/api/config/v2" 27 "go.chromium.org/luci/cv/internal/changelist" 28 "go.chromium.org/luci/cv/internal/gerrit/botdata" 29 gf "go.chromium.org/luci/cv/internal/gerrit/gerritfake" 30 "go.chromium.org/luci/cv/internal/run" 31 32 c "github.com/smartystreets/goconvey/convey" 33 la "go.chromium.org/luci/common/testing/assertions" 34 ) 35 36 func TestHasAutoSubmit(t *testing.T) { 37 t.Parallel() 38 39 c.Convey("HasAutoSubmit", t, func() { 40 now := testclock.TestRecentTimeUTC 41 ci := &gerritpb.ChangeInfo{ 42 Status: gerritpb.ChangeStatus_NEW, 43 CurrentRevision: "deadbeef~1", 44 Revisions: map[string]*gerritpb.RevisionInfo{ 45 "deadbeef~1": { 46 Number: 2, 47 Created: timestamppb.New(now.Add(-30 * time.Minute)), 48 }, 49 "deadbeef~2": { 50 Number: 1, 51 Created: timestamppb.New(now.Add(-1 * time.Hour)), 52 }, 53 }, 54 Labels: map[string]*gerritpb.LabelInfo{ 55 AutoSubmitLabelName: { 56 All: nil, // set in tests. 57 }, 58 }, 59 } 60 c.So(HasAutoSubmit(ci), c.ShouldBeFalse) 61 ci.Labels[AutoSubmitLabelName].All = []*gerritpb.ApprovalInfo{{ 62 User: gf.U("u-1"), 63 Value: modeToVote[run.FullRun], 64 Date: timestamppb.New(now.Add(-15 * time.Minute)), 65 }} 66 c.So(HasAutoSubmit(ci), c.ShouldBeTrue) 67 }) 68 } 69 70 func TestFindCQTrigger(t *testing.T) { 71 t.Parallel() 72 73 user1 := gf.U("u-1") 74 user2 := gf.U("u-2") 75 user3 := gf.U("u-3") 76 user4 := gf.U("u-4") 77 const dryRunVote = 1 78 const fullRunVote = 2 79 cg := &cfgpb.ConfigGroup{} 80 c.Convey("findCQTrigger", t, func() { 81 now := testclock.TestRecentTimeUTC 82 ci := &gerritpb.ChangeInfo{ 83 Status: gerritpb.ChangeStatus_NEW, 84 CurrentRevision: "deadbeef~1", 85 Revisions: map[string]*gerritpb.RevisionInfo{ 86 "deadbeef~1": { 87 Number: 2, 88 Created: timestamppb.New(now.Add(-30 * time.Minute)), 89 }, 90 "deadbeef~2": { 91 Number: 1, 92 Created: timestamppb.New(now.Add(-1 * time.Hour)), 93 }, 94 }, 95 Labels: map[string]*gerritpb.LabelInfo{ 96 CQLabelName: { 97 All: nil, // set in tests. 98 }, 99 }, 100 } 101 102 c.Convey("Abandoned CL", func() { 103 ci.Status = gerritpb.ChangeStatus_ABANDONED 104 c.So(findCQTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg}), c.ShouldBeNil) 105 }) 106 c.Convey("Merged CL", func() { 107 ci.Status = gerritpb.ChangeStatus_MERGED 108 c.So(findCQTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg}), c.ShouldBeNil) 109 }) 110 c.Convey("No votes", func() { 111 c.So(findCQTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg}), c.ShouldBeNil) 112 }) 113 c.Convey("No Commit-Queue label info", func() { 114 ci.Labels = nil 115 c.So(findCQTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg}), c.ShouldBeNil) 116 }) 117 c.Convey("Single vote", func() { 118 ci.Labels[CQLabelName].All = []*gerritpb.ApprovalInfo{{ 119 User: user1, 120 Value: dryRunVote, 121 Date: timestamppb.New(now.Add(-15 * time.Minute)), 122 }} 123 t := findCQTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg}) 124 c.So(t, la.ShouldResembleProto, &run.Trigger{ 125 Time: timestamppb.New(now.Add(-15 * time.Minute)), 126 Mode: string(run.DryRun), 127 GerritAccountId: user1.GetAccountId(), 128 Email: user1.GetEmail(), 129 }) 130 }) 131 c.Convey(">CQ+2 is clamped to CQ+2", func() { 132 ci.Labels[CQLabelName].All = []*gerritpb.ApprovalInfo{{ 133 User: user1, 134 Value: 3, 135 Date: timestamppb.New(now.Add(-15 * time.Minute)), 136 }} 137 t := findCQTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg}) 138 c.So(t, la.ShouldResembleProto, &run.Trigger{ 139 Time: timestamppb.New(now.Add(-15 * time.Minute)), 140 Mode: string(run.FullRun), 141 GerritAccountId: user1.GetAccountId(), 142 Email: user1.GetEmail(), 143 }) 144 }) 145 c.Convey("Earliest votes wins", func() { 146 ci.Labels[CQLabelName].All = []*gerritpb.ApprovalInfo{ 147 { 148 User: user1, 149 Value: dryRunVote, 150 Date: timestamppb.New(now.Add(-15 * time.Minute)), 151 }, 152 { 153 User: user2, 154 Value: dryRunVote, 155 Date: timestamppb.New(now.Add(-5 * time.Minute)), 156 }, 157 } 158 t := findCQTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg}) 159 c.So(t, la.ShouldResembleProto, &run.Trigger{ 160 Time: timestamppb.New(now.Add(-15 * time.Minute)), 161 Mode: string(run.DryRun), 162 GerritAccountId: user1.GetAccountId(), 163 Email: user1.GetEmail(), 164 }) 165 c.Convey("except when some later run was canceled via botdata message", func() { 166 cancelMsg, err := botdata.Append("", botdata.BotData{ 167 Action: botdata.Cancel, 168 TriggeredAt: now.Add(-15 * time.Minute), 169 Revision: ci.GetCurrentRevision(), 170 }) 171 c.So(err, c.ShouldBeNil) 172 ci.Messages = append(ci.Messages, &gerritpb.ChangeMessageInfo{Message: cancelMsg}) 173 t := findCQTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg}) 174 c.So(t, la.ShouldResembleProto, &run.Trigger{ 175 Time: timestamppb.New(now.Add(-5 * time.Minute)), 176 Mode: string(run.DryRun), 177 GerritAccountId: user2.GetAccountId(), 178 Email: user2.GetEmail(), 179 }) 180 }) 181 }) 182 c.Convey("Earliest CQ+2 vote wins against even earlier CQ+1", func() { 183 ci.Labels[CQLabelName].All = []*gerritpb.ApprovalInfo{ 184 { 185 User: user1, 186 Value: dryRunVote, 187 Date: timestamppb.New(now.Add(-15 * time.Minute)), 188 }, 189 { 190 User: user2, 191 Value: fullRunVote, 192 Date: timestamppb.New(now.Add(-10 * time.Minute)), 193 }, 194 { 195 User: user3, 196 Value: dryRunVote, 197 Date: timestamppb.New(now.Add(-5 * time.Minute)), 198 }, 199 { 200 User: user4, 201 Value: fullRunVote, 202 Date: timestamppb.New(now.Add(-1 * time.Minute)), 203 }, 204 } 205 t := findCQTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg}) 206 c.So(t, la.ShouldResembleProto, &run.Trigger{ 207 Time: timestamppb.New(now.Add(-10 * time.Minute)), 208 Mode: string(run.FullRun), 209 GerritAccountId: user2.GetAccountId(), 210 Email: user2.GetEmail(), 211 }) 212 }) 213 c.Convey("Sticky Vote bumps trigger time to cur revision creation time", func() { 214 ci.Labels[CQLabelName].All = []*gerritpb.ApprovalInfo{{ 215 User: user1, 216 Value: fullRunVote, 217 Date: timestamppb.New(now.Add(-15 * time.Minute)), 218 }} 219 ci.CurrentRevision = "deadbeef" 220 ci.Revisions["deadbeef"] = &gerritpb.RevisionInfo{ 221 Number: 3, 222 Created: timestamppb.New(now.Add(-10 * time.Minute)), 223 } 224 t := findCQTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg}) 225 c.So(t, la.ShouldResembleProto, &run.Trigger{ 226 Time: timestamppb.New(now.Add(-10 * time.Minute)), 227 Mode: string(run.FullRun), 228 GerritAccountId: user1.GetAccountId(), 229 Email: user1.GetEmail(), 230 }) 231 }) 232 c.Convey("Additional modes", func() { 233 const customLabel = "Custom" 234 const customRunMode = "CUSTOM_RUN" 235 cg.AdditionalModes = []*cfgpb.Mode{ 236 { 237 Name: customRunMode, 238 CqLabelValue: +1, 239 TriggeringLabel: customLabel, 240 TriggeringValue: +1, 241 }, 242 } 243 ci.Labels[CQLabelName].All = []*gerritpb.ApprovalInfo{ 244 { 245 User: user1, 246 Value: dryRunVote, 247 Date: timestamppb.New(now.Add(-15 * time.Minute)), 248 }, 249 } 250 ci.Labels[customLabel] = &gerritpb.LabelInfo{All: []*gerritpb.ApprovalInfo{ 251 { 252 User: user1, 253 Value: +1, 254 Date: timestamppb.New(now.Add(-15 * time.Minute)), 255 }, 256 }} 257 c.Convey("Simplest possible custom run", func() { 258 t := findCQTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg}) 259 c.So(t, la.ShouldResembleProto, &run.Trigger{ 260 Time: timestamppb.New(now.Add(-15 * time.Minute)), 261 Mode: customRunMode, 262 ModeDefinition: cg.AdditionalModes[0], 263 GerritAccountId: user1.GetAccountId(), 264 Email: user1.GetEmail(), 265 }) 266 }) 267 c.Convey("Custom run despite other users' votes", func() { 268 ci.Labels[CQLabelName].All = []*gerritpb.ApprovalInfo{ 269 { 270 User: user1, 271 Value: dryRunVote, 272 Date: timestamppb.New(now.Add(-15 * time.Minute)), 273 }, 274 { 275 User: user2, 276 Value: dryRunVote, 277 Date: timestamppb.New(now.Add(-10 * time.Minute)), 278 }, 279 } 280 ci.Labels[customLabel] = &gerritpb.LabelInfo{All: []*gerritpb.ApprovalInfo{ 281 { 282 User: user2, 283 Value: +2, 284 Date: timestamppb.New(now.Add(-20 * time.Minute)), 285 }, 286 { 287 User: user1, 288 Value: +1, 289 Date: timestamppb.New(now.Add(-15 * time.Minute)), 290 }, 291 }} 292 t := findCQTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg}) 293 c.So(t.GetMode(), c.ShouldEqual, customRunMode) 294 }) 295 c.Convey("Not applicable cases", func() { 296 c.Convey("Additional vote must have the same timestamp", func() { 297 c.Convey("before", func() { 298 ci.Labels[customLabel].GetAll()[0].Date.Seconds++ 299 t := findCQTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg}) 300 c.So(t.GetMode(), c.ShouldEqual, string(run.DryRun)) 301 }) 302 c.Convey("after", func() { 303 ci.Labels[customLabel].GetAll()[0].Date.Seconds-- 304 t := findCQTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg}) 305 c.So(t.GetMode(), c.ShouldEqual, string(run.DryRun)) 306 }) 307 }) 308 c.Convey("Additional vote be from the same account", func() { 309 ci.Labels[customLabel].GetAll()[0].User = user2 310 t := findCQTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg}) 311 c.So(t.GetMode(), c.ShouldEqual, string(run.DryRun)) 312 }) 313 c.Convey("Additional vote must have expected value", func() { 314 ci.Labels[customLabel].GetAll()[0].Value = 100 315 t := findCQTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg}) 316 c.So(t.GetMode(), c.ShouldEqual, string(run.DryRun)) 317 }) 318 c.Convey("Additional vote must be for the correct label", func() { 319 ci.Labels[customLabel+"-Other"] = ci.Labels[customLabel] 320 delete(ci.Labels, customLabel) 321 t := findCQTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg}) 322 c.So(t.GetMode(), c.ShouldEqual, string(run.DryRun)) 323 }) 324 c.Convey("CQ vote must have correct value", func() { 325 ci.Labels[CQLabelName].GetAll()[0].Value = fullRunVote 326 t := findCQTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg}) 327 c.So(t.GetMode(), c.ShouldEqual, string(run.FullRun)) 328 }) 329 }) 330 }) 331 }) 332 } 333 334 func TestFindNewPatchsetRunTrigger(t *testing.T) { 335 t.Parallel() 336 owner := gf.U("owner") 337 uploader1 := gf.U("ul-1") 338 uploader2 := gf.U("ul-2") 339 ts1 := testclock.TestRecentTimeUTC 340 ts2 := ts1.Add(30 * time.Minute) 341 // Create change info. 342 ci := &gerritpb.ChangeInfo{ 343 Owner: owner, 344 Status: gerritpb.ChangeStatus_NEW, 345 CurrentRevision: "deadbeef~2", 346 Revisions: map[string]*gerritpb.RevisionInfo{ 347 "deadbeef~1": { 348 Number: 1, 349 Uploader: uploader1, 350 Created: timestamppb.New(ts1), 351 }, 352 "deadbeef~2": { 353 Number: 2, 354 Uploader: uploader2, 355 Created: timestamppb.New(ts2), 356 }, 357 }, 358 } 359 360 // Create empty config. 361 cg := &cfgpb.ConfigGroup{} 362 363 // Create CLEntity. 364 cle := &changelist.CL{ 365 TriggerNewPatchsetRunAfterPS: 1, 366 } 367 368 c.Convey("findNewPatchsetRun", t, func() { 369 c.Convey("Not configured to trigger new patchset runs", func() { 370 c.So( 371 findNewPatchsetRunTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg, TriggerNewPatchsetRunAfterPS: cle.TriggerNewPatchsetRunAfterPS}), 372 c.ShouldBeNil, 373 ) 374 }) 375 c.Convey("Configured", func() { 376 // Add npr builder to config. 377 cg.Verifiers = &cfgpb.Verifiers{ 378 Tryjob: &cfgpb.Verifiers_Tryjob{ 379 Builders: []*cfgpb.Verifiers_Tryjob_Builder{ 380 { 381 Name: "builder_name", 382 ModeAllowlist: []string{string(run.NewPatchsetRun)}, 383 }, 384 }, 385 }, 386 } 387 c.Convey("Current patchset not ended", func() { 388 c.So( 389 findNewPatchsetRunTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg, TriggerNewPatchsetRunAfterPS: cle.TriggerNewPatchsetRunAfterPS}), 390 la.ShouldResembleProto, 391 &run.Trigger{ 392 Mode: string(run.NewPatchsetRun), 393 Time: timestamppb.New(ts2), 394 Email: owner.GetEmail(), 395 GerritAccountId: owner.GetAccountId(), 396 }, 397 ) 398 }) 399 c.Convey("Current patchset already ended", func() { 400 // Set CLEntity NewPatchsetUploaded 401 cle.TriggerNewPatchsetRunAfterPS = 2 402 c.So( 403 findNewPatchsetRunTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg, TriggerNewPatchsetRunAfterPS: cle.TriggerNewPatchsetRunAfterPS}), 404 c.ShouldBeNil, 405 ) 406 }) 407 }) 408 }) 409 }