github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/cmd/external-plugins/needs-rebase/plugin/plugin_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 plugin 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 "os" 24 "reflect" 25 "sort" 26 "testing" 27 "time" 28 29 "github.com/google/go-cmp/cmp" 30 "github.com/google/go-cmp/cmp/cmpopts" 31 githubql "github.com/shurcooL/githubv4" 32 "github.com/sirupsen/logrus" 33 "sigs.k8s.io/prow/pkg/github" 34 "sigs.k8s.io/prow/pkg/labels" 35 "sigs.k8s.io/prow/pkg/plugins" 36 ) 37 38 func testKey(org, repo string, num int) string { 39 return fmt.Sprintf("%s/%s#%d", org, repo, num) 40 } 41 42 type fghc struct { 43 allPRs []struct { 44 PullRequest pullRequest `graphql:"... on PullRequest"` 45 } 46 pr *github.PullRequest 47 48 initialLabels []github.Label 49 mergeable bool 50 51 // The following are maps are keyed using 'testKey' 52 commentCreated, commentDeleted map[string]bool 53 IssueLabelsAdded, IssueLabelsRemoved map[string][]string 54 } 55 56 func newFakeClient(prs []pullRequest, initialLabels []string, mergeable bool, pr *github.PullRequest) *fghc { 57 f := &fghc{ 58 mergeable: mergeable, 59 commentCreated: make(map[string]bool), 60 commentDeleted: make(map[string]bool), 61 IssueLabelsAdded: make(map[string][]string), 62 IssueLabelsRemoved: make(map[string][]string), 63 pr: pr, 64 } 65 for _, pr := range prs { 66 s := struct { 67 PullRequest pullRequest `graphql:"... on PullRequest"` 68 }{pr} 69 f.allPRs = append(f.allPRs, s) 70 } 71 for _, label := range initialLabels { 72 f.initialLabels = append(f.initialLabels, github.Label{Name: label}) 73 } 74 return f 75 } 76 77 func (f *fghc) GetIssueLabels(org, repo string, number int) ([]github.Label, error) { 78 return f.initialLabels, nil 79 } 80 81 func (f *fghc) CreateCommentWithContext(_ context.Context, org, repo string, number int, comment string) error { 82 f.commentCreated[testKey(org, repo, number)] = true 83 return nil 84 } 85 86 func (f *fghc) BotUserChecker() (func(candidate string) bool, error) { 87 return func(candidate string) bool { return candidate == "k8s-ci-robot" }, nil 88 } 89 90 func (f *fghc) AddLabelWithContext(_ context.Context, org, repo string, number int, label string) error { 91 key := testKey(org, repo, number) 92 f.IssueLabelsAdded[key] = append(f.IssueLabelsAdded[key], label) 93 return nil 94 } 95 96 func (f *fghc) RemoveLabelWithContext(_ context.Context, org, repo string, number int, label string) error { 97 key := testKey(org, repo, number) 98 f.IssueLabelsRemoved[key] = append(f.IssueLabelsRemoved[key], label) 99 return nil 100 } 101 102 func (f *fghc) IsMergeable(org, repo string, number int, sha string) (bool, error) { 103 return f.mergeable, nil 104 } 105 106 func (f *fghc) DeleteStaleCommentsWithContext(_ context.Context, org, repo string, number int, comments []github.IssueComment, isStale func(github.IssueComment) bool) error { 107 f.commentDeleted[testKey(org, repo, number)] = true 108 return nil 109 } 110 111 func (f *fghc) QueryWithGitHubAppsSupport(_ context.Context, q interface{}, _ map[string]interface{}, _ string) error { 112 query, ok := q.(*searchQuery) 113 if !ok { 114 return errors.New("invalid query format") 115 } 116 query.Search.Nodes = f.allPRs 117 return nil 118 } 119 120 func (f *fghc) GetPullRequest(org, repo string, number int) (*github.PullRequest, error) { 121 if f.pr != nil { 122 return f.pr, nil 123 } 124 return nil, fmt.Errorf("didn't find pull request %s/%s#%d", org, repo, number) 125 } 126 127 func (f *fghc) compareExpected(t *testing.T, org, repo string, num int, expectedAdded []string, expectedRemoved []string, expectComment bool, expectDeletion bool) { 128 key := testKey(org, repo, num) 129 sort.Strings(expectedAdded) 130 sort.Strings(expectedRemoved) 131 sort.Strings(f.IssueLabelsAdded[key]) 132 sort.Strings(f.IssueLabelsRemoved[key]) 133 if !reflect.DeepEqual(expectedAdded, f.IssueLabelsAdded[key]) { 134 t.Errorf("Expected the following labels to be added to %s: %q, but got %q.", key, expectedAdded, f.IssueLabelsAdded[key]) 135 } 136 if !reflect.DeepEqual(expectedRemoved, f.IssueLabelsRemoved[key]) { 137 t.Errorf("Expected the following labels to be removed from %s: %q, but got %q.", key, expectedRemoved, f.IssueLabelsRemoved[key]) 138 } 139 if expectComment && !f.commentCreated[key] { 140 t.Errorf("Expected a comment to be created on %s, but none was.", key) 141 } else if !expectComment && f.commentCreated[key] { 142 t.Errorf("Unexpected comment on %s.", key) 143 } 144 if expectDeletion && !f.commentDeleted[key] { 145 t.Errorf("Expected a comment to be deleted from %s, but none was.", key) 146 } else if !expectDeletion && f.commentDeleted[key] { 147 t.Errorf("Unexpected comment deletion on %s.", key) 148 } 149 } 150 151 func TestMain(m *testing.M) { 152 sleep = func(time.Duration) {} 153 code := m.Run() 154 os.Exit(code) 155 } 156 157 func TestHandleIssueCommentEvent(t *testing.T) { 158 t.Parallel() 159 pr := func() *github.PullRequest { 160 pr := github.PullRequest{ 161 Base: github.PullRequestBranch{ 162 Repo: github.Repo{ 163 Name: "repo", 164 Owner: github.User{Login: "org"}, 165 }, 166 }, 167 Number: 5, 168 } 169 return &pr 170 } 171 172 testCases := []struct { 173 name string 174 pr *github.PullRequest 175 176 mergeable bool 177 labels []string 178 state string 179 merged bool 180 181 expectedAdded []string 182 expectedRemoved []string 183 expectComment bool 184 expectDeletion bool 185 }{ 186 { 187 name: "No pull request, ignoring", 188 }, 189 { 190 name: "mergeable no-op", 191 pr: pr(), 192 mergeable: true, 193 labels: []string{labels.LGTM, labels.Approved}, 194 state: github.PullRequestStateOpen, 195 }, 196 { 197 name: "unmergeable no-op", 198 pr: pr(), 199 mergeable: false, 200 labels: []string{labels.LGTM, labels.Approved, labels.NeedsRebase}, 201 state: github.PullRequestStateOpen, 202 }, 203 { 204 name: "mergeable -> unmergeable", 205 pr: pr(), 206 mergeable: false, 207 labels: []string{labels.LGTM, labels.Approved}, 208 state: github.PullRequestStateOpen, 209 210 expectedAdded: []string{labels.NeedsRebase}, 211 expectComment: true, 212 }, 213 { 214 name: "unmergeable -> mergeable", 215 pr: pr(), 216 mergeable: true, 217 labels: []string{labels.LGTM, labels.Approved, labels.NeedsRebase}, 218 state: github.PullRequestStateOpen, 219 220 expectedRemoved: []string{labels.NeedsRebase}, 221 expectDeletion: true, 222 }, 223 { 224 name: "merged pr is ignored", 225 pr: pr(), 226 mergeable: false, 227 state: github.PullRequestStateClosed, 228 merged: true, 229 }, 230 { 231 name: "closed pr is ignored", 232 pr: pr(), 233 mergeable: false, 234 state: github.PullRequestStateClosed, 235 }, 236 } 237 238 for _, tc := range testCases { 239 t.Run(tc.name, func(t *testing.T) { 240 fake := newFakeClient(nil, tc.labels, tc.mergeable, tc.pr) 241 ice := &github.IssueCommentEvent{} 242 if tc.pr != nil { 243 ice.Issue.PullRequest = &struct{}{} 244 tc.pr.Merged = tc.merged 245 tc.pr.State = tc.state 246 } 247 cache := NewCache(0) 248 if err := HandleIssueCommentEvent(logrus.WithField("plugin", PluginName), fake, ice, cache); err != nil { 249 t.Fatalf("error handling issue comment event: %v", err) 250 } 251 fake.compareExpected(t, "org", "repo", 5, tc.expectedAdded, tc.expectedRemoved, tc.expectComment, tc.expectDeletion) 252 }) 253 } 254 } 255 256 func TestHandlePullRequestEvent(t *testing.T) { 257 t.Parallel() 258 259 testCases := []struct { 260 name string 261 262 mergeable bool 263 labels []string 264 state string 265 merged bool 266 267 expectedAdded []string 268 expectedRemoved []string 269 expectComment bool 270 expectDeletion bool 271 }{ 272 { 273 name: "mergeable no-op", 274 mergeable: true, 275 labels: []string{labels.LGTM, labels.Approved}, 276 state: github.PullRequestStateOpen, 277 }, 278 { 279 name: "unmergeable no-op", 280 mergeable: false, 281 labels: []string{labels.LGTM, labels.Approved, labels.NeedsRebase}, 282 state: github.PullRequestStateOpen, 283 }, 284 { 285 name: "mergeable -> unmergeable", 286 mergeable: false, 287 labels: []string{labels.LGTM, labels.Approved}, 288 state: github.PullRequestStateOpen, 289 290 expectedAdded: []string{labels.NeedsRebase}, 291 expectComment: true, 292 }, 293 { 294 name: "unmergeable -> mergeable", 295 mergeable: true, 296 labels: []string{labels.LGTM, labels.Approved, labels.NeedsRebase}, 297 state: github.PullRequestStateOpen, 298 299 expectedRemoved: []string{labels.NeedsRebase}, 300 expectDeletion: true, 301 }, 302 { 303 name: "merged pr is ignored", 304 merged: true, 305 state: github.PullRequestStateClosed, 306 }, 307 { 308 name: "closed pr is ignored", 309 state: github.PullRequestStateClosed, 310 }, 311 } 312 313 for _, tc := range testCases { 314 t.Run(tc.name, func(t *testing.T) { 315 fake := newFakeClient(nil, tc.labels, tc.mergeable, nil) 316 pre := &github.PullRequestEvent{ 317 Action: github.PullRequestActionSynchronize, 318 PullRequest: github.PullRequest{ 319 Base: github.PullRequestBranch{ 320 Repo: github.Repo{ 321 Name: "repo", 322 Owner: github.User{Login: "org"}, 323 }, 324 }, 325 Merged: tc.merged, 326 State: tc.state, 327 Number: 5, 328 }, 329 } 330 t.Logf("Running test scenario: %q", tc.name) 331 if err := HandlePullRequestEvent(logrus.WithField("plugin", PluginName), fake, pre); err != nil { 332 t.Fatalf("Unexpected error handling event: %v.", err) 333 } 334 fake.compareExpected(t, "org", "repo", 5, tc.expectedAdded, tc.expectedRemoved, tc.expectComment, tc.expectDeletion) 335 }) 336 } 337 } 338 339 func TestHandleAll(t *testing.T) { 340 t.Parallel() 341 testPRs := []struct { 342 name string 343 344 labels []string 345 mergeable bool 346 state githubql.PullRequestState 347 348 expectedAdded, expectedRemoved []string 349 expectComment, expectDeletion bool 350 }{ 351 { 352 name: "PR State Merged", 353 mergeable: false, 354 state: githubql.PullRequestStateMerged, 355 labels: []string{labels.LGTM, labels.Approved}, 356 }, 357 { 358 name: "PR State Closed", 359 mergeable: false, 360 state: githubql.PullRequestStateClosed, 361 labels: []string{labels.LGTM, labels.Approved}, 362 }, 363 { 364 name: "PR State Closed with need-rebase label", 365 mergeable: false, 366 state: githubql.PullRequestStateClosed, 367 labels: []string{labels.LGTM, labels.Approved, labels.NeedsRebase}, 368 }, 369 { 370 name: "PR State Open with non-mergeable", 371 mergeable: false, 372 state: githubql.PullRequestStateOpen, 373 labels: []string{labels.LGTM, labels.Approved}, 374 375 expectedAdded: []string{labels.NeedsRebase}, 376 expectComment: true, 377 }, 378 { 379 name: "PR State Open with mergeable", 380 mergeable: true, 381 state: githubql.PullRequestStateOpen, 382 labels: []string{labels.LGTM, labels.Approved, labels.NeedsRebase}, 383 384 expectedRemoved: []string{labels.NeedsRebase}, 385 expectDeletion: true, 386 }, 387 } 388 389 prs := []pullRequest{} 390 for i, testPR := range testPRs { 391 pr := pullRequest{ 392 Number: githubql.Int(i), 393 State: testPR.state, 394 } 395 if testPR.mergeable { 396 pr.Mergeable = githubql.MergeableStateMergeable 397 } else { 398 pr.Mergeable = githubql.MergeableStateConflicting 399 } 400 for _, label := range testPR.labels { 401 s := struct { 402 Name githubql.String 403 }{ 404 Name: githubql.String(label), 405 } 406 pr.Labels.Nodes = append(pr.Labels.Nodes, s) 407 } 408 prs = append(prs, pr) 409 } 410 fake := newFakeClient(prs, nil, false, nil) 411 config := &plugins.Configuration{ 412 Plugins: plugins.Plugins{"/": {Plugins: []string{labels.LGTM, PluginName}}}, 413 414 ExternalPlugins: map[string][]plugins.ExternalPlugin{"/": {{Name: PluginName}}}, 415 } 416 issueCache := NewFakeCache(0) 417 418 if err := HandleAll(logrus.WithField("plugin", PluginName), fake, config, false, issueCache); err != nil { 419 t.Fatalf("Unexpected error handling all prs: %v.", err) 420 } 421 for i, pr := range testPRs { 422 t.Run(pr.name, func(t *testing.T) { 423 fake.compareExpected(t, "", "", i, pr.expectedAdded, pr.expectedRemoved, pr.expectComment, pr.expectDeletion) 424 }) 425 } 426 } 427 428 func TestConstructQueries(t *testing.T) { 429 t.Parallel() 430 testCases := []struct { 431 name string 432 orgs []string 433 repos []string 434 usesAppsAuth bool 435 436 expected map[string][]string 437 }{ 438 { 439 name: "Kubernetes and Kubernetes-Sigs org, no repos", 440 orgs: []string{"kubernetes", "kubernetes-sigs"}, 441 442 expected: map[string][]string{ 443 "kubernetes": { 444 `archived:false is:pr is:open org:"kubernetes" -repo:"kubernetes/kubernetes"`, 445 `archived:false is:pr is:open repo:"kubernetes/kubernetes" created:>=0000-11-02`, 446 `archived:false is:pr is:open repo:"kubernetes/kubernetes" created:<0000-11-02`, 447 }, 448 "kubernetes-sigs": {`archived:false is:pr is:open org:"kubernetes-sigs"`}, 449 }, 450 }, 451 { 452 name: "Kubernetes and Kubernetes-Sigs org, no repos, apps auth", 453 orgs: []string{"kubernetes", "kubernetes-sigs"}, 454 usesAppsAuth: true, 455 456 expected: map[string][]string{ 457 "kubernetes": { 458 `archived:false is:pr is:open org:"kubernetes" -repo:"kubernetes/kubernetes"`, 459 `archived:false is:pr is:open repo:"kubernetes/kubernetes" created:>=0000-11-02`, 460 `archived:false is:pr is:open repo:"kubernetes/kubernetes" created:<0000-11-02`, 461 }, 462 "kubernetes-sigs": {`archived:false is:pr is:open org:"kubernetes-sigs"`}, 463 }, 464 }, 465 { 466 name: "Other orgs, no repos", 467 orgs: []string{"other", "other-sigs"}, 468 469 expected: map[string][]string{ 470 "other": {`archived:false is:pr is:open org:"other"`}, 471 "other-sigs": {`archived:false is:pr is:open org:"other-sigs"`}, 472 }, 473 }, 474 { 475 name: "Other org, no repos, apps auth", 476 orgs: []string{"other", "other-sigs"}, 477 usesAppsAuth: true, 478 479 expected: map[string][]string{ 480 "other": {`archived:false is:pr is:open org:"other"`}, 481 "other-sigs": {`archived:false is:pr is:open org:"other-sigs"`}, 482 }, 483 }, 484 { 485 name: "Some repos, no orgs", 486 repos: []string{"org/repo", "other/repo"}, 487 488 expected: map[string][]string{"": {`archived:false is:pr is:open repo:"org/repo" repo:"other/repo"`}}, 489 }, 490 { 491 name: "Some repos, no orgs, apps auth", 492 repos: []string{"org/repo", "other/repo"}, 493 usesAppsAuth: true, 494 495 expected: map[string][]string{ 496 "org": {`archived:false is:pr is:open repo:"org/repo"`}, 497 "other": {`archived:false is:pr is:open repo:"other/repo"`}, 498 }, 499 }, 500 { 501 name: "Invalid repo is ignored", 502 repos: []string{"repo"}, 503 }, 504 { 505 name: "Org and repo in that org, repo is ignored", 506 orgs: []string{"org"}, 507 repos: []string{"org/repo"}, 508 509 expected: map[string][]string{"org": {`archived:false is:pr is:open org:"org"`}}, 510 }, 511 { 512 name: "Org and repo in that org, repo is ignored, apps auth", 513 orgs: []string{"org"}, 514 repos: []string{"org/repo"}, 515 516 expected: map[string][]string{"org": {`archived:false is:pr is:open org:"org"`}}, 517 }, 518 { 519 name: "Some orgs and some repos", 520 orgs: []string{"org", "other"}, 521 repos: []string{"repoorg/repo", "otherrepoorg/repo"}, 522 523 expected: map[string][]string{ 524 "": {`archived:false is:pr is:open repo:"repoorg/repo" repo:"otherrepoorg/repo"`}, 525 "org": {`archived:false is:pr is:open org:"org"`}, 526 "other": {`archived:false is:pr is:open org:"other"`}, 527 }, 528 }, 529 { 530 name: "Some orgs and some repos, apps auth", 531 orgs: []string{"org", "other"}, 532 repos: []string{"repoorg/repo", "otherrepoorg/repo"}, 533 usesAppsAuth: true, 534 535 expected: map[string][]string{ 536 "org": {`archived:false is:pr is:open org:"org"`}, 537 "other": {`archived:false is:pr is:open org:"other"`}, 538 "otherrepoorg": {`archived:false is:pr is:open repo:"otherrepoorg/repo"`}, 539 "repoorg": {`archived:false is:pr is:open repo:"repoorg/repo"`}, 540 }, 541 }, 542 { 543 name: "Multiple repos in the same org", 544 repos: []string{"org/a", "org/b"}, 545 546 expected: map[string][]string{"": {`archived:false is:pr is:open repo:"org/a" repo:"org/b"`}}, 547 }, 548 { 549 name: "Multiple repos in the same org, apps auth", 550 repos: []string{"org/a", "org/b"}, 551 usesAppsAuth: true, 552 553 expected: map[string][]string{"org": {`archived:false is:pr is:open repo:"org/a" repo:"org/b"`}}, 554 }, 555 } 556 557 for _, tc := range testCases { 558 t.Run(tc.name, func(t *testing.T) { 559 result := constructQueries(logrus.WithField("test", tc.name), time.Time{}, tc.orgs, tc.repos, tc.usesAppsAuth) 560 if diff := cmp.Diff(tc.expected, result, cmpopts.EquateEmpty()); diff != "" { 561 t.Errorf("expected result differs from actual: %s", diff) 562 } 563 }) 564 } 565 } 566 567 func NewFakeCache(validTime time.Duration) *Cache { 568 return &Cache{ 569 cache: make(map[int]time.Time), 570 validTime: time.Second * validTime, 571 currentTime: getFakeTime(), 572 } 573 } 574 575 func getFakeTime() timeNow { 576 var i = 0 577 now := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC) 578 return func() time.Time { 579 i = i + 1 580 return now.Add(time.Duration(i) * time.Second) 581 } 582 } 583 584 func TestCache(t *testing.T) { 585 t.Parallel() 586 testCases := []struct { 587 name string 588 validTime time.Duration 589 keys []int 590 591 expected []bool 592 }{ 593 { 594 name: "Test cache - disabled", 595 validTime: 0, 596 597 keys: []int{11, 22, 22, 11}, 598 expected: []bool{false, false, false, false}, 599 }, 600 { 601 name: "Test cache - all miss", 602 validTime: 100, 603 604 keys: []int{11, 22, 33, 44}, 605 expected: []bool{false, false, false, false}, 606 }, 607 { 608 name: "Test cache - one key hits, other missed", 609 validTime: 100, 610 611 keys: []int{11, 22, 33, 11}, 612 expected: []bool{false, false, false, true}, 613 }, 614 { 615 name: "Test cache - repeated requested same key", 616 validTime: 100, 617 618 keys: []int{11, 11, 11, 11}, 619 expected: []bool{false, true, true, true}, 620 }, 621 } 622 623 for _, tc := range testCases { 624 t.Run(tc.name, func(t *testing.T) { 625 fake := NewFakeCache(tc.validTime) 626 t.Logf("Running test scenario: %q", tc.name) 627 for idx, key := range tc.keys { 628 age := fake.Get(key) 629 if age != tc.expected[idx] { 630 t.Errorf("Unexpected cache age %t, expected %t.", age, tc.expected[idx]) 631 } 632 fake.Set(key) 633 } 634 }) 635 } 636 }