github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/plugins/blockade/blockade_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 blockade
    18  
    19  import (
    20  	"fmt"
    21  	"reflect"
    22  	"sort"
    23  	"strings"
    24  	"testing"
    25  
    26  	"github.com/sirupsen/logrus"
    27  
    28  	"k8s.io/test-infra/prow/github"
    29  	"k8s.io/test-infra/prow/github/fakegithub"
    30  	"k8s.io/test-infra/prow/labels"
    31  	"k8s.io/test-infra/prow/plugins"
    32  )
    33  
    34  var (
    35  	// Sample changes:
    36  	docFile    = github.PullRequestChange{Filename: "docs/documentation.md", BlobURL: "<URL1>"}
    37  	docOwners  = github.PullRequestChange{Filename: "docs/OWNERS", BlobURL: "<URL2>"}
    38  	docOwners2 = github.PullRequestChange{Filename: "docs/2/OWNERS", BlobURL: "<URL3>"}
    39  	srcGo      = github.PullRequestChange{Filename: "src/code.go", BlobURL: "<URL4>"}
    40  	srcSh      = github.PullRequestChange{Filename: "src/shell.sh", BlobURL: "<URL5>"}
    41  	docSh      = github.PullRequestChange{Filename: "docs/shell.sh", BlobURL: "<URL6>"}
    42  
    43  	// Sample blockades:
    44  	blockDocs = plugins.Blockade{
    45  		Repos:        []string{"org/repo"},
    46  		BlockRegexps: []string{`docs/.*`},
    47  		Explanation:  "1",
    48  	}
    49  	blockDocsExceptOwners = plugins.Blockade{
    50  		Repos:            []string{"org/repo"},
    51  		BlockRegexps:     []string{`docs/.*`},
    52  		ExceptionRegexps: []string{`.*OWNERS`},
    53  		Explanation:      "2",
    54  	}
    55  	blockShell = plugins.Blockade{
    56  		Repos:        []string{"org/repo"},
    57  		BlockRegexps: []string{`.*\.sh`},
    58  		Explanation:  "3",
    59  	}
    60  	blockAllOrg = plugins.Blockade{
    61  		Repos:        []string{"org"},
    62  		BlockRegexps: []string{`.*`},
    63  		Explanation:  "4",
    64  	}
    65  	blockAllOther = plugins.Blockade{
    66  		Repos:        []string{"org2"},
    67  		BlockRegexps: []string{`.*`},
    68  		Explanation:  "5",
    69  	}
    70  )
    71  
    72  // TestCalculateBlocks validates that changes are blocked or allowed correctly.
    73  func TestCalculateBlocks(t *testing.T) {
    74  	tcs := []struct {
    75  		name            string
    76  		changes         []github.PullRequestChange
    77  		config          []plugins.Blockade
    78  		expectedSummary summary
    79  	}{
    80  		{
    81  			name:    "blocked by 1/1 blockade (no exceptions), extra file",
    82  			config:  []plugins.Blockade{blockDocs},
    83  			changes: []github.PullRequestChange{docFile, docOwners, srcGo},
    84  			expectedSummary: summary{
    85  				"1": []github.PullRequestChange{docFile, docOwners},
    86  			},
    87  		},
    88  		{
    89  			name:    "blocked by 1/1 blockade (1/2 files are exceptions), extra file",
    90  			config:  []plugins.Blockade{blockDocsExceptOwners},
    91  			changes: []github.PullRequestChange{docFile, docOwners, srcGo},
    92  			expectedSummary: summary{
    93  				"2": []github.PullRequestChange{docFile},
    94  			},
    95  		},
    96  		{
    97  			name:            "blocked by 0/1 blockades (2/2 exceptions), extra file",
    98  			config:          []plugins.Blockade{blockDocsExceptOwners},
    99  			changes:         []github.PullRequestChange{docOwners, docOwners2, srcGo},
   100  			expectedSummary: summary{},
   101  		},
   102  		{
   103  			name:            "blocked by 0/1 blockades (no exceptions), extra file",
   104  			config:          []plugins.Blockade{blockDocsExceptOwners},
   105  			changes:         []github.PullRequestChange{srcGo, srcSh},
   106  			expectedSummary: summary{},
   107  		},
   108  		{
   109  			name:    "blocked by 2/2 blockades (no exceptions), extra file",
   110  			config:  []plugins.Blockade{blockDocsExceptOwners, blockShell},
   111  			changes: []github.PullRequestChange{srcGo, srcSh, docFile},
   112  			expectedSummary: summary{
   113  				"2": []github.PullRequestChange{docFile},
   114  				"3": []github.PullRequestChange{srcSh},
   115  			},
   116  		},
   117  		{
   118  			name:    "blocked by 2/2 blockades w/ single file",
   119  			config:  []plugins.Blockade{blockDocsExceptOwners, blockShell},
   120  			changes: []github.PullRequestChange{docSh},
   121  			expectedSummary: summary{
   122  				"2": []github.PullRequestChange{docSh},
   123  				"3": []github.PullRequestChange{docSh},
   124  			},
   125  		},
   126  		{
   127  			name:    "blocked by 2/2 blockades w/ single file (1/2 exceptions)",
   128  			config:  []plugins.Blockade{blockDocsExceptOwners, blockShell},
   129  			changes: []github.PullRequestChange{docSh, docOwners},
   130  			expectedSummary: summary{
   131  				"2": []github.PullRequestChange{docSh},
   132  				"3": []github.PullRequestChange{docSh},
   133  			},
   134  		},
   135  		{
   136  			name:    "blocked by 1/2 blockades (1/2 exceptions), extra file",
   137  			config:  []plugins.Blockade{blockDocsExceptOwners, blockShell},
   138  			changes: []github.PullRequestChange{srcSh, docOwners, srcGo},
   139  			expectedSummary: summary{
   140  				"3": []github.PullRequestChange{srcSh},
   141  			},
   142  		},
   143  		{
   144  			name:            "blocked by 0/2 blockades (1/2 exceptions), extra file",
   145  			config:          []plugins.Blockade{blockDocsExceptOwners, blockShell},
   146  			changes:         []github.PullRequestChange{docOwners, srcGo},
   147  			expectedSummary: summary{},
   148  		},
   149  	}
   150  
   151  	for _, tc := range tcs {
   152  		blockades := compileApplicableBlockades("org", "repo", logrus.WithField("plugin", PluginName), tc.config)
   153  		sum := calculateBlocks(tc.changes, blockades)
   154  		if !reflect.DeepEqual(sum, tc.expectedSummary) {
   155  			t.Errorf("[%s] Expected summary: %#v, actual summary: %#v.", tc.name, tc.expectedSummary, sum)
   156  		}
   157  	}
   158  }
   159  
   160  func TestSummaryString(t *testing.T) {
   161  	// Just one example for now.
   162  	tcs := []struct {
   163  		name             string
   164  		sum              summary
   165  		expectedContents []string
   166  	}{
   167  		{
   168  			name: "Simple example",
   169  			sum: summary{
   170  				"reason A": []github.PullRequestChange{docFile},
   171  				"reason B": []github.PullRequestChange{srcGo, srcSh},
   172  			},
   173  			expectedContents: []string{
   174  				"#### Reasons for blocking this PR:\n",
   175  				"[reason A]\n- [docs/documentation.md](<URL1>)\n\n",
   176  				"[reason B]\n- [src/code.go](<URL4>)\n\n- [src/shell.sh](<URL5>)\n\n",
   177  			},
   178  		},
   179  	}
   180  
   181  	for _, tc := range tcs {
   182  		got := tc.sum.String()
   183  		for _, expected := range tc.expectedContents {
   184  			if !strings.Contains(got, expected) {
   185  				t.Errorf("[%s] Expected summary %#v to contain %q, but got %q.", tc.name, tc.sum, expected, got)
   186  			}
   187  		}
   188  	}
   189  }
   190  
   191  func formatLabel(label string) string {
   192  	return fmt.Sprintf("%s/%s#%d:%s", "org", "repo", 1, label)
   193  }
   194  
   195  type fakePruner struct{}
   196  
   197  func (f *fakePruner) PruneComments(_ func(ic github.IssueComment) bool) {}
   198  
   199  // TestHandle validates that:
   200  // - The correct labels are added/removed.
   201  // - A comment is created when needed.
   202  // - Uninteresting events are ignored.
   203  // - Blockades that don't apply to this repo are ignored.
   204  func TestHandle(t *testing.T) {
   205  	// Don't need to validate the following because they are validated by other tests:
   206  	// - Block calculation. (Whether or not changes justify blocking the PR.)
   207  	// - Comment contents, just existence.
   208  	otherLabel := labels.LGTM
   209  
   210  	tcs := []struct {
   211  		name       string
   212  		action     github.PullRequestEventAction
   213  		config     []plugins.Blockade
   214  		hasLabel   bool
   215  		filesBlock bool // This is ignored if there are no applicable blockades for the repo.
   216  
   217  		labelAdded     string
   218  		labelRemoved   string
   219  		commentCreated bool
   220  	}{
   221  		{
   222  			name:       "Boring action",
   223  			action:     github.PullRequestActionEdited,
   224  			config:     []plugins.Blockade{blockDocsExceptOwners},
   225  			hasLabel:   false,
   226  			filesBlock: true,
   227  		},
   228  		{
   229  			name:       "Basic block",
   230  			action:     github.PullRequestActionOpened,
   231  			config:     []plugins.Blockade{blockDocsExceptOwners},
   232  			hasLabel:   false,
   233  			filesBlock: true,
   234  
   235  			labelAdded:     labels.BlockedPaths,
   236  			commentCreated: true,
   237  		},
   238  		{
   239  			name:       "Basic block, already labeled",
   240  			action:     github.PullRequestActionOpened,
   241  			config:     []plugins.Blockade{blockDocsExceptOwners},
   242  			hasLabel:   true,
   243  			filesBlock: true,
   244  		},
   245  		{
   246  			name:       "Not blocked, not labeled",
   247  			action:     github.PullRequestActionOpened,
   248  			config:     []plugins.Blockade{blockDocsExceptOwners},
   249  			hasLabel:   false,
   250  			filesBlock: false,
   251  		},
   252  		{
   253  			name:       "Not blocked, has label",
   254  			action:     github.PullRequestActionOpened,
   255  			config:     []plugins.Blockade{blockDocsExceptOwners},
   256  			hasLabel:   true,
   257  			filesBlock: false,
   258  
   259  			labelRemoved: labels.BlockedPaths,
   260  		},
   261  		{
   262  			name:       "No blockade, not labeled",
   263  			action:     github.PullRequestActionOpened,
   264  			config:     []plugins.Blockade{},
   265  			hasLabel:   false,
   266  			filesBlock: true,
   267  		},
   268  		{
   269  			name:       "No blockade, has label",
   270  			action:     github.PullRequestActionOpened,
   271  			config:     []plugins.Blockade{},
   272  			hasLabel:   true,
   273  			filesBlock: true,
   274  
   275  			labelRemoved: labels.BlockedPaths,
   276  		},
   277  		{
   278  			name:       "Basic block (org scoped blockade)",
   279  			action:     github.PullRequestActionOpened,
   280  			config:     []plugins.Blockade{blockAllOrg},
   281  			hasLabel:   false,
   282  			filesBlock: true,
   283  
   284  			labelAdded:     labels.BlockedPaths,
   285  			commentCreated: true,
   286  		},
   287  		{
   288  			name:       "Would be blocked, but blockade is not applicable; not labeled",
   289  			action:     github.PullRequestActionOpened,
   290  			config:     []plugins.Blockade{blockAllOther},
   291  			hasLabel:   false,
   292  			filesBlock: true,
   293  		},
   294  	}
   295  
   296  	for _, tc := range tcs {
   297  		expectAdded := []string{}
   298  		fakeClient := &fakegithub.FakeClient{
   299  			RepoLabelsExisting: []string{labels.BlockedPaths, otherLabel},
   300  			IssueComments:      make(map[int][]github.IssueComment),
   301  			PullRequestChanges: make(map[int][]github.PullRequestChange),
   302  			IssueLabelsAdded:   []string{},
   303  			IssueLabelsRemoved: []string{},
   304  		}
   305  		if tc.hasLabel {
   306  			label := formatLabel(labels.BlockedPaths)
   307  			fakeClient.IssueLabelsAdded = append(fakeClient.IssueLabelsAdded, label)
   308  			expectAdded = append(expectAdded, label)
   309  		}
   310  		calcF := func(_ []github.PullRequestChange, blockades []blockade) summary {
   311  			if !tc.filesBlock {
   312  				return nil
   313  			}
   314  			sum := make(summary)
   315  			for _, b := range blockades {
   316  				// For this test assume 'docFile' is blocked by every blockade that is applicable to the repo.
   317  				sum[b.explanation] = []github.PullRequestChange{docFile}
   318  			}
   319  			return sum
   320  		}
   321  		pre := &github.PullRequestEvent{
   322  			Action: tc.action,
   323  			Repo:   github.Repo{Owner: github.User{Login: "org"}, Name: "repo"},
   324  			Number: 1,
   325  		}
   326  		if err := handle(fakeClient, logrus.WithField("plugin", PluginName), tc.config, &fakePruner{}, calcF, pre); err != nil {
   327  			t.Errorf("[%s] Unexpected error from handle: %v.", tc.name, err)
   328  			continue
   329  		}
   330  
   331  		if tc.labelAdded != "" {
   332  			expectAdded = append(expectAdded, formatLabel(tc.labelAdded))
   333  		}
   334  		sort.Strings(expectAdded)
   335  		sort.Strings(fakeClient.IssueLabelsAdded)
   336  		if !reflect.DeepEqual(expectAdded, fakeClient.IssueLabelsAdded) {
   337  			t.Errorf("[%s]: Expected labels to be added: %q, but got: %q.", tc.name, expectAdded, fakeClient.IssueLabelsAdded)
   338  		}
   339  		expectRemoved := []string{}
   340  		if tc.labelRemoved != "" {
   341  			expectRemoved = append(expectRemoved, formatLabel(tc.labelRemoved))
   342  		}
   343  		sort.Strings(expectRemoved)
   344  		sort.Strings(fakeClient.IssueLabelsRemoved)
   345  		if !reflect.DeepEqual(expectRemoved, fakeClient.IssueLabelsRemoved) {
   346  			t.Errorf("[%s]: Expected labels to be removed: %q, but got: %q.", tc.name, expectRemoved, fakeClient.IssueLabelsRemoved)
   347  		}
   348  
   349  		if count := len(fakeClient.IssueComments[1]); count > 1 {
   350  			t.Errorf("[%s] More than 1 comment created! (%d created).", tc.name, count)
   351  		} else if (count == 1) != tc.commentCreated {
   352  			t.Errorf("[%s] Expected comment created: %t, but got %t.", tc.name, tc.commentCreated, count == 1)
   353  		}
   354  	}
   355  }