go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/run/bq/bq_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 bq 16 17 import ( 18 "fmt" 19 "testing" 20 "time" 21 22 "google.golang.org/protobuf/types/known/timestamppb" 23 24 buildbucketpb "go.chromium.org/luci/buildbucket/proto" 25 bbutil "go.chromium.org/luci/buildbucket/protoutil" 26 gerritpb "go.chromium.org/luci/common/proto/gerrit" 27 "go.chromium.org/luci/gae/service/datastore" 28 29 cvbqpb "go.chromium.org/luci/cv/api/bigquery/v1" 30 cfgpb "go.chromium.org/luci/cv/api/config/v2" 31 "go.chromium.org/luci/cv/internal/changelist" 32 "go.chromium.org/luci/cv/internal/common" 33 "go.chromium.org/luci/cv/internal/configs/prjcfg/prjcfgtest" 34 "go.chromium.org/luci/cv/internal/cvtesting" 35 "go.chromium.org/luci/cv/internal/run" 36 "go.chromium.org/luci/cv/internal/tryjob" 37 38 . "github.com/smartystreets/goconvey/convey" 39 . "go.chromium.org/luci/common/testing/assertions" 40 ) 41 42 func TestMakeAttempt(t *testing.T) { 43 Convey("makeAttempt", t, func() { 44 ct := cvtesting.Test{} 45 ctx, cancel := ct.SetUp(t) 46 defer cancel() 47 epoch := ct.Clock.Now().UTC() 48 const ( 49 lProject = "infra" 50 bbHost = "cr-buildbucket.appspot.com" 51 gHost = "foo-review.googlesource.com" 52 gRepo = "test/repo" 53 gRef = "refs/head/main" 54 gChange = 101 55 gPatchset = 47 56 gEquiPatchset = 42 57 gBuildID1 = 100001 58 gBuildID2 = 100002 59 gBuildID3 = 100003 60 gBuildID4 = 100004 61 ) 62 63 plainBuilder := &buildbucketpb.BuilderID{ 64 Project: lProject, 65 Bucket: "try", 66 Builder: "plain", 67 } 68 reuseDisabledBuilder := &buildbucketpb.BuilderID{ 69 Project: lProject, 70 Bucket: "try", 71 Builder: "disable-reuse", 72 } 73 optionalBuilder := &buildbucketpb.BuilderID{ 74 Project: lProject, 75 Bucket: "try", 76 Builder: "optional", 77 } 78 runID := common.MakeRunID(lProject, epoch, 1, []byte("aaa")) 79 cfg := &cfgpb.Config{ 80 ConfigGroups: []*cfgpb.ConfigGroup{ 81 { 82 Name: "main", 83 Verifiers: &cfgpb.Verifiers{ 84 Tryjob: &cfgpb.Verifiers_Tryjob{ 85 Builders: []*cfgpb.Verifiers_Tryjob_Builder{ 86 { 87 Name: bbutil.FormatBuilderID(plainBuilder), 88 }, 89 { 90 Name: bbutil.FormatBuilderID(reuseDisabledBuilder), 91 DisableReuse: true, 92 }, 93 { 94 Name: bbutil.FormatBuilderID(optionalBuilder), 95 ExperimentPercentage: 100, 96 }, 97 }, 98 }, 99 }, 100 }, 101 }, 102 } 103 prjcfgtest.Create(ctx, lProject, cfg) 104 105 cl := &run.RunCL{ 106 ID: gChange + 1000, 107 Run: datastore.MakeKey(ctx, common.RunKind, string(runID)), 108 ExternalID: changelist.MustGobID(gHost, gChange), 109 Detail: &changelist.Snapshot{ 110 LuciProject: lProject, 111 Patchset: gPatchset, 112 MinEquivalentPatchset: gEquiPatchset, 113 Kind: &changelist.Snapshot_Gerrit{ 114 Gerrit: &changelist.Gerrit{ 115 Host: gHost, 116 Info: &gerritpb.ChangeInfo{ 117 Number: gChange, 118 Project: gRepo, 119 Ref: gRef, 120 Owner: &gerritpb.AccountInfo{ 121 Name: "Foo Bar", 122 Email: "foobar@example.com", 123 }, 124 }, 125 }, 126 }, 127 }, 128 Trigger: &run.Trigger{Time: timestamppb.New(epoch)}, 129 } 130 131 r := &run.Run{ 132 ID: common.RunID(runID), 133 Status: run.Status_SUCCEEDED, 134 ConfigGroupID: prjcfgtest.MustExist(ctx, lProject).ConfigGroupIDs[0], 135 CreateTime: epoch, 136 StartTime: epoch.Add(time.Minute * 2), 137 EndTime: epoch.Add(time.Minute * 25), 138 CLs: common.CLIDs{cl.ID}, 139 Mode: run.FullRun, 140 Submission: &run.Submission{ 141 Cls: []int64{int64(cl.ID)}, 142 SubmittedCls: []int64{int64(cl.ID)}, 143 }, 144 Tryjobs: &run.Tryjobs{ 145 State: &tryjob.ExecutionState{ 146 Requirement: &tryjob.Requirement{ 147 Definitions: []*tryjob.Definition{ 148 { 149 Backend: &tryjob.Definition_Buildbucket_{ 150 Buildbucket: &tryjob.Definition_Buildbucket{ 151 Host: bbHost, 152 Builder: plainBuilder, 153 }, 154 }, 155 Critical: true, 156 }, 157 { 158 Backend: &tryjob.Definition_Buildbucket_{ 159 Buildbucket: &tryjob.Definition_Buildbucket{ 160 Host: bbHost, 161 Builder: reuseDisabledBuilder, 162 }, 163 }, 164 DisableReuse: true, 165 Critical: true, 166 }, 167 { 168 Backend: &tryjob.Definition_Buildbucket_{ 169 Buildbucket: &tryjob.Definition_Buildbucket{ 170 Host: bbHost, 171 Builder: optionalBuilder, 172 }, 173 }, 174 Optional: true, 175 Critical: false, 176 }, 177 }, 178 }, 179 Executions: []*tryjob.ExecutionState_Execution{ 180 { 181 Attempts: []*tryjob.ExecutionState_Execution_Attempt{ 182 { 183 ExternalId: string(tryjob.MustBuildbucketID(bbHost, gBuildID4)), 184 Status: tryjob.Status_ENDED, 185 Reused: true, 186 }, 187 { 188 ExternalId: string(tryjob.MustBuildbucketID(bbHost, gBuildID1)), 189 Status: tryjob.Status_ENDED, 190 }, 191 }, 192 }, 193 { 194 Attempts: []*tryjob.ExecutionState_Execution_Attempt{ 195 { 196 ExternalId: string(tryjob.MustBuildbucketID(bbHost, gBuildID2)), 197 Status: tryjob.Status_ENDED, 198 }, 199 }, 200 }, 201 { 202 Attempts: []*tryjob.ExecutionState_Execution_Attempt{ 203 { 204 // tryjob not triggered so external id is missing. 205 ExternalId: "", 206 Status: tryjob.Status_UNTRIGGERED, 207 }, 208 { 209 ExternalId: string(tryjob.MustBuildbucketID(bbHost, gBuildID3)), 210 Status: tryjob.Status_ENDED, 211 }, 212 }, 213 }, 214 }, 215 Status: tryjob.ExecutionState_SUCCEEDED, 216 }, 217 }, 218 } 219 220 Convey("All fields", func() { 221 a, err := makeAttempt(ctx, r, []*run.RunCL{cl}) 222 So(err, ShouldBeNil) 223 So(a, ShouldResembleProto, &cvbqpb.Attempt{ 224 Key: runID.AttemptKey(), 225 LuciProject: lProject, 226 ConfigGroup: cfg.GetConfigGroups()[0].GetName(), 227 ClGroupKey: "2fb6f02ce54ceef7", 228 EquivalentClGroupKey: "b5aefc068a978ddc", 229 StartTime: timestamppb.New(epoch), 230 ActualStartTime: timestamppb.New(epoch.Add(2 * time.Minute)), 231 EndTime: timestamppb.New(epoch.Add(25 * time.Minute)), 232 Status: cvbqpb.AttemptStatus_SUCCESS, 233 Substatus: cvbqpb.AttemptSubstatus_NO_SUBSTATUS, 234 GerritChanges: []*cvbqpb.GerritChange{ 235 { 236 Host: gHost, 237 Project: gRepo, 238 Change: gChange, 239 Patchset: gPatchset, 240 EarliestEquivalentPatchset: gEquiPatchset, 241 TriggerTime: timestamppb.New(epoch), 242 Mode: cvbqpb.Mode_FULL_RUN, 243 SubmitStatus: cvbqpb.GerritChange_SUCCESS, 244 Owner: "foobar@example.com", 245 IsOwnerBot: false, 246 }, 247 }, 248 Builds: []*cvbqpb.Build{ 249 { 250 Host: bbHost, 251 Id: gBuildID1, 252 Origin: cvbqpb.Build_NOT_REUSED, 253 Critical: true, 254 }, 255 { 256 Host: bbHost, 257 Id: gBuildID2, 258 Origin: cvbqpb.Build_NOT_REUSABLE, 259 Critical: true, 260 }, 261 { 262 Host: bbHost, 263 Id: gBuildID3, 264 Origin: cvbqpb.Build_NOT_REUSED, 265 Critical: false, 266 }, 267 { 268 Host: bbHost, 269 Id: gBuildID4, 270 Origin: cvbqpb.Build_REUSED, 271 Critical: true, 272 }, 273 }, 274 }) 275 }) 276 277 Convey("Partial submission", func() { 278 clSubmitted := &run.RunCL{ 279 ID: 1, 280 Run: datastore.MakeKey(ctx, common.RunKind, string(runID)), 281 ExternalID: changelist.MustGobID(gHost, 1), 282 Detail: &changelist.Snapshot{ 283 LuciProject: lProject, 284 Patchset: 11, 285 MinEquivalentPatchset: 11, 286 Kind: &changelist.Snapshot_Gerrit{ 287 Gerrit: &changelist.Gerrit{ 288 Host: gHost, 289 Info: &gerritpb.ChangeInfo{ 290 Number: 1, 291 Project: gRepo, 292 Ref: gRef, 293 }, 294 }, 295 }, 296 }, 297 Trigger: &run.Trigger{Time: timestamppb.New(epoch)}, 298 } 299 clFailedToSubmit := &run.RunCL{ 300 ID: 2, 301 Run: datastore.MakeKey(ctx, common.RunKind, string(runID)), 302 ExternalID: changelist.MustGobID(gHost, 2), 303 Detail: &changelist.Snapshot{ 304 LuciProject: lProject, 305 Patchset: 22, 306 MinEquivalentPatchset: 22, 307 Kind: &changelist.Snapshot_Gerrit{ 308 Gerrit: &changelist.Gerrit{ 309 Host: gHost, 310 Info: &gerritpb.ChangeInfo{ 311 Number: 2, 312 Project: gRepo, 313 Ref: gRef, 314 }, 315 }, 316 }, 317 }, 318 Trigger: &run.Trigger{Time: timestamppb.New(epoch)}, 319 } 320 clPendingToSubmit := &run.RunCL{ 321 ID: 3, 322 Run: datastore.MakeKey(ctx, common.RunKind, string(runID)), 323 ExternalID: changelist.MustGobID(gHost, 3), 324 Detail: &changelist.Snapshot{ 325 LuciProject: lProject, 326 Patchset: 33, 327 MinEquivalentPatchset: 33, 328 Kind: &changelist.Snapshot_Gerrit{ 329 Gerrit: &changelist.Gerrit{ 330 Host: gHost, 331 Info: &gerritpb.ChangeInfo{ 332 Number: 3, 333 Project: gRepo, 334 Ref: gRef, 335 }, 336 }, 337 }, 338 }, 339 Trigger: &run.Trigger{Time: timestamppb.New(epoch)}, 340 } 341 r.CLs = common.CLIDs{clSubmitted.ID, clFailedToSubmit.ID, clPendingToSubmit.ID} 342 r.Status = run.Status_FAILED 343 r.Submission = &run.Submission{ 344 Cls: []int64{ 345 int64(clSubmitted.ID), 346 int64(clFailedToSubmit.ID), 347 int64(clPendingToSubmit.ID), 348 }, 349 SubmittedCls: []int64{int64(clSubmitted.ID)}, 350 FailedCls: []int64{int64(clFailedToSubmit.ID)}, 351 } 352 353 a, err := makeAttempt(ctx, r, []*run.RunCL{ 354 clSubmitted, clFailedToSubmit, clPendingToSubmit, 355 }) 356 So(err, ShouldBeNil) 357 So(a.GetGerritChanges(), ShouldResembleProto, []*cvbqpb.GerritChange{ 358 { 359 Host: gHost, 360 Project: gRepo, 361 Change: 1, 362 Patchset: 11, 363 EarliestEquivalentPatchset: 11, 364 TriggerTime: timestamppb.New(epoch), 365 Mode: cvbqpb.Mode_FULL_RUN, 366 SubmitStatus: cvbqpb.GerritChange_SUCCESS, 367 }, 368 { 369 Host: gHost, 370 Project: gRepo, 371 Change: 2, 372 Patchset: 22, 373 EarliestEquivalentPatchset: 22, 374 TriggerTime: timestamppb.New(epoch), 375 Mode: cvbqpb.Mode_FULL_RUN, 376 SubmitStatus: cvbqpb.GerritChange_FAILURE, 377 }, 378 { 379 Host: gHost, 380 Project: gRepo, 381 Change: 3, 382 Patchset: 33, 383 EarliestEquivalentPatchset: 33, 384 TriggerTime: timestamppb.New(epoch), 385 Mode: cvbqpb.Mode_FULL_RUN, 386 SubmitStatus: cvbqpb.GerritChange_PENDING, 387 }, 388 }) 389 // In the case of submit failure for one or more CLs, 390 // the Attempt value is still SUCCESS, for backwards 391 // compatibility. 392 So(a.Status, ShouldEqual, cvbqpb.AttemptStatus_SUCCESS) 393 So(a.Substatus, ShouldEqual, cvbqpb.AttemptSubstatus_NO_SUBSTATUS) 394 }) 395 396 Convey("Failed Tryjob", func() { 397 r.Tryjobs.GetState().Status = tryjob.ExecutionState_FAILED 398 r.Status = run.Status_FAILED 399 a, err := makeAttempt(ctx, r, []*run.RunCL{cl}) 400 So(err, ShouldBeNil) 401 So(a.Status, ShouldEqual, cvbqpb.AttemptStatus_FAILURE) 402 So(a.Substatus, ShouldEqual, cvbqpb.AttemptSubstatus_FAILED_TRYJOBS) 403 }) 404 405 Convey("Failed due to missing approval", func() { 406 // TODO(crbug/1342810): Populate run failure reason 407 r.Status = run.Status_FAILED 408 a, err := makeAttempt(ctx, r, []*run.RunCL{cl}) 409 So(err, ShouldBeNil) 410 So(a.Status, ShouldEqual, cvbqpb.AttemptStatus_FAILURE) 411 So(a.Substatus, ShouldEqual, cvbqpb.AttemptSubstatus_UNAPPROVED) 412 }) 413 414 Convey("Cancelled", func() { 415 // TODO(crbug/1342810): Populate run failure reason 416 r.Status = run.Status_CANCELLED 417 a, err := makeAttempt(ctx, r, []*run.RunCL{cl}) 418 So(err, ShouldBeNil) 419 So(a.Status, ShouldEqual, cvbqpb.AttemptStatus_ABORTED) 420 So(a.Substatus, ShouldEqual, cvbqpb.AttemptSubstatus_MANUAL_CANCEL) 421 }) 422 423 Convey("Empty actual start time", func() { 424 r.StartTime = time.Time{} 425 a, err := makeAttempt(ctx, r, []*run.RunCL{cl}) 426 So(err, ShouldBeNil) 427 So(a.GetActualStartTime(), ShouldBeNil) 428 }) 429 430 Convey("HasCustomRequirement", func() { 431 r.Options = &run.Options{ 432 IncludedTryjobs: []string{fmt.Sprintf("%s/try: cool-builder", lProject)}, 433 } 434 a, err := makeAttempt(ctx, r, []*run.RunCL{cl}) 435 So(err, ShouldBeNil) 436 So(a.GetHasCustomRequirement(), ShouldBeTrue) 437 }) 438 439 Convey("Owner is bot", func() { 440 Convey("tagged with service user", func() { 441 cl.Detail.GetGerrit().GetInfo().GetOwner().Tags = []string{"SERVICE_USER"} 442 a, err := makeAttempt(ctx, r, []*run.RunCL{cl}) 443 So(err, ShouldBeNil) 444 So(a.GerritChanges[0].IsOwnerBot, ShouldBeTrue) 445 }) 446 Convey("domain is prod.google.com", func() { 447 cl.Detail.GetGerrit().GetInfo().GetOwner().Email = "abc@prod.google.com" 448 a, err := makeAttempt(ctx, r, []*run.RunCL{cl}) 449 So(err, ShouldBeNil) 450 So(a.GerritChanges[0].IsOwnerBot, ShouldBeTrue) 451 }) 452 Convey("domain is gserviceaccount.com", func() { 453 cl.Detail.GetGerrit().GetInfo().GetOwner().Email = "xyz@proj-foo.iam.gserviceaccount.com" 454 a, err := makeAttempt(ctx, r, []*run.RunCL{cl}) 455 So(err, ShouldBeNil) 456 So(a.GerritChanges[0].IsOwnerBot, ShouldBeTrue) 457 }) 458 }) 459 }) 460 }