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  }