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 }