go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/bisection/util/protoutil/proto_util.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 protoutil contains the utility functions to convert to protobuf. 16 package protoutil 17 18 import ( 19 "context" 20 "strconv" 21 22 "google.golang.org/protobuf/types/known/timestamppb" 23 24 "go.chromium.org/luci/bisection/model" 25 pb "go.chromium.org/luci/bisection/proto/v1" 26 "go.chromium.org/luci/bisection/testfailureanalysis/bisection" 27 "go.chromium.org/luci/bisection/util/changelogutil" 28 "go.chromium.org/luci/bisection/util/datastoreutil" 29 buildbucketpb "go.chromium.org/luci/buildbucket/proto" 30 "go.chromium.org/luci/common/errors" 31 "go.chromium.org/luci/common/proto/mask" 32 ) 33 34 // TestFailureAnalysisToPb converts model.TestFailureAnalysis to pb.TestAnalysis 35 func TestFailureAnalysisToPb(ctx context.Context, tfa *model.TestFailureAnalysis, tfaMask *mask.Mask) (*pb.TestAnalysis, error) { 36 result := &pb.TestAnalysis{} 37 if tfaMask.MustIncludes("analysis_id") == mask.IncludeEntirely { 38 result.AnalysisId = tfa.ID 39 } 40 if tfaMask.MustIncludes("created_time") == mask.IncludeEntirely { 41 result.CreatedTime = timestamppb.New(tfa.CreateTime) 42 } 43 if tfaMask.MustIncludes("status") == mask.IncludeEntirely { 44 result.Status = tfa.Status 45 } 46 if tfaMask.MustIncludes("run_status") == mask.IncludeEntirely { 47 result.RunStatus = tfa.RunStatus 48 } 49 if tfaMask.MustIncludes("sample_bbid") == mask.IncludeEntirely { 50 result.SampleBbid = tfa.FailedBuildID 51 } 52 if tfaMask.MustIncludes("start_time") == mask.IncludeEntirely && tfa.HasStarted() { 53 result.StartTime = timestamppb.New(tfa.StartTime) 54 } 55 if tfaMask.MustIncludes("end_time") == mask.IncludeEntirely && tfa.HasEnded() { 56 result.EndTime = timestamppb.New(tfa.EndTime) 57 } 58 // It doesn't make sense to return builder information partially. 59 // We don't check mask.IncludePartially here. 60 if tfaMask.MustIncludes("builder") == mask.IncludeEntirely { 61 result.Builder = &buildbucketpb.BuilderID{ 62 Project: tfa.Project, 63 Bucket: tfa.Bucket, 64 Builder: tfa.Builder, 65 } 66 } 67 68 // Get test bundle. 69 bundle, err := datastoreutil.GetTestFailureBundle(ctx, tfa) 70 if err != nil { 71 return nil, errors.Annotate(err, "get test failure bundle").Err() 72 } 73 includeTestFailures := tfaMask.MustIncludes("test_failures") 74 if includeTestFailures == mask.IncludeEntirely || includeTestFailures == mask.IncludePartially { 75 tfMask := tfaMask.MustSubmask("test_failures.*") 76 result.TestFailures = TestFailureBundleToPb(ctx, bundle, tfMask) 77 } 78 primary := bundle.Primary() 79 80 if tfaMask.MustIncludes("start_failure_rate") == mask.IncludeEntirely { 81 result.StartFailureRate = float32(primary.StartPositionFailureRate) 82 } 83 if tfaMask.MustIncludes("end_failure_rate") == mask.IncludeEntirely { 84 result.EndFailureRate = float32(primary.EndPositionFailureRate) 85 } 86 // It doesn't make sense to return commit information partially. 87 // We don't check mask.IncludePartially here. 88 if tfaMask.MustIncludes("start_commit") == mask.IncludeEntirely { 89 result.StartCommit = &buildbucketpb.GitilesCommit{ 90 Host: primary.Ref.GetGitiles().GetHost(), 91 Project: primary.Ref.GetGitiles().GetProject(), 92 Ref: primary.Ref.GetGitiles().GetRef(), 93 Id: tfa.StartCommitHash, 94 Position: uint32(primary.RegressionStartPosition), 95 } 96 } 97 if tfaMask.MustIncludes("end_commit") == mask.IncludeEntirely { 98 result.EndCommit = &buildbucketpb.GitilesCommit{ 99 Host: primary.Ref.GetGitiles().GetHost(), 100 Project: primary.Ref.GetGitiles().GetProject(), 101 Ref: primary.Ref.GetGitiles().GetRef(), 102 Id: tfa.EndCommitHash, 103 Position: uint32(primary.RegressionEndPosition), 104 } 105 } 106 107 nsa, err := datastoreutil.GetTestNthSectionForAnalysis(ctx, tfa) 108 if err != nil { 109 return nil, errors.Annotate(err, "get test nthsection for analysis").Err() 110 } 111 if nsa != nil { 112 includeNsa := tfaMask.MustIncludes("nth_section_result") 113 if includeNsa == mask.IncludeEntirely || includeNsa == mask.IncludePartially { 114 nsaMask := tfaMask.MustSubmask("nth_section_result") 115 nsaResult, err := NthSectionAnalysisToPb(ctx, tfa, nsa, primary.Ref, nsaMask) 116 if err != nil { 117 return nil, errors.Annotate(err, "nthsection analysis to pb").Err() 118 } 119 result.NthSectionResult = nsaResult 120 } 121 includeCulprit := tfaMask.MustIncludes("culprit") 122 if includeCulprit == mask.IncludeEntirely || includeCulprit == mask.IncludePartially { 123 culpritMask := tfaMask.MustSubmask("culprit") 124 culprit, err := datastoreutil.GetVerifiedCulpritForTestAnalysis(ctx, tfa) 125 if err != nil { 126 return nil, errors.Annotate(err, "get verified culprit").Err() 127 } 128 if culprit != nil { 129 culpritPb, err := CulpritToPb(ctx, culprit, nsa, culpritMask) 130 if err != nil { 131 return nil, errors.Annotate(err, "culprit to pb").Err() 132 } 133 result.Culprit = culpritPb 134 } 135 } 136 } 137 return result, nil 138 } 139 140 func NthSectionAnalysisToPb(ctx context.Context, tfa *model.TestFailureAnalysis, nsa *model.TestNthSectionAnalysis, sourceRef *pb.SourceRef, nsaMask *mask.Mask) (*pb.TestNthSectionAnalysisResult, error) { 141 result := &pb.TestNthSectionAnalysisResult{ 142 StartTime: timestamppb.New(nsa.StartTime), 143 } 144 if nsaMask.MustIncludes("status") == mask.IncludeEntirely { 145 result.Status = nsa.Status 146 } 147 if nsaMask.MustIncludes("run_status") == mask.IncludeEntirely { 148 result.RunStatus = nsa.RunStatus 149 } 150 if nsaMask.MustIncludes("blame_list") == mask.IncludeEntirely { 151 result.BlameList = nsa.BlameList 152 } 153 if nsaMask.MustIncludes("start_time") == mask.IncludeEntirely { 154 result.StartTime = timestamppb.New(nsa.StartTime) 155 } 156 if nsaMask.MustIncludes("end_time") == mask.IncludeEntirely && nsa.HasEnded() { 157 result.EndTime = timestamppb.New(nsa.EndTime) 158 } 159 160 // Populate culprit. 161 includeSuspect := nsaMask.MustIncludes("suspect") 162 if (includeSuspect == mask.IncludeEntirely || includeSuspect == mask.IncludePartially) && nsa.CulpritKey != nil { 163 suspectMask := nsaMask.MustSubmask("suspect") 164 culprit, err := datastoreutil.GetSuspect(ctx, nsa.CulpritKey.IntID(), nsa.CulpritKey.Parent()) 165 if err != nil { 166 return nil, errors.Annotate(err, "get suspect").Err() 167 } 168 culpritPb, err := CulpritToPb(ctx, culprit, nsa, suspectMask) 169 if err != nil { 170 return nil, errors.Annotate(err, "culprit to pb").Err() 171 } 172 result.Suspect = culpritPb 173 } 174 175 // TODO(nqmtuan): Support selecting subfields of reruns. 176 // However, we don't need them now. 177 if nsaMask.MustIncludes("reruns") == mask.IncludeEntirely { 178 reruns, err := datastoreutil.GetTestNthSectionReruns(ctx, nsa) 179 if err != nil { 180 return nil, errors.Annotate(err, "get test nthsection reruns").Err() 181 } 182 pbReruns := []*pb.TestSingleRerun{} 183 for _, rerun := range reruns { 184 pbRerun, err := testSingleRerunToPb(ctx, rerun, nsa) 185 if err != nil { 186 return nil, errors.Annotate(err, "test single rerun to pb: %d", rerun.ID).Err() 187 } 188 pbReruns = append(pbReruns, pbRerun) 189 } 190 result.Reruns = pbReruns 191 } 192 193 // Populate remaining range. 194 // remaining regression range should be returned as whole. 195 if nsaMask.MustIncludes("remaining_nth_section_range") == mask.IncludeEntirely && !nsa.HasEnded() { 196 snapshot, err := bisection.CreateSnapshot(ctx, nsa) 197 if err != nil { 198 return nil, errors.Annotate(err, "couldn't create snapshot").Err() 199 } 200 ff, lp, err := snapshot.GetCurrentRegressionRange() 201 // GetCurrentRegressionRange return error if the regression is invalid. 202 // It is not exactly an error, but just a state of the analysis. 203 if err == nil { 204 // GetCurrentRegressionRange returns a pair of indices from the Snapshot that contains the culprit. 205 // So to really get the last pass, we should add 1. 206 lp++ 207 lpCommitID := "" 208 209 // Blamelist only contains the possible commits for culprit. 210 // So it may or may not contain last pass. It will contain 211 // last pass if the regression range has been narrowed down during bisection, 212 // and the original last pass has been updated. 213 // In case that the blamelist does not contain last pass, we should get it 214 // from LastPassCommit. 215 if lp < len(nsa.BlameList.Commits) { 216 lpCommitID = nsa.BlameList.Commits[lp].Commit 217 } else { 218 // Old data do not have LastPassCommit populated, so we will check here. 219 if nsa.BlameList.LastPassCommit != nil { 220 lpCommitID = nsa.BlameList.LastPassCommit.Commit 221 } 222 } 223 if lpCommitID != "" { 224 result.RemainingNthSectionRange = &pb.RegressionRange{ 225 FirstFailed: &buildbucketpb.GitilesCommit{ 226 Host: sourceRef.GetGitiles().Host, 227 Project: sourceRef.GetGitiles().Project, 228 Ref: sourceRef.GetGitiles().Ref, 229 Id: nsa.BlameList.Commits[ff].Commit, 230 }, 231 LastPassed: &buildbucketpb.GitilesCommit{ 232 Host: sourceRef.GetGitiles().Host, 233 Project: sourceRef.GetGitiles().Project, 234 Ref: sourceRef.GetGitiles().Ref, 235 Id: lpCommitID, 236 }, 237 } 238 } 239 } 240 } 241 242 return result, nil 243 } 244 245 func CulpritToPb(ctx context.Context, culprit *model.Suspect, nsa *model.TestNthSectionAnalysis, culpritMask *mask.Mask) (*pb.TestCulprit, error) { 246 result := &pb.TestCulprit{} 247 248 if culpritMask.MustIncludes("commit") == mask.IncludeEntirely { 249 result.Commit = &buildbucketpb.GitilesCommit{ 250 Host: culprit.GitilesCommit.Host, 251 Project: culprit.GitilesCommit.Project, 252 Ref: culprit.GitilesCommit.Ref, 253 Id: culprit.GitilesCommit.Id, 254 Position: culprit.GitilesCommit.Position, 255 } 256 } 257 258 if culpritMask.MustIncludes("review_url") == mask.IncludeEntirely { 259 result.ReviewUrl = culprit.ReviewUrl 260 } 261 262 if culpritMask.MustIncludes("review_title") == mask.IncludeEntirely { 263 result.ReviewTitle = culprit.ReviewTitle 264 } 265 266 // TODO (nqmtuan): Support selecting subfields of actions. 267 // However, we don't need them now. 268 if culpritMask.MustIncludes("culprit_action") == mask.IncludeEntirely { 269 result.CulpritAction = CulpritActionsForSuspect(culprit) 270 } 271 272 includeDetails := culpritMask.MustIncludes("verification_details") 273 if includeDetails == mask.IncludeEntirely || includeDetails == mask.IncludePartially { 274 detailsMask := culpritMask.MustSubmask("verification_details") 275 verificationDetails, err := testVerificationDetails(ctx, culprit, nsa, detailsMask) 276 if err != nil { 277 return nil, errors.Annotate(err, "test verification details").Err() 278 } 279 result.VerificationDetails = verificationDetails 280 } 281 return result, nil 282 } 283 284 func TestFailureBundleToPb(ctx context.Context, bundle *model.TestFailureBundle, mask *mask.Mask) []*pb.TestFailure { 285 result := []*pb.TestFailure{} 286 // Add primary test failure first, and the rest. 287 // Primary should not be nil here, because it is from GetTestFailureBundle. 288 primary := bundle.Primary() 289 result = append(result, testFailureToPb(ctx, primary, mask)) 290 for _, tf := range bundle.Others() { 291 result = append(result, testFailureToPb(ctx, tf, mask)) 292 } 293 return result 294 } 295 296 func testFailureToPb(ctx context.Context, tf *model.TestFailure, tfMask *mask.Mask) *pb.TestFailure { 297 result := &pb.TestFailure{} 298 if tfMask.MustIncludes("test_id") == mask.IncludeEntirely { 299 result.TestId = tf.TestID 300 } 301 if tfMask.MustIncludes("variant_hash") == mask.IncludeEntirely { 302 result.VariantHash = tf.VariantHash 303 } 304 if tfMask.MustIncludes("ref_hash") == mask.IncludeEntirely { 305 result.RefHash = tf.RefHash 306 } 307 if tfMask.MustIncludes("variant") == mask.IncludeEntirely { 308 result.Variant = tf.Variant 309 } 310 if tfMask.MustIncludes("is_diverged") == mask.IncludeEntirely { 311 result.IsDiverged = tf.IsDiverged 312 } 313 if tfMask.MustIncludes("is_primary") == mask.IncludeEntirely { 314 result.IsPrimary = tf.IsPrimary 315 } 316 if tfMask.MustIncludes("start_hour") == mask.IncludeEntirely { 317 result.StartHour = timestamppb.New(tf.StartHour) 318 } 319 return result 320 } 321 322 func testVerificationDetails(ctx context.Context, culprit *model.Suspect, nsa *model.TestNthSectionAnalysis, detailsMask *mask.Mask) (*pb.TestSuspectVerificationDetails, error) { 323 verificationDetails := &pb.TestSuspectVerificationDetails{} 324 if detailsMask.MustIncludes("status") == mask.IncludeEntirely { 325 verificationDetails.Status = verificationStatusToPb(culprit.VerificationStatus) 326 } 327 328 // TODO(nqmtuan): Support selecting subfields of reruns. 329 // However, we don't need them now. 330 if detailsMask.MustIncludes("suspect_rerun") == mask.IncludeEntirely || detailsMask.MustIncludes("parent_rerun") == mask.IncludeEntirely { 331 suspectRerun, parentRerun, err := datastoreutil.GetVerificationRerunsForTestCulprit(ctx, culprit) 332 if err != nil { 333 return nil, errors.Annotate(err, "get verification reruns for test culprit").Err() 334 } 335 336 if detailsMask.MustIncludes("suspect_rerun") == mask.IncludeEntirely && suspectRerun != nil { 337 verificationDetails.SuspectRerun, err = testSingleRerunToPb(ctx, suspectRerun, nsa) 338 if err != nil { 339 return nil, errors.Annotate(err, "suspect rerun to pb").Err() 340 } 341 } 342 if detailsMask.MustIncludes("parent_rerun") == mask.IncludeEntirely && parentRerun != nil { 343 verificationDetails.ParentRerun, err = testSingleRerunToPb(ctx, parentRerun, nsa) 344 if err != nil { 345 return nil, errors.Annotate(err, "parent rerun to pb").Err() 346 } 347 } 348 } 349 return verificationDetails, nil 350 } 351 352 func verificationStatusToPb(status model.SuspectVerificationStatus) pb.SuspectVerificationStatus { 353 switch status { 354 case model.SuspectVerificationStatus_Unverified: 355 return pb.SuspectVerificationStatus_UNVERIFIED 356 case model.SuspectVerificationStatus_VerificationScheduled: 357 return pb.SuspectVerificationStatus_VERIFICATION_SCHEDULED 358 case model.SuspectVerificationStatus_UnderVerification: 359 return pb.SuspectVerificationStatus_UNDER_VERIFICATION 360 case model.SuspectVerificationStatus_ConfirmedCulprit: 361 return pb.SuspectVerificationStatus_CONFIRMED_CULPRIT 362 case model.SuspectVerificationStatus_Vindicated: 363 return pb.SuspectVerificationStatus_VINDICATED 364 case model.SuspectVerificationStatus_VerificationError: 365 return pb.SuspectVerificationStatus_VERIFICATION_ERROR 366 case model.SuspectVerificationStatus_Canceled: 367 return pb.SuspectVerificationStatus_VERIFICATION_CANCELED 368 default: 369 return pb.SuspectVerificationStatus_SUSPECT_VERIFICATION_STATUS_UNSPECIFIED 370 } 371 } 372 373 func testSingleRerunToPb(ctx context.Context, rerun *model.TestSingleRerun, nsa *model.TestNthSectionAnalysis) (*pb.TestSingleRerun, error) { 374 result := &pb.TestSingleRerun{ 375 Bbid: rerun.ID, 376 CreateTime: timestamppb.New(rerun.CreateTime), 377 Commit: &buildbucketpb.GitilesCommit{ 378 Host: rerun.LUCIBuild.GitilesCommit.GetHost(), 379 Project: rerun.LUCIBuild.GitilesCommit.GetProject(), 380 Ref: rerun.LUCIBuild.GitilesCommit.GetRef(), 381 Id: rerun.LUCIBuild.GitilesCommit.GetId(), 382 }, 383 } 384 if rerun.HasStarted() { 385 result.StartTime = timestamppb.New(rerun.StartTime) 386 } 387 if rerun.HasEnded() { 388 result.EndTime = timestamppb.New(rerun.EndTime) 389 } 390 if rerun.ReportTime.Unix() != 0 { 391 result.ReportTime = timestamppb.New(rerun.ReportTime) 392 } 393 394 index := changelogutil.FindCommitIndexInBlameList(rerun.GitilesCommit, nsa.BlameList) 395 // There is only one case where we cannot find the rerun in blamelist 396 // It is when the rerun is part of the culprit verification and is 397 // the "last pass" revision. 398 // In this case, we should continue. 399 if index != -1 { 400 result.Index = strconv.FormatInt(int64(index), 10) 401 result.Commit.Position = uint32(nsa.BlameList.Commits[index].Position) 402 } 403 404 // Update rerun results. 405 pbRerunResults, err := rerunResultsToPb(ctx, rerun.TestResults, rerun.Status) 406 if err != nil { 407 return nil, errors.Annotate(err, "rerun results to pb").Err() 408 } 409 result.RerunResult = pbRerunResults 410 return result, nil 411 } 412 413 func rerunResultsToPb(ctx context.Context, testResults model.RerunTestResults, status pb.RerunStatus) (*pb.RerunTestResults, error) { 414 pb := &pb.RerunTestResults{ 415 RerunStatus: status, 416 } 417 if !testResults.IsFinalized { 418 return pb, nil 419 } 420 for _, singleResult := range testResults.Results { 421 pbSingleResult, err := rerunTestSingleResultToPb(ctx, singleResult) 422 if err != nil { 423 return nil, errors.Annotate(err, "rerun test single result to pb").Err() 424 } 425 pb.Results = append(pb.Results, pbSingleResult) 426 } 427 return pb, nil 428 } 429 430 func rerunTestSingleResultToPb(ctx context.Context, singleResult model.RerunSingleTestResult) (*pb.RerunTestSingleResult, error) { 431 pb := &pb.RerunTestSingleResult{ 432 ExpectedCount: singleResult.ExpectedCount, 433 UnexpectedCount: singleResult.UnexpectedCount, 434 } 435 tf, err := datastoreutil.GetTestFailure(ctx, singleResult.TestFailureKey.IntID()) 436 if err != nil { 437 return nil, errors.Annotate(err, "get test failure").Err() 438 } 439 pb.TestId = tf.TestID 440 pb.VariantHash = tf.VariantHash 441 return pb, nil 442 } 443 444 func CulpritActionsForSuspect(suspect *model.Suspect) []*pb.CulpritAction { 445 culpritActions := []*pb.CulpritAction{} 446 if suspect.IsRevertCommitted { 447 // culprit action for auto-committing a revert 448 culpritActions = append(culpritActions, &pb.CulpritAction{ 449 ActionType: pb.CulpritActionType_CULPRIT_AUTO_REVERTED, 450 RevertClUrl: suspect.RevertURL, 451 ActionTime: timestamppb.New(suspect.RevertCommitTime), 452 }) 453 } else if suspect.IsRevertCreated { 454 // culprit action for creating a revert 455 culpritActions = append(culpritActions, &pb.CulpritAction{ 456 ActionType: pb.CulpritActionType_REVERT_CL_CREATED, 457 RevertClUrl: suspect.RevertURL, 458 ActionTime: timestamppb.New(suspect.RevertCreateTime), 459 }) 460 } else if suspect.HasSupportRevertComment { 461 // culprit action for commenting on an existing revert 462 culpritActions = append(culpritActions, &pb.CulpritAction{ 463 ActionType: pb.CulpritActionType_EXISTING_REVERT_CL_COMMENTED, 464 RevertClUrl: suspect.RevertURL, 465 ActionTime: timestamppb.New(suspect.SupportRevertCommentTime), 466 }) 467 } else if suspect.HasCulpritComment { 468 // culprit action for commenting on the culprit 469 culpritActions = append(culpritActions, &pb.CulpritAction{ 470 ActionType: pb.CulpritActionType_CULPRIT_CL_COMMENTED, 471 ActionTime: timestamppb.New(suspect.CulpritCommentTime), 472 }) 473 } else { 474 action := &pb.CulpritAction{ 475 ActionType: pb.CulpritActionType_NO_ACTION, 476 InactionReason: suspect.InactionReason, 477 } 478 if suspect.RevertURL != "" { 479 action.RevertClUrl = suspect.RevertURL 480 } 481 culpritActions = append(culpritActions, action) 482 } 483 return culpritActions 484 }