github.com/abayer/test-infra@v0.0.5/robots/issue-creator/sources/triage-filer_test.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 "strconv" 22 "strings" 23 "testing" 24 "time" 25 26 "github.com/google/go-github/github" 27 "k8s.io/test-infra/robots/issue-creator/creator" 28 "k8s.io/test-infra/robots/issue-creator/testowner" 29 ) 30 31 var ( 32 // json1issue2job2test is a small example of the JSON format that loadClusters reads. 33 // It includes all of the different types of formatting that is accepted. Namely both types 34 // of buildnum to row index mappings. 35 json1issue2job2test []byte 36 // buildTimes is a map containing the build times of builds found in the json1issue2job2test JSON data. 37 buildTimes map[int]int64 38 // sampleOwnerCSV is a small sample test owners csv file that contains both real test owner 39 // data and owner/SIG info for a fake test in json1issue2job2test. 40 sampleOwnerCSV []byte 41 // latestBuildTime is the end time of the sliding window for these tests. 42 latestBuildTime int64 43 ) 44 45 func init() { 46 latestBuildTime = int64(947462400) // Jan 10, 2000 47 hourSecs := int64(60 * 60) 48 dailySecs := hourSecs * 24 49 buildTimes = map[int]int64{ 50 41: latestBuildTime - (dailySecs * 10), // before window start 51 42: latestBuildTime + hourSecs - (dailySecs * 5), // just inside window start 52 43: latestBuildTime + hourSecs - (dailySecs * 4), 53 52: latestBuildTime + hourSecs - (dailySecs * 2), 54 142: latestBuildTime - dailySecs, // a day before window end 55 144: latestBuildTime - hourSecs, // an hour before window end 56 } 57 58 json1issue2job2test = []byte( 59 `{ 60 "builds": 61 { 62 "cols": 63 { 64 "started": 65 [ 66 ` + strconv.FormatInt(buildTimes[41], 10) + `, 67 ` + strconv.FormatInt(buildTimes[42], 10) + `, 68 ` + strconv.FormatInt(buildTimes[43], 10) + `, 69 10000000, 70 10000000, 71 10000000, 72 10000000, 73 10000000, 74 10000000, 75 10000000, 76 10000000, 77 ` + strconv.FormatInt(buildTimes[52], 10) + `, 78 ` + strconv.FormatInt(buildTimes[142], 10) + `, 79 10000000, 80 ` + strconv.FormatInt(buildTimes[144], 10) + ` 81 ] 82 }, 83 "jobs": 84 { 85 "jobname1": [41, 12, 0], 86 "jobname2": {"142": 12, "144": 14}, 87 "pr:jobname3": {"200": 13} 88 }, 89 "job_paths": 90 { 91 "jobname1": "path//to/jobname1", 92 "jobname2": "path//to/jobname2", 93 "pr:jobname3": "path//to/pr:jobname3" 94 } 95 }, 96 "clustered": 97 [ 98 { 99 "id": "key_hash", 100 "key": "key_text", 101 "tests": 102 [ 103 { 104 "jobs": 105 [ 106 { 107 "builds": [42, 43, 52], 108 "name": "jobname1" 109 }, 110 { 111 "builds": [144], 112 "name": "jobname2" 113 } 114 ], 115 "name": "testname1" 116 }, 117 { 118 "jobs": 119 [ 120 { 121 "builds": [41, 42, 43], 122 "name": "jobname1" 123 }, 124 { 125 "builds": [200], 126 "name": "pr:jobname3" 127 } 128 ], 129 "name": "testname2" 130 } 131 ], 132 "text": "issue_name" 133 } 134 ] 135 }`) 136 137 sampleOwnerCSV = []byte( 138 `name,owner,auto-assigned,sig 139 Sysctls should support sysctls,Random-Liu,1,node 140 Sysctls should support unsafe sysctls which are actually whitelisted,deads2k,1,node 141 testname1,cjwagner ,1,sigarea 142 testname2,spxtr,1,sigarea 143 ThirdParty resources Simple Third Party creating/deleting thirdparty objects works,luxas,1,api-machinery 144 Upgrade cluster upgrade should maintain a functioning cluster,luxas,1,cluster-lifecycle 145 Upgrade master upgrade should maintain a functioning cluster,xiang90,1,cluster-lifecycle 146 Upgrade node upgrade should maintain a functioning cluster,zmerlynn,1,cluster-lifecycle 147 Variable Expansion should allow composing env vars into new env vars,derekwaynecarr,0,node 148 Variable Expansion should allow substituting values in a container's args,dchen1107,1,node 149 Variable Expansion should allow substituting values in a container's command,mml,1,node 150 Volume Disk Format verify disk format type - eagerzeroedthick is honored for dynamically provisioned pv using storageclass,piosz,1,`) 151 } 152 153 // NewTestTriageFiler creates a new TriageFiler that isn't connected to an IssueCreator so that 154 // it can be used for testing. 155 func NewTestTriageFiler() *TriageFiler { 156 return &TriageFiler{ 157 creator: &creator.IssueCreator{}, 158 topClustersCount: 3, 159 windowDays: 5, 160 } 161 } 162 163 func TestTFParserSimple(t *testing.T) { 164 f := NewTestTriageFiler() 165 issues, err := f.loadClusters(json1issue2job2test) 166 if err != nil { 167 t.Fatalf("Error parsing triage data: %v\n", err) 168 } 169 170 if len(issues) != 1 { 171 t.Error("Expected 1 issue, got ", len(issues)) 172 } 173 if issues[0].Text != "issue_name" { 174 t.Error("Expected Text='issue_name', got ", issues[0].Text) 175 } 176 if issues[0].Identifier != "key_hash" { 177 t.Error("Expected Identifier='key_hash', got ", issues[0].Identifier) 178 } 179 // Note that 5 builds failed in json, but one is outside the time window. 180 if issues[0].totalBuilds != 4 { 181 t.Error("Expected totalBuilds failed = 4, got ", issues[0].totalBuilds) 182 } 183 // Note that 3 jobs failed in json, but one is a PR job and should be ignored. 184 if issues[0].totalJobs != 2 || len(issues[0].jobs) != 2 { 185 t.Error("Expected totalJobs failed = 2, got ", issues[0].totalJobs) 186 } 187 if issues[0].totalTests != 2 || len(issues[0].Tests) != 2 { 188 t.Error("Expected totalTests failed = 2, got ", issues[0].totalTests) 189 } 190 if f.data.Builds.JobPaths["jobname1"] != "path//to/jobname1" || 191 f.data.Builds.JobPaths["jobname2"] != "path//to/jobname2" { 192 t.Error("Invalid jobpath. got jobname1: ", f.data.Builds.JobPaths["jobname1"], 193 " and jobname2: ", f.data.Builds.JobPaths["jobname2"]) 194 } 195 196 checkBuildStart(t, f, "jobname1", 42, buildTimes[42]) 197 checkBuildStart(t, f, "jobname1", 52, buildTimes[52]) 198 checkBuildStart(t, f, "jobname2", 144, buildTimes[144]) 199 200 checkCluster(issues[0], t) 201 } 202 203 func checkBuildStart(t *testing.T, f *TriageFiler, jobName string, build int, expected int64) { 204 row, err := f.data.Builds.Jobs[jobName].rowForBuild(build) 205 if err != nil { 206 t.Errorf("Failed to look up row index for %s:%d", jobName, build) 207 } 208 actual := f.data.Builds.Cols.Started[row] 209 if actual != expected { 210 t.Errorf("Expected build start time for build %s:%d to be %d, got %d.", jobName, build, expected, actual) 211 } 212 } 213 214 // checkCluster checks that the properties that should be true for all clusters hold for this cluster 215 func checkCluster(clust *Cluster, t *testing.T) { 216 if !checkTopFailingsSorted(clust) { 217 t.Errorf("Top tests or jobs is improperly sorted for cluster: %s\n", clust.Identifier) 218 } 219 if clust.totalJobs != len(clust.jobs) { 220 t.Errorf("Total job count is invalid for cluster: %s\n", clust.Identifier) 221 } 222 if clust.totalTests != len(clust.Tests) { 223 t.Errorf("Total test count is invalid for cluster: %s\n", clust.Identifier) 224 } 225 title := clust.Title() 226 body := clust.Body(nil) 227 id := clust.ID() 228 if len(title) <= 0 { 229 t.Errorf("Title of cluster: %s is empty!", clust.Identifier) 230 } 231 if len(body) <= 0 { 232 t.Errorf("Body of cluster: %s is empty!", clust.Identifier) 233 } 234 if len(id) <= 0 { 235 t.Errorf("ID of cluster: %s is empty!", clust.Identifier) 236 } 237 if !strings.Contains(body, id) { 238 t.Errorf("The body text for cluster: %s does not contain its ID!\n", clust.Identifier) 239 } 240 //ensure that 'kind/flake' is among the label set 241 found := false 242 for _, label := range clust.Labels() { 243 if label == "kind/flake" { 244 found = true 245 } else { 246 if label == "" { 247 t.Errorf("Cluster: %s has an empty label!\n", clust.Identifier) 248 } 249 } 250 } 251 if !found { 252 t.Errorf("The cluster: %s does not have the label 'kind/flake'!", clust.Identifier) 253 } 254 } 255 256 func TestTFOwnersAndSIGs(t *testing.T) { 257 // Integration test for triage-filers use of issue-creator's TestsOwners, TestsSIGs, and 258 // ExplainTestAssignments. These functions in turn rely on OwnerList. 259 f := NewTestTriageFiler() 260 var err error 261 f.creator.Collaborators = []string{"cjwagner", "spxtr"} 262 f.creator.Owners, err = testowner.NewOwnerListFromCsv(bytes.NewReader(sampleOwnerCSV)) 263 f.creator.MaxSIGCount = 3 264 f.creator.MaxAssignees = 3 265 if err != nil { 266 t.Fatalf("Failed to create a new OwnersList. errmsg: %v", err) 267 } 268 269 // Check that the usernames and sig areas are as expected (no stay commas or anything like that). 270 clusters, err := f.loadClusters(json1issue2job2test) 271 if err != nil { 272 t.Fatalf("Failed to load clusters: %v", err) 273 } 274 foundSIG := false 275 for _, label := range clusters[0].Labels() { 276 if label == "sig/sigarea" { 277 foundSIG = true 278 break 279 } 280 } 281 if !foundSIG { 282 t.Errorf("Failed to get the SIG for cluster: %s\n", clusters[0].Identifier) 283 } 284 285 // Check that the body contains a table that correctly explains why users and sig areas were assigned. 286 body := clusters[0].Body(nil) 287 if !strings.Contains(body, "| cjwagner | testname1 |") { 288 t.Errorf("Body should contain a table row to explain that 'cjwagner' was assigned due to ownership of 'testname1'.") 289 } 290 if !strings.Contains(body, "| spxtr | testname2 |") { 291 t.Errorf("Body should contain a table row to explain that 'spxtr' was assigned due to ownership of 'testname2'.") 292 } 293 if !strings.Contains(body, "| sig/sigarea | testname1; testname2 |") { 294 t.Errorf("Body should contain a table row to explain that 'sigarea' was set as a SIG due to ownership of 'testname1' and 'testname2'.") 295 } 296 297 // Check that the body contains the assignments themselves: 298 if !strings.Contains(body, "/assign @cjwagner @spxtr") && !strings.Contains(body, "/assign @spxtr @cjwagner") { 299 t.Errorf("Failed to find the '/assign' command in the body of cluster: %s\n%q\n", clusters[0].Identifier, body) 300 } 301 } 302 303 // TestTFPrevCloseInWindow checks that Cluster issues will abort issue creation by returning an empty 304 // body if there is a recently closed issue for the cluster. 305 func TestTFPrevCloseInWindow(t *testing.T) { 306 f := NewTestTriageFiler() 307 clusters, err := f.loadClusters(json1issue2job2test) 308 if err != nil || len(clusters) == 0 { 309 t.Fatalf("Error parsing triage data: %v\n", err) 310 } 311 clust := clusters[0] 312 313 lastWeek := time.Unix(latestBuildTime, 0).AddDate(0, 0, -7) 314 yesterday := time.Unix(latestBuildTime, 0).AddDate(0, 0, -1) 315 five := 5 316 // Only need to populate the Issue.ClosedAt and Issue.Number field of the MungeObject. 317 prevIssues := []*github.Issue{{ClosedAt: &yesterday, Number: &five}} 318 if clust.Body(prevIssues) != "" { 319 t.Errorf("Cluster returned an issue body when there was a recently closed issue for the cluster.") 320 } 321 322 prevIssues = []*github.Issue{{ClosedAt: &lastWeek, Number: &five}} 323 if clust.Body(prevIssues) == "" { 324 t.Errorf("Cluster returned an empty issue body when it should have returned a valid body.") 325 } 326 } 327 328 func checkTopFailingsSorted(issue *Cluster) bool { 329 return checkTopJobsFailedSorted(issue) && checkTopTestsFailedSorted(issue) 330 } 331 332 func checkTopJobsFailedSorted(issue *Cluster) bool { 333 topJobs := issue.topJobsFailed(len(issue.jobs)) 334 for i := 1; i < len(topJobs); i++ { 335 if len(topJobs[i-1].Builds) < len(topJobs[i].Builds) { 336 return false 337 } 338 } 339 return true 340 } 341 342 func checkTopTestsFailedSorted(issue *Cluster) bool { 343 topTests := issue.topTestsFailed(len(issue.Tests)) 344 for i := 1; i < len(topTests); i++ { 345 if len(topTests[i-1].Jobs) < len(topTests[i].Jobs) { 346 return false 347 } 348 } 349 return true 350 }