go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/internal/buildsource/buildbucket/build_sync_test.go (about) 1 // Copyright 2017 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 buildbucket 16 17 import ( 18 "bytes" 19 "context" 20 "encoding/base64" 21 "encoding/json" 22 "fmt" 23 "io" 24 "net/http" 25 "net/http/httptest" 26 "testing" 27 "time" 28 29 "github.com/alicebob/miniredis/v2" 30 "github.com/golang/mock/gomock" 31 "github.com/gomodule/redigo/redis" 32 "google.golang.org/protobuf/encoding/protojson" 33 "google.golang.org/protobuf/proto" 34 "google.golang.org/protobuf/types/known/structpb" 35 "google.golang.org/protobuf/types/known/timestamppb" 36 37 "go.chromium.org/luci/appengine/gaetesting" 38 "go.chromium.org/luci/auth/identity" 39 buildbucketpb "go.chromium.org/luci/buildbucket/proto" 40 "go.chromium.org/luci/common/clock" 41 "go.chromium.org/luci/common/clock/testclock" 42 "go.chromium.org/luci/common/sync/parallel" 43 "go.chromium.org/luci/gae/impl/memory" 44 "go.chromium.org/luci/gae/service/datastore" 45 "go.chromium.org/luci/server/auth" 46 "go.chromium.org/luci/server/auth/authtest" 47 "go.chromium.org/luci/server/caching" 48 "go.chromium.org/luci/server/redisconn" 49 "go.chromium.org/luci/server/router" 50 51 "go.chromium.org/luci/milo/internal/model" 52 "go.chromium.org/luci/milo/internal/model/milostatus" 53 "go.chromium.org/luci/milo/internal/projectconfig" 54 "go.chromium.org/luci/milo/internal/utils" 55 56 . "github.com/smartystreets/goconvey/convey" 57 . "go.chromium.org/luci/common/testing/assertions" 58 ) 59 60 func newMockClient(c context.Context, t *testing.T) (context.Context, *gomock.Controller, *buildbucketpb.MockBuildsClient) { 61 ctrl := gomock.NewController(t) 62 client := buildbucketpb.NewMockBuildsClient(ctrl) 63 factory := func(c context.Context, host string, as auth.RPCAuthorityKind, opts ...auth.RPCOption) (buildbucketpb.BuildsClient, error) { 64 return client, nil 65 } 66 return WithBuildsClientFactory(c, factory), ctrl, client 67 } 68 69 // Buildbucket timestamps round off to milliseconds, so define a reference. 70 var RefTime = time.Date(2016, time.February, 3, 4, 5, 6, 0, time.UTC) 71 72 func makeReq(build *buildbucketpb.Build) io.ReadCloser { 73 bmsg := &buildbucketpb.BuildsV2PubSub{Build: build} 74 bm, _ := protojson.Marshal(bmsg) 75 76 msg := utils.PubSubSubscription{ 77 Message: utils.PubSubMessage{ 78 Data: base64.StdEncoding.EncodeToString(bm), 79 }, 80 } 81 jmsg, _ := json.Marshal(msg) 82 return io.NopCloser(bytes.NewReader(jmsg)) 83 } 84 85 func TestV2PubSub(t *testing.T) { 86 t.Parallel() 87 88 Convey(`TestV2PubSub`, t, func() { 89 c := gaetesting.TestingContextWithAppID("luci-milo-dev") 90 datastore.GetTestable(c).Consistent(true) 91 c, _ = testclock.UseTime(c, RefTime) 92 c = auth.WithState(c, &authtest.FakeState{ 93 Identity: identity.AnonymousIdentity, 94 IdentityGroups: []string{"all"}, 95 }) 96 c = caching.WithRequestCache(c) 97 98 // Initialize the appropriate builder. 99 builderSummary := &model.BuilderSummary{ 100 BuilderID: "buildbucket/luci.fake.bucket/fake_builder", 101 } 102 err := datastore.Put(c, builderSummary) 103 So(err, ShouldBeNil) 104 105 // Initialize the appropriate project config. 106 err = datastore.Put(c, &projectconfig.Project{ 107 ID: "fake", 108 }) 109 So(err, ShouldBeNil) 110 111 // We'll copy this LegacyApiCommonBuildMessage base for convenience. 112 buildBase := &buildbucketpb.Build{ 113 Builder: &buildbucketpb.BuilderID{ 114 Project: "fake", 115 Bucket: "bucket", 116 Builder: "fake_builder", 117 }, 118 Infra: &buildbucketpb.BuildInfra{ 119 Buildbucket: &buildbucketpb.BuildInfra_Buildbucket{ 120 Hostname: "hostname", 121 }, 122 }, 123 Input: &buildbucketpb.Build_Input{}, 124 Output: &buildbucketpb.Build_Output{}, 125 CreatedBy: string(identity.AnonymousIdentity), 126 CreateTime: timestamppb.New(RefTime.Add(2 * time.Hour)), 127 } 128 129 Convey("New in-process build", func() { 130 bKey := model.MakeBuildKey(c, "hostname", "1234") 131 buildExp := proto.Clone(buildBase).(*buildbucketpb.Build) 132 buildExp.Id = 1234 133 buildExp.Status = buildbucketpb.Status_STARTED 134 buildExp.CreateTime = timestamppb.New(RefTime.Add(2 * time.Hour)) 135 buildExp.StartTime = timestamppb.New(RefTime.Add(3 * time.Hour)) 136 buildExp.UpdateTime = timestamppb.New(RefTime.Add(5 * time.Hour)) 137 buildExp.Input.Experimental = true 138 propertiesMap := map[string]any{ 139 "$recipe_engine/milo/blamelist_pins": []any{ 140 map[string]any{ 141 "host": "chromium.googlesource.com", 142 "id": "8930f18245df678abc944376372c77ba5e2a658b", 143 "project": "angle/angle", 144 }, 145 map[string]any{ 146 "host": "chromium.googlesource.com", 147 "id": "07033c702f81a75dfc2d83888ba3f8b354d0e920", 148 "project": "chromium/src", 149 }, 150 }, 151 } 152 buildExp.Output.Properties, _ = structpb.NewStruct(propertiesMap) 153 154 h := httptest.NewRecorder() 155 r := &http.Request{Body: makeReq(buildExp)} 156 V2PubSubHandler(&router.Context{ 157 Writer: h, 158 Request: r.WithContext(c), 159 }) 160 So(h.Code, ShouldEqual, 200) 161 datastore.GetTestable(c).CatchupIndexes() 162 163 Convey("stores BuildSummary and BuilderSummary", func() { 164 buildAct := model.BuildSummary{BuildKey: bKey} 165 err := datastore.Get(c, &buildAct) 166 So(err, ShouldBeNil) 167 So(buildAct.BuildKey.String(), ShouldEqual, bKey.String()) 168 So(buildAct.BuilderID, ShouldEqual, "buildbucket/luci.fake.bucket/fake_builder") 169 So(buildAct.Summary, ShouldResemble, model.Summary{ 170 Status: milostatus.Running, 171 Start: RefTime.Add(3 * time.Hour), 172 }) 173 So(buildAct.Created, ShouldResemble, RefTime.Add(2*time.Hour)) 174 So(buildAct.Experimental, ShouldBeTrue) 175 So(buildAct.BlamelistPins, ShouldResemble, []string{ 176 "commit/gitiles/chromium.googlesource.com/angle/angle/+/8930f18245df678abc944376372c77ba5e2a658b", 177 "commit/gitiles/chromium.googlesource.com/chromium/src/+/07033c702f81a75dfc2d83888ba3f8b354d0e920", 178 }) 179 180 blder := model.BuilderSummary{BuilderID: "buildbucket/luci.fake.bucket/fake_builder"} 181 err = datastore.Get(c, &blder) 182 So(err, ShouldBeNil) 183 So(blder.LastFinishedStatus, ShouldResemble, milostatus.NotRun) 184 So(blder.LastFinishedBuildID, ShouldEqual, "") 185 }) 186 }) 187 188 Convey("Completed build", func() { 189 bKey := model.MakeBuildKey(c, "hostname", "2234") 190 buildExp := buildBase 191 buildExp.Id = 2234 192 buildExp.Status = buildbucketpb.Status_SUCCESS 193 buildExp.CreateTime = timestamppb.New(RefTime.Add(2 * time.Hour)) 194 buildExp.StartTime = timestamppb.New(RefTime.Add(3 * time.Hour)) 195 buildExp.UpdateTime = timestamppb.New(RefTime.Add(6 * time.Hour)) 196 buildExp.EndTime = timestamppb.New(RefTime.Add(6 * time.Hour)) 197 buildExp.Input.GitilesCommit = &buildbucketpb.GitilesCommit{ 198 Host: "chromium.googlesource.com", 199 Id: "8930f18245df678abc944376372c77ba5e2a658b", 200 Project: "angle/angle", 201 } 202 203 h := httptest.NewRecorder() 204 r := &http.Request{Body: makeReq(buildExp)} 205 V2PubSubHandler(&router.Context{ 206 Writer: h, 207 Request: r.WithContext(c), 208 }) 209 So(h.Code, ShouldEqual, 200) 210 211 Convey("stores BuildSummary and BuilderSummary", func() { 212 buildAct := model.BuildSummary{BuildKey: bKey} 213 err := datastore.Get(c, &buildAct) 214 So(err, ShouldBeNil) 215 So(buildAct.BuildKey.String(), ShouldEqual, bKey.String()) 216 So(buildAct.BuilderID, ShouldEqual, "buildbucket/luci.fake.bucket/fake_builder") 217 So(buildAct.Summary, ShouldResemble, model.Summary{ 218 Status: milostatus.Success, 219 Start: RefTime.Add(3 * time.Hour), 220 End: RefTime.Add(6 * time.Hour), 221 }) 222 So(buildAct.Created, ShouldResemble, RefTime.Add(2*time.Hour)) 223 224 blder := model.BuilderSummary{BuilderID: "buildbucket/luci.fake.bucket/fake_builder"} 225 err = datastore.Get(c, &blder) 226 So(err, ShouldBeNil) 227 So(blder.LastFinishedCreated, ShouldResemble, RefTime.Add(2*time.Hour)) 228 So(blder.LastFinishedStatus, ShouldResemble, milostatus.Success) 229 So(blder.LastFinishedBuildID, ShouldEqual, "buildbucket/2234") 230 So(buildAct.BlamelistPins, ShouldResemble, []string{ 231 "commit/gitiles/chromium.googlesource.com/angle/angle/+/8930f18245df678abc944376372c77ba5e2a658b", 232 }) 233 }) 234 235 Convey("results in earlier update not being ingested", func() { 236 eBuild := &buildbucketpb.Build{ 237 Id: 2234, 238 Builder: &buildbucketpb.BuilderID{ 239 Project: "fake", 240 Bucket: "bucket", 241 Builder: "fake_builder", 242 }, 243 Infra: &buildbucketpb.BuildInfra{ 244 Buildbucket: &buildbucketpb.BuildInfra_Buildbucket{ 245 Hostname: "hostname", 246 }, 247 }, 248 CreatedBy: string(identity.AnonymousIdentity), 249 CreateTime: timestamppb.New(RefTime.Add(2 * time.Hour)), 250 StartTime: timestamppb.New(RefTime.Add(3 * time.Hour)), 251 UpdateTime: timestamppb.New(RefTime.Add(4 * time.Hour)), 252 Status: buildbucketpb.Status_STARTED, 253 } 254 255 h := httptest.NewRecorder() 256 r := &http.Request{Body: makeReq(eBuild)} 257 V2PubSubHandler(&router.Context{ 258 Writer: h, 259 Request: r.WithContext(c), 260 }) 261 So(h.Code, ShouldEqual, 200) 262 263 buildAct := model.BuildSummary{BuildKey: bKey} 264 err := datastore.Get(c, &buildAct) 265 So(err, ShouldBeNil) 266 So(buildAct.Summary, ShouldResemble, model.Summary{ 267 Status: milostatus.Success, 268 Start: RefTime.Add(3 * time.Hour), 269 End: RefTime.Add(6 * time.Hour), 270 }) 271 So(buildAct.Created, ShouldResemble, RefTime.Add(2*time.Hour)) 272 273 blder := model.BuilderSummary{BuilderID: "buildbucket/luci.fake.bucket/fake_builder"} 274 err = datastore.Get(c, &blder) 275 So(err, ShouldBeNil) 276 So(blder.LastFinishedCreated, ShouldResemble, RefTime.Add(2*time.Hour)) 277 So(blder.LastFinishedStatus, ShouldResemble, milostatus.Success) 278 So(blder.LastFinishedBuildID, ShouldEqual, "buildbucket/2234") 279 }) 280 }) 281 }) 282 } 283 284 func TestShouldUpdateBuilderSummary(t *testing.T) { 285 Convey("TestShouldUpdateBuilderSummary", t, func() { 286 c := context.Background() 287 startTime := time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC). 288 Truncate(time.Duration(entityUpdateIntervalInS) * time.Second) 289 290 // Set up a test redis server. 291 s, err := miniredis.Run() 292 So(err, ShouldBeNil) 293 defer s.Close() 294 c = redisconn.UsePool(c, &redis.Pool{ 295 Dial: func() (redis.Conn, error) { 296 return redis.Dial("tcp", s.Addr()) 297 }, 298 }) 299 300 createBuildSummary := func(builderID string, status buildbucketpb.Status, createdAt time.Time) *model.BuildSummary { 301 return &model.BuildSummary{ 302 BuilderID: builderID, 303 Summary: model.Summary{ 304 Status: milostatus.FromBuildbucket(status), 305 }, 306 Created: createdAt, 307 } 308 } 309 310 Convey("Single call", func() { 311 // Ensures `shouldUpdateBuilderSummary` is called at the start of the time bucket. 312 c, _ := testclock.UseTime(c, startTime) 313 314 start := clock.Now(c) 315 // Should return without advancing the clock. 316 shouldUpdate, err := shouldUpdateBuilderSummary(c, createBuildSummary("test-builder-id-1", buildbucketpb.Status_SUCCESS, start)) 317 So(err, ShouldBeNil) 318 So(shouldUpdate, ShouldBeTrue) 319 }) 320 321 Convey("Single call followed by multiple parallel calls", func(tc C) { 322 // Ensures all `shouldUpdateBuilderSummary` calls are in the same time bucket. 323 c, tClock := testclock.UseTime(c, startTime) 324 325 pivot := clock.Now(c).Add(-time.Hour) 326 327 shouldUpdates := make([]bool, 4) 328 329 shouldUpdate, err := shouldUpdateBuilderSummary(c, createBuildSummary("test-builder-id-2", buildbucketpb.Status_SUCCESS, pivot)) 330 So(err, ShouldBeNil) 331 shouldUpdates[0] = shouldUpdate 332 333 err = parallel.FanOutIn(func(tasks chan<- func() error) { 334 eventC := make(chan string) 335 defer close(eventC) 336 tClock.SetTimerCallback(func(d time.Duration, t clock.Timer) { 337 eventC <- "timer" 338 }) 339 340 tasks <- func() error { 341 createdAt := pivot.Add(5 * time.Millisecond) 342 shouldUpdate, err := shouldUpdateBuilderSummary(c, createBuildSummary("test-builder-id-2", buildbucketpb.Status_SUCCESS, createdAt)) 343 shouldUpdates[1] = shouldUpdate 344 return err 345 } 346 347 // Wait until the previous call reaches a blocking point. 348 tc.So(<-eventC, ShouldEqual, "timer") 349 tasks <- func() error { 350 createdAt := pivot.Add(15 * time.Millisecond) 351 shouldUpdate, err := shouldUpdateBuilderSummary(c, createBuildSummary("test-builder-id-2", buildbucketpb.Status_SUCCESS, createdAt)) 352 shouldUpdates[2] = shouldUpdate 353 return err 354 } 355 356 // Wait until the previous call reaches a blocking point. 357 tc.So(<-eventC, ShouldEqual, "timer") 358 tasks <- func() error { 359 createdAt := pivot.Add(10 * time.Millisecond) 360 shouldUpdate, err := shouldUpdateBuilderSummary(c, createBuildSummary("test-builder-id-2", buildbucketpb.Status_SUCCESS, createdAt)) 361 shouldUpdates[3] = shouldUpdate 362 eventC <- "return" 363 return err 364 } 365 366 // Wait until the last shouldUpdateBuilderSummary call returns then 367 // advance the clock to the next time bucket. 368 tc.So(<-eventC, ShouldEqual, "return") 369 tClock.Add(time.Duration(entityUpdateIntervalInS) * time.Second) 370 }) 371 So(err, ShouldBeNil) 372 373 // The first value should be true because there's no recent updates. 374 So(shouldUpdates[0], ShouldBeTrue) 375 376 // The second value should be false because there's a recent update, so it 377 // moves to the next time bucket, wait for the timebucket to begin. 378 // Then it's replaced by the next shouldUpdateBuilderSummary call. 379 So(shouldUpdates[1], ShouldBeFalse) 380 381 // The third value should be true because it has a newer build than the 382 // current one. And it's not replaced by any new builds. 383 So(shouldUpdates[2], ShouldBeTrue) 384 385 // The forth value should be false because the build is not created earlier 386 // than the build associated with the current pending update in it's pending bucket. 387 So(shouldUpdates[3], ShouldBeFalse) 388 }) 389 390 Convey("Single call followed by multiple parallel calls that are nanoseconds apart", func(tc C) { 391 // This test ensures that the timestamp percision is not lost. 392 393 // Ensures all `shouldUpdateBuilderSummary` calls are in the same time bucket. 394 c, tClock := testclock.UseTime(c, startTime) 395 396 pivot := clock.Now(c).Add(-time.Hour) 397 398 shouldUpdates := make([]bool, 4) 399 400 shouldUpdate, err := shouldUpdateBuilderSummary(c, createBuildSummary("test-builder-id-3", buildbucketpb.Status_SUCCESS, pivot)) 401 So(err, ShouldBeNil) 402 shouldUpdates[0] = shouldUpdate 403 404 err = parallel.FanOutIn(func(tasks chan<- func() error) { 405 eventC := make(chan string) 406 defer close(eventC) 407 tClock.SetTimerCallback(func(d time.Duration, t clock.Timer) { 408 eventC <- "timer" 409 }) 410 411 tasks <- func() error { 412 createdAt := pivot.Add(time.Nanosecond) 413 shouldUpdate, err := shouldUpdateBuilderSummary(c, createBuildSummary("test-builder-id-3", buildbucketpb.Status_SUCCESS, createdAt)) 414 shouldUpdates[1] = shouldUpdate 415 return err 416 } 417 418 // Wait until the previous call reaches a blocking point. 419 tc.So(<-eventC, ShouldEqual, "timer") 420 tasks <- func() error { 421 createdAt := pivot.Add(3 * time.Nanosecond) 422 shouldUpdate, err := shouldUpdateBuilderSummary(c, createBuildSummary("test-builder-id-3", buildbucketpb.Status_SUCCESS, createdAt)) 423 shouldUpdates[2] = shouldUpdate 424 return err 425 } 426 427 // Wait until the previous call reaches a blocking point. 428 tc.So(<-eventC, ShouldEqual, "timer") 429 tasks <- func() error { 430 createdAt := pivot.Add(2 * time.Nanosecond) 431 shouldUpdate, err := shouldUpdateBuilderSummary(c, createBuildSummary("test-builder-id-3", buildbucketpb.Status_SUCCESS, createdAt)) 432 shouldUpdates[3] = shouldUpdate 433 eventC <- "return" 434 return err 435 } 436 437 // Wait until the last shouldUpdateBuilderSummary call returns then 438 // advance the clock to the next time bucket. 439 tc.So(<-eventC, ShouldEqual, "return") 440 tClock.Add(time.Duration(entityUpdateIntervalInS) * time.Second) 441 }) 442 So(err, ShouldBeNil) 443 444 // The first value should be true because there's no pending/recent updates. 445 So(shouldUpdates[0], ShouldBeTrue) 446 447 // The second value should be false because there's a recent update, so it 448 // moves to the next time bucket, wait for the timebucket to begin. 449 // Then it's replaced by the next shouldUpdateBuilderSummary call. 450 So(shouldUpdates[1], ShouldBeFalse) 451 452 // The third value should be true because it has a newer build than the 453 // current pending one. And it's not replaced by any new builds. 454 So(shouldUpdates[2], ShouldBeTrue) 455 456 // The forth value should be false because the build is not created earlier 457 // than the build associated with the current pending update in it's pending bucket. 458 So(shouldUpdates[3], ShouldBeFalse) 459 }) 460 461 Convey("Single call followed by multiple parallel calls in different time buckets", func(tc C) { 462 c, tClock := testclock.UseTime(c, startTime) 463 464 pivot := clock.Now(c).Add(-time.Hour) 465 shouldUpdates := make([]bool, 4) 466 467 shouldUpdate, err := shouldUpdateBuilderSummary(c, createBuildSummary("test-builder-id-4", buildbucketpb.Status_SUCCESS, pivot)) 468 So(err, ShouldBeNil) 469 shouldUpdates[0] = shouldUpdate 470 471 // Ensures the following `shouldUpdateBuilderSummary` calls are in a 472 // different time bucket. 473 tClock.Add(time.Duration(entityUpdateIntervalInS) * time.Second) 474 475 err = parallel.FanOutIn(func(tasks chan<- func() error) { 476 eventC := make(chan string) 477 defer close(eventC) 478 tClock.SetTimerCallback(func(d time.Duration, t clock.Timer) { 479 eventC <- "timer" 480 }) 481 482 tasks <- func() error { 483 createdAt := pivot.Add(5 * time.Millisecond) 484 shouldUpdate, err := shouldUpdateBuilderSummary(c, createBuildSummary("test-builder-id-4", buildbucketpb.Status_SUCCESS, createdAt)) 485 shouldUpdates[1] = shouldUpdate 486 eventC <- "return" 487 return err 488 } 489 490 // Wait until the previous shouldUpdateBuilderSummary call returns. 491 tc.So(<-eventC, ShouldEqual, "return") 492 tasks <- func() error { 493 createdAt := pivot.Add(15 * time.Millisecond) 494 shouldUpdate, err := shouldUpdateBuilderSummary(c, createBuildSummary("test-builder-id-4", buildbucketpb.Status_SUCCESS, createdAt)) 495 shouldUpdates[2] = shouldUpdate 496 return err 497 } 498 499 // Wait until the previous call reaches a blocking point. 500 tc.So(<-eventC, ShouldEqual, "timer") 501 tasks <- func() error { 502 createdAt := pivot.Add(20 * time.Millisecond) 503 shouldUpdate, err := shouldUpdateBuilderSummary(c, createBuildSummary("test-builder-id-4", buildbucketpb.Status_SUCCESS, createdAt)) 504 shouldUpdates[3] = shouldUpdate 505 return err 506 } 507 508 // Wait until the last shouldUpdateBuilderSummary call returns then 509 // advance the clock to the next time bucket. 510 tc.So(<-eventC, ShouldEqual, "timer") 511 tClock.Add(time.Duration(entityUpdateIntervalInS) * time.Second) 512 }) 513 So(err, ShouldBeNil) 514 515 // The first value should be true because there's no pending/recent updates. 516 So(shouldUpdates[0], ShouldBeTrue) 517 518 // The second value should be true because it has move to a new time bucket 519 // and there's no pending/recent updates in that bucket. 520 So(shouldUpdates[1], ShouldBeTrue) 521 522 // The third value should be false because there's a recent update, so it 523 // moves to the next time bucket, wait for the timebucket to begin. 524 // Then it's replaced by the next shouldUpdateBuilderSummary call. 525 So(shouldUpdates[2], ShouldBeFalse) 526 527 // The forth value should be true because it has a newer build than the 528 // current pending one. And it's not replaced by any new builds. 529 So(shouldUpdates[3], ShouldBeTrue) 530 }) 531 }) 532 } 533 534 func TestDeleteOldBuilds(t *testing.T) { 535 t.Parallel() 536 537 Convey("DeleteOldBuilds", t, func() { 538 now := time.Date(2020, 01, 01, 0, 0, 0, 0, time.UTC) 539 540 ctx, _ := testclock.UseTime(memory.Use(context.Background()), now) 541 datastore.GetTestable(ctx).AutoIndex(true) 542 datastore.GetTestable(ctx).Consistent(true) 543 544 createBuild := func(id string, t time.Time) *model.BuildSummary { 545 b := &model.BuildSummary{ 546 BuildKey: model.MakeBuildKey(ctx, "host", id), 547 Created: t, 548 } 549 So(datastore.Put(ctx, b), ShouldBeNil) 550 return b 551 } 552 553 Convey("keeps builds", func() { 554 Convey("as old as BuildSummaryStorageDuration", func() { 555 build := createBuild("1", now.Add(-BuildSummaryStorageDuration)) 556 So(DeleteOldBuilds(ctx), ShouldBeNil) 557 So(datastore.Get(ctx, build), ShouldBeNil) 558 }) 559 Convey("younger than BuildSummaryStorageDuration", func() { 560 build := createBuild("2", now.Add(-BuildSummaryStorageDuration+time.Minute)) 561 So(DeleteOldBuilds(ctx), ShouldBeNil) 562 So(datastore.Get(ctx, build), ShouldBeNil) 563 }) 564 }) 565 566 Convey("deletes builds older than BuildSummaryStorageDuration", func() { 567 build := createBuild("3", now.Add(-BuildSummaryStorageDuration-time.Minute)) 568 So(DeleteOldBuilds(ctx), ShouldBeNil) 569 So(datastore.Get(ctx, build), ShouldEqual, datastore.ErrNoSuchEntity) 570 }) 571 572 Convey("removes many builds", func() { 573 bs := make([]*model.BuildSummary, 234) 574 old := now.Add(-BuildSummaryStorageDuration - time.Minute) 575 for i := range bs { 576 bs[i] = createBuild(fmt.Sprintf("4-%d", i), old) 577 } 578 So(DeleteOldBuilds(ctx), ShouldBeNil) 579 So(datastore.Get(ctx, bs), ShouldErrLike, 580 "datastore: no such entity (and 233 other errors)") 581 }) 582 }) 583 } 584 585 func TestSyncBuilds(t *testing.T) { 586 t.Parallel() 587 588 Convey("SyncBuilds", t, func() { 589 now := time.Date(2020, 01, 01, 0, 0, 0, 0, time.UTC) 590 591 c, _ := testclock.UseTime(memory.Use(context.Background()), now) 592 datastore.GetTestable(c).AutoIndex(true) 593 datastore.GetTestable(c).Consistent(true) 594 595 createBuild := func(id string, t time.Time, status milostatus.Status) *model.BuildSummary { 596 b := &model.BuildSummary{ 597 BuildKey: model.MakeBuildKey(c, "host", id), 598 BuilderID: "buildbucket/luci.proj.bucket/builder", 599 BuildID: "buildbucket/" + id, 600 Created: t, 601 Summary: model.Summary{ 602 Status: status, 603 }, 604 Version: t.UnixNano(), 605 } 606 So(datastore.Put(c, b), ShouldBeNil) 607 return b 608 } 609 610 Convey("don't update builds", func() { 611 Convey("as old as BuildSummaryStorageDuration", func() { 612 build := createBuild("luci.proj.bucket/builder/1234", now.Add(-BuildSummarySyncThreshold), milostatus.Running) 613 So(syncBuildsImpl(c), ShouldBeNil) 614 So(datastore.Get(c, build), ShouldBeNil) 615 So(build.Summary.Status, ShouldEqual, milostatus.Running) 616 }) 617 618 Convey("younger than BuildSummaryStorageDuration", func() { 619 build := createBuild("luci.proj.bucket/builder/1234", now.Add(-BuildSummarySyncThreshold+time.Minute), milostatus.NotRun) 620 So(syncBuildsImpl(c), ShouldBeNil) 621 So(datastore.Get(c, build), ShouldBeNil) 622 So(build.Summary.Status, ShouldEqual, milostatus.NotRun) 623 }) 624 }) 625 626 Convey("update builds older than BuildSummarySyncThreshold", func() { 627 build := createBuild("luci.proj.bucket/builder/1234", now.Add(-BuildSummarySyncThreshold-time.Minute), milostatus.NotRun) 628 629 c, ctrl, mbc := newMockClient(c, t) 630 defer ctrl.Finish() 631 mbc.EXPECT().GetBuild(gomock.Any(), gomock.Any()).Return(&buildbucketpb.Build{ 632 Number: 1234, 633 Builder: &buildbucketpb.BuilderID{ 634 Project: "proj", 635 Bucket: "bucket", 636 Builder: "builder", 637 }, 638 Status: buildbucketpb.Status_SUCCESS, 639 CreateTime: timestamppb.New(build.Created), 640 UpdateTime: timestamppb.New(build.Created.Add(time.Hour)), 641 }, nil).AnyTimes() 642 643 So(syncBuildsImpl(c), ShouldBeNil) 644 So(datastore.Get(c, build), ShouldBeNil) 645 So(build.Summary.Status, ShouldEqual, milostatus.Success) 646 }) 647 648 Convey("ensure BuildKey stays the same", func() { 649 build := createBuild("123456", now.Add(-BuildSummarySyncThreshold-time.Minute), milostatus.NotRun) 650 651 c, ctrl, mbc := newMockClient(c, t) 652 defer ctrl.Finish() 653 mbc.EXPECT().GetBuild(gomock.Any(), gomock.Any()).Return(&buildbucketpb.Build{ 654 Id: 123456, 655 Number: 1234, 656 Builder: &buildbucketpb.BuilderID{ 657 Project: "proj", 658 Bucket: "bucket", 659 Builder: "builder", 660 }, 661 Status: buildbucketpb.Status_SUCCESS, 662 CreateTime: timestamppb.New(build.Created), 663 UpdateTime: timestamppb.New(build.Created.Add(time.Hour)), 664 }, nil).AnyTimes() 665 666 So(syncBuildsImpl(c), ShouldBeNil) 667 So(datastore.Get(c, build), ShouldBeNil) 668 So(build.Summary.Status, ShouldEqual, milostatus.Success) 669 670 buildWithNewKey := &model.BuildSummary{ 671 BuildKey: model.MakeBuildKey(c, "host", "luci.proj.bucket/builder/1234"), 672 } 673 So(datastore.Get(c, buildWithNewKey), ShouldEqual, datastore.ErrNoSuchEntity) 674 }) 675 }) 676 }