go.fuchsia.dev/infra@v0.0.0-20240507153436-9b593402251b/cmd/autogardener/suspect.go (about)

     1  // Copyright 2022 The Fuchsia Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style license that can be
     3  // found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"fmt"
     9  	"path"
    10  	"strings"
    11  
    12  	buildbucketpb "go.chromium.org/luci/buildbucket/proto"
    13  	gerritpb "go.chromium.org/luci/common/proto/gerrit"
    14  	"go.chromium.org/luci/common/proto/git"
    15  	"golang.org/x/exp/maps"
    16  )
    17  
    18  // suspectCommit represents a commit that is being considered as a potential
    19  // culprit.
    20  type suspectCommit struct {
    21  	// Signature of the failure mode for which this commit is a suspect. This is
    22  	// only attached to each suspectCommit so that `features()` can access it
    23  	// features that depend on the details of the failure mode.
    24  	signature failureSignature `json:"-"`
    25  
    26  	CommitInfo   *git.Commit                 `json:"-"`
    27  	GerritChange *buildbucketpb.GerritChange `json:"-"`
    28  	// ChangeInfo is only set after filtering down the list of commits to
    29  	// high-probability suspects, so it cannot be assumed to be set.
    30  	ChangeInfo     *gerritpb.ChangeInfo `json:"-"`
    31  	CommitPosition int                  `json:"commit_position"`
    32  	AffectedTest   bool                 `json:"affected_test"`
    33  	// TODO(olivernewman): Only consider tags that appear in a small subset of
    34  	// test names. Tags like "test" or "fuchsia" should not be considered.
    35  	TagMatchesTest bool `json:"tag_matches_test"`
    36  	// For each CI builder where the test is failing, the number of builds
    37  	// containing this commit that passed before the first failed build.
    38  	// If the first failure was *before* the commit landed, the value will be
    39  	// negative.
    40  	BlamelistDistances map[string]int `json:"blamelist_distances"`
    41  }
    42  
    43  func (c *suspectCommit) changedFiles() []string {
    44  	if c.ChangeInfo == nil {
    45  		return nil
    46  	}
    47  
    48  	// TODO(olivernewman): Get the path to each project in the checkout using a
    49  	// roller commit message footer. We shouldn't be hardcoding this mapping
    50  	// here.
    51  	projectMap := map[string]string{
    52  		"fuchsia":     "",
    53  		"experiences": "src/experiences",
    54  	}
    55  	projectDir, ok := projectMap[c.ChangeInfo.Project]
    56  	if !ok {
    57  		projectDir = c.ChangeInfo.Project
    58  	}
    59  
    60  	var files []string
    61  	revision := c.ChangeInfo.Revisions[c.ChangeInfo.CurrentRevision]
    62  	for file := range revision.Files {
    63  		if projectDir != "" {
    64  			file = path.Join(projectDir, file)
    65  		}
    66  		files = append(files, file)
    67  	}
    68  	return files
    69  }
    70  
    71  func (c *suspectCommit) gerritURL() string {
    72  	return fmt.Sprintf(
    73  		"https://%s/c/%s/+/%d",
    74  		c.GerritChange.Host,
    75  		c.GerritChange.Project,
    76  		c.GerritChange.Change,
    77  	)
    78  }
    79  
    80  func (c *suspectCommit) commitSummary() string {
    81  	return strings.Split(c.CommitInfo.Message, "\n")[0]
    82  }
    83  
    84  // score computes the 0-100 culprit score of the change, where a higher number
    85  // means the commit is more likely to be the cause of the failure mode in
    86  // question.
    87  func (c *suspectCommit) score() int {
    88  	var total, possible int
    89  	for _, feature := range c.features() {
    90  		if feature.Score < 0 || feature.Score > 100 {
    91  			fmt.Printf("warning: score is out of range for change %s: %+v\n", c.gerritURL(), feature)
    92  			if feature.Score < 0 {
    93  				feature.Score = 0
    94  			} else if feature.Score > 100 {
    95  				feature.Score = 100
    96  			}
    97  		}
    98  		// TODO: also take into account how rare it is for a suspect commit to
    99  		// have a high score for this feature. We might want to lower the
   100  		// weighting for features where many suspects are determined to have the
   101  		// same score.
   102  		total += feature.Score * feature.Weight
   103  		possible += 100 * feature.Weight
   104  	}
   105  	return (100 * total) / possible
   106  }
   107  
   108  // features returns all the individually computed feature scores from the
   109  // various data sources used to assess the suspect commit.
   110  func (c *suspectCommit) features() []culpritFeature {
   111  	var affectedScore int
   112  	if c.AffectedTest {
   113  		affectedScore = 100
   114  	}
   115  	var tagMatchesScore int
   116  	if c.TagMatchesTest {
   117  		tagMatchesScore = 100
   118  	}
   119  	res := []culpritFeature{
   120  		{
   121  			Name:   "blamelist distances",
   122  			Score:  scoreBlamelistDistances(maps.Values(c.BlamelistDistances)),
   123  			Weight: 4,
   124  		},
   125  		{
   126  			Name:   "test affected by change",
   127  			Score:  affectedScore,
   128  			Weight: 3,
   129  		},
   130  		{
   131  			Name:   "commit tag matches test name",
   132  			Score:  tagMatchesScore,
   133  			Weight: 1,
   134  		},
   135  	}
   136  	if c.ChangeInfo != nil {
   137  		res = append(res, culpritFeature{
   138  			Name:   "changed file proximity",
   139  			Score:  scoreChangedFileProximity(c.changedFiles(), c.signature.TestGNLabel),
   140  			Weight: 4,
   141  		})
   142  	}
   143  	return res
   144  }