golang.org/x/build@v0.0.0-20240506185731-218518f32b70/cmd/gopherbot/gopherbot_test.go (about) 1 // Copyright 2018 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package main 6 7 import ( 8 "context" 9 "flag" 10 "net/http" 11 "testing" 12 "time" 13 14 "github.com/google/go-cmp/cmp" 15 "github.com/google/go-cmp/cmp/cmpopts" 16 "github.com/google/go-github/v48/github" 17 "golang.org/x/build/devapp/owners" 18 "golang.org/x/build/maintner" 19 ) 20 21 func TestLabelCommandsFromComments(t *testing.T) { 22 created := time.Now() 23 testCases := []struct { 24 desc string 25 body string 26 cmds []labelCommand 27 }{ 28 { 29 "basic add/remove", 30 "We should fix this issue, but we need help\n\n@gopherbot please add help wanted, needsfix and remove needsinvestigation", 31 []labelCommand{ 32 {action: "add", label: "help wanted", created: created}, 33 {action: "add", label: "needsfix", created: created}, 34 {action: "remove", label: "needsinvestigation", created: created}, 35 }, 36 }, 37 { 38 "no please", 39 "@gopherbot add NeedsFix", 40 []labelCommand{ 41 {action: "add", label: "needsfix", created: created}, 42 }, 43 }, 44 { 45 "with comma", 46 "@gopherbot, NeedsFix", 47 []labelCommand{ 48 {action: "add", label: "needsfix", created: created}, 49 }, 50 }, 51 { 52 "with semicolons", 53 "@gopherbot NeedsFix;help wanted; remove needsinvestigation", 54 []labelCommand{ 55 {action: "add", label: "needsfix", created: created}, 56 {action: "add", label: "help wanted", created: created}, 57 {action: "remove", label: "needsinvestigation", created: created}, 58 }, 59 }, 60 { 61 "case insensitive", 62 "@gopherbot please add HelP WanteD", 63 []labelCommand{ 64 {action: "add", label: "help wanted", created: created}, 65 }, 66 }, 67 { 68 "fun input", 69 "@gopherbot please add help wanted,;needsfix;", 70 []labelCommand{ 71 {action: "add", label: "help wanted", created: created}, 72 {action: "add", label: "needsfix", created: created}, 73 }, 74 }, 75 { 76 "with hyphen", 77 "@gopherbot please add label OS-macOS", 78 []labelCommand{ 79 {action: "add", label: "os-macos", created: created}, 80 }, 81 }, 82 { 83 "unlabel keyword", 84 "@gopherbot please unlabel needsinvestigation, NeedsDecision", 85 []labelCommand{ 86 {action: "remove", label: "needsinvestigation", created: created}, 87 {action: "remove", label: "needsdecision", created: created}, 88 }, 89 }, 90 { 91 "with label[s] keyword", 92 "@gopherbot please add label help wanted and remove labels needsinvestigation, NeedsDecision", 93 []labelCommand{ 94 {action: "add", label: "help wanted", created: created}, 95 {action: "remove", label: "needsinvestigation", created: created}, 96 {action: "remove", label: "needsdecision", created: created}, 97 }, 98 }, 99 { 100 "no label commands", 101 "The cake was a lie", 102 nil, 103 }, 104 } 105 for _, tc := range testCases { 106 cmds := labelCommandsFromBody(tc.body, created) 107 if diff := cmp.Diff(cmds, tc.cmds, cmp.AllowUnexported(labelCommand{})); diff != "" { 108 t.Errorf("%s: commands differ: (-got +want)\n%s", tc.desc, diff) 109 } 110 } 111 } 112 113 func TestLabelMutations(t *testing.T) { 114 testCases := []struct { 115 desc string 116 cmds []labelCommand 117 add []string 118 remove []string 119 }{ 120 { 121 "basic", 122 []labelCommand{ 123 {action: "add", label: "foo"}, 124 {action: "remove", label: "baz"}, 125 }, 126 []string{"foo"}, 127 []string{"baz"}, 128 }, 129 { 130 "add/remove of same label", 131 []labelCommand{ 132 {action: "add", label: "foo"}, 133 {action: "remove", label: "foo"}, 134 {action: "remove", label: "bar"}, 135 {action: "add", label: "bar"}, 136 }, 137 nil, 138 nil, 139 }, 140 { 141 "deduplication of labels", 142 []labelCommand{ 143 {action: "add", label: "foo"}, 144 {action: "add", label: "foo"}, 145 {action: "remove", label: "bar"}, 146 {action: "remove", label: "bar"}, 147 }, 148 []string{"foo"}, 149 []string{"bar"}, 150 }, 151 { 152 "forbidden actions", 153 []labelCommand{ 154 {action: "add", label: "Proposal-Accepted"}, 155 {action: "add", label: "CherryPickApproved"}, 156 {action: "add", label: "cla: yes"}, 157 {action: "remove", label: "Security"}, 158 }, 159 nil, 160 nil, 161 }, 162 { 163 "can add Security", 164 []labelCommand{ 165 {action: "add", label: "Security"}, 166 }, 167 []string{"Security"}, 168 nil, 169 }, 170 } 171 for _, tc := range testCases { 172 add, remove := mutationsFromCommands(tc.cmds) 173 if diff := cmp.Diff(add, tc.add); diff != "" { 174 t.Errorf("%s: label additions differ: (-got, +want)\n%s", tc.desc, diff) 175 } 176 if diff := cmp.Diff(remove, tc.remove); diff != "" { 177 t.Errorf("%s: label removals differ: (-got, +want)\n%s", tc.desc, diff) 178 } 179 } 180 } 181 182 type fakeIssuesService struct { 183 labels map[int][]string 184 } 185 186 func (f *fakeIssuesService) ListLabelsByIssue(ctx context.Context, owner string, repo string, number int, opt *github.ListOptions) ([]*github.Label, *github.Response, error) { 187 var labels []*github.Label 188 if ls, ok := f.labels[number]; ok { 189 for _, l := range ls { 190 name := l 191 labels = append(labels, &github.Label{Name: &name}) 192 } 193 } 194 return labels, nil, nil 195 } 196 197 func (f *fakeIssuesService) AddLabelsToIssue(ctx context.Context, owner string, repo string, number int, labels []string) ([]*github.Label, *github.Response, error) { 198 if f.labels == nil { 199 f.labels = map[int][]string{number: labels} 200 return nil, nil, nil 201 } 202 ls, ok := f.labels[number] 203 if !ok { 204 f.labels[number] = labels 205 return nil, nil, nil 206 } 207 for _, label := range labels { 208 var found bool 209 for _, l := range ls { 210 if l == label { 211 found = true 212 } 213 } 214 if found { 215 continue 216 } 217 f.labels[number] = append(f.labels[number], label) 218 } 219 return nil, nil, nil 220 } 221 222 func (f *fakeIssuesService) RemoveLabelForIssue(ctx context.Context, owner string, repo string, number int, label string) (*github.Response, error) { 223 if ls, ok := f.labels[number]; ok { 224 for i, l := range ls { 225 if l == label { 226 f.labels[number] = append(f.labels[number][:i], f.labels[number][i+1:]...) 227 return nil, nil 228 } 229 } 230 } 231 // The GitHub API returns a NotFound error if the label did not exist. 232 return nil, &github.ErrorResponse{ 233 Response: &http.Response{ 234 Status: http.StatusText(http.StatusNotFound), 235 StatusCode: http.StatusNotFound, 236 }, 237 } 238 } 239 240 func TestAddLabels(t *testing.T) { 241 testCases := []struct { 242 desc string 243 gi *maintner.GitHubIssue 244 labels []string 245 added []string 246 }{ 247 { 248 "basic add", 249 &maintner.GitHubIssue{}, 250 []string{"foo"}, 251 []string{"foo"}, 252 }, 253 { 254 "some labels already present in maintner", 255 &maintner.GitHubIssue{ 256 Labels: map[int64]*maintner.GitHubLabel{ 257 0: {Name: "NeedsDecision"}, 258 }, 259 }, 260 []string{"foo", "NeedsDecision"}, 261 []string{"foo"}, 262 }, 263 { 264 "all labels already present in maintner", 265 &maintner.GitHubIssue{ 266 Labels: map[int64]*maintner.GitHubLabel{ 267 0: {Name: "NeedsDecision"}, 268 }, 269 }, 270 []string{"NeedsDecision"}, 271 nil, 272 }, 273 } 274 275 b := &gopherbot{} 276 for _, tc := range testCases { 277 // Clear any previous state from fake addLabelsToIssue since some test cases may skip calls to it. 278 fis := &fakeIssuesService{} 279 b.is = fis 280 281 if err := b.addLabels(context.Background(), maintner.GitHubRepoID{ 282 Owner: "golang", 283 Repo: "go", 284 }, tc.gi, tc.labels); err != nil { 285 t.Errorf("%s: b.addLabels got unexpected error: %v", tc.desc, err) 286 continue 287 } 288 if diff := cmp.Diff(fis.labels[int(tc.gi.ID)], tc.added); diff != "" { 289 t.Errorf("%s: labels added differ: (-got, +want)\n%s", tc.desc, diff) 290 } 291 } 292 } 293 294 func TestRemoveLabels(t *testing.T) { 295 testCases := []struct { 296 desc string 297 gi *maintner.GitHubIssue 298 ghLabels []string 299 toRemove []string 300 want []string 301 }{ 302 { 303 "basic remove", 304 &maintner.GitHubIssue{ 305 Number: 123, 306 Labels: map[int64]*maintner.GitHubLabel{ 307 0: {Name: "NeedsFix"}, 308 1: {Name: "help wanted"}, 309 }, 310 }, 311 []string{"NeedsFix", "help wanted"}, 312 []string{"NeedsFix"}, 313 []string{"help wanted"}, 314 }, 315 { 316 "label not present in maintner", 317 &maintner.GitHubIssue{}, 318 []string{"NeedsFix"}, 319 []string{"NeedsFix"}, 320 []string{"NeedsFix"}, 321 }, 322 { 323 "label not present in GitHub", 324 &maintner.GitHubIssue{ 325 Labels: map[int64]*maintner.GitHubLabel{ 326 0: {Name: "foo"}, 327 }, 328 }, 329 []string{"NeedsFix"}, 330 []string{"foo"}, 331 []string{"NeedsFix"}, 332 }, 333 } 334 335 b := &gopherbot{} 336 for _, tc := range testCases { 337 // Clear any previous state from fakeIssuesService since some test cases may skip calls to it. 338 fis := &fakeIssuesService{map[int][]string{ 339 int(tc.gi.Number): tc.ghLabels, 340 }} 341 b.is = fis 342 343 if err := b.removeLabels(context.Background(), maintner.GitHubRepoID{ 344 Owner: "golang", 345 Repo: "go", 346 }, tc.gi, tc.toRemove); err != nil { 347 t.Errorf("%s: b.addLabels got unexpected error: %v", tc.desc, err) 348 continue 349 } 350 if diff := cmp.Diff(fis.labels[int(tc.gi.Number)], tc.want); diff != "" { 351 t.Errorf("%s: labels differ: (-got, +want)\n%s", tc.desc, diff) 352 } 353 } 354 } 355 356 func TestReviewersInMetas(t *testing.T) { 357 testCases := []struct { 358 desc string 359 commitMsg string 360 wantIDs []string 361 }{ 362 { 363 desc: "one human reviewer", 364 commitMsg: `Patch-set: 6 365 Reviewer: Andrew Bonventre <22285@62eb7196-b449-3ce5-99f1-c037f21e1705> 366 `, 367 wantIDs: []string{"22285"}, 368 }, 369 { 370 desc: "one human CC", 371 commitMsg: `Patch-set: 6 372 CC: Andrew Bonventre <22285@62eb7196-b449-3ce5-99f1-c037f21e1705> 373 `, 374 wantIDs: []string{"22285"}, 375 }, 376 { 377 desc: "gobot reviewer", 378 commitMsg: `Patch-set: 6 379 Reviewer: Gobot Gobot <5976@62eb7196-b449-3ce5-99f1-c037f21e1705> 380 `, 381 wantIDs: []string{"5976"}, 382 }, 383 { 384 desc: "gobot reviewer and human CC", 385 commitMsg: `Patch-set: 6 386 Reviewer: Gobot Gobot <5976@62eb7196-b449-3ce5-99f1-c037f21e1705> 387 CC: Andrew Bonventre <22285@62eb7196-b449-3ce5-99f1-c037f21e1705> 388 `, 389 wantIDs: []string{"5976", "22285"}, 390 }, 391 { 392 desc: "gobot reviewer and human reviewer", 393 commitMsg: `Patch-set: 6 394 Reviewer: Gobot Gobot <5976@62eb7196-b449-3ce5-99f1-c037f21e1705> 395 Reviewer: Andrew Bonventre <22285@62eb7196-b449-3ce5-99f1-c037f21e1705> 396 `, 397 wantIDs: []string{"5976", "22285"}, 398 }, 399 { 400 desc: "gobot reviewer and two human reviewers", 401 commitMsg: `Patch-set: 6 402 Reviewer: Gobot Gobot <5976@62eb7196-b449-3ce5-99f1-c037f21e1705> 403 Reviewer: Andrew Bonventre <22285@62eb7196-b449-3ce5-99f1-c037f21e1705> 404 Reviewer: Rebecca Stambler <16140@62eb7196-b449-3ce5-99f1-c037f21e1705> 405 `, 406 wantIDs: []string{"5976", "22285", "16140"}, 407 }, 408 { 409 desc: "reviewersInMetas should not return duplicate IDs", // Happened in go.dev/cl/534975. 410 commitMsg: `Reviewer: Gerrit User 5190 <5190@62eb7196-b449-3ce5-99f1-c037f21e1705> 411 CC: Gerrit User 60063 <60063@62eb7196-b449-3ce5-99f1-c037f21e1705> 412 Reviewer: Gerrit User 60063 <60063@62eb7196-b449-3ce5-99f1-c037f21e1705>`, 413 wantIDs: []string{"5190", "60063"}, 414 }, 415 } 416 417 cmpFn := func(a, b string) bool { 418 return a < b 419 } 420 for _, tc := range testCases { 421 t.Run(tc.desc, func(t *testing.T) { 422 metas := []*maintner.GerritMeta{ 423 {Commit: &maintner.GitCommit{Msg: tc.commitMsg}}, 424 } 425 ids := reviewersInMetas(metas) 426 if diff := cmp.Diff(tc.wantIDs, ids, cmpopts.SortSlices(cmpFn)); diff != "" { 427 t.Fatalf("reviewersInMetas() mismatch (-want +got):\n%s", diff) 428 } 429 }) 430 } 431 } 432 433 func TestMergeOwnersEntries(t *testing.T) { 434 var ( 435 andybons = owners.Owner{GitHubUsername: "andybons", GerritEmail: "andybons@golang.org"} 436 bradfitz = owners.Owner{GitHubUsername: "bradfitz", GerritEmail: "bradfitz@golang.org"} 437 filippo = owners.Owner{GitHubUsername: "filippo", GerritEmail: "filippo@golang.org"} 438 iant = owners.Owner{GitHubUsername: "iant", GerritEmail: "iant@golang.org"} 439 rsc = owners.Owner{GitHubUsername: "rsc", GerritEmail: "rsc@golang.org"} 440 ) 441 testCases := []struct { 442 desc string 443 entries []*owners.Entry 444 authorEmail string 445 result *owners.Entry 446 }{ 447 { 448 "no entries", 449 nil, 450 "", 451 &owners.Entry{}, 452 }, 453 { 454 "primary merge", 455 []*owners.Entry{ 456 {Primary: []owners.Owner{andybons}}, 457 {Primary: []owners.Owner{bradfitz}}, 458 }, 459 "", 460 &owners.Entry{ 461 Primary: []owners.Owner{andybons, bradfitz}, 462 }, 463 }, 464 { 465 "secondary merge", 466 []*owners.Entry{ 467 {Secondary: []owners.Owner{andybons}}, 468 {Secondary: []owners.Owner{filippo}}, 469 }, 470 "", 471 &owners.Entry{ 472 Secondary: []owners.Owner{andybons, filippo}, 473 }, 474 }, 475 { 476 "promote from secondary to primary", 477 []*owners.Entry{ 478 {Primary: []owners.Owner{andybons, filippo}}, 479 {Secondary: []owners.Owner{filippo}}, 480 }, 481 "", 482 &owners.Entry{ 483 Primary: []owners.Owner{andybons, filippo}, 484 }, 485 }, 486 { 487 "primary filter", 488 []*owners.Entry{ 489 {Primary: []owners.Owner{filippo, andybons}}, 490 }, 491 filippo.GerritEmail, 492 &owners.Entry{ 493 Primary: []owners.Owner{andybons}, 494 }, 495 }, 496 { 497 "secondary filter", 498 []*owners.Entry{ 499 {Secondary: []owners.Owner{filippo, andybons}}, 500 }, 501 filippo.GerritEmail, 502 &owners.Entry{ 503 Secondary: []owners.Owner{andybons}, 504 }, 505 }, 506 { 507 "too many reviewers", 508 []*owners.Entry{ 509 {Primary: []owners.Owner{iant, bradfitz}, Secondary: []owners.Owner{andybons}}, 510 {Primary: []owners.Owner{andybons}, Secondary: []owners.Owner{iant, bradfitz}}, 511 {Primary: []owners.Owner{iant, filippo}, Secondary: []owners.Owner{bradfitz, andybons, rsc}}, 512 }, 513 "", 514 &owners.Entry{ 515 Primary: []owners.Owner{andybons, bradfitz, iant}, 516 }, 517 }, 518 } 519 cmpFn := func(a, b owners.Owner) bool { 520 return a.GitHubUsername < b.GitHubUsername 521 } 522 for _, tc := range testCases { 523 got := mergeOwnersEntries(tc.entries, tc.authorEmail) 524 if diff := cmp.Diff(got, tc.result, cmpopts.SortSlices(cmpFn)); diff != "" { 525 t.Errorf("%s: final entry results differ: (-got, +want)\n%s", tc.desc, diff) 526 } 527 } 528 } 529 530 func TestFilterGerritOwners(t *testing.T) { 531 var ( 532 andybons = owners.Owner{GitHubUsername: "andybons", GerritEmail: "andybons@golang.org"} 533 bradfitz = owners.Owner{GitHubUsername: "bradfitz", GerritEmail: "bradfitz@golang.org"} 534 toolsTeam = owners.Owner{GitHubUsername: "golang/tools-team"} 535 ) 536 testCases := []struct { 537 name string 538 entries []*owners.Entry 539 want []*owners.Entry 540 }{ 541 { 542 name: "no entries", 543 entries: nil, 544 want: []*owners.Entry{}, 545 }, 546 { 547 name: "all valid", 548 entries: []*owners.Entry{ 549 {Primary: []owners.Owner{andybons}}, 550 {Primary: []owners.Owner{bradfitz}}, 551 }, 552 want: []*owners.Entry{ 553 {Primary: []owners.Owner{andybons}}, 554 {Primary: []owners.Owner{bradfitz}}, 555 }, 556 }, 557 { 558 name: "drop primary", 559 entries: []*owners.Entry{ 560 {Primary: []owners.Owner{andybons, toolsTeam}}, 561 {Primary: []owners.Owner{toolsTeam, bradfitz}}, 562 }, 563 want: []*owners.Entry{ 564 {Primary: []owners.Owner{andybons}}, 565 {Primary: []owners.Owner{bradfitz}}, 566 }, 567 }, 568 { 569 name: "drop secondary", 570 entries: []*owners.Entry{ 571 { 572 Primary: []owners.Owner{andybons}, 573 Secondary: []owners.Owner{bradfitz, toolsTeam}, 574 }, 575 { 576 Primary: []owners.Owner{bradfitz}, 577 Secondary: []owners.Owner{toolsTeam, andybons}, 578 }, 579 }, 580 want: []*owners.Entry{ 581 { 582 Primary: []owners.Owner{andybons}, 583 Secondary: []owners.Owner{bradfitz}, 584 }, 585 { 586 Primary: []owners.Owner{bradfitz}, 587 Secondary: []owners.Owner{andybons}, 588 }, 589 }, 590 }, 591 { 592 name: "upgrade secondary", 593 entries: []*owners.Entry{ 594 { 595 Primary: []owners.Owner{toolsTeam}, 596 Secondary: []owners.Owner{bradfitz}, 597 }, 598 }, 599 want: []*owners.Entry{ 600 { 601 Primary: []owners.Owner{bradfitz}, 602 }, 603 }, 604 }, 605 { 606 name: "no primary", 607 entries: []*owners.Entry{ 608 { 609 Secondary: []owners.Owner{bradfitz}, 610 }, 611 }, 612 want: []*owners.Entry{ 613 { 614 Primary: []owners.Owner{bradfitz}, 615 }, 616 }, 617 }, 618 } 619 cmpFn := func(a, b owners.Owner) bool { 620 return a.GitHubUsername < b.GitHubUsername 621 } 622 for _, tc := range testCases { 623 t.Run(tc.name, func(t *testing.T) { 624 got := filterGerritOwners(tc.entries) 625 if diff := cmp.Diff(got, tc.want, cmpopts.SortSlices(cmpFn)); diff != "" { 626 t.Errorf("final entry results differ: (-got, +want)\n%s", diff) 627 } 628 }) 629 } 630 } 631 632 func TestForeachIssue(t *testing.T) { 633 if testing.Short() || flag.Lookup("test.run").Value.String() != "^TestForeachIssue$" { 634 t.Skip("not running test requiring large Go corpus download in short mode and if not explicitly requested with go test -run=^TestForeachIssue$") 635 } 636 637 b := &gopherbot{} 638 b.initCorpus() 639 640 var num int 641 err := b.foreachIssue(b.gorepo, open, func(gi *maintner.GitHubIssue) error { 642 if gi.Closed || gi.PullRequest || gi.NotExist { 643 t.Errorf("issue %d should be skipped but isn't: %#v", gi.Number, gi) 644 } 645 num++ 646 return nil 647 }) 648 if err != nil { 649 t.Errorf("gopherbot.foreachIssue: got %v error, want nil", err) 650 } 651 t.Logf("gopherbot.foreachIssue walked over %d open issues (not including PRs and deleted/transferred/converted issues)", num) 652 653 var got struct { 654 Open, Closed, PR bool 655 } 656 err = b.foreachIssue(b.gorepo, open|closed|includePRs, func(gi *maintner.GitHubIssue) error { 657 if gi.NotExist { 658 t.Errorf("issue %d should be skipped but isn't: %#v", gi.Number, gi) 659 } 660 got.Open = got.Open || !gi.Closed 661 got.Closed = got.Closed || gi.Closed 662 got.PR = got.PR || gi.PullRequest 663 return nil 664 }) 665 if err != nil { 666 t.Errorf("gopherbot.foreachIssue: got %v error, want nil", err) 667 } 668 if !got.Open || !got.Closed || !got.PR { 669 t.Errorf("got %+v, want all true", got) 670 } 671 }