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  }