go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/buildbucket/facade/search_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 "testing" 19 "time" 20 21 "google.golang.org/protobuf/proto" 22 "google.golang.org/protobuf/types/known/structpb" 23 "google.golang.org/protobuf/types/known/timestamppb" 24 25 bbpb "go.chromium.org/luci/buildbucket/proto" 26 "go.chromium.org/luci/common/data/stringset" 27 gerritpb "go.chromium.org/luci/common/proto/gerrit" 28 29 "go.chromium.org/luci/cv/internal/changelist" 30 "go.chromium.org/luci/cv/internal/common" 31 "go.chromium.org/luci/cv/internal/cvtesting" 32 "go.chromium.org/luci/cv/internal/run" 33 "go.chromium.org/luci/cv/internal/tryjob" 34 35 . "github.com/smartystreets/goconvey/convey" 36 ) 37 38 func TestSearch(t *testing.T) { 39 Convey("Search", t, func() { 40 ct := cvtesting.Test{} 41 ctx, cancel := ct.SetUp(t) 42 defer cancel() 43 f := &Facade{ 44 ClientFactory: ct.BuildbucketFake.NewClientFactory(), 45 } 46 47 const ( 48 clid = common.CLID(123) 49 gHost = "example-review.googlesource.com" 50 gRepo = "repo/example" 51 gChangeNum = 753 52 gPatchset = 10 53 gMinEquiPatchset = 5 54 55 bbHost = "buildbucket.example.com" 56 lProject = "testProj" 57 ) 58 builderID := &bbpb.BuilderID{ 59 Project: lProject, 60 Bucket: "testBucket", 61 Builder: "testBuilder", 62 } 63 equiBuilderID := &bbpb.BuilderID{ 64 Project: lProject, 65 Bucket: "testBucket", 66 Builder: "testEquivalentBuilder", 67 } 68 gc := &bbpb.GerritChange{ 69 Host: gHost, 70 Project: gRepo, 71 Change: gChangeNum, 72 Patchset: gPatchset, 73 } 74 epoch := ct.Clock.Now().UTC() 75 cl := &run.RunCL{ 76 ID: clid, 77 ExternalID: changelist.MustGobID(gHost, gChangeNum), 78 Detail: &changelist.Snapshot{ 79 Patchset: gPatchset, 80 MinEquivalentPatchset: gMinEquiPatchset, 81 Kind: &changelist.Snapshot_Gerrit{ 82 Gerrit: &changelist.Gerrit{ 83 Host: gHost, 84 Info: &gerritpb.ChangeInfo{ 85 Project: gRepo, 86 Number: gChangeNum, 87 }, 88 }, 89 }, 90 }, 91 } 92 definition := &tryjob.Definition{ 93 Backend: &tryjob.Definition_Buildbucket_{ 94 Buildbucket: &tryjob.Definition_Buildbucket{ 95 Host: bbHost, 96 Builder: builderID, 97 }, 98 }, 99 EquivalentTo: &tryjob.Definition{ 100 Backend: &tryjob.Definition_Buildbucket_{ 101 Buildbucket: &tryjob.Definition_Buildbucket{ 102 Host: bbHost, 103 Builder: equiBuilderID, 104 }, 105 }, 106 }, 107 } 108 109 ct.BuildbucketFake.AddBuilder(bbHost, builderID, nil) 110 ct.BuildbucketFake.AddBuilder(bbHost, equiBuilderID, nil) 111 bbClient := ct.BuildbucketFake.MustNewClient(ctx, bbHost, lProject) 112 commonMutateFn := func(build *bbpb.Build) { 113 build.Status = bbpb.Status_SUCCESS 114 build.StartTime = timestamppb.New(epoch.Add(1 * time.Minute)) 115 build.EndTime = timestamppb.New(epoch.Add(2 * time.Minute)) 116 } 117 118 Convey("Single Buildbucket host", func() { 119 searchAll := func() []*tryjob.Tryjob { 120 var ret []*tryjob.Tryjob 121 err := f.Search(ctx, []*run.RunCL{cl}, []*tryjob.Definition{definition}, lProject, func(t *tryjob.Tryjob) bool { 122 ret = append(ret, t) 123 return true 124 }) 125 So(err, ShouldBeNil) 126 return ret 127 } 128 Convey("Match", func() { 129 var build *bbpb.Build 130 Convey("Simple", func() { 131 var err error 132 build, err = bbClient.ScheduleBuild(ctx, &bbpb.ScheduleBuildRequest{ 133 Builder: builderID, 134 GerritChanges: []*bbpb.GerritChange{gc}, 135 }) 136 So(err, ShouldBeNil) 137 build = ct.BuildbucketFake.MutateBuild(ctx, bbHost, build.GetId(), commonMutateFn) 138 }) 139 Convey("With permitted additional properties", func() { 140 prop, err := structpb.NewStruct(map[string]any{ 141 "$recipe_engine/cq": map[string]any{ 142 "active": true, 143 "run_mode": "FULL_RUN", 144 }, 145 }) 146 So(err, ShouldBeNil) 147 build, err = bbClient.ScheduleBuild(ctx, &bbpb.ScheduleBuildRequest{ 148 Builder: builderID, 149 Properties: prop, 150 GerritChanges: []*bbpb.GerritChange{gc}, 151 }) 152 So(err, ShouldBeNil) 153 build = ct.BuildbucketFake.MutateBuild(ctx, bbHost, build.GetId(), commonMutateFn) 154 }) 155 Convey("Match equivalent tryjob", func() { 156 var err error 157 build, err = bbClient.ScheduleBuild(ctx, &bbpb.ScheduleBuildRequest{ 158 Builder: equiBuilderID, 159 GerritChanges: []*bbpb.GerritChange{gc}, 160 }) 161 So(err, ShouldBeNil) 162 build = ct.BuildbucketFake.MutateBuild(ctx, bbHost, build.GetId(), commonMutateFn) 163 }) 164 results := searchAll() 165 So(results, ShouldHaveLength, 1) 166 tj := results[0] 167 So(tj.Result, ShouldNotBeNil) 168 tj.Result = nil 169 So(tj, cvtesting.SafeShouldResemble, &tryjob.Tryjob{ 170 ExternalID: tryjob.MustBuildbucketID(bbHost, build.GetId()), 171 Definition: definition, 172 Status: tryjob.Status_ENDED, 173 }) 174 }) 175 176 Convey("No match", func() { 177 Convey("Patchset out of range ", func() { 178 for _, ps := range []int{3, 11, 20} { 179 So(ps < gMinEquiPatchset || ps > gPatchset, ShouldBeTrue) 180 gc.Patchset = int64(ps) 181 build, err := bbClient.ScheduleBuild(ctx, &bbpb.ScheduleBuildRequest{ 182 Builder: builderID, 183 GerritChanges: []*bbpb.GerritChange{gc}, 184 }) 185 So(err, ShouldBeNil) 186 ct.BuildbucketFake.MutateBuild(ctx, bbHost, build.GetId(), commonMutateFn) 187 results := searchAll() 188 So(results, ShouldBeEmpty) 189 } 190 }) 191 192 Convey("Mismatch CL", func() { 193 anotherChange := proto.Clone(gc).(*bbpb.GerritChange) 194 anotherChange.Change = anotherChange.Change + 50 195 build, err := bbClient.ScheduleBuild(ctx, &bbpb.ScheduleBuildRequest{ 196 Builder: builderID, 197 GerritChanges: []*bbpb.GerritChange{anotherChange}, 198 }) 199 So(err, ShouldBeNil) 200 ct.BuildbucketFake.MutateBuild(ctx, bbHost, build.GetId(), commonMutateFn) 201 results := searchAll() 202 So(results, ShouldBeEmpty) 203 }) 204 205 Convey("Mismatch Builder", func() { 206 anotherBuilder := &bbpb.BuilderID{ 207 Project: lProject, 208 Bucket: "anotherBucket", 209 Builder: "anotherBuilder", 210 } 211 ct.BuildbucketFake.AddBuilder(bbHost, anotherBuilder, nil) 212 build, err := bbClient.ScheduleBuild(ctx, &bbpb.ScheduleBuildRequest{ 213 Builder: anotherBuilder, 214 GerritChanges: []*bbpb.GerritChange{gc}, 215 }) 216 So(err, ShouldBeNil) 217 ct.BuildbucketFake.MutateBuild(ctx, bbHost, build.GetId(), commonMutateFn) 218 results := searchAll() 219 So(results, ShouldBeEmpty) 220 }) 221 222 Convey("Not permitted additional properties", func() { 223 prop, err := structpb.NewStruct(map[string]any{ 224 "$recipe_engine/cq": map[string]any{ 225 "active": true, 226 "run_mode": "FULL_RUN", 227 }, // permitted 228 "foo": "bar", // not permitted 229 }) 230 So(err, ShouldBeNil) 231 build, err := bbClient.ScheduleBuild(ctx, &bbpb.ScheduleBuildRequest{ 232 Builder: builderID, 233 Properties: prop, 234 GerritChanges: []*bbpb.GerritChange{gc}, 235 }) 236 So(err, ShouldBeNil) 237 ct.BuildbucketFake.MutateBuild(ctx, bbHost, build.GetId(), commonMutateFn) 238 results := searchAll() 239 So(results, ShouldBeEmpty) 240 }) 241 242 Convey("Multiple CLs", func() { 243 Convey("Build involves extra Gerrit change", func() { 244 anotherChange := proto.Clone(gc).(*bbpb.GerritChange) 245 anotherChange.Change = anotherChange.Change + 1 246 build, err := bbClient.ScheduleBuild(ctx, &bbpb.ScheduleBuildRequest{ 247 Builder: builderID, 248 GerritChanges: []*bbpb.GerritChange{gc, anotherChange}, 249 }) 250 So(err, ShouldBeNil) 251 ct.BuildbucketFake.MutateBuild(ctx, bbHost, build.GetId(), commonMutateFn) 252 results := searchAll() 253 So(results, ShouldBeEmpty) 254 }) 255 256 Convey("Expecting extra Gerrit change", func() { 257 build, err := bbClient.ScheduleBuild(ctx, &bbpb.ScheduleBuildRequest{ 258 Builder: builderID, 259 GerritChanges: []*bbpb.GerritChange{gc}, 260 }) 261 So(err, ShouldBeNil) 262 ct.BuildbucketFake.MutateBuild(ctx, bbHost, build.GetId(), commonMutateFn) 263 264 anotherChange := proto.Clone(gc).(*bbpb.GerritChange) 265 anotherChange.Change = anotherChange.Change + 1 266 anotherCL := &run.RunCL{ 267 ID: clid + 1, 268 ExternalID: changelist.MustGobID(gHost, gChangeNum+1), 269 Detail: &changelist.Snapshot{ 270 Patchset: 3, 271 MinEquivalentPatchset: 1, 272 Kind: &changelist.Snapshot_Gerrit{ 273 Gerrit: &changelist.Gerrit{ 274 Host: gHost, 275 Info: &gerritpb.ChangeInfo{ 276 Project: gRepo, 277 Number: gChangeNum + 1, 278 }, 279 }, 280 }, 281 }, 282 } 283 var tryjobs []*tryjob.Tryjob 284 err = f.Search(ctx, []*run.RunCL{cl, anotherCL}, []*tryjob.Definition{definition}, lProject, func(t *tryjob.Tryjob) bool { 285 tryjobs = append(tryjobs, t) 286 return true 287 }) 288 So(err, ShouldBeNil) 289 So(tryjobs, ShouldBeEmpty) 290 }) 291 }) 292 }) 293 }) 294 295 Convey("Paging builds", func() { 296 // Scenario: 297 // Buildbucket hosts defined in `bbHosts`. Each Buildbucket host has 298 // `numBuildsPerHost` of builds with build ID 1..numBuildsPerHost. 299 // Each even buildID is from builderFoo and each odd buildID is from 300 // builderBar 301 bbHosts := []string{"bb-dev.example.com", "bb-staging.example.com", "bb-prod.example.com"} 302 numBuildsPerHost := 50 303 builderFoo := &bbpb.BuilderID{ 304 Project: lProject, 305 Bucket: "testBucket", 306 Builder: "foo", 307 } 308 builderBar := &bbpb.BuilderID{ 309 Project: lProject, 310 Bucket: "testBucket", 311 Builder: "bar", 312 } 313 allBuilds := make([]*bbpb.Build, 0, len(bbHosts)*numBuildsPerHost) 314 for _, bbHost := range bbHosts { 315 ct.BuildbucketFake.AddBuilder(bbHost, builderFoo, nil) 316 ct.BuildbucketFake.AddBuilder(bbHost, builderBar, nil) 317 bbClient = ct.BuildbucketFake.MustNewClient(ctx, bbHost, lProject) 318 for i := 1; i <= numBuildsPerHost; i++ { 319 epoch = ct.Clock.Now().UTC() 320 builder := builderFoo 321 if i%2 == 1 { 322 builder = builderBar 323 } 324 build, err := bbClient.ScheduleBuild(ctx, &bbpb.ScheduleBuildRequest{ 325 Builder: builder, 326 GerritChanges: []*bbpb.GerritChange{gc}, 327 }) 328 So(err, ShouldBeNil) 329 build = ct.BuildbucketFake.MutateBuild(ctx, bbHost, build.GetId(), func(build *bbpb.Build) { 330 build.Status = bbpb.Status_SUCCESS 331 build.StartTime = timestamppb.New(epoch.Add(1 * time.Minute)) 332 build.EndTime = timestamppb.New(epoch.Add(2 * time.Minute)) 333 }) 334 allBuilds = append(allBuilds, build) 335 ct.Clock.Add(1 * time.Minute) 336 } 337 } 338 Convey("Search for builds from builderFoo", func() { 339 var definitions []*tryjob.Definition 340 expected := stringset.New(numBuildsPerHost / 2 * len(bbHosts)) 341 for _, build := range allBuilds { 342 if proto.Equal(build.GetBuilder(), builderFoo) { 343 expected.Add(string(tryjob.MustBuildbucketID(build.GetInfra().GetBuildbucket().GetHostname(), build.GetId()))) 344 } 345 } 346 for _, bbHost := range bbHosts { 347 definitions = append(definitions, &tryjob.Definition{ 348 Backend: &tryjob.Definition_Buildbucket_{ 349 Buildbucket: &tryjob.Definition_Buildbucket{ 350 Host: bbHost, 351 Builder: builderFoo, 352 }, 353 }, 354 }) 355 } 356 got := stringset.New(numBuildsPerHost / 2 * len(bbHosts)) 357 err := f.Search(ctx, []*run.RunCL{cl}, definitions, lProject, func(t *tryjob.Tryjob) bool { 358 So(got.Has(string(t.ExternalID)), ShouldBeFalse) 359 got.Add(string(t.ExternalID)) 360 return true 361 }) 362 So(err, ShouldBeNil) 363 So(got, ShouldResemble, expected) 364 }) 365 366 Convey("Can stop paging", func() { 367 var definitions []*tryjob.Definition 368 for _, bbHost := range bbHosts { 369 // matching all 370 definitions = append(definitions, 371 &tryjob.Definition{ 372 Backend: &tryjob.Definition_Buildbucket_{ 373 Buildbucket: &tryjob.Definition_Buildbucket{ 374 Host: bbHost, 375 Builder: builderFoo, 376 }, 377 }, 378 }, 379 &tryjob.Definition{ 380 Backend: &tryjob.Definition_Buildbucket_{ 381 Buildbucket: &tryjob.Definition_Buildbucket{ 382 Host: bbHost, 383 Builder: builderBar, 384 }, 385 }, 386 }, 387 ) 388 } 389 stopAfter := numBuildsPerHost * len(bbHosts) / 2 390 count := 0 391 392 err := f.Search(ctx, []*run.RunCL{cl}, definitions, lProject, func(t *tryjob.Tryjob) bool { 393 count++ 394 switch { 395 case count < stopAfter: 396 return true 397 case count == stopAfter: 398 return false 399 default: 400 So("Callback is called after it indicates to stop", ShouldBeEmpty) 401 return true // never reached 402 } 403 }) 404 So(err, ShouldBeNil) 405 }) 406 }) 407 }) 408 }