go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/tryjob/execute/launch_test.go (about)

     1  // Copyright 2022 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 execute
    16  
    17  import (
    18  	"context"
    19  	"testing"
    20  	"time"
    21  
    22  	"google.golang.org/protobuf/types/known/timestamppb"
    23  
    24  	bbpb "go.chromium.org/luci/buildbucket/proto"
    25  	gerritpb "go.chromium.org/luci/common/proto/gerrit"
    26  	"go.chromium.org/luci/gae/service/datastore"
    27  
    28  	bbfacade "go.chromium.org/luci/cv/internal/buildbucket/facade"
    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  	. "go.chromium.org/luci/common/testing/assertions"
    37  )
    38  
    39  func TestLaunch(t *testing.T) {
    40  	t.Parallel()
    41  
    42  	Convey("Launch", t, func() {
    43  		ct := cvtesting.Test{}
    44  		ctx, cancel := ct.SetUp(t)
    45  		defer cancel()
    46  
    47  		const (
    48  			lProject     = "testProj"
    49  			bbHost       = "buildbucket.example.com"
    50  			buildID      = 9524107902457
    51  			reuseKey     = "cafecafe"
    52  			clid         = 34586452134
    53  			gHost        = "example-review.com"
    54  			gRepo        = "repo/a"
    55  			gChange      = 123
    56  			gPatchset    = 5
    57  			gMinPatchset = 4
    58  		)
    59  		var runID = common.MakeRunID(lProject, ct.Clock.Now().Add(-1*time.Hour), 1, []byte("abcd"))
    60  
    61  		w := &worker{
    62  			run: &run.Run{
    63  				ID:   runID,
    64  				Mode: run.DryRun,
    65  				CLs:  common.CLIDs{clid},
    66  			},
    67  			cls: []*run.RunCL{
    68  				{
    69  					ID: clid,
    70  					Detail: &changelist.Snapshot{
    71  						Patchset:              gPatchset,
    72  						MinEquivalentPatchset: gMinPatchset,
    73  						Kind: &changelist.Snapshot_Gerrit{
    74  							Gerrit: &changelist.Gerrit{
    75  								Host: gHost,
    76  								Info: &gerritpb.ChangeInfo{
    77  									Project: gRepo,
    78  									Number:  gChange,
    79  									Owner: &gerritpb.AccountInfo{
    80  										Email: "owner@example.com",
    81  									},
    82  								},
    83  							},
    84  						},
    85  					},
    86  					Trigger: &run.Trigger{
    87  						Mode:  string(run.DryRun),
    88  						Email: "triggerer@example.com",
    89  					},
    90  				},
    91  			},
    92  			knownTryjobIDs: make(common.TryjobIDSet),
    93  			reuseKey:       reuseKey,
    94  			clPatchsets:    tryjob.CLPatchsets{tryjob.MakeCLPatchset(clid, gPatchset)},
    95  			backend: &bbfacade.Facade{
    96  				ClientFactory: ct.BuildbucketFake.NewClientFactory(),
    97  			},
    98  			rm: run.NewNotifier(ct.TQDispatcher),
    99  		}
   100  		builder := &bbpb.BuilderID{
   101  			Project: lProject,
   102  			Bucket:  "BucketFoo",
   103  			Builder: "BuilderFoo",
   104  		}
   105  		defFoo := &tryjob.Definition{
   106  			Backend: &tryjob.Definition_Buildbucket_{
   107  				Buildbucket: &tryjob.Definition_Buildbucket{
   108  					Host:    bbHost,
   109  					Builder: builder,
   110  				},
   111  			},
   112  		}
   113  		ct.BuildbucketFake.AddBuilder(bbHost, builder, nil)
   114  		Convey("Works", func() {
   115  			tj := w.makePendingTryjob(ctx, defFoo)
   116  			So(datastore.RunInTransaction(ctx, func(ctx context.Context) error {
   117  				return tryjob.SaveTryjobs(ctx, []*tryjob.Tryjob{tj}, nil)
   118  			}, nil), ShouldBeNil)
   119  			tryjobs, err := w.launchTryjobs(ctx, []*tryjob.Tryjob{tj})
   120  			So(err, ShouldBeNil)
   121  			So(tryjobs, ShouldHaveLength, 1)
   122  			eid := tryjobs[0].ExternalID
   123  			So(eid, ShouldNotBeEmpty)
   124  			So(eid.MustLoad(ctx), ShouldNotBeNil)
   125  			So(tryjobs[0].Status, ShouldEqual, tryjob.Status_TRIGGERED)
   126  			host, buildID, err := eid.ParseBuildbucketID()
   127  			So(err, ShouldBeNil)
   128  			So(host, ShouldEqual, bbHost)
   129  			bbClient, err := ct.BuildbucketFake.NewClientFactory().MakeClient(ctx, bbHost, lProject)
   130  			So(err, ShouldBeNil)
   131  			build, err := bbClient.GetBuild(ctx, &bbpb.GetBuildRequest{Id: buildID})
   132  			So(err, ShouldBeNil)
   133  			So(build, ShouldNotBeNil)
   134  			So(w.logEntries, ShouldResembleProto, []*tryjob.ExecutionLogEntry{
   135  				{
   136  					Time: timestamppb.New(ct.Clock.Now().UTC()),
   137  					Kind: &tryjob.ExecutionLogEntry_TryjobsLaunched_{
   138  						TryjobsLaunched: &tryjob.ExecutionLogEntry_TryjobsLaunched{
   139  							Tryjobs: []*tryjob.ExecutionLogEntry_TryjobSnapshot{
   140  								makeLogTryjobSnapshot(defFoo, tryjobs[0], false),
   141  							},
   142  						},
   143  					},
   144  				},
   145  			})
   146  		})
   147  
   148  		Convey("Failed to trigger", func() {
   149  			def := &tryjob.Definition{
   150  				Backend: &tryjob.Definition_Buildbucket_{
   151  					Buildbucket: &tryjob.Definition_Buildbucket{
   152  						Host: bbHost,
   153  						Builder: &bbpb.BuilderID{
   154  							Project: lProject,
   155  							Bucket:  "BucketFoo",
   156  							Builder: "non-existent-builder",
   157  						},
   158  					},
   159  				},
   160  			}
   161  			tj := w.makePendingTryjob(ctx, def)
   162  			So(datastore.RunInTransaction(ctx, func(ctx context.Context) error {
   163  				return tryjob.SaveTryjobs(ctx, []*tryjob.Tryjob{tj}, nil)
   164  			}, nil), ShouldBeNil)
   165  			tryjobs, err := w.launchTryjobs(ctx, []*tryjob.Tryjob{tj})
   166  			So(err, ShouldBeNil)
   167  			So(tryjobs, ShouldHaveLength, 1)
   168  			So(tryjobs[0].ExternalID, ShouldBeEmpty)
   169  			So(tryjobs[0].Status, ShouldEqual, tryjob.Status_UNTRIGGERED)
   170  			So(tryjobs[0].UntriggeredReason, ShouldEqual, "received NotFound from buildbucket. message: builder testProj/BucketFoo/non-existent-builder not found")
   171  			So(w.logEntries, ShouldResembleProto, []*tryjob.ExecutionLogEntry{
   172  				{
   173  					Time: timestamppb.New(ct.Clock.Now().UTC()),
   174  					Kind: &tryjob.ExecutionLogEntry_TryjobsLaunchFailed_{
   175  						TryjobsLaunchFailed: &tryjob.ExecutionLogEntry_TryjobsLaunchFailed{
   176  							Tryjobs: []*tryjob.ExecutionLogEntry_TryjobLaunchFailed{
   177  								{
   178  									Definition: def,
   179  									Reason:     tryjobs[0].UntriggeredReason,
   180  								},
   181  							},
   182  						},
   183  					},
   184  				},
   185  			})
   186  		})
   187  
   188  		Convey("Reconcile with existing", func() {
   189  			tj := w.makePendingTryjob(ctx, defFoo)
   190  			tj.LaunchedBy = runID
   191  			reuseRun := common.MakeRunID(lProject, ct.Clock.Now().Add(-30*time.Minute), 1, []byte("beef"))
   192  			tj.ReusedBy = append(tj.ReusedBy, reuseRun)
   193  			So(datastore.RunInTransaction(ctx, func(ctx context.Context) error {
   194  				return tryjob.SaveTryjobs(ctx, []*tryjob.Tryjob{tj}, nil)
   195  			}, nil), ShouldBeNil)
   196  			existingTryjobID := tj.ID + 59
   197  			w.backend = &decoratedBackend{
   198  				TryjobBackend: w.backend,
   199  				launchedTryjobsHook: func(tryjobs []*tryjob.Tryjob) {
   200  					So(tryjobs, ShouldHaveLength, 1)
   201  					// Save a tryjob that has the same external ID but different internal
   202  					// ID from the input tryjob.
   203  					originalID := tj.ID
   204  					tryjobs[0].ID = existingTryjobID
   205  					So(datastore.RunInTransaction(ctx, func(ctx context.Context) error {
   206  						return tryjob.SaveTryjobs(ctx, []*tryjob.Tryjob{tj}, nil)
   207  					}, nil), ShouldBeNil)
   208  					tryjobs[0].ID = originalID
   209  				},
   210  			}
   211  
   212  			tryjobs, err := w.launchTryjobs(ctx, []*tryjob.Tryjob{tj})
   213  			So(err, ShouldBeNil)
   214  			So(tryjobs, ShouldHaveLength, 1)
   215  			So(tryjobs[0].ID, ShouldEqual, existingTryjobID)
   216  			So(tryjobs[0].ExternalID.MustLoad(ctx).ID, ShouldEqual, existingTryjobID)
   217  			// Check the dropped tryjob
   218  			tj = &tryjob.Tryjob{ID: tj.ID}
   219  			So(datastore.Get(ctx, tj), ShouldBeNil)
   220  			So(tj.Status, ShouldEqual, tryjob.Status_UNTRIGGERED)
   221  		})
   222  		Convey("Launched Tryjob has CL in submission order", func() {
   223  			depCL := &run.RunCL{
   224  				ID: clid + 1,
   225  				Detail: &changelist.Snapshot{
   226  					Patchset:              gPatchset,
   227  					MinEquivalentPatchset: gMinPatchset,
   228  					Kind: &changelist.Snapshot_Gerrit{
   229  						Gerrit: &changelist.Gerrit{
   230  							Host: gHost,
   231  							Info: &gerritpb.ChangeInfo{
   232  								Project: gRepo,
   233  								Number:  gChange + 1,
   234  								Owner: &gerritpb.AccountInfo{
   235  									Email: "owner@example.com",
   236  								},
   237  							},
   238  						},
   239  					},
   240  				},
   241  				Trigger: &run.Trigger{
   242  					Mode:  string(run.DryRun),
   243  					Email: "triggerer@example.com",
   244  				},
   245  			}
   246  			w.cls[0].Detail.Deps = []*changelist.Dep{
   247  				{Clid: int64(depCL.ID), Kind: changelist.DepKind_HARD},
   248  			}
   249  			w.cls[0].Detail.GetGerrit().GitDeps = []*changelist.GerritGitDep{
   250  				{Change: depCL.Detail.GetGerrit().GetInfo().GetNumber(), Immediate: true},
   251  			}
   252  			w.cls = append(w.cls, depCL)
   253  			tj := w.makePendingTryjob(ctx, defFoo)
   254  			So(datastore.RunInTransaction(ctx, func(ctx context.Context) error {
   255  				return tryjob.SaveTryjobs(ctx, []*tryjob.Tryjob{tj}, nil)
   256  			}, nil), ShouldBeNil)
   257  			tryjobs, err := w.launchTryjobs(ctx, []*tryjob.Tryjob{tj})
   258  			So(err, ShouldBeNil)
   259  			So(tryjobs, ShouldHaveLength, 1)
   260  			eid := tryjobs[0].ExternalID
   261  			So(eid, ShouldNotBeEmpty)
   262  			So(eid.MustLoad(ctx), ShouldNotBeNil)
   263  			So(tryjobs[0].Status, ShouldEqual, tryjob.Status_TRIGGERED)
   264  			host, buildID, err := eid.ParseBuildbucketID()
   265  			So(err, ShouldBeNil)
   266  			So(host, ShouldEqual, bbHost)
   267  			bbClient, err := ct.BuildbucketFake.NewClientFactory().MakeClient(ctx, bbHost, lProject)
   268  			So(err, ShouldBeNil)
   269  			build, err := bbClient.GetBuild(ctx, &bbpb.GetBuildRequest{Id: buildID})
   270  			So(err, ShouldBeNil)
   271  			So(build.Input.GetGerritChanges(), ShouldResembleProto, []*bbpb.GerritChange{
   272  				// Dep CL is listed first even though in the worker it is after the
   273  				// dependent CL.
   274  				{Host: gHost, Project: gRepo, Change: depCL.Detail.GetGerrit().GetInfo().GetNumber(), Patchset: gPatchset},
   275  				{Host: gHost, Project: gRepo, Change: w.cls[0].Detail.GetGerrit().GetInfo().GetNumber(), Patchset: gPatchset},
   276  			})
   277  		})
   278  	})
   279  }
   280  
   281  // decoratedBackend allows test to instrument TryjobBackend interface.
   282  type decoratedBackend struct {
   283  	TryjobBackend
   284  	launchedTryjobsHook func([]*tryjob.Tryjob)
   285  }
   286  
   287  func (db *decoratedBackend) Launch(ctx context.Context, tryjobs []*tryjob.Tryjob, r *run.Run, cls []*run.RunCL) error {
   288  	if err := db.TryjobBackend.Launch(ctx, tryjobs, r, cls); err != nil {
   289  		return err
   290  	}
   291  	if db.launchedTryjobsHook != nil {
   292  		db.launchedTryjobsHook(tryjobs)
   293  	}
   294  	return nil
   295  }