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 }