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 }