blob: e2530d8ee31ec78739ed6a4b25ae13f38d64c544 [file] [log] [blame]
// 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
}