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  }