| // Copyright 2022 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package main |
| |
| import ( |
| "fmt" |
| "path" |
| "strings" |
| |
| buildbucketpb "go.chromium.org/luci/buildbucket/proto" |
| gerritpb "go.chromium.org/luci/common/proto/gerrit" |
| "go.chromium.org/luci/common/proto/git" |
| "golang.org/x/exp/maps" |
| ) |
| |
| // suspectCommit represents a commit that is being considered as a potential |
| // culprit. |
| type suspectCommit struct { |
| // Signature of the failure mode for which this commit is a suspect. This is |
| // only attached to each suspectCommit so that `features()` can access it |
| // features that depend on the details of the failure mode. |
| signature failureSignature `json:"-"` |
| |
| CommitInfo *git.Commit `json:"-"` |
| GerritChange *buildbucketpb.GerritChange `json:"-"` |
| // ChangeInfo is only set after filtering down the list of commits to |
| // high-probability suspects, so it cannot be assumed to be set. |
| ChangeInfo *gerritpb.ChangeInfo `json:"-"` |
| CommitPosition int `json:"commit_position"` |
| AffectedTest bool `json:"affected_test"` |
| // TODO(olivernewman): Only consider tags that appear in a small subset of |
| // test names. Tags like "test" or "fuchsia" should not be considered. |
| TagMatchesTest bool `json:"tag_matches_test"` |
| // For each CI builder where the test is failing, the number of builds |
| // containing this commit that passed before the first failed build. |
| // If the first failure was *before* the commit landed, the value will be |
| // negative. |
| BlamelistDistances map[string]int `json:"blamelist_distances"` |
| } |
| |
| func (c *suspectCommit) changedFiles() []string { |
| if c.ChangeInfo == nil { |
| return nil |
| } |
| |
| // TODO(olivernewman): Get the path to each project in the checkout using a |
| // roller commit message footer. We shouldn't be hardcoding this mapping |
| // here. |
| projectMap := map[string]string{ |
| "fuchsia": "", |
| "experiences": "src/experiences", |
| } |
| projectDir, ok := projectMap[c.ChangeInfo.Project] |
| if !ok { |
| projectDir = c.ChangeInfo.Project |
| } |
| |
| var files []string |
| revision := c.ChangeInfo.Revisions[c.ChangeInfo.CurrentRevision] |
| for file := range revision.Files { |
| if projectDir != "" { |
| file = path.Join(projectDir, file) |
| } |
| files = append(files, file) |
| } |
| return files |
| } |
| |
| func (c *suspectCommit) gerritURL() string { |
| return fmt.Sprintf( |
| "https://%s/c/%s/+/%d", |
| c.GerritChange.Host, |
| c.GerritChange.Project, |
| c.GerritChange.Change, |
| ) |
| } |
| |
| func (c *suspectCommit) commitSummary() string { |
| return strings.Split(c.CommitInfo.Message, "\n")[0] |
| } |
| |
| // score computes the 0-100 culprit score of the change, where a higher number |
| // means the commit is more likely to be the cause of the failure mode in |
| // question. |
| func (c *suspectCommit) score() int { |
| var total, possible int |
| for _, feature := range c.features() { |
| if feature.Score < 0 || feature.Score > 100 { |
| fmt.Printf("warning: score is out of range for change %s: %+v\n", c.gerritURL(), feature) |
| if feature.Score < 0 { |
| feature.Score = 0 |
| } else if feature.Score > 100 { |
| feature.Score = 100 |
| } |
| } |
| // TODO: also take into account how rare it is for a suspect commit to |
| // have a high score for this feature. We might want to lower the |
| // weighting for features where many suspects are determined to have the |
| // same score. |
| total += feature.Score * feature.Weight |
| possible += 100 * feature.Weight |
| } |
| return (100 * total) / possible |
| } |
| |
| // features returns all the individually computed feature scores from the |
| // various data sources used to assess the suspect commit. |
| func (c *suspectCommit) features() []culpritFeature { |
| var affectedScore int |
| if c.AffectedTest { |
| affectedScore = 100 |
| } |
| var tagMatchesScore int |
| if c.TagMatchesTest { |
| tagMatchesScore = 100 |
| } |
| res := []culpritFeature{ |
| { |
| Name: "blamelist distances", |
| Score: scoreBlamelistDistances(maps.Values(c.BlamelistDistances)), |
| Weight: 4, |
| }, |
| { |
| Name: "test affected by change", |
| Score: affectedScore, |
| Weight: 3, |
| }, |
| { |
| Name: "commit tag matches test name", |
| Score: tagMatchesScore, |
| Weight: 1, |
| }, |
| } |
| if c.ChangeInfo != nil { |
| res = append(res, culpritFeature{ |
| Name: "changed file proximity", |
| Score: scoreChangedFileProximity(c.changedFiles(), c.signature.TestGNLabel), |
| Weight: 4, |
| }) |
| } |
| return res |
| } |