github.com/shashidharatd/test-infra@v0.0.0-20171006011030-71304e1ca560/robots/issue-creator/creator/creator_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 creator 18 19 import ( 20 "bytes" 21 "errors" 22 "fmt" 23 "reflect" 24 "sort" 25 "strings" 26 "testing" 27 28 "k8s.io/test-infra/robots/issue-creator/testowner" 29 30 "github.com/google/go-github/github" 31 ) 32 33 // fakeClient implements the RepoClient interface in order to be substituted for a 34 // ghclient.Client github client when creating an IssueCreator. 35 type fakeClient struct { 36 userName string 37 repoLabels []string 38 issues []*github.Issue 39 org string 40 project string 41 t *testing.T 42 } 43 44 func (c *fakeClient) GetUser(login string) (*github.User, error) { 45 if login == "" { 46 return &github.User{Login: &c.userName}, nil 47 } 48 return nil, fmt.Errorf("Fake Client is only able to retrieve the current authenticated user in its current state.") 49 } 50 51 func (c *fakeClient) GetRepoLabels(org, repo string) ([]*github.Label, error) { 52 return makeLabelSlice(c.repoLabels), nil 53 } 54 55 func (c *fakeClient) GetIssues(org, repo string, options *github.IssueListByRepoOptions) ([]*github.Issue, error) { 56 return c.issues, nil 57 } 58 59 func (c *fakeClient) CreateIssue(org, repo string, title, body string, labels, owners []string) (*github.Issue, error) { 60 // Check if labels are valid. 61 for _, label := range labels { 62 found := false 63 for _, validLabel := range c.repoLabels { 64 if validLabel == label { 65 found = true 66 break 67 } 68 } 69 if !found { 70 c.t.Errorf("%s is not a valid label!\n", label) 71 } 72 } 73 74 issue := makeTestIssue(title, body, "open", labels, owners, len(c.issues)) 75 76 c.issues = append(c.issues, issue) 77 return issue, nil 78 } 79 80 func (c *fakeClient) GetCollaborators(org, repo string) ([]*github.User, error) { 81 return nil, errors.New("some error (allow all assignees)") 82 } 83 84 // Verify checks that exactly 1 issue in c.issues matches the parameters and that no 85 // issues in c.issues have an empty body string (since that means they shouldn't have been created). 86 func (c *fakeClient) Verify(title, body string, owners, labels []string) bool { 87 matchCount := 0 88 for _, issue := range c.issues { 89 if *issue.Title != title || *issue.Body != body { 90 continue 91 } 92 // Verify that owners matches Assignees. 93 assignees := make([]string, len(issue.Assignees)) 94 for i := 0; i < len(issue.Assignees); i++ { 95 assignees[i] = *issue.Assignees[i].Login 96 } 97 if !stringSlicesEqual(assignees, owners) { 98 continue 99 } 100 // Verify that labels matches issue.Labels. 101 issueLabels := make([]string, len(issue.Labels)) 102 for i := 0; i < len(issue.Labels); i++ { 103 issueLabels[i] = *issue.Labels[i].Name 104 } 105 if !stringSlicesEqual(issueLabels, labels) { 106 continue 107 } 108 matchCount++ 109 } 110 return matchCount == 1 111 } 112 113 type fakeIssue struct { 114 title, body, id string 115 labels, owners []string 116 priority string // A value of "" indicates no priority is set. 117 } 118 119 func (i *fakeIssue) Title() string { 120 return i.title 121 } 122 123 func (i *fakeIssue) Body(closed []*github.Issue) string { 124 // the functionality to check that there are no recently closed issues on github for a cluster is 125 // part of the TriageFiler code and is tested in triage-filer_test.go 126 // we ignore the param here 127 return i.body 128 } 129 130 func (i *fakeIssue) ID() string { 131 return i.id 132 } 133 134 func (i *fakeIssue) Labels() []string { 135 return i.labels 136 } 137 138 func (i *fakeIssue) Owners() []string { 139 return i.owners 140 } 141 142 func (i *fakeIssue) Priority() (string, bool) { 143 if i.priority == "" { 144 return "", false 145 } 146 return i.priority, true 147 } 148 149 func TestIssueCreator(t *testing.T) { 150 151 i1 := &fakeIssue{ 152 title: "title1", 153 body: "body<ID1>", 154 id: "<ID1>", 155 labels: []string{"kind/flake"}, 156 owners: []string{}, 157 priority: "", 158 } 159 160 c := &fakeClient{ 161 t: t, 162 userName: "BOT_USERNAME", 163 org: "MY_ORG", 164 project: "MY_PROJ", 165 repoLabels: []string{"kind/flake", "kind/flakeypastry", "priority/P0"}, 166 issues: []*github.Issue{ 167 makeTestIssue(i1.title, i1.body, "open", i1.labels, i1.owners, 0), 168 }, 169 } 170 creator := &IssueCreator{ 171 client: c, 172 } 173 if err := creator.loadCache(); err != nil { 174 t.Fatalf("IssueCreator failed to load data from github while initing: %v", err) 175 } 176 177 // Test that an issue can be created normally. 178 i0 := &fakeIssue{ 179 title: "title0", 180 body: "body<ID0>moarbody", 181 id: "<ID0>", 182 labels: []string{"kind/flake"}, 183 owners: []string{"user0"}, 184 priority: "", 185 } 186 creator.sync(i0) 187 if !c.Verify(i0.title, i0.body, i0.owners, i0.labels) { 188 t.Errorf("Failed to do a simple sync of i0\n") 189 } 190 191 // Test that issues can't be double synced. 192 origLen := len(c.issues) 193 creator.sync(i1) 194 if len(c.issues) > origLen { 195 t.Errorf("Second sync of i1 created a duplicate issue!\n") 196 } 197 if !c.Verify(i1.title, i1.body, i1.owners, i1.labels) { 198 t.Errorf("Second sync of i1 was not idempotent.\n") 199 } 200 201 // Test that issues with empty bodies dont get synced. 202 i2 := &fakeIssue{ 203 title: "title2", 204 body: "", // Indicates issue should not be synced. 205 id: "<ID2>", 206 labels: []string{"kind/flake"}, 207 owners: []string{"user2"}, 208 priority: "", 209 } 210 origLen = len(c.issues) 211 creator.sync(i2) 212 if len(c.issues) > origLen { 213 t.Errorf("sync of i2 with empty body should not have created issue!\n") 214 } 215 216 // Test that invalid labels are not synced. 217 i3 := &fakeIssue{ 218 title: "title3", 219 body: "body\\@^*<ID3>\\moarbody", 220 id: "<ID3>", 221 labels: []string{"kind/flake", "label/wannabe"}, 222 owners: []string{"user3"}, 223 priority: "", 224 } 225 creator.sync(i3) 226 if !c.Verify(i3.title, i3.body, i3.owners, []string{"kind/flake"}) { 227 t.Errorf("sync of i3 was invalid. The label 'label/wannabe' should not be added to the new issue.\n") 228 } 229 230 // Test that DryRun prevents issue creation. 231 creator.dryRun = true 232 i4 := &fakeIssue{ 233 title: "title4", 234 body: "<ID4>thebody", 235 id: "<ID4>", 236 labels: []string{"kind/flake"}, 237 owners: []string{"user4"}, 238 priority: "", 239 } 240 origLen = len(c.issues) 241 creator.sync(i4) 242 if len(c.issues) > origLen { 243 t.Errorf("sync of i4 with DryRun on should not have created issue!\n") 244 } 245 246 creator.dryRun = false 247 248 // Test that priority labels are created properly if an issue knows its priority. 249 i5 := &fakeIssue{ 250 title: "title5", 251 body: "<ID5>thebody", 252 id: "<ID5>", 253 labels: []string{"kind/flake", "kind/flakeypastry"}, 254 owners: []string{"user5", "user1"}, // Test multiple users and labels here too. 255 priority: "P0", 256 } 257 creator.sync(i5) 258 if !c.Verify(i5.title, i5.body, i5.owners, []string{"kind/flake", "kind/flakeypastry", "priority/P0"}) { 259 t.Errorf("sync of i5 was invalid. The labels in the created issue were incorrect.\n") 260 } 261 } 262 263 func makeTestIssue(title, body, state string, labels, owners []string, number int) *github.Issue { 264 return &github.Issue{ 265 Title: &title, 266 Body: &body, 267 State: &state, 268 Number: &number, 269 Assignees: makeUserSlice(owners), 270 Labels: makeLabelSliceNoPtr(labels), 271 } 272 } 273 274 func makeLabelSlice(strs []string) []*github.Label { 275 result := make([]*github.Label, len(strs)) 276 for i := 0; i < len(strs); i++ { 277 result[i] = &github.Label{Name: &strs[i]} 278 } 279 return result 280 } 281 282 func makeLabelSliceNoPtr(strs []string) []github.Label { 283 result := make([]github.Label, len(strs)) 284 for i := 0; i < len(strs); i++ { 285 result[i] = github.Label{Name: &strs[i]} 286 } 287 return result 288 } 289 290 func makeUserSlice(strs []string) []*github.User { 291 result := make([]*github.User, len(strs)) 292 for i := 0; i < len(strs); i++ { 293 result[i] = &github.User{Login: &strs[i]} 294 } 295 return result 296 } 297 298 func stringSlicesEqual(strs1, strs2 []string) bool { 299 sort.Strings(strs1) 300 sort.Strings(strs2) 301 return reflect.DeepEqual(strs1, strs2) 302 } 303 304 func TestOwnersSIGs(t *testing.T) { 305 sampleOwnerCSV := []byte( 306 `name,owner,auto-assigned,sig 307 some test, cjwagner,0,sigarea2 308 some test2, cjwagner, 1, sigarea3 309 some test3, cjwagner, 0, sigarea4 310 Sysctls should support sysctls,Random-Liu,1,node 311 Sysctls should support unsafe sysctls which are actually whitelisted,deads2k,1,node 312 testname1,cjwagner ,1,sigarea 313 testname2,spxtr,1,sigarea 314 ThirdParty resources Simple Third Party creating/deleting thirdparty objects works,luxas,1,api-machinery 315 Upgrade cluster upgrade should maintain a functioning cluster,luxas,1,cluster-lifecycle 316 Upgrade master upgrade should maintain a functioning cluster,xiang90,1,cluster-lifecycle`) 317 318 ownerlist, err := testowner.NewOwnerListFromCsv(bytes.NewReader(sampleOwnerCSV)) 319 if err != nil { 320 t.Fatalf("Failed to init an OwnerList: %v\n", err) 321 } 322 c := &IssueCreator{ 323 Collaborators: []string{"cjwagner", "spxtr"}, 324 Owners: ownerlist, 325 MaxAssignees: 3, 326 MaxSIGCount: 3, 327 } 328 329 cases := []struct { 330 tests []string 331 owners, sigs map[string][]string 332 }{ 333 { 334 tests: []string{"testname1"}, 335 owners: map[string][]string{"cjwagner": {"testname1"}}, 336 sigs: map[string][]string{"sigarea": {"testname1"}}, 337 }, 338 { 339 tests: []string{"testname1", "testname2"}, 340 owners: map[string][]string{"cjwagner": {"testname1"}, "spxtr": {"testname2"}}, 341 sigs: map[string][]string{"sigarea": {"testname1", "testname2"}}, 342 }, 343 { 344 tests: []string{"testname1", "testname2", "some test"}, 345 owners: map[string][]string{"cjwagner": {"testname1", "some test"}, "spxtr": {"testname2"}}, 346 sigs: map[string][]string{"sigarea": {"testname1", "testname2"}, "sigarea2": {"some test"}}, 347 }, 348 { 349 tests: []string{"testname1", "some test", "some test2", "some_test3"}, 350 owners: map[string][]string{"cjwagner": {"testname1", "some test", "some test2"}}, 351 sigs: map[string][]string{"sigarea": {"testname1"}, "sigarea2": {"some test"}, "sigarea3": {"some test2"}}, 352 }, 353 } 354 for _, test := range cases { 355 owners := c.TestsOwners(test.tests) 356 sigs := c.TestsSIGs(test.tests) 357 if !reflect.DeepEqual(owners, test.owners) { 358 t.Errorf("Expected owners map was %v but got %v\n", test.owners, owners) 359 } 360 if !reflect.DeepEqual(sigs, test.sigs) { 361 t.Errorf("Expected sigs map was %v but got %v\n", test.sigs, sigs) 362 } 363 364 table := c.ExplainTestAssignments(test.tests) 365 for owner, testNames := range owners { 366 row := fmt.Sprintf("| %s | %s |", owner, strings.Join(testNames, "; ")) 367 if !strings.Contains(table, row) { 368 t.Errorf("Assignment explanation table is missing row: '%s'\n", row) 369 } 370 } 371 for sig, testNames := range sigs { 372 row := fmt.Sprintf("| sig/%s | %s |", sig, strings.Join(testNames, "; ")) 373 if !strings.Contains(table, row) { 374 t.Errorf("Assignment explanation table is missing row: '%s'\n", row) 375 } 376 } 377 } 378 }