go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/bisection/server/analyses.go (about) 1 // Copyright 2023 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 server implements the LUCI Bisection servers to handle pRPC requests. 16 package server 17 18 import ( 19 "context" 20 "fmt" 21 "sort" 22 "strconv" 23 24 "go.chromium.org/luci/bisection/compilefailureanalysis/heuristic" 25 "go.chromium.org/luci/bisection/compilefailureanalysis/nthsection" 26 "go.chromium.org/luci/bisection/internal/lucianalysis" 27 "go.chromium.org/luci/bisection/model" 28 pb "go.chromium.org/luci/bisection/proto/v1" 29 "go.chromium.org/luci/bisection/util" 30 "go.chromium.org/luci/bisection/util/datastoreutil" 31 "go.chromium.org/luci/bisection/util/loggingutil" 32 "go.chromium.org/luci/bisection/util/protoutil" 33 buildbucketpb "go.chromium.org/luci/buildbucket/proto" 34 "go.chromium.org/luci/common/errors" 35 "go.chromium.org/luci/common/logging" 36 "go.chromium.org/luci/common/pagination" 37 "go.chromium.org/luci/common/pagination/dscursor" 38 "go.chromium.org/luci/common/proto/mask" 39 "go.chromium.org/luci/common/sync/parallel" 40 "go.chromium.org/luci/gae/service/datastore" 41 rdbpbutil "go.chromium.org/luci/resultdb/pbutil" 42 "google.golang.org/grpc/codes" 43 "google.golang.org/grpc/status" 44 "google.golang.org/protobuf/types/known/fieldmaskpb" 45 "google.golang.org/protobuf/types/known/timestamppb" 46 ) 47 48 var listAnalysesPageTokenVault = dscursor.NewVault([]byte("luci.bisection.v1.ListAnalyses")) 49 var listTestAnalysesPageTokenVault = dscursor.NewVault([]byte("luci.bisection.v1.ListTestAnalyses")) 50 51 // Max and default page sizes for ListAnalyses - the proto should be updated 52 // to reflect any changes to these values. 53 var listAnalysesPageSizeLimiter = PageSizeLimiter{ 54 Max: 200, 55 Default: 50, 56 } 57 58 // Max and default page sizes for ListTestAnalyses - the proto should be updated 59 // to reflect any changes to these values. 60 var listTestAnalysesPageSizeLimiter = PageSizeLimiter{ 61 Max: 200, 62 Default: 50, 63 } 64 65 type AnalysisClient interface { 66 ChangepointAnalysisForTestVariant(ctx context.Context, project string, keys []lucianalysis.TestVerdictKey) (map[lucianalysis.TestVerdictKey]*lucianalysis.ChangepointResult, error) 67 } 68 69 // AnalysesServer implements the LUCI Bisection proto service for Analyses. 70 type AnalysesServer struct { 71 AnalysisClient AnalysisClient 72 } 73 74 // GetAnalysis returns the analysis given the analysis id 75 func (server *AnalysesServer) GetAnalysis(c context.Context, req *pb.GetAnalysisRequest) (*pb.Analysis, error) { 76 c = loggingutil.SetAnalysisID(c, req.AnalysisId) 77 analysis := &model.CompileFailureAnalysis{ 78 Id: req.AnalysisId, 79 } 80 switch err := datastore.Get(c, analysis); err { 81 case nil: 82 //continue 83 case datastore.ErrNoSuchEntity: 84 return nil, status.Errorf(codes.NotFound, "Analysis %d not found: %v", req.AnalysisId, err) 85 default: 86 return nil, status.Errorf(codes.Internal, "Error in retrieving analysis: %s", err) 87 } 88 result, err := GetAnalysisResult(c, analysis) 89 if err != nil { 90 return nil, status.Errorf(codes.Internal, "Error getting analysis result: %s", err) 91 } 92 return result, nil 93 } 94 95 // QueryAnalysis returns the analysis given a query 96 func (server *AnalysesServer) QueryAnalysis(c context.Context, req *pb.QueryAnalysisRequest) (*pb.QueryAnalysisResponse, error) { 97 if err := validateQueryAnalysisRequest(req); err != nil { 98 return nil, err 99 } 100 if req.BuildFailure.FailedStepName != "compile" { 101 return nil, status.Errorf(codes.Unimplemented, "only compile failures are supported") 102 } 103 bbid := req.BuildFailure.GetBbid() 104 c = loggingutil.SetQueryBBID(c, bbid) 105 logging.Infof(c, "QueryAnalysis for build %d", bbid) 106 107 analysis, err := datastoreutil.GetAnalysisForBuild(c, bbid) 108 if err != nil { 109 logging.Errorf(c, "Could not query analysis for build %d: %s", bbid, err) 110 return nil, status.Errorf(codes.Internal, "failed to get analysis for build %d: %s", bbid, err) 111 } 112 if analysis == nil { 113 logging.Infof(c, "No analysis for build %d", bbid) 114 return nil, status.Errorf(codes.NotFound, "analysis not found for build %d", bbid) 115 } 116 c = loggingutil.SetAnalysisID(c, analysis.Id) 117 analysispb, err := GetAnalysisResult(c, analysis) 118 if err != nil { 119 logging.Errorf(c, "Could not get analysis data for build %d: %s", bbid, err) 120 return nil, status.Errorf(codes.Internal, "failed to get analysis data %s", err) 121 } 122 123 res := &pb.QueryAnalysisResponse{ 124 Analyses: []*pb.Analysis{analysispb}, 125 } 126 return res, nil 127 } 128 129 // TriggerAnalysis triggers an analysis for a failure 130 func (server *AnalysesServer) TriggerAnalysis(c context.Context, req *pb.TriggerAnalysisRequest) (*pb.TriggerAnalysisResponse, error) { 131 // TODO(nqmtuan): Implement this 132 return nil, nil 133 } 134 135 // UpdateAnalysis updates the information of an analysis. 136 // At the mean time, it is only used for update the bugs associated with an 137 // analysis. 138 func (server *AnalysesServer) UpdateAnalysis(c context.Context, req *pb.UpdateAnalysisRequest) (*pb.Analysis, error) { 139 // TODO(nqmtuan): Implement this 140 return nil, nil 141 } 142 143 func (server *AnalysesServer) ListTestAnalyses(ctx context.Context, req *pb.ListTestAnalysesRequest) (*pb.ListTestAnalysesResponse, error) { 144 logging.Infof(ctx, "ListTestAnalyses for project %s", req.Project) 145 146 // Validate the request. 147 if err := validateListTestAnalysesRequest(req); err != nil { 148 return nil, err 149 } 150 151 // By default, returning all fields. 152 fieldMask := req.Fields 153 if fieldMask == nil { 154 fieldMask = defaultFieldMask() 155 } 156 mask, err := mask.FromFieldMask(fieldMask, &pb.TestAnalysis{}, false, false) 157 if err != nil { 158 return nil, errors.Annotate(err, "from field mask").Err() 159 } 160 161 // Decode cursor from page token. 162 cursor, err := listTestAnalysesPageTokenVault.Cursor(ctx, req.PageToken) 163 switch err { 164 case pagination.ErrInvalidPageToken: 165 return nil, status.Errorf(codes.InvalidArgument, "invalid page token") 166 case nil: 167 // Continue 168 default: 169 return nil, status.Errorf(codes.Internal, err.Error()) 170 } 171 172 // Override the page size if necessary. 173 pageSize := int(listTestAnalysesPageSizeLimiter.Adjust(req.PageSize)) 174 175 // Query datastore for test analyses. 176 q := datastore.NewQuery("TestFailureAnalysis").Eq("project", req.Project).Order("-create_time").Start(cursor) 177 tfas := make([]*model.TestFailureAnalysis, 0, pageSize) 178 var nextCursor datastore.Cursor 179 err = datastore.Run(ctx, q, func(tfa *model.TestFailureAnalysis, getCursor datastore.CursorCB) error { 180 tfas = append(tfas, tfa) 181 182 // Check whether the page size limit has been reached 183 if len(tfas) == pageSize { 184 nextCursor, err = getCursor() 185 if err != nil { 186 return err 187 } 188 return datastore.Stop 189 } 190 return nil 191 }) 192 if err != nil { 193 logging.Errorf(ctx, err.Error()) 194 return nil, status.Errorf(codes.Internal, err.Error()) 195 } 196 197 // Construct the next page token. 198 nextPageToken, err := listTestAnalysesPageTokenVault.PageToken(ctx, nextCursor) 199 if err != nil { 200 return nil, status.Errorf(codes.Internal, err.Error()) 201 } 202 203 // Get the result for each test failure analysis. 204 analyses := make([]*pb.TestAnalysis, len(tfas)) 205 err = parallel.FanOutIn(func(workC chan<- func() error) { 206 for i, tfa := range tfas { 207 // Assign to local variables. 208 i := i 209 tfa := tfa 210 workC <- func() error { 211 analysis, err := protoutil.TestFailureAnalysisToPb(ctx, tfa, mask) 212 if err != nil { 213 err = errors.Annotate(err, "test failure analysis to pb").Err() 214 logging.Errorf(ctx, "Could not get analysis data for analysis %d: %s", tfa.ID, err) 215 return err 216 } 217 analyses[i] = analysis 218 return nil 219 } 220 } 221 }) 222 if err != nil { 223 logging.Errorf(ctx, err.Error()) 224 return nil, status.Errorf(codes.Internal, err.Error()) 225 } 226 227 return &pb.ListTestAnalysesResponse{ 228 Analyses: analyses, 229 NextPageToken: nextPageToken, 230 }, nil 231 } 232 233 func (server *AnalysesServer) GetTestAnalysis(ctx context.Context, req *pb.GetTestAnalysisRequest) (*pb.TestAnalysis, error) { 234 ctx = loggingutil.SetAnalysisID(ctx, req.AnalysisId) 235 236 // By default, returning all fields. 237 fieldMask := req.Fields 238 if fieldMask == nil { 239 fieldMask = defaultFieldMask() 240 } 241 mask, err := mask.FromFieldMask(fieldMask, &pb.TestAnalysis{}, false, false) 242 if err != nil { 243 return nil, errors.Annotate(err, "from field mask").Err() 244 } 245 246 tfa, err := datastoreutil.GetTestFailureAnalysis(ctx, req.AnalysisId) 247 if err != nil { 248 if errors.Is(err, datastore.ErrNoSuchEntity) { 249 logging.Errorf(ctx, err.Error()) 250 return nil, status.Errorf(codes.NotFound, "analysis not found: %v", err) 251 } 252 err = errors.Annotate(err, "get test failure analysis").Err() 253 logging.Errorf(ctx, err.Error()) 254 return nil, status.Errorf(codes.Internal, err.Error()) 255 } 256 result, err := protoutil.TestFailureAnalysisToPb(ctx, tfa, mask) 257 if err != nil { 258 err = errors.Annotate(err, "test failure analysis to pb").Err() 259 logging.Errorf(ctx, err.Error()) 260 return nil, status.Errorf(codes.Internal, err.Error()) 261 } 262 return result, nil 263 } 264 265 func (server *AnalysesServer) BatchGetTestAnalyses(ctx context.Context, req *pb.BatchGetTestAnalysesRequest) (*pb.BatchGetTestAnalysesResponse, error) { 266 // Validate request. 267 if err := validateBatchGetTestAnalysesRequest(req); err != nil { 268 return nil, status.Errorf(codes.InvalidArgument, err.Error()) 269 } 270 // By default, returning all fields. 271 fieldMask := req.Fields 272 if fieldMask == nil { 273 fieldMask = defaultFieldMask() 274 } 275 tfamask, err := mask.FromFieldMask(fieldMask, &pb.TestAnalysis{}, false, false) 276 if err != nil { 277 return nil, errors.Annotate(err, "from field mask").Err() 278 } 279 // Query Changepoint analysis. 280 keys := []lucianalysis.TestVerdictKey{} 281 for _, tf := range req.TestFailures { 282 keys = append(keys, lucianalysis.TestVerdictKey{ 283 TestID: tf.TestId, 284 VariantHash: tf.VariantHash, 285 RefHash: tf.RefHash, 286 }) 287 } 288 changePointResults, err := server.AnalysisClient.ChangepointAnalysisForTestVariant(ctx, req.Project, keys) 289 if err != nil { 290 return nil, status.Errorf(codes.Internal, "read changepoint analysis %s", err) 291 } 292 293 result := make([]*pb.TestAnalysis, len(req.TestFailures)) 294 err = parallel.FanOutIn(func(workC chan<- func() error) { 295 for i, tf := range req.TestFailures { 296 // Assign to local variables. 297 i := i 298 tf := tf 299 workC <- func() error { 300 tfs, err := datastoreutil.GetTestFailures(ctx, req.Project, tf.TestId, tf.RefHash, tf.VariantHash) 301 if err != nil { 302 return errors.Annotate(err, "get test failures").Err() 303 } 304 if len(tfs) == 0 { 305 return nil 306 } 307 sort.Slice(tfs, func(i, j int) bool { 308 return tfs[i].RegressionStartPosition > tfs[j].RegressionStartPosition 309 }) 310 latestTestFailure := tfs[0] 311 if latestTestFailure.IsDiverged { 312 // Do not return test analysis if diverged. 313 // Because diverged test failure is considered excluded from the test analyses. 314 return nil 315 } 316 changepointResult, ok := changePointResults[lucianalysis.TestVerdictKey{ 317 TestID: tf.TestId, 318 VariantHash: tf.VariantHash, 319 RefHash: tf.RefHash, 320 }] 321 if !ok { 322 logging.Infof(ctx, "no changepoint analysis for test %s %s %s", tf.TestId, tf.VariantHash, tf.RefHash) 323 return nil 324 } 325 ongoing, reason := isTestFailureDeterministicallyOngoing(latestTestFailure, changepointResult) 326 // Do not return the test analysis if the failure is not ongoing. 327 if !ongoing { 328 logging.Infof(ctx, "no bisection returned for test %s %s %s because %s", tf.TestId, tf.VariantHash, tf.RefHash, reason) 329 return nil 330 } 331 // Return the test analysis that analyze this test failure. 332 tfa, err := datastoreutil.GetTestFailureAnalysis(ctx, latestTestFailure.AnalysisKey.IntID()) 333 if err != nil { 334 return errors.Annotate(err, "get test failure analysis").Err() 335 } 336 tfaProto, err := protoutil.TestFailureAnalysisToPb(ctx, tfa, tfamask) 337 if err != nil { 338 return errors.Annotate(err, "convert test failure analysis to protobuf").Err() 339 } 340 result[i] = tfaProto 341 return nil 342 } 343 } 344 }) 345 346 if err != nil { 347 return nil, status.Errorf(codes.Internal, err.Error()) 348 } 349 350 return &pb.BatchGetTestAnalysesResponse{ 351 TestAnalyses: result, 352 }, nil 353 } 354 355 // IsTestFailureDeterministicallyOngoing returns a boolean which indicate whether 356 // a test failure is still deterministically failing. 357 // It also returns a string to explain why it is not deterministically failing. 358 func isTestFailureDeterministicallyOngoing(tf *model.TestFailure, changepointResult *lucianalysis.ChangepointResult) (bool, string) { 359 segments := changepointResult.Segments 360 if len(segments) < 2 { 361 return false, "not deterministically failing" 362 } 363 curSegment := segments[0] 364 prevSegment := segments[1] 365 // The latest failure is not deterministically failing, return false. 366 if curSegment.CountTotalResults != curSegment.CountUnexpectedResults { 367 return false, "not deterministically failing" 368 } 369 // If the test failure is still ongoing, the regression range of the failure 370 // on record should equal or contain the current regression range of 371 // the latest segment in changepoint analysis. 372 // Because the regression range of deterministic failure obtained from changepoint analysis 373 // only shrinks or stays the same over time. 374 if (curSegment.StartPosition.Int64 <= tf.RegressionEndPosition) && 375 (prevSegment.EndPosition.Int64 >= tf.RegressionStartPosition) { 376 return true, "" 377 } 378 return false, "latest bisected failure is not ongoing" 379 } 380 381 // GetAnalysisResult returns an analysis for pRPC from CompileFailureAnalysis 382 func GetAnalysisResult(c context.Context, analysis *model.CompileFailureAnalysis) (*pb.Analysis, error) { 383 result := &pb.Analysis{ 384 AnalysisId: analysis.Id, 385 Status: analysis.Status, 386 RunStatus: analysis.RunStatus, 387 CreatedTime: timestamppb.New(analysis.CreateTime), 388 FirstFailedBbid: analysis.FirstFailedBuildId, 389 LastPassedBbid: analysis.LastPassedBuildId, 390 } 391 392 if analysis.HasEnded() { 393 result.EndTime = timestamppb.New(analysis.EndTime) 394 } 395 396 // Populate Builder and BuildFailureType data 397 if analysis.CompileFailure != nil && analysis.CompileFailure.Parent() != nil { 398 // Add details from associated compile failure 399 failedBuild, err := datastoreutil.GetBuild(c, analysis.CompileFailure.Parent().IntID()) 400 if err != nil { 401 return nil, err 402 } 403 if failedBuild != nil { 404 result.Builder = &buildbucketpb.BuilderID{ 405 Project: failedBuild.Project, 406 Bucket: failedBuild.Bucket, 407 Builder: failedBuild.Builder, 408 } 409 result.BuildFailureType = failedBuild.BuildFailureType 410 } 411 } 412 413 heuristicAnalysis, err := datastoreutil.GetHeuristicAnalysis(c, analysis) 414 if err != nil { 415 return nil, err 416 } 417 if heuristicAnalysis != nil { 418 suspects, err := datastoreutil.GetSuspectsForHeuristicAnalysis(c, heuristicAnalysis) 419 if err != nil { 420 return nil, err 421 } 422 423 pbSuspects := make([]*pb.HeuristicSuspect, len(suspects)) 424 for i, suspect := range suspects { 425 pbSuspects[i] = &pb.HeuristicSuspect{ 426 GitilesCommit: &suspect.GitilesCommit, 427 ReviewUrl: suspect.ReviewUrl, 428 Score: int32(suspect.Score), 429 Justification: suspect.Justification, 430 ConfidenceLevel: heuristic.GetConfidenceLevel(suspect.Score), 431 } 432 433 verificationDetails, err := constructSuspectVerificationDetails(c, suspect) 434 if err != nil { 435 return nil, errors.Annotate(err, "couldn't constructSuspectVerificationDetails").Err() 436 } 437 pbSuspects[i].VerificationDetails = verificationDetails 438 439 // TODO: check access permissions before including the review title. 440 // For now, we will include it by default as LUCI Bisection access 441 // should already be restricted to internal users only. 442 pbSuspects[i].ReviewTitle = suspect.ReviewTitle 443 } 444 heuristicResult := &pb.HeuristicAnalysisResult{ 445 Status: heuristicAnalysis.Status, 446 StartTime: timestamppb.New(heuristicAnalysis.StartTime), 447 Suspects: pbSuspects, 448 } 449 if heuristicAnalysis.HasEnded() { 450 heuristicResult.EndTime = timestamppb.New(heuristicAnalysis.EndTime) 451 } 452 453 result.HeuristicResult = heuristicResult 454 } 455 456 // Get culprits 457 culprits := make([]*pb.Culprit, len(analysis.VerifiedCulprits)) 458 for i, culprit := range analysis.VerifiedCulprits { 459 suspect := &model.Suspect{ 460 Id: culprit.IntID(), 461 ParentAnalysis: culprit.Parent(), 462 } 463 err = datastore.Get(c, suspect) 464 if err != nil { 465 return nil, err 466 } 467 468 pbCulprit := &pb.Culprit{ 469 Commit: &suspect.GitilesCommit, 470 ReviewUrl: suspect.ReviewUrl, 471 ReviewTitle: suspect.ReviewTitle, 472 } 473 474 // Add suspect verification details for the culprit 475 verificationDetails, err := constructSuspectVerificationDetails(c, suspect) 476 if err != nil { 477 return nil, err 478 } 479 pbCulprit.VerificationDetails = verificationDetails 480 pbCulprit.CulpritAction = protoutil.CulpritActionsForSuspect(suspect) 481 culprits[i] = pbCulprit 482 } 483 result.Culprits = culprits 484 485 nthSectionResult, err := getNthSectionResult(c, analysis) 486 if err != nil { 487 // If fetching nthSection analysis result failed for some reasons, print 488 // out the error, but we still continue. 489 err = errors.Annotate(err, "getNthSectionResult for analysis %d", analysis.Id).Err() 490 logging.Errorf(c, err.Error()) 491 } else { 492 result.NthSectionResult = nthSectionResult 493 } 494 495 // TODO (nqmtuan): add culprit actions for: 496 // * commenting on related bugs 497 498 return result, nil 499 } 500 501 func getNthSectionResult(c context.Context, cfa *model.CompileFailureAnalysis) (*pb.NthSectionAnalysisResult, error) { 502 nsa, err := datastoreutil.GetNthSectionAnalysis(c, cfa) 503 if err != nil { 504 return nil, errors.Annotate(err, "getting nthsection analysis").Err() 505 } 506 if nsa == nil { 507 return nil, nil 508 } 509 if nsa.BlameList == nil { 510 return nil, errors.Annotate(err, "couldn't find blamelist").Err() 511 } 512 result := &pb.NthSectionAnalysisResult{ 513 Status: nsa.Status, 514 StartTime: timestamppb.New(nsa.StartTime), 515 BlameList: nsa.BlameList, 516 } 517 if nsa.HasEnded() { 518 result.EndTime = timestamppb.New(nsa.EndTime) 519 } 520 521 // Get all reruns for the current analysis 522 // This should contain all reruns for nth section and culprit verification 523 reruns, err := datastoreutil.GetRerunsForAnalysis(c, cfa) 524 if err != nil { 525 return nil, err 526 } 527 528 for _, rerun := range reruns { 529 rerunResult := &pb.SingleRerun{ 530 StartTime: timestamppb.New(rerun.StartTime), 531 RerunResult: &pb.RerunResult{ 532 RerunStatus: rerun.Status, 533 }, 534 Bbid: rerun.RerunBuild.IntID(), 535 Commit: &rerun.GitilesCommit, 536 Type: string(rerun.Type), 537 } 538 if rerun.HasEnded() { 539 rerunResult.EndTime = timestamppb.New(rerun.EndTime) 540 } 541 index, err := findRerunIndexInBlameList(rerun, nsa.BlameList) 542 if err != nil { 543 // There is only one case where we cannot find the rerun in blamelist 544 // It is when the rerun is part of the culprit verification and is 545 // the "last pass" revision. 546 // In this case, we should just log and continue, and the run will appear 547 // as part of culprit verification component. 548 logging.Warningf(c, errors.Annotate(err, "couldn't find index for rerun").Err().Error()) 549 continue 550 } 551 rerunResult.Index = strconv.FormatInt(int64(index), 10) 552 result.Reruns = append(result.Reruns, rerunResult) 553 } 554 555 // Find remaining regression range 556 snapshot, err := nthsection.CreateSnapshot(c, nsa) 557 if err != nil { 558 return nil, errors.Annotate(err, "couldn't create snapshot").Err() 559 } 560 561 ff, lp, err := snapshot.GetCurrentRegressionRange() 562 // GetCurrentRegressionRange return error if the regression is invalid 563 // We don't want to return the error here, but just continue 564 if err != nil { 565 err = errors.Annotate(err, "getCurrentRegressionRange").Err() 566 // Log as Debugf because it is not exactly an error, but just a state of the analysis 567 logging.Debugf(c, err.Error()) 568 } else { 569 result.RemainingNthSectionRange = &pb.RegressionRange{ 570 FirstFailed: getCommitFromIndex(ff, nsa.BlameList, cfa), 571 LastPassed: getCommitFromIndex(lp, nsa.BlameList, cfa), 572 } 573 } 574 575 // Find suspect 576 suspect, err := datastoreutil.GetSuspectForNthSectionAnalysis(c, nsa) 577 if err != nil { 578 return nil, err 579 } 580 if suspect != nil { 581 pbSuspect := &pb.NthSectionSuspect{ 582 GitilesCommit: &suspect.GitilesCommit, 583 ReviewUrl: suspect.ReviewUrl, 584 ReviewTitle: suspect.ReviewTitle, 585 Commit: &suspect.GitilesCommit, 586 } 587 588 verificationDetails, err := constructSuspectVerificationDetails(c, suspect) 589 if err != nil { 590 return nil, errors.Annotate(err, "couldn't constructSuspectVerificationDetails").Err() 591 } 592 pbSuspect.VerificationDetails = verificationDetails 593 result.Suspect = pbSuspect 594 } 595 596 return result, nil 597 } 598 599 func findRerunIndexInBlameList(rerun *model.SingleRerun, blamelist *pb.BlameList) (int32, error) { 600 for i, commit := range blamelist.Commits { 601 if commit.Commit == rerun.GitilesCommit.Id { 602 return int32(i), nil 603 } 604 } 605 return -1, fmt.Errorf("couldn't find index for rerun %d", rerun.Id) 606 } 607 608 func getCommitFromIndex(index int, blamelist *pb.BlameList, cfa *model.CompileFailureAnalysis) *buildbucketpb.GitilesCommit { 609 return &buildbucketpb.GitilesCommit{ 610 Id: blamelist.Commits[index].Commit, 611 Host: cfa.InitialRegressionRange.FirstFailed.Host, 612 Project: cfa.InitialRegressionRange.FirstFailed.Project, 613 Ref: cfa.InitialRegressionRange.FirstFailed.Ref, 614 } 615 } 616 617 // constructSingleRerun constructs a pb.SingleRerun using the details from the 618 // rerun build and latest single rerun 619 func constructSingleRerun(c context.Context, rerunBBID int64) (*pb.SingleRerun, error) { 620 rerunBuild := &model.CompileRerunBuild{ 621 Id: rerunBBID, 622 } 623 err := datastore.Get(c, rerunBuild) 624 if err != nil { 625 return nil, errors.Annotate(err, "failed getting rerun build").Err() 626 } 627 628 singleRerun, err := datastoreutil.GetLastRerunForRerunBuild(c, rerunBuild) 629 if err != nil { 630 return nil, errors.Annotate(err, "failed getting single rerun").Err() 631 } 632 633 result := &pb.SingleRerun{ 634 StartTime: timestamppb.New(singleRerun.StartTime), 635 Bbid: rerunBBID, 636 RerunResult: &pb.RerunResult{ 637 RerunStatus: singleRerun.Status, 638 }, 639 Commit: &singleRerun.GitilesCommit, 640 } 641 if singleRerun.HasEnded() { 642 result.EndTime = timestamppb.New(singleRerun.EndTime) 643 } 644 return result, nil 645 } 646 647 // constructSuspectVerificationDetails constructs a pb.SuspectVerificationDetails for the given suspect 648 func constructSuspectVerificationDetails(c context.Context, suspect *model.Suspect) (*pb.SuspectVerificationDetails, error) { 649 // Add the current verification status 650 verificationDetails := &pb.SuspectVerificationDetails{ 651 Status: string(suspect.VerificationStatus), 652 } 653 654 // Add rerun details for the suspect commit 655 if suspect.SuspectRerunBuild != nil { 656 singleRerun, err := constructSingleRerun(c, suspect.SuspectRerunBuild.IntID()) 657 if err != nil { 658 return nil, errors.Annotate(err, "failed getting verification rerun for suspect commit").Err() 659 } 660 verificationDetails.SuspectRerun = singleRerun 661 } 662 663 // Add rerun details for the parent commit of suspect 664 if suspect.ParentRerunBuild != nil { 665 singleRerun, err := constructSingleRerun(c, suspect.ParentRerunBuild.IntID()) 666 if err != nil { 667 return nil, errors.Annotate(err, "failed getting verification rerun for parent commit of suspect").Err() 668 } 669 verificationDetails.ParentRerun = singleRerun 670 } 671 672 return verificationDetails, nil 673 } 674 675 // validateQueryAnalysisRequest checks if the request is valid. 676 func validateQueryAnalysisRequest(req *pb.QueryAnalysisRequest) error { 677 if req.BuildFailure == nil { 678 return status.Errorf(codes.InvalidArgument, "BuildFailure must not be empty") 679 } 680 if req.BuildFailure.GetBbid() == 0 { 681 return status.Errorf(codes.InvalidArgument, "BuildFailure bbid must not be empty") 682 } 683 return nil 684 } 685 686 // ListAnalyses returns existing analyses 687 func (server *AnalysesServer) ListAnalyses(c context.Context, req *pb.ListAnalysesRequest) (*pb.ListAnalysesResponse, error) { 688 // Validate the request 689 if err := validateListAnalysesRequest(req); err != nil { 690 return nil, err 691 } 692 693 // Decode cursor from page token 694 cursor, err := listAnalysesPageTokenVault.Cursor(c, req.PageToken) 695 switch err { 696 case pagination.ErrInvalidPageToken: 697 return nil, status.Errorf(codes.InvalidArgument, "Invalid page token") 698 case nil: 699 // Continue 700 default: 701 return nil, status.Errorf(codes.Internal, err.Error()) 702 } 703 704 // Override the page size if necessary 705 pageSize := int(listAnalysesPageSizeLimiter.Adjust(req.PageSize)) 706 707 // Construct the query 708 q := datastore.NewQuery("CompileFailureAnalysis").Order("-create_time").Start(cursor) 709 710 // Query datastore for compile failure analyses 711 compileFailureAnalyses := make([]*model.CompileFailureAnalysis, 0, pageSize) 712 var nextCursor datastore.Cursor 713 err = datastore.Run(c, q, func(compileFailureAnalysis *model.CompileFailureAnalysis, getCursor datastore.CursorCB) error { 714 compileFailureAnalyses = append(compileFailureAnalyses, compileFailureAnalysis) 715 716 // Check whether the page size limit has been reached 717 if len(compileFailureAnalyses) == pageSize { 718 nextCursor, err = getCursor() 719 if err != nil { 720 return err 721 } 722 return datastore.Stop 723 } 724 return nil 725 }) 726 if err != nil { 727 return nil, status.Errorf(codes.Internal, err.Error()) 728 } 729 730 // Construct the next page token 731 nextPageToken, err := listAnalysesPageTokenVault.PageToken(c, nextCursor) 732 if err != nil { 733 return nil, status.Errorf(codes.Internal, err.Error()) 734 } 735 736 // Get the result for each compile failure analysis 737 analyses := make([]*pb.Analysis, len(compileFailureAnalyses)) 738 err = parallel.FanOutIn(func(workC chan<- func() error) { 739 for i, compileFailureAnalysis := range compileFailureAnalyses { 740 i := i 741 compileFailureAnalysis := compileFailureAnalysis 742 workC <- func() error { 743 analysis, err := GetAnalysisResult(c, compileFailureAnalysis) 744 if err != nil { 745 logging.Errorf(c, "Could not get analysis data for analysis %d: %s", 746 compileFailureAnalysis.Id, err) 747 return err 748 } 749 analyses[i] = analysis 750 return nil 751 } 752 } 753 }) 754 if err != nil { 755 return nil, status.Errorf(codes.Internal, err.Error()) 756 } 757 758 return &pb.ListAnalysesResponse{ 759 Analyses: analyses, 760 NextPageToken: nextPageToken, 761 }, nil 762 } 763 764 // validateListAnalysesRequest checks if the request is valid. 765 func validateListAnalysesRequest(req *pb.ListAnalysesRequest) error { 766 if req.PageSize < 0 { 767 return status.Errorf(codes.InvalidArgument, "Page size can't be negative") 768 } 769 770 return nil 771 } 772 773 // validateListTestAnalysesRequest checks if the ListTestAnalysesRequest is valid. 774 func validateListTestAnalysesRequest(req *pb.ListTestAnalysesRequest) error { 775 if req.Project == "" { 776 return status.Errorf(codes.InvalidArgument, "project must not be empty") 777 } 778 if req.PageSize < 0 { 779 return status.Errorf(codes.InvalidArgument, "page size must not be negative") 780 } 781 return nil 782 } 783 784 func validateBatchGetTestAnalysesRequest(req *pb.BatchGetTestAnalysesRequest) error { 785 // MaxTestFailures is the maximum number of test failures to be queried in one request. 786 const MaxTestFailures = 100 787 if err := util.ValidateProject(req.Project); err != nil { 788 return errors.Annotate(err, "project").Err() 789 } 790 if len(req.TestFailures) == 0 { 791 return errors.Reason("test_failures: unspecified").Err() 792 } 793 if len(req.TestFailures) > MaxTestFailures { 794 return errors.Reason("test_failures: no more than %v may be queried at a time", MaxTestFailures).Err() 795 } 796 for i, tf := range req.TestFailures { 797 if tf.GetTestId() == "" { 798 return errors.Reason("test_variants[%v]: test_id: unspecified", i).Err() 799 } 800 if tf.VariantHash == "" { 801 return errors.Reason("test_variants[%v]: variant_hash: unspecified", i).Err() 802 } 803 if tf.RefHash == "" { 804 return errors.Reason("test_variants[%v]: ref_hash: unspecified", i).Err() 805 } 806 if err := rdbpbutil.ValidateTestID(tf.TestId); err != nil { 807 return errors.Annotate(err, "test_variants[%v].test_id", i).Err() 808 } 809 if err := util.ValidateVariantHash(tf.VariantHash); err != nil { 810 return errors.Annotate(err, "test_variants[%v].variant_hash", i).Err() 811 } 812 if err := util.ValidateRefHash(tf.RefHash); err != nil { 813 return errors.Annotate(err, "test_variants[%v].ref_hash", i).Err() 814 } 815 } 816 return nil 817 } 818 819 func defaultFieldMask() *fieldmaskpb.FieldMask { 820 return &fieldmaskpb.FieldMask{ 821 Paths: []string{"*"}, 822 } 823 }