go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/rpc/test_variant_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 "fmt" 19 "testing" 20 21 "google.golang.org/grpc/codes" 22 grpcStatus "google.golang.org/grpc/status" 23 24 "go.chromium.org/luci/common/clock/testclock" 25 "go.chromium.org/luci/gae/impl/memory" 26 "go.chromium.org/luci/resultdb/rdbperms" 27 "go.chromium.org/luci/server/auth" 28 "go.chromium.org/luci/server/auth/authtest" 29 "go.chromium.org/luci/server/secrets" 30 "go.chromium.org/luci/server/secrets/testsecrets" 31 32 "go.chromium.org/luci/analysis/internal/config" 33 "go.chromium.org/luci/analysis/internal/perms" 34 "go.chromium.org/luci/analysis/internal/testresults" 35 "go.chromium.org/luci/analysis/internal/testresults/stability" 36 "go.chromium.org/luci/analysis/internal/testutil" 37 "go.chromium.org/luci/analysis/pbutil" 38 configpb "go.chromium.org/luci/analysis/proto/config" 39 pb "go.chromium.org/luci/analysis/proto/v1" 40 41 . "github.com/smartystreets/goconvey/convey" 42 . "go.chromium.org/luci/common/testing/assertions" 43 ) 44 45 func TestTestVariantsServer(t *testing.T) { 46 Convey("Given a test variants server", t, func() { 47 ctx := testutil.IntegrationTestContext(t) 48 49 // For user identification. 50 ctx = authtest.MockAuthConfig(ctx) 51 authState := &authtest.FakeState{ 52 Identity: "user:someone@example.com", 53 IdentityGroups: []string{"luci-analysis-access"}, 54 } 55 ctx = auth.WithState(ctx, authState) 56 ctx = secrets.Use(ctx, &testsecrets.Store{}) 57 58 // Provides datastore implementation needed for project config. 59 ctx = memory.Use(ctx) 60 server := NewTestVariantsServer() 61 62 Convey("Unauthorised requests are rejected", func() { 63 ctx = auth.WithState(ctx, &authtest.FakeState{ 64 Identity: "user:someone@example.com", 65 // Not a member of luci-analysis-access. 66 IdentityGroups: []string{"other-group"}, 67 }) 68 69 // Make some request (the request should not matter, as 70 // a common decorator is used for all requests.) 71 request := &pb.QueryTestVariantStabilityRequest{} 72 73 response, err := server.QueryStability(ctx, request) 74 So(err, ShouldBeRPCPermissionDenied, "not a member of luci-analysis-access") 75 So(response, ShouldBeNil) 76 }) 77 Convey("QueryFailureRate", func() { 78 // Grant the permissions needed for this RPC. 79 authState.IdentityPermissions = []authtest.RealmPermission{ 80 { 81 Realm: "project:realm", 82 Permission: rdbperms.PermListTestResults, 83 }, 84 } 85 86 err := testresults.CreateQueryFailureRateTestData(ctx) 87 So(err, ShouldBeNil) 88 89 Convey("Valid input", func() { 90 project, asAtTime, tvs := testresults.QueryFailureRateSampleRequest() 91 request := &pb.QueryTestVariantFailureRateRequest{ 92 Project: project, 93 TestVariants: tvs, 94 } 95 ctx, _ := testclock.UseTime(ctx, asAtTime) 96 97 response, err := server.QueryFailureRate(ctx, request) 98 So(err, ShouldBeNil) 99 100 expectedResult := testresults.QueryFailureRateSampleResponse() 101 So(response, ShouldResembleProto, expectedResult) 102 }) 103 Convey("Query by VariantHash", func() { 104 project, asAtTime, tvs := testresults.QueryFailureRateSampleRequest() 105 for _, tv := range tvs { 106 tv.VariantHash = pbutil.VariantHash(tv.Variant) 107 tv.Variant = nil 108 } 109 request := &pb.QueryTestVariantFailureRateRequest{ 110 Project: project, 111 TestVariants: tvs, 112 } 113 ctx, _ := testclock.UseTime(ctx, asAtTime) 114 115 response, err := server.QueryFailureRate(ctx, request) 116 So(err, ShouldBeNil) 117 118 expectedResult := testresults.QueryFailureRateSampleResponse() 119 for _, tv := range expectedResult.TestVariants { 120 tv.VariantHash = pbutil.VariantHash(tv.Variant) 121 tv.Variant = nil 122 } 123 So(response, ShouldResembleProto, expectedResult) 124 }) 125 Convey("No list test results permission", func() { 126 authState.IdentityPermissions = []authtest.RealmPermission{ 127 { 128 // This permission is for a project other than the one 129 // being queried. 130 Realm: "otherproject:realm", 131 Permission: rdbperms.PermListTestResults, 132 }, 133 } 134 135 project, asAtTime, tvs := testresults.QueryFailureRateSampleRequest() 136 request := &pb.QueryTestVariantFailureRateRequest{ 137 Project: project, 138 TestVariants: tvs, 139 } 140 ctx, _ := testclock.UseTime(ctx, asAtTime) 141 142 response, err := server.QueryFailureRate(ctx, request) 143 So(err, ShouldBeRPCPermissionDenied, "caller does not have permissions [resultdb.testResults.list] in any realm") 144 So(response, ShouldBeNil) 145 }) 146 Convey("Invalid input", func() { 147 // This checks at least one case of invalid input is detected, sufficient to verify 148 // validation is invoked. 149 // Exhaustive checking of request validation is performed in TestValidateQueryRateRequest. 150 request := &pb.QueryTestVariantFailureRateRequest{ 151 Project: "", 152 TestVariants: []*pb.TestVariantIdentifier{ 153 { 154 TestId: "my_test", 155 }, 156 }, 157 } 158 159 response, err := server.QueryFailureRate(ctx, request) 160 st, _ := grpcStatus.FromError(err) 161 So(st.Code(), ShouldEqual, codes.InvalidArgument) 162 So(st.Message(), ShouldEqual, `project: unspecified`) 163 So(response, ShouldBeNil) 164 }) 165 }) 166 Convey("QueryStability", func() { 167 // Grant the permissions needed for this RPC. 168 authState.IdentityPermissions = []authtest.RealmPermission{ 169 { 170 Realm: "project:realm", 171 Permission: rdbperms.PermListTestResults, 172 }, 173 { 174 Realm: "project:@project", 175 Permission: perms.PermGetConfig, 176 }, 177 } 178 179 err := stability.CreateQueryStabilityTestData(ctx) 180 So(err, ShouldBeNil) 181 182 opts := stability.QueryStabilitySampleRequest() 183 request := &pb.QueryTestVariantStabilityRequest{ 184 Project: opts.Project, 185 TestVariants: opts.TestVariantPositions, 186 } 187 ctx, _ := testclock.UseTime(ctx, opts.AsAtTime) 188 189 projectCfg := config.CreateConfigWithBothBuganizerAndMonorail(configpb.BugSystem_MONORAIL) 190 projectCfg.TestStabilityCriteria = toTestStabilityCriteriaConfig(opts.Criteria) 191 configs := make(map[string]*configpb.ProjectConfig) 192 configs["project"] = projectCfg 193 err = config.SetTestProjectConfig(ctx, configs) 194 So(err, ShouldBeNil) 195 196 Convey("Valid input", func() { 197 rsp, err := server.QueryStability(ctx, request) 198 So(err, ShouldBeNil) 199 200 expectedResult := &pb.QueryTestVariantStabilityResponse{ 201 TestVariants: stability.QueryStabilitySampleResponse(), 202 Criteria: opts.Criteria, 203 } 204 So(rsp, ShouldResembleProto, expectedResult) 205 }) 206 Convey("Query by VariantHash", func() { 207 for _, tv := range request.TestVariants { 208 tv.VariantHash = pbutil.VariantHash(tv.Variant) 209 tv.Variant = nil 210 } 211 rsp, err := server.QueryStability(ctx, request) 212 So(err, ShouldBeNil) 213 214 expectedAnalysis := stability.QueryStabilitySampleResponse() 215 for _, tv := range expectedAnalysis { 216 tv.VariantHash = pbutil.VariantHash(tv.Variant) 217 tv.Variant = nil 218 } 219 expectedResult := &pb.QueryTestVariantStabilityResponse{ 220 TestVariants: expectedAnalysis, 221 Criteria: opts.Criteria, 222 } 223 So(rsp, ShouldResembleProto, expectedResult) 224 }) 225 Convey("No test stability configuration", func() { 226 // Remove test stability configuration. 227 projectCfg.TestStabilityCriteria = nil 228 err = config.SetTestProjectConfig(ctx, configs) 229 So(err, ShouldBeNil) 230 231 response, err := server.QueryStability(ctx, request) 232 So(err, ShouldBeRPCFailedPrecondition, "project has not defined test stability criteria; set test_stability_criteria in project configuration and try again") 233 So(response, ShouldBeNil) 234 }) 235 Convey("No list test results permission", func() { 236 authState.IdentityPermissions = removePermission(authState.IdentityPermissions, rdbperms.PermListTestResults) 237 238 response, err := server.QueryStability(ctx, request) 239 So(err, ShouldBeRPCPermissionDenied, "caller does not have permissions [resultdb.testResults.list] in any realm") 240 So(response, ShouldBeNil) 241 }) 242 Convey("No get project config permission", func() { 243 authState.IdentityPermissions = removePermission(authState.IdentityPermissions, perms.PermGetConfig) 244 245 response, err := server.QueryStability(ctx, request) 246 So(err, ShouldBeRPCPermissionDenied, `caller does not have permission analysis.config.get in realm "project:@project"`) 247 So(response, ShouldBeNil) 248 }) 249 Convey("Invalid input", func() { 250 // This checks at least one case of invalid input is detected, sufficient to verify 251 // validation is invoked. 252 // Exhaustive checking of request validation is performed in 253 // TestValidateQueryTestVariantStabilityRequest. 254 request.Project = "" 255 256 response, err := server.QueryStability(ctx, request) 257 So(err, ShouldBeRPCInvalidArgument, `project: unspecified`) 258 So(response, ShouldBeNil) 259 }) 260 }) 261 }) 262 } 263 264 func TestValidateQueryFailureRateRequest(t *testing.T) { 265 Convey("ValidateQueryFailureRateRequest", t, func() { 266 req := &pb.QueryTestVariantFailureRateRequest{ 267 Project: "project", 268 TestVariants: []*pb.TestVariantIdentifier{ 269 { 270 TestId: "my_test", 271 // Variant is optional as not all tests have variants. 272 }, 273 { 274 TestId: "my_test2", 275 Variant: pbutil.Variant("key1", "val1", "key2", "val2"), 276 }, 277 }, 278 } 279 280 Convey("valid", func() { 281 err := validateQueryTestVariantFailureRateRequest(req) 282 So(err, ShouldBeNil) 283 }) 284 285 Convey("no project", func() { 286 req.Project = "" 287 err := validateQueryTestVariantFailureRateRequest(req) 288 So(err, ShouldErrLike, "project: unspecified") 289 }) 290 291 Convey("invalid project", func() { 292 req.Project = ":" 293 err := validateQueryTestVariantFailureRateRequest(req) 294 So(err, ShouldErrLike, `project: must match ^[a-z0-9\-]{1,40}$`) 295 }) 296 297 Convey("no test variants", func() { 298 req.TestVariants = nil 299 err := validateQueryTestVariantFailureRateRequest(req) 300 So(err, ShouldErrLike, `test_variants: unspecified`) 301 }) 302 303 Convey("too many test variants", func() { 304 req.TestVariants = make([]*pb.TestVariantIdentifier, 0, 101) 305 for i := 0; i < 101; i++ { 306 req.TestVariants = append(req.TestVariants, &pb.TestVariantIdentifier{ 307 TestId: fmt.Sprintf("test_id%v", i), 308 }) 309 } 310 err := validateQueryTestVariantFailureRateRequest(req) 311 So(err, ShouldErrLike, `no more than 100 may be queried at a time`) 312 }) 313 314 Convey("no test id", func() { 315 req.TestVariants[1].TestId = "" 316 err := validateQueryTestVariantFailureRateRequest(req) 317 So(err, ShouldErrLike, `test_variants[1]: test_id: unspecified`) 318 }) 319 320 Convey("variant_hash invalid", func() { 321 req.TestVariants[1].VariantHash = "invalid" 322 err := validateQueryTestVariantFailureRateRequest(req) 323 So(err, ShouldErrLike, `test_variants[1]: variant_hash: must match ^[0-9a-f]{16}$`) 324 }) 325 326 Convey("variant_hash mismatch with variant", func() { 327 req.TestVariants[1].VariantHash = "0123456789abcdef" 328 err := validateQueryTestVariantFailureRateRequest(req) 329 So(err, ShouldErrLike, `test_variants[1]: variant and variant_hash mismatch`) 330 }) 331 332 Convey("duplicate test variants", func() { 333 req.TestVariants = []*pb.TestVariantIdentifier{ 334 { 335 TestId: "my_test", 336 Variant: pbutil.Variant("key1", "val1", "key2", "val2"), 337 }, 338 { 339 TestId: "my_test", 340 Variant: pbutil.Variant("key1", "val1", "key2", "val2"), 341 }, 342 } 343 err := validateQueryTestVariantFailureRateRequest(req) 344 So(err, ShouldErrLike, `test_variants[1]: already requested in the same request`) 345 }) 346 }) 347 } 348 349 func TestValidateQueryTestVariantStabilityRequest(t *testing.T) { 350 Convey("ValidateQueryTestVariantStabilityRequest", t, func() { 351 req := &pb.QueryTestVariantStabilityRequest{ 352 Project: "project", 353 TestVariants: []*pb.QueryTestVariantStabilityRequest_TestVariantPosition{ 354 { 355 TestId: "my_test", 356 // Variant is optional as not all tests have variants. 357 Sources: testSources(), 358 }, 359 { 360 TestId: "my_test2", 361 Variant: pbutil.Variant("key1", "val1", "key2", "val2"), 362 Sources: testSources(), 363 }, 364 }, 365 } 366 367 Convey("valid", func() { 368 err := validateQueryTestVariantStabilityRequest(req) 369 So(err, ShouldBeNil) 370 }) 371 372 Convey("no project", func() { 373 req.Project = "" 374 err := validateQueryTestVariantStabilityRequest(req) 375 So(err, ShouldErrLike, "project: unspecified") 376 }) 377 378 Convey("invalid project", func() { 379 req.Project = ":" 380 err := validateQueryTestVariantStabilityRequest(req) 381 So(err, ShouldErrLike, `project: must match ^[a-z0-9\-]{1,40}$`) 382 }) 383 384 Convey("no test variants", func() { 385 req.TestVariants = nil 386 err := validateQueryTestVariantStabilityRequest(req) 387 So(err, ShouldErrLike, `test_variants: unspecified`) 388 }) 389 390 Convey("too many test variants", func() { 391 req.TestVariants = make([]*pb.QueryTestVariantStabilityRequest_TestVariantPosition, 0, 101) 392 for i := 0; i < 101; i++ { 393 req.TestVariants = append(req.TestVariants, &pb.QueryTestVariantStabilityRequest_TestVariantPosition{ 394 TestId: fmt.Sprintf("test_id%v", i), 395 Sources: testSources(), 396 }) 397 } 398 err := validateQueryTestVariantStabilityRequest(req) 399 So(err, ShouldErrLike, `no more than 100 may be queried at a time`) 400 }) 401 402 Convey("no test id", func() { 403 req.TestVariants[1].TestId = "" 404 err := validateQueryTestVariantStabilityRequest(req) 405 So(err, ShouldErrLike, `test_variants[1]: test_id: unspecified`) 406 }) 407 408 Convey("variant_hash invalid", func() { 409 req.TestVariants[1].VariantHash = "invalid" 410 err := validateQueryTestVariantStabilityRequest(req) 411 So(err, ShouldErrLike, `test_variants[1]: variant_hash: must match ^[0-9a-f]{16}$`) 412 }) 413 414 Convey("variant_hash mismatch with variant", func() { 415 req.TestVariants[1].VariantHash = "0123456789abcdef" 416 err := validateQueryTestVariantStabilityRequest(req) 417 So(err, ShouldErrLike, `test_variants[1]: variant and variant_hash mismatch`) 418 }) 419 420 Convey("variant_hash only", func() { 421 req.TestVariants[1].Variant = nil 422 req.TestVariants[1].VariantHash = "0123456789abcdef" 423 err := validateQueryTestVariantStabilityRequest(req) 424 So(err, ShouldBeNil) 425 }) 426 427 Convey("no sources", func() { 428 req.TestVariants[1].Sources = nil 429 err := validateQueryTestVariantStabilityRequest(req) 430 So(err, ShouldErrLike, `test_variants[1]: sources: unspecified`) 431 }) 432 433 Convey("invalid sources", func() { 434 // This checks at least one case of invalid input is detected, sufficient to verify 435 // sources validation is invoked. 436 // Exhaustive checking of sources validation is performed in pbutil. 437 req.TestVariants[1].Sources.GitilesCommit.Host = "" 438 err := validateQueryTestVariantStabilityRequest(req) 439 So(err, ShouldErrLike, `test_variants[1]: sources: gitiles_commit: host: unspecified`) 440 }) 441 442 Convey("multiple branches of same test variant", func() { 443 sources2 := testSources() 444 sources2.GitilesCommit.Ref = "refs/heads/other" 445 req.TestVariants = append(req.TestVariants, []*pb.QueryTestVariantStabilityRequest_TestVariantPosition{ 446 { 447 TestId: "my_test", 448 Variant: pbutil.Variant("key1", "val1", "key2", "val2"), 449 Sources: testSources(), 450 }, 451 { 452 TestId: "my_test", 453 Variant: pbutil.Variant("key1", "val1", "key2", "val2"), 454 Sources: sources2, 455 }, 456 }...) 457 err := validateQueryTestVariantStabilityRequest(req) 458 So(err, ShouldBeNil) 459 }) 460 461 Convey("duplicate test variant branches", func() { 462 req.TestVariants = append(req.TestVariants, []*pb.QueryTestVariantStabilityRequest_TestVariantPosition{ 463 { 464 TestId: "my_test", 465 Variant: pbutil.Variant("key1", "val1", "key2", "val2"), 466 Sources: testSources(), 467 }, 468 { 469 TestId: "my_test", 470 Variant: pbutil.Variant("key1", "val1", "key2", "val2"), 471 Sources: testSources(), 472 }, 473 }...) 474 err := validateQueryTestVariantStabilityRequest(req) 475 So(err, ShouldErrLike, `test_variants[3]: same test variant branch already requested at index 2`) 476 }) 477 }) 478 } 479 480 func toTestStabilityCriteriaConfig(criteria *pb.TestStabilityCriteria) *configpb.TestStabilityCriteria { 481 return &configpb.TestStabilityCriteria{ 482 FailureRate: &configpb.TestStabilityCriteria_FailureRateCriteria{ 483 FailureThreshold: criteria.FailureRate.FailureThreshold, 484 ConsecutiveFailureThreshold: criteria.FailureRate.ConsecutiveFailureThreshold, 485 }, 486 FlakeRate: &configpb.TestStabilityCriteria_FlakeRateCriteria{ 487 MinWindow: criteria.FlakeRate.MinWindow, 488 FlakeThreshold: criteria.FlakeRate.FlakeThreshold, 489 FlakeRateThreshold: criteria.FlakeRate.FlakeRateThreshold, 490 }, 491 } 492 } 493 494 func testSources() *pb.Sources { 495 result := &pb.Sources{ 496 GitilesCommit: &pb.GitilesCommit{ 497 Host: "chromium.googlesource.com", 498 Project: "infra/infra", 499 Ref: "refs/heads/main", 500 CommitHash: "1234567890abcdefabcd1234567890abcdefabcd", 501 Position: 12345, 502 }, 503 IsDirty: true, 504 Changelists: []*pb.GerritChange{ 505 { 506 Host: "chromium-review.googlesource.com", 507 Project: "myproject", 508 Change: 87654, 509 Patchset: 321, 510 }, 511 }, 512 } 513 return result 514 }