go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/gerrit/updater/updater_test.go (about) 1 // Copyright 2020 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 updater 16 17 import ( 18 "context" 19 "sort" 20 "sync/atomic" 21 "testing" 22 "time" 23 24 "google.golang.org/grpc/codes" 25 "google.golang.org/grpc/status" 26 "google.golang.org/protobuf/proto" 27 "google.golang.org/protobuf/types/known/timestamppb" 28 29 "go.chromium.org/luci/common/errors" 30 gerritpb "go.chromium.org/luci/common/proto/gerrit" 31 "go.chromium.org/luci/server/tq" 32 33 cfgpb "go.chromium.org/luci/cv/api/config/v2" 34 "go.chromium.org/luci/cv/internal/changelist" 35 "go.chromium.org/luci/cv/internal/common" 36 "go.chromium.org/luci/cv/internal/configs/prjcfg/prjcfgtest" 37 "go.chromium.org/luci/cv/internal/cvtesting" 38 "go.chromium.org/luci/cv/internal/gerrit" 39 gf "go.chromium.org/luci/cv/internal/gerrit/gerritfake" 40 "go.chromium.org/luci/cv/internal/gerrit/gobmap/gobmaptest" 41 42 . "github.com/smartystreets/goconvey/convey" 43 . "go.chromium.org/luci/common/testing/assertions" 44 ) 45 46 func TestUpdaterBackend(t *testing.T) { 47 t.Parallel() 48 49 Convey("updaterBackend methods work, except Fetch()", t, func() { 50 // Fetch() is covered in TestUpdaterBackendFetch. 51 ct := cvtesting.Test{} 52 ctx, cancel := ct.SetUp(t) 53 defer cancel() 54 55 gu := &updaterBackend{ 56 clUpdater: changelist.NewUpdater(ct.TQDispatcher, changelist.NewMutator(ct.TQDispatcher, &pmMock{}, &rmMock{}, &tjMock{})), 57 gFactory: ct.GFactory(), 58 } 59 60 Convey("Kind", func() { 61 So(gu.Kind(), ShouldEqual, "gerrit") 62 }) 63 Convey("TQErrorSpec", func() { 64 tqSpec := gu.TQErrorSpec() 65 err := errors.Annotate(gerrit.ErrStaleData, "retry, don't ignore").Err() 66 So(tq.Ignore.In(tqSpec.Error(ctx, err)), ShouldBeFalse) 67 }) 68 Convey("LookupApplicableConfig", func() { 69 const gHost = "x-review.example.com" 70 prjcfgtest.Create(ctx, "luci-project-x", singleRepoConfig(gHost, "x")) 71 gobmaptest.Update(ctx, "luci-project-x") 72 xConfigGroupID := string(prjcfgtest.MustExist(ctx, "luci-project-x").ConfigGroupIDs[0]) 73 74 // Setup valid CL snapshot; it'll be modified in tests below. 75 cl := &changelist.CL{ 76 ID: 11111, 77 Snapshot: &changelist.Snapshot{ 78 Kind: &changelist.Snapshot_Gerrit{Gerrit: &changelist.Gerrit{ 79 Host: gHost, 80 Info: &gerritpb.ChangeInfo{ 81 Number: 456, 82 Ref: "refs/heads/main", 83 Project: "x", 84 }, 85 }}, 86 }, 87 } 88 89 Convey("Happy path", func() { 90 acfg, err := gu.LookupApplicableConfig(ctx, cl) 91 So(err, ShouldBeNil) 92 So(acfg, ShouldResembleProto, &changelist.ApplicableConfig{ 93 Projects: []*changelist.ApplicableConfig_Project{ 94 {Name: "luci-project-x", ConfigGroupIds: []string{string(xConfigGroupID)}}, 95 }, 96 }) 97 }) 98 99 Convey("CL without Gerrit Snapshot can't be decided on", func() { 100 cl.Snapshot.Kind = nil 101 acfg, err := gu.LookupApplicableConfig(ctx, cl) 102 So(err, ShouldBeNil) 103 So(acfg, ShouldBeNil) 104 }) 105 106 Convey("No watching projects", func() { 107 cl.Snapshot.GetGerrit().GetInfo().Ref = "ref/un/watched" 108 acfg, err := gu.LookupApplicableConfig(ctx, cl) 109 So(err, ShouldBeNil) 110 // Must be empty, but not nil per updaterBackend interface contract. 111 So(acfg, ShouldNotBeNil) 112 So(acfg, ShouldResembleProto, &changelist.ApplicableConfig{}) 113 }) 114 115 Convey("Works with >1 LUCI project watching the same CL", func() { 116 prjcfgtest.Create(ctx, "luci-project-dupe", singleRepoConfig(gHost, "x")) 117 gobmaptest.Update(ctx, "luci-project-dupe") 118 acfg, err := gu.LookupApplicableConfig(ctx, cl) 119 So(err, ShouldBeNil) 120 So(acfg.GetProjects(), ShouldHaveLength, 2) 121 }) 122 }) 123 124 Convey("HasChanged", func() { 125 ciInCV := gf.CI(1, gf.Updated(ct.Clock.Now()), gf.MetaRevID("deadbeef")) 126 ciInGerrit := proto.Clone(ciInCV).(*gerritpb.ChangeInfo) 127 snapshotInCV := &changelist.Snapshot{ 128 Kind: &changelist.Snapshot_Gerrit{ 129 Gerrit: &changelist.Gerrit{ 130 Info: ciInCV, 131 }, 132 }, 133 } 134 snapshotInGerrit := &changelist.Snapshot{ 135 Kind: &changelist.Snapshot_Gerrit{ 136 Gerrit: &changelist.Gerrit{ 137 Info: ciInGerrit, 138 }, 139 }, 140 } 141 Convey("Returns false for exact same snapshot", func() { 142 So(gu.HasChanged(snapshotInCV, snapshotInGerrit), ShouldBeFalse) 143 }) 144 Convey("Returns true for greater update time at backend", func() { 145 ct.Clock.Add(1 * time.Minute) 146 gf.Updated(ct.Clock.Now())(ciInGerrit) 147 So(gu.HasChanged(snapshotInCV, snapshotInGerrit), ShouldBeTrue) 148 }) 149 Convey("Returns false for greater update time in CV", func() { 150 ct.Clock.Add(1 * time.Minute) 151 gf.Updated(ct.Clock.Now())(ciInCV) 152 So(gu.HasChanged(snapshotInCV, snapshotInGerrit), ShouldBeFalse) 153 }) 154 Convey("Returns true if meta rev id is different", func() { 155 gf.MetaRevID("cafecafe")(ciInGerrit) 156 So(gu.HasChanged(snapshotInCV, snapshotInGerrit), ShouldBeTrue) 157 }) 158 }) 159 }) 160 } 161 162 func TestUpdaterBackendFetch(t *testing.T) { 163 t.Parallel() 164 165 Convey("updaterBackend.Fetch() works", t, func() { 166 ct := cvtesting.Test{} 167 ctx, cancel := ct.SetUp(t) 168 defer cancel() 169 170 const ( 171 lProject = "proj-1" 172 gHost = "chromium-review.example.com" 173 gHostInternal = "internal-review.example.com" 174 gRepo = "depot_tools" 175 gChange = 123 176 ) 177 task := &changelist.UpdateCLTask{ 178 LuciProject: lProject, 179 Hint: &changelist.UpdateCLTask_Hint{ExternalUpdateTime: timestamppb.New(time.Time{})}, 180 Requester: changelist.UpdateCLTask_RUN_POKE, 181 } 182 183 prjcfgtest.Create(ctx, lProject, singleRepoConfig(gHost, gRepo)) 184 gobmaptest.Update(ctx, lProject) 185 externalID := changelist.MustGobID(gHost, gChange) 186 187 // NOTE: this test doesn't actually care about pmMock/rmMock, but they are 188 // provided because they are used by the changelist.Updater to process deps 189 // on our (backend's) behalf. 190 gu := &updaterBackend{ 191 clUpdater: changelist.NewUpdater(ct.TQDispatcher, changelist.NewMutator(ct.TQDispatcher, &pmMock{}, &rmMock{}, &tjMock{})), 192 gFactory: ct.GFactory(), 193 } 194 gu.clUpdater.RegisterBackend(gu) 195 196 assertUpdateCLScheduledFor := func(expectedChanges ...int) { 197 var actual []int 198 for _, p := range ct.TQ.Tasks().Payloads() { 199 if t, ok := p.(*changelist.UpdateCLTask); ok { 200 _, changeNumber, err := changelist.ExternalID(t.GetExternalId()).ParseGobID() 201 So(err, ShouldBeNil) 202 actual = append(actual, int(changeNumber)) 203 } 204 } 205 sort.Ints(actual) 206 sort.Ints(expectedChanges) 207 So(actual, ShouldResemble, expectedChanges) 208 } 209 210 // Most of the code doesn't care if CL exists, so for simplicity we test it 211 // with non yet existing CL input. 212 newCL := changelist.CL{ExternalID: externalID} 213 214 Convey("happy path: fetches CL which has no deps and produces correct Snapshot", func() { 215 expUpdateTime := ct.Clock.Now().Add(-time.Minute) 216 ci := gf.CI( 217 gChange, 218 gf.Project(gRepo), 219 gf.Ref("refs/heads/main"), 220 gf.PS(2), 221 gf.Updated(expUpdateTime), 222 gf.Desc("Title\n\nMeta: data\nNo-Try: true\nChange-Id: Ideadbeef"), 223 gf.Files("z.cpp", "dir/a.py"), 224 ) 225 ct.GFake.AddFrom(gf.WithCIs(gHost, gf.ACLPublic(), ci)) 226 227 Convey("works if parent commits are empty", func() { 228 ct.GFake.MutateChange(gHost, gChange, func(c *gf.Change) { 229 gf.ParentCommits(nil)(c.Info) 230 }) 231 }) 232 233 res, err := gu.Fetch(ctx, changelist.NewFetchInput(&newCL, task)) 234 So(err, ShouldBeNil) 235 236 So(res.AddDependentMeta, ShouldBeNil) 237 So(res.ApplicableConfig, ShouldResembleProto, &changelist.ApplicableConfig{ 238 Projects: []*changelist.ApplicableConfig_Project{ 239 { 240 Name: lProject, 241 ConfigGroupIds: []string{ 242 string(prjcfgtest.MustExist(ctx, lProject).ConfigGroupIDs[0]), 243 }, 244 }, 245 }, 246 }) 247 // Verify Snapshot in two separate steps for easier debugging: high level 248 // fields and the noisy Gerrit Change Info. 249 // But first, backup Snapshot for later use. 250 So(res.Snapshot, ShouldNotBeNil) 251 backedupSnapshot := proto.Clone(res.Snapshot).(*changelist.Snapshot) 252 // save Gerrit CI portion for later check and check high-level fields first. 253 ci = res.Snapshot.GetGerrit().GetInfo() 254 res.Snapshot.GetGerrit().Info = nil 255 So(res.Snapshot, ShouldResembleProto, &changelist.Snapshot{ 256 Deps: nil, 257 ExternalUpdateTime: timestamppb.New(expUpdateTime), 258 LuciProject: lProject, 259 MinEquivalentPatchset: 2, 260 Patchset: 2, 261 Metadata: []*changelist.StringPair{ 262 {Key: "Change-Id", Value: "Ideadbeef"}, 263 {Key: "Meta", Value: "data"}, 264 {Key: "No-Try", Value: "true"}, 265 }, 266 Kind: &changelist.Snapshot_Gerrit{Gerrit: &changelist.Gerrit{ 267 Host: gHost, 268 Files: []string{"dir/a.py", "z.cpp"}, 269 }}, 270 }) 271 expectedCI := ct.GFake.GetChange(gHost, gChange).Info 272 changelist.RemoveUnusedGerritInfo(expectedCI) 273 So(ci, ShouldResembleProto, expectedCI) 274 275 Convey("may re-uses files & related changes of the existing Snapshot", func() { 276 // Simulate previously saved CL. 277 existingCL := changelist.CL{ 278 ID: 123123213, 279 ExternalID: newCL.ExternalID, 280 ApplicableConfig: res.ApplicableConfig, 281 Snapshot: backedupSnapshot, 282 } 283 ct.Clock.Add(time.Minute) 284 285 Convey("possible", func() { 286 // Simulate a new message posted to the Gerrit CL. 287 ct.GFake.MutateChange(gHost, gChange, func(c *gf.Change) { 288 c.Info.Messages = append(c.Info.Messages, &gerritpb.ChangeMessageInfo{ 289 Message: "this message is ignored by CV", 290 }) 291 c.Info.Updated = timestamppb.New(ct.Clock.Now()) 292 }) 293 294 res2, err := gu.Fetch(ctx, changelist.NewFetchInput(&existingCL, task)) 295 So(err, ShouldBeNil) 296 // Only the ChangeInfo & ExternalUpdateTime must change. 297 expectedCI := ct.GFake.GetChange(gHost, gChange).Info 298 changelist.RemoveUnusedGerritInfo(expectedCI) 299 So(res2.Snapshot.GetGerrit().GetInfo(), ShouldResembleProto, expectedCI) 300 // NOTE: the prior result Snapshot already has nil ChangeInfo due to 301 // the assertions done above. Now modify it to match what we expect to 302 // get in res2. 303 res.Snapshot.ExternalUpdateTime = timestamppb.New(ct.Clock.Now()) 304 res2.Snapshot.GetGerrit().Info = nil 305 So(res2.Snapshot, ShouldResembleProto, res.Snapshot) 306 }) 307 308 Convey("not possible", func() { 309 // Simulate a new revision with new file set. 310 ct.GFake.MutateChange(gHost, gChange, func(c *gf.Change) { 311 gf.PS(4)(c.Info) 312 gf.Files("new.file")(c.Info) 313 gf.Desc("See footer.\n\nCq-Depend: 444")(c.Info) 314 gf.Updated(ct.Clock.Now())(c.Info) 315 }) 316 317 res2, err := gu.Fetch(ctx, changelist.NewFetchInput(&existingCL, task)) 318 So(err, ShouldBeNil) 319 So(res2.Snapshot.GetGerrit().GetFiles(), ShouldResemble, []string{"new.file"}) 320 So(res2.Snapshot.GetGerrit().GetSoftDeps(), ShouldResemble, []*changelist.GerritSoftDep{{Change: 444, Host: gHost}}) 321 }) 322 }) 323 }) 324 325 Convey("happy path: fetches CL with deps", func() { 326 // This test focuses on deps only. The rest is tested by the no deps case. 327 328 // Simulate a CL with 2 deps via Cq-Depend -- 444 on internal host and 55, 329 // and with 2 parents -- 54, 55 (yes, 55 can be both). 330 ci := gf.CI( 331 gChange, gf.Project(gRepo), gf.Ref("refs/heads/main"), gf.PS(2), 332 gf.Desc("Title\n\nCq-Depend: 55,internal:444"), 333 gf.Files("a.cpp"), 334 ) 335 ct.GFake.AddFrom(gf.WithCIs(gHost, gf.ACLPublic(), 336 ci, // this CL 337 gf.CI(55, gf.Project(gRepo), gf.Ref("refs/heads/main"), gf.PS(1)), 338 gf.CI(54, gf.Project(gRepo), gf.Ref("refs/heads/main"), gf.PS(3)), 339 )) 340 // Make this CL depend on 55 ps#1, which in turn depends on 54 ps#3. 341 ct.GFake.SetDependsOn(gHost, ci, "55_1") 342 ct.GFake.SetDependsOn(gHost, "55_1", "54_3") 343 344 res, err := gu.Fetch(ctx, changelist.NewFetchInput(&newCL, task)) 345 So(err, ShouldBeNil) 346 347 So(res.Snapshot.GetGerrit().GetGitDeps(), ShouldResembleProto, []*changelist.GerritGitDep{ 348 {Change: 55, Immediate: true}, 349 {Change: 54, Immediate: false}, 350 }) 351 So(res.Snapshot.GetGerrit().GetSoftDeps(), ShouldResembleProto, []*changelist.GerritSoftDep{ 352 {Change: 55, Host: gHost}, 353 {Change: 444, Host: gHostInternal}, 354 }) 355 // The Snapshot.Deps must use internal CV CL IDs. 356 cl54 := changelist.MustGobID(gHost, 54).MustCreateIfNotExists(ctx) 357 cl55 := changelist.MustGobID(gHost, 55).MustCreateIfNotExists(ctx) 358 cl444 := changelist.MustGobID(gHostInternal, 444).MustCreateIfNotExists(ctx) 359 expected := []*changelist.Dep{ 360 {Clid: int64(cl54.ID), Kind: changelist.DepKind_HARD}, 361 {Clid: int64(cl55.ID), Kind: changelist.DepKind_HARD}, 362 {Clid: int64(cl444.ID), Kind: changelist.DepKind_SOFT}, 363 } 364 sort.Slice(expected, func(i, j int) bool { return expected[i].GetClid() < expected[j].GetClid() }) 365 So(res.Snapshot.GetDeps(), ShouldResembleProto, expected) 366 }) 367 368 Convey("happy path: fetches CL in ignorable state", func() { 369 for _, s := range []gerritpb.ChangeStatus{gerritpb.ChangeStatus_ABANDONED, gerritpb.ChangeStatus_MERGED} { 370 s := s 371 Convey(s.String(), func() { 372 ct.GFake.AddFrom(gf.WithCIs(gHost, gf.ACLPublic(), 373 gf.CI(gChange, gf.Project(gRepo), gf.Ref("refs/heads/main"), gf.Status(s), 374 gf.Desc("All deps and files are ignored for such CL.\n\nCq-Depend: 44"), 375 ))) 376 res, err := gu.Fetch(ctx, changelist.NewFetchInput(&newCL, task)) 377 So(err, ShouldBeNil) 378 So(res.Snapshot.GetGerrit().GetInfo().GetStatus(), ShouldResemble, s) 379 So(res.Snapshot.GetGerrit().GetFiles(), ShouldBeNil) 380 So(res.Snapshot.GetDeps(), ShouldBeNil) 381 382 }) 383 } 384 Convey("regression: NEW -> ABANDON -> NEW transitions don't lose file list", func() { 385 ct.GFake.AddFrom(gf.WithCIs(gHost, gf.ACLPublic(), gf.CI( 386 gChange, gf.Project(gRepo), gf.Ref("refs/heads/main"), gf.Files("a.txt"), 387 gf.Status(gerritpb.ChangeStatus_NEW), 388 ))) 389 res, err := gu.Fetch(ctx, changelist.NewFetchInput(&newCL, task)) 390 So(err, ShouldBeNil) 391 So(res.Snapshot.GetGerrit().GetFiles(), ShouldResemble, []string{"a.txt"}) 392 393 savedCL := changelist.CL{ 394 ID: 123123213, 395 ExternalID: newCL.ExternalID, 396 Snapshot: res.Snapshot, 397 ApplicableConfig: res.ApplicableConfig, 398 } 399 ct.Clock.Add(time.Minute) 400 ct.GFake.MutateChange(gHost, gChange, func(c *gf.Change) { 401 gf.Status(gerritpb.ChangeStatus_ABANDONED)(c.Info) 402 gf.Updated(ct.Clock.Now())(c.Info) 403 }) 404 res, err = gu.Fetch(ctx, changelist.NewFetchInput(&newCL, task)) 405 So(err, ShouldBeNil) 406 // CV doesn't care about files of ABANDONED CLs. 407 So(res.Snapshot.GetGerrit().GetFiles(), ShouldBeEmpty) 408 409 // Back to NEW => files must be restored. 410 savedCL.Snapshot = res.Snapshot 411 ct.Clock.Add(time.Minute) 412 ct.GFake.MutateChange(gHost, gChange, func(c *gf.Change) { 413 gf.Status(gerritpb.ChangeStatus_NEW)(c.Info) 414 gf.Updated(ct.Clock.Now())(c.Info) 415 }) 416 res, err = gu.Fetch(ctx, changelist.NewFetchInput(&newCL, task)) 417 So(err, ShouldBeNil) 418 So(res.Snapshot.GetGerrit().GetFiles(), ShouldResemble, []string{"a.txt"}) 419 }) 420 }) 421 422 Convey("stale data", func() { 423 staleUpdateTime := ct.Clock.Now().Add(-time.Hour) 424 ct.GFake.AddFrom(gf.WithCIs(gHost, gf.ACLPublic(), gf.CI( 425 gChange, gf.Project(gRepo), gf.Ref("refs/heads/main"), 426 gf.Updated(staleUpdateTime), 427 ))) 428 429 Convey("CL's updated timestamp is before updatedHint", func() { 430 task.Hint.ExternalUpdateTime = timestamppb.New(staleUpdateTime.Add(time.Minute)) 431 _, err := gu.Fetch(ctx, changelist.NewFetchInput(&newCL, task)) 432 So(err, ShouldErrLike, gerrit.ErrStaleData) 433 }) 434 435 Convey("CL's existing Snapshot is more recent", func() { 436 existingCL := changelist.CL{ 437 ID: 1231231232, 438 ExternalID: newCL.ExternalID, 439 Snapshot: &changelist.Snapshot{ 440 ExternalUpdateTime: timestamppb.New(staleUpdateTime.Add(time.Minute)), 441 }, 442 } 443 444 _, err := gu.Fetch(ctx, changelist.NewFetchInput(&existingCL, task)) 445 So(err, ShouldErrLike, gerrit.ErrStaleData) 446 447 Convey("if MetaRevId was set, skip updating Snapshot", func() { 448 task.Hint.MetaRevId = "deadbeef" 449 res, err := gu.Fetch(ctx, changelist.NewFetchInput(&existingCL, task)) 450 // The Fetch() should succeed with nil in toUpdate.Snapshot. 451 So(err, ShouldBeNil) 452 So(res.Snapshot, ShouldBeNil) 453 }) 454 }) 455 }) 456 457 Convey("Uncertain lack of access", func() { 458 // Simulate Gerrit responding with 404. 459 gfResponse := status.New(codes.NotFound, "not found or no access") 460 gfACLmock := func(_ gf.Operation, luciProject string) *status.Status { 461 return gfResponse 462 } 463 ct.GFake.AddFrom(gf.WithCIs(gHost, gfACLmock, gf.CI(gChange, gf.Ref("refs/heads/main"), gf.Project(gRepo)))) 464 465 // First time 404 isn't certain. 466 res, err := gu.Fetch(ctx, changelist.NewFetchInput(&newCL, task)) 467 So(err, ShouldBeNil) 468 So(res.Snapshot, ShouldBeNil) 469 So(res.ApplicableConfig, ShouldBeNil) 470 So(res.AddDependentMeta.GetByProject()[lProject], ShouldResembleProto, &changelist.Access_Project{ 471 NoAccess: true, 472 NoAccessTime: timestamppb.New(ct.Clock.Now().Add(noAccessGraceDuration)), 473 UpdateTime: timestamppb.New(ct.Clock.Now()), 474 }) 475 assertUpdateCLScheduledFor(gChange) 476 477 // Simulate intermediate save to Datastore. 478 cl := changelist.CL{ID: 1123123, ExternalID: externalID, Access: res.AddDependentMeta} 479 // For now, access denied isn't certain. 480 So(cl.AccessKind(ctx, lProject), ShouldEqual, changelist.AccessDeniedProbably) 481 482 Convey("403 is treated same as 404, ie potentially stale", func() { 483 ct.Clock.Add(noAccessGraceRetryDelay) 484 // Because 403 can be caused due to stale ACLs on a stale mirror. 485 gfResponse = status.New(codes.PermissionDenied, "403") 486 res2, err := gu.Fetch(ctx, changelist.NewFetchInput(&cl, task)) 487 So(err, ShouldBeNil) 488 // res2 must be the same, except for the .UpdateTime. 489 So(res2.AddDependentMeta.GetByProject()[lProject].UpdateTime, ShouldResembleProto, timestamppb.New(ct.Clock.Now())) 490 res.AddDependentMeta.GetByProject()[lProject].UpdateTime = nil 491 res2.AddDependentMeta.GetByProject()[lProject].UpdateTime = nil 492 So(res2, cvtesting.SafeShouldResemble, res) 493 // And thus, lack of access is still uncertain. 494 So(cl.AccessKind(ctx, lProject), ShouldEqual, changelist.AccessDeniedProbably) 495 }) 496 497 Convey("still no access after grace duration", func() { 498 ct.Clock.Add(noAccessGraceDuration + time.Second) 499 res2, err := gu.Fetch(ctx, changelist.NewFetchInput(&cl, task)) 500 So(err, ShouldBeNil) 501 // res2 must be the same, except for the .UpdateTime. 502 So(res2.AddDependentMeta.GetByProject()[lProject].UpdateTime, ShouldResembleProto, timestamppb.New(ct.Clock.Now())) 503 res.AddDependentMeta.GetByProject()[lProject].UpdateTime = nil 504 res2.AddDependentMeta.GetByProject()[lProject].UpdateTime = nil 505 So(res2, cvtesting.SafeShouldResemble, res) 506 // Nothing new should be scheduled (on top of the existing task). 507 assertUpdateCLScheduledFor(gChange) 508 // Finally, certainty is reached. 509 So(cl.AccessKind(ctx, lProject), ShouldEqual, changelist.AccessDenied) 510 }) 511 512 Convey("access not found is forgotten on a successful fetch", func() { 513 // At a later time, the CL "magically" appears, e.g. if ACLs are fixed. 514 gfResponse = status.New(codes.OK, "OK") 515 ct.Clock.Add(time.Minute) 516 res2, err := gu.Fetch(ctx, changelist.NewFetchInput(&newCL, task)) 517 So(err, ShouldBeNil) 518 // The previous record of lack of Access must be expunged. 519 So(res2.AddDependentMeta, ShouldBeNil) 520 So(res2.DelAccess, ShouldResemble, []string{lProject}) 521 // Exact value of Snapshot and ApplicableConfig is tested in happy path, 522 // here we only care that both are set. 523 So(res2.Snapshot, ShouldNotBeNil) 524 So(res2.ApplicableConfig, ShouldNotBeNil) 525 526 Convey("can lose access again, which should not erase the snapshot saved before", func() { 527 // Simulate saved CL. 528 cl.Snapshot = res.Snapshot 529 cl.ApplicableConfig = res.ApplicableConfig 530 531 gfResponse = status.New(codes.NotFound, "not found, again") 532 ct.Clock.Add(time.Minute) 533 res3, err := gu.Fetch(ctx, changelist.NewFetchInput(&newCL, task)) 534 So(err, ShouldBeNil) 535 536 So(res3.Snapshot, ShouldBeNil) // nothing to update 537 So(res3.ApplicableConfig, ShouldBeNil) // nothing to update 538 So(res3.AddDependentMeta.GetByProject()[lProject], ShouldResembleProto, &changelist.Access_Project{ 539 NoAccess: true, 540 NoAccessTime: timestamppb.New(ct.Clock.Now().Add(noAccessGraceDuration)), 541 UpdateTime: timestamppb.New(ct.Clock.Now()), 542 }) 543 }) 544 }) 545 }) 546 547 Convey("Not watched CL", func() { 548 Convey("Gerrit host is not watched", func() { 549 bogusCL := changelist.CL{ExternalID: changelist.MustGobID("404.example.com", 404)} 550 res, err := gu.Fetch(ctx, changelist.NewFetchInput(&bogusCL, task)) 551 So(err, ShouldBeNil) 552 So(res.Snapshot, ShouldBeNil) 553 So(res.ApplicableConfig, ShouldBeNil) 554 So(res.AddDependentMeta.GetByProject()[lProject], ShouldResembleProto, &changelist.Access_Project{ 555 NoAccess: true, 556 NoAccessTime: timestamppb.New(ct.Clock.Now()), // immediate no access. 557 UpdateTime: timestamppb.New(ct.Clock.Now()), 558 }) 559 bogusCL.Access = res.AddDependentMeta 560 So(bogusCL.AccessKind(ctx, lProject), ShouldEqual, changelist.AccessDenied) 561 }) 562 Convey("Only the ref isn't watched", func() { 563 ct.GFake.AddFrom(gf.WithCIs(gHost, gf.ACLPublic(), gf.CI(gChange, gf.Ref("refs/un/watched"), gf.Project(gRepo)))) 564 res, err := gu.Fetch(ctx, changelist.NewFetchInput(&newCL, task)) 565 So(err, ShouldBeNil) 566 // Although technically, LUCI project currently has access, 567 // we mark it as lacking access from CV's PoV. 568 // TODO(tandrii): this is weird, and ought to be refactored together 569 // with weird "AddDependentMeta" field. 570 So(res.AddDependentMeta.GetByProject()[lProject], ShouldNotBeNil) 571 So(res.ApplicableConfig, ShouldResembleProto, &changelist.ApplicableConfig{ 572 // No watching projects. 573 }) 574 }) 575 }) 576 577 Convey("Gerrit errors are propagated", func() { 578 Convey("GetChange fails", func() { 579 fakeResponseStatus := func(_ gf.Operation, _ string) *status.Status { 580 return status.New(codes.ResourceExhausted, "doesn't matter") 581 } 582 ct.GFake.AddFrom(gf.WithCIs(gHost, fakeResponseStatus, gf.CI(gChange, gf.Ref("refs/heads/main"), gf.Project(gRepo)))) 583 _, err := gu.Fetch(ctx, changelist.NewFetchInput(&newCL, task)) 584 So(err, ShouldErrLike, gerrit.ErrOutOfQuota) 585 }) 586 Convey("ListFiles or GetRelatedChanges fails", func() { 587 // ListFiles and GetRelatedChanges are done in parallel but after the 588 // GetChange call. 589 cnt := int32(0) 590 fakeResponseStatus := func(_ gf.Operation, _ string) *status.Status { 591 if atomic.AddInt32(&cnt, 1) == 2 { 592 return status.New(codes.Unavailable, "2nd call failed") 593 } 594 return status.New(codes.OK, "ok") 595 } 596 ct.GFake.AddFrom(gf.WithCIs(gHost, fakeResponseStatus, gf.CI(gChange, gf.Ref("refs/heads/main"), gf.Project(gRepo)))) 597 _, err := gu.Fetch(ctx, changelist.NewFetchInput(&newCL, task)) 598 So(err, ShouldErrLike, "2nd call failed") 599 }) 600 }) 601 602 Convey("MetaRevID", func() { 603 expUpdateTime := ct.Clock.Now().Add(-time.Minute) 604 ci := gf.CI( 605 gChange, 606 gf.Project(gRepo), 607 gf.Ref("refs/heads/main"), 608 gf.PS(2), 609 gf.Updated(expUpdateTime), 610 gf.MetaRevID("deadbeef"), 611 ) 612 ct.GFake.AddFrom(gf.WithCIs(gHost, gf.ACLPublic(), ci)) 613 task.Hint.MetaRevId = "deadbeef" 614 }) 615 }) 616 } 617 618 func singleRepoConfig(gHost string, gRepos ...string) *cfgpb.Config { 619 projects := make([]*cfgpb.ConfigGroup_Gerrit_Project, len(gRepos)) 620 for i, gRepo := range gRepos { 621 projects[i] = &cfgpb.ConfigGroup_Gerrit_Project{ 622 Name: gRepo, 623 RefRegexp: []string{"refs/heads/main"}, 624 } 625 } 626 return &cfgpb.Config{ 627 ConfigGroups: []*cfgpb.ConfigGroup{ 628 { 629 Name: "main", 630 Gerrit: []*cfgpb.ConfigGroup_Gerrit{ 631 { 632 Url: "https://" + gHost + "/", 633 Projects: projects, 634 }, 635 }, 636 }, 637 }, 638 } 639 } 640 641 type pmMock struct { 642 } 643 644 func (*pmMock) NotifyCLsUpdated(ctx context.Context, project string, cls *changelist.CLUpdatedEvents) error { 645 return nil 646 } 647 648 type rmMock struct { 649 } 650 651 func (*rmMock) NotifyCLsUpdated(ctx context.Context, rid common.RunID, cls *changelist.CLUpdatedEvents) error { 652 return nil 653 } 654 655 type tjMock struct{} 656 657 func (t *tjMock) ScheduleCancelStale(ctx context.Context, clid common.CLID, prevMinEquivalentPatchset, currentMinEquivalentPatchset int32, eta time.Time) error { 658 return nil 659 }