go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/clustering/ingestion/ingest_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 ingestion 16 17 import ( 18 "encoding/hex" 19 "fmt" 20 "sort" 21 "testing" 22 "time" 23 24 "google.golang.org/protobuf/proto" 25 "google.golang.org/protobuf/types/known/durationpb" 26 "google.golang.org/protobuf/types/known/timestamppb" 27 28 "go.chromium.org/luci/gae/impl/memory" 29 rdbpb "go.chromium.org/luci/resultdb/proto/v1" 30 "go.chromium.org/luci/server/caching" 31 32 "go.chromium.org/luci/analysis/internal/analysis" 33 "go.chromium.org/luci/analysis/internal/analysis/clusteredfailures" 34 "go.chromium.org/luci/analysis/internal/clustering" 35 "go.chromium.org/luci/analysis/internal/clustering/algorithms" 36 "go.chromium.org/luci/analysis/internal/clustering/algorithms/failurereason" 37 "go.chromium.org/luci/analysis/internal/clustering/algorithms/rulesalgorithm" 38 "go.chromium.org/luci/analysis/internal/clustering/algorithms/testname" 39 "go.chromium.org/luci/analysis/internal/clustering/chunkstore" 40 clusteringpb "go.chromium.org/luci/analysis/internal/clustering/proto" 41 "go.chromium.org/luci/analysis/internal/clustering/rules" 42 "go.chromium.org/luci/analysis/internal/config" 43 "go.chromium.org/luci/analysis/internal/config/compiledcfg" 44 "go.chromium.org/luci/analysis/internal/testutil" 45 bqpb "go.chromium.org/luci/analysis/proto/bq" 46 configpb "go.chromium.org/luci/analysis/proto/config" 47 pb "go.chromium.org/luci/analysis/proto/v1" 48 49 . "github.com/smartystreets/goconvey/convey" 50 . "go.chromium.org/luci/common/testing/assertions" 51 ) 52 53 func TestIngest(t *testing.T) { 54 Convey(`With Ingestor`, t, func() { 55 ctx := testutil.IntegrationTestContext(t) 56 ctx = caching.WithEmptyProcessCache(ctx) // For rules cache. 57 ctx = memory.Use(ctx) // For project config in datastore. 58 59 chunkStore := chunkstore.NewFakeClient() 60 clusteredFailures := clusteredfailures.NewFakeClient() 61 analysis := analysis.NewClusteringHandler(clusteredFailures) 62 ingestor := New(chunkStore, analysis) 63 64 opts := Options{ 65 TaskIndex: 1, 66 Project: "chromium", 67 PartitionTime: time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC), 68 Realm: "chromium:ci", 69 InvocationID: "build-123456790123456", 70 PresubmitRun: &PresubmitRun{ 71 ID: &pb.PresubmitRunId{System: "luci-cv", Id: "cq-run-123"}, 72 Owner: "automation", 73 Mode: pb.PresubmitRunMode_FULL_RUN, 74 Status: pb.PresubmitRunStatus_PRESUBMIT_RUN_STATUS_FAILED, 75 }, 76 BuildStatus: pb.BuildStatus_BUILD_STATUS_FAILURE, 77 BuildCritical: true, 78 BuildGardenerRotations: []string{"gardener-rotation1", "gardener-rotation2"}, 79 } 80 testIngestion := func(input []TestVerdict, expectedCFs []*bqpb.ClusteredFailureRow) { 81 err := ingestor.Ingest(ctx, opts, input) 82 So(err, ShouldBeNil) 83 84 insertions := clusteredFailures.Insertions 85 So(len(insertions), ShouldEqual, len(expectedCFs)) 86 87 // Sort both actuals and expectations by key so that we compare corresponding rows. 88 sortClusteredFailures(insertions) 89 sortClusteredFailures(expectedCFs) 90 for i, exp := range expectedCFs { 91 actual := insertions[i] 92 So(actual, ShouldNotBeNil) 93 94 // Chunk ID and index is assigned by ingestion. 95 copyExp := proto.Clone(exp).(*bqpb.ClusteredFailureRow) 96 So(actual.ChunkId, ShouldNotBeEmpty) 97 So(actual.ChunkIndex, ShouldBeGreaterThanOrEqualTo, 1) 98 copyExp.ChunkId = actual.ChunkId 99 copyExp.ChunkIndex = actual.ChunkIndex 100 101 // LastUpdated time is assigned by Spanner. 102 So(actual.LastUpdated, ShouldNotBeZeroValue) 103 copyExp.LastUpdated = actual.LastUpdated 104 105 So(actual, ShouldResembleProto, copyExp) 106 } 107 } 108 109 // This rule should match failures used in this test. 110 rule := rules.NewRule(100).WithProject(opts.Project).WithRuleDefinition(`reason LIKE "Failure reason%"`).Build() 111 err := rules.SetForTesting(ctx, []*rules.Entry{ 112 rule, 113 }) 114 So(err, ShouldBeNil) 115 116 // Setup clustering configuration 117 projectCfg := &configpb.ProjectConfig{ 118 Clustering: algorithms.TestClusteringConfig(), 119 LastUpdated: timestamppb.New(time.Date(2020, time.January, 5, 0, 0, 0, 1, time.UTC)), 120 } 121 projectCfgs := map[string]*configpb.ProjectConfig{ 122 "chromium": projectCfg, 123 } 124 So(config.SetTestProjectConfig(ctx, projectCfgs), ShouldBeNil) 125 126 cfg, err := compiledcfg.NewConfig(projectCfg) 127 So(err, ShouldBeNil) 128 129 Convey(`Ingest one failure`, func() { 130 const uniqifier = 1 131 const testRunCount = 1 132 const resultsPerTestRun = 1 133 tv := newTestVerdict(uniqifier, testRunCount, resultsPerTestRun, nil) 134 tvs := []TestVerdict{tv} 135 136 // Expect the test result to be clustered by both reason and test name. 137 const testRunNum = 0 138 const resultNum = 0 139 regexpCF := expectedClusteredFailure(uniqifier, testRunCount, testRunNum, resultsPerTestRun, resultNum) 140 setRegexpClustered(cfg, regexpCF) 141 testnameCF := expectedClusteredFailure(uniqifier, testRunCount, testRunNum, resultsPerTestRun, resultNum) 142 setTestNameClustered(cfg, testnameCF) 143 ruleCF := expectedClusteredFailure(uniqifier, testRunCount, testRunNum, resultsPerTestRun, resultNum) 144 setRuleClustered(ruleCF, rule) 145 expectedCFs := []*bqpb.ClusteredFailureRow{regexpCF, testnameCF, ruleCF} 146 147 Convey(`Unexpected failure`, func() { 148 tv.Verdict.Results[0].Result.Status = rdbpb.TestStatus_FAIL 149 tv.Verdict.Results[0].Result.Expected = false 150 151 testIngestion(tvs, expectedCFs) 152 So(len(chunkStore.Contents), ShouldEqual, 1) 153 }) 154 Convey(`Expected failure`, func() { 155 tv.Verdict.Results[0].Result.Status = rdbpb.TestStatus_FAIL 156 tv.Verdict.Results[0].Result.Expected = true 157 158 // Expect no test results ingested for an expected 159 // failure. 160 expectedCFs = nil 161 162 testIngestion(tvs, expectedCFs) 163 So(len(chunkStore.Contents), ShouldEqual, 0) 164 }) 165 Convey(`Unexpected pass`, func() { 166 tv.Verdict.Results[0].Result.Status = rdbpb.TestStatus_PASS 167 tv.Verdict.Results[0].Result.Expected = false 168 169 // Expect no test results ingested for a passed test 170 // (even if unexpected). 171 expectedCFs = nil 172 testIngestion(tvs, expectedCFs) 173 So(len(chunkStore.Contents), ShouldEqual, 0) 174 }) 175 Convey(`Unexpected skip`, func() { 176 tv.Verdict.Results[0].Result.Status = rdbpb.TestStatus_SKIP 177 tv.Verdict.Results[0].Result.Expected = false 178 179 // Expect no test results ingested for a skipped test 180 // (even if unexpected). 181 expectedCFs = nil 182 183 testIngestion(tvs, expectedCFs) 184 So(len(chunkStore.Contents), ShouldEqual, 0) 185 }) 186 Convey(`Failure with no tags`, func() { 187 // Tests are allowed to have no tags. 188 tv.Verdict.Results[0].Result.Tags = nil 189 190 for _, cf := range expectedCFs { 191 cf.Tags = nil 192 cf.BugTrackingComponent = nil 193 } 194 195 testIngestion(tvs, expectedCFs) 196 So(len(chunkStore.Contents), ShouldEqual, 1) 197 }) 198 Convey(`Failure without variant`, func() { 199 // Tests are allowed to have no variant. 200 tv.Verdict.Variant = nil 201 tv.Verdict.Results[0].Result.Variant = nil 202 203 for _, cf := range expectedCFs { 204 cf.Variant = nil 205 } 206 207 testIngestion(tvs, expectedCFs) 208 So(len(chunkStore.Contents), ShouldEqual, 1) 209 }) 210 Convey(`Failure without failure reason`, func() { 211 // Failures may not have a failure reason. 212 tv.Verdict.Results[0].Result.FailureReason = nil 213 testnameCF.FailureReason = nil 214 215 // As the test result does not match any rules, the 216 // test result is included in the suggested cluster 217 // with high priority. 218 testnameCF.IsIncludedWithHighPriority = true 219 expectedCFs = []*bqpb.ClusteredFailureRow{testnameCF} 220 221 testIngestion(tvs, expectedCFs) 222 So(len(chunkStore.Contents), ShouldEqual, 1) 223 }) 224 Convey(`Failure without presubmit run`, func() { 225 opts.PresubmitRun = nil 226 for _, cf := range expectedCFs { 227 cf.PresubmitRunId = nil 228 cf.PresubmitRunMode = "" 229 cf.PresubmitRunOwner = "" 230 cf.PresubmitRunStatus = "" 231 cf.BuildCritical = false 232 } 233 234 testIngestion(tvs, expectedCFs) 235 So(len(chunkStore.Contents), ShouldEqual, 1) 236 }) 237 Convey(`Failure with multiple exoneration`, func() { 238 tv.Verdict.Exonerations = []*rdbpb.TestExoneration{ 239 { 240 Name: fmt.Sprintf("invocations/testrun-mytestrun/tests/test-name-%v/exonerations/exon-1", uniqifier), 241 TestId: tv.Verdict.TestId, 242 Variant: proto.Clone(tv.Verdict.Variant).(*rdbpb.Variant), 243 VariantHash: "hash", 244 ExonerationId: "exon-1", 245 ExplanationHtml: "<p>Some description</p>", 246 Reason: rdbpb.ExonerationReason_OCCURS_ON_MAINLINE, 247 }, 248 { 249 Name: fmt.Sprintf("invocations/testrun-mytestrun/tests/test-name-%v/exonerations/exon-1", uniqifier), 250 TestId: tv.Verdict.TestId, 251 Variant: proto.Clone(tv.Verdict.Variant).(*rdbpb.Variant), 252 VariantHash: "hash", 253 ExonerationId: "exon-1", 254 ExplanationHtml: "<p>Some description</p>", 255 Reason: rdbpb.ExonerationReason_OCCURS_ON_OTHER_CLS, 256 }, 257 } 258 259 for _, cf := range expectedCFs { 260 cf.Exonerations = []*bqpb.ClusteredFailureRow_TestExoneration{ 261 { 262 Reason: pb.ExonerationReason_OCCURS_ON_MAINLINE, 263 }, { 264 Reason: pb.ExonerationReason_OCCURS_ON_OTHER_CLS, 265 }, 266 } 267 } 268 269 testIngestion(tvs, expectedCFs) 270 So(len(chunkStore.Contents), ShouldEqual, 1) 271 }) 272 Convey(`Failure with only suggested clusters`, func() { 273 reason := &pb.FailureReason{ 274 PrimaryErrorMessage: "Should not match rule", 275 } 276 tv.Verdict.Results[0].Result.FailureReason = &rdbpb.FailureReason{ 277 PrimaryErrorMessage: "Should not match rule", 278 } 279 testnameCF.FailureReason = reason 280 regexpCF.FailureReason = reason 281 282 // Recompute the cluster ID to reflect the different 283 // failure reason. 284 setRegexpClustered(cfg, regexpCF) 285 286 // As the test result does not match any rules, the 287 // test result should be included in the suggested clusters 288 // with high priority. 289 testnameCF.IsIncludedWithHighPriority = true 290 regexpCF.IsIncludedWithHighPriority = true 291 expectedCFs = []*bqpb.ClusteredFailureRow{testnameCF, regexpCF} 292 293 testIngestion(tvs, expectedCFs) 294 So(len(chunkStore.Contents), ShouldEqual, 1) 295 }) 296 297 Convey(`Failure with bug component metadata`, func() { 298 Convey(`With monorail bug system`, func() { 299 tv.Verdict.TestMetadata.BugComponent = &rdbpb.BugComponent{ 300 System: &rdbpb.BugComponent_Monorail{ 301 Monorail: &rdbpb.MonorailComponent{ 302 Project: "chromium", 303 Value: "Blink>Component", 304 }, 305 }, 306 } 307 for _, cf := range expectedCFs { 308 cf.BugTrackingComponent.System = "monorail" 309 cf.BugTrackingComponent.Component = "Blink>Component" 310 } 311 testIngestion(tvs, expectedCFs) 312 So(len(chunkStore.Contents), ShouldEqual, 1) 313 }) 314 Convey(`With Buganizer bug system`, func() { 315 tv.Verdict.TestMetadata.BugComponent = &rdbpb.BugComponent{ 316 System: &rdbpb.BugComponent_IssueTracker{ 317 IssueTracker: &rdbpb.IssueTrackerComponent{ 318 ComponentId: 12345, 319 }, 320 }, 321 } 322 for _, cf := range expectedCFs { 323 cf.BugTrackingComponent.System = "buganizer" 324 cf.BugTrackingComponent.Component = "12345" 325 } 326 testIngestion(tvs, expectedCFs) 327 So(len(chunkStore.Contents), ShouldEqual, 1) 328 }) 329 Convey(`No BugComponent metadata, but public_buganizer_component tag present`, func() { 330 tv.Verdict.TestMetadata.BugComponent = nil 331 332 for _, result := range tv.Verdict.Results { 333 result.Result.Tags = []*rdbpb.StringPair{ 334 { 335 Key: "public_buganizer_component", 336 Value: "654321", 337 }, 338 } 339 } 340 for _, cf := range expectedCFs { 341 cf.BugTrackingComponent.System = "buganizer" 342 cf.BugTrackingComponent.Component = "654321" 343 cf.Tags = []*pb.StringPair{ 344 { 345 Key: "public_buganizer_component", 346 Value: "654321", 347 }, 348 } 349 } 350 testIngestion(tvs, expectedCFs) 351 So(len(chunkStore.Contents), ShouldEqual, 1) 352 }) 353 Convey(`No BugComponent metadata, both public_buganizer_component and monorail_component present`, func() { 354 tv.Verdict.TestMetadata.BugComponent = nil 355 356 for _, result := range tv.Verdict.Results { 357 result.Result.Tags = []*rdbpb.StringPair{ 358 { 359 Key: "monorail_component", 360 Value: "Component>MyComponent", 361 }, 362 { 363 Key: "public_buganizer_component", 364 Value: "654321", 365 }, 366 } 367 } 368 for _, cf := range expectedCFs { 369 cf.Tags = []*pb.StringPair{ 370 { 371 Key: "monorail_component", 372 Value: "Component>MyComponent", 373 }, 374 { 375 Key: "public_buganizer_component", 376 Value: "654321", 377 }, 378 } 379 } 380 381 Convey("With monorail as preferred system", func() { 382 opts.PreferBuganizerComponents = false 383 384 for _, cf := range expectedCFs { 385 cf.BugTrackingComponent.System = "monorail" 386 cf.BugTrackingComponent.Component = "Component>MyComponent" 387 } 388 testIngestion(tvs, expectedCFs) 389 So(len(chunkStore.Contents), ShouldEqual, 1) 390 }) 391 Convey("With buganizer as preferred system", func() { 392 opts.PreferBuganizerComponents = true 393 394 for _, cf := range expectedCFs { 395 cf.BugTrackingComponent.System = "buganizer" 396 cf.BugTrackingComponent.Component = "654321" 397 } 398 testIngestion(tvs, expectedCFs) 399 So(len(chunkStore.Contents), ShouldEqual, 1) 400 }) 401 }) 402 }) 403 Convey(`Failure with no sources`, func() { 404 tv.Sources = nil 405 for _, cf := range expectedCFs { 406 cf.Sources = nil 407 cf.SourceRef = nil 408 cf.SourceRefHash = "<fix me>" 409 } 410 // No sources also means no test variant branch analysis. 411 tv.TestVariantBranch = nil 412 }) 413 }) 414 Convey(`Ingest multiple failures`, func() { 415 const uniqifier = 1 416 const testRunsPerVariant = 2 417 const resultsPerTestRun = 2 418 tv := newTestVerdict(uniqifier, testRunsPerVariant, resultsPerTestRun, nil) 419 tvs := []TestVerdict{tv} 420 421 // Setup a scenario as follows: 422 // - A test was run four times in total, consisting of two test 423 // runs with two tries each. 424 // - The test failed on all tries. 425 var expectedCFs []*bqpb.ClusteredFailureRow 426 var expectedCFsByTestRun [][]*bqpb.ClusteredFailureRow 427 for t := 0; t < testRunsPerVariant; t++ { 428 var testRunExp []*bqpb.ClusteredFailureRow 429 for j := 0; j < resultsPerTestRun; j++ { 430 regexpCF := expectedClusteredFailure(uniqifier, testRunsPerVariant, t, resultsPerTestRun, j) 431 setRegexpClustered(cfg, regexpCF) 432 testnameCF := expectedClusteredFailure(uniqifier, testRunsPerVariant, t, resultsPerTestRun, j) 433 setTestNameClustered(cfg, testnameCF) 434 ruleCF := expectedClusteredFailure(uniqifier, testRunsPerVariant, t, resultsPerTestRun, j) 435 setRuleClustered(ruleCF, rule) 436 testRunExp = append(testRunExp, regexpCF, testnameCF, ruleCF) 437 } 438 expectedCFsByTestRun = append(expectedCFsByTestRun, testRunExp) 439 expectedCFs = append(expectedCFs, testRunExp...) 440 } 441 442 // Expectation: all test results show both the test run and 443 // invocation blocked by failures. 444 for _, exp := range expectedCFs { 445 exp.IsIngestedInvocationBlocked = true 446 exp.IsTestRunBlocked = true 447 } 448 449 Convey(`Some test runs blocked and presubmit run not blocked`, func() { 450 // Let the last retry of the last test run pass. 451 tv.Verdict.Results[testRunsPerVariant*resultsPerTestRun-1].Result.Status = rdbpb.TestStatus_PASS 452 // Drop the expected clustered failures for the last test result. 453 expectedCFs = expectedCFs[0 : (testRunsPerVariant*resultsPerTestRun-1)*3] 454 455 // First test run should be blocked. 456 for _, exp := range expectedCFsByTestRun[0] { 457 exp.IsIngestedInvocationBlocked = false 458 exp.IsTestRunBlocked = true 459 } 460 // Last test run should not be blocked. 461 for _, exp := range expectedCFsByTestRun[testRunsPerVariant-1] { 462 exp.IsIngestedInvocationBlocked = false 463 exp.IsTestRunBlocked = false 464 } 465 testIngestion(tvs, expectedCFs) 466 So(len(chunkStore.Contents), ShouldEqual, 1) 467 }) 468 }) 469 Convey(`Ingest many failures`, func() { 470 var tvs []TestVerdict 471 var expectedCFs []*bqpb.ClusteredFailureRow 472 473 const variantCount = 20 474 const testRunsPerVariant = 10 475 const resultsPerTestRun = 10 476 for uniqifier := 0; uniqifier < variantCount; uniqifier++ { 477 tv := newTestVerdict(uniqifier, testRunsPerVariant, resultsPerTestRun, nil) 478 tvs = append(tvs, tv) 479 for t := 0; t < testRunsPerVariant; t++ { 480 for j := 0; j < resultsPerTestRun; j++ { 481 regexpCF := expectedClusteredFailure(uniqifier, testRunsPerVariant, t, resultsPerTestRun, j) 482 setRegexpClustered(cfg, regexpCF) 483 testnameCF := expectedClusteredFailure(uniqifier, testRunsPerVariant, t, resultsPerTestRun, j) 484 setTestNameClustered(cfg, testnameCF) 485 ruleCF := expectedClusteredFailure(uniqifier, testRunsPerVariant, t, resultsPerTestRun, j) 486 setRuleClustered(ruleCF, rule) 487 expectedCFs = append(expectedCFs, regexpCF, testnameCF, ruleCF) 488 } 489 } 490 } 491 // Verify more than one chunk is ingested. 492 testIngestion(tvs, expectedCFs) 493 So(len(chunkStore.Contents), ShouldBeGreaterThan, 1) 494 }) 495 }) 496 } 497 498 func setTestNameClustered(cfg *compiledcfg.ProjectConfig, e *bqpb.ClusteredFailureRow) { 499 e.ClusterAlgorithm = testname.AlgorithmName 500 e.ClusterId = hex.EncodeToString((&testname.Algorithm{}).Cluster(cfg, &clustering.Failure{ 501 TestID: e.TestId, 502 })) 503 } 504 505 func setRegexpClustered(cfg *compiledcfg.ProjectConfig, e *bqpb.ClusteredFailureRow) { 506 e.ClusterAlgorithm = failurereason.AlgorithmName 507 e.ClusterId = hex.EncodeToString((&failurereason.Algorithm{}).Cluster(cfg, &clustering.Failure{ 508 Reason: &pb.FailureReason{PrimaryErrorMessage: e.FailureReason.PrimaryErrorMessage}, 509 })) 510 } 511 512 func setRuleClustered(e *bqpb.ClusteredFailureRow, rule *rules.Entry) { 513 e.ClusterAlgorithm = rulesalgorithm.AlgorithmName 514 e.ClusterId = rule.RuleID 515 e.IsIncludedWithHighPriority = true 516 } 517 518 func sortClusteredFailures(cfs []*bqpb.ClusteredFailureRow) { 519 sort.Slice(cfs, func(i, j int) bool { 520 return clusteredFailureKey(cfs[i]) < clusteredFailureKey(cfs[j]) 521 }) 522 } 523 524 func clusteredFailureKey(cf *bqpb.ClusteredFailureRow) string { 525 return fmt.Sprintf("%q/%q/%q/%q", cf.ClusterAlgorithm, cf.ClusterId, cf.TestResultSystem, cf.TestResultId) 526 } 527 528 func newTestVerdict(uniqifier, testRunCount, resultsPerTestRun int, bugComponent *rdbpb.BugComponent) TestVerdict { 529 testID := fmt.Sprintf("ninja://test_name/%v", uniqifier) 530 variant := &rdbpb.Variant{ 531 Def: map[string]string{ 532 "k1": "v1", 533 }, 534 } 535 rdbVerdict := &rdbpb.TestVariant{ 536 TestId: testID, 537 Variant: variant, 538 VariantHash: "hash", 539 Status: rdbpb.TestVariantStatus_UNEXPECTED, 540 Exonerations: nil, 541 TestMetadata: &rdbpb.TestMetadata{ 542 BugComponent: bugComponent, 543 }, 544 } 545 for i := 0; i < testRunCount; i++ { 546 for j := 0; j < resultsPerTestRun; j++ { 547 tr := newTestResult(uniqifier, i, j) 548 // Test ID, Variant, VariantHash are not populated on the test 549 // results of a Test Variant as it is present on the parent record. 550 tr.TestId = "" 551 tr.Variant = nil 552 tr.VariantHash = "" 553 rdbVerdict.Results = append(rdbVerdict.Results, &rdbpb.TestResultBundle{Result: tr}) 554 } 555 } 556 return TestVerdict{ 557 Verdict: rdbVerdict, 558 Sources: testSources(), 559 TestVariantBranch: &clusteringpb.TestVariantBranch{ 560 FlakyVerdicts_24H: 111, 561 UnexpectedVerdicts_24H: 222, 562 TotalVerdicts_24H: 555, 563 }, 564 } 565 } 566 567 func newTestResult(uniqifier, testRunNum, resultNum int) *rdbpb.TestResult { 568 return newFakeTestResult(uniqifier, testRunNum, resultNum) 569 } 570 571 func newFakeTestResult(uniqifier, testRunNum, resultNum int) *rdbpb.TestResult { 572 resultID := fmt.Sprintf("result-%v-%v", testRunNum, resultNum) 573 return &rdbpb.TestResult{ 574 Name: fmt.Sprintf("invocations/testrun-%v/tests/test-name-%v/results/%s", testRunNum, uniqifier, resultID), 575 ResultId: resultID, 576 Expected: false, 577 Status: rdbpb.TestStatus_CRASH, 578 SummaryHtml: "<p>Some SummaryHTML</p>", 579 StartTime: timestamppb.New(time.Date(2022, time.February, 12, 0, 0, 0, 0, time.UTC)), 580 Duration: durationpb.New(time.Second * 10), 581 Tags: []*rdbpb.StringPair{ 582 { 583 Key: "monorail_component", 584 Value: "Component>MyComponent", 585 }, 586 }, 587 FailureReason: &rdbpb.FailureReason{ 588 PrimaryErrorMessage: "Failure reason.", 589 }, 590 } 591 } 592 593 func testSources() *pb.Sources { 594 result := &pb.Sources{ 595 GitilesCommit: &pb.GitilesCommit{ 596 Host: "chromium.googlesource.com", 597 Project: "infra/infra", 598 Ref: "refs/heads/main", 599 CommitHash: "1234567890abcdefabcd1234567890abcdefabcd", 600 Position: 12345, 601 }, 602 IsDirty: true, 603 Changelists: []*pb.GerritChange{ 604 { 605 Host: "chromium-review.googlesource.com", 606 Project: "myproject", 607 Change: 87654, 608 Patchset: 321, 609 }, 610 }, 611 } 612 return result 613 } 614 615 func expectedClusteredFailure(uniqifier, testRunCount, testRunNum, resultsPerTestRun, resultNum int) *bqpb.ClusteredFailureRow { 616 resultID := fmt.Sprintf("result-%v-%v", testRunNum, resultNum) 617 return &bqpb.ClusteredFailureRow{ 618 ClusterAlgorithm: "", // Determined by clustering algorithm. 619 ClusterId: "", // Determined by clustering algorithm. 620 TestResultSystem: "resultdb", 621 TestResultId: fmt.Sprintf("invocations/testrun-%v/tests/test-name-%v/results/%s", testRunNum, uniqifier, resultID), 622 LastUpdated: nil, // Only known at runtime, Spanner commit timestamp. 623 Project: "chromium", 624 PartitionTime: timestamppb.New(time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)), 625 IsIncluded: true, 626 IsIncludedWithHighPriority: false, 627 628 ChunkId: "", 629 ChunkIndex: 0, // To be set by caller as needed. 630 631 Realm: "chromium:ci", 632 TestId: fmt.Sprintf("ninja://test_name/%v", uniqifier), 633 Tags: []*pb.StringPair{ 634 { 635 Key: "monorail_component", 636 Value: "Component>MyComponent", 637 }, 638 }, 639 Variant: []*pb.StringPair{ 640 { 641 Key: "k1", 642 Value: "v1", 643 }, 644 }, 645 VariantHash: "hash", 646 FailureReason: &pb.FailureReason{PrimaryErrorMessage: "Failure reason."}, 647 BugTrackingComponent: &pb.BugTrackingComponent{ 648 System: "monorail", 649 Component: "Component>MyComponent", 650 }, 651 StartTime: timestamppb.New(time.Date(2022, time.February, 12, 0, 0, 0, 0, time.UTC)), 652 Duration: 10.0, 653 Exonerations: nil, 654 655 PresubmitRunId: &pb.PresubmitRunId{System: "luci-cv", Id: "cq-run-123"}, 656 PresubmitRunOwner: "automation", 657 PresubmitRunMode: "FULL_RUN", // pb.PresubmitRunMode_FULL_RUN 658 PresubmitRunStatus: "FAILED", // pb.PresubmitRunStatus_PRESUBMIT_RUN_STATUS_FAILED, 659 BuildStatus: "FAILURE", // pb.BuildStatus_BUILD_STATUS_FAILURE 660 BuildCritical: true, 661 IngestedInvocationId: "build-123456790123456", 662 IngestedInvocationResultIndex: int64(testRunNum*resultsPerTestRun + resultNum), 663 IngestedInvocationResultCount: int64(testRunCount * resultsPerTestRun), 664 IsIngestedInvocationBlocked: true, 665 TestRunId: fmt.Sprintf("testrun-%v", testRunNum), 666 TestRunResultIndex: int64(resultNum), 667 TestRunResultCount: int64(resultsPerTestRun), 668 IsTestRunBlocked: true, 669 670 SourceRefHash: `923c0d1af67f8ef8`, 671 SourceRef: &pb.SourceRef{ 672 System: &pb.SourceRef_Gitiles{ 673 Gitiles: &pb.GitilesRef{ 674 Host: "chromium.googlesource.com", 675 Project: "infra/infra", 676 Ref: "refs/heads/main", 677 }, 678 }, 679 }, 680 Sources: testSources(), 681 BuildGardenerRotations: []string{"gardener-rotation1", "gardener-rotation2"}, 682 TestVariantBranch: &bqpb.ClusteredFailureRow_TestVariantBranch{ 683 FlakyVerdicts_24H: 111, 684 UnexpectedVerdicts_24H: 222, 685 TotalVerdicts_24H: 555, 686 }, 687 } 688 }