github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/tide/status_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 tide
    18  
    19  import (
    20  	"fmt"
    21  	"strings"
    22  	"testing"
    23  
    24  	githubql "github.com/shurcooL/githubv4"
    25  	"github.com/sirupsen/logrus"
    26  
    27  	"k8s.io/apimachinery/pkg/util/sets"
    28  	"k8s.io/test-infra/prow/config"
    29  	"k8s.io/test-infra/prow/github"
    30  )
    31  
    32  func TestExpectedStatus(t *testing.T) {
    33  	neededLabels := []string{"need-1", "need-2", "need-a-very-super-duper-extra-not-short-at-all-label-name"}
    34  	forbiddenLabels := []string{"forbidden-1", "forbidden-2"}
    35  	testcases := []struct {
    36  		name string
    37  
    38  		baseref         string
    39  		branchWhitelist []string
    40  		branchBlacklist []string
    41  		sameBranchReqs  bool
    42  		labels          []string
    43  		milestone       string
    44  		contexts        []Context
    45  		inPool          bool
    46  
    47  		state string
    48  		desc  string
    49  	}{
    50  		{
    51  			name:   "in pool",
    52  			inPool: true,
    53  
    54  			state: github.StatusSuccess,
    55  			desc:  statusInPool,
    56  		},
    57  		{
    58  			name:      "check truncation of label list",
    59  			milestone: "v1.0",
    60  			inPool:    false,
    61  
    62  			state: github.StatusPending,
    63  			desc:  fmt.Sprintf(statusNotInPool, " Needs need-1, need-2 labels."),
    64  		},
    65  		{
    66  			name:      "check truncation of label list is not excessive",
    67  			labels:    append([]string{}, neededLabels[:2]...),
    68  			milestone: "v1.0",
    69  			inPool:    false,
    70  
    71  			state: github.StatusPending,
    72  			desc:  fmt.Sprintf(statusNotInPool, " Needs need-a-very-super-duper-extra-not-short-at-all-label-name label."),
    73  		},
    74  		{
    75  			name:      "has forbidden labels",
    76  			labels:    append(append([]string{}, neededLabels...), forbiddenLabels...),
    77  			milestone: "v1.0",
    78  			inPool:    false,
    79  
    80  			state: github.StatusPending,
    81  			desc:  fmt.Sprintf(statusNotInPool, " Should not have forbidden-1, forbidden-2 labels."),
    82  		},
    83  		{
    84  			name:      "has one forbidden label",
    85  			labels:    append(append([]string{}, neededLabels...), forbiddenLabels[0]),
    86  			milestone: "v1.0",
    87  			inPool:    false,
    88  
    89  			state: github.StatusPending,
    90  			desc:  fmt.Sprintf(statusNotInPool, " Should not have forbidden-1 label."),
    91  		},
    92  		{
    93  			name:      "only mention one requirement class",
    94  			labels:    append(append([]string{}, neededLabels[1:]...), forbiddenLabels[0]),
    95  			milestone: "v1.0",
    96  			inPool:    false,
    97  
    98  			state: github.StatusPending,
    99  			desc:  fmt.Sprintf(statusNotInPool, " Needs need-1 label."),
   100  		},
   101  		{
   102  			name:            "against excluded branch",
   103  			baseref:         "bad",
   104  			branchBlacklist: []string{"bad"},
   105  			sameBranchReqs:  true,
   106  			labels:          neededLabels,
   107  			inPool:          false,
   108  
   109  			state: github.StatusPending,
   110  			desc:  fmt.Sprintf(statusNotInPool, " Merging to branch bad is forbidden."),
   111  		},
   112  		{
   113  			name:            "not against included branch",
   114  			baseref:         "bad",
   115  			branchWhitelist: []string{"good"},
   116  			sameBranchReqs:  true,
   117  			labels:          neededLabels,
   118  			inPool:          false,
   119  
   120  			state: github.StatusPending,
   121  			desc:  fmt.Sprintf(statusNotInPool, " Merging to branch bad is forbidden."),
   122  		},
   123  		{
   124  			name:            "choose query for correct branch",
   125  			baseref:         "bad",
   126  			branchWhitelist: []string{"good"},
   127  			milestone:       "v1.0",
   128  			labels:          neededLabels,
   129  			inPool:          false,
   130  
   131  			state: github.StatusPending,
   132  			desc:  fmt.Sprintf(statusNotInPool, " Needs 1, 2, 3, 4, 5, 6, 7 labels."),
   133  		},
   134  		{
   135  			name:      "only failed tide context",
   136  			labels:    neededLabels,
   137  			milestone: "v1.0",
   138  			contexts:  []Context{{Context: githubql.String(statusContext), State: githubql.StatusStateError}},
   139  			inPool:    false,
   140  
   141  			state: github.StatusPending,
   142  			desc:  fmt.Sprintf(statusNotInPool, ""),
   143  		},
   144  		{
   145  			name:      "single bad context",
   146  			labels:    neededLabels,
   147  			contexts:  []Context{{Context: githubql.String("job-name"), State: githubql.StatusStateError}},
   148  			milestone: "v1.0",
   149  			inPool:    false,
   150  
   151  			state: github.StatusPending,
   152  			desc:  fmt.Sprintf(statusNotInPool, " Job job-name has not succeeded."),
   153  		},
   154  		{
   155  			name:      "multiple bad contexts",
   156  			labels:    neededLabels,
   157  			milestone: "v1.0",
   158  			contexts: []Context{
   159  				{Context: githubql.String("job-name"), State: githubql.StatusStateError},
   160  				{Context: githubql.String("other-job-name"), State: githubql.StatusStateError},
   161  			},
   162  			inPool: false,
   163  
   164  			state: github.StatusPending,
   165  			desc:  fmt.Sprintf(statusNotInPool, " Jobs job-name, other-job-name have not succeeded."),
   166  		},
   167  		{
   168  			name:      "wrong milestone",
   169  			labels:    neededLabels,
   170  			milestone: "v1.1",
   171  			contexts:  []Context{{Context: githubql.String("job-name"), State: githubql.StatusStateSuccess}},
   172  			inPool:    false,
   173  
   174  			state: github.StatusPending,
   175  			desc:  fmt.Sprintf(statusNotInPool, " Must be in milestone v1.0."),
   176  		},
   177  		{
   178  			name:      "unknown requirement",
   179  			labels:    neededLabels,
   180  			milestone: "v1.0",
   181  			contexts:  []Context{{Context: githubql.String("job-name"), State: githubql.StatusStateSuccess}},
   182  			inPool:    false,
   183  
   184  			state: github.StatusPending,
   185  			desc:  fmt.Sprintf(statusNotInPool, ""),
   186  		},
   187  		{
   188  			name:      "check that min diff query is used",
   189  			labels:    []string{"3", "4", "5", "6", "7"},
   190  			milestone: "v1.0",
   191  			inPool:    false,
   192  
   193  			state: github.StatusPending,
   194  			desc:  fmt.Sprintf(statusNotInPool, " Needs 1, 2 labels."),
   195  		},
   196  	}
   197  
   198  	for _, tc := range testcases {
   199  		t.Logf("Test Case: %q\n", tc.name)
   200  		secondQuery := config.TideQuery{
   201  			Orgs:      []string{""},
   202  			Labels:    []string{"1", "2", "3", "4", "5", "6", "7"}, // lots of requirements
   203  			Milestone: "v1.0",
   204  		}
   205  		if tc.sameBranchReqs {
   206  			secondQuery.ExcludedBranches = tc.branchBlacklist
   207  			secondQuery.IncludedBranches = tc.branchWhitelist
   208  		}
   209  		queriesByRepo := config.TideQueries{
   210  			config.TideQuery{
   211  				Orgs:             []string{""},
   212  				ExcludedBranches: tc.branchBlacklist,
   213  				IncludedBranches: tc.branchWhitelist,
   214  				Labels:           neededLabels,
   215  				MissingLabels:    forbiddenLabels,
   216  				Milestone:        "v1.0",
   217  			},
   218  			secondQuery,
   219  		}.QueryMap()
   220  		var pr PullRequest
   221  		pr.BaseRef = struct {
   222  			Name   githubql.String
   223  			Prefix githubql.String
   224  		}{
   225  			Name: githubql.String(tc.baseref),
   226  		}
   227  		for _, label := range tc.labels {
   228  			pr.Labels.Nodes = append(
   229  				pr.Labels.Nodes,
   230  				struct{ Name githubql.String }{Name: githubql.String(label)},
   231  			)
   232  		}
   233  		if len(tc.contexts) > 0 {
   234  			pr.HeadRefOID = githubql.String("head")
   235  			pr.Commits.Nodes = append(
   236  				pr.Commits.Nodes,
   237  				struct{ Commit Commit }{
   238  					Commit: Commit{
   239  						Status: struct{ Contexts []Context }{
   240  							Contexts: tc.contexts,
   241  						},
   242  						OID: githubql.String("head"),
   243  					},
   244  				},
   245  			)
   246  		}
   247  		if tc.milestone != "" {
   248  			pr.Milestone = &struct {
   249  				Title githubql.String
   250  			}{githubql.String(tc.milestone)}
   251  		}
   252  		var pool map[string]PullRequest
   253  		if tc.inPool {
   254  			pool = map[string]PullRequest{"#0": {}}
   255  		}
   256  
   257  		state, desc := expectedStatus(queriesByRepo, &pr, pool, &config.TideContextPolicy{})
   258  		if state != tc.state {
   259  			t.Errorf("Expected status state %q, but got %q.", string(tc.state), string(state))
   260  		}
   261  		if desc != tc.desc {
   262  			t.Errorf("Expected status description %q, but got %q.", tc.desc, desc)
   263  		}
   264  	}
   265  }
   266  
   267  func TestSetStatuses(t *testing.T) {
   268  	statusNotInPoolEmpty := fmt.Sprintf(statusNotInPool, "")
   269  	testcases := []struct {
   270  		name string
   271  
   272  		inPool     bool
   273  		hasContext bool
   274  		state      githubql.StatusState
   275  		desc       string
   276  
   277  		shouldSet bool
   278  	}{
   279  		{
   280  			name: "in pool with proper context",
   281  
   282  			inPool:     true,
   283  			hasContext: true,
   284  			state:      githubql.StatusStateSuccess,
   285  			desc:       statusInPool,
   286  
   287  			shouldSet: false,
   288  		},
   289  		{
   290  			name: "in pool without context",
   291  
   292  			inPool:     true,
   293  			hasContext: false,
   294  
   295  			shouldSet: true,
   296  		},
   297  		{
   298  			name: "in pool with improper context",
   299  
   300  			inPool:     true,
   301  			hasContext: true,
   302  			state:      githubql.StatusStateSuccess,
   303  			desc:       statusNotInPoolEmpty,
   304  
   305  			shouldSet: true,
   306  		},
   307  		{
   308  			name: "in pool with wrong state",
   309  
   310  			inPool:     true,
   311  			hasContext: true,
   312  			state:      githubql.StatusStatePending,
   313  			desc:       statusInPool,
   314  
   315  			shouldSet: true,
   316  		},
   317  		{
   318  			name: "not in pool with proper context",
   319  
   320  			inPool:     false,
   321  			hasContext: true,
   322  			state:      githubql.StatusStatePending,
   323  			desc:       statusNotInPoolEmpty,
   324  
   325  			shouldSet: false,
   326  		},
   327  		{
   328  			name: "not in pool with improper context",
   329  
   330  			inPool:     false,
   331  			hasContext: true,
   332  			state:      githubql.StatusStatePending,
   333  			desc:       statusInPool,
   334  
   335  			shouldSet: true,
   336  		},
   337  		{
   338  			name: "not in pool with no context",
   339  
   340  			inPool:     false,
   341  			hasContext: false,
   342  
   343  			shouldSet: true,
   344  		},
   345  	}
   346  	for _, tc := range testcases {
   347  		var pr PullRequest
   348  		pr.Commits.Nodes = []struct{ Commit Commit }{{}}
   349  		if tc.hasContext {
   350  			pr.Commits.Nodes[0].Commit.Status.Contexts = []Context{
   351  				{
   352  					Context:     githubql.String(statusContext),
   353  					State:       tc.state,
   354  					Description: githubql.String(tc.desc),
   355  				},
   356  			}
   357  		}
   358  		pool := make(map[string]PullRequest)
   359  		if tc.inPool {
   360  			pool[prKey(&pr)] = pr
   361  		}
   362  		fc := &fgc{}
   363  		ca := &config.Agent{}
   364  		ca.Set(&config.Config{})
   365  		// setStatuses logs instead of returning errors.
   366  		// Construct a logger to watch for errors to be printed.
   367  		log := logrus.WithField("component", "tide")
   368  		initialLog, err := log.String()
   369  		if err != nil {
   370  			t.Fatalf("Failed to get log output before testing: %v", err)
   371  		}
   372  
   373  		sc := &statusController{ghc: fc, ca: ca, logger: log}
   374  		sc.setStatuses([]PullRequest{pr}, pool)
   375  		if str, err := log.String(); err != nil {
   376  			t.Fatalf("For case %s: failed to get log output: %v", tc.name, err)
   377  		} else if str != initialLog {
   378  			t.Errorf("For case %s: error setting status: %s", tc.name, str)
   379  		}
   380  		if tc.shouldSet && !fc.setStatus {
   381  			t.Errorf("For case %s: should set but didn't", tc.name)
   382  		} else if !tc.shouldSet && fc.setStatus {
   383  			t.Errorf("For case %s: should not set but did", tc.name)
   384  		}
   385  	}
   386  }
   387  
   388  func TestTargetUrl(t *testing.T) {
   389  	testcases := []struct {
   390  		name   string
   391  		pr     *PullRequest
   392  		config config.Tide
   393  
   394  		expectedURL string
   395  	}{
   396  		{
   397  			name:        "no config",
   398  			pr:          &PullRequest{},
   399  			config:      config.Tide{},
   400  			expectedURL: "",
   401  		},
   402  		{
   403  			name:        "tide overview config",
   404  			pr:          &PullRequest{},
   405  			config:      config.Tide{TargetURL: "tide.com"},
   406  			expectedURL: "tide.com",
   407  		},
   408  		{
   409  			name:        "PR dashboard config and overview config",
   410  			pr:          &PullRequest{},
   411  			config:      config.Tide{TargetURL: "tide.com", PRStatusBaseURL: "pr.status.com"},
   412  			expectedURL: "tide.com",
   413  		},
   414  		{
   415  			name: "PR dashboard config",
   416  			pr: &PullRequest{
   417  				Author: struct {
   418  					Login githubql.String
   419  				}{Login: githubql.String("author")},
   420  				Repository: struct {
   421  					Name          githubql.String
   422  					NameWithOwner githubql.String
   423  					Owner         struct {
   424  						Login githubql.String
   425  					}
   426  				}{NameWithOwner: githubql.String("org/repo")},
   427  				HeadRefName: "head",
   428  			},
   429  			config:      config.Tide{PRStatusBaseURL: "pr.status.com"},
   430  			expectedURL: "pr.status.com?query=is%3Apr+repo%3Aorg%2Frepo+author%3Aauthor+head%3Ahead",
   431  		},
   432  	}
   433  
   434  	for _, tc := range testcases {
   435  		ca := &config.Agent{}
   436  		ca.Set(&config.Config{ProwConfig: config.ProwConfig{Tide: tc.config}})
   437  		log := logrus.WithField("controller", "status-update")
   438  		if actual, expected := targetURL(ca, tc.pr, log), tc.expectedURL; actual != expected {
   439  			t.Errorf("%s: expected target URL %s but got %s", tc.name, expected, actual)
   440  		}
   441  	}
   442  }
   443  
   444  func TestOpenPRsQuery(t *testing.T) {
   445  	var q string
   446  	checkTok := func(tok string) {
   447  		if !strings.Contains(q, " "+tok+" ") {
   448  			t.Errorf("Expected query to contain \"%s\", got \"%s\"", tok, q)
   449  		}
   450  	}
   451  
   452  	orgs := []string{"org", "kuber"}
   453  	repos := []string{"k8s/k8s", "k8s/t-i"}
   454  	exceptions := map[string]sets.String{
   455  		"org":            sets.NewString("org/repo1", "org/repo2"),
   456  		"irrelevant-org": sets.NewString("irrelevant-org/repo1", "irrelevant-org/repo2"),
   457  	}
   458  
   459  	q = " " + openPRsQuery(orgs, repos, exceptions) + " "
   460  	checkTok("is:pr")
   461  	checkTok("state:open")
   462  	checkTok("org:\"org\"")
   463  	checkTok("org:\"kuber\"")
   464  	checkTok("repo:\"k8s/k8s\"")
   465  	checkTok("repo:\"k8s/t-i\"")
   466  	checkTok("-repo:\"org/repo1\"")
   467  	checkTok("-repo:\"org/repo2\"")
   468  }