go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/services/resultingester/ingest_test_results_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 resultingester 16 17 import ( 18 "context" 19 "encoding/hex" 20 "sort" 21 "strings" 22 "testing" 23 "time" 24 25 "cloud.google.com/go/spanner" 26 "github.com/golang/mock/gomock" 27 "google.golang.org/protobuf/proto" 28 "google.golang.org/protobuf/types/known/timestamppb" 29 30 bbpb "go.chromium.org/luci/buildbucket/proto" 31 "go.chromium.org/luci/common/clock" 32 "go.chromium.org/luci/gae/impl/memory" 33 rdbpbutil "go.chromium.org/luci/resultdb/pbutil" 34 rdbpb "go.chromium.org/luci/resultdb/proto/v1" 35 "go.chromium.org/luci/server/caching" 36 "go.chromium.org/luci/server/span" 37 "go.chromium.org/luci/server/tq" 38 "go.chromium.org/luci/server/tq/tqtesting" 39 40 "go.chromium.org/luci/analysis/internal/analysis" 41 "go.chromium.org/luci/analysis/internal/analysis/clusteredfailures" 42 "go.chromium.org/luci/analysis/internal/buildbucket" 43 "go.chromium.org/luci/analysis/internal/changepoints" 44 "go.chromium.org/luci/analysis/internal/changepoints/bqexporter" 45 "go.chromium.org/luci/analysis/internal/changepoints/inputbuffer" 46 changepointspb "go.chromium.org/luci/analysis/internal/changepoints/proto" 47 "go.chromium.org/luci/analysis/internal/changepoints/testvariantbranch" 48 "go.chromium.org/luci/analysis/internal/clustering/chunkstore" 49 "go.chromium.org/luci/analysis/internal/clustering/ingestion" 50 "go.chromium.org/luci/analysis/internal/config" 51 "go.chromium.org/luci/analysis/internal/gerrit" 52 "go.chromium.org/luci/analysis/internal/ingestion/control" 53 ctrlpb "go.chromium.org/luci/analysis/internal/ingestion/control/proto" 54 "go.chromium.org/luci/analysis/internal/resultdb" 55 "go.chromium.org/luci/analysis/internal/tasks/taskspb" 56 "go.chromium.org/luci/analysis/internal/testresults" 57 "go.chromium.org/luci/analysis/internal/testutil" 58 "go.chromium.org/luci/analysis/internal/testverdicts" 59 "go.chromium.org/luci/analysis/pbutil" 60 bqpb "go.chromium.org/luci/analysis/proto/bq" 61 configpb "go.chromium.org/luci/analysis/proto/config" 62 pb "go.chromium.org/luci/analysis/proto/v1" 63 64 _ "go.chromium.org/luci/server/tq/txn/spanner" 65 66 . "github.com/smartystreets/goconvey/convey" 67 . "go.chromium.org/luci/common/testing/assertions" 68 ) 69 70 func TestSchedule(t *testing.T) { 71 Convey(`TestSchedule`, t, func() { 72 ctx := testutil.IntegrationTestContext(t) 73 ctx, skdr := tq.TestingContext(ctx, nil) 74 75 task := &taskspb.IngestTestResults{ 76 Build: &ctrlpb.BuildResult{}, 77 PartitionTime: timestamppb.New(time.Date(2025, time.January, 1, 12, 0, 0, 0, time.UTC)), 78 } 79 expected := proto.Clone(task).(*taskspb.IngestTestResults) 80 81 _, err := span.ReadWriteTransaction(ctx, func(ctx context.Context) error { 82 Schedule(ctx, task) 83 return nil 84 }) 85 So(err, ShouldBeNil) 86 So(skdr.Tasks().Payloads()[0], ShouldResembleProto, expected) 87 }) 88 } 89 90 const testInvocation = "invocations/build-87654321" 91 const testRealm = "project:ci" 92 const testBuildID = int64(87654321) 93 94 func TestIngestTestResults(t *testing.T) { 95 Convey(`TestIngestTestResults`, t, func() { 96 ctx := testutil.IntegrationTestContext(t) 97 ctx = caching.WithEmptyProcessCache(ctx) // For failure association rules cache. 98 ctx, skdr := tq.TestingContext(ctx, nil) 99 ctx = memory.Use(ctx) 100 101 chunkStore := chunkstore.NewFakeClient() 102 clusteredFailures := clusteredfailures.NewFakeClient() 103 testVerdicts := testverdicts.NewFakeClient() 104 tvBQExporterClient := bqexporter.NewFakeClient() 105 analysis := analysis.NewClusteringHandler(clusteredFailures) 106 ri := &resultIngester{ 107 clustering: ingestion.New(chunkStore, analysis), 108 verdictExporter: testverdicts.NewExporter(testVerdicts), 109 testVariantBranchExporter: bqexporter.NewExporter(tvBQExporterClient), 110 } 111 112 Convey(`partition time`, func() { 113 payload := &taskspb.IngestTestResults{ 114 Build: &ctrlpb.BuildResult{ 115 Host: "host", 116 Id: 13131313, 117 Project: "project", 118 }, 119 PartitionTime: timestamppb.New(clock.Now(ctx).Add(-1 * time.Hour)), 120 } 121 Convey(`too early`, func() { 122 payload.PartitionTime = timestamppb.New(clock.Now(ctx).Add(25 * time.Hour)) 123 err := ri.ingestTestResults(ctx, payload) 124 So(err, ShouldErrLike, "too far in the future") 125 }) 126 Convey(`too late`, func() { 127 payload.PartitionTime = timestamppb.New(clock.Now(ctx).Add(-91 * 24 * time.Hour)) 128 err := ri.ingestTestResults(ctx, payload) 129 So(err, ShouldErrLike, "too long ago") 130 }) 131 }) 132 133 Convey(`valid payload`, func() { 134 ctl := gomock.NewController(t) 135 defer ctl.Finish() 136 137 mrc := resultdb.NewMockedClient(ctx, ctl) 138 mbc := buildbucket.NewMockedClient(mrc.Ctx, ctl) 139 ctx = mbc.Ctx 140 141 clsByHost := gerritChangesByHostForTesting() 142 ctx = gerrit.UseFakeClient(ctx, clsByHost) 143 144 bHost := "host" 145 partitionTime := clock.Now(ctx).Add(-1 * time.Hour) 146 147 setupGetInvocationMock := func() { 148 invReq := &rdbpb.GetInvocationRequest{ 149 Name: testInvocation, 150 } 151 invRes := &rdbpb.Invocation{ 152 Name: testInvocation, 153 Realm: testRealm, 154 } 155 mrc.GetInvocation(invReq, invRes) 156 } 157 158 setupQueryTestVariantsMock := func(modifiers ...func(*rdbpb.QueryTestVariantsResponse)) { 159 tvReq := &rdbpb.QueryTestVariantsRequest{ 160 Invocations: []string{testInvocation}, 161 PageSize: 10000, 162 ResultLimit: 100, 163 ReadMask: testVariantReadMask, 164 PageToken: "expected_token", 165 } 166 tvRsp := mockedQueryTestVariantsRsp() 167 tvRsp.NextPageToken = "continuation_token" 168 for _, modifier := range modifiers { 169 modifier(tvRsp) 170 } 171 mrc.QueryTestVariants(tvReq, tvRsp) 172 } 173 174 cfg := &configpb.Config{ 175 TestVariantAnalysis: &configpb.TestVariantAnalysis{ 176 Enabled: true, 177 BigqueryExportEnabled: true, 178 }, 179 TestVerdictExport: &configpb.TestVerdictExport{ 180 Enabled: true, 181 }, 182 Clustering: &configpb.ClusteringSystem{ 183 QueryTestVariantAnalysisEnabled: true, 184 }, 185 } 186 setupConfig := func(ctx context.Context, cfg *configpb.Config) { 187 err := config.SetTestConfig(ctx, cfg) 188 So(err, ShouldBeNil) 189 } 190 191 // Populate some existing test variant analysis. 192 setupTestVariantAnalysis(ctx, partitionTime) 193 194 payload := &taskspb.IngestTestResults{ 195 Build: &ctrlpb.BuildResult{ 196 Host: bHost, 197 Id: testBuildID, 198 CreationTime: timestamppb.New(time.Date(2020, time.April, 1, 2, 3, 4, 5, time.UTC)), 199 Project: "project", 200 Bucket: "bucket", 201 Builder: "builder", 202 Status: pb.BuildStatus_BUILD_STATUS_FAILURE, 203 Changelists: []*pb.Changelist{ 204 { 205 Host: "anothergerrit.gerrit.instance", 206 Change: 77788, 207 Patchset: 19, 208 OwnerKind: pb.ChangelistOwnerKind_HUMAN, 209 }, 210 { 211 Host: "mygerrit-review.googlesource.com", 212 Change: 12345, 213 Patchset: 5, 214 OwnerKind: pb.ChangelistOwnerKind_AUTOMATION, 215 }, 216 }, 217 Commit: &bbpb.GitilesCommit{ 218 Host: "myproject.googlesource.com", 219 Project: "someproject/src", 220 Id: strings.Repeat("0a", 20), 221 Ref: "refs/heads/mybranch", 222 Position: 111888, 223 }, 224 HasInvocation: true, 225 ResultdbHost: "results.api.cr.dev", 226 IsIncludedByAncestor: false, 227 GardenerRotations: []string{"rotation1", "rotation2"}, 228 }, 229 PartitionTime: timestamppb.New(partitionTime), 230 PresubmitRun: &ctrlpb.PresubmitResult{ 231 PresubmitRunId: &pb.PresubmitRunId{ 232 System: "luci-cv", 233 Id: "infra/12345", 234 }, 235 Status: pb.PresubmitRunStatus_PRESUBMIT_RUN_STATUS_SUCCEEDED, 236 Mode: pb.PresubmitRunMode_FULL_RUN, 237 Owner: "automation", 238 CreationTime: timestamppb.New(time.Date(2021, time.April, 1, 2, 3, 4, 5, time.UTC)), 239 }, 240 PageToken: "expected_token", 241 TaskIndex: 0, 242 } 243 expectedContinuation := proto.Clone(payload).(*taskspb.IngestTestResults) 244 expectedContinuation.PageToken = "continuation_token" 245 expectedContinuation.TaskIndex = 1 246 247 ingestionCtl := 248 control.NewEntry(0). 249 WithBuildID(control.BuildID(bHost, testBuildID)). 250 WithBuildResult(proto.Clone(payload.Build).(*ctrlpb.BuildResult)). 251 WithPresubmitResult(proto.Clone(payload.PresubmitRun).(*ctrlpb.PresubmitResult)). 252 WithTaskCount(1). 253 Build() 254 255 Convey(`First task`, func() { 256 setupGetInvocationMock() 257 setupQueryTestVariantsMock() 258 setupConfig(ctx, cfg) 259 _, err := control.SetEntriesForTesting(ctx, ingestionCtl) 260 So(err, ShouldBeNil) 261 262 // Act 263 err = ri.ingestTestResults(ctx, payload) 264 So(err, ShouldBeNil) 265 266 // Verify 267 268 // Expect a continuation task to be created. 269 verifyContinuationTask(skdr, expectedContinuation) 270 ingestionCtl.TaskCount = ingestionCtl.TaskCount + 1 // Expect to have been incremented. 271 verifyIngestionControl(ctx, ingestionCtl) 272 verifyTestResults(ctx, partitionTime) 273 verifyClustering(chunkStore, clusteredFailures) 274 verifyTestVerdicts(testVerdicts, partitionTime) 275 verifyTestVariantAnalysis(ctx, partitionTime, tvBQExporterClient) 276 }) 277 Convey(`Last task`, func() { 278 payload.TaskIndex = 10 279 ingestionCtl.TaskCount = 11 280 281 setupGetInvocationMock() 282 setupQueryTestVariantsMock(func(rsp *rdbpb.QueryTestVariantsResponse) { 283 rsp.NextPageToken = "" 284 }) 285 setupConfig(ctx, cfg) 286 287 _, err := control.SetEntriesForTesting(ctx, ingestionCtl) 288 So(err, ShouldBeNil) 289 290 // Act 291 err = ri.ingestTestResults(ctx, payload) 292 So(err, ShouldBeNil) 293 294 // Verify 295 296 // As this is the last task, do not expect a continuation 297 // task to be created. 298 verifyContinuationTask(skdr, nil) 299 verifyIngestionControl(ctx, ingestionCtl) 300 verifyTestResults(ctx, partitionTime) 301 verifyClustering(chunkStore, clusteredFailures) 302 verifyTestVerdicts(testVerdicts, partitionTime) 303 verifyTestVariantAnalysis(ctx, partitionTime, tvBQExporterClient) 304 }) 305 306 Convey(`Retry task after continuation task already created`, func() { 307 // Scenario: First task fails after it has already scheduled 308 // its continuation. 309 ingestionCtl.TaskCount = 2 310 311 setupGetInvocationMock() 312 setupQueryTestVariantsMock() 313 setupConfig(ctx, cfg) 314 315 _, err := control.SetEntriesForTesting(ctx, ingestionCtl) 316 So(err, ShouldBeNil) 317 318 // Act 319 err = ri.ingestTestResults(ctx, payload) 320 So(err, ShouldBeNil) 321 322 // Verify 323 324 // Do not expect a continuation task to be created, 325 // as it was already scheduled. 326 verifyContinuationTask(skdr, nil) 327 verifyIngestionControl(ctx, ingestionCtl) 328 verifyTestResults(ctx, partitionTime) 329 verifyClustering(chunkStore, clusteredFailures) 330 verifyTestVerdicts(testVerdicts, partitionTime) 331 verifyTestVariantAnalysis(ctx, partitionTime, tvBQExporterClient) 332 }) 333 Convey(`No project config`, func() { 334 // If no project config exists, results should be ingested into 335 // TestResults and clustered, but not used for the legacy test variant 336 // analysis. 337 config.SetTestProjectConfig(ctx, map[string]*configpb.ProjectConfig{}) 338 339 setupGetInvocationMock() 340 setupQueryTestVariantsMock() 341 setupConfig(ctx, cfg) 342 343 _, err := control.SetEntriesForTesting(ctx, ingestionCtl) 344 So(err, ShouldBeNil) 345 346 // Act 347 err = ri.ingestTestResults(ctx, payload) 348 So(err, ShouldBeNil) 349 350 // Verify 351 // Test results still ingested. 352 verifyTestResults(ctx, partitionTime) 353 354 // Cluster has happened. 355 verifyClustering(chunkStore, clusteredFailures) 356 357 // Test verdicts exported. 358 verifyTestVerdicts(testVerdicts, partitionTime) 359 verifyTestVariantAnalysis(ctx, partitionTime, tvBQExporterClient) 360 }) 361 Convey(`Build included by ancestor`, func() { 362 payload.Build.IsIncludedByAncestor = true 363 setupConfig(ctx, cfg) 364 365 // Act 366 err := ri.ingestTestResults(ctx, payload) 367 So(err, ShouldBeNil) 368 369 // Verify no test results ingested into test history. 370 var actualTRs []*testresults.TestResult 371 err = testresults.ReadTestResults(span.Single(ctx), spanner.AllKeys(), func(tr *testresults.TestResult) error { 372 actualTRs = append(actualTRs, tr) 373 return nil 374 }) 375 So(err, ShouldBeNil) 376 So(actualTRs, ShouldHaveLength, 0) 377 }) 378 Convey(`Project not allowed`, func() { 379 cfg.Ingestion = &configpb.Ingestion{ 380 ProjectAllowlistEnabled: true, 381 ProjectAllowlist: []string{"other"}, 382 } 383 setupConfig(ctx, cfg) 384 385 // Act 386 err := ri.ingestTestResults(ctx, payload) 387 So(err, ShouldBeNil) 388 389 // Verify no test results ingested into test history. 390 var actualTRs []*testresults.TestResult 391 err = testresults.ReadTestResults(span.Single(ctx), spanner.AllKeys(), func(tr *testresults.TestResult) error { 392 actualTRs = append(actualTRs, tr) 393 return nil 394 }) 395 So(err, ShouldBeNil) 396 So(actualTRs, ShouldHaveLength, 0) 397 }) 398 }) 399 }) 400 } 401 402 func setupTestVariantAnalysis(ctx context.Context, partitionTime time.Time) { 403 sr := &pb.SourceRef{ 404 System: &pb.SourceRef_Gitiles{ 405 Gitiles: &pb.GitilesRef{ 406 Host: "project.googlesource.com", 407 Project: "myproject/src", 408 Ref: "refs/heads/main", 409 }, 410 }, 411 } 412 // Truncated to nearest hour. 413 hour := partitionTime.Unix() / 3600 414 415 branch := &testvariantbranch.Entry{ 416 IsNew: true, 417 Project: "project", 418 TestID: "ninja://test_consistent_failure", 419 VariantHash: "hash", 420 SourceRef: sr, 421 RefHash: rdbpbutil.SourceRefHash(pbutil.SourceRefToResultDB(sr)), 422 InputBuffer: &inputbuffer.Buffer{ 423 HotBufferCapacity: 100, 424 ColdBufferCapacity: 2000, 425 HotBuffer: inputbuffer.History{ 426 Verdicts: []inputbuffer.PositionVerdict{}, 427 }, 428 ColdBuffer: inputbuffer.History{ 429 Verdicts: []inputbuffer.PositionVerdict{}, 430 }, 431 }, 432 Statistics: &changepointspb.Statistics{ 433 HourlyBuckets: []*changepointspb.Statistics_HourBucket{ 434 { 435 Hour: int64(hour - 23), 436 UnexpectedVerdicts: 123, 437 FlakyVerdicts: 456, 438 TotalVerdicts: 1999, 439 }, 440 }, 441 }, 442 } 443 var hs inputbuffer.HistorySerializer 444 m, err := branch.ToMutation(&hs) 445 So(err, ShouldBeNil) 446 testutil.MustApply(ctx, m) 447 } 448 449 func verifyTestVariantAnalysis(ctx context.Context, partitionTime time.Time, client *bqexporter.FakeClient) { 450 tvbs, err := changepoints.FetchTestVariantBranches(ctx) 451 So(err, ShouldBeNil) 452 So(len(tvbs), ShouldEqual, 1) 453 sr := &pb.SourceRef{ 454 System: &pb.SourceRef_Gitiles{ 455 Gitiles: &pb.GitilesRef{ 456 Host: "project.googlesource.com", 457 Project: "myproject/src", 458 Ref: "refs/heads/main", 459 }, 460 }, 461 } 462 // Truncated to nearest hour. 463 hour := time.Unix(partitionTime.Unix()/3600*3600, 0) 464 465 So(tvbs[0], ShouldResembleProto, &testvariantbranch.Entry{ 466 Project: "project", 467 TestID: "ninja://test_consistent_failure", 468 VariantHash: "hash", 469 SourceRef: sr, 470 RefHash: rdbpbutil.SourceRefHash(pbutil.SourceRefToResultDB(sr)), 471 InputBuffer: &inputbuffer.Buffer{ 472 HotBufferCapacity: 100, 473 ColdBufferCapacity: 2000, 474 HotBuffer: inputbuffer.History{ 475 Verdicts: []inputbuffer.PositionVerdict{ 476 { 477 CommitPosition: 16801, 478 Hour: hour, 479 Details: inputbuffer.VerdictDetails{ 480 IsExonerated: true, 481 Runs: []inputbuffer.Run{ 482 { 483 Unexpected: inputbuffer.ResultCounts{ 484 FailCount: 1, 485 }, 486 }, 487 }, 488 }, 489 }, 490 }, 491 }, 492 ColdBuffer: inputbuffer.History{ 493 Verdicts: []inputbuffer.PositionVerdict{}, 494 }, 495 }, 496 Statistics: &changepointspb.Statistics{ 497 HourlyBuckets: []*changepointspb.Statistics_HourBucket{ 498 { 499 Hour: int64(hour.Unix()/3600 - 23), 500 UnexpectedVerdicts: 123, 501 FlakyVerdicts: 456, 502 TotalVerdicts: 1999, 503 }, 504 }, 505 }, 506 }) 507 508 So(len(client.Insertions), ShouldEqual, 1) 509 } 510 511 func verifyTestResults(ctx context.Context, expectedPartitionTime time.Time) { 512 trBuilder := testresults.NewTestResult(). 513 WithProject("project"). 514 WithPartitionTime(expectedPartitionTime.In(time.UTC)). 515 WithIngestedInvocationID("build-87654321"). 516 WithSubRealm("ci"). 517 WithSources(testresults.Sources{ 518 Changelists: []testresults.Changelist{ 519 { 520 Host: "anothergerrit.gerrit.instance", 521 Change: 77788, 522 Patchset: 19, 523 OwnerKind: pb.ChangelistOwnerKind_HUMAN, 524 }, 525 { 526 Host: "mygerrit-review.googlesource.com", 527 Change: 12345, 528 Patchset: 5, 529 OwnerKind: pb.ChangelistOwnerKind_AUTOMATION, 530 }, 531 }, 532 }) 533 534 rdbSources := testresults.Sources{ 535 RefHash: pbutil.SourceRefHash(&pb.SourceRef{ 536 System: &pb.SourceRef_Gitiles{ 537 Gitiles: &pb.GitilesRef{ 538 Host: "project.googlesource.com", 539 Project: "myproject/src", 540 Ref: "refs/heads/main", 541 }, 542 }, 543 }), 544 Position: 16801, 545 Changelists: []testresults.Changelist{ 546 { 547 Host: "project-review.googlesource.com", 548 Change: 9991, 549 Patchset: 82, 550 OwnerKind: pb.ChangelistOwnerKind_HUMAN, 551 }, 552 }, 553 } 554 555 expectedTRs := []*testresults.TestResult{ 556 trBuilder.WithTestID("ninja://test_consistent_failure"). 557 WithVariantHash("hash"). 558 WithRunIndex(0). 559 WithResultIndex(0). 560 WithIsUnexpected(true). 561 WithStatus(pb.TestResultStatus_FAIL). 562 WithRunDuration(3*time.Second+1*time.Microsecond). 563 WithExonerationReasons(pb.ExonerationReason_OCCURS_ON_OTHER_CLS, pb.ExonerationReason_NOT_CRITICAL, pb.ExonerationReason_OCCURS_ON_MAINLINE). 564 WithIsFromBisection(false). 565 WithSources(rdbSources). 566 Build(), 567 trBuilder.WithTestID("ninja://test_expected"). 568 WithVariantHash("hash"). 569 WithRunIndex(0). 570 WithResultIndex(0). 571 WithIsUnexpected(false). 572 WithStatus(pb.TestResultStatus_PASS). 573 WithRunDuration(5 * time.Second). 574 WithoutExoneration(). 575 WithIsFromBisection(false). 576 Build(), 577 trBuilder.WithTestID("ninja://test_filtering_event"). 578 WithVariantHash("hash"). 579 WithRunIndex(0). 580 WithResultIndex(0). 581 WithIsUnexpected(false). 582 WithStatus(pb.TestResultStatus_SKIP). 583 WithoutRunDuration(). 584 WithoutExoneration(). 585 WithIsFromBisection(false). 586 Build(), 587 trBuilder.WithTestID("ninja://test_from_luci_bisection"). 588 WithVariantHash("hash"). 589 WithRunIndex(0). 590 WithResultIndex(0). 591 WithIsUnexpected(true). 592 WithStatus(pb.TestResultStatus_PASS). 593 WithoutRunDuration(). 594 WithoutExoneration(). 595 WithIsFromBisection(true). 596 Build(), 597 trBuilder.WithTestID("ninja://test_has_unexpected"). 598 WithVariantHash("hash"). 599 WithRunIndex(0). 600 WithResultIndex(0). 601 WithIsUnexpected(true). 602 WithStatus(pb.TestResultStatus_FAIL). 603 WithoutRunDuration(). 604 WithoutExoneration(). 605 WithIsFromBisection(false). 606 Build(), 607 trBuilder.WithTestID("ninja://test_has_unexpected"). 608 WithVariantHash("hash"). 609 WithRunIndex(1). 610 WithResultIndex(0). 611 WithIsUnexpected(false). 612 WithStatus(pb.TestResultStatus_PASS). 613 WithoutRunDuration(). 614 WithoutExoneration(). 615 WithIsFromBisection(false). 616 Build(), 617 trBuilder.WithTestID("ninja://test_known_flake"). 618 WithVariantHash("hash_2"). 619 WithRunIndex(0). 620 WithResultIndex(0). 621 WithIsUnexpected(true). 622 WithStatus(pb.TestResultStatus_FAIL). 623 WithRunDuration(2 * time.Second). 624 WithoutExoneration(). 625 WithIsFromBisection(false). 626 Build(), 627 trBuilder.WithTestID("ninja://test_new_failure"). 628 WithVariantHash("hash_1"). 629 WithRunIndex(0). 630 WithResultIndex(0). 631 WithIsUnexpected(true). 632 WithStatus(pb.TestResultStatus_FAIL). 633 WithRunDuration(1 * time.Second). 634 WithoutExoneration(). 635 WithIsFromBisection(false). 636 Build(), 637 trBuilder.WithTestID("ninja://test_new_flake"). 638 WithVariantHash("hash"). 639 WithRunIndex(0). 640 WithResultIndex(0). 641 WithIsUnexpected(true). 642 WithStatus(pb.TestResultStatus_FAIL). 643 WithRunDuration(10 * time.Second). 644 WithoutExoneration(). 645 WithIsFromBisection(false). 646 Build(), 647 trBuilder.WithTestID("ninja://test_new_flake"). 648 WithVariantHash("hash"). 649 WithRunIndex(0). 650 WithResultIndex(1). 651 WithIsUnexpected(true). 652 WithStatus(pb.TestResultStatus_FAIL). 653 WithRunDuration(11 * time.Second). 654 WithoutExoneration(). 655 WithIsFromBisection(false). 656 Build(), 657 trBuilder.WithTestID("ninja://test_new_flake"). 658 WithVariantHash("hash"). 659 WithRunIndex(1). 660 WithResultIndex(0). 661 WithIsUnexpected(false). 662 WithStatus(pb.TestResultStatus_PASS). 663 WithRunDuration(12 * time.Second). 664 WithoutExoneration(). 665 WithIsFromBisection(false). 666 Build(), 667 trBuilder.WithTestID("ninja://test_no_new_results"). 668 WithVariantHash("hash"). 669 WithRunIndex(0). 670 WithResultIndex(0). 671 WithIsUnexpected(true). 672 WithStatus(pb.TestResultStatus_FAIL). 673 WithRunDuration(4 * time.Second). 674 WithoutExoneration(). 675 WithIsFromBisection(false). 676 Build(), 677 trBuilder.WithTestID("ninja://test_skip"). 678 WithVariantHash("hash"). 679 WithRunIndex(0). 680 WithResultIndex(0). 681 WithIsUnexpected(true). 682 WithStatus(pb.TestResultStatus_SKIP). 683 WithoutRunDuration(). 684 WithoutExoneration(). 685 WithIsFromBisection(false). 686 Build(), 687 trBuilder.WithTestID("ninja://test_unexpected_pass"). 688 WithVariantHash("hash"). 689 WithRunIndex(0). 690 WithResultIndex(0). 691 WithIsUnexpected(true). 692 WithStatus(pb.TestResultStatus_PASS). 693 WithoutRunDuration(). 694 WithoutExoneration(). 695 WithIsFromBisection(false). 696 Build(), 697 } 698 699 // Validate TestResults table is populated. 700 var actualTRs []*testresults.TestResult 701 err := testresults.ReadTestResults(span.Single(ctx), spanner.AllKeys(), func(tr *testresults.TestResult) error { 702 actualTRs = append(actualTRs, tr) 703 return nil 704 }) 705 So(err, ShouldBeNil) 706 So(actualTRs, ShouldResemble, expectedTRs) 707 708 // Validate TestVariantRealms table is populated. 709 tvrs := make([]*testresults.TestVariantRealm, 0) 710 err = testresults.ReadTestVariantRealms(span.Single(ctx), spanner.AllKeys(), func(tvr *testresults.TestVariantRealm) error { 711 tvrs = append(tvrs, tvr) 712 return nil 713 }) 714 So(err, ShouldBeNil) 715 716 expectedRealms := []*testresults.TestVariantRealm{ 717 { 718 Project: "project", 719 TestID: "ninja://test_consistent_failure", 720 VariantHash: "hash", 721 SubRealm: "ci", 722 Variant: nil, 723 }, 724 { 725 Project: "project", 726 TestID: "ninja://test_expected", 727 VariantHash: "hash", 728 SubRealm: "ci", 729 Variant: nil, 730 }, 731 { 732 Project: "project", 733 TestID: "ninja://test_filtering_event", 734 VariantHash: "hash", 735 SubRealm: "ci", 736 Variant: nil, 737 }, 738 { 739 Project: "project", 740 TestID: "ninja://test_from_luci_bisection", 741 VariantHash: "hash", 742 SubRealm: "ci", 743 Variant: nil, 744 }, 745 { 746 Project: "project", 747 TestID: "ninja://test_has_unexpected", 748 VariantHash: "hash", 749 SubRealm: "ci", 750 Variant: nil, 751 }, 752 { 753 Project: "project", 754 TestID: "ninja://test_known_flake", 755 VariantHash: "hash_2", 756 SubRealm: "ci", 757 Variant: pbutil.VariantFromResultDB(rdbpbutil.Variant("k1", "v2")), 758 }, 759 { 760 Project: "project", 761 TestID: "ninja://test_new_failure", 762 VariantHash: "hash_1", 763 SubRealm: "ci", 764 Variant: pbutil.VariantFromResultDB(rdbpbutil.Variant("k1", "v1")), 765 }, 766 { 767 Project: "project", 768 TestID: "ninja://test_new_flake", 769 VariantHash: "hash", 770 SubRealm: "ci", 771 Variant: nil, 772 }, 773 { 774 Project: "project", 775 TestID: "ninja://test_no_new_results", 776 VariantHash: "hash", 777 SubRealm: "ci", 778 Variant: nil, 779 }, 780 { 781 Project: "project", 782 TestID: "ninja://test_skip", 783 VariantHash: "hash", 784 SubRealm: "ci", 785 Variant: nil, 786 }, 787 { 788 Project: "project", 789 TestID: "ninja://test_unexpected_pass", 790 VariantHash: "hash", 791 SubRealm: "ci", 792 Variant: nil, 793 }, 794 } 795 796 So(tvrs, ShouldHaveLength, len(expectedRealms)) 797 for i, tvr := range tvrs { 798 expectedTVR := expectedRealms[i] 799 So(tvr.LastIngestionTime, ShouldNotBeZeroValue) 800 expectedTVR.LastIngestionTime = tvr.LastIngestionTime 801 So(tvr, ShouldResemble, expectedTVR) 802 } 803 804 // Validate TestRealms table is populated. 805 testRealms := make([]*testresults.TestRealm, 0) 806 err = testresults.ReadTestRealms(span.Single(ctx), spanner.AllKeys(), func(tvr *testresults.TestRealm) error { 807 testRealms = append(testRealms, tvr) 808 return nil 809 }) 810 So(err, ShouldBeNil) 811 812 // The order of test realms doesn't matter. Sort it to make comparing it 813 // against the expected test realm list easier. 814 sort.Slice(testRealms, func(i, j int) bool { 815 item1 := testRealms[i] 816 item2 := testRealms[j] 817 if item1.Project != item2.Project { 818 return item1.Project <= item2.Project 819 } 820 if item1.SubRealm != item2.SubRealm { 821 return item1.SubRealm <= item2.SubRealm 822 } 823 if item1.TestID != item2.TestID { 824 return item1.TestID <= item2.TestID 825 } 826 return false 827 }) 828 829 expectedTestRealms := []*testresults.TestRealm{ 830 { 831 Project: "project", 832 TestID: "ninja://test_consistent_failure", 833 SubRealm: "ci", 834 }, 835 { 836 Project: "project", 837 TestID: "ninja://test_expected", 838 SubRealm: "ci", 839 }, 840 { 841 Project: "project", 842 TestID: "ninja://test_filtering_event", 843 SubRealm: "ci", 844 }, 845 { 846 Project: "project", 847 TestID: "ninja://test_from_luci_bisection", 848 SubRealm: "ci", 849 }, 850 { 851 Project: "project", 852 TestID: "ninja://test_has_unexpected", 853 SubRealm: "ci", 854 }, 855 { 856 Project: "project", 857 TestID: "ninja://test_known_flake", 858 SubRealm: "ci", 859 }, 860 { 861 Project: "project", 862 TestID: "ninja://test_new_failure", 863 SubRealm: "ci", 864 }, 865 { 866 Project: "project", 867 TestID: "ninja://test_new_flake", 868 SubRealm: "ci", 869 }, 870 { 871 Project: "project", 872 TestID: "ninja://test_no_new_results", 873 SubRealm: "ci", 874 }, 875 { 876 Project: "project", 877 TestID: "ninja://test_skip", 878 SubRealm: "ci", 879 }, 880 { 881 Project: "project", 882 TestID: "ninja://test_unexpected_pass", 883 SubRealm: "ci", 884 }, 885 } 886 887 So(testRealms, ShouldHaveLength, len(expectedTestRealms)) 888 for i, tr := range testRealms { 889 expectedTR := expectedTestRealms[i] 890 So(tr.LastIngestionTime, ShouldNotBeZeroValue) 891 expectedTR.LastIngestionTime = tr.LastIngestionTime 892 So(tr, ShouldResemble, expectedTR) 893 } 894 } 895 896 func verifyClustering(chunkStore *chunkstore.FakeClient, clusteredFailures *clusteredfailures.FakeClient) { 897 // Confirm chunks have been written to GCS. 898 So(len(chunkStore.Contents), ShouldEqual, 1) 899 900 // Confirm clustering has occurred, with each test result in at 901 // least one cluster. 902 actualClusteredFailures := make(map[string]int) 903 for _, f := range clusteredFailures.Insertions { 904 So(f.Project, ShouldEqual, "project") 905 actualClusteredFailures[f.TestId] += 1 906 } 907 expectedClusteredFailures := map[string]int{ 908 "ninja://test_new_failure": 1, 909 "ninja://test_known_flake": 1, 910 "ninja://test_consistent_failure": 2, // One failure is in two clusters due it having a failure reason. 911 "ninja://test_no_new_results": 1, 912 "ninja://test_new_flake": 2, 913 "ninja://test_has_unexpected": 1, 914 } 915 So(actualClusteredFailures, ShouldResemble, expectedClusteredFailures) 916 917 for _, cf := range clusteredFailures.Insertions { 918 So(cf.BuildGardenerRotations, ShouldResemble, []string{"rotation1", "rotation2"}) 919 920 // Verify test variant branch stats were correctly populated. 921 if cf.TestId == "ninja://test_consistent_failure" { 922 So(cf.TestVariantBranch, ShouldResembleProto, &bqpb.ClusteredFailureRow_TestVariantBranch{ 923 UnexpectedVerdicts_24H: 124, 924 FlakyVerdicts_24H: 456, 925 TotalVerdicts_24H: 2000, 926 }) 927 } else { 928 So(cf.TestVariantBranch, ShouldBeNil) 929 } 930 } 931 } 932 933 func verifyTestVerdicts(client *testverdicts.FakeClient, expectedPartitionTime time.Time) { 934 actualRows := client.Insertions 935 936 invocation := &bqpb.TestVerdictRow_InvocationRecord{ 937 Id: "build-87654321", 938 Realm: "project:ci", 939 Properties: "{}", 940 } 941 942 testMetadata := &pb.TestMetadata{ 943 Name: "updated_name", 944 Location: &pb.TestLocation{ 945 Repo: "repo", 946 FileName: "file_name", 947 Line: 456, 948 }, 949 BugComponent: &pb.BugComponent{ 950 System: &pb.BugComponent_IssueTracker{ 951 IssueTracker: &pb.IssueTrackerComponent{ 952 ComponentId: 12345, 953 }, 954 }, 955 }, 956 } 957 958 buildbucketBuild := &bqpb.TestVerdictRow_BuildbucketBuild{ 959 Id: testBuildID, 960 Builder: &bqpb.TestVerdictRow_BuildbucketBuild_Builder{ 961 Project: "project", 962 Bucket: "bucket", 963 Builder: "builder", 964 }, 965 Status: "FAILURE", 966 GardenerRotations: []string{"rotation1", "rotation2"}, 967 } 968 969 cvRun := &bqpb.TestVerdictRow_ChangeVerifierRun{ 970 Id: "infra/12345", 971 Mode: pb.PresubmitRunMode_FULL_RUN, 972 Status: "SUCCEEDED", 973 IsBuildCritical: false, 974 } 975 976 // Different platforms may use different spacing when serializing 977 // JSONPB. Expect the spacing scheme used by this platform. 978 expectedProperties, err := testverdicts.MarshalStructPB(testProperties) 979 So(err, ShouldBeNil) 980 981 sr := &pb.SourceRef{ 982 System: &pb.SourceRef_Gitiles{ 983 Gitiles: &pb.GitilesRef{ 984 Host: "project.googlesource.com", 985 Project: "myproject/src", 986 Ref: "refs/heads/main", 987 }, 988 }, 989 } 990 expectedRows := []*bqpb.TestVerdictRow{ 991 { 992 Project: "project", 993 TestId: "ninja://test_consistent_failure", 994 Variant: "{}", 995 VariantHash: "hash", 996 Invocation: invocation, 997 PartitionTime: timestamppb.New(expectedPartitionTime), 998 Status: pb.TestVerdictStatus_EXONERATED, 999 Results: []*bqpb.TestVerdictRow_TestResult{ 1000 { 1001 Parent: &bqpb.TestVerdictRow_ParentInvocationRecord{ 1002 Id: "build-1234", 1003 }, 1004 Name: "invocations/build-1234/tests/ninja%3A%2F%2Ftest_consistent_failure/results/one", 1005 ResultId: "one", 1006 Expected: false, 1007 Status: pb.TestResultStatus_FAIL, 1008 SummaryHtml: "SummaryHTML", 1009 StartTime: timestamppb.New(time.Date(2010, time.March, 1, 0, 0, 0, 0, time.UTC)), 1010 Duration: 3.000001, 1011 FailureReason: &pb.FailureReason{ 1012 PrimaryErrorMessage: "abc.def(123): unexpected nil-deference", 1013 }, 1014 Properties: expectedProperties, 1015 }, 1016 }, 1017 Exonerations: []*bqpb.TestVerdictRow_Exoneration{ 1018 { 1019 ExplanationHtml: "LUCI Analysis reported this test as flaky.", 1020 Reason: pb.ExonerationReason_OCCURS_ON_OTHER_CLS, 1021 }, 1022 { 1023 ExplanationHtml: "Test is marked informational.", 1024 Reason: pb.ExonerationReason_NOT_CRITICAL, 1025 }, 1026 { 1027 Reason: pb.ExonerationReason_OCCURS_ON_MAINLINE, 1028 }, 1029 }, 1030 Counts: &bqpb.TestVerdictRow_Counts{ 1031 Total: 1, TotalNonSkipped: 1, Unexpected: 1, UnexpectedNonSkipped: 1, UnexpectedNonSkippedNonPassed: 1, 1032 }, 1033 BuildbucketBuild: buildbucketBuild, 1034 ChangeVerifierRun: cvRun, 1035 Sources: &pb.Sources{ 1036 GitilesCommit: &pb.GitilesCommit{ 1037 Host: "project.googlesource.com", 1038 Project: "myproject/src", 1039 Ref: "refs/heads/main", 1040 CommitHash: "abcdefabcd1234567890abcdefabcd1234567890", 1041 Position: 16801, 1042 }, 1043 Changelists: []*pb.GerritChange{ 1044 { 1045 Host: "project-review.googlesource.com", 1046 Project: "myproject/src2", 1047 Change: 9991, 1048 Patchset: 82, 1049 OwnerKind: pb.ChangelistOwnerKind_HUMAN, 1050 }, 1051 }, 1052 IsDirty: false, 1053 }, 1054 SourceRef: sr, 1055 SourceRefHash: hex.EncodeToString(pbutil.SourceRefHash(sr)), 1056 }, 1057 { 1058 Project: "project", 1059 TestId: "ninja://test_expected", 1060 Variant: "{}", 1061 VariantHash: "hash", 1062 Invocation: invocation, 1063 PartitionTime: timestamppb.New(expectedPartitionTime), 1064 Status: pb.TestVerdictStatus_EXPECTED, 1065 Results: []*bqpb.TestVerdictRow_TestResult{ 1066 { 1067 Parent: &bqpb.TestVerdictRow_ParentInvocationRecord{ 1068 Id: "build-1234", 1069 }, 1070 Name: "invocations/build-1234/tests/ninja%3A%2F%2Ftest_expected/results/one", 1071 ResultId: "one", 1072 StartTime: timestamppb.New(time.Date(2010, time.May, 1, 0, 0, 0, 0, time.UTC)), 1073 Status: pb.TestResultStatus_PASS, 1074 Expected: true, 1075 Duration: 5.0, 1076 Properties: "{}", 1077 }, 1078 }, 1079 Counts: &bqpb.TestVerdictRow_Counts{ 1080 Total: 1, TotalNonSkipped: 1, Unexpected: 0, UnexpectedNonSkipped: 0, UnexpectedNonSkippedNonPassed: 0, 1081 }, 1082 BuildbucketBuild: buildbucketBuild, 1083 ChangeVerifierRun: cvRun, 1084 }, 1085 { 1086 Project: "project", 1087 TestId: "ninja://test_filtering_event", 1088 Variant: "{}", 1089 VariantHash: "hash", 1090 Invocation: invocation, 1091 PartitionTime: timestamppb.New(expectedPartitionTime), 1092 Status: pb.TestVerdictStatus_EXPECTED, 1093 Results: []*bqpb.TestVerdictRow_TestResult{ 1094 { 1095 Parent: &bqpb.TestVerdictRow_ParentInvocationRecord{ 1096 Id: "build-1234", 1097 }, 1098 Name: "invocations/build-1234/tests/ninja%3A%2F%2Ftest_filtering_event/results/one", 1099 ResultId: "one", 1100 StartTime: timestamppb.New(time.Date(2010, time.February, 2, 0, 0, 0, 0, time.UTC)), 1101 Status: pb.TestResultStatus_SKIP, 1102 SkipReason: pb.SkipReason_AUTOMATICALLY_DISABLED_FOR_FLAKINESS.String(), 1103 Expected: true, 1104 Properties: "{}", 1105 }, 1106 }, 1107 Counts: &bqpb.TestVerdictRow_Counts{ 1108 Total: 1, TotalNonSkipped: 0, Unexpected: 0, UnexpectedNonSkipped: 0, UnexpectedNonSkippedNonPassed: 0, 1109 }, 1110 BuildbucketBuild: buildbucketBuild, 1111 ChangeVerifierRun: cvRun, 1112 }, 1113 { 1114 Project: "project", 1115 TestId: "ninja://test_from_luci_bisection", 1116 Variant: "{}", 1117 VariantHash: "hash", 1118 Invocation: invocation, 1119 PartitionTime: timestamppb.New(expectedPartitionTime), 1120 Status: pb.TestVerdictStatus_UNEXPECTED, 1121 Results: []*bqpb.TestVerdictRow_TestResult{ 1122 { 1123 Parent: &bqpb.TestVerdictRow_ParentInvocationRecord{ 1124 Id: "build-1234", 1125 }, 1126 Name: "invocations/build-1234/tests/ninja%3A%2F%2Ftest_from_luci_bisection/results/one", 1127 ResultId: "one", 1128 Status: pb.TestResultStatus_PASS, 1129 Expected: false, 1130 Properties: "{}", 1131 Tags: []*pb.StringPair{{Key: "is_luci_bisection", Value: "true"}}, 1132 }, 1133 }, 1134 Counts: &bqpb.TestVerdictRow_Counts{ 1135 Total: 1, TotalNonSkipped: 1, Unexpected: 1, UnexpectedNonSkipped: 1, UnexpectedNonSkippedNonPassed: 0, 1136 }, 1137 BuildbucketBuild: buildbucketBuild, 1138 ChangeVerifierRun: cvRun, 1139 }, 1140 { 1141 Project: "project", 1142 TestId: "ninja://test_has_unexpected", 1143 VariantHash: "hash", 1144 Variant: "{}", 1145 Invocation: invocation, 1146 PartitionTime: timestamppb.New(expectedPartitionTime), 1147 Status: pb.TestVerdictStatus_FLAKY, 1148 Results: []*bqpb.TestVerdictRow_TestResult{ 1149 { 1150 Parent: &bqpb.TestVerdictRow_ParentInvocationRecord{ 1151 Id: "invocation-0b", 1152 }, 1153 Name: "invocations/invocation-0b/tests/ninja%3A%2F%2Ftest_has_unexpected/results/one", 1154 ResultId: "one", 1155 StartTime: timestamppb.New(time.Date(2010, time.February, 1, 0, 0, 10, 0, time.UTC)), 1156 Status: pb.TestResultStatus_FAIL, 1157 Expected: false, 1158 Properties: "{}", 1159 }, 1160 { 1161 Parent: &bqpb.TestVerdictRow_ParentInvocationRecord{ 1162 Id: "invocation-0a", 1163 }, 1164 Name: "invocations/invocation-0a/tests/ninja%3A%2F%2Ftest_has_unexpected/results/two", 1165 ResultId: "two", 1166 StartTime: timestamppb.New(time.Date(2010, time.February, 1, 0, 0, 20, 0, time.UTC)), 1167 Status: pb.TestResultStatus_PASS, 1168 Expected: true, 1169 Properties: "{}", 1170 }, 1171 }, 1172 Counts: &bqpb.TestVerdictRow_Counts{ 1173 Total: 2, TotalNonSkipped: 2, Unexpected: 1, UnexpectedNonSkipped: 1, UnexpectedNonSkippedNonPassed: 1, 1174 }, 1175 BuildbucketBuild: buildbucketBuild, 1176 ChangeVerifierRun: cvRun, 1177 }, 1178 { 1179 Project: "project", 1180 TestId: "ninja://test_known_flake", 1181 Variant: `{"k1":"v2"}`, 1182 VariantHash: "hash_2", 1183 Invocation: invocation, 1184 PartitionTime: timestamppb.New(expectedPartitionTime), 1185 Status: pb.TestVerdictStatus_UNEXPECTED, 1186 TestMetadata: testMetadata, 1187 Results: []*bqpb.TestVerdictRow_TestResult{ 1188 { 1189 Parent: &bqpb.TestVerdictRow_ParentInvocationRecord{ 1190 Id: "build-1234", 1191 }, 1192 Name: "invocations/build-1234/tests/ninja%3A%2F%2Ftest_known_flake/results/one", 1193 ResultId: "one", 1194 StartTime: timestamppb.New(time.Date(2010, time.February, 1, 0, 0, 0, 0, time.UTC)), 1195 Status: pb.TestResultStatus_FAIL, 1196 Expected: false, 1197 Duration: 2.0, 1198 Tags: pbutil.StringPairs("os", "Mac", "monorail_component", "Monorail>Component"), 1199 Properties: "{}", 1200 }, 1201 }, 1202 Counts: &bqpb.TestVerdictRow_Counts{ 1203 Total: 1, TotalNonSkipped: 1, Unexpected: 1, UnexpectedNonSkipped: 1, UnexpectedNonSkippedNonPassed: 1, 1204 }, 1205 BuildbucketBuild: buildbucketBuild, 1206 ChangeVerifierRun: cvRun, 1207 }, 1208 { 1209 Project: "project", 1210 TestId: "ninja://test_new_failure", 1211 Variant: `{"k1":"v1"}`, 1212 VariantHash: "hash_1", 1213 Invocation: invocation, 1214 PartitionTime: timestamppb.New(expectedPartitionTime), 1215 Status: pb.TestVerdictStatus_UNEXPECTED, 1216 TestMetadata: testMetadata, 1217 Results: []*bqpb.TestVerdictRow_TestResult{ 1218 { 1219 Parent: &bqpb.TestVerdictRow_ParentInvocationRecord{ 1220 Id: "build-1234", 1221 }, 1222 Name: "invocations/build-1234/tests/ninja%3A%2F%2Ftest_new_failure/results/one", 1223 ResultId: "one", 1224 StartTime: timestamppb.New(time.Date(2010, time.January, 1, 0, 0, 0, 0, time.UTC)), 1225 Status: pb.TestResultStatus_FAIL, 1226 Expected: false, 1227 Duration: 1.0, 1228 Tags: pbutil.StringPairs("random_tag", "random_tag_value", "public_buganizer_component", "951951951"), 1229 Properties: "{}", 1230 }, 1231 }, 1232 Counts: &bqpb.TestVerdictRow_Counts{ 1233 Total: 1, TotalNonSkipped: 1, Unexpected: 1, UnexpectedNonSkipped: 1, UnexpectedNonSkippedNonPassed: 1, 1234 }, 1235 BuildbucketBuild: buildbucketBuild, 1236 ChangeVerifierRun: cvRun, 1237 }, 1238 { 1239 Project: "project", 1240 TestId: "ninja://test_new_flake", 1241 Variant: "{}", 1242 VariantHash: "hash", 1243 Invocation: invocation, 1244 PartitionTime: timestamppb.New(expectedPartitionTime), 1245 Status: pb.TestVerdictStatus_FLAKY, 1246 Results: []*bqpb.TestVerdictRow_TestResult{ 1247 { 1248 Parent: &bqpb.TestVerdictRow_ParentInvocationRecord{ 1249 Id: "invocation-1234", 1250 }, 1251 Name: "invocations/invocation-1234/tests/ninja%3A%2F%2Ftest_new_flake/results/two", 1252 ResultId: "two", 1253 StartTime: timestamppb.New(time.Date(2010, time.January, 1, 0, 0, 20, 0, time.UTC)), 1254 Status: pb.TestResultStatus_FAIL, 1255 Expected: false, 1256 Duration: 11.0, 1257 Properties: "{}", 1258 }, 1259 { 1260 Parent: &bqpb.TestVerdictRow_ParentInvocationRecord{ 1261 Id: "invocation-1234", 1262 }, 1263 Name: "invocations/invocation-1234/tests/ninja%3A%2F%2Ftest_new_flake/results/one", 1264 ResultId: "one", 1265 StartTime: timestamppb.New(time.Date(2010, time.January, 1, 0, 0, 10, 0, time.UTC)), 1266 Status: pb.TestResultStatus_FAIL, 1267 Expected: false, 1268 Duration: 10.0, 1269 Properties: "{}", 1270 }, 1271 { 1272 Parent: &bqpb.TestVerdictRow_ParentInvocationRecord{ 1273 Id: "invocation-4567", 1274 }, 1275 Name: "invocations/invocation-4567/tests/ninja%3A%2F%2Ftest_new_flake/results/three", 1276 ResultId: "three", 1277 StartTime: timestamppb.New(time.Date(2010, time.January, 1, 0, 0, 15, 0, time.UTC)), 1278 Status: pb.TestResultStatus_PASS, 1279 Expected: true, 1280 Duration: 12.0, 1281 Properties: "{}", 1282 }, 1283 }, 1284 Counts: &bqpb.TestVerdictRow_Counts{ 1285 Total: 3, TotalNonSkipped: 3, Unexpected: 2, UnexpectedNonSkipped: 2, UnexpectedNonSkippedNonPassed: 2, 1286 }, 1287 BuildbucketBuild: buildbucketBuild, 1288 ChangeVerifierRun: cvRun, 1289 }, 1290 { 1291 Project: "project", 1292 TestId: "ninja://test_no_new_results", 1293 Variant: "{}", 1294 VariantHash: "hash", 1295 Invocation: invocation, 1296 PartitionTime: timestamppb.New(expectedPartitionTime), 1297 Status: pb.TestVerdictStatus_UNEXPECTED, 1298 Results: []*bqpb.TestVerdictRow_TestResult{ 1299 { 1300 Parent: &bqpb.TestVerdictRow_ParentInvocationRecord{ 1301 Id: "build-1234", 1302 }, 1303 Name: "invocations/build-1234/tests/ninja%3A%2F%2Ftest_no_new_results/results/one", 1304 ResultId: "one", 1305 StartTime: timestamppb.New(time.Date(2010, time.April, 1, 0, 0, 0, 0, time.UTC)), 1306 Status: pb.TestResultStatus_FAIL, 1307 Expected: false, 1308 Duration: 4.0, 1309 Properties: "{}", 1310 }, 1311 }, 1312 Counts: &bqpb.TestVerdictRow_Counts{ 1313 Total: 1, TotalNonSkipped: 1, Unexpected: 1, UnexpectedNonSkipped: 1, UnexpectedNonSkippedNonPassed: 1, 1314 }, 1315 BuildbucketBuild: buildbucketBuild, 1316 ChangeVerifierRun: cvRun, 1317 }, 1318 { 1319 Project: "project", 1320 TestId: "ninja://test_skip", 1321 Variant: "{}", 1322 VariantHash: "hash", 1323 Invocation: invocation, 1324 PartitionTime: timestamppb.New(expectedPartitionTime), 1325 Status: pb.TestVerdictStatus_UNEXPECTEDLY_SKIPPED, 1326 Results: []*bqpb.TestVerdictRow_TestResult{ 1327 { 1328 Parent: &bqpb.TestVerdictRow_ParentInvocationRecord{ 1329 Id: "build-1234", 1330 }, 1331 Name: "invocations/build-1234/tests/ninja%3A%2F%2Ftest_skip/results/one", 1332 ResultId: "one", 1333 StartTime: timestamppb.New(time.Date(2010, time.February, 2, 0, 0, 0, 0, time.UTC)), 1334 Status: pb.TestResultStatus_SKIP, 1335 Expected: false, 1336 Properties: "{}", 1337 }, 1338 }, 1339 Counts: &bqpb.TestVerdictRow_Counts{ 1340 Total: 1, TotalNonSkipped: 0, Unexpected: 1, UnexpectedNonSkipped: 0, UnexpectedNonSkippedNonPassed: 0, 1341 }, 1342 BuildbucketBuild: buildbucketBuild, 1343 ChangeVerifierRun: cvRun, 1344 }, 1345 { 1346 Project: "project", 1347 TestId: "ninja://test_unexpected_pass", 1348 Variant: "{}", 1349 VariantHash: "hash", 1350 Invocation: invocation, 1351 PartitionTime: timestamppb.New(expectedPartitionTime), 1352 Status: pb.TestVerdictStatus_UNEXPECTED, 1353 Results: []*bqpb.TestVerdictRow_TestResult{ 1354 { 1355 Parent: &bqpb.TestVerdictRow_ParentInvocationRecord{ 1356 Id: "build-1234", 1357 }, 1358 Name: "invocations/build-1234/tests/ninja%3A%2F%2Ftest_unexpected_pass/results/one", 1359 ResultId: "one", 1360 Status: pb.TestResultStatus_PASS, 1361 Expected: false, 1362 Properties: "{}", 1363 }, 1364 }, 1365 Counts: &bqpb.TestVerdictRow_Counts{ 1366 Total: 1, TotalNonSkipped: 1, Unexpected: 1, UnexpectedNonSkipped: 1, UnexpectedNonSkippedNonPassed: 0, 1367 }, 1368 BuildbucketBuild: buildbucketBuild, 1369 ChangeVerifierRun: cvRun, 1370 }, 1371 } 1372 So(actualRows, ShouldHaveLength, len(expectedRows)) 1373 for i, row := range actualRows { 1374 So(row, ShouldResembleProto, expectedRows[i]) 1375 } 1376 } 1377 1378 func verifyContinuationTask(skdr *tqtesting.Scheduler, expectedContinuation *taskspb.IngestTestResults) { 1379 count := 0 1380 for _, pl := range skdr.Tasks().Payloads() { 1381 if pl, ok := pl.(*taskspb.IngestTestResults); ok { 1382 So(pl, ShouldResembleProto, expectedContinuation) 1383 count++ 1384 } 1385 } 1386 if expectedContinuation != nil { 1387 So(count, ShouldEqual, 1) 1388 } else { 1389 So(count, ShouldEqual, 0) 1390 } 1391 } 1392 1393 func verifyIngestionControl(ctx context.Context, expected *control.Entry) { 1394 actual, err := control.Read(span.Single(ctx), []string{expected.BuildID}) 1395 So(err, ShouldBeNil) 1396 So(actual, ShouldHaveLength, 1) 1397 a := *actual[0] 1398 e := *expected 1399 1400 // Compare protos separately, as they are not compared 1401 // correctly by ShouldResemble. 1402 So(a.PresubmitResult, ShouldResembleProto, e.PresubmitResult) 1403 a.PresubmitResult = nil 1404 e.PresubmitResult = nil 1405 1406 So(a.BuildResult, ShouldResembleProto, e.BuildResult) 1407 a.BuildResult = nil 1408 e.BuildResult = nil 1409 1410 So(a.InvocationResult, ShouldResembleProto, e.InvocationResult) 1411 a.InvocationResult = nil 1412 e.InvocationResult = nil 1413 1414 // Do not compare last updated time, as it is determined 1415 // by commit timestamp. 1416 So(a.LastUpdated, ShouldNotBeEmpty) 1417 e.LastUpdated = a.LastUpdated 1418 1419 So(a, ShouldResemble, e) 1420 }