github.com/shashidharatd/test-infra@v0.0.0-20171006011030-71304e1ca560/robots/issue-creator/creator/creator_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 creator
    18  
    19  import (
    20  	"bytes"
    21  	"errors"
    22  	"fmt"
    23  	"reflect"
    24  	"sort"
    25  	"strings"
    26  	"testing"
    27  
    28  	"k8s.io/test-infra/robots/issue-creator/testowner"
    29  
    30  	"github.com/google/go-github/github"
    31  )
    32  
    33  // fakeClient implements the RepoClient interface in order to be substituted for a
    34  // ghclient.Client github client when creating an IssueCreator.
    35  type fakeClient struct {
    36  	userName   string
    37  	repoLabels []string
    38  	issues     []*github.Issue
    39  	org        string
    40  	project    string
    41  	t          *testing.T
    42  }
    43  
    44  func (c *fakeClient) GetUser(login string) (*github.User, error) {
    45  	if login == "" {
    46  		return &github.User{Login: &c.userName}, nil
    47  	}
    48  	return nil, fmt.Errorf("Fake Client is only able to retrieve the current authenticated user in its current state.")
    49  }
    50  
    51  func (c *fakeClient) GetRepoLabels(org, repo string) ([]*github.Label, error) {
    52  	return makeLabelSlice(c.repoLabels), nil
    53  }
    54  
    55  func (c *fakeClient) GetIssues(org, repo string, options *github.IssueListByRepoOptions) ([]*github.Issue, error) {
    56  	return c.issues, nil
    57  }
    58  
    59  func (c *fakeClient) CreateIssue(org, repo string, title, body string, labels, owners []string) (*github.Issue, error) {
    60  	// Check if labels are valid.
    61  	for _, label := range labels {
    62  		found := false
    63  		for _, validLabel := range c.repoLabels {
    64  			if validLabel == label {
    65  				found = true
    66  				break
    67  			}
    68  		}
    69  		if !found {
    70  			c.t.Errorf("%s is not a valid label!\n", label)
    71  		}
    72  	}
    73  
    74  	issue := makeTestIssue(title, body, "open", labels, owners, len(c.issues))
    75  
    76  	c.issues = append(c.issues, issue)
    77  	return issue, nil
    78  }
    79  
    80  func (c *fakeClient) GetCollaborators(org, repo string) ([]*github.User, error) {
    81  	return nil, errors.New("some error (allow all assignees)")
    82  }
    83  
    84  // Verify checks that exactly 1 issue in c.issues matches the parameters and that no
    85  // issues in c.issues have an empty body string (since that means they shouldn't have been created).
    86  func (c *fakeClient) Verify(title, body string, owners, labels []string) bool {
    87  	matchCount := 0
    88  	for _, issue := range c.issues {
    89  		if *issue.Title != title || *issue.Body != body {
    90  			continue
    91  		}
    92  		// Verify that owners matches Assignees.
    93  		assignees := make([]string, len(issue.Assignees))
    94  		for i := 0; i < len(issue.Assignees); i++ {
    95  			assignees[i] = *issue.Assignees[i].Login
    96  		}
    97  		if !stringSlicesEqual(assignees, owners) {
    98  			continue
    99  		}
   100  		// Verify that labels matches issue.Labels.
   101  		issueLabels := make([]string, len(issue.Labels))
   102  		for i := 0; i < len(issue.Labels); i++ {
   103  			issueLabels[i] = *issue.Labels[i].Name
   104  		}
   105  		if !stringSlicesEqual(issueLabels, labels) {
   106  			continue
   107  		}
   108  		matchCount++
   109  	}
   110  	return matchCount == 1
   111  }
   112  
   113  type fakeIssue struct {
   114  	title, body, id string
   115  	labels, owners  []string
   116  	priority        string // A value of "" indicates no priority is set.
   117  }
   118  
   119  func (i *fakeIssue) Title() string {
   120  	return i.title
   121  }
   122  
   123  func (i *fakeIssue) Body(closed []*github.Issue) string {
   124  	// the functionality to check that there are no recently closed issues on github for a cluster is
   125  	// part of the TriageFiler code and is tested in triage-filer_test.go
   126  	// we ignore the param here
   127  	return i.body
   128  }
   129  
   130  func (i *fakeIssue) ID() string {
   131  	return i.id
   132  }
   133  
   134  func (i *fakeIssue) Labels() []string {
   135  	return i.labels
   136  }
   137  
   138  func (i *fakeIssue) Owners() []string {
   139  	return i.owners
   140  }
   141  
   142  func (i *fakeIssue) Priority() (string, bool) {
   143  	if i.priority == "" {
   144  		return "", false
   145  	}
   146  	return i.priority, true
   147  }
   148  
   149  func TestIssueCreator(t *testing.T) {
   150  
   151  	i1 := &fakeIssue{
   152  		title:    "title1",
   153  		body:     "body<ID1>",
   154  		id:       "<ID1>",
   155  		labels:   []string{"kind/flake"},
   156  		owners:   []string{},
   157  		priority: "",
   158  	}
   159  
   160  	c := &fakeClient{
   161  		t:          t,
   162  		userName:   "BOT_USERNAME",
   163  		org:        "MY_ORG",
   164  		project:    "MY_PROJ",
   165  		repoLabels: []string{"kind/flake", "kind/flakeypastry", "priority/P0"},
   166  		issues: []*github.Issue{
   167  			makeTestIssue(i1.title, i1.body, "open", i1.labels, i1.owners, 0),
   168  		},
   169  	}
   170  	creator := &IssueCreator{
   171  		client: c,
   172  	}
   173  	if err := creator.loadCache(); err != nil {
   174  		t.Fatalf("IssueCreator failed to load data from github while initing: %v", err)
   175  	}
   176  
   177  	// Test that an issue can be created normally.
   178  	i0 := &fakeIssue{
   179  		title:    "title0",
   180  		body:     "body<ID0>moarbody",
   181  		id:       "<ID0>",
   182  		labels:   []string{"kind/flake"},
   183  		owners:   []string{"user0"},
   184  		priority: "",
   185  	}
   186  	creator.sync(i0)
   187  	if !c.Verify(i0.title, i0.body, i0.owners, i0.labels) {
   188  		t.Errorf("Failed to do a simple sync of i0\n")
   189  	}
   190  
   191  	// Test that issues can't be double synced.
   192  	origLen := len(c.issues)
   193  	creator.sync(i1)
   194  	if len(c.issues) > origLen {
   195  		t.Errorf("Second sync of i1 created a duplicate issue!\n")
   196  	}
   197  	if !c.Verify(i1.title, i1.body, i1.owners, i1.labels) {
   198  		t.Errorf("Second sync of i1 was not idempotent.\n")
   199  	}
   200  
   201  	// Test that issues with empty bodies dont get synced.
   202  	i2 := &fakeIssue{
   203  		title:    "title2",
   204  		body:     "", // Indicates issue should not be synced.
   205  		id:       "<ID2>",
   206  		labels:   []string{"kind/flake"},
   207  		owners:   []string{"user2"},
   208  		priority: "",
   209  	}
   210  	origLen = len(c.issues)
   211  	creator.sync(i2)
   212  	if len(c.issues) > origLen {
   213  		t.Errorf("sync of i2 with empty body should not have created issue!\n")
   214  	}
   215  
   216  	// Test that invalid labels are not synced.
   217  	i3 := &fakeIssue{
   218  		title:    "title3",
   219  		body:     "body\\@^*<ID3>\\moarbody",
   220  		id:       "<ID3>",
   221  		labels:   []string{"kind/flake", "label/wannabe"},
   222  		owners:   []string{"user3"},
   223  		priority: "",
   224  	}
   225  	creator.sync(i3)
   226  	if !c.Verify(i3.title, i3.body, i3.owners, []string{"kind/flake"}) {
   227  		t.Errorf("sync of i3 was invalid. The label 'label/wannabe' should not be added to the new issue.\n")
   228  	}
   229  
   230  	// Test that DryRun prevents issue creation.
   231  	creator.dryRun = true
   232  	i4 := &fakeIssue{
   233  		title:    "title4",
   234  		body:     "<ID4>thebody",
   235  		id:       "<ID4>",
   236  		labels:   []string{"kind/flake"},
   237  		owners:   []string{"user4"},
   238  		priority: "",
   239  	}
   240  	origLen = len(c.issues)
   241  	creator.sync(i4)
   242  	if len(c.issues) > origLen {
   243  		t.Errorf("sync of i4 with DryRun on should not have created issue!\n")
   244  	}
   245  
   246  	creator.dryRun = false
   247  
   248  	// Test that priority labels are created properly if an issue knows its priority.
   249  	i5 := &fakeIssue{
   250  		title:    "title5",
   251  		body:     "<ID5>thebody",
   252  		id:       "<ID5>",
   253  		labels:   []string{"kind/flake", "kind/flakeypastry"},
   254  		owners:   []string{"user5", "user1"}, // Test multiple users and labels here too.
   255  		priority: "P0",
   256  	}
   257  	creator.sync(i5)
   258  	if !c.Verify(i5.title, i5.body, i5.owners, []string{"kind/flake", "kind/flakeypastry", "priority/P0"}) {
   259  		t.Errorf("sync of i5 was invalid. The labels in the created issue were incorrect.\n")
   260  	}
   261  }
   262  
   263  func makeTestIssue(title, body, state string, labels, owners []string, number int) *github.Issue {
   264  	return &github.Issue{
   265  		Title:     &title,
   266  		Body:      &body,
   267  		State:     &state,
   268  		Number:    &number,
   269  		Assignees: makeUserSlice(owners),
   270  		Labels:    makeLabelSliceNoPtr(labels),
   271  	}
   272  }
   273  
   274  func makeLabelSlice(strs []string) []*github.Label {
   275  	result := make([]*github.Label, len(strs))
   276  	for i := 0; i < len(strs); i++ {
   277  		result[i] = &github.Label{Name: &strs[i]}
   278  	}
   279  	return result
   280  }
   281  
   282  func makeLabelSliceNoPtr(strs []string) []github.Label {
   283  	result := make([]github.Label, len(strs))
   284  	for i := 0; i < len(strs); i++ {
   285  		result[i] = github.Label{Name: &strs[i]}
   286  	}
   287  	return result
   288  }
   289  
   290  func makeUserSlice(strs []string) []*github.User {
   291  	result := make([]*github.User, len(strs))
   292  	for i := 0; i < len(strs); i++ {
   293  		result[i] = &github.User{Login: &strs[i]}
   294  	}
   295  	return result
   296  }
   297  
   298  func stringSlicesEqual(strs1, strs2 []string) bool {
   299  	sort.Strings(strs1)
   300  	sort.Strings(strs2)
   301  	return reflect.DeepEqual(strs1, strs2)
   302  }
   303  
   304  func TestOwnersSIGs(t *testing.T) {
   305  	sampleOwnerCSV := []byte(
   306  		`name,owner,auto-assigned,sig
   307  some test, cjwagner,0,sigarea2
   308  some test2, cjwagner, 1, sigarea3
   309  some test3, cjwagner, 0, sigarea4
   310  Sysctls should support sysctls,Random-Liu,1,node
   311  Sysctls should support unsafe sysctls which are actually whitelisted,deads2k,1,node
   312  testname1,cjwagner ,1,sigarea
   313  testname2,spxtr,1,sigarea
   314  ThirdParty resources Simple Third Party creating/deleting thirdparty objects works,luxas,1,api-machinery
   315  Upgrade cluster upgrade should maintain a functioning cluster,luxas,1,cluster-lifecycle
   316  Upgrade master upgrade should maintain a functioning cluster,xiang90,1,cluster-lifecycle`)
   317  
   318  	ownerlist, err := testowner.NewOwnerListFromCsv(bytes.NewReader(sampleOwnerCSV))
   319  	if err != nil {
   320  		t.Fatalf("Failed to init an OwnerList: %v\n", err)
   321  	}
   322  	c := &IssueCreator{
   323  		Collaborators: []string{"cjwagner", "spxtr"},
   324  		Owners:        ownerlist,
   325  		MaxAssignees:  3,
   326  		MaxSIGCount:   3,
   327  	}
   328  
   329  	cases := []struct {
   330  		tests        []string
   331  		owners, sigs map[string][]string
   332  	}{
   333  		{
   334  			tests:  []string{"testname1"},
   335  			owners: map[string][]string{"cjwagner": {"testname1"}},
   336  			sigs:   map[string][]string{"sigarea": {"testname1"}},
   337  		},
   338  		{
   339  			tests:  []string{"testname1", "testname2"},
   340  			owners: map[string][]string{"cjwagner": {"testname1"}, "spxtr": {"testname2"}},
   341  			sigs:   map[string][]string{"sigarea": {"testname1", "testname2"}},
   342  		},
   343  		{
   344  			tests:  []string{"testname1", "testname2", "some test"},
   345  			owners: map[string][]string{"cjwagner": {"testname1", "some test"}, "spxtr": {"testname2"}},
   346  			sigs:   map[string][]string{"sigarea": {"testname1", "testname2"}, "sigarea2": {"some test"}},
   347  		},
   348  		{
   349  			tests:  []string{"testname1", "some test", "some test2", "some_test3"},
   350  			owners: map[string][]string{"cjwagner": {"testname1", "some test", "some test2"}},
   351  			sigs:   map[string][]string{"sigarea": {"testname1"}, "sigarea2": {"some test"}, "sigarea3": {"some test2"}},
   352  		},
   353  	}
   354  	for _, test := range cases {
   355  		owners := c.TestsOwners(test.tests)
   356  		sigs := c.TestsSIGs(test.tests)
   357  		if !reflect.DeepEqual(owners, test.owners) {
   358  			t.Errorf("Expected owners map was %v but got %v\n", test.owners, owners)
   359  		}
   360  		if !reflect.DeepEqual(sigs, test.sigs) {
   361  			t.Errorf("Expected sigs map was %v but got %v\n", test.sigs, sigs)
   362  		}
   363  
   364  		table := c.ExplainTestAssignments(test.tests)
   365  		for owner, testNames := range owners {
   366  			row := fmt.Sprintf("| %s | %s |", owner, strings.Join(testNames, "; "))
   367  			if !strings.Contains(table, row) {
   368  				t.Errorf("Assignment explanation table is missing row: '%s'\n", row)
   369  			}
   370  		}
   371  		for sig, testNames := range sigs {
   372  			row := fmt.Sprintf("| sig/%s | %s |", sig, strings.Join(testNames, "; "))
   373  			if !strings.Contains(table, row) {
   374  				t.Errorf("Assignment explanation table is missing row: '%s'\n", row)
   375  			}
   376  		}
   377  	}
   378  }