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 }