go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/clustering/ingestion/resultdb.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 ingestion
    16  
    17  import (
    18  	"fmt"
    19  
    20  	"google.golang.org/protobuf/proto"
    21  	"google.golang.org/protobuf/types/known/timestamppb"
    22  
    23  	rdbpb "go.chromium.org/luci/resultdb/proto/v1"
    24  
    25  	cpb "go.chromium.org/luci/analysis/internal/clustering/proto"
    26  	"go.chromium.org/luci/analysis/internal/ingestion/resultdb"
    27  	"go.chromium.org/luci/analysis/pbutil"
    28  	pb "go.chromium.org/luci/analysis/proto/v1"
    29  )
    30  
    31  func failuresFromTestVariant(opts Options, tv TestVerdict) []*cpb.Failure {
    32  	var failures []*cpb.Failure
    33  	if tv.Verdict.Status == rdbpb.TestVariantStatus_EXPECTED {
    34  		// Short circuit: There will be nothing in the test variant to
    35  		// ingest, as everything is expected.
    36  		return nil
    37  	}
    38  
    39  	// Whether there were any (non-skip) passed or expected results.
    40  	var hasPass bool
    41  	for _, tr := range tv.Verdict.Results {
    42  		if tr.Result.Status != rdbpb.TestStatus_SKIP &&
    43  			(tr.Result.Status == rdbpb.TestStatus_PASS ||
    44  				tr.Result.Expected) {
    45  			hasPass = true
    46  		}
    47  	}
    48  
    49  	// Group test results by run and sort in order of start time.
    50  	resultsByRun := resultdb.GroupAndOrderTestResults(tv.Verdict.Results)
    51  
    52  	resultIndex := 0
    53  	for _, run := range resultsByRun {
    54  		// Whether there were any passed or expected results in the run.
    55  		var testRunHasPass bool
    56  		for _, tr := range run {
    57  			if tr.Result.Status != rdbpb.TestStatus_SKIP &&
    58  				(tr.Result.Status == rdbpb.TestStatus_PASS ||
    59  					tr.Result.Expected) {
    60  				testRunHasPass = true
    61  			}
    62  		}
    63  
    64  		for i, tr := range run {
    65  			if tr.Result.Expected || !isFailure(tr.Result.Status) {
    66  				// Only unexpected failures are ingested for clustering.
    67  				resultIndex++
    68  				continue
    69  			}
    70  
    71  			failure := failureFromResult(tr.Result, tv, opts)
    72  			failure.IngestedInvocationResultIndex = int64(resultIndex)
    73  			failure.IngestedInvocationResultCount = int64(len(tv.Verdict.Results))
    74  			failure.IsIngestedInvocationBlocked = !hasPass
    75  			failure.TestRunResultIndex = int64(i)
    76  			failure.TestRunResultCount = int64(len(run))
    77  			failure.IsTestRunBlocked = !testRunHasPass
    78  			failures = append(failures, failure)
    79  
    80  			resultIndex++
    81  		}
    82  	}
    83  	return failures
    84  }
    85  
    86  func isFailure(s rdbpb.TestStatus) bool {
    87  	return (s == rdbpb.TestStatus_ABORT ||
    88  		s == rdbpb.TestStatus_CRASH ||
    89  		s == rdbpb.TestStatus_FAIL)
    90  }
    91  
    92  func failureFromResult(tr *rdbpb.TestResult, tv TestVerdict, opts Options) *cpb.Failure {
    93  	exonerations := make([]*cpb.TestExoneration, 0, len(tv.Verdict.Exonerations))
    94  	for _, e := range tv.Verdict.Exonerations {
    95  		exonerations = append(exonerations, exonerationFromResultDB(e))
    96  	}
    97  
    98  	var presubmitRun *cpb.PresubmitRun
    99  	var buildCritical *bool
   100  	if opts.PresubmitRun != nil {
   101  		presubmitRun = &cpb.PresubmitRun{
   102  			PresubmitRunId: opts.PresubmitRun.ID,
   103  			Owner:          opts.PresubmitRun.Owner,
   104  			Mode:           opts.PresubmitRun.Mode,
   105  			Status:         opts.PresubmitRun.Status,
   106  		}
   107  		buildCritical = &opts.BuildCritical
   108  	}
   109  
   110  	testRunID, err := resultdb.InvocationFromTestResultName(tr.Name)
   111  	if err != nil {
   112  		// Should never happen, as the result name from ResultDB
   113  		// should be valid.
   114  		panic(err)
   115  	}
   116  
   117  	result := &cpb.Failure{
   118  		TestResultId:                  pbutil.TestResultIDFromResultDB(tr.Name),
   119  		PartitionTime:                 timestamppb.New(opts.PartitionTime),
   120  		ChunkIndex:                    0, // To be populated by chunking.
   121  		Realm:                         opts.Realm,
   122  		TestId:                        tv.Verdict.TestId,                              // Get from variant, as it is not populated on each result.
   123  		Variant:                       pbutil.VariantFromResultDB(tv.Verdict.Variant), // Get from variant, as it is not populated on each result.
   124  		Tags:                          pbutil.StringPairFromResultDB(tr.Tags),
   125  		VariantHash:                   tv.Verdict.VariantHash, // Get from variant, as it is not populated on each result.
   126  		FailureReason:                 pbutil.FailureReasonFromResultDB(tr.FailureReason),
   127  		BugTrackingComponent:          extractBugTrackingComponent(tr.Tags, tv.Verdict.TestMetadata, opts.PreferBuganizerComponents),
   128  		StartTime:                     tr.StartTime,
   129  		Duration:                      tr.Duration,
   130  		Exonerations:                  exonerations,
   131  		PresubmitRun:                  presubmitRun,
   132  		BuildStatus:                   opts.BuildStatus,
   133  		BuildCritical:                 buildCritical,
   134  		IngestedInvocationId:          opts.InvocationID,
   135  		IngestedInvocationResultIndex: -1,    // To be populated by caller.
   136  		IngestedInvocationResultCount: -1,    // To be populated by caller.
   137  		IsIngestedInvocationBlocked:   false, // To be populated by caller.
   138  		TestRunId:                     testRunID,
   139  		TestRunResultIndex:            -1,    // To be populated by caller.
   140  		TestRunResultCount:            -1,    // To be populated by caller.
   141  		IsTestRunBlocked:              false, // To be populated by caller.
   142  		Sources:                       tv.Sources,
   143  		BuildGardenerRotations:        opts.BuildGardenerRotations,
   144  		TestVariantBranch:             tv.TestVariantBranch,
   145  	}
   146  
   147  	// Copy the result to avoid the result aliasing any of the protos used as input.
   148  	return proto.Clone(result).(*cpb.Failure)
   149  }
   150  
   151  func exonerationFromResultDB(e *rdbpb.TestExoneration) *cpb.TestExoneration {
   152  	return &cpb.TestExoneration{
   153  		Reason: pbutil.ExonerationReasonFromResultDB(e.Reason),
   154  	}
   155  }
   156  
   157  func extractBugTrackingComponent(tags []*rdbpb.StringPair, testMetadata *rdbpb.TestMetadata, preferBuganizerComponents bool) *pb.BugTrackingComponent {
   158  	if testMetadata != nil && testMetadata.BugComponent != nil {
   159  		bc := testMetadata.BugComponent
   160  		switch bcSystem := bc.System.(type) {
   161  		case *rdbpb.BugComponent_IssueTracker:
   162  			return &pb.BugTrackingComponent{
   163  				System:    "buganizer",
   164  				Component: fmt.Sprint(bcSystem.IssueTracker.ComponentId),
   165  			}
   166  		case *rdbpb.BugComponent_Monorail:
   167  			return &pb.BugTrackingComponent{
   168  				System:    "monorail",
   169  				Component: bcSystem.Monorail.Value,
   170  			}
   171  		}
   172  	} else {
   173  		var buganizerComponent string
   174  		for _, tag := range tags {
   175  			if tag.Key == "public_buganizer_component" {
   176  				buganizerComponent = tag.Value
   177  				break
   178  			}
   179  		}
   180  		var monorailComponent string
   181  		for _, tag := range tags {
   182  			if tag.Key == "monorail_component" {
   183  				monorailComponent = tag.Value
   184  				break
   185  			}
   186  		}
   187  
   188  		if preferBuganizerComponents && buganizerComponent != "" {
   189  			return &pb.BugTrackingComponent{
   190  				System:    "buganizer",
   191  				Component: buganizerComponent,
   192  			}
   193  		}
   194  
   195  		if monorailComponent != "" {
   196  			return &pb.BugTrackingComponent{
   197  				System:    "monorail",
   198  				Component: monorailComponent,
   199  			}
   200  		}
   201  
   202  		if buganizerComponent != "" {
   203  			return &pb.BugTrackingComponent{
   204  				System:    "buganizer",
   205  				Component: buganizerComponent,
   206  			}
   207  		}
   208  	}
   209  
   210  	return nil
   211  }