go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/buildbucket/facade/launch_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 bbfacade 16 17 import ( 18 "fmt" 19 "testing" 20 "time" 21 22 "google.golang.org/protobuf/types/known/structpb" 23 "google.golang.org/protobuf/types/known/timestamppb" 24 25 "go.chromium.org/luci/auth/identity" 26 bbpb "go.chromium.org/luci/buildbucket/proto" 27 "go.chromium.org/luci/common/errors" 28 gerritpb "go.chromium.org/luci/common/proto/gerrit" 29 30 "go.chromium.org/luci/cv/internal/changelist" 31 "go.chromium.org/luci/cv/internal/common" 32 "go.chromium.org/luci/cv/internal/cvtesting" 33 "go.chromium.org/luci/cv/internal/run" 34 "go.chromium.org/luci/cv/internal/tryjob" 35 36 . "github.com/smartystreets/goconvey/convey" 37 . "go.chromium.org/luci/common/testing/assertions" 38 ) 39 40 func TestLaunch(t *testing.T) { 41 Convey("Launch", t, func() { 42 ct := cvtesting.Test{} 43 ctx, cancel := ct.SetUp(t) 44 defer cancel() 45 f := &Facade{ 46 ClientFactory: ct.BuildbucketFake.NewClientFactory(), 47 } 48 49 const ( 50 gHost = "example-review.googlesource.com" 51 gRepo = "repo/example" 52 gChange1 = 11 53 gChange2 = 22 54 55 bbHost = "buildbucket.example.com" 56 bbHost2 = "buildbucket-2.example.com" 57 58 lProject = "testProj" 59 owner1Email = "owner1@example.com" 60 owner2Email = "owner2@example.com" 61 triggerEmail = "triggerer@example.com" 62 ) 63 ownerIdentity, err := identity.MakeIdentity(fmt.Sprintf("user:%s", owner1Email)) 64 So(err, ShouldBeNil) 65 ct.AddMember(ownerIdentity.Email(), "googlers") // Run owner is a googler 66 builderID := &bbpb.BuilderID{ 67 Project: lProject, 68 Bucket: "testBucket", 69 Builder: "testBuilder", 70 } 71 ct.BuildbucketFake.AddBuilder(bbHost, builderID, map[string]any{ 72 "foo": "bar", 73 }) 74 bbClient, err := ct.BuildbucketFake.NewClientFactory().MakeClient(ctx, bbHost, lProject) 75 So(err, ShouldBeNil) 76 77 epoch := ct.Clock.Now().UTC() 78 cl1 := &run.RunCL{ 79 ID: 1, 80 ExternalID: changelist.MustGobID(gHost, gChange1), 81 Detail: &changelist.Snapshot{ 82 Patchset: 4, 83 MinEquivalentPatchset: 3, 84 Kind: &changelist.Snapshot_Gerrit{ 85 Gerrit: &changelist.Gerrit{ 86 Host: gHost, 87 Info: &gerritpb.ChangeInfo{ 88 Project: gRepo, 89 Number: gChange1, 90 Owner: &gerritpb.AccountInfo{ 91 Email: owner1Email, 92 }, 93 }, 94 }, 95 }, 96 }, 97 Trigger: &run.Trigger{ 98 Email: triggerEmail, 99 }, 100 } 101 cl2 := &run.RunCL{ 102 ID: 2, 103 ExternalID: changelist.MustGobID(gHost, gChange2), 104 Detail: &changelist.Snapshot{ 105 Patchset: 10, 106 MinEquivalentPatchset: 10, 107 Kind: &changelist.Snapshot_Gerrit{ 108 Gerrit: &changelist.Gerrit{ 109 Host: gHost, 110 Info: &gerritpb.ChangeInfo{ 111 Project: gRepo, 112 Number: gChange2, 113 Owner: &gerritpb.AccountInfo{ 114 Email: owner2Email, 115 }, 116 }, 117 }, 118 }, 119 }, 120 Trigger: &run.Trigger{ 121 Email: triggerEmail, 122 }, 123 } 124 cls := []*run.RunCL{cl1, cl2} 125 r := &run.Run{ 126 ID: common.MakeRunID(lProject, epoch, 1, []byte("cafe")), 127 Owner: ownerIdentity, 128 CLs: common.CLIDs{cl1.ID, cl2.ID}, 129 Mode: run.DryRun, 130 Options: &run.Options{ 131 CustomTryjobTags: []string{"foo:bar"}, 132 }, 133 } 134 135 Convey("Single Tryjob", func() { 136 definition := &tryjob.Definition{ 137 Backend: &tryjob.Definition_Buildbucket_{ 138 Buildbucket: &tryjob.Definition_Buildbucket{ 139 Host: bbHost, 140 Builder: builderID, 141 }, 142 }, 143 Experiments: []string{"infra.experiment.foo", "infra.experiment.bar"}, 144 } 145 Convey("Not Optional", func() { 146 tj := &tryjob.Tryjob{ 147 ID: 65535, 148 Definition: definition, 149 Status: tryjob.Status_PENDING, 150 } 151 err := f.Launch(ctx, []*tryjob.Tryjob{tj}, r, cls) 152 So(err, ShouldBeNil) 153 So(tj.ExternalID, ShouldNotBeEmpty) 154 host, id, err := tj.ExternalID.ParseBuildbucketID() 155 So(err, ShouldBeNil) 156 So(host, ShouldEqual, bbHost) 157 So(tj.Status, ShouldEqual, tryjob.Status_TRIGGERED) 158 So(tj.Result, ShouldResembleProto, &tryjob.Result{ 159 Status: tryjob.Result_UNKNOWN, 160 CreateTime: timestamppb.New(ct.Clock.Now()), 161 UpdateTime: timestamppb.New(ct.Clock.Now()), 162 Backend: &tryjob.Result_Buildbucket_{ 163 Buildbucket: &tryjob.Result_Buildbucket{ 164 Id: id, 165 Status: bbpb.Status_SCHEDULED, 166 Builder: builderID, 167 }, 168 }, 169 }) 170 build, err := bbClient.GetBuild(ctx, &bbpb.GetBuildRequest{ 171 Id: id, 172 Mask: &bbpb.BuildMask{ 173 AllFields: true, 174 }, 175 }) 176 So(err, ShouldBeNil) 177 So(build.GetBuilder(), ShouldResembleProto, builderID) 178 So(build.GetInput().GetProperties(), ShouldResembleProto, &structpb.Struct{ 179 Fields: map[string]*structpb.Value{ 180 "foo": structpb.NewStringValue("bar"), 181 propertyKey: structpb.NewStructValue(&structpb.Struct{ 182 Fields: map[string]*structpb.Value{ 183 "active": structpb.NewBoolValue(true), 184 "dryRun": structpb.NewBoolValue(true), 185 "topLevel": structpb.NewBoolValue(true), 186 "runMode": structpb.NewStringValue(string(run.DryRun)), 187 "ownerIsGoogler": structpb.NewBoolValue(true), 188 }, 189 }), 190 }, 191 }) 192 So(build.GetInput().GetGerritChanges(), ShouldResembleProto, []*bbpb.GerritChange{ 193 {Host: gHost, Project: gRepo, Change: gChange1, Patchset: 4}, 194 {Host: gHost, Project: gRepo, Change: gChange2, Patchset: 10}, 195 }) 196 So(build.GetTags(), ShouldResembleProto, []*bbpb.StringPair{ 197 {Key: "cq_attempt_key", Value: "63616665"}, 198 {Key: "cq_cl_group_key", Value: "42497728aa4b5097"}, 199 {Key: "cq_cl_owner", Value: owner1Email}, 200 {Key: "cq_cl_owner", Value: owner2Email}, 201 {Key: "cq_cl_tag", Value: "foo:bar"}, 202 {Key: "cq_equivalent_cl_group_key", Value: "1aac15146c0bc164"}, 203 {Key: "cq_experimental", Value: "false"}, 204 {Key: "cq_triggerer", Value: triggerEmail}, 205 {Key: "user_agent", Value: "cq"}, 206 }) 207 So(build.GetInput().GetExperiments(), ShouldResemble, []string{"infra.experiment.bar", "infra.experiment.foo"}) 208 }) 209 210 Convey("Optional Tryjob", func() { 211 definition.Optional = true 212 tj := &tryjob.Tryjob{ 213 ID: 65535, 214 Definition: definition, 215 Status: tryjob.Status_PENDING, 216 } 217 err := f.Launch(ctx, []*tryjob.Tryjob{tj}, r, cls) 218 So(err, ShouldBeNil) 219 So(tj.ExternalID, ShouldNotBeEmpty) 220 _, id, err := tj.ExternalID.ParseBuildbucketID() 221 So(err, ShouldBeNil) 222 build, err := bbClient.GetBuild(ctx, &bbpb.GetBuildRequest{ 223 Id: id, 224 Mask: &bbpb.BuildMask{ 225 AllFields: true, 226 }, 227 }) 228 So(err, ShouldBeNil) 229 So(build.GetInput().GetProperties().GetFields()[propertyKey].GetStructValue().GetFields()["experimental"].GetBoolValue(), ShouldBeTrue) 230 var experimentalTag *bbpb.StringPair 231 for _, tag := range build.GetTags() { 232 if tag.GetKey() == "cq_experimental" { 233 experimentalTag = tag 234 break 235 } 236 } 237 So(experimentalTag, ShouldNotBeNil) 238 So(experimentalTag.GetValue(), ShouldEqual, "true") 239 }) 240 }) 241 242 Convey("Multiple across hosts", func() { 243 tryjobs := []*tryjob.Tryjob{ 244 { 245 ID: 65533, 246 Definition: &tryjob.Definition{ 247 Backend: &tryjob.Definition_Buildbucket_{ 248 Buildbucket: &tryjob.Definition_Buildbucket{ 249 Host: bbHost, 250 Builder: &bbpb.BuilderID{ 251 Project: lProject, 252 Bucket: "bucketFoo", 253 Builder: "builderFoo", 254 }, 255 }, 256 }, 257 }, 258 Status: tryjob.Status_PENDING, 259 }, 260 { 261 ID: 65534, 262 Definition: &tryjob.Definition{ 263 Backend: &tryjob.Definition_Buildbucket_{ 264 Buildbucket: &tryjob.Definition_Buildbucket{ 265 Host: bbHost, 266 Builder: &bbpb.BuilderID{ 267 Project: lProject, 268 Bucket: "bucketBar", 269 Builder: "builderBar", 270 }, 271 }, 272 }, 273 Optional: true, 274 }, 275 Status: tryjob.Status_PENDING, 276 }, 277 { 278 ID: 65535, 279 Definition: &tryjob.Definition{ 280 Backend: &tryjob.Definition_Buildbucket_{ 281 Buildbucket: &tryjob.Definition_Buildbucket{ 282 Host: bbHost2, 283 Builder: &bbpb.BuilderID{ 284 Project: lProject, 285 Bucket: "bucketBaz", 286 Builder: "builderBaz", 287 }, 288 }, 289 }, 290 }, 291 Status: tryjob.Status_PENDING, 292 }, 293 } 294 ct.BuildbucketFake.AddBuilder(bbHost, tryjobs[0].Definition.GetBuildbucket().GetBuilder(), nil) 295 ct.BuildbucketFake.AddBuilder(bbHost, tryjobs[1].Definition.GetBuildbucket().GetBuilder(), nil) 296 ct.BuildbucketFake.AddBuilder(bbHost2, tryjobs[2].Definition.GetBuildbucket().GetBuilder(), nil) 297 err := f.Launch(ctx, tryjobs, r, cls) 298 So(err, ShouldBeNil) 299 for i, tj := range tryjobs { 300 So(tj.ExternalID, ShouldNotBeEmpty) 301 host, id, err := tj.ExternalID.ParseBuildbucketID() 302 So(err, ShouldBeNil) 303 switch i { 304 case 0, 1: 305 So(host, ShouldEqual, bbHost) 306 default: 307 So(host, ShouldEqual, bbHost2) 308 } 309 bbClient, err := ct.BuildbucketFake.NewClientFactory().MakeClient(ctx, host, lProject) 310 So(err, ShouldBeNil) 311 build, err := bbClient.GetBuild(ctx, &bbpb.GetBuildRequest{ 312 Id: id, 313 Mask: &bbpb.BuildMask{ 314 AllFields: true, 315 }, 316 }) 317 So(err, ShouldBeNil) 318 So(build, ShouldNotBeNil) 319 } 320 }) 321 322 Convey("Large number of tryjobs", func() { 323 tryjobs := make([]*tryjob.Tryjob, 2000) 324 for i := range tryjobs { 325 tryjobs[i] = &tryjob.Tryjob{ 326 ID: common.TryjobID(10000 + i), 327 Definition: &tryjob.Definition{ 328 Backend: &tryjob.Definition_Buildbucket_{ 329 Buildbucket: &tryjob.Definition_Buildbucket{ 330 Host: bbHost, 331 Builder: &bbpb.BuilderID{ 332 Project: lProject, 333 Bucket: "bucketFoo", 334 Builder: fmt.Sprintf("builderFoo-%d", i), 335 }, 336 }, 337 }, 338 }, 339 Status: tryjob.Status_PENDING, 340 } 341 ct.BuildbucketFake.AddBuilder(bbHost, tryjobs[i].Definition.GetBuildbucket().GetBuilder(), nil) 342 } 343 err := f.Launch(ctx, tryjobs, r, cls) 344 So(err, ShouldBeNil) 345 for i, tj := range tryjobs { 346 So(tj.ID, ShouldEqual, common.TryjobID(10000+i)) 347 So(tj.ExternalID, ShouldNotBeEmpty) 348 host, id, err := tj.ExternalID.ParseBuildbucketID() 349 So(err, ShouldBeNil) 350 bbClient, err := ct.BuildbucketFake.NewClientFactory().MakeClient(ctx, host, lProject) 351 So(err, ShouldBeNil) 352 build, err := bbClient.GetBuild(ctx, &bbpb.GetBuildRequest{ 353 Id: id, 354 Mask: &bbpb.BuildMask{ 355 AllFields: true, 356 }, 357 }) 358 So(err, ShouldBeNil) 359 So(build.GetBuilder(), ShouldResembleProto, tj.Definition.GetBuildbucket().GetBuilder()) 360 } 361 }) 362 363 Convey("Failure", func() { 364 tryjobs := []*tryjob.Tryjob{ 365 { 366 ID: 65534, 367 Definition: &tryjob.Definition{ 368 Backend: &tryjob.Definition_Buildbucket_{ 369 Buildbucket: &tryjob.Definition_Buildbucket{ 370 Host: bbHost, 371 Builder: builderID, 372 }, 373 }, 374 }, 375 Status: tryjob.Status_PENDING, 376 }, 377 { 378 ID: 65535, 379 Definition: &tryjob.Definition{ 380 Backend: &tryjob.Definition_Buildbucket_{ 381 Buildbucket: &tryjob.Definition_Buildbucket{ 382 Host: bbHost, 383 Builder: &bbpb.BuilderID{ 384 Project: lProject, 385 Bucket: "testBucket", 386 Builder: "anotherBuilder", 387 }, 388 }, 389 }, 390 }, 391 Status: tryjob.Status_PENDING, 392 }, 393 } 394 err := f.Launch(ctx, tryjobs, r, cls) 395 So(err, ShouldNotBeNil) 396 So(err, ShouldHaveSameTypeAs, errors.MultiError{}) 397 merrs := err.(errors.MultiError) 398 So(merrs[0], ShouldBeNil) // First Tryjob launched successfully 399 So(tryjobs[0].ExternalID, ShouldNotBeEmpty) 400 So(merrs[1], ShouldBeRPCNotFound) 401 So(tryjobs[1].ExternalID, ShouldBeEmpty) 402 }) 403 404 Convey("Deduplicate", func() { 405 first := &tryjob.Tryjob{ 406 ID: 655365, 407 Definition: &tryjob.Definition{ 408 Backend: &tryjob.Definition_Buildbucket_{ 409 Buildbucket: &tryjob.Definition_Buildbucket{ 410 Host: bbHost, 411 Builder: builderID, 412 }, 413 }, 414 }, 415 Status: tryjob.Status_PENDING, 416 } 417 second := *first // make a copy 418 err := f.Launch(ctx, []*tryjob.Tryjob{first}, r, cls) 419 So(err, ShouldBeNil) 420 ct.Clock.Add(10 * time.Second) 421 err = f.Launch(ctx, []*tryjob.Tryjob{&second}, r, cls) 422 So(err, ShouldBeNil) 423 So(second.ExternalID, ShouldEqual, first.ExternalID) 424 }) 425 }) 426 }