github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/cmd/external-plugins/cherrypicker/server_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 main 18 19 import ( 20 "errors" 21 "fmt" 22 "reflect" 23 "sync" 24 "testing" 25 26 "github.com/google/go-cmp/cmp" 27 "github.com/sirupsen/logrus" 28 29 "sigs.k8s.io/prow/pkg/git/localgit" 30 v2 "sigs.k8s.io/prow/pkg/git/v2" 31 "sigs.k8s.io/prow/pkg/github" 32 ) 33 34 var ( 35 commentFormat = "%s/%s#%d %s" 36 fakePR prNumberGenerator 37 ) 38 39 type fghc struct { 40 sync.Mutex 41 pr *github.PullRequest 42 isMember bool 43 44 diff []byte 45 patch []byte 46 comments []string 47 prs []github.PullRequest 48 prComments []github.IssueComment 49 prLabels []github.Label 50 orgMembers []github.TeamMember 51 issues []github.Issue 52 } 53 54 func (f *fghc) AddLabel(org, repo string, number int, label string) error { 55 f.Lock() 56 defer f.Unlock() 57 for i := range f.prs { 58 if number == f.prs[i].Number { 59 f.prs[i].Labels = append(f.prs[i].Labels, github.Label{Name: label}) 60 } 61 } 62 return nil 63 } 64 65 func (f *fghc) AssignIssue(org, repo string, number int, logins []string) error { 66 var users []github.User 67 for _, login := range logins { 68 users = append(users, github.User{Login: login}) 69 } 70 71 f.Lock() 72 for i := range f.prs { 73 if number == f.prs[i].Number { 74 f.prs[i].Assignees = append(f.prs[i].Assignees, users...) 75 } 76 } 77 defer f.Unlock() 78 return nil 79 } 80 81 func (f *fghc) GetPullRequest(org, repo string, number int) (*github.PullRequest, error) { 82 f.Lock() 83 defer f.Unlock() 84 return f.pr, nil 85 } 86 87 func (f *fghc) GetPullRequestDiff(org, repo string, number int) ([]byte, error) { 88 f.Lock() 89 defer f.Unlock() 90 return f.diff, nil 91 } 92 93 func (f *fghc) GetPullRequestPatch(org, repo string, number int) ([]byte, error) { 94 f.Lock() 95 defer f.Unlock() 96 return f.patch, nil 97 } 98 99 func (f *fghc) GetPullRequests(org, repo string) ([]github.PullRequest, error) { 100 f.Lock() 101 defer f.Unlock() 102 return f.prs, nil 103 } 104 105 func (f *fghc) CreateComment(org, repo string, number int, comment string) error { 106 f.Lock() 107 defer f.Unlock() 108 f.comments = append(f.comments, fmt.Sprintf(commentFormat, org, repo, number, comment)) 109 return nil 110 } 111 112 func (f *fghc) IsMember(org, user string) (bool, error) { 113 f.Lock() 114 defer f.Unlock() 115 return f.isMember, nil 116 } 117 118 func (f *fghc) GetRepo(owner, name string) (github.FullRepo, error) { 119 f.Lock() 120 defer f.Unlock() 121 return github.FullRepo{}, nil 122 } 123 124 func (f *fghc) EnsureFork(forkingUser, org, repo string) (string, error) { 125 if repo == "changeme" { 126 return "changed", nil 127 } 128 if repo == "error" { 129 return repo, errors.New("errors") 130 } 131 return repo, nil 132 } 133 134 var expectedFmt = `title=%q body=%q head=%s base=%s labels=%v` 135 136 func prToString(pr github.PullRequest) string { 137 var labels []string 138 for _, label := range pr.Labels { 139 labels = append(labels, label.Name) 140 } 141 return fmt.Sprintf(expectedFmt, pr.Title, pr.Body, pr.Head.Ref, pr.Base.Ref, labels) 142 } 143 144 func (f *fghc) CreateIssue(org, repo, title, body string, milestone int, labels, assignees []string) (int, error) { 145 f.Lock() 146 defer f.Unlock() 147 148 var ghLabels []github.Label 149 var ghAssignees []github.User 150 151 var num int 152 for _, issue := range f.issues { 153 if issue.Number > num { 154 num = issue.Number 155 } 156 } 157 num++ 158 159 for _, label := range labels { 160 ghLabels = append(ghLabels, github.Label{Name: label}) 161 } 162 163 for _, assignee := range assignees { 164 ghAssignees = append(ghAssignees, github.User{Login: assignee}) 165 } 166 167 f.issues = append(f.issues, github.Issue{ 168 Title: title, 169 Body: body, 170 Number: num, 171 Labels: ghLabels, 172 Assignees: ghAssignees, 173 }) 174 175 return num, nil 176 } 177 178 func (f *fghc) CreatePullRequest(org, repo, title, body, head, base string, canModify bool) (int, error) { 179 f.Lock() 180 defer f.Unlock() 181 var num int 182 for _, pr := range f.prs { 183 if pr.Number > num { 184 num = pr.Number 185 } 186 } 187 num++ 188 f.prs = append(f.prs, github.PullRequest{ 189 Title: title, 190 Body: body, 191 Number: num, 192 Head: github.PullRequestBranch{Ref: head}, 193 Base: github.PullRequestBranch{Ref: base}, 194 }) 195 return num, nil 196 } 197 198 func (f *fghc) ListIssueComments(org, repo string, number int) ([]github.IssueComment, error) { 199 f.Lock() 200 defer f.Unlock() 201 return f.prComments, nil 202 } 203 204 func (f *fghc) GetIssueLabels(org, repo string, number int) ([]github.Label, error) { 205 f.Lock() 206 defer f.Unlock() 207 return f.prLabels, nil 208 } 209 210 func (f *fghc) ListOrgMembers(org, role string) ([]github.TeamMember, error) { 211 f.Lock() 212 defer f.Unlock() 213 if role != "all" { 214 return nil, fmt.Errorf("all is only supported role, not: %s", role) 215 } 216 return f.orgMembers, nil 217 } 218 219 func (f *fghc) CreateFork(org, repo string) (string, error) { 220 return repo, nil 221 } 222 223 var initialFiles = map[string][]byte{ 224 "bar.go": []byte(`// Package bar does an interesting thing. 225 package bar 226 227 // Foo does a thing. 228 func Foo(wow int) int { 229 return 42 + wow 230 } 231 `), 232 } 233 234 var patch = []byte(`From af468c9e69dfdf39db591f1e3e8de5b64b0e62a2 Mon Sep 17 00:00:00 2001 235 From: Wise Guy <wise@guy.com> 236 Date: Thu, 19 Oct 2017 15:14:36 +0200 237 Subject: [PATCH] Update magic number 238 239 --- 240 bar.go | 3 ++- 241 1 file changed, 2 insertions(+), 1 deletion(-) 242 243 diff --git a/bar.go b/bar.go 244 index 1ea52dc..5bd70a9 100644 245 --- a/bar.go 246 +++ b/bar.go 247 @@ -3,5 +3,6 @@ package bar 248 249 // Foo does a thing. 250 func Foo(wow int) int { 251 - return 42 + wow 252 + // Needs to be 49 because of a reason. 253 + return 49 + wow 254 } 255 `) 256 257 var body = "This PR updates the magic number.\n\n```release-note\nUpdate the magic number from 42 to 49\n```" 258 259 func makeFakeRepoWithCommit(clients localgit.Clients, t *testing.T) (*localgit.LocalGit, v2.ClientFactory) { 260 lg, c, err := clients() 261 if err != nil { 262 t.Fatalf("Making localgit: %v", err) 263 } 264 t.Cleanup(func() { 265 if err := lg.Clean(); err != nil { 266 t.Errorf("Cleaning up localgit: %v", err) 267 } 268 if err := c.Clean(); err != nil { 269 t.Errorf("Cleaning up client: %v", err) 270 } 271 }) 272 if err := lg.MakeFakeRepo("foo", "bar"); err != nil { 273 t.Fatalf("Making fake repo: %v", err) 274 } 275 if err := lg.AddCommit("foo", "bar", initialFiles); err != nil { 276 t.Fatalf("Adding initial commit: %v", err) 277 } 278 return lg, c 279 } 280 281 func TestCherryPickICV2(t *testing.T) { 282 t.Parallel() 283 testCherryPickIC(localgit.NewV2, t) 284 } 285 286 func testCherryPickIC(clients localgit.Clients, t *testing.T) { 287 iNumber := fakePR.GetPRNumber() 288 lg, c := makeFakeRepoWithCommit(clients, t) 289 if err := lg.CheckoutNewBranch("foo", "bar", "stage"); err != nil { 290 t.Fatalf("Checking out pull branch: %v", err) 291 } 292 293 ghc := &fghc{ 294 pr: &github.PullRequest{ 295 Base: github.PullRequestBranch{ 296 Ref: "master", 297 }, 298 Merged: true, 299 Title: "This is a fix for X", 300 Body: body, 301 }, 302 isMember: true, 303 patch: patch, 304 } 305 ic := github.IssueCommentEvent{ 306 Action: github.IssueCommentActionCreated, 307 Repo: github.Repo{ 308 Owner: github.User{ 309 Login: "foo", 310 }, 311 Name: "bar", 312 FullName: "foo/bar", 313 }, 314 Issue: github.Issue{ 315 Number: iNumber, 316 State: "closed", 317 PullRequest: &struct{}{}, 318 }, 319 Comment: github.IssueComment{ 320 User: github.User{ 321 Login: "wiseguy", 322 }, 323 Body: "/cherrypick stage", 324 }, 325 } 326 327 botUser := &github.UserData{Login: "ci-robot", Email: "ci-robot@users.noreply.github.com"} 328 expectedTitle := "[stage] This is a fix for X" 329 expectedBody := fmt.Sprintf("This is an automated cherry-pick of #%d\n\n/assign wiseguy\n\n```release-note\nUpdate the magic number from 42 to 49\n```", iNumber) 330 expectedBase := "stage" 331 expectedHead := fmt.Sprintf(botUser.Login+":"+cherryPickBranchFmt, iNumber, expectedBase) 332 expectedLabels := []string{} 333 expected := fmt.Sprintf(expectedFmt, expectedTitle, expectedBody, expectedHead, expectedBase, expectedLabels) 334 335 getSecret := func() []byte { 336 return []byte("sha=abcdefg") 337 } 338 339 s := &Server{ 340 botUser: botUser, 341 gc: c, 342 push: func(forkName, newBranch string, force bool) error { return nil }, 343 ghc: ghc, 344 tokenGenerator: getSecret, 345 log: logrus.StandardLogger().WithField("client", "cherrypicker"), 346 repos: []github.Repo{{Fork: true, FullName: "ci-robot/bar"}}, 347 348 prowAssignments: true, 349 } 350 351 if err := s.handleIssueComment(logrus.NewEntry(logrus.StandardLogger()), ic); err != nil { 352 t.Fatalf("unexpected error: %v", err) 353 } 354 got := prToString(ghc.prs[0]) 355 if got != expected { 356 t.Errorf("Expected (%d):\n%s\nGot (%d):\n%+v\n", len(expected), expected, len(got), got) 357 } 358 } 359 360 func TestCherryPickPRV2(t *testing.T) { 361 t.Parallel() 362 testCherryPickPR(localgit.NewV2, t) 363 } 364 365 func testCherryPickPR(clients localgit.Clients, t *testing.T) { 366 prNumber := fakePR.GetPRNumber() 367 lg, c := makeFakeRepoWithCommit(clients, t) 368 expectedBranches := []string{"release-1.5", "release-1.6", "release-1.8", "release-1.3", "release-1.12"} 369 for _, branch := range expectedBranches { 370 if err := lg.CheckoutNewBranch("foo", "bar", branch); err != nil { 371 t.Fatalf("Checking out pull branch: %v", err) 372 } 373 } 374 if err := lg.CheckoutNewBranch("foo", "bar", fmt.Sprintf("cherry-pick-%d-to-release-1.5", prNumber)); err != nil { 375 t.Fatalf("Checking out existing PR branch: %v", err) 376 } 377 378 ghc := &fghc{ 379 orgMembers: []github.TeamMember{ 380 { 381 Login: "approver", 382 }, 383 { 384 Login: "merge-bot", 385 }, 386 }, 387 prComments: []github.IssueComment{ 388 { 389 User: github.User{ 390 Login: "developer", 391 }, 392 Body: "a review comment", 393 }, 394 { 395 User: github.User{ 396 Login: "approver", 397 }, 398 Body: "/cherrypick release-1.5\r\n/cherrypick release-1.8", 399 }, 400 { 401 User: github.User{ 402 Login: "approver", 403 }, 404 Body: "/cherrypick release-1.6", 405 }, 406 { 407 User: github.User{ 408 Login: "approver", 409 }, 410 Body: "/cherrypick release-1.3 release-1.2", 411 }, 412 { 413 User: github.User{ 414 Login: "approver", 415 }, 416 Body: "/cherrypick release-1.12 release-1.11 release-1.10 release-1.9", 417 }, 418 { 419 User: github.User{ 420 Login: "fan", 421 }, 422 Body: "/cherrypick release-1.7", 423 }, 424 { 425 User: github.User{ 426 Login: "approver", 427 }, 428 Body: "/approve", 429 }, 430 { 431 User: github.User{ 432 Login: "merge-bot", 433 }, 434 Body: "Automatic merge from submit-queue.", 435 }, 436 }, 437 prs: []github.PullRequest{ 438 { 439 Title: "[release-1.5] This is a fix for Y", 440 Body: fmt.Sprintf("This is an automated cherry-pick of #%d", prNumber), 441 Base: github.PullRequestBranch{ 442 Ref: "release-1.5", 443 }, 444 Head: github.PullRequestBranch{ 445 Ref: fmt.Sprintf("ci-robot:cherry-pick-%d-to-release-1.5", prNumber), 446 }, 447 }, 448 }, 449 isMember: true, 450 patch: patch, 451 } 452 pr := github.PullRequestEvent{ 453 Action: github.PullRequestActionClosed, 454 PullRequest: github.PullRequest{ 455 Base: github.PullRequestBranch{ 456 Ref: "master", 457 Repo: github.Repo{ 458 Owner: github.User{ 459 Login: "foo", 460 }, 461 Name: "bar", 462 }, 463 }, 464 Number: prNumber, 465 Merged: true, 466 MergeSHA: new(string), 467 Title: "This is a fix for Y", 468 }, 469 } 470 471 botUser := &github.UserData{Login: "ci-robot", Email: "ci-robot@users.noreply.github.com"} 472 473 getSecret := func() []byte { 474 return []byte("sha=abcdefg") 475 } 476 477 s := &Server{ 478 botUser: botUser, 479 gc: c, 480 push: func(forkName, newBranch string, force bool) error { return nil }, 481 ghc: ghc, 482 tokenGenerator: getSecret, 483 log: logrus.StandardLogger().WithField("client", "cherrypicker"), 484 repos: []github.Repo{{Fork: true, FullName: "ci-robot/bar"}}, 485 486 prowAssignments: false, 487 } 488 489 if err := s.handlePullRequest(logrus.NewEntry(logrus.StandardLogger()), pr); err != nil { 490 t.Fatalf("unexpected error: %v", err) 491 } 492 493 var expectedFn = func(branch string) string { 494 expectedTitle := fmt.Sprintf("[%s] This is a fix for Y", branch) 495 expectedBody := fmt.Sprintf("This is an automated cherry-pick of #%d", prNumber) 496 if branch == "release-1.3" { 497 expectedBody = fmt.Sprintf("%s\n\n/cherrypick release-1.2", expectedBody) 498 } 499 if branch == "release-1.12" { 500 expectedBody = fmt.Sprintf("%s\n\n/cherrypick release-1.11 release-1.10 release-1.9", expectedBody) 501 } 502 expectedHead := fmt.Sprintf(botUser.Login+":"+cherryPickBranchFmt, prNumber, branch) 503 expectedLabels := s.labels 504 return fmt.Sprintf(expectedFmt, expectedTitle, expectedBody, expectedHead, branch, expectedLabels) 505 } 506 507 if len(ghc.prs) != len(expectedBranches) { 508 t.Fatalf("Expected %d PRs, got %d", len(expectedBranches), len(ghc.prs)) 509 } 510 511 expectedPrs := make(map[string]string) 512 for _, branch := range expectedBranches { 513 expectedPrs[expectedFn(branch)] = branch 514 } 515 seenBranches := make(map[string]struct{}) 516 for _, p := range ghc.prs { 517 pr := prToString(p) 518 branch, present := expectedPrs[pr] 519 if !present { 520 t.Errorf("Unexpected PR:\n%s\nExpected to target one of the following branches: %v\n", pr, expectedBranches) 521 } 522 seenBranches[branch] = struct{}{} 523 } 524 if len(seenBranches) != len(expectedBranches) { 525 t.Fatalf("Expected to see PRs for %d branches, got %d (%v)", len(expectedBranches), len(seenBranches), seenBranches) 526 } 527 } 528 529 func TestCherryPickOfCherryPickPRV2(t *testing.T) { 530 t.Parallel() 531 testCherryPickOfCherryPickPR(localgit.NewV2, t) 532 } 533 534 // testCherryPickOfCherryPickPR checks that the omitBaseBranchFromTitle 535 // function works as intended when the user performs a cherry-pick from 536 // a branch that's already a cherry-pick branch 537 func testCherryPickOfCherryPickPR(clients localgit.Clients, t *testing.T) { 538 prNumber := fakePR.GetPRNumber() 539 lg, c := makeFakeRepoWithCommit(clients, t) 540 expectedBranches := []string{"release-1.5", "release-1.6", "release-1.8"} 541 for _, branch := range expectedBranches { 542 if err := lg.CheckoutNewBranch("foo", "bar", branch); err != nil { 543 t.Fatalf("Checking out pull branch: %v", err) 544 } 545 } 546 547 ghc := &fghc{ 548 orgMembers: []github.TeamMember{ 549 { 550 Login: "approver", 551 }, 552 }, 553 prComments: []github.IssueComment{ 554 { 555 User: github.User{ 556 Login: "approver", 557 }, 558 Body: "/cherrypick release-1.8", 559 }, 560 }, 561 prs: []github.PullRequest{}, 562 isMember: true, 563 patch: patch, 564 } 565 566 pr := github.PullRequestEvent{ 567 Action: github.PullRequestActionClosed, 568 PullRequest: github.PullRequest{ 569 Base: github.PullRequestBranch{ 570 Ref: "master", 571 Repo: github.Repo{ 572 Owner: github.User{ 573 Login: "foo", 574 }, 575 Name: "bar", 576 }, 577 }, 578 Number: prNumber, 579 Merged: true, 580 MergeSHA: new(string), 581 Title: "This is a fix for Y", 582 }, 583 } 584 585 botUser := &github.UserData{Login: "ci-robot", Email: "ci-robot@users.noreply.github.com"} 586 587 getSecret := func() []byte { 588 return []byte("sha=abcdefg") 589 } 590 591 s := &Server{ 592 botUser: botUser, 593 gc: c, 594 push: func(forkName, newBranch string, force bool) error { return nil }, 595 ghc: ghc, 596 tokenGenerator: getSecret, 597 log: logrus.StandardLogger().WithField("client", "cherrypicker"), 598 repos: []github.Repo{{Fork: true, FullName: "ci-robot/bar"}}, 599 600 prowAssignments: false, 601 } 602 603 // Cherry pick master -> release-1.8 604 if err := s.handlePullRequest(logrus.NewEntry(logrus.StandardLogger()), pr); err != nil { 605 t.Fatalf("unexpected error: %v", err) 606 } 607 608 // Cherry pick release-1.8 -> release-1.6 609 pr.PullRequest.Base.Ref = "release-1.8" 610 pr.PullRequest.Title = "[release-1.8] This is a fix for Y" 611 ghc.prComments[0].Body = "/cherrypick release-1.6" 612 if err := s.handlePullRequest(logrus.NewEntry(logrus.StandardLogger()), pr); err != nil { 613 t.Fatalf("unexpected error: %v", err) 614 } 615 616 // Cherry pick release-1.6 -> release-1.5 617 pr.PullRequest.Base.Ref = "release-1.6" 618 pr.PullRequest.Title = "[release-1.6] This is a fix for Y" 619 ghc.prComments[0].Body = "/cherrypick release-1.5" 620 if err := s.handlePullRequest(logrus.NewEntry(logrus.StandardLogger()), pr); err != nil { 621 t.Fatalf("unexpected error: %v", err) 622 } 623 624 var expectedFn = func(branch string) string { 625 expectedTitle := fmt.Sprintf("[%s] This is a fix for Y", branch) 626 expectedBody := fmt.Sprintf("This is an automated cherry-pick of #%d", prNumber) 627 expectedHead := fmt.Sprintf(botUser.Login+":"+cherryPickBranchFmt, prNumber, branch) 628 expectedLabels := s.labels 629 return fmt.Sprintf(expectedFmt, expectedTitle, expectedBody, expectedHead, branch, expectedLabels) 630 } 631 632 if len(ghc.prs) != len(expectedBranches) { 633 t.Fatalf("Expected %d PRs, got %d", len(expectedBranches), len(ghc.prs)) 634 } 635 636 expectedPrs := make(map[string]string) 637 for _, branch := range expectedBranches { 638 expectedPrs[expectedFn(branch)] = branch 639 } 640 seenBranches := make(map[string]struct{}) 641 for _, p := range ghc.prs { 642 pr := prToString(p) 643 branch, present := expectedPrs[pr] 644 if !present { 645 t.Errorf("Unexpected PR:\n%s\nExpected to target one of the following branches: %v\n", pr, expectedBranches) 646 } 647 seenBranches[branch] = struct{}{} 648 } 649 if len(seenBranches) != len(expectedBranches) { 650 t.Fatalf("Expected to see PRs for %d branches, got %d (%v)", len(expectedBranches), len(seenBranches), seenBranches) 651 } 652 } 653 654 func TestCherryPickPRWithLabelsV2(t *testing.T) { 655 t.Parallel() 656 testCherryPickPRWithLabels(localgit.NewV2, t) 657 } 658 659 func testCherryPickPRWithLabels(clients localgit.Clients, t *testing.T) { 660 prNumber := fakePR.GetPRNumber() 661 lg, c := makeFakeRepoWithCommit(clients, t) 662 if err := lg.CheckoutNewBranch("foo", "bar", "release-1.5"); err != nil { 663 t.Fatalf("Checking out pull branch: %v", err) 664 } 665 if err := lg.CheckoutNewBranch("foo", "bar", "release-1.6"); err != nil { 666 t.Fatalf("Checking out pull branch: %v", err) 667 } 668 669 pr := func(evt github.PullRequestEventAction) github.PullRequestEvent { 670 return github.PullRequestEvent{ 671 Action: evt, 672 PullRequest: github.PullRequest{ 673 User: github.User{ 674 Login: "developer", 675 }, 676 Base: github.PullRequestBranch{ 677 Ref: "master", 678 Repo: github.Repo{ 679 Owner: github.User{ 680 Login: "foo", 681 }, 682 Name: "bar", 683 }, 684 }, 685 Number: prNumber, 686 Merged: true, 687 MergeSHA: new(string), 688 Title: "This is a fix for Y", 689 }, 690 } 691 } 692 693 events := []github.PullRequestEventAction{github.PullRequestActionClosed, github.PullRequestActionLabeled} 694 695 botUser := &github.UserData{Login: "ci-robot", Email: "ci-robot@users.noreply.github.com"} 696 697 getSecret := func() []byte { 698 return []byte("sha=abcdefg") 699 } 700 701 testCases := []struct { 702 name string 703 labelPrefix string 704 prLabels []github.Label 705 prComments []github.IssueComment 706 }{ 707 { 708 name: "Default label prefix", 709 labelPrefix: defaultLabelPrefix, 710 prLabels: []github.Label{ 711 { 712 Name: "cherrypick/release-1.5", 713 }, 714 { 715 Name: "cherrypick/release-1.6", 716 }, 717 { 718 Name: "cherrypick/release-1.7", 719 }, 720 }, 721 }, 722 { 723 name: "Custom label prefix", 724 labelPrefix: "needs-cherry-pick-", 725 prLabels: []github.Label{ 726 { 727 Name: "needs-cherry-pick-release-1.5", 728 }, 729 { 730 Name: "needs-cherry-pick-release-1.6", 731 }, 732 { 733 Name: "needs-cherry-pick-release-1.7", 734 }, 735 }, 736 }, 737 { 738 name: "No labels, label gets ignored", 739 labelPrefix: "needs-cherry-pick-", 740 }, 741 } 742 743 for _, tc := range testCases { 744 t.Run(tc.name, func(t *testing.T) { 745 for _, evt := range events { 746 t.Run(string(evt), func(t *testing.T) { 747 ghc := &fghc{ 748 orgMembers: []github.TeamMember{ 749 { 750 Login: "approver", 751 }, 752 { 753 Login: "merge-bot", 754 }, 755 { 756 Login: "developer", 757 }, 758 }, 759 prComments: []github.IssueComment{ 760 { 761 User: github.User{ 762 Login: "developer", 763 }, 764 Body: "a review comment", 765 }, 766 { 767 User: github.User{ 768 Login: "approver", 769 }, 770 Body: "/cherrypick release-1.5\r", 771 }, 772 }, 773 prLabels: tc.prLabels, 774 isMember: true, 775 patch: patch, 776 } 777 778 s := &Server{ 779 botUser: botUser, 780 gc: c, 781 push: func(forkName, newBranch string, force bool) error { return nil }, 782 ghc: ghc, 783 tokenGenerator: getSecret, 784 log: logrus.StandardLogger().WithField("client", "cherrypicker"), 785 repos: []github.Repo{{Fork: true, FullName: "ci-robot/bar"}}, 786 787 labels: []string{"cla: yes"}, 788 prowAssignments: false, 789 labelPrefix: tc.labelPrefix, 790 } 791 792 if err := s.handlePullRequest(logrus.NewEntry(logrus.StandardLogger()), pr(evt)); err != nil { 793 t.Fatalf("unexpected error: %v", err) 794 } 795 796 expectedFn := func(branch string) string { 797 expectedTitle := fmt.Sprintf("[%s] This is a fix for Y", branch) 798 expectedBody := fmt.Sprintf("This is an automated cherry-pick of #%d", prNumber) 799 expectedHead := fmt.Sprintf(botUser.Login+":"+cherryPickBranchFmt, prNumber, branch) 800 expectedLabels := s.labels 801 return fmt.Sprintf(expectedFmt, expectedTitle, expectedBody, expectedHead, branch, expectedLabels) 802 } 803 804 expectedPRs := 2 805 if len(tc.prLabels) == 0 { 806 if evt == github.PullRequestActionLabeled { 807 expectedPRs = 0 808 } else { 809 expectedPRs = 1 810 } 811 } 812 if len(ghc.prs) != expectedPRs { 813 t.Errorf("Expected %d PRs, got %d", expectedPRs, len(ghc.prs)) 814 } 815 816 expectedBranches := []string{"release-1.5", "release-1.6"} 817 seenBranches := make(map[string]struct{}) 818 for _, p := range ghc.prs { 819 pr := prToString(p) 820 if pr != expectedFn("release-1.5") && pr != expectedFn("release-1.6") { 821 t.Errorf("Unexpected PR:\n%s\nExpected to target one of the following branches: %v", pr, expectedBranches) 822 } 823 if pr == expectedFn("release-1.5") { 824 seenBranches["release-1.5"] = struct{}{} 825 } 826 if pr == expectedFn("release-1.6") { 827 seenBranches["release-1.6"] = struct{}{} 828 } 829 } 830 if len(seenBranches) != expectedPRs { 831 t.Fatalf("Expected to see PRs for %d branches, got %d (%v)", expectedPRs, len(seenBranches), seenBranches) 832 } 833 }) 834 } 835 }) 836 } 837 } 838 839 func TestCherryPickCreateIssue(t *testing.T) { 840 t.Parallel() 841 testCases := []struct { 842 org string 843 repo string 844 title string 845 body string 846 prNum int 847 labels []string 848 assignees []string 849 }{ 850 { 851 org: "istio", 852 repo: "istio", 853 title: "brand new feature", 854 body: "automated cherry-pick", 855 prNum: 2190, 856 labels: nil, 857 assignees: []string{"clarketm"}, 858 }, 859 { 860 org: "kubernetes", 861 repo: "kubernetes", 862 title: "alpha feature", 863 body: "automated cherry-pick", 864 prNum: 3444, 865 labels: []string{"new", "1.18"}, 866 assignees: nil, 867 }, 868 } 869 870 errMsg := func(field string) string { 871 return fmt.Sprintf("GH issue %q does not match: \nexpected: \"%%v\" \nactual: \"%%v\"", field) 872 } 873 874 for _, tc := range testCases { 875 876 ghc := &fghc{} 877 878 s := &Server{ 879 ghc: ghc, 880 } 881 882 if err := s.createIssue(logrus.WithField("test", t.Name()), tc.org, tc.repo, tc.title, tc.body, tc.prNum, nil, tc.labels, tc.assignees); err != nil { 883 t.Fatalf("unexpected error: %v", err) 884 } 885 886 if len(ghc.issues) < 1 { 887 t.Fatalf("Expected 1 GH issue to be created but got: %d", len(ghc.issues)) 888 } 889 890 ghIssue := ghc.issues[len(ghc.issues)-1] 891 892 if tc.title != ghIssue.Title { 893 t.Fatalf(errMsg("title"), tc.title, ghIssue.Title) 894 } 895 896 if tc.body != ghIssue.Body { 897 t.Fatalf(errMsg("body"), tc.title, ghIssue.Title) 898 } 899 900 if len(ghc.issues) != ghIssue.Number { 901 t.Fatalf(errMsg("number"), len(ghc.issues), ghIssue.Number) 902 } 903 904 var actualAssignees []string 905 for _, assignee := range ghIssue.Assignees { 906 actualAssignees = append(actualAssignees, assignee.Login) 907 } 908 909 if !reflect.DeepEqual(tc.assignees, actualAssignees) { 910 t.Fatalf(errMsg("assignees"), tc.assignees, actualAssignees) 911 } 912 913 var actualLabels []string 914 for _, label := range ghIssue.Labels { 915 actualLabels = append(actualLabels, label.Name) 916 } 917 918 if !reflect.DeepEqual(tc.labels, actualLabels) { 919 t.Fatalf(errMsg("labels"), tc.labels, actualLabels) 920 } 921 922 cpFormat := fmt.Sprintf(commentFormat, tc.org, tc.repo, tc.prNum, "In response to a cherrypick label: %s") 923 expectedComment := fmt.Sprintf(cpFormat, fmt.Sprintf("new issue created for failed cherrypick: #%d", ghIssue.Number)) 924 actualComment := ghc.comments[len(ghc.comments)-1] 925 926 if expectedComment != actualComment { 927 t.Fatalf(errMsg("comment"), expectedComment, actualComment) 928 } 929 930 } 931 } 932 933 func TestCherryPickPRAssignmentsV2(t *testing.T) { 934 t.Parallel() 935 testCherryPickPRAssignments(localgit.NewV2, t) 936 } 937 938 func testCherryPickPRAssignments(clients localgit.Clients, t *testing.T) { 939 iNumber := fakePR.GetPRNumber() 940 for _, prowAssignments := range []bool{true, false} { 941 lg, c := makeFakeRepoWithCommit(clients, t) 942 if err := lg.CheckoutNewBranch("foo", "bar", "stage"); err != nil { 943 t.Fatalf("Checking out pull branch: %v", err) 944 } 945 946 user := github.User{ 947 Login: "wiseguy", 948 } 949 ghc := &fghc{ 950 pr: &github.PullRequest{ 951 Base: github.PullRequestBranch{ 952 Ref: "master", 953 }, 954 Merged: true, 955 Title: "This is a fix for X", 956 Body: body, 957 }, 958 isMember: true, 959 patch: patch, 960 } 961 ic := github.IssueCommentEvent{ 962 Action: github.IssueCommentActionCreated, 963 Repo: github.Repo{ 964 Owner: github.User{ 965 Login: "foo", 966 }, 967 Name: "bar", 968 FullName: "foo/bar", 969 }, 970 Issue: github.Issue{ 971 Number: iNumber, 972 State: "closed", 973 PullRequest: &struct{}{}, 974 }, 975 Comment: github.IssueComment{ 976 User: user, 977 Body: "/cherrypick stage", 978 }, 979 } 980 981 botUser := &github.UserData{Login: "ci-robot", Email: "ci-robot@users.noreply.github.com"} 982 getSecret := func() []byte { 983 return []byte("sha=abcdefg") 984 } 985 986 s := &Server{ 987 botUser: botUser, 988 gc: c, 989 push: func(forkName, newBranch string, force bool) error { return nil }, 990 ghc: ghc, 991 tokenGenerator: getSecret, 992 log: logrus.StandardLogger().WithField("client", "cherrypicker"), 993 repos: []github.Repo{{Fork: true, FullName: "ci-robot/bar"}}, 994 995 prowAssignments: prowAssignments, 996 } 997 998 if err := s.handleIssueComment(logrus.NewEntry(logrus.StandardLogger()), ic); err != nil { 999 t.Fatalf("unexpected error: %v", err) 1000 } 1001 1002 var expected []github.User 1003 if prowAssignments { 1004 expected = append(expected, user) 1005 } 1006 1007 got := ghc.prs[0].Assignees 1008 if !cmp.Equal(got, expected) { 1009 t.Errorf("Expected (%d):\n+%v\nGot (%d):\n%+v\n", len(expected), expected, len(got), got) 1010 } 1011 } 1012 } 1013 1014 func TestHandleLocks(t *testing.T) { 1015 t.Parallel() 1016 s := &Server{ 1017 ghc: &threadUnsafeFGHC{fghc: &fghc{}}, 1018 botUser: &github.UserData{}, 1019 } 1020 1021 routine1Done := make(chan struct{}) 1022 routine2Done := make(chan struct{}) 1023 1024 l := logrus.WithField("test", t.Name()) 1025 1026 go func() { 1027 defer close(routine1Done) 1028 if err := s.handle(l, "", &github.IssueComment{}, "org", "repo", "targetBranch", "baseBranch", []string{}, "title", "body", 0); err != nil { 1029 t.Errorf("routine failed: %v", err) 1030 } 1031 }() 1032 go func() { 1033 defer close(routine2Done) 1034 if err := s.handle(l, "", &github.IssueComment{}, "org", "repo", "targetBranch", "baseBranch", []string{}, "title", "body", 0); err != nil { 1035 t.Errorf("routine failed: %v", err) 1036 } 1037 }() 1038 1039 <-routine1Done 1040 <-routine2Done 1041 1042 if actual := s.ghc.(*threadUnsafeFGHC).orgRepoCountCalled; actual != 2 { 1043 t.Errorf("expected two EnsureFork calls, got %d", actual) 1044 } 1045 } 1046 1047 func TestEnsureForkExists(t *testing.T) { 1048 botUser := &github.UserData{Login: "ci-robot", Email: "ci-robot@users.noreply.github.com"} 1049 1050 ghc := &fghc{} 1051 1052 s := &Server{ 1053 botUser: botUser, 1054 ghc: ghc, 1055 repos: []github.Repo{{Fork: true, FullName: "ci-robot/bar"}}, 1056 } 1057 1058 testCases := []struct { 1059 name string 1060 org string 1061 repo string 1062 expected string 1063 errors bool 1064 }{ 1065 { 1066 name: "Repo name does not change after ensured", 1067 org: "whatever", 1068 repo: "repo", 1069 expected: "repo", 1070 errors: false, 1071 }, 1072 { 1073 name: "EnsureFork changes repo name", 1074 org: "whatever", 1075 repo: "changeme", 1076 expected: "changed", 1077 errors: false, 1078 }, 1079 { 1080 name: "EnsureFork errors", 1081 org: "whatever", 1082 repo: "error", 1083 expected: "error", 1084 errors: true, 1085 }, 1086 } 1087 for _, tc := range testCases { 1088 t.Run(tc.name, func(t *testing.T) { 1089 res, err := s.ensureForkExists(tc.org, tc.repo) 1090 if tc.errors && err == nil { 1091 t.Errorf("expected error, but did not get one") 1092 } 1093 if !tc.errors && err != nil { 1094 t.Errorf("expected no error, but got one") 1095 } 1096 if res != tc.expected { 1097 t.Errorf("expected %s but got %s", tc.expected, res) 1098 } 1099 }) 1100 } 1101 1102 } 1103 1104 type threadUnsafeFGHC struct { 1105 *fghc 1106 orgRepoCountCalled int 1107 } 1108 1109 func (tuf *threadUnsafeFGHC) EnsureFork(login, org, repo string) (string, error) { 1110 tuf.orgRepoCountCalled++ 1111 return "", errors.New("that is enough") 1112 } 1113 1114 type prNumberGenerator struct { 1115 sync.Mutex 1116 prNumber int 1117 } 1118 1119 func (p *prNumberGenerator) GetPRNumber() int { 1120 p.Lock() 1121 defer p.Unlock() 1122 p.prNumber = p.prNumber + 10 1123 return p.prNumber 1124 }