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  }