github.com/abayer/test-infra@v0.0.5/robots/issue-creator/sources/flakyjob-reporter.go (about)

     1  /*
     2  Copyright 2017 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package sources
    18  
    19  import (
    20  	"bytes"
    21  	"encoding/json"
    22  	"flag"
    23  	"fmt"
    24  	"sort"
    25  	"time"
    26  
    27  	"github.com/golang/glog"
    28  
    29  	githubapi "github.com/google/go-github/github"
    30  	"k8s.io/test-infra/mungegithub/mungers/mungerutil"
    31  	"k8s.io/test-infra/robots/issue-creator/creator"
    32  )
    33  
    34  // FlakyJob is a struct that represents a single job and the flake data associated with it.
    35  // FlakyJob implements the Issue interface so that it can be synced with github issues via the IssueCreator.
    36  type FlakyJob struct {
    37  	// Name is the job's name.
    38  	Name string
    39  	// Consistency is the percentage of builds that passed.
    40  	Consistency *float64 `json:"consistency"`
    41  	// FlakeCount is the number of flakes.
    42  	FlakeCount *int `json:"flakes"`
    43  	// FlakyTests is a map of test names to the number of times that test failed.
    44  	// Any test that failed at least once a day for the past week on this job is included.
    45  	FlakyTests map[string]int `json:"flakiest"`
    46  	// testsSorted is a list of the FlakyTests test names sorted by desc. number of flakes.
    47  	// This field is lazily populated and should be accessed via TestsSorted().
    48  	testsSorted []string
    49  
    50  	// reporter is a pointer to the FlakyJobReporter that created this FlakyJob.
    51  	reporter *FlakyJobReporter
    52  }
    53  
    54  // FlakyJobReporter is a munger that creates github issues for the flakiest kubernetes jobs.
    55  // The flakiest jobs are parsed from JSON generated by /test-infra/experiment/bigquery/flakes.sh
    56  type FlakyJobReporter struct {
    57  	flakyJobDataURL string
    58  	syncCount       int
    59  
    60  	creator *creator.IssueCreator
    61  }
    62  
    63  func init() {
    64  	creator.RegisterSourceOrDie("flakyjob-reporter", &FlakyJobReporter{})
    65  }
    66  
    67  // RegisterFlags registers options for this munger; returns any that require a restart when changed.
    68  func (fjr *FlakyJobReporter) RegisterFlags() {
    69  	flag.StringVar(&fjr.flakyJobDataURL, "flakyjob-url", "https://storage.googleapis.com/k8s-metrics/flakes-latest.json", "The url where flaky job JSON data can be found.")
    70  	flag.IntVar(&fjr.syncCount, "flakyjob-count", 3, "The number of flaky jobs to try to sync to github.")
    71  }
    72  
    73  // Issues is the main work method of FlakyJobReporter. It fetches and parses flaky job data,
    74  // then syncs the top issues to github with the IssueCreator.
    75  func (fjr *FlakyJobReporter) Issues(c *creator.IssueCreator) ([]creator.Issue, error) {
    76  	fjr.creator = c
    77  	json, err := mungerutil.ReadHTTP(fjr.flakyJobDataURL)
    78  	if err != nil {
    79  		return nil, err
    80  	}
    81  
    82  	flakyJobs, err := fjr.parseFlakyJobs(json)
    83  	if err != nil {
    84  		return nil, err
    85  	}
    86  
    87  	count := fjr.syncCount
    88  	if len(flakyJobs) < count {
    89  		count = len(flakyJobs)
    90  	}
    91  	issues := make([]creator.Issue, 0, count)
    92  	for _, fj := range flakyJobs[0:count] {
    93  		issues = append(issues, fj)
    94  	}
    95  
    96  	return issues, nil
    97  }
    98  
    99  // parseFlakyJobs parses JSON generated by the 'flakes' bigquery metric into a sorted slice of
   100  // *FlakyJob.
   101  func (fjr *FlakyJobReporter) parseFlakyJobs(jsonIn []byte) ([]*FlakyJob, error) {
   102  	var flakeMap map[string]*FlakyJob
   103  	err := json.Unmarshal(jsonIn, &flakeMap)
   104  	if err != nil || flakeMap == nil {
   105  		return nil, fmt.Errorf("error unmarshaling flaky jobs json: %v", err)
   106  	}
   107  	flakyJobs := make([]*FlakyJob, 0, len(flakeMap))
   108  
   109  	for job, fj := range flakeMap {
   110  		if job == "" {
   111  			glog.Errorf("Flaky jobs json contained a job with an empty jobname.\n")
   112  			continue
   113  		}
   114  		if fj == nil {
   115  			glog.Errorf("Flaky jobs json has invalid data for job '%s'.\n", job)
   116  			continue
   117  		}
   118  		if fj.Consistency == nil {
   119  			glog.Errorf("Flaky jobs json has no 'consistency' field for job '%s'.\n", job)
   120  			continue
   121  		}
   122  		if fj.FlakeCount == nil {
   123  			glog.Errorf("Flaky jobs json has no 'flakes' field for job '%s'.\n", job)
   124  			continue
   125  		}
   126  		if fj.FlakyTests == nil {
   127  			glog.Errorf("Flaky jobs json has no 'flakiest' field for job '%s'.\n", job)
   128  			continue
   129  		}
   130  		fj.Name = job
   131  		fj.reporter = fjr
   132  		flakyJobs = append(flakyJobs, fj)
   133  	}
   134  
   135  	sort.SliceStable(flakyJobs, func(i, j int) bool {
   136  		if *flakyJobs[i].FlakeCount == *flakyJobs[j].FlakeCount {
   137  			return *flakyJobs[i].Consistency < *flakyJobs[j].Consistency
   138  		}
   139  		return *flakyJobs[i].FlakeCount > *flakyJobs[j].FlakeCount
   140  	})
   141  
   142  	return flakyJobs, nil
   143  }
   144  
   145  // TestsSorted returns a slice of the testnames from a FlakyJob's FlakyTests map. The slice is
   146  // sorted by descending number of failures for the tests.
   147  func (fj *FlakyJob) TestsSorted() []string {
   148  	if fj.testsSorted != nil {
   149  		return fj.testsSorted
   150  	}
   151  	fj.testsSorted = make([]string, len(fj.FlakyTests))
   152  	i := 0
   153  	for test := range fj.FlakyTests {
   154  		fj.testsSorted[i] = test
   155  		i++
   156  	}
   157  	sort.SliceStable(fj.testsSorted, func(i, j int) bool {
   158  		return fj.FlakyTests[fj.testsSorted[i]] > fj.FlakyTests[fj.testsSorted[j]]
   159  	})
   160  	return fj.testsSorted
   161  }
   162  
   163  // Title yields the initial title text of the github issue.
   164  func (fj *FlakyJob) Title() string {
   165  	return fmt.Sprintf("%s flaked %d times in the past week", fj.Name, *fj.FlakeCount)
   166  }
   167  
   168  // ID yields the string identifier that uniquely identifies this issue.
   169  // This ID must appear in the body of the issue.
   170  // DO NOT CHANGE how this ID is formatted or duplicate issues may be created on github.
   171  func (fj *FlakyJob) ID() string {
   172  	return fmt.Sprintf("Flaky Job: %s", fj.Name)
   173  }
   174  
   175  // Body returns the body text of the github issue and *must* contain the output of ID().
   176  // closedIssues is a (potentially empty) slice containing all closed issues authored by this bot
   177  // that contain ID() in their body.
   178  // If Body returns an empty string no issue is created.
   179  func (fj *FlakyJob) Body(closedIssues []*githubapi.Issue) string {
   180  	// First check that the most recently closed issue (if any exist) was closed
   181  	// at least a week ago (since that is the sliding window size used by the flake metric).
   182  	cutoffTime := time.Now().AddDate(0, 0, -7)
   183  	for _, closed := range closedIssues {
   184  		if closed.ClosedAt.After(cutoffTime) {
   185  			return ""
   186  		}
   187  	}
   188  
   189  	// Print stats about the flaky job.
   190  	var buf bytes.Buffer
   191  	fmt.Fprintf(&buf, "### %s\n Flakes in the past week: **%d**\n Consistency: **%.2f%%**\n",
   192  		fj.ID(), *fj.FlakeCount, *fj.Consistency*100)
   193  	if len(fj.FlakyTests) > 0 {
   194  		fmt.Fprint(&buf, "\n#### Flakiest tests by flake count:\n| Test | Flake Count |\n| --- | --- |\n")
   195  		for _, testName := range fj.TestsSorted() {
   196  			fmt.Fprintf(&buf, "| %s | %d |\n", testName, fj.FlakyTests[testName])
   197  		}
   198  	}
   199  	// List previously closed issues if there are any.
   200  	if len(closedIssues) > 0 {
   201  		fmt.Fprint(&buf, "\n#### Previously closed issues for this job flaking:\n")
   202  		for _, closed := range closedIssues {
   203  			fmt.Fprintf(&buf, "#%d ", *closed.Number)
   204  		}
   205  		fmt.Fprint(&buf, "\n")
   206  	}
   207  
   208  	// Create /assign command.
   209  	testsSorted := fj.TestsSorted()
   210  	ownersMap := fj.reporter.creator.TestsOwners(testsSorted)
   211  	if len(ownersMap) > 0 {
   212  		fmt.Fprint(&buf, "\n/assign")
   213  		for user := range ownersMap {
   214  			fmt.Fprintf(&buf, " @%s", user)
   215  		}
   216  		fmt.Fprint(&buf, "\n")
   217  	}
   218  
   219  	// Explain why assignees were assigned and why sig labels were applied.
   220  	fmt.Fprintf(&buf, "\n%s", fj.reporter.creator.ExplainTestAssignments(testsSorted))
   221  
   222  	fmt.Fprintf(&buf, "\n[Flakiest Jobs](%s)\n", fj.reporter.flakyJobDataURL)
   223  	return buf.String()
   224  }
   225  
   226  // Labels returns the labels to apply to the issue created for this flaky job on github.
   227  func (fj *FlakyJob) Labels() []string {
   228  	labels := []string{"kind/flake"}
   229  	// get sig labels
   230  	for sig := range fj.reporter.creator.TestsSIGs(fj.TestsSorted()) {
   231  		labels = append(labels, "sig/"+sig)
   232  	}
   233  	return labels
   234  }
   235  
   236  // Owners returns the list of usernames to assign to this issue on github.
   237  func (fj *FlakyJob) Owners() []string {
   238  	// Assign owners by including a /assign command in the body instead of using Owners to set
   239  	// assignees on the issue request. This lets prow do the assignee validation and will mention
   240  	// the user we want to assign even if they can't be assigned.
   241  	return nil
   242  }
   243  
   244  // Priority calculates and returns the priority of this issue
   245  // The returned bool indicates if the returned priority is valid and can be used
   246  func (fj *FlakyJob) Priority() (string, bool) {
   247  	// TODO: implement priority calculations later
   248  	return "", false
   249  }