go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/changepoints/analyze_changepoints_test.go (about) 1 // Copyright 2023 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 changepoints 16 17 import ( 18 "context" 19 "fmt" 20 "sort" 21 "testing" 22 "time" 23 24 "cloud.google.com/go/spanner" 25 "google.golang.org/protobuf/types/known/timestamppb" 26 27 "go.chromium.org/luci/gae/impl/memory" 28 rdbpb "go.chromium.org/luci/resultdb/proto/v1" 29 "go.chromium.org/luci/server/span" 30 31 "go.chromium.org/luci/analysis/internal/changepoints/bqexporter" 32 "go.chromium.org/luci/analysis/internal/changepoints/inputbuffer" 33 cpb "go.chromium.org/luci/analysis/internal/changepoints/proto" 34 tu "go.chromium.org/luci/analysis/internal/changepoints/testutil" 35 "go.chromium.org/luci/analysis/internal/changepoints/testvariantbranch" 36 "go.chromium.org/luci/analysis/internal/config" 37 controlpb "go.chromium.org/luci/analysis/internal/ingestion/control/proto" 38 spanutil "go.chromium.org/luci/analysis/internal/span" 39 "go.chromium.org/luci/analysis/internal/tasks/taskspb" 40 "go.chromium.org/luci/analysis/internal/testutil" 41 "go.chromium.org/luci/analysis/pbutil" 42 bqpb "go.chromium.org/luci/analysis/proto/bq" 43 pb "go.chromium.org/luci/analysis/proto/v1" 44 45 . "github.com/smartystreets/goconvey/convey" 46 . "go.chromium.org/luci/common/testing/assertions" 47 ) 48 49 type Invocation struct { 50 Project string 51 InvocationID string 52 IngestedInvocationID string 53 } 54 55 func TestAnalyzeChangePoint(t *testing.T) { 56 exporter, _ := fakeExporter() 57 Convey(`Can batch result`, t, func() { 58 ctx := newContext(t) 59 payload := tu.SamplePayload() 60 sourcesMap := tu.SampleSourcesMap(10) 61 // 900 test variants should result in 5 batches (1000 each, last one has 500). 62 tvs := testVariants(4500) 63 err := Analyze(ctx, tvs, payload, sourcesMap, exporter) 64 So(err, ShouldBeNil) 65 66 // Check that there are 5 checkpoints created. 67 So(countCheckPoint(ctx), ShouldEqual, 5) 68 }) 69 70 Convey(`Can skip batch`, t, func() { 71 ctx := newContext(t) 72 payload := tu.SamplePayload() 73 sourcesMap := tu.SampleSourcesMap(10) 74 tvs := testVariants(100) 75 err := analyzeSingleBatch(ctx, tvs, payload, sourcesMap, exporter) 76 So(err, ShouldBeNil) 77 So(countCheckPoint(ctx), ShouldEqual, 1) 78 79 // Analyze the batch again should not throw an error. 80 err = analyzeSingleBatch(ctx, tvs, payload, sourcesMap, exporter) 81 So(err, ShouldBeNil) 82 So(countCheckPoint(ctx), ShouldEqual, 1) 83 }) 84 85 Convey(`No commit position should skip`, t, func() { 86 ctx := newContext(t) 87 payload := tu.SamplePayload() 88 sourcesMap := map[string]*pb.Sources{ 89 "sources_id": { 90 GitilesCommit: &pb.GitilesCommit{ 91 Host: "host", 92 Project: "proj", 93 Ref: "ref", 94 }, 95 }, 96 } 97 tvs := testVariants(100) 98 err := Analyze(ctx, tvs, payload, sourcesMap, exporter) 99 So(err, ShouldBeNil) 100 So(countCheckPoint(ctx), ShouldEqual, 0) 101 So(verdictCounter.Get(ctx, "chromium", "skipped_no_commit_data"), ShouldEqual, 100) 102 }) 103 104 Convey(`Filter test variant`, t, func() { 105 ctx := newContext(t) 106 payload := &taskspb.IngestTestResults{ 107 Build: &controlpb.BuildResult{ 108 Project: "chromium", 109 }, 110 } 111 112 sourcesMap := map[string]*pb.Sources{ 113 "sources_id": { 114 GitilesCommit: &pb.GitilesCommit{ 115 Host: "host", 116 Project: "proj", 117 Ref: "ref", 118 Position: 10, 119 }, 120 }, 121 "sources_id_2": { 122 GitilesCommit: &pb.GitilesCommit{ 123 Host: "host_2", 124 Project: "proj_2", 125 Ref: "ref_2", 126 Position: 10, 127 }, 128 IsDirty: true, 129 }, 130 } 131 tvs := []*rdbpb.TestVariant{ 132 { 133 // All skip. 134 TestId: "1", 135 Results: []*rdbpb.TestResultBundle{ 136 { 137 Result: &rdbpb.TestResult{ 138 Name: "invocations/inv-1/tests/abc", 139 Status: rdbpb.TestStatus_SKIP, 140 }, 141 }, 142 }, 143 SourcesId: "sources_id", 144 }, 145 { 146 // Duplicate. 147 TestId: "2", 148 Results: []*rdbpb.TestResultBundle{ 149 { 150 Result: &rdbpb.TestResult{ 151 Name: "invocations/inv-2/tests/abc", 152 Status: rdbpb.TestStatus_PASS, 153 }, 154 }, 155 { 156 Result: &rdbpb.TestResult{ 157 Name: "invocations/inv-2/tests/abc", 158 Status: rdbpb.TestStatus_FAIL, 159 }, 160 }, 161 }, 162 SourcesId: "sources_id", 163 }, 164 { 165 // OK. 166 TestId: "3", 167 Results: []*rdbpb.TestResultBundle{ 168 { 169 Result: &rdbpb.TestResult{ 170 Name: "invocations/inv-3/tests/abc", 171 Status: rdbpb.TestStatus_PASS, 172 }, 173 }, 174 }, 175 SourcesId: "sources_id", 176 }, 177 { 178 // No source ID. 179 TestId: "4", 180 Results: []*rdbpb.TestResultBundle{ 181 { 182 Result: &rdbpb.TestResult{ 183 Name: "invocations/inv-4/tests/abc", 184 Status: rdbpb.TestStatus_PASS, 185 }, 186 }, 187 }, 188 SourcesId: "sources_id_1", 189 }, 190 { 191 // Source is dirty. 192 TestId: "5", 193 Results: []*rdbpb.TestResultBundle{ 194 { 195 Result: &rdbpb.TestResult{ 196 Name: "invocations/inv-5/tests/abc", 197 Status: rdbpb.TestStatus_PASS, 198 }, 199 }, 200 }, 201 SourcesId: "sources_id_2", 202 }, 203 } 204 duplicateMap := map[string]bool{ 205 "inv-2": true, 206 } 207 tvs, err := filterTestVariants(ctx, tvs, payload, duplicateMap, sourcesMap) 208 So(err, ShouldBeNil) 209 So(len(tvs), ShouldEqual, 1) 210 So(tvs[0].TestId, ShouldEqual, "3") 211 So(verdictCounter.Get(ctx, "chromium", "skipped_no_source"), ShouldEqual, 1) 212 So(verdictCounter.Get(ctx, "chromium", "skipped_no_commit_data"), ShouldEqual, 1) 213 So(verdictCounter.Get(ctx, "chromium", "skipped_all_skipped_or_duplicate"), ShouldEqual, 2) 214 }) 215 216 Convey(`Filter test variant with failed presubmit`, t, func() { 217 ctx := newContext(t) 218 payload := &taskspb.IngestTestResults{ 219 Build: &controlpb.BuildResult{ 220 Project: "chromium", 221 }, 222 PresubmitRun: &controlpb.PresubmitResult{ 223 Status: pb.PresubmitRunStatus_PRESUBMIT_RUN_STATUS_FAILED, 224 Mode: pb.PresubmitRunMode_FULL_RUN, 225 }, 226 } 227 228 sourcesMap := tu.SampleSourcesMap(10) 229 sourcesMap["sources_id"].Changelists = []*pb.GerritChange{ 230 { 231 Host: "host", 232 Project: "proj", 233 Patchset: 1, 234 Change: 12345, 235 }, 236 } 237 tvs := []*rdbpb.TestVariant{ 238 { 239 TestId: "1", 240 Results: []*rdbpb.TestResultBundle{ 241 { 242 Result: &rdbpb.TestResult{ 243 Name: "invocations/inv-1/tests/abc", 244 Status: rdbpb.TestStatus_PASS, 245 }, 246 }, 247 }, 248 SourcesId: "sources_id", 249 }, 250 } 251 duplicateMap := map[string]bool{} 252 tvs, err := filterTestVariants(ctx, tvs, payload, duplicateMap, sourcesMap) 253 So(err, ShouldBeNil) 254 So(len(tvs), ShouldEqual, 0) 255 So(verdictCounter.Get(ctx, "chromium", "skipped_unsubmitted_code"), ShouldEqual, 1) 256 }) 257 } 258 259 func TestAnalyzeSingleBatch(t *testing.T) { 260 exporter, client := fakeExporter() 261 Convey(`Analyze batch with empty buffer`, t, func() { 262 ctx := newContext(t) 263 payload := tu.SamplePayload() 264 sourcesMap := tu.SampleSourcesMap(10) 265 tvs := []*rdbpb.TestVariant{ 266 { 267 TestId: "test_1", 268 VariantHash: "hash_1", 269 Variant: &rdbpb.Variant{ 270 Def: map[string]string{ 271 "k": "v", 272 }, 273 }, 274 Status: rdbpb.TestVariantStatus_EXPECTED, 275 Results: []*rdbpb.TestResultBundle{ 276 { 277 Result: &rdbpb.TestResult{ 278 Name: "invocations/abc/tests/xyz", 279 Status: rdbpb.TestStatus_PASS, 280 Expected: true, 281 }, 282 }, 283 }, 284 SourcesId: "sources_id", 285 }, 286 { 287 TestId: "test_2", 288 VariantHash: "hash_2", 289 Variant: &rdbpb.Variant{ 290 Def: map[string]string{ 291 "k2": "v2", 292 }, 293 }, 294 Status: rdbpb.TestVariantStatus_UNEXPECTED, 295 Results: []*rdbpb.TestResultBundle{ 296 { 297 Result: &rdbpb.TestResult{ 298 Name: "invocations/def/tests/xyz", 299 Status: rdbpb.TestStatus_PASS, 300 Expected: true, 301 }, 302 }, 303 { 304 Result: &rdbpb.TestResult{ 305 Name: "invocations/def/tests/xyz", 306 Status: rdbpb.TestStatus_FAIL, 307 Expected: true, 308 }, 309 }, 310 { 311 Result: &rdbpb.TestResult{ 312 Name: "invocations/def/tests/xyz", 313 Status: rdbpb.TestStatus_CRASH, 314 Expected: true, 315 }, 316 }, 317 { 318 Result: &rdbpb.TestResult{ 319 Name: "invocations/def/tests/xyz", 320 Status: rdbpb.TestStatus_ABORT, 321 Expected: true, 322 }, 323 }, 324 { 325 Result: &rdbpb.TestResult{ 326 Name: "invocations/def/tests/xyz", 327 Status: rdbpb.TestStatus_PASS, 328 }, 329 }, 330 { 331 Result: &rdbpb.TestResult{ 332 Name: "invocations/def/tests/xyz", 333 Status: rdbpb.TestStatus_FAIL, 334 }, 335 }, 336 { 337 Result: &rdbpb.TestResult{ 338 Name: "invocations/def/tests/xyz", 339 Status: rdbpb.TestStatus_CRASH, 340 }, 341 }, 342 { 343 Result: &rdbpb.TestResult{ 344 Name: "invocations/def/tests/xyz", 345 Status: rdbpb.TestStatus_ABORT, 346 }, 347 }, 348 }, 349 SourcesId: "sources_id", 350 }, 351 } 352 353 err := analyzeSingleBatch(ctx, tvs, payload, sourcesMap, exporter) 354 So(err, ShouldBeNil) 355 So(countCheckPoint(ctx), ShouldEqual, 1) 356 357 // Check invocations. 358 invs := fetchInvocations(ctx) 359 So(invs, ShouldResemble, []Invocation{ 360 { 361 Project: "chromium", 362 InvocationID: "abc", 363 IngestedInvocationID: "build-1234", 364 }, 365 { 366 Project: "chromium", 367 InvocationID: "def", 368 IngestedInvocationID: "build-1234", 369 }, 370 }) 371 372 // Check test variant branch. 373 tvbs, err := FetchTestVariantBranches(ctx) 374 So(err, ShouldBeNil) 375 So(len(tvbs), ShouldEqual, 2) 376 377 So(tvbs[0], ShouldResembleProto, &testvariantbranch.Entry{ 378 Project: "chromium", 379 TestID: "test_1", 380 VariantHash: "hash_1", 381 RefHash: pbutil.SourceRefHash(pbutil.SourceRefFromSources(sourcesMap["sources_id"])), 382 Variant: &pb.Variant{ 383 Def: map[string]string{ 384 "k": "v", 385 }, 386 }, 387 SourceRef: &pb.SourceRef{ 388 System: &pb.SourceRef_Gitiles{ 389 Gitiles: &pb.GitilesRef{ 390 Host: "host", 391 Project: "proj", 392 Ref: "ref", 393 }, 394 }, 395 }, 396 InputBuffer: &inputbuffer.Buffer{ 397 HotBuffer: inputbuffer.History{ 398 Verdicts: []inputbuffer.PositionVerdict{ 399 { 400 CommitPosition: 10, 401 IsSimpleExpectedPass: true, 402 Hour: payload.PartitionTime.AsTime(), 403 }, 404 }, 405 }, 406 ColdBuffer: inputbuffer.History{ 407 Verdicts: []inputbuffer.PositionVerdict{}, 408 }, 409 HotBufferCapacity: inputbuffer.DefaultHotBufferCapacity, 410 ColdBufferCapacity: inputbuffer.DefaultColdBufferCapacity, 411 }, 412 }) 413 414 So(tvbs[1], ShouldResembleProto, &testvariantbranch.Entry{ 415 Project: "chromium", 416 TestID: "test_2", 417 VariantHash: "hash_2", 418 RefHash: pbutil.SourceRefHash(pbutil.SourceRefFromSources(sourcesMap["sources_id"])), 419 Variant: &pb.Variant{ 420 Def: map[string]string{ 421 "k2": "v2", 422 }, 423 }, 424 SourceRef: &pb.SourceRef{ 425 System: &pb.SourceRef_Gitiles{ 426 Gitiles: &pb.GitilesRef{ 427 Host: "host", 428 Project: "proj", 429 Ref: "ref", 430 }, 431 }, 432 }, 433 InputBuffer: &inputbuffer.Buffer{ 434 HotBuffer: inputbuffer.History{ 435 Verdicts: []inputbuffer.PositionVerdict{ 436 { 437 CommitPosition: 10, 438 IsSimpleExpectedPass: false, 439 Hour: payload.PartitionTime.AsTime(), 440 Details: inputbuffer.VerdictDetails{ 441 IsExonerated: false, 442 Runs: []inputbuffer.Run{ 443 { 444 Expected: inputbuffer.ResultCounts{ 445 PassCount: 1, 446 FailCount: 1, 447 CrashCount: 1, 448 AbortCount: 1, 449 }, 450 Unexpected: inputbuffer.ResultCounts{ 451 PassCount: 1, 452 FailCount: 1, 453 CrashCount: 1, 454 AbortCount: 1, 455 }, 456 }, 457 }, 458 }, 459 }, 460 }, 461 }, 462 ColdBuffer: inputbuffer.History{ 463 Verdicts: []inputbuffer.PositionVerdict{}, 464 }, 465 HotBufferCapacity: inputbuffer.DefaultHotBufferCapacity, 466 ColdBufferCapacity: inputbuffer.DefaultColdBufferCapacity, 467 }, 468 }) 469 470 So(len(client.Insertions), ShouldEqual, 2) 471 for _, insert := range client.Insertions { 472 // Check the version is populated, but do not assert its value 473 // as it is a commit timestamp that varies each time the test runs. 474 So(insert.Version, ShouldNotBeNil) 475 insert.Version = nil 476 } 477 478 sort.Slice(client.Insertions, func(i, j int) bool { 479 return client.Insertions[i].TestId < client.Insertions[j].TestId 480 }) 481 So(client.Insertions[0], ShouldResembleProto, &bqpb.TestVariantBranchRow{ 482 Project: "chromium", 483 TestId: "test_1", 484 VariantHash: "hash_1", 485 RefHash: "6de221242e011c91", 486 Variant: "{\"k\":\"v\"}", 487 Ref: &pb.SourceRef{ 488 System: &pb.SourceRef_Gitiles{ 489 Gitiles: &pb.GitilesRef{ 490 Host: "host", 491 Project: "proj", 492 Ref: "ref", 493 }, 494 }, 495 }, 496 Segments: []*bqpb.Segment{ 497 { 498 StartPosition: 10, 499 StartHour: timestamppb.New(time.Unix(3600*10, 0)), 500 EndPosition: 10, 501 EndHour: timestamppb.New(time.Unix(3600*10, 0)), 502 Counts: &bqpb.Segment_Counts{ 503 TotalResults: 1, 504 TotalRuns: 1, 505 TotalVerdicts: 1, 506 ExpectedPassedResults: 1, 507 }, 508 }, 509 }, 510 }) 511 So(client.Insertions[1], ShouldResembleProto, &bqpb.TestVariantBranchRow{ 512 Project: "chromium", 513 TestId: "test_2", 514 VariantHash: "hash_2", 515 RefHash: "6de221242e011c91", 516 Variant: "{\"k2\":\"v2\"}", 517 Ref: &pb.SourceRef{ 518 System: &pb.SourceRef_Gitiles{ 519 Gitiles: &pb.GitilesRef{ 520 Host: "host", 521 Project: "proj", 522 Ref: "ref", 523 }, 524 }, 525 }, 526 Segments: []*bqpb.Segment{ 527 { 528 StartPosition: 10, 529 StartHour: timestamppb.New(time.Unix(3600*10, 0)), 530 EndPosition: 10, 531 EndHour: timestamppb.New(time.Unix(3600*10, 0)), 532 Counts: &bqpb.Segment_Counts{ 533 TotalVerdicts: 1, 534 FlakyVerdicts: 1, 535 TotalRuns: 1, 536 FlakyRuns: 1, 537 TotalResults: 8, 538 UnexpectedResults: 4, 539 ExpectedPassedResults: 1, 540 ExpectedFailedResults: 1, 541 ExpectedCrashedResults: 1, 542 ExpectedAbortedResults: 1, 543 UnexpectedPassedResults: 1, 544 UnexpectedFailedResults: 1, 545 UnexpectedCrashedResults: 1, 546 UnexpectedAbortedResults: 1, 547 }, 548 }, 549 }, 550 }) 551 552 So(verdictCounter.Get(ctx, "chromium", "ingested"), ShouldEqual, 2) 553 }) 554 555 Convey(`Analyze batch run analysis got change point`, t, func() { 556 ctx := newContext(t) 557 exporter, client := fakeExporter() 558 559 // Store some existing data in spanner first. 560 sourcesMap := tu.SampleSourcesMap(10) 561 positions := make([]int, 2000) 562 total := make([]int, 2000) 563 hasUnexpected := make([]int, 2000) 564 for i := 0; i < 2000; i++ { 565 positions[i] = i + 1 566 total[i] = 1 567 if i >= 100 { 568 hasUnexpected[i] = 1 569 } 570 } 571 vs := inputbuffer.Verdicts(positions, total, hasUnexpected) 572 ref := pbutil.SourceRefFromSources(sourcesMap["sources_id"]) 573 tvb := &testvariantbranch.Entry{ 574 IsNew: true, 575 Project: "chromium", 576 TestID: "test_1", 577 VariantHash: "hash_1", 578 SourceRef: ref, 579 RefHash: pbutil.SourceRefHash(ref), 580 Variant: &pb.Variant{ 581 Def: map[string]string{ 582 "k": "v", 583 }, 584 }, 585 InputBuffer: &inputbuffer.Buffer{ 586 HotBuffer: inputbuffer.History{ 587 Verdicts: []inputbuffer.PositionVerdict{}, 588 }, 589 ColdBuffer: inputbuffer.History{ 590 Verdicts: vs, 591 }, 592 IsColdBufferDirty: true, 593 }, 594 } 595 var hs inputbuffer.HistorySerializer 596 mutation, err := tvb.ToMutation(&hs) 597 So(err, ShouldBeNil) 598 testutil.MustApply(ctx, mutation) 599 600 // Insert a new verdict. 601 payload := tu.SamplePayload() 602 const ingestedVerdictHour = 55 603 payload.PartitionTime = timestamppb.New(time.Unix(ingestedVerdictHour*3600, 0)) 604 605 tvs := []*rdbpb.TestVariant{ 606 { 607 TestId: "test_1", 608 VariantHash: "hash_1", 609 Status: rdbpb.TestVariantStatus_EXPECTED, 610 Results: []*rdbpb.TestResultBundle{ 611 { 612 Result: &rdbpb.TestResult{ 613 Name: "invocations/abc/tests/xyz", 614 Status: rdbpb.TestStatus_PASS, 615 }, 616 }, 617 }, 618 SourcesId: "sources_id", 619 }, 620 } 621 622 err = analyzeSingleBatch(ctx, tvs, payload, sourcesMap, exporter) 623 So(err, ShouldBeNil) 624 So(countCheckPoint(ctx), ShouldEqual, 1) 625 626 // Check invocations. 627 invs := fetchInvocations(ctx) 628 So(invs, ShouldResemble, []Invocation{ 629 { 630 Project: "chromium", 631 InvocationID: "abc", 632 IngestedInvocationID: "build-1234", 633 }, 634 }) 635 636 // Check test variant branch. 637 tvbs, err := FetchTestVariantBranches(ctx) 638 So(err, ShouldBeNil) 639 So(len(tvbs), ShouldEqual, 1) 640 tvb = tvbs[0] 641 642 // Setup bucket expectations. 643 // Only statistics for verdicts evicted from the output buffer should be 644 // included in the statistics. 645 var expectedBuckets []*cpb.Statistics_HourBucket 646 for i := 1; i <= 100; i++ { 647 bucket := &cpb.Statistics_HourBucket{ 648 Hour: int64(i), 649 TotalVerdicts: 1, 650 } 651 expectedBuckets = append(expectedBuckets, bucket) 652 if bucket.Hour == ingestedVerdictHour { 653 // Add one for the verdict we just ingested. 654 bucket.TotalVerdicts += 1 655 } 656 } 657 658 So(tvb, ShouldResembleProto, &testvariantbranch.Entry{ 659 Project: "chromium", 660 TestID: "test_1", 661 VariantHash: "hash_1", 662 RefHash: pbutil.SourceRefHash(pbutil.SourceRefFromSources(sourcesMap["sources_id"])), 663 Variant: &pb.Variant{ 664 Def: map[string]string{ 665 "k": "v", 666 }, 667 }, 668 SourceRef: &pb.SourceRef{ 669 System: &pb.SourceRef_Gitiles{ 670 Gitiles: &pb.GitilesRef{ 671 Host: "host", 672 Project: "proj", 673 Ref: "ref", 674 }, 675 }, 676 }, 677 InputBuffer: &inputbuffer.Buffer{ 678 HotBuffer: inputbuffer.History{ 679 Verdicts: []inputbuffer.PositionVerdict{}, 680 }, 681 ColdBuffer: inputbuffer.History{ 682 Verdicts: vs[100:], 683 }, 684 HotBufferCapacity: inputbuffer.DefaultHotBufferCapacity, 685 ColdBufferCapacity: inputbuffer.DefaultColdBufferCapacity, 686 }, 687 FinalizingSegment: &cpb.Segment{ 688 State: cpb.SegmentState_FINALIZING, 689 HasStartChangepoint: true, 690 StartPosition: 101, 691 StartHour: timestamppb.New(time.Unix(101*3600, 0)), 692 FinalizedCounts: &cpb.Counts{}, 693 StartPositionLowerBound_99Th: 100, 694 StartPositionUpperBound_99Th: 101, 695 }, 696 FinalizedSegments: &cpb.Segments{ 697 Segments: []*cpb.Segment{ 698 { 699 State: cpb.SegmentState_FINALIZED, 700 HasStartChangepoint: false, 701 StartPosition: 1, 702 StartHour: timestamppb.New(time.Unix(3600, 0)), 703 EndPosition: 100, 704 EndHour: timestamppb.New(time.Unix(100*3600, 0)), 705 FinalizedCounts: &cpb.Counts{ 706 TotalResults: 101, 707 TotalRuns: 101, 708 TotalVerdicts: 101, 709 ExpectedPassedResults: 101, 710 }, 711 }, 712 }, 713 }, 714 Statistics: &cpb.Statistics{ 715 HourlyBuckets: expectedBuckets, 716 }, 717 }) 718 So(len(client.Insertions), ShouldEqual, 1) 719 So(verdictCounter.Get(ctx, "chromium", "ingested"), ShouldEqual, 1) 720 }) 721 722 Convey(`Analyze batch should apply retention policy`, t, func() { 723 ctx := newContext(t) 724 exporter, client := fakeExporter() 725 726 // Store some existing data in spanner first. 727 sourcesMap := tu.SampleSourcesMap(10) 728 729 // Set up 110 finalized segments. 730 finalizedSegments := []*cpb.Segment{} 731 for i := 0; i < 110; i++ { 732 finalizedSegments = append(finalizedSegments, &cpb.Segment{ 733 EndHour: timestamppb.New(time.Unix(int64(i*3600), 0)), 734 FinalizedCounts: &cpb.Counts{}, 735 }) 736 } 737 sourceRef := pbutil.SourceRefFromSources(sourcesMap["sources_id"]) 738 tvb := &testvariantbranch.Entry{ 739 IsNew: true, 740 Project: "chromium", 741 TestID: "test_1", 742 VariantHash: "hash_1", 743 SourceRef: sourceRef, 744 RefHash: pbutil.SourceRefHash(sourceRef), 745 Variant: &pb.Variant{ 746 Def: map[string]string{ 747 "k": "v", 748 }, 749 }, 750 InputBuffer: &inputbuffer.Buffer{ 751 HotBuffer: inputbuffer.History{ 752 Verdicts: []inputbuffer.PositionVerdict{ 753 { 754 CommitPosition: 1, 755 IsSimpleExpectedPass: true, 756 }, 757 }, 758 }, 759 ColdBuffer: inputbuffer.History{ 760 Verdicts: []inputbuffer.PositionVerdict{}, 761 }, 762 IsColdBufferDirty: true, 763 }, 764 FinalizedSegments: &cpb.Segments{Segments: finalizedSegments}, 765 } 766 var hs inputbuffer.HistorySerializer 767 mutation, err := tvb.ToMutation(&hs) 768 So(err, ShouldBeNil) 769 testutil.MustApply(ctx, mutation) 770 771 // Insert a new verdict. 772 payload := tu.SamplePayload() 773 const ingestedVerdictHour = 5*365*24 + 13 774 payload.PartitionTime = timestamppb.New(time.Unix(ingestedVerdictHour*3600, 0)) 775 776 tvs := []*rdbpb.TestVariant{ 777 { 778 TestId: "test_1", 779 VariantHash: "hash_1", 780 Status: rdbpb.TestVariantStatus_EXPECTED, 781 Results: []*rdbpb.TestResultBundle{ 782 { 783 Result: &rdbpb.TestResult{ 784 Name: "invocations/abc/tests/xyz", 785 Status: rdbpb.TestStatus_PASS, 786 }, 787 }, 788 }, 789 SourcesId: "sources_id", 790 }, 791 } 792 793 err = analyzeSingleBatch(ctx, tvs, payload, sourcesMap, exporter) 794 So(err, ShouldBeNil) 795 So(countCheckPoint(ctx), ShouldEqual, 1) 796 797 // Check invocations. 798 invs := fetchInvocations(ctx) 799 So(invs, ShouldResemble, []Invocation{ 800 { 801 Project: "chromium", 802 InvocationID: "abc", 803 IngestedInvocationID: "build-1234", 804 }, 805 }) 806 807 // Check test variant branch. 808 tvbs, err := FetchTestVariantBranches(ctx) 809 So(err, ShouldBeNil) 810 So(len(tvbs), ShouldEqual, 1) 811 tvb = tvbs[0] 812 813 So(tvb, ShouldResembleProto, &testvariantbranch.Entry{ 814 Project: "chromium", 815 TestID: "test_1", 816 VariantHash: "hash_1", 817 RefHash: pbutil.SourceRefHash(sourceRef), 818 Variant: &pb.Variant{ 819 Def: map[string]string{ 820 "k": "v", 821 }, 822 }, 823 SourceRef: &pb.SourceRef{ 824 System: &pb.SourceRef_Gitiles{ 825 Gitiles: &pb.GitilesRef{ 826 Host: "host", 827 Project: "proj", 828 Ref: "ref", 829 }, 830 }, 831 }, 832 InputBuffer: &inputbuffer.Buffer{ 833 HotBuffer: inputbuffer.History{ 834 Verdicts: []inputbuffer.PositionVerdict{ 835 { 836 CommitPosition: 1, 837 IsSimpleExpectedPass: true, 838 }, 839 { 840 CommitPosition: 10, 841 IsSimpleExpectedPass: true, 842 Hour: payload.PartitionTime.AsTime(), 843 }, 844 }, 845 }, 846 ColdBuffer: inputbuffer.History{ 847 Verdicts: []inputbuffer.PositionVerdict{}, 848 }, 849 HotBufferCapacity: inputbuffer.DefaultHotBufferCapacity, 850 ColdBufferCapacity: inputbuffer.DefaultColdBufferCapacity, 851 }, 852 853 FinalizedSegments: &cpb.Segments{ 854 Segments: finalizedSegments[14:], 855 }, 856 }) 857 So(len(client.Insertions), ShouldEqual, 1) 858 So(verdictCounter.Get(ctx, "chromium", "ingested"), ShouldEqual, 1) 859 }) 860 } 861 862 func TestOutOfOrderVerdict(t *testing.T) { 863 Convey("Out of order verdict", t, func() { 864 sourcesMap := tu.SampleSourcesMap(10) 865 sources := sourcesMap["sources_id"] 866 Convey("No test variant branch", func() { 867 So(isOutOfOrderAndShouldBeDiscarded(nil, sources), ShouldBeFalse) 868 }) 869 870 Convey("No finalizing or finalized segment", func() { 871 tvb := &testvariantbranch.Entry{} 872 So(isOutOfOrderAndShouldBeDiscarded(tvb, sources), ShouldBeFalse) 873 }) 874 875 Convey("Have finalizing segments", func() { 876 tvb := finalizingTvbWithPositions([]int{1}, []int{}) 877 So(isOutOfOrderAndShouldBeDiscarded(tvb, sources), ShouldBeFalse) 878 tvb = finalizingTvbWithPositions([]int{}, []int{1}) 879 So(isOutOfOrderAndShouldBeDiscarded(tvb, sources), ShouldBeFalse) 880 tvb = finalizingTvbWithPositions([]int{8, 13}, []int{7, 9}) 881 So(isOutOfOrderAndShouldBeDiscarded(tvb, sources), ShouldBeFalse) 882 tvb = finalizingTvbWithPositions([]int{11, 15}, []int{6, 8}) 883 So(isOutOfOrderAndShouldBeDiscarded(tvb, sources), ShouldBeFalse) 884 tvb = finalizingTvbWithPositions([]int{11, 15}, []int{10, 16}) 885 So(isOutOfOrderAndShouldBeDiscarded(tvb, sources), ShouldBeFalse) 886 tvb = finalizingTvbWithPositions([]int{11, 15}, []int{12, 16}) 887 So(isOutOfOrderAndShouldBeDiscarded(tvb, sources), ShouldBeTrue) 888 }) 889 }) 890 } 891 892 func countCheckPoint(ctx context.Context) int { 893 st := spanner.NewStatement(` 894 SELECT * 895 FROM TestVariantBranchCheckPoint 896 `) 897 it := span.Query(span.Single(ctx), st) 898 count := 0 899 err := it.Do(func(r *spanner.Row) error { 900 count++ 901 return nil 902 }) 903 So(err, ShouldBeNil) 904 return count 905 } 906 907 func fetchInvocations(ctx context.Context) []Invocation { 908 st := spanner.NewStatement(` 909 SELECT Project, InvocationID, IngestedInvocationID 910 FROM Invocations 911 ORDER BY InvocationID 912 `) 913 it := span.Query(span.Single(ctx), st) 914 results := []Invocation{} 915 err := it.Do(func(r *spanner.Row) error { 916 var b spanutil.Buffer 917 inv := Invocation{} 918 err := b.FromSpanner(r, &inv.Project, &inv.InvocationID, &inv.IngestedInvocationID) 919 if err != nil { 920 return err 921 } 922 results = append(results, inv) 923 return nil 924 }) 925 So(err, ShouldBeNil) 926 return results 927 } 928 929 func testVariants(n int) []*rdbpb.TestVariant { 930 tvs := make([]*rdbpb.TestVariant, n) 931 for i := 0; i < n; i++ { 932 tvs[i] = &rdbpb.TestVariant{ 933 TestId: fmt.Sprintf("test_%d", i), 934 VariantHash: fmt.Sprintf("hash_%d", i), 935 SourcesId: "sources_id", 936 } 937 } 938 return tvs 939 } 940 941 func finalizingTvbWithPositions(hotPositions []int, coldPositions []int) *testvariantbranch.Entry { 942 tvb := &testvariantbranch.Entry{ 943 FinalizingSegment: &cpb.Segment{}, 944 InputBuffer: &inputbuffer.Buffer{}, 945 } 946 for _, pos := range hotPositions { 947 tvb.InputBuffer.HotBuffer.Verdicts = append(tvb.InputBuffer.HotBuffer.Verdicts, inputbuffer.PositionVerdict{ 948 CommitPosition: pos, 949 }) 950 } 951 952 for _, pos := range coldPositions { 953 tvb.InputBuffer.ColdBuffer.Verdicts = append(tvb.InputBuffer.ColdBuffer.Verdicts, inputbuffer.PositionVerdict{ 954 CommitPosition: pos, 955 }) 956 } 957 return tvb 958 } 959 960 func newContext(t *testing.T) context.Context { 961 ctx := memory.Use(testutil.IntegrationTestContext(t)) 962 So(config.SetTestConfig(ctx, tu.TestConfig()), ShouldBeNil) 963 return ctx 964 } 965 966 func fakeExporter() (*bqexporter.Exporter, *bqexporter.FakeClient) { 967 client := bqexporter.NewFakeClient() 968 exporter := bqexporter.NewExporter(client) 969 return exporter, client 970 }