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  }