infra/utils/fbf/cmd/flaky.go (126 lines of code) (raw):

package cmd import ( "fmt" "os" "time" "github.com/briandowns/spinner" "github.com/jedib0t/go-pretty/v6/table" ) // FlakyFinder finds flakes between start and end times type FlakyFinder struct { startTime time.Time endTime time.Time projectID string verbose bool flakes map[string]flake } // flake represents a collection of flaky builds for a given commit type flake struct { repo string // repo name commitSHA string // commit SHA passes map[string]*build // builds passed for commit fails map[string]*build // builds passed for commit } // build represents a single instance of a job invoken at commitSHA on a source repo type build struct { repoName string jobName string commitSHA string id string status string } // todo(bharathkkb): use config func NewFlakyFinder(start, end, projectID string, verbose bool) (*FlakyFinder, error) { startTime, err := getTimeFromStr(start) if err != nil { return nil, fmt.Errorf("error parsing startime: %v", err) } endTime, err := getTimeFromStr(end) if err != nil { return nil, fmt.Errorf("error parsing endTime: %v", err) } if projectID == "" { return nil, fmt.Errorf("error got empty project ID") } return &FlakyFinder{ startTime: startTime, endTime: endTime, projectID: projectID, verbose: verbose, }, nil } func (f *FlakyFinder) ComputeFlakes() error { // get builds s := spinner.New(spinner.CharSets[35], 500*time.Millisecond) s.Start() // todo(bharathkkb): support other build systems builds, err := getCBBuildsWithFilter(f.startTime, f.endTime, f.projectID) if err != nil { return fmt.Errorf("error getting builds: %v", err) } s.Stop() // compute flakes f.flakes = computeFlakesFromBuilds(builds) return nil } // computeFlakesFromBuilds computes flakes from a slice of builds // a collection of builds are considered flakey iff at least two builds // have passed and failed at the same commit in a repo when triggered by the same job func computeFlakesFromBuilds(builds []*build) map[string]flake { flakes := make(map[string]flake) for _, b1 := range builds { // commit may have multiple builds so the key for flake lookup is // computed from commitSHA and job name flakeKey := fmt.Sprintf("%s-%s", b1.commitSHA, b1.jobName) // skip if flakes with same flakeKey were previously computed //todo(bharathkkb): optimize, we can probably remove elems in a flake from build slice _, exists := flakes[flakeKey] if exists { continue } // store individual build info passedBuildsWithCommit := make(map[string]*build) failedBuildsWithCommit := make(map[string]*build) storeBuildInfo := func(b *build) { switch b.status { case successStatus: passedBuildsWithCommit[b.id] = b case failedStatus: failedBuildsWithCommit[b.id] = b } } storeBuildInfo(b1) for _, b2 := range builds { // match other builds with same commit,repo and job if b1.commitSHA == b2.commitSHA && b1.repoName == b2.repoName && b1.jobName == b2.jobName { storeBuildInfo(b2) } } // At least one pass and one fail for a given commit is necessary to become a flake if len(passedBuildsWithCommit) > 0 && len(failedBuildsWithCommit) > 0 { flakes[flakeKey] = flake{repo: b1.repoName, commitSHA: b1.commitSHA, passes: passedBuildsWithCommit, fails: failedBuildsWithCommit} } } return flakes } // render displays results in a tabular format func (f *FlakyFinder) Render() { // verbose table with build ids tableVerbose := table.NewWriter() tableVerbose.SetOutputMirror(os.Stdout) tableVerbose.AppendHeader(table.Row{"Repo", "Commit", "Pass Build IDs", "Fail Build IDs"}) // flakes per repo repoFlakeCount := make(map[string]int) // flake failures per repo repoFlakeFailCount := make(map[string]int) for _, f := range f.flakes { repoFlakeCount[f.repo]++ repoFlakeFailCount[f.repo] += len(f.fails) pass := "" for id := range f.passes { pass += id + "\n" } fail := "" for id := range f.fails { fail += id + "\n" } tableVerbose.AppendRow(table.Row{f.repo, f.commitSHA, pass, fail}) tableVerbose.AppendSeparator() } if f.verbose { tableVerbose.Render() } // overview table with total number of flakes per repo tableOverview := table.NewWriter() tableOverview.SetOutputMirror(os.Stdout) tableOverview.AppendHeader(table.Row{"Repo", "Flakes", "Flake Failures"}) totalFlakeCount := 0 totalFlakeFailCount := 0 for repo, flakeCount := range repoFlakeCount { tableOverview.AppendRow(table.Row{repo, flakeCount, repoFlakeFailCount[repo]}) totalFlakeCount += flakeCount totalFlakeFailCount += repoFlakeFailCount[repo] } tableOverview.AppendFooter(table.Row{"Total", totalFlakeCount, totalFlakeFailCount}) tableOverview.Render() }