go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/rpc/test_history.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 "fmt" 20 "unicode" 21 "unicode/utf8" 22 23 "golang.org/x/text/unicode/norm" 24 25 "go.chromium.org/luci/common/errors" 26 rdbpbutil "go.chromium.org/luci/resultdb/pbutil" 27 "go.chromium.org/luci/resultdb/rdbperms" 28 "go.chromium.org/luci/server/auth/realms" 29 "go.chromium.org/luci/server/span" 30 31 "go.chromium.org/luci/analysis/internal/pagination" 32 "go.chromium.org/luci/analysis/internal/perms" 33 "go.chromium.org/luci/analysis/internal/testresults" 34 "go.chromium.org/luci/analysis/pbutil" 35 pb "go.chromium.org/luci/analysis/proto/v1" 36 ) 37 38 func init() { 39 rdbperms.PermListTestResults.AddFlags(realms.UsedInQueryRealms) 40 rdbperms.PermListTestExonerations.AddFlags(realms.UsedInQueryRealms) 41 } 42 43 var pageSizeLimiter = pagination.PageSizeLimiter{ 44 Default: 100, 45 Max: 1000, 46 } 47 48 // testHistoryServer implements pb.TestHistoryServer. 49 type testHistoryServer struct { 50 } 51 52 // NewTestHistoryServer returns a new pb.TestHistoryServer. 53 func NewTestHistoryServer() pb.TestHistoryServer { 54 return &pb.DecoratedTestHistory{ 55 Service: &testHistoryServer{}, 56 Postlude: gRPCifyAndLogPostlude, 57 } 58 } 59 60 // Retrieves test verdicts for a given test ID in a given project and in a given 61 // range of time. 62 func (s *testHistoryServer) Query(ctx context.Context, req *pb.QueryTestHistoryRequest) (*pb.QueryTestHistoryResponse, error) { 63 if err := validateQueryTestHistoryRequest(req); err != nil { 64 return nil, invalidArgumentError(err) 65 } 66 67 subRealms, err := perms.QuerySubRealmsNonEmpty(ctx, req.Project, req.Predicate.SubRealm, nil, perms.ListTestResultsAndExonerations...) 68 if err != nil { 69 return nil, err 70 } 71 72 pageSize := int(pageSizeLimiter.Adjust(req.PageSize)) 73 opts := testresults.ReadTestHistoryOptions{ 74 Project: req.Project, 75 TestID: req.TestId, 76 SubRealms: subRealms, 77 VariantPredicate: req.Predicate.VariantPredicate, 78 SubmittedFilter: req.Predicate.SubmittedFilter, 79 TimeRange: req.Predicate.PartitionTimeRange, 80 ExcludeBisectionResults: !req.Predicate.IncludeBisectionResults, 81 PageSize: pageSize, 82 PageToken: req.PageToken, 83 } 84 85 verdicts, nextPageToken, err := testresults.ReadTestHistory(span.Single(ctx), opts) 86 if err != nil { 87 return nil, err 88 } 89 90 return &pb.QueryTestHistoryResponse{ 91 Verdicts: verdicts, 92 NextPageToken: nextPageToken, 93 }, nil 94 } 95 96 func validateQueryTestHistoryRequest(req *pb.QueryTestHistoryRequest) error { 97 if err := pbutil.ValidateProject(req.GetProject()); err != nil { 98 return errors.Annotate(err, "project").Err() 99 } 100 if err := rdbpbutil.ValidateTestID(req.TestId); err != nil { 101 return errors.Annotate(err, "test_id").Err() 102 } 103 104 if err := pbutil.ValidateTestVerdictPredicate(req.GetPredicate()); err != nil { 105 return errors.Annotate(err, "predicate").Err() 106 } 107 108 if err := pagination.ValidatePageSize(req.GetPageSize()); err != nil { 109 return errors.Annotate(err, "page_size").Err() 110 } 111 112 return nil 113 } 114 115 // Retrieves a summary of test verdicts for a given test ID in a given project 116 // and in a given range of times. 117 func (s *testHistoryServer) QueryStats(ctx context.Context, req *pb.QueryTestHistoryStatsRequest) (*pb.QueryTestHistoryStatsResponse, error) { 118 if err := validateQueryTestHistoryStatsRequest(req); err != nil { 119 return nil, invalidArgumentError(err) 120 } 121 122 subRealms, err := perms.QuerySubRealmsNonEmpty(ctx, req.Project, req.Predicate.SubRealm, nil, perms.ListTestResultsAndExonerations...) 123 if err != nil { 124 return nil, err 125 } 126 127 pageSize := int(pageSizeLimiter.Adjust(req.PageSize)) 128 opts := testresults.ReadTestHistoryOptions{ 129 Project: req.Project, 130 TestID: req.TestId, 131 SubRealms: subRealms, 132 VariantPredicate: req.Predicate.VariantPredicate, 133 SubmittedFilter: req.Predicate.SubmittedFilter, 134 TimeRange: req.Predicate.PartitionTimeRange, 135 ExcludeBisectionResults: !req.Predicate.IncludeBisectionResults, 136 PageSize: pageSize, 137 PageToken: req.PageToken, 138 } 139 140 groups, nextPageToken, err := testresults.ReadTestHistoryStats(span.Single(ctx), opts) 141 if err != nil { 142 return nil, err 143 } 144 145 return &pb.QueryTestHistoryStatsResponse{ 146 Groups: groups, 147 NextPageToken: nextPageToken, 148 }, nil 149 } 150 151 func validateQueryTestHistoryStatsRequest(req *pb.QueryTestHistoryStatsRequest) error { 152 if err := pbutil.ValidateProject(req.GetProject()); err != nil { 153 return errors.Annotate(err, "project").Err() 154 } 155 if err := rdbpbutil.ValidateTestID(req.TestId); err != nil { 156 return errors.Annotate(err, "test_id").Err() 157 } 158 159 if err := pbutil.ValidateTestVerdictPredicate(req.GetPredicate()); err != nil { 160 return errors.Annotate(err, "predicate").Err() 161 } 162 163 if err := pagination.ValidatePageSize(req.GetPageSize()); err != nil { 164 return errors.Annotate(err, "page_size").Err() 165 } 166 167 return nil 168 } 169 170 // Retrieves variants for a given test ID in a given project that were recorded 171 // in the past 90 days. 172 func (*testHistoryServer) QueryVariants(ctx context.Context, req *pb.QueryVariantsRequest) (*pb.QueryVariantsResponse, error) { 173 if err := validateQueryVariantsRequest(req); err != nil { 174 return nil, invalidArgumentError(err) 175 } 176 177 subRealms, err := perms.QuerySubRealmsNonEmpty(ctx, req.Project, req.SubRealm, nil, rdbperms.PermListTestResults) 178 if err != nil { 179 return nil, err 180 } 181 182 pageSize := int(pageSizeLimiter.Adjust(req.PageSize)) 183 opts := testresults.ReadVariantsOptions{ 184 SubRealms: subRealms, 185 VariantPredicate: req.VariantPredicate, 186 PageSize: pageSize, 187 PageToken: req.PageToken, 188 } 189 190 variants, nextPageToken, err := testresults.ReadVariants(span.Single(ctx), req.GetProject(), req.GetTestId(), opts) 191 if err != nil { 192 return nil, err 193 } 194 195 return &pb.QueryVariantsResponse{ 196 Variants: variants, 197 NextPageToken: nextPageToken, 198 }, nil 199 } 200 201 func validateQueryVariantsRequest(req *pb.QueryVariantsRequest) error { 202 if err := pbutil.ValidateProject(req.GetProject()); err != nil { 203 return errors.Annotate(err, "project").Err() 204 } 205 if err := rdbpbutil.ValidateTestID(req.TestId); err != nil { 206 return errors.Annotate(err, "test_id").Err() 207 } 208 if req.SubRealm != "" { 209 if err := realms.ValidateRealmName(req.SubRealm, realms.ProjectScope); err != nil { 210 return errors.Annotate(err, "sub_realm").Err() 211 } 212 } 213 214 if err := pagination.ValidatePageSize(req.GetPageSize()); err != nil { 215 return errors.Annotate(err, "page_size").Err() 216 } 217 218 if req.GetVariantPredicate() != nil { 219 if err := pbutil.ValidateVariantPredicate(req.GetVariantPredicate()); err != nil { 220 return errors.Annotate(err, "predicate").Err() 221 } 222 } 223 224 return nil 225 } 226 227 // QueryTests finds all test IDs that contain the given substring in a given 228 // project that were recorded in the past 90 days. 229 func (*testHistoryServer) QueryTests(ctx context.Context, req *pb.QueryTestsRequest) (*pb.QueryTestsResponse, error) { 230 if err := validateQueryTestsRequest(req); err != nil { 231 return nil, invalidArgumentError(err) 232 } 233 234 subRealms, err := perms.QuerySubRealmsNonEmpty(ctx, req.Project, req.SubRealm, nil, rdbperms.PermListTestResults) 235 if err != nil { 236 return nil, err 237 } 238 239 pageSize := int(pageSizeLimiter.Adjust(req.PageSize)) 240 opts := testresults.QueryTestsOptions{ 241 SubRealms: subRealms, 242 PageSize: pageSize, 243 PageToken: req.GetPageToken(), 244 } 245 246 testIDs, nextPageToken, err := testresults.QueryTests(span.Single(ctx), req.Project, req.TestIdSubstring, opts) 247 if err != nil { 248 return nil, err 249 } 250 251 return &pb.QueryTestsResponse{ 252 TestIds: testIDs, 253 NextPageToken: nextPageToken, 254 }, nil 255 } 256 257 func validateQueryTestsRequest(req *pb.QueryTestsRequest) error { 258 if err := pbutil.ValidateProject(req.GetProject()); err != nil { 259 return errors.Annotate(err, "project").Err() 260 } 261 if err := validateTestIDPart(req.TestIdSubstring); err != nil { 262 return errors.Annotate(err, "test_id_substring").Err() 263 } 264 if req.SubRealm != "" { 265 if err := realms.ValidateRealmName(req.SubRealm, realms.ProjectScope); err != nil { 266 return errors.Annotate(err, "sub_realm").Err() 267 } 268 } 269 270 if err := pagination.ValidatePageSize(req.GetPageSize()); err != nil { 271 return errors.Annotate(err, "page_size").Err() 272 } 273 274 return nil 275 } 276 277 func validateTestIDPart(testIDPart string) error { 278 if testIDPart == "" { 279 return errors.Reason("unspecified").Err() 280 } 281 if len(testIDPart) > 512 { 282 return errors.Reason("length exceeds 512 bytes").Err() 283 } 284 if !utf8.ValidString(testIDPart) { 285 return errors.Reason("not a valid utf8 string").Err() 286 } 287 if !norm.NFC.IsNormalString(testIDPart) { 288 return errors.Reason("not in unicode normalized form C").Err() 289 } 290 for i, rune := range testIDPart { 291 if !unicode.IsPrint(rune) { 292 return fmt.Errorf("non-printable rune %+q at byte index %d", rune, i) 293 } 294 } 295 return nil 296 }