github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/plugins/blunderbuss/blunderbuss_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 blunderbuss
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"os"
    23  	"path/filepath"
    24  	"reflect"
    25  	"regexp"
    26  	"sort"
    27  	"strings"
    28  	"testing"
    29  
    30  	githubql "github.com/shurcooL/githubv4"
    31  	"github.com/sirupsen/logrus"
    32  	"sigs.k8s.io/yaml"
    33  
    34  	"k8s.io/apimachinery/pkg/util/sets"
    35  	"sigs.k8s.io/prow/pkg/config"
    36  	"sigs.k8s.io/prow/pkg/github"
    37  	"sigs.k8s.io/prow/pkg/layeredsets"
    38  	"sigs.k8s.io/prow/pkg/plugins"
    39  	"sigs.k8s.io/prow/pkg/plugins/ownersconfig"
    40  	"sigs.k8s.io/prow/pkg/repoowners"
    41  )
    42  
    43  type fakeGitHubClient struct {
    44  	pr        *github.PullRequest
    45  	changes   []github.PullRequestChange
    46  	requested []string
    47  }
    48  
    49  func newFakeGitHubClient(pr *github.PullRequest, filesChanged []string) *fakeGitHubClient {
    50  	changes := make([]github.PullRequestChange, 0, len(filesChanged))
    51  	for _, name := range filesChanged {
    52  		changes = append(changes, github.PullRequestChange{Filename: name})
    53  	}
    54  	return &fakeGitHubClient{pr: pr, changes: changes}
    55  }
    56  
    57  func (c *fakeGitHubClient) RequestReview(org, repo string, number int, logins []string) error {
    58  	if org != "org" {
    59  		return errors.New("org should be 'org'")
    60  	}
    61  	if repo != "repo" {
    62  		return errors.New("repo should be 'repo'")
    63  	}
    64  	if number != 5 {
    65  		return errors.New("number should be 5")
    66  	}
    67  	c.requested = append(c.requested, logins...)
    68  	return nil
    69  }
    70  
    71  func (c *fakeGitHubClient) GetPullRequestChanges(org, repo string, num int) ([]github.PullRequestChange, error) {
    72  	if org != "org" {
    73  		return nil, errors.New("org should be 'org'")
    74  	}
    75  	if repo != "repo" {
    76  		return nil, errors.New("repo should be 'repo'")
    77  	}
    78  	if num != 5 {
    79  		return nil, errors.New("number should be 5")
    80  	}
    81  	return c.changes, nil
    82  }
    83  
    84  func (c *fakeGitHubClient) GetPullRequest(org, repo string, num int) (*github.PullRequest, error) {
    85  	return c.pr, nil
    86  }
    87  
    88  func (c *fakeGitHubClient) Query(ctx context.Context, q interface{}, vars map[string]interface{}) error {
    89  	sq, ok := q.(*githubAvailabilityQuery)
    90  	if !ok {
    91  		return errors.New("unexpected query type")
    92  	}
    93  	sq.User.Login = vars["user"].(githubql.String)
    94  	if sq.User.Login == githubql.String("busy-user") {
    95  		sq.User.Status.IndicatesLimitedAvailability = githubql.Boolean(true)
    96  	}
    97  	return nil
    98  }
    99  
   100  type fakeRepoownersClient struct {
   101  	foc *fakeOwnersClient
   102  }
   103  
   104  func (froc fakeRepoownersClient) LoadRepoOwners(org, repo, base string) (repoowners.RepoOwner, error) {
   105  	return froc.foc, nil
   106  }
   107  
   108  type fakeOwnersClient struct {
   109  	owners            map[string]string
   110  	approvers         map[string]layeredsets.String
   111  	leafApprovers     map[string]sets.Set[string]
   112  	reviewers         map[string]layeredsets.String
   113  	requiredReviewers map[string]sets.Set[string]
   114  	leafReviewers     map[string]sets.Set[string]
   115  	dirDenylist       []*regexp.Regexp
   116  }
   117  
   118  func (foc *fakeOwnersClient) AllApprovers() sets.Set[string] {
   119  	return sets.Set[string]{}
   120  }
   121  
   122  func (foc *fakeOwnersClient) AllOwners() sets.Set[string] {
   123  	return sets.Set[string]{}
   124  }
   125  
   126  func (foc *fakeOwnersClient) AllReviewers() sets.Set[string] {
   127  	return sets.Set[string]{}
   128  }
   129  
   130  func (foc *fakeOwnersClient) Filenames() ownersconfig.Filenames {
   131  	return ownersconfig.FakeFilenames
   132  }
   133  
   134  func (foc *fakeOwnersClient) Approvers(path string) layeredsets.String {
   135  	return foc.approvers[path]
   136  }
   137  
   138  func (foc *fakeOwnersClient) LeafApprovers(path string) sets.Set[string] {
   139  	return foc.leafApprovers[path]
   140  }
   141  
   142  func (foc *fakeOwnersClient) FindApproverOwnersForFile(path string) string {
   143  	return foc.owners[path]
   144  }
   145  
   146  func (foc *fakeOwnersClient) Reviewers(path string) layeredsets.String {
   147  	return foc.reviewers[path]
   148  }
   149  
   150  func (foc *fakeOwnersClient) RequiredReviewers(path string) sets.Set[string] {
   151  	return foc.requiredReviewers[path]
   152  }
   153  
   154  func (foc *fakeOwnersClient) LeafReviewers(path string) sets.Set[string] {
   155  	return foc.leafReviewers[path]
   156  }
   157  
   158  func (foc *fakeOwnersClient) FindReviewersOwnersForFile(path string) string {
   159  	return foc.owners[path]
   160  }
   161  
   162  func (foc *fakeOwnersClient) FindLabelsForFile(path string) sets.Set[string] {
   163  	return sets.Set[string]{}
   164  }
   165  
   166  func (foc *fakeOwnersClient) IsNoParentOwners(path string) bool {
   167  	return false
   168  }
   169  
   170  func (foc *fakeOwnersClient) IsAutoApproveUnownedSubfolders(path string) bool {
   171  	return false
   172  }
   173  
   174  func (foc *fakeOwnersClient) ParseSimpleConfig(path string) (repoowners.SimpleConfig, error) {
   175  	dir := filepath.Dir(path)
   176  	for _, re := range foc.dirDenylist {
   177  		if re.MatchString(dir) {
   178  			return repoowners.SimpleConfig{}, filepath.SkipDir
   179  		}
   180  	}
   181  
   182  	b, err := os.ReadFile(path)
   183  	if err != nil {
   184  		return repoowners.SimpleConfig{}, err
   185  	}
   186  	full := new(repoowners.SimpleConfig)
   187  	err = yaml.Unmarshal(b, full)
   188  	return *full, err
   189  }
   190  
   191  func (foc *fakeOwnersClient) ParseFullConfig(path string) (repoowners.FullConfig, error) {
   192  	dir := filepath.Dir(path)
   193  	for _, re := range foc.dirDenylist {
   194  		if re.MatchString(dir) {
   195  			return repoowners.FullConfig{}, filepath.SkipDir
   196  		}
   197  	}
   198  
   199  	b, err := os.ReadFile(path)
   200  	if err != nil {
   201  		return repoowners.FullConfig{}, err
   202  	}
   203  	full := new(repoowners.FullConfig)
   204  	err = yaml.Unmarshal(b, full)
   205  	return *full, err
   206  }
   207  
   208  func (foc *fakeOwnersClient) TopLevelApprovers() sets.Set[string] {
   209  	return sets.Set[string]{}
   210  }
   211  
   212  var (
   213  	owners = map[string]string{
   214  		"a.go":  "1",
   215  		"b.go":  "2",
   216  		"bb.go": "3",
   217  		"c.go":  "4",
   218  
   219  		"e.go":  "5",
   220  		"ee.go": "5",
   221  	}
   222  	reviewers = map[string]layeredsets.String{
   223  		"a.go": layeredsets.NewString("al"),
   224  		"b.go": layeredsets.NewString("al"),
   225  		"c.go": layeredsets.NewStringFromSlices([]string{"charles"}, []string{"ben"}), // ben is top level, charles is lower
   226  
   227  		"e.go":  layeredsets.NewString("erick", "evan"),
   228  		"ee.go": layeredsets.NewString("erick", "evan"),
   229  		"f.go":  layeredsets.NewString("author", "non-author"),
   230  	}
   231  	requiredReviewers = map[string]sets.Set[string]{
   232  		"a.go": sets.New[string]("ben"),
   233  
   234  		"ee.go": sets.New[string]("chris", "charles"),
   235  	}
   236  	leafReviewers = map[string]sets.Set[string]{
   237  		"a.go":  sets.New[string]("alice"),
   238  		"b.go":  sets.New[string]("bob"),
   239  		"bb.go": sets.New[string]("bob", "ben"),
   240  		"c.go":  sets.New[string]("cole", "carl", "chad"),
   241  
   242  		"e.go":  sets.New[string]("erick", "ellen"),
   243  		"ee.go": sets.New[string]("erick", "ellen"),
   244  		"f.go":  sets.New[string]("author"),
   245  	}
   246  	testcases = []struct {
   247  		name                       string
   248  		filesChanged               []string
   249  		reviewerCount              int
   250  		maxReviewerCount           int
   251  		expectedRequested          []string
   252  		alternateExpectedRequested []string
   253  	}{
   254  		{
   255  			name:              "one file, 3 leaf reviewers, 1 parent reviewer, 1 top level reviewer, request 3",
   256  			filesChanged:      []string{"c.go"},
   257  			reviewerCount:     3,
   258  			expectedRequested: []string{"cole", "carl", "chad"},
   259  		},
   260  		{
   261  			name:              "one file, 3 leaf reviewers, 1 parent reviewer, 1 top level reviewer, request 4",
   262  			filesChanged:      []string{"c.go"},
   263  			reviewerCount:     4,
   264  			expectedRequested: []string{"cole", "carl", "chad", "charles"},
   265  		},
   266  		{
   267  			name:              "one file, 3 leaf reviewers, 1 parent reviewer, 1 top level reviewer, request 5",
   268  			filesChanged:      []string{"c.go"},
   269  			reviewerCount:     5,
   270  			expectedRequested: []string{"cole", "carl", "chad", "charles", "ben"}, // last resort we take the top level reviewer
   271  		},
   272  		{
   273  			name:              "two files, 2 leaf reviewers, 1 common parent, request 2",
   274  			filesChanged:      []string{"a.go", "b.go"},
   275  			reviewerCount:     2,
   276  			expectedRequested: []string{"alice", "ben", "bob"},
   277  		},
   278  		{
   279  			name:              "two files, 2 leaf reviewers, 1 common parent, request 3",
   280  			filesChanged:      []string{"a.go", "b.go"},
   281  			reviewerCount:     3,
   282  			expectedRequested: []string{"alice", "ben", "bob", "al"},
   283  		},
   284  		{
   285  			name:              "one files, 1 leaf reviewers, request 1",
   286  			filesChanged:      []string{"a.go"},
   287  			reviewerCount:     1,
   288  			maxReviewerCount:  1,
   289  			expectedRequested: []string{"alice", "ben"},
   290  		},
   291  		{
   292  			name:              "one file, 2 leaf reviewer, 2 parent reviewers (1 dup), request 3",
   293  			filesChanged:      []string{"e.go"},
   294  			reviewerCount:     3,
   295  			expectedRequested: []string{"erick", "ellen", "evan"},
   296  		},
   297  		{
   298  			name:                       "two files, 2 leaf reviewer, 2 parent reviewers (1 dup), request 1",
   299  			filesChanged:               []string{"e.go"},
   300  			reviewerCount:              1,
   301  			expectedRequested:          []string{"erick"},
   302  			alternateExpectedRequested: []string{"ellen"},
   303  		},
   304  		{
   305  			name:              "two files, 1 common leaf reviewer, one additional leaf, one parent, request 1",
   306  			filesChanged:      []string{"b.go", "bb.go"},
   307  			reviewerCount:     1,
   308  			expectedRequested: []string{"bob", "ben"},
   309  		},
   310  		{
   311  			name:              "two files, 2 leaf reviewers, 1 common parent, request 1",
   312  			filesChanged:      []string{"a.go", "b.go"},
   313  			reviewerCount:     1,
   314  			expectedRequested: []string{"alice", "ben", "bob"},
   315  		},
   316  		{
   317  			name:                       "two files, 2 leaf reviewers, 1 common parent, request 1, limit 2",
   318  			filesChanged:               []string{"a.go", "b.go"},
   319  			reviewerCount:              1,
   320  			maxReviewerCount:           1,
   321  			expectedRequested:          []string{"alice", "ben"},
   322  			alternateExpectedRequested: []string{"ben", "bob"},
   323  		},
   324  		{
   325  			name:              "exclude author",
   326  			filesChanged:      []string{"f.go"},
   327  			reviewerCount:     1,
   328  			expectedRequested: []string{"non-author"},
   329  		},
   330  		{
   331  			name:          "reviewerCount==0",
   332  			filesChanged:  []string{"f.go"},
   333  			reviewerCount: 0,
   334  		},
   335  	}
   336  )
   337  
   338  // TestHandleWithExcludeApprovers tests that the handle function requests
   339  // reviews from the correct number of unique users when ExcludeApprovers is
   340  // true.
   341  func TestHandleWithExcludeApproversOnlyReviewers(t *testing.T) {
   342  	froc := &fakeRepoownersClient{
   343  		foc: &fakeOwnersClient{
   344  			owners:            owners,
   345  			reviewers:         reviewers,
   346  			requiredReviewers: requiredReviewers,
   347  			leafReviewers:     leafReviewers,
   348  		},
   349  	}
   350  
   351  	for _, tc := range testcases {
   352  		pr := github.PullRequest{Number: 5, User: github.User{Login: "author"}}
   353  		repo := github.Repo{Owner: github.User{Login: "org"}, Name: "repo"}
   354  		fghc := newFakeGitHubClient(&pr, tc.filesChanged)
   355  
   356  		if err := handle(
   357  			fghc, froc, logrus.WithField("plugin", PluginName),
   358  			&tc.reviewerCount, tc.maxReviewerCount, true, false, &repo, &pr,
   359  		); err != nil {
   360  			t.Errorf("[%s] unexpected error from handle: %v", tc.name, err)
   361  			continue
   362  		}
   363  
   364  		sort.Strings(fghc.requested)
   365  		sort.Strings(tc.expectedRequested)
   366  		sort.Strings(tc.alternateExpectedRequested)
   367  		if !reflect.DeepEqual(fghc.requested, tc.expectedRequested) {
   368  			if len(tc.alternateExpectedRequested) > 0 {
   369  				if !reflect.DeepEqual(fghc.requested, tc.alternateExpectedRequested) {
   370  					t.Errorf("[%s] expected the requested reviewers to be %q or %q, but got %q.", tc.name, tc.expectedRequested, tc.alternateExpectedRequested, fghc.requested)
   371  				}
   372  				continue
   373  			}
   374  			t.Errorf("[%s] expected the requested reviewers to be %q, but got %q.", tc.name, tc.expectedRequested, fghc.requested)
   375  		}
   376  	}
   377  }
   378  
   379  // TestHandleWithoutExcludeApprovers verifies that behavior is the same
   380  // when ExcludeApprovers is false and only approvers exist in the OWNERS files.
   381  // The owners fixture and test cases should always be the same as the ones in
   382  // TestHandleWithExcludeApprovers.
   383  func TestHandleWithoutExcludeApproversNoReviewers(t *testing.T) {
   384  	froc := &fakeRepoownersClient{
   385  		foc: &fakeOwnersClient{
   386  			owners:            owners,
   387  			approvers:         reviewers,
   388  			leafApprovers:     leafReviewers,
   389  			requiredReviewers: requiredReviewers,
   390  		},
   391  	}
   392  
   393  	for _, tc := range testcases {
   394  		pr := github.PullRequest{Number: 5, User: github.User{Login: "author"}}
   395  		repo := github.Repo{Owner: github.User{Login: "org"}, Name: "repo"}
   396  		fghc := newFakeGitHubClient(&pr, tc.filesChanged)
   397  
   398  		if err := handle(
   399  			fghc, froc, logrus.WithField("plugin", PluginName),
   400  			&tc.reviewerCount, tc.maxReviewerCount, false, false, &repo, &pr,
   401  		); err != nil {
   402  			t.Errorf("[%s] unexpected error from handle: %v", tc.name, err)
   403  			continue
   404  		}
   405  
   406  		sort.Strings(fghc.requested)
   407  		sort.Strings(tc.expectedRequested)
   408  		sort.Strings(tc.alternateExpectedRequested)
   409  		if !reflect.DeepEqual(fghc.requested, tc.expectedRequested) {
   410  			if len(tc.alternateExpectedRequested) > 0 {
   411  				if !reflect.DeepEqual(fghc.requested, tc.alternateExpectedRequested) {
   412  					t.Errorf("[%s] expected the requested reviewers to be %q or %q, but got %q.", tc.name, tc.expectedRequested, tc.alternateExpectedRequested, fghc.requested)
   413  				}
   414  				continue
   415  			}
   416  			t.Errorf("[%s] expected the requested reviewers to be %q, but got %q.", tc.name, tc.expectedRequested, fghc.requested)
   417  		}
   418  	}
   419  }
   420  
   421  func TestHandleWithoutExcludeApproversMixed(t *testing.T) {
   422  	froc := &fakeRepoownersClient{
   423  		foc: &fakeOwnersClient{
   424  			owners: map[string]string{
   425  				"a.go":  "1",
   426  				"b.go":  "2",
   427  				"bb.go": "3",
   428  				"c.go":  "4",
   429  
   430  				"e.go":  "5",
   431  				"ee.go": "5",
   432  				"f.go":  "6",
   433  				"g.go":  "7",
   434  			},
   435  			approvers: map[string]layeredsets.String{
   436  				"a.go": layeredsets.NewString("al"),
   437  				"b.go": layeredsets.NewString("jeff"),
   438  				"c.go": layeredsets.NewString("jeff"),
   439  
   440  				"e.go":  layeredsets.NewString(),
   441  				"ee.go": layeredsets.NewString("larry"),
   442  				"f.go":  layeredsets.NewString("approver1"),
   443  				"g.go":  layeredsets.NewString("Approver1"),
   444  			},
   445  			leafApprovers: map[string]sets.Set[string]{
   446  				"a.go": sets.New[string]("alice"),
   447  				"b.go": sets.New[string]("brad"),
   448  				"c.go": sets.New[string]("evan"),
   449  
   450  				"e.go":  sets.New[string]("erick", "evan"),
   451  				"ee.go": sets.New[string]("erick", "evan"),
   452  				"f.go":  sets.New[string]("leafApprover1", "leafApprover2"),
   453  				"g.go":  sets.New[string]("leafApprover1", "leafApprover2"),
   454  			},
   455  			reviewers: map[string]layeredsets.String{
   456  				"a.go": layeredsets.NewString("al"),
   457  				"b.go": layeredsets.NewString(),
   458  				"c.go": layeredsets.NewString("charles"),
   459  
   460  				"e.go":  layeredsets.NewString("erick", "evan"),
   461  				"ee.go": layeredsets.NewString("erick", "evan"),
   462  			},
   463  			leafReviewers: map[string]sets.Set[string]{
   464  				"a.go":  sets.New[string]("alice"),
   465  				"b.go":  sets.New[string]("bob"),
   466  				"bb.go": sets.New[string]("bob", "ben"),
   467  				"c.go":  sets.New[string]("cole", "carl", "chad"),
   468  
   469  				"e.go":  sets.New[string]("erick", "ellen"),
   470  				"ee.go": sets.New[string]("erick", "ellen"),
   471  			},
   472  		},
   473  	}
   474  
   475  	var testcases = []struct {
   476  		name                       string
   477  		filesChanged               []string
   478  		reviewerCount              int
   479  		maxReviewerCount           int
   480  		expectedRequested          []string
   481  		alternateExpectedRequested []string
   482  	}{
   483  		{
   484  			name:              "1 file, 1 leaf reviewer, 1 leaf approver, 1 approver, request 3",
   485  			filesChanged:      []string{"b.go"},
   486  			reviewerCount:     3,
   487  			expectedRequested: []string{"bob", "brad", "jeff"},
   488  		},
   489  		{
   490  			name:              "1 file, 1 leaf reviewer, 1 leaf approver, 1 approver, request 1, limit 1",
   491  			filesChanged:      []string{"b.go"},
   492  			reviewerCount:     1,
   493  			expectedRequested: []string{"bob"},
   494  		},
   495  		{
   496  			name:              "2 file, 2 leaf reviewers, 1 parent reviewers, 1 leaf approver, 1 approver, request 5",
   497  			filesChanged:      []string{"a.go", "b.go"},
   498  			reviewerCount:     5,
   499  			expectedRequested: []string{"alice", "bob", "al", "brad", "jeff"},
   500  		},
   501  		{
   502  			name:              "1 file, 1 leaf reviewer+approver, 1 reviewer+approver, request 3",
   503  			filesChanged:      []string{"a.go"},
   504  			reviewerCount:     3,
   505  			expectedRequested: []string{"alice", "al"},
   506  		},
   507  		{
   508  			name:              "1 file, 2 leaf reviewers, request 2",
   509  			filesChanged:      []string{"e.go"},
   510  			reviewerCount:     2,
   511  			expectedRequested: []string{"erick", "ellen"},
   512  		},
   513  		{
   514  			name:              "2 files, 2 leaf+parent reviewers, 1 parent reviewer, 1 parent approver, request 4",
   515  			filesChanged:      []string{"e.go", "ee.go"},
   516  			reviewerCount:     4,
   517  			expectedRequested: []string{"erick", "ellen", "evan", "larry"},
   518  		},
   519  		{
   520  			name:              "1 file, 2 leaf approvers, 1 approver, request 3, max 2",
   521  			filesChanged:      []string{"f.go"},
   522  			reviewerCount:     3,
   523  			maxReviewerCount:  2,
   524  			expectedRequested: []string{"leafApprover1", "leafApprover2"},
   525  		},
   526  		{
   527  			name:              "1 file, 2 leaf approvers, 1 approver (capitalized), request 3, max 2",
   528  			filesChanged:      []string{"g.go"},
   529  			reviewerCount:     3,
   530  			maxReviewerCount:  2,
   531  			expectedRequested: []string{"leafApprover1", "leafApprover2"},
   532  		},
   533  		{
   534  			name:          "reviewerCount==0",
   535  			filesChanged:  []string{"g.go"},
   536  			reviewerCount: 0,
   537  		},
   538  	}
   539  	for _, tc := range testcases {
   540  		pr := github.PullRequest{Number: 5, User: github.User{Login: "author"}}
   541  		repo := github.Repo{Owner: github.User{Login: "org"}, Name: "repo"}
   542  		fghc := newFakeGitHubClient(&pr, tc.filesChanged)
   543  		if err := handle(
   544  			fghc, froc, logrus.WithField("plugin", PluginName),
   545  			&tc.reviewerCount, tc.maxReviewerCount, false, false, &repo, &pr,
   546  		); err != nil {
   547  			t.Errorf("[%s] unexpected error from handle: %v", tc.name, err)
   548  			continue
   549  		}
   550  
   551  		sort.Strings(fghc.requested)
   552  		sort.Strings(tc.expectedRequested)
   553  		sort.Strings(tc.alternateExpectedRequested)
   554  		if !reflect.DeepEqual(fghc.requested, tc.expectedRequested) {
   555  			if len(tc.alternateExpectedRequested) > 0 {
   556  				if !reflect.DeepEqual(fghc.requested, tc.alternateExpectedRequested) {
   557  					t.Errorf("[%s] expected the requested reviewers to be %q or %q, but got %q.", tc.name, tc.expectedRequested, tc.alternateExpectedRequested, fghc.requested)
   558  				}
   559  				continue
   560  			}
   561  			t.Errorf("[%s] expected the requested reviewers to be %q, but got %q.", tc.name, tc.expectedRequested, fghc.requested)
   562  		}
   563  	}
   564  }
   565  
   566  func TestHandlePullRequest(t *testing.T) {
   567  	froc := &fakeRepoownersClient{
   568  		foc: &fakeOwnersClient{
   569  			owners: map[string]string{
   570  				"a.go": "1",
   571  			},
   572  			leafReviewers: map[string]sets.Set[string]{
   573  				"a.go": sets.New[string]("al"),
   574  			},
   575  		},
   576  	}
   577  
   578  	var testcases = []struct {
   579  		name              string
   580  		action            github.PullRequestEventAction
   581  		body              string
   582  		filesChanged      []string
   583  		reviewerCount     int
   584  		expectedRequested []string
   585  		draft             bool
   586  		ignoreDrafts      bool
   587  		ignoreAuthors     []string
   588  	}{
   589  		{
   590  			name:              "PR opened",
   591  			action:            github.PullRequestActionOpened,
   592  			body:              "/auto-cc",
   593  			filesChanged:      []string{"a.go"},
   594  			reviewerCount:     1,
   595  			expectedRequested: []string{"al"},
   596  		},
   597  		{
   598  			name:          "PR opened with /cc command",
   599  			action:        github.PullRequestActionOpened,
   600  			body:          "/cc",
   601  			filesChanged:  []string{"a.go"},
   602  			reviewerCount: 1,
   603  		},
   604  		{
   605  			name:          "PR closed",
   606  			action:        github.PullRequestActionClosed,
   607  			body:          "/auto-cc",
   608  			filesChanged:  []string{"a.go"},
   609  			reviewerCount: 1,
   610  		},
   611  		{
   612  			name:         "draft pr opened, ignoreDrafts true, do not assign review to PR",
   613  			action:       github.PullRequestActionOpened,
   614  			filesChanged: []string{"a.go"},
   615  			draft:        true,
   616  			ignoreDrafts: true,
   617  		},
   618  		{
   619  			name:              "non-draft pr opened, ignoreDrafts true, assign review to PR",
   620  			action:            github.PullRequestActionOpened,
   621  			filesChanged:      []string{"a.go"},
   622  			draft:             false,
   623  			ignoreDrafts:      true,
   624  			reviewerCount:     1,
   625  			expectedRequested: []string{"al"},
   626  		},
   627  		{
   628  			name:              "draft is ready for review, ignoreDrafts true, assign review to PR",
   629  			action:            github.PullRequestActionReadyForReview,
   630  			filesChanged:      []string{"a.go"},
   631  			reviewerCount:     1,
   632  			expectedRequested: []string{"al"},
   633  		},
   634  		{
   635  			name:          "PR opened by ignored author, do not assign review to PR",
   636  			action:        github.PullRequestActionOpened,
   637  			filesChanged:  []string{"a.go"},
   638  			ignoreAuthors: []string{"author"},
   639  		},
   640  	}
   641  	for _, tc := range testcases {
   642  		t.Run(tc.name, func(t *testing.T) {
   643  			pr := github.PullRequest{Number: 5, User: github.User{Login: "author"}, Body: tc.body, Draft: tc.draft}
   644  			repo := github.Repo{Owner: github.User{Login: "org"}, Name: "repo"}
   645  			fghc := newFakeGitHubClient(&pr, tc.filesChanged)
   646  			c := plugins.Blunderbuss{
   647  				ReviewerCount:    &tc.reviewerCount,
   648  				MaxReviewerCount: 0,
   649  				ExcludeApprovers: false,
   650  				IgnoreDrafts:     tc.ignoreDrafts,
   651  				IgnoreAuthors:    tc.ignoreAuthors,
   652  			}
   653  
   654  			if err := handlePullRequest(
   655  				fghc, froc, logrus.WithField("plugin", PluginName),
   656  				c, tc.action, &pr, &repo,
   657  			); err != nil {
   658  				t.Fatalf("unexpected error from handle: %v", err)
   659  			}
   660  
   661  			sort.Strings(fghc.requested)
   662  			sort.Strings(tc.expectedRequested)
   663  			if !reflect.DeepEqual(fghc.requested, tc.expectedRequested) {
   664  				t.Fatalf("expected the requested reviewers to be %q, but got %q.", tc.expectedRequested, fghc.requested)
   665  			}
   666  		})
   667  	}
   668  }
   669  
   670  func TestHandleGenericComment(t *testing.T) {
   671  	froc := &fakeRepoownersClient{
   672  		foc: &fakeOwnersClient{
   673  			owners: map[string]string{
   674  				"a.go": "1",
   675  			},
   676  			leafReviewers: map[string]sets.Set[string]{
   677  				"a.go": sets.New[string]("al"),
   678  			},
   679  		},
   680  	}
   681  
   682  	var testcases = []struct {
   683  		name              string
   684  		action            github.GenericCommentEventAction
   685  		issueState        string
   686  		isPR              bool
   687  		body              string
   688  		filesChanged      []string
   689  		reviewerCount     int
   690  		expectedRequested []string
   691  	}{
   692  		{
   693  			name:              "comment with a valid command in an open PR triggers auto-assignment",
   694  			action:            github.GenericCommentActionCreated,
   695  			issueState:        "open",
   696  			isPR:              true,
   697  			body:              "/auto-cc",
   698  			filesChanged:      []string{"a.go"},
   699  			reviewerCount:     1,
   700  			expectedRequested: []string{"al"},
   701  		},
   702  		{
   703  			name:          "comment with an invalid command in an open PR will not trigger auto-assignment",
   704  			action:        github.GenericCommentActionCreated,
   705  			issueState:    "open",
   706  			isPR:          true,
   707  			body:          "/automatic-review",
   708  			filesChanged:  []string{"a.go"},
   709  			reviewerCount: 1,
   710  		},
   711  		{
   712  			name:          "comment with a valid command in a closed PR will not trigger auto-assignment",
   713  			action:        github.GenericCommentActionCreated,
   714  			issueState:    "closed",
   715  			isPR:          true,
   716  			body:          "/auto-cc",
   717  			filesChanged:  []string{"a.go"},
   718  			reviewerCount: 1,
   719  		},
   720  		{
   721  			name:          "comment deleted from an open PR will not trigger auto-assignment",
   722  			action:        github.GenericCommentActionDeleted,
   723  			issueState:    "open",
   724  			isPR:          true,
   725  			body:          "/auto-cc",
   726  			filesChanged:  []string{"a.go"},
   727  			reviewerCount: 1,
   728  		},
   729  		{
   730  			name:          "comment with valid command in an open issue will not trigger auto-assignment",
   731  			action:        github.GenericCommentActionCreated,
   732  			issueState:    "open",
   733  			isPR:          false,
   734  			body:          "/auto-cc",
   735  			reviewerCount: 1,
   736  		},
   737  	}
   738  	for _, tc := range testcases {
   739  		t.Run(tc.name, func(t *testing.T) {
   740  			pr := github.PullRequest{Number: 5, User: github.User{Login: "author"}}
   741  			fghc := newFakeGitHubClient(&pr, tc.filesChanged)
   742  			repo := github.Repo{Owner: github.User{Login: "org"}, Name: "repo"}
   743  			config := plugins.Blunderbuss{
   744  				ReviewerCount:    &tc.reviewerCount,
   745  				MaxReviewerCount: 0,
   746  				ExcludeApprovers: false,
   747  			}
   748  
   749  			if err := handleGenericComment(
   750  				fghc, froc, logrus.WithField("plugin", PluginName), config,
   751  				tc.action, tc.isPR, pr.Number, tc.issueState, &repo, tc.body,
   752  			); err != nil {
   753  				t.Fatalf("unexpected error from handle: %v", err)
   754  			}
   755  
   756  			sort.Strings(fghc.requested)
   757  			sort.Strings(tc.expectedRequested)
   758  			if !reflect.DeepEqual(fghc.requested, tc.expectedRequested) {
   759  				t.Fatalf("expected the requested reviewers to be %q, but got %q.", tc.expectedRequested, fghc.requested)
   760  			}
   761  		})
   762  	}
   763  }
   764  
   765  func TestHandleGenericCommentEvent(t *testing.T) {
   766  	pc := plugins.Agent{
   767  		PluginConfig: &plugins.Configuration{},
   768  	}
   769  	ce := github.GenericCommentEvent{}
   770  	handleGenericCommentEvent(pc, ce)
   771  }
   772  
   773  func TestHandlePullRequestEvent(t *testing.T) {
   774  	pc := plugins.Agent{
   775  		PluginConfig: &plugins.Configuration{},
   776  	}
   777  	pre := github.PullRequestEvent{}
   778  	handlePullRequestEvent(pc, pre)
   779  }
   780  
   781  func TestHelpProvider(t *testing.T) {
   782  	enabledRepos := []config.OrgRepo{
   783  		{Org: "org1", Repo: "repo"},
   784  		{Org: "org2", Repo: "repo"},
   785  	}
   786  	cases := []struct {
   787  		name               string
   788  		config             *plugins.Configuration
   789  		enabledRepos       []config.OrgRepo
   790  		err                bool
   791  		configInfoIncludes []string
   792  	}{
   793  		{
   794  			name:               "Empty config",
   795  			config:             &plugins.Configuration{},
   796  			enabledRepos:       enabledRepos,
   797  			configInfoIncludes: []string{configString(0)},
   798  		},
   799  		{
   800  			name: "ReviewerCount specified",
   801  			config: &plugins.Configuration{
   802  				Blunderbuss: plugins.Blunderbuss{
   803  					ReviewerCount: &[]int{2}[0],
   804  				},
   805  			},
   806  			enabledRepos:       enabledRepos,
   807  			configInfoIncludes: []string{configString(2)},
   808  		},
   809  	}
   810  	for _, c := range cases {
   811  		t.Run(c.name, func(t *testing.T) {
   812  			pluginHelp, err := helpProvider(c.config, c.enabledRepos)
   813  			if err != nil && !c.err {
   814  				t.Fatalf("helpProvider error: %v", err)
   815  			}
   816  			for _, msg := range c.configInfoIncludes {
   817  				if !strings.Contains(pluginHelp.Config[""], msg) {
   818  					t.Fatalf("helpProvider.Config error mismatch: didn't get %v, but wanted it", msg)
   819  				}
   820  			}
   821  		})
   822  	}
   823  }
   824  
   825  // TestPopActiveReviewer checks to ensure that no matter how hard we try, we
   826  // never assign a user that has their availability marked as busy.
   827  func TestPopActiveReviewer(t *testing.T) {
   828  	froc := &fakeRepoownersClient{
   829  		foc: &fakeOwnersClient{
   830  			owners: map[string]string{
   831  				"a.go":  "1",
   832  				"b.go":  "2",
   833  				"bb.go": "3",
   834  				"c.go":  "4",
   835  			},
   836  			approvers: map[string]layeredsets.String{
   837  				"a.go": layeredsets.NewString("alice"),
   838  				"b.go": layeredsets.NewString("brad"),
   839  				"c.go": layeredsets.NewString("busy-user"),
   840  			},
   841  			leafApprovers: map[string]sets.Set[string]{
   842  				"a.go": sets.New[string]("alice"),
   843  				"b.go": sets.New[string]("brad"),
   844  				"c.go": sets.New[string]("busy-user"),
   845  			},
   846  			reviewers: map[string]layeredsets.String{
   847  				"a.go": layeredsets.NewString("alice"),
   848  				"b.go": layeredsets.NewString("brad"),
   849  				"c.go": layeredsets.NewString("busy-user"),
   850  			},
   851  			leafReviewers: map[string]sets.Set[string]{
   852  				"a.go": sets.New[string]("alice"),
   853  				"b.go": sets.New[string]("brad"),
   854  				"c.go": sets.New[string]("busy-user"),
   855  			},
   856  		},
   857  	}
   858  
   859  	var testcases = []struct {
   860  		name                       string
   861  		filesChanged               []string
   862  		reviewerCount              int
   863  		maxReviewerCount           int
   864  		expectedRequested          []string
   865  		alternateExpectedRequested []string
   866  	}{
   867  		{
   868  			name:              "request three reviewers, only receive two, never get the busy user",
   869  			filesChanged:      []string{"a.go", "b.go", "c.go"},
   870  			reviewerCount:     3,
   871  			expectedRequested: []string{"alice", "brad"},
   872  		},
   873  	}
   874  	for _, tc := range testcases {
   875  		pr := github.PullRequest{Number: 5, User: github.User{Login: "author"}}
   876  		repo := github.Repo{Owner: github.User{Login: "org"}, Name: "repo"}
   877  		fghc := newFakeGitHubClient(&pr, tc.filesChanged)
   878  		if err := handle(
   879  			fghc, froc, logrus.WithField("plugin", PluginName),
   880  			&tc.reviewerCount, tc.maxReviewerCount, false, true, &repo, &pr,
   881  		); err != nil {
   882  			t.Errorf("[%s] unexpected error from handle: %v", tc.name, err)
   883  			continue
   884  		}
   885  
   886  		sort.Strings(fghc.requested)
   887  		sort.Strings(tc.expectedRequested)
   888  		sort.Strings(tc.alternateExpectedRequested)
   889  		if !reflect.DeepEqual(fghc.requested, tc.expectedRequested) {
   890  			if len(tc.alternateExpectedRequested) > 0 {
   891  				if !reflect.DeepEqual(fghc.requested, tc.alternateExpectedRequested) {
   892  					t.Errorf("[%s] expected the requested reviewers to be %q or %q, but got %q.", tc.name, tc.expectedRequested, tc.alternateExpectedRequested, fghc.requested)
   893  				}
   894  				continue
   895  			}
   896  			t.Errorf("[%s] expected the requested reviewers to be %q, but got %q.", tc.name, tc.expectedRequested, fghc.requested)
   897  		}
   898  	}
   899  }