go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/rpc/clusters_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 rpc 16 17 import ( 18 "context" 19 "encoding/hex" 20 "sort" 21 "testing" 22 "time" 23 24 "cloud.google.com/go/bigquery" 25 "google.golang.org/protobuf/types/known/timestamppb" 26 27 "go.chromium.org/luci/common/clock" 28 "go.chromium.org/luci/common/data/stringset" 29 "go.chromium.org/luci/common/errors" 30 "go.chromium.org/luci/gae/impl/memory" 31 "go.chromium.org/luci/resultdb/rdbperms" 32 "go.chromium.org/luci/server/auth" 33 "go.chromium.org/luci/server/auth/authtest" 34 "go.chromium.org/luci/server/auth/realms" 35 "go.chromium.org/luci/server/caching" 36 "go.chromium.org/luci/server/secrets" 37 "go.chromium.org/luci/server/secrets/testsecrets" 38 39 "go.chromium.org/luci/analysis/internal/analysis" 40 "go.chromium.org/luci/analysis/internal/analysis/metrics" 41 "go.chromium.org/luci/analysis/internal/bugs" 42 "go.chromium.org/luci/analysis/internal/clustering" 43 "go.chromium.org/luci/analysis/internal/clustering/algorithms" 44 "go.chromium.org/luci/analysis/internal/clustering/algorithms/failurereason" 45 "go.chromium.org/luci/analysis/internal/clustering/algorithms/rulesalgorithm" 46 "go.chromium.org/luci/analysis/internal/clustering/algorithms/testname" 47 "go.chromium.org/luci/analysis/internal/clustering/rules" 48 "go.chromium.org/luci/analysis/internal/clustering/runs" 49 "go.chromium.org/luci/analysis/internal/config" 50 "go.chromium.org/luci/analysis/internal/config/compiledcfg" 51 "go.chromium.org/luci/analysis/internal/perms" 52 "go.chromium.org/luci/analysis/internal/testutil" 53 "go.chromium.org/luci/analysis/pbutil" 54 configpb "go.chromium.org/luci/analysis/proto/config" 55 pb "go.chromium.org/luci/analysis/proto/v1" 56 57 . "github.com/smartystreets/goconvey/convey" 58 . "go.chromium.org/luci/common/testing/assertions" 59 ) 60 61 func TestClusters(t *testing.T) { 62 Convey("With a clusters server", t, func() { 63 ctx := testutil.IntegrationTestContext(t) 64 ctx = caching.WithEmptyProcessCache(ctx) 65 66 // For user identification. 67 ctx = authtest.MockAuthConfig(ctx) 68 authState := &authtest.FakeState{ 69 Identity: "user:someone@example.com", 70 IdentityGroups: []string{"luci-analysis-access"}, 71 } 72 ctx = auth.WithState(ctx, authState) 73 ctx = secrets.Use(ctx, &testsecrets.Store{}) 74 75 // Provides datastore implementation needed for project config. 76 ctx = memory.Use(ctx) 77 analysisClient := newFakeAnalysisClient() 78 server := NewClustersServer(analysisClient) 79 80 configVersion := time.Date(2025, time.August, 12, 0, 1, 2, 3, time.UTC) 81 projectCfg := config.CreateConfigWithBothBuganizerAndMonorail(configpb.BugSystem_MONORAIL) 82 projectCfg.LastUpdated = timestamppb.New(configVersion) 83 projectCfg.BugManagement.Monorail.DisplayPrefix = "crbug.com" 84 projectCfg.BugManagement.Monorail.MonorailHostname = "bugs.chromium.org" 85 configs := make(map[string]*configpb.ProjectConfig) 86 configs["testproject"] = projectCfg 87 err := config.SetTestProjectConfig(ctx, configs) 88 So(err, ShouldBeNil) 89 90 compiledTestProjectCfg, err := compiledcfg.NewConfig(projectCfg) 91 So(err, ShouldBeNil) 92 93 // Rules version is in microsecond granularity, consistent with 94 // the granularity of Spanner commit timestamps. 95 rulesVersion := time.Date(2021, time.February, 12, 1, 2, 4, 5000, time.UTC) 96 rs := []*rules.Entry{ 97 rules.NewRule(0). 98 WithProject("testproject"). 99 WithRuleDefinition(`test LIKE "%TestSuite.TestName%"`). 100 WithPredicateLastUpdateTime(rulesVersion.Add(-1 * time.Hour)). 101 WithBug(bugs.BugID{ 102 System: "monorail", 103 ID: "chromium/7654321", 104 }).Build(), 105 rules.NewRule(1). 106 WithProject("testproject"). 107 WithRuleDefinition(`reason LIKE "my_file.cc(%): Check failed: false."`). 108 WithPredicateLastUpdateTime(rulesVersion). 109 WithBug(bugs.BugID{ 110 System: "buganizer", 111 ID: "82828282", 112 }).Build(), 113 rules.NewRule(2). 114 WithProject("testproject"). 115 WithRuleDefinition(`test LIKE "%Other%"`). 116 WithPredicateLastUpdateTime(rulesVersion.Add(-2 * time.Hour)). 117 WithBug(bugs.BugID{ 118 System: "monorail", 119 ID: "chromium/912345", 120 }).Build(), 121 } 122 err = rules.SetForTesting(ctx, rs) 123 So(err, ShouldBeNil) 124 125 Convey("Unauthorized requests are rejected", func() { 126 // Ensure no access to luci-analysis-access. 127 ctx = auth.WithState(ctx, &authtest.FakeState{ 128 Identity: "user:someone@example.com", 129 // Not a member of luci-analysis-access. 130 IdentityGroups: []string{"other-group"}, 131 }) 132 133 // Make some request (the request should not matter, as 134 // a common decorator is used for all requests.) 135 request := &pb.ClusterRequest{ 136 Project: "testproject", 137 } 138 139 rule, err := server.Cluster(ctx, request) 140 So(err, ShouldBeRPCPermissionDenied, "not a member of luci-analysis-access") 141 So(rule, ShouldBeNil) 142 }) 143 Convey("Cluster", func() { 144 authState.IdentityPermissions = []authtest.RealmPermission{ 145 { 146 Realm: "testproject:@project", 147 Permission: perms.PermGetClustersByFailure, 148 }, 149 { 150 Realm: "testproject:@project", 151 Permission: perms.PermGetRule, 152 }, 153 } 154 155 request := &pb.ClusterRequest{ 156 Project: "testproject", 157 TestResults: []*pb.ClusterRequest_TestResult{ 158 { 159 RequestTag: "my tag 1", 160 TestId: "ninja://chrome/test:interactive_ui_tests/TestSuite.TestName", 161 FailureReason: &pb.FailureReason{ 162 PrimaryErrorMessage: "my_file.cc(123): Check failed: false.", 163 }, 164 }, 165 { 166 RequestTag: "my tag 2", 167 TestId: "Other_test", 168 }, 169 }, 170 } 171 Convey("Not authorised to cluster", func() { 172 authState.IdentityPermissions = removePermission(authState.IdentityPermissions, perms.PermGetClustersByFailure) 173 174 response, err := server.Cluster(ctx, request) 175 So(err, ShouldBeRPCPermissionDenied, "caller does not have permission analysis.clusters.getByFailure") 176 So(response, ShouldBeNil) 177 }) 178 Convey("Not authorised to get rule", func() { 179 authState.IdentityPermissions = removePermission(authState.IdentityPermissions, perms.PermGetRule) 180 181 response, err := server.Cluster(ctx, request) 182 So(err, ShouldBeRPCPermissionDenied, "caller does not have permission analysis.rules.get") 183 So(response, ShouldBeNil) 184 }) 185 Convey("With a valid request", func() { 186 // Run 187 response, err := server.Cluster(ctx, request) 188 189 // Verify 190 So(err, ShouldBeNil) 191 So(response, ShouldResembleProto, &pb.ClusterResponse{ 192 ClusteredTestResults: []*pb.ClusterResponse_ClusteredTestResult{ 193 { 194 RequestTag: "my tag 1", 195 Clusters: sortClusterEntries([]*pb.ClusterResponse_ClusteredTestResult_ClusterEntry{ 196 { 197 ClusterId: &pb.ClusterId{ 198 Algorithm: "rules", 199 Id: rs[0].RuleID, 200 }, 201 Bug: &pb.AssociatedBug{ 202 System: "monorail", 203 Id: "chromium/7654321", 204 LinkText: "crbug.com/7654321", 205 Url: "https://bugs.chromium.org/p/chromium/issues/detail?id=7654321", 206 }, 207 }, { 208 ClusterId: &pb.ClusterId{ 209 Algorithm: "rules", 210 Id: rs[1].RuleID, 211 }, 212 Bug: &pb.AssociatedBug{ 213 System: "buganizer", 214 Id: "82828282", 215 LinkText: "b/82828282", 216 Url: "https://issuetracker.google.com/issues/82828282", 217 }, 218 }, 219 failureReasonClusterEntry(compiledTestProjectCfg, "my_file.cc(123): Check failed: false."), 220 testNameClusterEntry(compiledTestProjectCfg, "ninja://chrome/test:interactive_ui_tests/TestSuite.TestName"), 221 }), 222 }, 223 { 224 RequestTag: "my tag 2", 225 Clusters: sortClusterEntries([]*pb.ClusterResponse_ClusteredTestResult_ClusterEntry{ 226 { 227 ClusterId: &pb.ClusterId{ 228 Algorithm: "rules", 229 Id: rs[2].RuleID, 230 }, 231 Bug: &pb.AssociatedBug{ 232 System: "monorail", 233 Id: "chromium/912345", 234 LinkText: "crbug.com/912345", 235 Url: "https://bugs.chromium.org/p/chromium/issues/detail?id=912345", 236 }, 237 }, 238 testNameClusterEntry(compiledTestProjectCfg, "Other_test"), 239 }), 240 }, 241 }, 242 ClusteringVersion: &pb.ClusteringVersion{ 243 AlgorithmsVersion: algorithms.AlgorithmsVersion, 244 RulesVersion: timestamppb.New(rulesVersion), 245 ConfigVersion: timestamppb.New(configVersion), 246 }, 247 }) 248 }) 249 Convey("With no monorail configuration", func() { 250 // Setup 251 projectCfg.BugManagement.Monorail = nil 252 configs := make(map[string]*configpb.ProjectConfig) 253 configs["testproject"] = projectCfg 254 err := config.SetTestProjectConfig(ctx, configs) 255 So(err, ShouldBeNil) 256 // Run 257 response, err := server.Cluster(ctx, request) 258 So(err, ShouldBeNil) 259 So(response.ClusteredTestResults[0].Clusters[1].Bug.Url, ShouldEqual, "") 260 }) 261 Convey("With missing test ID", func() { 262 request.TestResults[1].TestId = "" 263 264 // Run 265 response, err := server.Cluster(ctx, request) 266 267 // Verify 268 So(response, ShouldBeNil) 269 So(err, ShouldBeRPCInvalidArgument, "test result 1: test ID must not be empty") 270 }) 271 Convey("With too many test results", func() { 272 var testResults []*pb.ClusterRequest_TestResult 273 for i := 0; i < 1001; i++ { 274 testResults = append(testResults, &pb.ClusterRequest_TestResult{ 275 TestId: "AnotherTest", 276 }) 277 } 278 request.TestResults = testResults 279 280 // Run 281 response, err := server.Cluster(ctx, request) 282 283 // Verify 284 So(response, ShouldBeNil) 285 So(err, ShouldBeRPCInvalidArgument, "too many test results: at most 1000 test results can be clustered in one request") 286 }) 287 Convey("With project not configured", func() { 288 err := config.SetTestProjectConfig(ctx, map[string]*configpb.ProjectConfig{}) 289 So(err, ShouldBeNil) 290 291 // Run 292 response, err := server.Cluster(ctx, request) 293 294 // Verify 295 So(response.ClusteringVersion.ConfigVersion.AsTime(), ShouldEqual, config.StartingEpoch) 296 So(err, ShouldBeNil) 297 }) 298 }) 299 Convey("Get", func() { 300 authState.IdentityPermissions = []authtest.RealmPermission{ 301 { 302 Realm: "testproject:@project", 303 Permission: perms.PermGetCluster, 304 }, 305 { 306 Realm: "testproject:realm1", 307 Permission: rdbperms.PermListTestResults, 308 }, 309 { 310 Realm: "testproject:realm3", 311 Permission: rdbperms.PermListTestResults, 312 }, 313 } 314 315 example := &clustering.Failure{ 316 TestID: "TestID_Example", 317 Reason: &pb.FailureReason{ 318 PrimaryErrorMessage: "Example failure reason 123.", 319 }, 320 } 321 a := &failurereason.Algorithm{} 322 reasonClusterID := a.Cluster(compiledTestProjectCfg, example) 323 324 analysisClient.clustersByProject["testproject"] = []*analysis.Cluster{} 325 326 request := &pb.GetClusterRequest{ 327 Name: "projects/testproject/clusters/rules/22222200000000000000000000000000", 328 } 329 330 Convey("Not authorised to get cluster", func() { 331 authState.IdentityPermissions = removePermission(authState.IdentityPermissions, perms.PermGetCluster) 332 333 response, err := server.Get(ctx, request) 334 So(err, ShouldBeRPCPermissionDenied, "caller does not have permission analysis.clusters.get") 335 So(response, ShouldBeNil) 336 }) 337 Convey("With a valid request", func() { 338 analysisClient.clustersByProject["testproject"] = []*analysis.Cluster{ 339 { 340 ClusterID: clustering.ClusterID{ 341 Algorithm: rulesalgorithm.AlgorithmName, 342 ID: "11111100000000000000000000000000", 343 }, 344 MetricValues: map[metrics.ID]metrics.TimewiseCounts{ 345 metrics.HumanClsFailedPresubmit.ID: { 346 OneDay: metrics.Counts{Nominal: 1}, 347 ThreeDay: metrics.Counts{Nominal: 2}, 348 SevenDay: metrics.Counts{Nominal: 3}, 349 }, 350 metrics.CriticalFailuresExonerated.ID: { 351 OneDay: metrics.Counts{Nominal: 4}, 352 ThreeDay: metrics.Counts{Nominal: 5}, 353 SevenDay: metrics.Counts{Nominal: 6}, 354 }, 355 metrics.Failures.ID: { 356 OneDay: metrics.Counts{Nominal: 7}, 357 ThreeDay: metrics.Counts{Nominal: 8}, 358 SevenDay: metrics.Counts{Nominal: 9}, 359 }, 360 }, 361 DistinctUserCLsWithFailures7d: metrics.Counts{Nominal: 13}, 362 PostsubmitBuildsWithFailures7d: metrics.Counts{Nominal: 14}, 363 ExampleFailureReason: bigquery.NullString{Valid: true, StringVal: "Example failure reason."}, 364 TopTestIDs: []analysis.TopCount{ 365 {Value: "TestID 1", Count: 2}, 366 {Value: "TestID 2", Count: 1}, 367 }, 368 Realms: []string{"testproject:realm1", "testproject:realm2"}, 369 }, 370 } 371 request := &pb.GetClusterRequest{ 372 Name: "projects/testproject/clusters/rules/11111100000000000000000000000000", 373 } 374 expectedResponse := &pb.Cluster{ 375 Name: "projects/testproject/clusters/rules/11111100000000000000000000000000", 376 HasExample: true, 377 Metrics: map[string]*pb.Cluster_TimewiseCounts{ 378 metrics.HumanClsFailedPresubmit.ID.String(): { 379 OneDay: &pb.Cluster_Counts{Nominal: 1}, 380 ThreeDay: &pb.Cluster_Counts{Nominal: 2}, 381 SevenDay: &pb.Cluster_Counts{Nominal: 3}, 382 }, 383 metrics.CriticalFailuresExonerated.ID.String(): { 384 OneDay: &pb.Cluster_Counts{Nominal: 4}, 385 ThreeDay: &pb.Cluster_Counts{Nominal: 5}, 386 SevenDay: &pb.Cluster_Counts{Nominal: 6}, 387 }, 388 metrics.Failures.ID.String(): { 389 OneDay: &pb.Cluster_Counts{Nominal: 7}, 390 ThreeDay: &pb.Cluster_Counts{Nominal: 8}, 391 SevenDay: &pb.Cluster_Counts{Nominal: 9}, 392 }, 393 }, 394 } 395 396 Convey("Rule with clustered failures", func() { 397 // Run 398 response, err := server.Get(ctx, request) 399 400 // Verify 401 So(err, ShouldBeNil) 402 So(response, ShouldResembleProto, expectedResponse) 403 }) 404 Convey("Rule without clustered failures", func() { 405 analysisClient.clustersByProject["testproject"] = []*analysis.Cluster{} 406 407 expectedResponse.HasExample = false 408 expectedResponse.Metrics = map[string]*pb.Cluster_TimewiseCounts{} 409 for _, metric := range metrics.ComputedMetrics { 410 expectedResponse.Metrics[metric.ID.String()] = emptyMetricValues() 411 } 412 413 // Run 414 response, err := server.Get(ctx, request) 415 416 // Verify 417 So(err, ShouldBeNil) 418 So(response, ShouldResembleProto, expectedResponse) 419 }) 420 Convey("Suggested cluster with example failure matching cluster definition", func() { 421 // Suggested cluster for which there are clustered failures, and 422 // the cluster ID matches the example provided for the cluster. 423 analysisClient.clustersByProject["testproject"] = []*analysis.Cluster{ 424 { 425 ClusterID: clustering.ClusterID{ 426 Algorithm: failurereason.AlgorithmName, 427 ID: hex.EncodeToString(reasonClusterID), 428 }, 429 MetricValues: map[metrics.ID]metrics.TimewiseCounts{ 430 metrics.HumanClsFailedPresubmit.ID: { 431 SevenDay: metrics.Counts{Nominal: 15}, 432 }, 433 }, 434 ExampleFailureReason: bigquery.NullString{Valid: true, StringVal: "Example failure reason 123."}, 435 TopTestIDs: []analysis.TopCount{ 436 {Value: "TestID_Example", Count: 10}, 437 }, 438 Realms: []string{"testproject:realm1", "testproject:realm3"}, 439 }, 440 } 441 442 request := &pb.GetClusterRequest{ 443 Name: "projects/testproject/clusters/" + failurereason.AlgorithmName + "/" + hex.EncodeToString(reasonClusterID), 444 } 445 expectedResponse := &pb.Cluster{ 446 Name: "projects/testproject/clusters/" + failurereason.AlgorithmName + "/" + hex.EncodeToString(reasonClusterID), 447 Title: "Example failure reason %.", 448 HasExample: true, 449 Metrics: map[string]*pb.Cluster_TimewiseCounts{ 450 metrics.HumanClsFailedPresubmit.ID.String(): { 451 OneDay: &pb.Cluster_Counts{}, 452 ThreeDay: &pb.Cluster_Counts{}, 453 SevenDay: &pb.Cluster_Counts{Nominal: 15}, 454 }, 455 }, 456 EquivalentFailureAssociationRule: `reason LIKE "Example failure reason %."`, 457 } 458 459 // Run 460 response, err := server.Get(ctx, request) 461 462 // Verify 463 So(err, ShouldBeNil) 464 So(response, ShouldResembleProto, expectedResponse) 465 466 Convey("No test result list permission", func() { 467 authState.IdentityPermissions = removePermission(authState.IdentityPermissions, rdbperms.PermListTestResults) 468 469 // Run 470 response, err := server.Get(ctx, request) 471 472 // Verify 473 expectedResponse.Title = "" 474 expectedResponse.EquivalentFailureAssociationRule = "" 475 So(err, ShouldBeNil) 476 So(response, ShouldResembleProto, expectedResponse) 477 }) 478 }) 479 Convey("Suggested cluster with example failure not matching cluster definition", func() { 480 // Suggested cluster for which there are clustered failures, 481 // but cluster ID mismatches the example provided for the cluster. 482 // This could be because clustering configuration has changed and 483 // re-clustering is not yet complete. 484 analysisClient.clustersByProject["testproject"] = []*analysis.Cluster{ 485 { 486 ClusterID: clustering.ClusterID{ 487 Algorithm: testname.AlgorithmName, 488 ID: "cccccc00000000000000000000000001", 489 }, 490 MetricValues: map[metrics.ID]metrics.TimewiseCounts{ 491 metrics.HumanClsFailedPresubmit.ID: { 492 SevenDay: metrics.Counts{Nominal: 11}, 493 }, 494 }, 495 ExampleFailureReason: bigquery.NullString{Valid: true, StringVal: "Example failure reason 2."}, 496 TopTestIDs: []analysis.TopCount{ 497 {Value: "TestID 3", Count: 2}, 498 }, 499 Realms: []string{"testproject:realm2", "testproject:realm3"}, 500 }, 501 } 502 503 request := &pb.GetClusterRequest{ 504 Name: "projects/testproject/clusters/" + testname.AlgorithmName + "/cccccc00000000000000000000000001", 505 } 506 expectedResponse := &pb.Cluster{ 507 Name: "projects/testproject/clusters/" + testname.AlgorithmName + "/cccccc00000000000000000000000001", 508 Title: "(definition unavailable due to ongoing reclustering)", 509 HasExample: true, 510 Metrics: map[string]*pb.Cluster_TimewiseCounts{ 511 metrics.HumanClsFailedPresubmit.ID.String(): { 512 OneDay: &pb.Cluster_Counts{}, 513 ThreeDay: &pb.Cluster_Counts{}, 514 SevenDay: &pb.Cluster_Counts{Nominal: 11}, 515 }, 516 }, 517 EquivalentFailureAssociationRule: ``, 518 } 519 520 // Run 521 response, err := server.Get(ctx, request) 522 523 // Verify 524 So(err, ShouldBeNil) 525 So(response, ShouldResembleProto, expectedResponse) 526 }) 527 Convey("Suggested cluster without clustered failures", func() { 528 // Suggested cluster for which no impact data exists. 529 request := &pb.GetClusterRequest{ 530 Name: "projects/testproject/clusters/reason-v3/cccccc0000000000000000000000ffff", 531 } 532 expectedResponse := &pb.Cluster{ 533 Name: "projects/testproject/clusters/reason-v3/cccccc0000000000000000000000ffff", 534 HasExample: false, 535 Metrics: map[string]*pb.Cluster_TimewiseCounts{}, 536 } 537 for _, metric := range metrics.ComputedMetrics { 538 expectedResponse.Metrics[metric.ID.String()] = emptyMetricValues() 539 } 540 541 // Run 542 response, err := server.Get(ctx, request) 543 544 // Verify 545 So(err, ShouldBeNil) 546 So(response, ShouldResembleProto, expectedResponse) 547 }) 548 Convey("With project not configured", func() { 549 err := config.SetTestProjectConfig(ctx, map[string]*configpb.ProjectConfig{}) 550 So(err, ShouldBeNil) 551 552 // Run 553 response, err := server.Get(ctx, request) 554 555 // Verify 556 So(response, ShouldResembleProto, expectedResponse) 557 So(err, ShouldBeNil) 558 }) 559 }) 560 Convey("With invalid request", func() { 561 Convey("No name specified", func() { 562 request.Name = "" 563 564 // Run 565 response, err := server.Get(ctx, request) 566 567 // Verify 568 So(response, ShouldBeNil) 569 So(err, ShouldBeRPCInvalidArgument, "name: must be specified") 570 }) 571 Convey("Invalid name", func() { 572 request.Name = "invalid" 573 574 // Run 575 response, err := server.Get(ctx, request) 576 577 // Verify 578 So(response, ShouldBeNil) 579 So(err, ShouldBeRPCInvalidArgument, "name: invalid cluster name, expected format: projects/{project}/clusters/{cluster_alg}/{cluster_id}") 580 }) 581 Convey("Invalid cluster algorithm in name", func() { 582 request.Name = "projects/blah/clusters/reason/cccccc00000000000000000000000001" 583 584 // Run 585 response, err := server.Get(ctx, request) 586 587 // Verify 588 So(response, ShouldBeNil) 589 So(err, ShouldBeRPCInvalidArgument, "name: invalid cluster identity: algorithm not valid") 590 }) 591 Convey("Invalid cluster ID in name", func() { 592 request.Name = "projects/blah/clusters/reason-v3/123" 593 594 // Run 595 response, err := server.Get(ctx, request) 596 597 // Verify 598 So(response, ShouldBeNil) 599 So(err, ShouldBeRPCInvalidArgument, "name: invalid cluster identity: ID is not valid lowercase hexadecimal bytes") 600 }) 601 }) 602 }) 603 Convey("QueryClusterSummaries", func() { 604 authState.IdentityPermissions = listTestResultsPermissions( 605 "testproject:realm1", 606 "testproject:realm2", 607 "otherproject:realm3", 608 ) 609 authState.IdentityPermissions = append(authState.IdentityPermissions, []authtest.RealmPermission{ 610 { 611 Realm: "testproject:@project", 612 Permission: perms.PermListClusters, 613 }, 614 { 615 Realm: "testproject:@project", 616 Permission: perms.PermGetRule, 617 }, 618 { 619 Realm: "testproject:@project", 620 Permission: perms.PermGetRuleDefinition, 621 }, 622 }...) 623 624 analysisClient.clusterMetricsByProject["testproject"] = []*analysis.ClusterSummary{ 625 { 626 ClusterID: clustering.ClusterID{ 627 Algorithm: rulesalgorithm.AlgorithmName, 628 ID: rs[0].RuleID, 629 }, 630 MetricValues: map[metrics.ID]*analysis.MetricValue{ 631 metrics.HumanClsFailedPresubmit.ID: { 632 Value: 1, 633 DailyBreakdown: []int64{1, 0, 0, 0, 0, 0, 0}, 634 }, 635 metrics.CriticalFailuresExonerated.ID: { 636 Value: 2, 637 DailyBreakdown: []int64{1, 1, 0, 0, 0, 0, 0}, 638 }, 639 metrics.Failures.ID: { 640 Value: 3, 641 DailyBreakdown: []int64{1, 1, 1, 0, 0, 0, 0}, 642 }, 643 }, 644 ExampleFailureReason: bigquery.NullString{Valid: true, StringVal: "Example failure reason."}, 645 ExampleTestID: "TestID 1", 646 }, 647 { 648 ClusterID: clustering.ClusterID{ 649 Algorithm: "reason-v3", 650 ID: "cccccc00000000000000000000000001", 651 }, 652 MetricValues: map[metrics.ID]*analysis.MetricValue{ 653 metrics.HumanClsFailedPresubmit.ID: { 654 Value: 4, 655 DailyBreakdown: []int64{1, 1, 1, 1, 0, 0, 0}, 656 }, 657 metrics.CriticalFailuresExonerated.ID: { 658 Value: 5, 659 DailyBreakdown: []int64{1, 1, 1, 1, 1, 0, 0}, 660 }, 661 metrics.Failures.ID: { 662 Value: 6, 663 DailyBreakdown: []int64{1, 1, 1, 1, 1, 1, 0}, 664 }, 665 }, 666 ExampleFailureReason: bigquery.NullString{Valid: true, StringVal: "Example failure reason 2."}, 667 ExampleTestID: "TestID 3", 668 }, 669 { 670 ClusterID: clustering.ClusterID{ 671 // Rule that is no longer active. 672 Algorithm: rulesalgorithm.AlgorithmName, 673 ID: "01234567890abcdef01234567890abcdef", 674 }, 675 MetricValues: map[metrics.ID]*analysis.MetricValue{ 676 metrics.HumanClsFailedPresubmit.ID: { 677 Value: 7, 678 DailyBreakdown: []int64{1, 1, 1, 1, 1, 1, 1}, 679 }, 680 metrics.CriticalFailuresExonerated.ID: { 681 Value: 8, 682 DailyBreakdown: []int64{2, 1, 1, 1, 1, 1, 1}, 683 }, 684 metrics.Failures.ID: { 685 Value: 9, 686 DailyBreakdown: []int64{2, 2, 1, 1, 1, 1, 1}, 687 }, 688 }, 689 ExampleFailureReason: bigquery.NullString{Valid: true, StringVal: "Example failure reason."}, 690 ExampleTestID: "TestID 1", 691 }, 692 } 693 analysisClient.expectedRealmsQueried = []string{"testproject:realm1", "testproject:realm2"} 694 695 now := clock.Now(ctx) 696 request := &pb.QueryClusterSummariesRequest{ 697 Project: "testproject", 698 FailureFilter: "test_id:\"pita.Boot\" failure_reason:\"failed to boot\"", 699 OrderBy: "metrics.`human-cls-failed-presubmit`.value desc, metrics.`critical-failures-exonerated`.value desc, metrics.failures.value desc", 700 Metrics: []string{ 701 "projects/testproject/metrics/human-cls-failed-presubmit", 702 "projects/testproject/metrics/critical-failures-exonerated", 703 "projects/testproject/metrics/failures", 704 }, 705 TimeRange: &pb.TimeRange{ 706 Earliest: timestamppb.New(now.Add(-24 * time.Hour)), 707 Latest: timestamppb.New(now), 708 }, 709 } 710 Convey("Invalid time range", func() { 711 request.TimeRange = &pb.TimeRange{ 712 Earliest: timestamppb.New(now), 713 Latest: timestamppb.New(now.Add(-24 * time.Hour)), 714 } 715 716 response, err := server.QueryClusterSummaries(ctx, request) 717 So(err, ShouldBeRPCInvalidArgument, "time_range: earliest must be before latest") 718 So(response, ShouldBeNil) 719 }) 720 Convey("Not authorised to list clusters", func() { 721 authState.IdentityPermissions = removePermission(authState.IdentityPermissions, perms.PermListClusters) 722 723 response, err := server.QueryClusterSummaries(ctx, request) 724 So(err, ShouldBeRPCPermissionDenied, "caller does not have permission analysis.clusters.list") 725 So(response, ShouldBeNil) 726 }) 727 Convey("Not authorised to get rules", func() { 728 authState.IdentityPermissions = removePermission(authState.IdentityPermissions, perms.PermGetRule) 729 730 response, err := server.QueryClusterSummaries(ctx, request) 731 So(err, ShouldBeRPCPermissionDenied, "caller does not have permission analysis.rules.get") 732 So(response, ShouldBeNil) 733 }) 734 Convey("Not authorised to list test results in any realm", func() { 735 authState.IdentityPermissions = removePermission(authState.IdentityPermissions, rdbperms.PermListTestResults) 736 737 response, err := server.QueryClusterSummaries(ctx, request) 738 So(err, ShouldBeRPCPermissionDenied, "caller does not have permissions [resultdb.testResults.list resultdb.testExonerations.list] in any realm in project \"testproject\"") 739 So(response, ShouldBeNil) 740 }) 741 Convey("Not authorised to list test exonerations in any realm", func() { 742 authState.IdentityPermissions = removePermission(authState.IdentityPermissions, rdbperms.PermListTestExonerations) 743 744 response, err := server.QueryClusterSummaries(ctx, request) 745 So(err, ShouldBeRPCPermissionDenied, "caller does not have permissions [resultdb.testResults.list resultdb.testExonerations.list] in any realm in project \"testproject\"") 746 So(response, ShouldBeNil) 747 }) 748 Convey("Valid request", func() { 749 expectedResponse := &pb.QueryClusterSummariesResponse{ 750 ClusterSummaries: []*pb.ClusterSummary{ 751 { 752 ClusterId: &pb.ClusterId{ 753 Algorithm: "rules", 754 Id: rs[0].RuleID, 755 }, 756 Title: rs[0].RuleDefinition, 757 Bug: &pb.AssociatedBug{ 758 System: "monorail", 759 Id: "chromium/7654321", 760 LinkText: "crbug.com/7654321", 761 Url: "https://bugs.chromium.org/p/chromium/issues/detail?id=7654321", 762 }, 763 Metrics: map[string]*pb.ClusterSummary_MetricValue{ 764 metrics.HumanClsFailedPresubmit.ID.String(): { 765 Value: 1, 766 }, 767 metrics.CriticalFailuresExonerated.ID.String(): { 768 Value: 2, 769 }, 770 metrics.Failures.ID.String(): { 771 Value: 3, 772 }, 773 }, 774 }, 775 { 776 ClusterId: &pb.ClusterId{ 777 Algorithm: "reason-v3", 778 Id: "cccccc00000000000000000000000001", 779 }, 780 Title: `Example failure reason 2.`, 781 Metrics: map[string]*pb.ClusterSummary_MetricValue{ 782 metrics.HumanClsFailedPresubmit.ID.String(): { 783 Value: 4, 784 }, 785 metrics.CriticalFailuresExonerated.ID.String(): { 786 Value: 5, 787 }, 788 metrics.Failures.ID.String(): { 789 Value: 6, 790 }, 791 }, 792 }, 793 { 794 ClusterId: &pb.ClusterId{ 795 Algorithm: "rules", 796 Id: "01234567890abcdef01234567890abcdef", 797 }, 798 Title: `(rule archived)`, 799 Metrics: map[string]*pb.ClusterSummary_MetricValue{ 800 metrics.HumanClsFailedPresubmit.ID.String(): { 801 Value: 7, 802 }, 803 metrics.CriticalFailuresExonerated.ID.String(): { 804 Value: 8, 805 }, 806 metrics.Failures.ID.String(): { 807 Value: 9, 808 }, 809 }, 810 }, 811 }, 812 } 813 814 Convey("With filters and order by", func() { 815 response, err := server.QueryClusterSummaries(ctx, request) 816 So(err, ShouldBeNil) 817 So(response, ShouldResembleProto, expectedResponse) 818 }) 819 Convey("Without filters or order", func() { 820 request.FailureFilter = "" 821 request.OrderBy = "" 822 823 response, err := server.QueryClusterSummaries(ctx, request) 824 So(err, ShouldBeNil) 825 So(response, ShouldResembleProto, expectedResponse) 826 }) 827 Convey("With full view", func() { 828 request.View = pb.ClusterSummaryView_FULL 829 830 expectedFullResponse := &pb.QueryClusterSummariesResponse{ 831 ClusterSummaries: []*pb.ClusterSummary{ 832 { 833 ClusterId: &pb.ClusterId{ 834 Algorithm: "rules", 835 Id: rs[0].RuleID, 836 }, 837 Title: rs[0].RuleDefinition, 838 Bug: &pb.AssociatedBug{ 839 System: "monorail", 840 Id: "chromium/7654321", 841 LinkText: "crbug.com/7654321", 842 Url: "https://bugs.chromium.org/p/chromium/issues/detail?id=7654321", 843 }, 844 Metrics: map[string]*pb.ClusterSummary_MetricValue{ 845 metrics.HumanClsFailedPresubmit.ID.String(): { 846 Value: 1, 847 DailyBreakdown: []int64{1, 0, 0, 0, 0, 0, 0}, 848 }, 849 metrics.CriticalFailuresExonerated.ID.String(): { 850 Value: 2, 851 DailyBreakdown: []int64{1, 1, 0, 0, 0, 0, 0}, 852 }, 853 metrics.Failures.ID.String(): { 854 Value: 3, 855 DailyBreakdown: []int64{1, 1, 1, 0, 0, 0, 0}, 856 }, 857 }, 858 }, 859 { 860 ClusterId: &pb.ClusterId{ 861 Algorithm: "reason-v3", 862 Id: "cccccc00000000000000000000000001", 863 }, 864 Title: `Example failure reason 2.`, 865 Metrics: map[string]*pb.ClusterSummary_MetricValue{ 866 metrics.HumanClsFailedPresubmit.ID.String(): { 867 Value: 4, 868 DailyBreakdown: []int64{1, 1, 1, 1, 0, 0, 0}, 869 }, 870 metrics.CriticalFailuresExonerated.ID.String(): { 871 Value: 5, 872 DailyBreakdown: []int64{1, 1, 1, 1, 1, 0, 0}, 873 }, 874 metrics.Failures.ID.String(): { 875 Value: 6, 876 DailyBreakdown: []int64{1, 1, 1, 1, 1, 1, 0}, 877 }, 878 }, 879 }, 880 { 881 ClusterId: &pb.ClusterId{ 882 Algorithm: "rules", 883 Id: "01234567890abcdef01234567890abcdef", 884 }, 885 Title: `(rule archived)`, 886 Metrics: map[string]*pb.ClusterSummary_MetricValue{ 887 metrics.HumanClsFailedPresubmit.ID.String(): { 888 Value: 7, 889 DailyBreakdown: []int64{1, 1, 1, 1, 1, 1, 1}, 890 }, 891 metrics.CriticalFailuresExonerated.ID.String(): { 892 Value: 8, 893 DailyBreakdown: []int64{2, 1, 1, 1, 1, 1, 1}, 894 }, 895 metrics.Failures.ID.String(): { 896 Value: 9, 897 DailyBreakdown: []int64{2, 2, 1, 1, 1, 1, 1}, 898 }, 899 }, 900 }, 901 }, 902 } 903 904 response, err := server.QueryClusterSummaries(ctx, request) 905 So(err, ShouldBeNil) 906 So(response, ShouldResembleProto, expectedFullResponse) 907 }) 908 Convey("Without rule definition get permission", func() { 909 authState.IdentityPermissions = removePermission(authState.IdentityPermissions, perms.PermGetRuleDefinition) 910 911 // The RPC cannot return the rule definition as the 912 // cluster title as the user is not authorised to see it. 913 // Instead, it should generate a description of the 914 // content of the cluster based on what the user can see. 915 expectedResponse.ClusterSummaries[0].Title = "Selected failures in TestID 1" 916 917 response, err := server.QueryClusterSummaries(ctx, request) 918 So(err, ShouldBeNil) 919 So(response, ShouldResembleProto, expectedResponse) 920 }) 921 Convey("Without metrics", func() { 922 request.Metrics = []string{} 923 request.OrderBy = "" 924 925 for _, item := range expectedResponse.ClusterSummaries { 926 item.Metrics = make(map[string]*pb.ClusterSummary_MetricValue) 927 } 928 929 response, err := server.QueryClusterSummaries(ctx, request) 930 So(err, ShouldBeNil) 931 So(response, ShouldResembleProto, expectedResponse) 932 }) 933 }) 934 Convey("Invalid request", func() { 935 Convey("Failure filter syntax is invalid", func() { 936 request.FailureFilter = "test_id::" 937 938 // Run 939 response, err := server.QueryClusterSummaries(ctx, request) 940 941 // Verify 942 So(response, ShouldBeNil) 943 So(err, ShouldBeRPCInvalidArgument, "failure_filter: expected arg after :") 944 }) 945 Convey("Failure filter references non-existant column", func() { 946 request.FailureFilter = `test:"pita.Boot"` 947 948 // Run 949 response, err := server.QueryClusterSummaries(ctx, request) 950 951 // Verify 952 So(response, ShouldBeNil) 953 So(err, ShouldBeRPCInvalidArgument, `failure_filter: no filterable field "test"`) 954 }) 955 Convey("Failure filter references unimplemented feature", func() { 956 request.FailureFilter = "test_id<=\"blah\"" 957 958 // Run 959 response, err := server.QueryClusterSummaries(ctx, request) 960 961 // Verify 962 So(response, ShouldBeNil) 963 So(err, ShouldBeRPCInvalidArgument, "failure_filter: comparator operator not implemented yet") 964 }) 965 Convey("Metrics references non-existent metric", func() { 966 request.Metrics = []string{"projects/testproject/metrics/not-exists"} 967 // Run 968 response, err := server.QueryClusterSummaries(ctx, request) 969 970 // Verify 971 So(response, ShouldBeNil) 972 So(err, ShouldBeRPCInvalidArgument, `metrics: no metric with ID "not-exists"`) 973 }) 974 Convey("Metrics references metric in another project", func() { 975 request.Metrics = []string{"projects/anotherproject/metrics/failures"} 976 // Run 977 response, err := server.QueryClusterSummaries(ctx, request) 978 979 // Verify 980 So(response, ShouldBeNil) 981 So(err, ShouldBeRPCInvalidArgument, `metrics: metric projects/anotherproject/metrics/failures cannot be used as it is from a different LUCI Project`) 982 }) 983 Convey("Order by references metric that is not selected", func() { 984 request.Metrics = []string{"projects/testproject/metrics/failures"} 985 request.OrderBy = "metrics.`human-cls-failed-presubmit`.value desc" 986 987 // Run 988 response, err := server.QueryClusterSummaries(ctx, request) 989 990 // Verify 991 So(response, ShouldBeNil) 992 So(err, ShouldBeRPCInvalidArgument, "order_by: no sortable field named \"metrics.`human-cls-failed-presubmit`.value\", valid fields are metrics.failures.value") 993 }) 994 Convey("Order by syntax invalid", func() { 995 // To sort in ascending order, "desc" should be omittted. "asc" is not valid syntax. 996 request.OrderBy = "metrics.`human-cls-failed-presubmit`.value asc" 997 998 // Run 999 response, err := server.QueryClusterSummaries(ctx, request) 1000 1001 // Verify 1002 So(response, ShouldBeNil) 1003 So(err, ShouldBeRPCInvalidArgument, `order_by: syntax error: 1:44: unexpected token "asc"`) 1004 }) 1005 Convey("Order by syntax references invalid column", func() { 1006 request.OrderBy = "not_exists desc" 1007 1008 // Run 1009 response, err := server.QueryClusterSummaries(ctx, request) 1010 1011 // Verify 1012 So(response, ShouldBeNil) 1013 So(err, ShouldBeRPCInvalidArgument, `order_by: no sortable field named "not_exists"`) 1014 }) 1015 }) 1016 }) 1017 Convey("GetReclusteringProgress", func() { 1018 authState.IdentityPermissions = []authtest.RealmPermission{{ 1019 Realm: "testproject:@project", 1020 Permission: perms.PermGetCluster, 1021 }} 1022 1023 request := &pb.GetReclusteringProgressRequest{ 1024 Name: "projects/testproject/reclusteringProgress", 1025 } 1026 Convey("Not authorised to get cluster", func() { 1027 authState.IdentityPermissions = removePermission(authState.IdentityPermissions, perms.PermGetCluster) 1028 1029 response, err := server.GetReclusteringProgress(ctx, request) 1030 So(err, ShouldBeRPCPermissionDenied, "caller does not have permission analysis.clusters.get") 1031 So(response, ShouldBeNil) 1032 }) 1033 Convey("With a valid request", func() { 1034 rulesVersion := time.Date(2021, time.January, 1, 1, 0, 0, 0, time.UTC) 1035 reference := time.Date(2020, time.February, 1, 1, 0, 0, 0, time.UTC) 1036 configVersion := time.Date(2019, time.March, 1, 1, 0, 0, 0, time.UTC) 1037 rns := []*runs.ReclusteringRun{ 1038 runs.NewRun(0). 1039 WithProject("testproject"). 1040 WithAttemptTimestamp(reference.Add(-5 * time.Minute)). 1041 WithRulesVersion(rulesVersion). 1042 WithAlgorithmsVersion(2). 1043 WithConfigVersion(configVersion). 1044 WithNoReportedProgress(). 1045 Build(), 1046 runs.NewRun(1). 1047 WithProject("testproject"). 1048 WithAttemptTimestamp(reference.Add(-10 * time.Minute)). 1049 WithRulesVersion(rulesVersion). 1050 WithAlgorithmsVersion(2). 1051 WithConfigVersion(configVersion). 1052 WithReportedProgress(500). 1053 Build(), 1054 runs.NewRun(2). 1055 WithProject("testproject"). 1056 WithAttemptTimestamp(reference.Add(-20 * time.Minute)). 1057 WithRulesVersion(rulesVersion.Add(-1 * time.Hour)). 1058 WithAlgorithmsVersion(1). 1059 WithConfigVersion(configVersion.Add(-1 * time.Hour)). 1060 WithCompletedProgress(). 1061 Build(), 1062 } 1063 err := runs.SetRunsForTesting(ctx, rns) 1064 So(err, ShouldBeNil) 1065 1066 // Run 1067 response, err := server.GetReclusteringProgress(ctx, request) 1068 1069 // Verify. 1070 So(err, ShouldBeNil) 1071 So(response, ShouldResembleProto, &pb.ReclusteringProgress{ 1072 Name: "projects/testproject/reclusteringProgress", 1073 ProgressPerMille: 500, 1074 Last: &pb.ClusteringVersion{ 1075 AlgorithmsVersion: 1, 1076 ConfigVersion: timestamppb.New(configVersion.Add(-1 * time.Hour)), 1077 RulesVersion: timestamppb.New(rulesVersion.Add(-1 * time.Hour)), 1078 }, 1079 Next: &pb.ClusteringVersion{ 1080 AlgorithmsVersion: 2, 1081 ConfigVersion: timestamppb.New(configVersion), 1082 RulesVersion: timestamppb.New(rulesVersion), 1083 }, 1084 }) 1085 }) 1086 Convey("With an invalid request", func() { 1087 Convey("Invalid name", func() { 1088 request.Name = "invalid" 1089 1090 // Run 1091 response, err := server.GetReclusteringProgress(ctx, request) 1092 1093 // Verify 1094 So(response, ShouldBeNil) 1095 So(err, ShouldBeRPCInvalidArgument, "name: invalid reclustering progress name, expected format: projects/{project}/reclusteringProgress") 1096 }) 1097 }) 1098 }) 1099 Convey("QueryClusterFailures", func() { 1100 authState.IdentityPermissions = listTestResultsPermissions( 1101 "testproject:realm1", 1102 "testproject:realm2", 1103 "otherproject:realm3", 1104 ) 1105 authState.IdentityPermissions = append(authState.IdentityPermissions, authtest.RealmPermission{ 1106 Realm: "testproject:@project", 1107 Permission: perms.PermGetCluster, 1108 }) 1109 1110 request := &pb.QueryClusterFailuresRequest{ 1111 Parent: "projects/testproject/clusters/reason-v1/cccccc00000000000000000000000001/failures", 1112 } 1113 Convey("Not authorised to get cluster", func() { 1114 authState.IdentityPermissions = removePermission(authState.IdentityPermissions, perms.PermGetCluster) 1115 1116 response, err := server.QueryClusterFailures(ctx, request) 1117 So(err, ShouldBeRPCPermissionDenied, "caller does not have permission analysis.clusters.get") 1118 So(response, ShouldBeNil) 1119 }) 1120 Convey("Not authorised to list test results in any realm", func() { 1121 authState.IdentityPermissions = removePermission(authState.IdentityPermissions, rdbperms.PermListTestResults) 1122 1123 response, err := server.QueryClusterFailures(ctx, request) 1124 So(err, ShouldBeRPCPermissionDenied, "caller does not have permissions [resultdb.testResults.list resultdb.testExonerations.list] in any realm in project \"testproject\"") 1125 So(response, ShouldBeNil) 1126 }) 1127 Convey("Not authorised to list test exonerations in any realm", func() { 1128 authState.IdentityPermissions = removePermission(authState.IdentityPermissions, rdbperms.PermListTestExonerations) 1129 1130 response, err := server.QueryClusterFailures(ctx, request) 1131 So(err, ShouldBeRPCPermissionDenied, "caller does not have permissions [resultdb.testResults.list resultdb.testExonerations.list] in any realm in project \"testproject\"") 1132 So(response, ShouldBeNil) 1133 }) 1134 Convey("With a valid request", func() { 1135 analysisClient.expectedRealmsQueried = []string{"testproject:realm1", "testproject:realm2"} 1136 analysisClient.failuresByProjectAndCluster["testproject"] = map[clustering.ClusterID][]*analysis.ClusterFailure{ 1137 { 1138 Algorithm: "reason-v1", 1139 ID: "cccccc00000000000000000000000001", 1140 }: { 1141 { 1142 TestID: bqString("testID-1"), 1143 Variant: []*analysis.Variant{ 1144 { 1145 Key: bqString("key1"), 1146 Value: bqString("value1"), 1147 }, 1148 { 1149 Key: bqString("key2"), 1150 Value: bqString("value2"), 1151 }, 1152 }, 1153 PresubmitRunID: &analysis.PresubmitRunID{ 1154 System: bqString("luci-cv"), 1155 ID: bqString("123456789"), 1156 }, 1157 PresubmitRunOwner: bqString("user"), 1158 PresubmitRunMode: bqString(analysis.ToBQPresubmitRunMode(pb.PresubmitRunMode_QUICK_DRY_RUN)), 1159 Changelists: []*analysis.Changelist{ 1160 { 1161 Host: bqString("testproject.googlesource.com"), 1162 Change: bigquery.NullInt64{Int64: 100006, Valid: true}, 1163 Patchset: bigquery.NullInt64{Int64: 106, Valid: true}, 1164 }, 1165 { 1166 Host: bqString("testproject-internal.googlesource.com"), 1167 Change: bigquery.NullInt64{Int64: 100007, Valid: true}, 1168 Patchset: bigquery.NullInt64{Int64: 107, Valid: true}, 1169 }, 1170 }, 1171 PartitionTime: bigquery.NullTimestamp{Timestamp: time.Date(2123, time.April, 1, 2, 3, 4, 5, time.UTC), Valid: true}, 1172 Exonerations: []*analysis.Exoneration{ 1173 { 1174 Reason: bqString(pb.ExonerationReason_OCCURS_ON_MAINLINE.String()), 1175 }, 1176 { 1177 Reason: bqString(pb.ExonerationReason_NOT_CRITICAL.String()), 1178 }, 1179 }, 1180 BuildStatus: bqString(analysis.ToBQBuildStatus(pb.BuildStatus_BUILD_STATUS_FAILURE)), 1181 IsBuildCritical: bigquery.NullBool{Bool: true, Valid: true}, 1182 IngestedInvocationID: bqString("build-1234567890"), 1183 IsIngestedInvocationBlocked: bigquery.NullBool{Bool: true, Valid: true}, 1184 Count: 15, 1185 }, 1186 { 1187 TestID: bigquery.NullString{StringVal: "testID-2"}, 1188 Variant: []*analysis.Variant{ 1189 { 1190 Key: bqString("key1"), 1191 Value: bqString("value2"), 1192 }, 1193 { 1194 Key: bqString("key3"), 1195 Value: bqString("value3"), 1196 }, 1197 }, 1198 PresubmitRunID: nil, 1199 PresubmitRunOwner: bigquery.NullString{}, 1200 PresubmitRunMode: bigquery.NullString{}, 1201 Changelists: nil, 1202 PartitionTime: bigquery.NullTimestamp{Timestamp: time.Date(2124, time.May, 2, 3, 4, 5, 6, time.UTC), Valid: true}, 1203 BuildStatus: bqString(analysis.ToBQBuildStatus(pb.BuildStatus_BUILD_STATUS_CANCELED)), 1204 IsBuildCritical: bigquery.NullBool{}, 1205 IngestedInvocationID: bqString("build-9888887771"), 1206 IsIngestedInvocationBlocked: bigquery.NullBool{Bool: true, Valid: true}, 1207 Count: 1, 1208 }, 1209 }, 1210 } 1211 1212 expectedResponse := &pb.QueryClusterFailuresResponse{ 1213 Failures: []*pb.DistinctClusterFailure{ 1214 { 1215 TestId: "testID-1", 1216 Variant: pbutil.Variant("key1", "value1", "key2", "value2"), 1217 PartitionTime: timestamppb.New(time.Date(2123, time.April, 1, 2, 3, 4, 5, time.UTC)), 1218 PresubmitRun: &pb.DistinctClusterFailure_PresubmitRun{ 1219 PresubmitRunId: &pb.PresubmitRunId{ 1220 System: "luci-cv", 1221 Id: "123456789", 1222 }, 1223 Owner: "user", 1224 Mode: pb.PresubmitRunMode_QUICK_DRY_RUN, 1225 }, 1226 IsBuildCritical: true, 1227 Exonerations: []*pb.DistinctClusterFailure_Exoneration{{ 1228 Reason: pb.ExonerationReason_OCCURS_ON_MAINLINE, 1229 }, { 1230 Reason: pb.ExonerationReason_NOT_CRITICAL, 1231 }}, 1232 BuildStatus: pb.BuildStatus_BUILD_STATUS_FAILURE, 1233 IngestedInvocationId: "build-1234567890", 1234 IsIngestedInvocationBlocked: true, 1235 Changelists: []*pb.Changelist{ 1236 { 1237 Host: "testproject.googlesource.com", 1238 Change: 100006, 1239 Patchset: 106, 1240 }, 1241 { 1242 Host: "testproject-internal.googlesource.com", 1243 Change: 100007, 1244 Patchset: 107, 1245 }, 1246 }, 1247 Count: 15, 1248 }, 1249 { 1250 TestId: "testID-2", 1251 Variant: pbutil.Variant("key1", "value2", "key3", "value3"), 1252 PartitionTime: timestamppb.New(time.Date(2124, time.May, 2, 3, 4, 5, 6, time.UTC)), 1253 PresubmitRun: nil, 1254 IsBuildCritical: false, 1255 Exonerations: nil, 1256 BuildStatus: pb.BuildStatus_BUILD_STATUS_CANCELED, 1257 IngestedInvocationId: "build-9888887771", 1258 IsIngestedInvocationBlocked: true, 1259 Count: 1, 1260 }, 1261 }, 1262 } 1263 1264 Convey("Without metric filter", func() { 1265 // Run 1266 response, err := server.QueryClusterFailures(ctx, request) 1267 1268 // Verify. 1269 So(err, ShouldBeNil) 1270 So(response, ShouldResembleProto, expectedResponse) 1271 }) 1272 Convey("With metric filter", func() { 1273 request.MetricFilter = "projects/testproject/metrics/human-cls-failed-presubmit" 1274 metric, err := metrics.ByID(metrics.HumanClsFailedPresubmit.ID) 1275 So(err, ShouldBeNil) 1276 analysisClient.expectedMetricFilter = &metrics.Definition{} 1277 *analysisClient.expectedMetricFilter = metric.AdaptToProject("testproject", projectCfg.Metrics) 1278 1279 // Run 1280 response, err := server.QueryClusterFailures(ctx, request) 1281 1282 // Verify. 1283 So(err, ShouldBeNil) 1284 So(response, ShouldResembleProto, expectedResponse) 1285 1286 }) 1287 }) 1288 Convey("With an invalid request", func() { 1289 Convey("Invalid parent", func() { 1290 request.Parent = "blah" 1291 1292 // Run 1293 response, err := server.QueryClusterFailures(ctx, request) 1294 1295 // Verify 1296 So(response, ShouldBeNil) 1297 So(err, ShouldBeRPCInvalidArgument, "parent: invalid cluster failures name, expected format: projects/{project}/clusters/{cluster_alg}/{cluster_id}/failures") 1298 }) 1299 Convey("Invalid cluster algorithm in parent", func() { 1300 request.Parent = "projects/blah/clusters/reason/cccccc00000000000000000000000001/failures" 1301 1302 // Run 1303 response, err := server.QueryClusterFailures(ctx, request) 1304 1305 // Verify 1306 So(response, ShouldBeNil) 1307 So(err, ShouldBeRPCInvalidArgument, "parent: cluster id: algorithm not valid") 1308 }) 1309 Convey("Invalid cluster ID in parent", func() { 1310 request.Parent = "projects/blah/clusters/reason-v3/123/failures" 1311 1312 // Run 1313 response, err := server.QueryClusterFailures(ctx, request) 1314 1315 // Verify 1316 So(response, ShouldBeNil) 1317 So(err, ShouldBeRPCInvalidArgument, "parent: cluster id: ID is not valid lowercase hexadecimal bytes") 1318 }) 1319 Convey("Invalid metric ID format", func() { 1320 request.MetricFilter = "metrics/human-cls-failed-presubmit" 1321 1322 // Run 1323 response, err := server.QueryClusterFailures(ctx, request) 1324 1325 // Verify 1326 So(response, ShouldBeNil) 1327 So(err, ShouldBeRPCInvalidArgument, "filter_metric: invalid project metric name, expected format: projects/{project}/metrics/{metric_id}") 1328 }) 1329 Convey("Filter metric references non-existant metric", func() { 1330 request.MetricFilter = "projects/testproject/metrics/not-exists" 1331 1332 // Run 1333 response, err := server.QueryClusterFailures(ctx, request) 1334 1335 // Verify 1336 So(response, ShouldBeNil) 1337 So(err, ShouldBeRPCInvalidArgument, `filter_metric: no metric with ID "not-exists"`) 1338 }) 1339 }) 1340 }) 1341 1342 Convey("QueryExoneratedTestVariants", func() { 1343 authState.IdentityPermissions = listTestResultsPermissions( 1344 "testproject:realm1", 1345 "testproject:realm2", 1346 "otherproject:realm3", 1347 ) 1348 authState.IdentityPermissions = append(authState.IdentityPermissions, authtest.RealmPermission{ 1349 Realm: "testproject:@project", 1350 Permission: perms.PermGetCluster, 1351 }) 1352 1353 request := &pb.QueryClusterExoneratedTestVariantsRequest{ 1354 Parent: "projects/testproject/clusters/reason-v1/cccccc00000000000000000000000001/exoneratedTestVariants", 1355 } 1356 Convey("Not authorised to get cluster", func() { 1357 authState.IdentityPermissions = removePermission(authState.IdentityPermissions, perms.PermGetCluster) 1358 1359 response, err := server.QueryExoneratedTestVariants(ctx, request) 1360 So(err, ShouldBeRPCPermissionDenied, "caller does not have permission analysis.clusters.get") 1361 So(response, ShouldBeNil) 1362 }) 1363 Convey("Not authorised to list test results in any realm", func() { 1364 authState.IdentityPermissions = removePermission(authState.IdentityPermissions, rdbperms.PermListTestResults) 1365 1366 response, err := server.QueryExoneratedTestVariants(ctx, request) 1367 So(err, ShouldBeRPCPermissionDenied, "caller does not have permissions [resultdb.testResults.list resultdb.testExonerations.list] in any realm in project \"testproject\"") 1368 So(response, ShouldBeNil) 1369 }) 1370 Convey("Not authorised to list test exonerations in any realm", func() { 1371 authState.IdentityPermissions = removePermission(authState.IdentityPermissions, rdbperms.PermListTestExonerations) 1372 1373 response, err := server.QueryExoneratedTestVariants(ctx, request) 1374 So(err, ShouldBeRPCPermissionDenied, "caller does not have permissions [resultdb.testResults.list resultdb.testExonerations.list] in any realm in project \"testproject\"") 1375 So(response, ShouldBeNil) 1376 }) 1377 Convey("With a valid request", func() { 1378 analysisClient.expectedRealmsQueried = []string{"testproject:realm1", "testproject:realm2"} 1379 analysisClient.exoneratedTVsByProjectAndCluster["testproject"] = map[clustering.ClusterID][]*analysis.ExoneratedTestVariant{ 1380 { 1381 Algorithm: "reason-v1", 1382 ID: "cccccc00000000000000000000000001", 1383 }: { 1384 { 1385 TestID: bqString("testID-1"), 1386 Variant: []*analysis.Variant{ 1387 { 1388 Key: bqString("key1"), 1389 Value: bqString("value1"), 1390 }, 1391 { 1392 Key: bqString("key2"), 1393 Value: bqString("value2"), 1394 }, 1395 }, 1396 CriticalFailuresExonerated: 51, 1397 LastExoneration: bigquery.NullTimestamp{Timestamp: time.Date(2123, time.April, 1, 2, 3, 4, 5, time.UTC), Valid: true}, 1398 }, 1399 { 1400 TestID: bigquery.NullString{StringVal: "testID-2"}, 1401 Variant: []*analysis.Variant{ 1402 { 1403 Key: bqString("key1"), 1404 Value: bqString("value2"), 1405 }, 1406 { 1407 Key: bqString("key3"), 1408 Value: bqString("value3"), 1409 }, 1410 }, 1411 CriticalFailuresExonerated: 172, 1412 LastExoneration: bigquery.NullTimestamp{Timestamp: time.Date(2124, time.May, 2, 3, 4, 5, 6, time.UTC), Valid: true}, 1413 }, 1414 }, 1415 } 1416 1417 expectedResponse := &pb.QueryClusterExoneratedTestVariantsResponse{ 1418 TestVariants: []*pb.ClusterExoneratedTestVariant{ 1419 { 1420 TestId: "testID-1", 1421 Variant: pbutil.Variant("key1", "value1", "key2", "value2"), 1422 CriticalFailuresExonerated: 51, 1423 LastExoneration: timestamppb.New(time.Date(2123, time.April, 1, 2, 3, 4, 5, time.UTC)), 1424 }, 1425 { 1426 TestId: "testID-2", 1427 Variant: pbutil.Variant("key1", "value2", "key3", "value3"), 1428 CriticalFailuresExonerated: 172, 1429 LastExoneration: timestamppb.New(time.Date(2124, time.May, 2, 3, 4, 5, 6, time.UTC)), 1430 }, 1431 }, 1432 } 1433 1434 // Run 1435 response, err := server.QueryExoneratedTestVariants(ctx, request) 1436 1437 // Verify. 1438 So(err, ShouldBeNil) 1439 So(response, ShouldResembleProto, expectedResponse) 1440 }) 1441 Convey("With an invalid request", func() { 1442 Convey("Invalid parent", func() { 1443 request.Parent = "blah" 1444 1445 // Run 1446 response, err := server.QueryExoneratedTestVariants(ctx, request) 1447 1448 // Verify 1449 So(response, ShouldBeNil) 1450 So(err, ShouldBeRPCInvalidArgument, "parent: invalid resource name, expected format: projects/{project}/clusters/{cluster_alg}/{cluster_id}/exoneratedTestVariants") 1451 }) 1452 Convey("Invalid cluster algorithm in parent", func() { 1453 request.Parent = "projects/blah/clusters/reason/cccccc00000000000000000000000001/exoneratedTestVariants" 1454 1455 // Run 1456 response, err := server.QueryExoneratedTestVariants(ctx, request) 1457 1458 // Verify 1459 So(response, ShouldBeNil) 1460 So(err, ShouldBeRPCInvalidArgument, "parent: cluster id: algorithm not valid") 1461 }) 1462 Convey("Invalid cluster ID in parent", func() { 1463 request.Parent = "projects/blah/clusters/reason-v3/123/exoneratedTestVariants" 1464 1465 // Run 1466 response, err := server.QueryExoneratedTestVariants(ctx, request) 1467 1468 // Verify 1469 So(response, ShouldBeNil) 1470 So(err, ShouldBeRPCInvalidArgument, "parent: cluster id: ID is not valid lowercase hexadecimal bytes") 1471 }) 1472 }) 1473 }) 1474 1475 Convey("QueryExoneratedTestVariantBranches", func() { 1476 authState.IdentityPermissions = listTestResultsPermissions( 1477 "testproject:realm1", 1478 "testproject:realm2", 1479 "otherproject:realm3", 1480 ) 1481 authState.IdentityPermissions = append(authState.IdentityPermissions, authtest.RealmPermission{ 1482 Realm: "testproject:@project", 1483 Permission: perms.PermGetCluster, 1484 }) 1485 1486 request := &pb.QueryClusterExoneratedTestVariantBranchesRequest{ 1487 Parent: "projects/testproject/clusters/reason-v1/cccccc00000000000000000000000001/exoneratedTestVariantBranches", 1488 } 1489 Convey("Not authorised to get cluster", func() { 1490 authState.IdentityPermissions = removePermission(authState.IdentityPermissions, perms.PermGetCluster) 1491 1492 response, err := server.QueryExoneratedTestVariantBranches(ctx, request) 1493 So(err, ShouldBeRPCPermissionDenied, "caller does not have permission analysis.clusters.get") 1494 So(response, ShouldBeNil) 1495 }) 1496 Convey("Not authorised to list test results in any realm", func() { 1497 authState.IdentityPermissions = removePermission(authState.IdentityPermissions, rdbperms.PermListTestResults) 1498 1499 response, err := server.QueryExoneratedTestVariantBranches(ctx, request) 1500 So(err, ShouldBeRPCPermissionDenied, "caller does not have permissions [resultdb.testResults.list resultdb.testExonerations.list] in any realm in project \"testproject\"") 1501 So(response, ShouldBeNil) 1502 }) 1503 Convey("Not authorised to list test exonerations in any realm", func() { 1504 authState.IdentityPermissions = removePermission(authState.IdentityPermissions, rdbperms.PermListTestExonerations) 1505 1506 response, err := server.QueryExoneratedTestVariantBranches(ctx, request) 1507 So(err, ShouldBeRPCPermissionDenied, "caller does not have permissions [resultdb.testResults.list resultdb.testExonerations.list] in any realm in project \"testproject\"") 1508 So(response, ShouldBeNil) 1509 }) 1510 Convey("With a valid request", func() { 1511 analysisClient.expectedRealmsQueried = []string{"testproject:realm1", "testproject:realm2"} 1512 analysisClient.exoneratedTVBsByProjectAndCluster["testproject"] = map[clustering.ClusterID][]*analysis.ExoneratedTestVariantBranch{ 1513 { 1514 Algorithm: "reason-v1", 1515 ID: "cccccc00000000000000000000000001", 1516 }: { 1517 { 1518 Project: bqString("testproject"), 1519 TestID: bqString("testID-1"), 1520 Variant: []*analysis.Variant{ 1521 { 1522 Key: bqString("key1"), 1523 Value: bqString("value1"), 1524 }, 1525 { 1526 Key: bqString("key2"), 1527 Value: bqString("value2"), 1528 }, 1529 }, 1530 SourceRef: analysis.SourceRef{ 1531 Gitiles: &analysis.GitilesRef{ 1532 Host: bqString("myproject.googlesource.com"), 1533 Project: bqString("myproject/src"), 1534 Ref: bqString("refs/heads/main"), 1535 }, 1536 }, 1537 CriticalFailuresExonerated: 51, 1538 LastExoneration: bigquery.NullTimestamp{Timestamp: time.Date(2123, time.April, 1, 2, 3, 4, 5, time.UTC), Valid: true}, 1539 }, 1540 { 1541 Project: bqString("testproject"), 1542 TestID: bigquery.NullString{StringVal: "testID-2"}, 1543 Variant: []*analysis.Variant{ 1544 { 1545 Key: bqString("key1"), 1546 Value: bqString("value2"), 1547 }, 1548 { 1549 Key: bqString("key3"), 1550 Value: bqString("value3"), 1551 }, 1552 }, 1553 SourceRef: analysis.SourceRef{ 1554 Gitiles: &analysis.GitilesRef{ 1555 Host: bqString("myproject2.googlesource.com"), 1556 Project: bqString("myproject2/src"), 1557 Ref: bqString("refs/heads/main2"), 1558 }, 1559 }, 1560 CriticalFailuresExonerated: 172, 1561 LastExoneration: bigquery.NullTimestamp{Timestamp: time.Date(2124, time.May, 2, 3, 4, 5, 6, time.UTC), Valid: true}, 1562 }, 1563 }, 1564 } 1565 1566 expectedResponse := &pb.QueryClusterExoneratedTestVariantBranchesResponse{ 1567 TestVariantBranches: []*pb.ClusterExoneratedTestVariantBranch{ 1568 { 1569 Project: "testproject", 1570 TestId: "testID-1", 1571 Variant: pbutil.Variant("key1", "value1", "key2", "value2"), 1572 SourceRef: &pb.SourceRef{ 1573 System: &pb.SourceRef_Gitiles{ 1574 Gitiles: &pb.GitilesRef{ 1575 Host: "myproject.googlesource.com", 1576 Project: "myproject/src", 1577 Ref: "refs/heads/main", 1578 }, 1579 }, 1580 }, 1581 CriticalFailuresExonerated: 51, 1582 LastExoneration: timestamppb.New(time.Date(2123, time.April, 1, 2, 3, 4, 5, time.UTC)), 1583 }, 1584 { 1585 Project: "testproject", 1586 TestId: "testID-2", 1587 Variant: pbutil.Variant("key1", "value2", "key3", "value3"), 1588 SourceRef: &pb.SourceRef{ 1589 System: &pb.SourceRef_Gitiles{ 1590 Gitiles: &pb.GitilesRef{ 1591 Host: "myproject2.googlesource.com", 1592 Project: "myproject2/src", 1593 Ref: "refs/heads/main2", 1594 }, 1595 }, 1596 }, 1597 CriticalFailuresExonerated: 172, 1598 LastExoneration: timestamppb.New(time.Date(2124, time.May, 2, 3, 4, 5, 6, time.UTC)), 1599 }, 1600 }, 1601 } 1602 1603 // Run 1604 response, err := server.QueryExoneratedTestVariantBranches(ctx, request) 1605 1606 // Verify. 1607 So(err, ShouldBeNil) 1608 So(response, ShouldResembleProto, expectedResponse) 1609 }) 1610 Convey("With an invalid request", func() { 1611 Convey("Invalid parent", func() { 1612 request.Parent = "blah" 1613 1614 // Run 1615 response, err := server.QueryExoneratedTestVariantBranches(ctx, request) 1616 1617 // Verify 1618 So(response, ShouldBeNil) 1619 So(err, ShouldBeRPCInvalidArgument, "parent: invalid resource name, expected format: projects/{project}/clusters/{cluster_alg}/{cluster_id}/exoneratedTestVariantBranches") 1620 }) 1621 Convey("Invalid cluster algorithm in parent", func() { 1622 request.Parent = "projects/blah/clusters/reason/cccccc00000000000000000000000001/exoneratedTestVariantBranches" 1623 1624 // Run 1625 response, err := server.QueryExoneratedTestVariantBranches(ctx, request) 1626 1627 // Verify 1628 So(response, ShouldBeNil) 1629 So(err, ShouldBeRPCInvalidArgument, "parent: cluster id: algorithm not valid") 1630 }) 1631 Convey("Invalid cluster ID in parent", func() { 1632 request.Parent = "projects/blah/clusters/reason-v3/123/exoneratedTestVariantBranches" 1633 1634 // Run 1635 response, err := server.QueryExoneratedTestVariantBranches(ctx, request) 1636 1637 // Verify 1638 So(response, ShouldBeNil) 1639 So(err, ShouldBeRPCInvalidArgument, "parent: cluster id: ID is not valid lowercase hexadecimal bytes") 1640 }) 1641 }) 1642 }) 1643 }) 1644 } 1645 1646 func bqString(value string) bigquery.NullString { 1647 return bigquery.NullString{StringVal: value, Valid: true} 1648 } 1649 1650 func listTestResultsPermissions(realms ...string) []authtest.RealmPermission { 1651 var result []authtest.RealmPermission 1652 for _, r := range realms { 1653 result = append(result, authtest.RealmPermission{ 1654 Realm: r, 1655 Permission: rdbperms.PermListTestResults, 1656 }) 1657 result = append(result, authtest.RealmPermission{ 1658 Realm: r, 1659 Permission: rdbperms.PermListTestExonerations, 1660 }) 1661 } 1662 return result 1663 } 1664 1665 func removePermission(perms []authtest.RealmPermission, permission realms.Permission) []authtest.RealmPermission { 1666 var result []authtest.RealmPermission 1667 for _, p := range perms { 1668 if p.Permission != permission { 1669 result = append(result, p) 1670 } 1671 } 1672 return result 1673 } 1674 1675 func emptyMetricValues() *pb.Cluster_TimewiseCounts { 1676 return &pb.Cluster_TimewiseCounts{ 1677 OneDay: &pb.Cluster_Counts{}, 1678 ThreeDay: &pb.Cluster_Counts{}, 1679 SevenDay: &pb.Cluster_Counts{}, 1680 } 1681 } 1682 1683 func failureReasonClusterEntry(projectcfg *compiledcfg.ProjectConfig, primaryErrorMessage string) *pb.ClusterResponse_ClusteredTestResult_ClusterEntry { 1684 alg := &failurereason.Algorithm{} 1685 clusterID := alg.Cluster(projectcfg, &clustering.Failure{ 1686 Reason: &pb.FailureReason{ 1687 PrimaryErrorMessage: primaryErrorMessage, 1688 }, 1689 }) 1690 return &pb.ClusterResponse_ClusteredTestResult_ClusterEntry{ 1691 ClusterId: &pb.ClusterId{ 1692 Algorithm: failurereason.AlgorithmName, 1693 Id: hex.EncodeToString(clusterID), 1694 }, 1695 } 1696 } 1697 1698 func testNameClusterEntry(projectcfg *compiledcfg.ProjectConfig, testID string) *pb.ClusterResponse_ClusteredTestResult_ClusterEntry { 1699 alg := &testname.Algorithm{} 1700 clusterID := alg.Cluster(projectcfg, &clustering.Failure{ 1701 TestID: testID, 1702 }) 1703 return &pb.ClusterResponse_ClusteredTestResult_ClusterEntry{ 1704 ClusterId: &pb.ClusterId{ 1705 Algorithm: testname.AlgorithmName, 1706 Id: hex.EncodeToString(clusterID), 1707 }, 1708 } 1709 } 1710 1711 // sortClusterEntries sorts clusters by ascending Cluster ID. 1712 func sortClusterEntries(entries []*pb.ClusterResponse_ClusteredTestResult_ClusterEntry) []*pb.ClusterResponse_ClusteredTestResult_ClusterEntry { 1713 result := make([]*pb.ClusterResponse_ClusteredTestResult_ClusterEntry, len(entries)) 1714 copy(result, entries) 1715 sort.Slice(result, func(i, j int) bool { 1716 if result[i].ClusterId.Algorithm != result[j].ClusterId.Algorithm { 1717 return result[i].ClusterId.Algorithm < result[j].ClusterId.Algorithm 1718 } 1719 return result[i].ClusterId.Id < result[j].ClusterId.Id 1720 }) 1721 return result 1722 } 1723 1724 type fakeAnalysisClient struct { 1725 clustersByProject map[string][]*analysis.Cluster 1726 failuresByProjectAndCluster map[string]map[clustering.ClusterID][]*analysis.ClusterFailure 1727 exoneratedTVsByProjectAndCluster map[string]map[clustering.ClusterID][]*analysis.ExoneratedTestVariant 1728 exoneratedTVBsByProjectAndCluster map[string]map[clustering.ClusterID][]*analysis.ExoneratedTestVariantBranch 1729 clusterMetricsByProject map[string][]*analysis.ClusterSummary 1730 clusterMetricBreakdownsByProject map[string][]*analysis.ClusterMetricBreakdown 1731 expectedRealmsQueried []string 1732 expectedMetricFilter *metrics.Definition 1733 } 1734 1735 func newFakeAnalysisClient() *fakeAnalysisClient { 1736 return &fakeAnalysisClient{ 1737 clustersByProject: make(map[string][]*analysis.Cluster), 1738 failuresByProjectAndCluster: make(map[string]map[clustering.ClusterID][]*analysis.ClusterFailure), 1739 exoneratedTVsByProjectAndCluster: make(map[string]map[clustering.ClusterID][]*analysis.ExoneratedTestVariant), 1740 exoneratedTVBsByProjectAndCluster: make(map[string]map[clustering.ClusterID][]*analysis.ExoneratedTestVariantBranch), 1741 clusterMetricsByProject: make(map[string][]*analysis.ClusterSummary), 1742 clusterMetricBreakdownsByProject: make(map[string][]*analysis.ClusterMetricBreakdown), 1743 } 1744 } 1745 1746 func (f *fakeAnalysisClient) ReadCluster(ctx context.Context, project string, clusterID clustering.ClusterID) (*analysis.Cluster, error) { 1747 clusters, ok := f.clustersByProject[project] 1748 if !ok { 1749 return nil, nil 1750 } 1751 1752 var result *analysis.Cluster 1753 for _, c := range clusters { 1754 if c.ClusterID == clusterID { 1755 result = c 1756 break 1757 } 1758 } 1759 if result == nil { 1760 return analysis.EmptyCluster(clusterID), nil 1761 } 1762 return result, nil 1763 } 1764 1765 func (f *fakeAnalysisClient) QueryClusterSummaries(ctx context.Context, project string, options *analysis.QueryClusterSummariesOptions) ([]*analysis.ClusterSummary, error) { 1766 clusters, ok := f.clusterMetricsByProject[project] 1767 if !ok { 1768 return nil, nil 1769 } 1770 1771 set := stringset.NewFromSlice(options.Realms...) 1772 if set.Len() != len(f.expectedRealmsQueried) || !set.HasAll(f.expectedRealmsQueried...) { 1773 panic("realms passed to QueryClusterSummaries do not match expected") 1774 } 1775 1776 _, _, err := analysis.ClusteredFailuresTable.WhereClause(options.FailureFilter, "w_") 1777 if err != nil { 1778 return nil, analysis.InvalidArgumentTag.Apply(errors.Annotate(err, "failure_filter").Err()) 1779 } 1780 _, err = analysis.ClusterSummariesTable(options.Metrics).OrderByClause(options.OrderBy) 1781 if err != nil { 1782 return nil, analysis.InvalidArgumentTag.Apply(errors.Annotate(err, "order_by").Err()) 1783 } 1784 1785 var results []*analysis.ClusterSummary 1786 for _, c := range clusters { 1787 results = append(results, copyClusterSummary(c, options.Metrics, options.IncludeMetricBreakdown)) 1788 } 1789 return results, nil 1790 } 1791 1792 func copyClusterSummary(cs *analysis.ClusterSummary, queriedMetrics []metrics.Definition, includeMetricBreakdown bool) *analysis.ClusterSummary { 1793 result := &analysis.ClusterSummary{ 1794 ClusterID: cs.ClusterID, 1795 ExampleFailureReason: cs.ExampleFailureReason, 1796 ExampleTestID: cs.ExampleTestID, 1797 UniqueTestIDs: cs.UniqueTestIDs, 1798 MetricValues: make(map[metrics.ID]*analysis.MetricValue), 1799 } 1800 for _, m := range queriedMetrics { 1801 metricValue := &analysis.MetricValue{ 1802 Value: cs.MetricValues[m.ID].Value, 1803 } 1804 if includeMetricBreakdown { 1805 metricValue.DailyBreakdown = cs.MetricValues[m.ID].DailyBreakdown 1806 } 1807 result.MetricValues[m.ID] = metricValue 1808 } 1809 return result 1810 } 1811 1812 func (f *fakeAnalysisClient) ReadClusterFailures(ctx context.Context, options analysis.ReadClusterFailuresOptions) ([]*analysis.ClusterFailure, error) { 1813 failuresByCluster, ok := f.failuresByProjectAndCluster[options.Project] 1814 if !ok { 1815 return nil, nil 1816 } 1817 if f.expectedMetricFilter != nil && (options.MetricFilter == nil || *options.MetricFilter != *f.expectedMetricFilter) || 1818 f.expectedMetricFilter == nil && options.MetricFilter != nil { 1819 panic("filter metric passed to ReadClusterFailures does not match expected") 1820 } 1821 1822 set := stringset.NewFromSlice(options.Realms...) 1823 if set.Len() != len(f.expectedRealmsQueried) || !set.HasAll(f.expectedRealmsQueried...) { 1824 panic("realms passed to ReadClusterFailures do not match expected") 1825 } 1826 1827 return failuresByCluster[options.ClusterID], nil 1828 } 1829 1830 func (f *fakeAnalysisClient) ReadClusterExoneratedTestVariants(ctx context.Context, options analysis.ReadClusterExoneratedTestVariantsOptions) ([]*analysis.ExoneratedTestVariant, error) { 1831 exoneratedTVsByCluster, ok := f.exoneratedTVsByProjectAndCluster[options.Project] 1832 if !ok { 1833 return nil, nil 1834 } 1835 1836 set := stringset.NewFromSlice(options.Realms...) 1837 if set.Len() != len(f.expectedRealmsQueried) || !set.HasAll(f.expectedRealmsQueried...) { 1838 panic("realms passed to ReadClusterExoneratedTestVariants do not match expected") 1839 } 1840 1841 return exoneratedTVsByCluster[options.ClusterID], nil 1842 } 1843 1844 func (f *fakeAnalysisClient) ReadClusterExoneratedTestVariantBranches(ctx context.Context, options analysis.ReadClusterExoneratedTestVariantBranchesOptions) ([]*analysis.ExoneratedTestVariantBranch, error) { 1845 exoneratedTVBsByCluster, ok := f.exoneratedTVBsByProjectAndCluster[options.Project] 1846 if !ok { 1847 return nil, nil 1848 } 1849 1850 set := stringset.NewFromSlice(options.Realms...) 1851 if set.Len() != len(f.expectedRealmsQueried) || !set.HasAll(f.expectedRealmsQueried...) { 1852 panic("realms passed to ReadClusterExoneratedTestVariantBranches do not match expected") 1853 } 1854 1855 return exoneratedTVBsByCluster[options.ClusterID], nil 1856 } 1857 1858 func (f *fakeAnalysisClient) ReadClusterHistory(ctx context.Context, options analysis.ReadClusterHistoryOptions) (ret []*analysis.ReadClusterHistoryDay, err error) { 1859 return nil, nil 1860 }