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 }