go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/rpc/test_variant.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  	"regexp"
    21  
    22  	"go.chromium.org/luci/common/clock"
    23  	"go.chromium.org/luci/common/errors"
    24  	"go.chromium.org/luci/resultdb/rdbperms"
    25  	"go.chromium.org/luci/server/span"
    26  
    27  	"go.chromium.org/luci/analysis/internal/perms"
    28  	"go.chromium.org/luci/analysis/internal/testresults"
    29  	"go.chromium.org/luci/analysis/internal/testresults/stability"
    30  	"go.chromium.org/luci/analysis/pbutil"
    31  	configpb "go.chromium.org/luci/analysis/proto/config"
    32  	pb "go.chromium.org/luci/analysis/proto/v1"
    33  )
    34  
    35  var variantHashRe = regexp.MustCompile("^[0-9a-f]{16}$")
    36  
    37  // testVariantsServer implements pb.TestVariantServer.
    38  type testVariantsServer struct {
    39  }
    40  
    41  // NewTestVariantsServer returns a new pb.TestVariantServer.
    42  func NewTestVariantsServer() pb.TestVariantsServer {
    43  	return &pb.DecoratedTestVariants{
    44  		Prelude:  checkAllowedPrelude,
    45  		Service:  &testVariantsServer{},
    46  		Postlude: gRPCifyAndLogPostlude,
    47  	}
    48  }
    49  
    50  // QueryFailureRate queries the failure rate of specified test variants,
    51  // returning signals indicating if the test variant is flaky and/or
    52  // deterministically failing.
    53  func (*testVariantsServer) QueryFailureRate(ctx context.Context, req *pb.QueryTestVariantFailureRateRequest) (*pb.QueryTestVariantFailureRateResponse, error) {
    54  	now := clock.Now(ctx)
    55  	if err := validateQueryTestVariantFailureRateRequest(req); err != nil {
    56  		return nil, invalidArgumentError(err)
    57  	}
    58  
    59  	opts := testresults.QueryFailureRateOptions{
    60  		Project:      req.Project,
    61  		TestVariants: req.TestVariants,
    62  		AsAtTime:     now,
    63  	}
    64  
    65  	var err error
    66  	// Query all subrealms the caller can see test results in.
    67  	const subRealm = ""
    68  	opts.SubRealms, err = perms.QuerySubRealmsNonEmpty(ctx, req.Project, subRealm, nil, rdbperms.PermListTestResults)
    69  	if err != nil {
    70  		return nil, err
    71  	}
    72  
    73  	ctx, cancel := span.ReadOnlyTransaction(ctx)
    74  	defer cancel()
    75  	response, err := testresults.QueryFailureRate(ctx, opts)
    76  	if err != nil {
    77  		return nil, err
    78  	}
    79  	return response, nil
    80  }
    81  
    82  func validateQueryTestVariantFailureRateRequest(req *pb.QueryTestVariantFailureRateRequest) error {
    83  	// MaxTestVariants is the maximum number of test variants to be queried in one request.
    84  	const MaxTestVariants = 100
    85  
    86  	if err := pbutil.ValidateProject(req.Project); err != nil {
    87  		return errors.Annotate(err, "project").Err()
    88  	}
    89  	if len(req.TestVariants) == 0 {
    90  		return errors.Reason("test_variants: unspecified").Err()
    91  	}
    92  	if len(req.TestVariants) > MaxTestVariants {
    93  		return errors.Reason("test_variants: no more than %v may be queried at a time", MaxTestVariants).Err()
    94  	}
    95  	type testVariant struct {
    96  		testID      string
    97  		variantHash string
    98  	}
    99  	uniqueTestVariants := make(map[testVariant]struct{})
   100  	for i, tv := range req.TestVariants {
   101  		if tv.GetTestId() == "" {
   102  			return errors.Reason("test_variants[%v]: test_id: unspecified", i).Err()
   103  		}
   104  		var variantHash string
   105  		if tv.VariantHash != "" {
   106  			if !variantHashRe.MatchString(tv.VariantHash) {
   107  				return errors.Reason("test_variants[%v]: variant_hash: must match %s", i, variantHashRe).Err()
   108  			}
   109  			variantHash = tv.VariantHash
   110  		}
   111  
   112  		// Variant may be nil as not all tests have variants.
   113  		if tv.Variant != nil || tv.VariantHash == "" {
   114  			calculatedHash := pbutil.VariantHash(tv.Variant)
   115  			if tv.VariantHash != "" && calculatedHash != tv.VariantHash {
   116  				return errors.Reason("test_variants[%v]: variant and variant_hash mismatch, variant hashed to %s, expected %s", i, calculatedHash, tv.VariantHash).Err()
   117  			}
   118  			variantHash = calculatedHash
   119  		}
   120  
   121  		key := testVariant{testID: tv.TestId, variantHash: variantHash}
   122  		if _, ok := uniqueTestVariants[key]; ok {
   123  			return errors.Reason("test_variants[%v]: already requested in the same request", i).Err()
   124  		}
   125  		uniqueTestVariants[key] = struct{}{}
   126  	}
   127  	return nil
   128  }
   129  
   130  func (*testVariantsServer) QueryStability(ctx context.Context, req *pb.QueryTestVariantStabilityRequest) (*pb.QueryTestVariantStabilityResponse, error) {
   131  	if err := validateQueryTestVariantStabilityRequest(req); err != nil {
   132  		return nil, invalidArgumentError(err)
   133  	}
   134  
   135  	if err := perms.VerifyProjectPermissions(ctx, req.Project, perms.PermGetConfig); err != nil {
   136  		return nil, err
   137  	}
   138  
   139  	// Fetch a recent project configuration.
   140  	// (May be a recent value that was cached.)
   141  	cfg, err := readProjectConfig(ctx, req.Project)
   142  	if err != nil {
   143  		return nil, err
   144  	}
   145  
   146  	criteria, err := fromTestStabilityCriteriaConfig(cfg.Config)
   147  	if err != nil {
   148  		return nil, err
   149  	}
   150  
   151  	// Query all subrealms the caller can see test results in.
   152  	const subRealm = ""
   153  	subRealms, err := perms.QuerySubRealmsNonEmpty(ctx, req.Project, subRealm, nil, rdbperms.PermListTestResults)
   154  	if err != nil {
   155  		return nil, err
   156  	}
   157  
   158  	opts := stability.QueryStabilityOptions{
   159  		Project:              req.Project,
   160  		SubRealms:            subRealms,
   161  		TestVariantPositions: req.TestVariants,
   162  		Criteria:             criteria,
   163  		AsAtTime:             clock.Now(ctx),
   164  	}
   165  
   166  	ctx, cancel := span.ReadOnlyTransaction(ctx)
   167  	defer cancel()
   168  	stabilityAnalysis, err := stability.QueryStability(ctx, opts)
   169  	if err != nil {
   170  		return nil, err
   171  	}
   172  	return &pb.QueryTestVariantStabilityResponse{
   173  		TestVariants: stabilityAnalysis,
   174  		Criteria:     criteria,
   175  	}, nil
   176  }
   177  
   178  func fromTestStabilityCriteriaConfig(cfg *configpb.ProjectConfig) (*pb.TestStabilityCriteria, error) {
   179  	if cfg.TestStabilityCriteria == nil {
   180  		return nil, failedPreconditionError(errors.Reason("project has not defined test stability criteria; set test_stability_criteria in project configuration and try again").Err())
   181  	}
   182  
   183  	criteria := cfg.TestStabilityCriteria
   184  
   185  	// We have test stability criteria. We can rely on project configuration
   186  	// validation to ensure mandatory fields have been set.
   187  	return &pb.TestStabilityCriteria{
   188  		FailureRate: &pb.TestStabilityCriteria_FailureRateCriteria{
   189  			FailureThreshold:            criteria.FailureRate.FailureThreshold,
   190  			ConsecutiveFailureThreshold: criteria.FailureRate.ConsecutiveFailureThreshold,
   191  		},
   192  		FlakeRate: &pb.TestStabilityCriteria_FlakeRateCriteria{
   193  			MinWindow:          criteria.FlakeRate.MinWindow,
   194  			FlakeThreshold:     criteria.FlakeRate.FlakeThreshold,
   195  			FlakeRateThreshold: criteria.FlakeRate.FlakeRateThreshold,
   196  		},
   197  	}, nil
   198  }
   199  
   200  func validateQueryTestVariantStabilityRequest(req *pb.QueryTestVariantStabilityRequest) error {
   201  	// MaxTestVariants is the maximum number of test variants to be queried in one request.
   202  	const MaxTestVariants = 100
   203  
   204  	if err := pbutil.ValidateProject(req.Project); err != nil {
   205  		return errors.Annotate(err, "project").Err()
   206  	}
   207  	if len(req.TestVariants) == 0 {
   208  		return errors.Reason("test_variants: unspecified").Err()
   209  	}
   210  	if len(req.TestVariants) > MaxTestVariants {
   211  		return errors.Reason("test_variants: no more than %v may be queried at a time", MaxTestVariants).Err()
   212  	}
   213  	seenTestVariantBranches := make(map[testVariantBranch]int)
   214  	for i, tv := range req.TestVariants {
   215  		if err := validateTestVariantPosition(tv, i, seenTestVariantBranches); err != nil {
   216  			return errors.Annotate(err, "test_variants[%v]", i).Err()
   217  		}
   218  	}
   219  	return nil
   220  }
   221  
   222  type testVariantBranch struct {
   223  	testID        string
   224  	variantHash   string
   225  	sourceRefHash string
   226  }
   227  
   228  // validateTestVariantPosition validates the given test variant position at the given offset
   229  // in the request. seenTestVariants is used to track the previously seen test variants
   230  // and their offsets so that duplicates can be identified.
   231  func validateTestVariantPosition(tv *pb.QueryTestVariantStabilityRequest_TestVariantPosition, offset int, seenTestVariantBranches map[testVariantBranch]int) error {
   232  	if tv.GetTestId() == "" {
   233  		return errors.Reason("test_id: unspecified").Err()
   234  	}
   235  	var variantHash string
   236  	if tv.VariantHash != "" {
   237  		if !variantHashRe.MatchString(tv.VariantHash) {
   238  			return errors.Reason("variant_hash: must match %s", variantHashRe).Err()
   239  		}
   240  		variantHash = tv.VariantHash
   241  	}
   242  
   243  	// This RPC allows the Variant or VariantHash (or both)
   244  	// to be set to specify the variant. If both are specified,
   245  	// they must be consistent.
   246  	if tv.Variant != nil {
   247  		calculatedHash := pbutil.VariantHash(tv.Variant)
   248  		if tv.VariantHash != "" && calculatedHash != tv.VariantHash {
   249  			return errors.Reason("variant and variant_hash mismatch, variant hashed to %s, expected %s", calculatedHash, tv.VariantHash).Err()
   250  		}
   251  		variantHash = calculatedHash
   252  	}
   253  	// It is possible neither the VariantHash nor Variant is set.
   254  	// In this case, we interpret the request as being for the
   255  	// nil Variant (which is a valid variant).
   256  	if tv.VariantHash == "" && tv.Variant == nil {
   257  		variantHash = pbutil.VariantHash(nil)
   258  	}
   259  
   260  	if err := pbutil.ValidateSources(tv.Sources); err != nil {
   261  		return errors.Annotate(err, "sources").Err()
   262  	}
   263  
   264  	sourceRefHash := hex.EncodeToString(pbutil.SourceRefHash(pbutil.SourceRefFromSources(tv.Sources)))
   265  
   266  	// Each test variant branch may appear in the request only once.
   267  	key := testVariantBranch{testID: tv.TestId, variantHash: variantHash, sourceRefHash: sourceRefHash}
   268  	if previousOffset, ok := seenTestVariantBranches[key]; ok {
   269  		return errors.Reason("same test variant branch already requested at index %v", previousOffset).Err()
   270  	}
   271  	seenTestVariantBranches[key] = offset
   272  
   273  	return nil
   274  }