| // Copyright 2018 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package main |
| |
| import ( |
| "context" |
| "net/http" |
| "testing" |
| "time" |
| |
| "github.com/google/go-cmp/cmp" |
| "github.com/google/go-cmp/cmp/cmpopts" |
| "github.com/google/go-github/github" |
| "golang.org/x/build/devapp/owners" |
| "golang.org/x/build/maintner" |
| ) |
| |
| func TestLabelCommandsFromComments(t *testing.T) { |
| created := time.Now() |
| testCases := []struct { |
| desc string |
| body string |
| cmds []labelCommand |
| }{ |
| { |
| "basic add/remove", |
| "We should fix this issue, but we need help\n\n@gopherbot please add help wanted, needsfix and remove needsinvestigation", |
| []labelCommand{ |
| {action: "add", label: "help wanted", created: created}, |
| {action: "add", label: "needsfix", created: created}, |
| {action: "remove", label: "needsinvestigation", created: created}, |
| }, |
| }, |
| { |
| "no please", |
| "@gopherbot add NeedsFix", |
| []labelCommand{ |
| {action: "add", label: "needsfix", created: created}, |
| }, |
| }, |
| { |
| "with comma", |
| "@gopherbot, NeedsFix", |
| []labelCommand{ |
| {action: "add", label: "needsfix", created: created}, |
| }, |
| }, |
| { |
| "with semicolons", |
| "@gopherbot NeedsFix;help wanted; remove needsinvestigation", |
| []labelCommand{ |
| {action: "add", label: "needsfix", created: created}, |
| {action: "add", label: "help wanted", created: created}, |
| {action: "remove", label: "needsinvestigation", created: created}, |
| }, |
| }, |
| { |
| "case insensitive", |
| "@gopherbot please add HelP WanteD", |
| []labelCommand{ |
| {action: "add", label: "help wanted", created: created}, |
| }, |
| }, |
| { |
| "fun input", |
| "@gopherbot please add help wanted,;needsfix;", |
| []labelCommand{ |
| {action: "add", label: "help wanted", created: created}, |
| {action: "add", label: "needsfix", created: created}, |
| }, |
| }, |
| { |
| "with hyphen", |
| "@gopherbot please add label OS-macOS", |
| []labelCommand{ |
| {action: "add", label: "os-macos", created: created}, |
| }, |
| }, |
| { |
| "unlabel keyword", |
| "@gopherbot please unlabel needsinvestigation, NeedsDecision", |
| []labelCommand{ |
| {action: "remove", label: "needsinvestigation", created: created}, |
| {action: "remove", label: "needsdecision", created: created}, |
| }, |
| }, |
| { |
| "with label[s] keyword", |
| "@gopherbot please add label help wanted and remove labels needsinvestigation, NeedsDecision", |
| []labelCommand{ |
| {action: "add", label: "help wanted", created: created}, |
| {action: "remove", label: "needsinvestigation", created: created}, |
| {action: "remove", label: "needsdecision", created: created}, |
| }, |
| }, |
| { |
| "no label commands", |
| "The cake was a lie", |
| nil, |
| }, |
| } |
| for _, tc := range testCases { |
| cmds := labelCommandsFromBody(tc.body, created) |
| if diff := cmp.Diff(cmds, tc.cmds, cmp.AllowUnexported(labelCommand{})); diff != "" { |
| t.Errorf("%s: commands differ: (-got +want)\n%s", tc.desc, diff) |
| } |
| } |
| } |
| |
| func TestLabelMutations(t *testing.T) { |
| testCases := []struct { |
| desc string |
| cmds []labelCommand |
| add []string |
| remove []string |
| }{ |
| { |
| "basic", |
| []labelCommand{ |
| {action: "add", label: "foo"}, |
| {action: "remove", label: "baz"}, |
| }, |
| []string{"foo"}, |
| []string{"baz"}, |
| }, |
| { |
| "add/remove of same label", |
| []labelCommand{ |
| {action: "add", label: "foo"}, |
| {action: "remove", label: "foo"}, |
| {action: "remove", label: "bar"}, |
| {action: "add", label: "bar"}, |
| }, |
| nil, |
| nil, |
| }, |
| { |
| "deduplication of labels", |
| []labelCommand{ |
| {action: "add", label: "foo"}, |
| {action: "add", label: "foo"}, |
| {action: "remove", label: "bar"}, |
| {action: "remove", label: "bar"}, |
| }, |
| []string{"foo"}, |
| []string{"bar"}, |
| }, |
| { |
| "forbidden actions", |
| []labelCommand{ |
| {action: "add", label: "Proposal-Accepted"}, |
| {action: "add", label: "CherryPickApproved"}, |
| {action: "add", label: "cla: yes"}, |
| {action: "remove", label: "Security"}, |
| }, |
| nil, |
| nil, |
| }, |
| { |
| "can add Security", |
| []labelCommand{ |
| {action: "add", label: "Security"}, |
| }, |
| []string{"Security"}, |
| nil, |
| }, |
| } |
| for _, tc := range testCases { |
| add, remove := mutationsFromCommands(tc.cmds) |
| if diff := cmp.Diff(add, tc.add); diff != "" { |
| t.Errorf("%s: label additions differ: (-got, +want)\n%s", tc.desc, diff) |
| } |
| if diff := cmp.Diff(remove, tc.remove); diff != "" { |
| t.Errorf("%s: label removals differ: (-got, +want)\n%s", tc.desc, diff) |
| } |
| } |
| } |
| |
| type fakeIssuesService struct { |
| labels map[int][]string |
| } |
| |
| func (f *fakeIssuesService) ListLabelsByIssue(ctx context.Context, owner string, repo string, number int, opt *github.ListOptions) ([]*github.Label, *github.Response, error) { |
| var labels []*github.Label |
| if ls, ok := f.labels[number]; ok { |
| for _, l := range ls { |
| name := l |
| labels = append(labels, &github.Label{Name: &name}) |
| } |
| } |
| return labels, nil, nil |
| } |
| |
| func (f *fakeIssuesService) AddLabelsToIssue(ctx context.Context, owner string, repo string, number int, labels []string) ([]*github.Label, *github.Response, error) { |
| if f.labels == nil { |
| f.labels = map[int][]string{number: labels} |
| return nil, nil, nil |
| } |
| ls, ok := f.labels[number] |
| if !ok { |
| f.labels[number] = labels |
| return nil, nil, nil |
| } |
| for _, label := range labels { |
| var found bool |
| for _, l := range ls { |
| if l == label { |
| found = true |
| } |
| } |
| if found { |
| continue |
| } |
| f.labels[number] = append(f.labels[number], label) |
| } |
| return nil, nil, nil |
| } |
| |
| func (f *fakeIssuesService) RemoveLabelForIssue(ctx context.Context, owner string, repo string, number int, label string) (*github.Response, error) { |
| if ls, ok := f.labels[number]; ok { |
| for i, l := range ls { |
| if l == label { |
| f.labels[number] = append(f.labels[number][:i], f.labels[number][i+1:]...) |
| return nil, nil |
| } |
| } |
| } |
| // The GitHub API returns a NotFound error if the label did not exist. |
| return nil, &github.ErrorResponse{ |
| Response: &http.Response{ |
| Status: http.StatusText(http.StatusNotFound), |
| StatusCode: http.StatusNotFound, |
| }, |
| } |
| } |
| |
| func TestAddLabels(t *testing.T) { |
| testCases := []struct { |
| desc string |
| gi *maintner.GitHubIssue |
| labels []string |
| added []string |
| }{ |
| { |
| "basic add", |
| &maintner.GitHubIssue{}, |
| []string{"foo"}, |
| []string{"foo"}, |
| }, |
| { |
| "some labels already present in maintner", |
| &maintner.GitHubIssue{ |
| Labels: map[int64]*maintner.GitHubLabel{ |
| 0: {Name: "NeedsDecision"}, |
| }, |
| }, |
| []string{"foo", "NeedsDecision"}, |
| []string{"foo"}, |
| }, |
| { |
| "all labels already present in maintner", |
| &maintner.GitHubIssue{ |
| Labels: map[int64]*maintner.GitHubLabel{ |
| 0: {Name: "NeedsDecision"}, |
| }, |
| }, |
| []string{"NeedsDecision"}, |
| nil, |
| }, |
| } |
| |
| b := &gopherbot{} |
| for _, tc := range testCases { |
| // Clear any previous state from fake addLabelsToIssue since some test cases may skip calls to it. |
| fis := &fakeIssuesService{} |
| b.is = fis |
| |
| if err := b.addLabels(context.Background(), maintner.GitHubRepoID{ |
| Owner: "golang", |
| Repo: "go", |
| }, tc.gi, tc.labels); err != nil { |
| t.Errorf("%s: b.addLabels got unexpected error: %v", tc.desc, err) |
| continue |
| } |
| if diff := cmp.Diff(fis.labels[int(tc.gi.ID)], tc.added); diff != "" { |
| t.Errorf("%s: labels added differ: (-got, +want)\n%s", tc.desc, diff) |
| } |
| } |
| } |
| |
| func TestRemoveLabels(t *testing.T) { |
| testCases := []struct { |
| desc string |
| gi *maintner.GitHubIssue |
| ghLabels []string |
| toRemove []string |
| want []string |
| }{ |
| { |
| "basic remove", |
| &maintner.GitHubIssue{ |
| Number: 123, |
| Labels: map[int64]*maintner.GitHubLabel{ |
| 0: {Name: "NeedsFix"}, |
| 1: {Name: "help wanted"}, |
| }, |
| }, |
| []string{"NeedsFix", "help wanted"}, |
| []string{"NeedsFix"}, |
| []string{"help wanted"}, |
| }, |
| { |
| "label not present in maintner", |
| &maintner.GitHubIssue{}, |
| []string{"NeedsFix"}, |
| []string{"NeedsFix"}, |
| []string{"NeedsFix"}, |
| }, |
| { |
| "label not present in GitHub", |
| &maintner.GitHubIssue{ |
| Labels: map[int64]*maintner.GitHubLabel{ |
| 0: {Name: "foo"}, |
| }, |
| }, |
| []string{"NeedsFix"}, |
| []string{"foo"}, |
| []string{"NeedsFix"}, |
| }, |
| } |
| |
| b := &gopherbot{} |
| for _, tc := range testCases { |
| // Clear any previous state from fakeIssuesService since some test cases may skip calls to it. |
| fis := &fakeIssuesService{map[int][]string{ |
| int(tc.gi.Number): tc.ghLabels, |
| }} |
| b.is = fis |
| |
| if err := b.removeLabels(context.Background(), maintner.GitHubRepoID{ |
| Owner: "golang", |
| Repo: "go", |
| }, tc.gi, tc.toRemove); err != nil { |
| t.Errorf("%s: b.addLabels got unexpected error: %v", tc.desc, err) |
| continue |
| } |
| if diff := cmp.Diff(fis.labels[int(tc.gi.Number)], tc.want); diff != "" { |
| t.Errorf("%s: labels differ: (-got, +want)\n%s", tc.desc, diff) |
| } |
| } |
| } |
| |
| func TestHumanReviewersInMetas(t *testing.T) { |
| testCases := []struct { |
| commitMsg string |
| hasHuman bool |
| atLeast int |
| wantIDs []string |
| }{ |
| {`Patch-set: 6 |
| Reviewer: Andrew Bonventre <22285@62eb7196-b449-3ce5-99f1-c037f21e1705> |
| `, |
| true, |
| 1, |
| []string{"22285"}, |
| }, |
| {`Patch-set: 6 |
| CC: Andrew Bonventre <22285@62eb7196-b449-3ce5-99f1-c037f21e1705> |
| `, |
| true, |
| 1, |
| []string{"22285"}, |
| }, |
| {`Patch-set: 6 |
| Reviewer: Gobot Gobot <5976@62eb7196-b449-3ce5-99f1-c037f21e1705> |
| `, |
| false, |
| 1, |
| []string{}, |
| }, |
| {`Patch-set: 6 |
| Reviewer: Gobot Gobot <5976@62eb7196-b449-3ce5-99f1-c037f21e1705> |
| CC: Andrew Bonventre <22285@62eb7196-b449-3ce5-99f1-c037f21e1705> |
| `, |
| true, |
| 1, |
| []string{"22285"}, |
| }, |
| {`Patch-set: 6 |
| Reviewer: Gobot Gobot <5976@62eb7196-b449-3ce5-99f1-c037f21e1705> |
| Reviewer: Andrew Bonventre <22285@62eb7196-b449-3ce5-99f1-c037f21e1705> |
| `, |
| true, |
| 1, |
| []string{"22285"}, |
| }, |
| {`Patch-set: 6 |
| Reviewer: Gobot Gobot <5976@62eb7196-b449-3ce5-99f1-c037f21e1705> |
| Reviewer: Andrew Bonventre <22285@62eb7196-b449-3ce5-99f1-c037f21e1705> |
| `, |
| false, |
| 2, |
| []string{"22285"}, |
| }, |
| {`Patch-set: 6 |
| Reviewer: Gobot Gobot <5976@62eb7196-b449-3ce5-99f1-c037f21e1705> |
| Reviewer: Andrew Bonventre <22285@62eb7196-b449-3ce5-99f1-c037f21e1705> |
| Reviewer: Rebecca Stambler <16140@62eb7196-b449-3ce5-99f1-c037f21e1705> |
| `, |
| true, |
| 2, |
| []string{"22285", "16140"}, |
| }, |
| } |
| |
| for _, tc := range testCases { |
| metas := []*maintner.GerritMeta{ |
| {Commit: &maintner.GitCommit{Msg: tc.commitMsg}}, |
| } |
| ids, got := humanReviewersInMetas(metas, tc.atLeast) |
| if want := tc.hasHuman; got != want { |
| t.Errorf("Unexpected result for meta commit message: got %v; want %v for\n%s", got, want, tc.commitMsg) |
| continue |
| } |
| if len(ids) != len(tc.wantIDs) { |
| t.Errorf("Unexpected result for meta commit message: got %v reviewer IDs, want %v", len(ids), len(tc.wantIDs)) |
| continue |
| } |
| for i, id := range tc.wantIDs { |
| if id != ids[i] { |
| t.Errorf("Unexpected ID at %d: got %v, want %v", i, ids[i], id) |
| } |
| } |
| } |
| } |
| |
| func TestMergeOwnersEntries(t *testing.T) { |
| var ( |
| andybons = owners.Owner{GitHubUsername: "andybons", GerritEmail: "andybons@golang.org"} |
| bradfitz = owners.Owner{GitHubUsername: "bradfitz", GerritEmail: "bradfitz@golang.org"} |
| filippo = owners.Owner{GitHubUsername: "filippo", GerritEmail: "filippo@golang.org"} |
| ) |
| testCases := []struct { |
| desc string |
| entries []*owners.Entry |
| authorEmail string |
| result *owners.Entry |
| }{ |
| { |
| "no entries", |
| nil, |
| "", |
| &owners.Entry{}, |
| }, |
| { |
| "primary merge", |
| []*owners.Entry{ |
| {Primary: []owners.Owner{andybons}}, |
| {Primary: []owners.Owner{bradfitz}}, |
| }, |
| "", |
| &owners.Entry{ |
| Primary: []owners.Owner{andybons, bradfitz}, |
| }, |
| }, |
| { |
| "secondary merge", |
| []*owners.Entry{ |
| {Secondary: []owners.Owner{andybons}}, |
| {Secondary: []owners.Owner{filippo}}, |
| }, |
| "", |
| &owners.Entry{ |
| Secondary: []owners.Owner{andybons, filippo}, |
| }, |
| }, |
| { |
| "promote from secondary to primary", |
| []*owners.Entry{ |
| {Primary: []owners.Owner{andybons, filippo}}, |
| {Secondary: []owners.Owner{filippo}}, |
| }, |
| "", |
| &owners.Entry{ |
| Primary: []owners.Owner{andybons, filippo}, |
| }, |
| }, |
| { |
| "primary filter", |
| []*owners.Entry{ |
| {Primary: []owners.Owner{filippo, andybons}}, |
| }, |
| filippo.GerritEmail, |
| &owners.Entry{ |
| Primary: []owners.Owner{andybons}, |
| }, |
| }, |
| { |
| "secondary filter", |
| []*owners.Entry{ |
| {Secondary: []owners.Owner{filippo, andybons}}, |
| }, |
| filippo.GerritEmail, |
| &owners.Entry{ |
| Secondary: []owners.Owner{andybons}, |
| }, |
| }, |
| } |
| cmpFn := func(a, b owners.Owner) bool { |
| return a.GitHubUsername < b.GitHubUsername |
| } |
| for _, tc := range testCases { |
| got := mergeOwnersEntries(tc.entries, tc.authorEmail) |
| if diff := cmp.Diff(got, tc.result, cmpopts.SortSlices(cmpFn)); diff != "" { |
| t.Errorf("%s: final entry results differ: (-got, +want)\n%s", tc.desc, diff) |
| } |
| } |
| } |