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  }