github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/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  	"regexp"
    23  	"sort"
    24  	"strings"
    25  	"testing"
    26  
    27  	"github.com/sirupsen/logrus"
    28  
    29  	"sigs.k8s.io/prow/pkg/config"
    30  	"sigs.k8s.io/prow/pkg/github"
    31  	"sigs.k8s.io/prow/pkg/github/fakegithub"
    32  	"sigs.k8s.io/prow/pkg/labels"
    33  	"sigs.k8s.io/prow/pkg/plugins"
    34  )
    35  
    36  var (
    37  	// Sample changes:
    38  	docFile         = github.PullRequestChange{Filename: "docs/documentation.md", BlobURL: "<URL1>"}
    39  	docOwners       = github.PullRequestChange{Filename: "docs/OWNERS", BlobURL: "<URL2>"}
    40  	docOwners2      = github.PullRequestChange{Filename: "docs/2/OWNERS", BlobURL: "<URL3>"}
    41  	srcGo           = github.PullRequestChange{Filename: "src/code.go", BlobURL: "<URL4>"}
    42  	srcSh           = github.PullRequestChange{Filename: "src/shell.sh", BlobURL: "<URL5>"}
    43  	docSh           = github.PullRequestChange{Filename: "docs/shell.sh", BlobURL: "<URL6>"}
    44  	conformanceYaml = github.PullRequestChange{Filename: "test/conformance/testdata/conformance.yaml", BlobURL: "<URL6>"}
    45  
    46  	// branches
    47  	releaseBranchRegexp = "^release-*"
    48  	releaseBranchRe     = regexp.MustCompile(releaseBranchRegexp)
    49  
    50  	// Sample blockades:
    51  	blockDocs = plugins.Blockade{
    52  		Repos:        []string{"org/repo"},
    53  		BlockRegexps: []string{`docs/.*`},
    54  		Explanation:  "1",
    55  	}
    56  	blockDocsExceptOwners = plugins.Blockade{
    57  		Repos:            []string{"org/repo"},
    58  		BlockRegexps:     []string{`docs/.*`},
    59  		ExceptionRegexps: []string{`.*OWNERS`},
    60  		Explanation:      "2",
    61  	}
    62  	blockShell = plugins.Blockade{
    63  		Repos:        []string{"org/repo"},
    64  		BlockRegexps: []string{`.*\.sh`},
    65  		Explanation:  "3",
    66  	}
    67  	blockAllOrg = plugins.Blockade{
    68  		Repos:        []string{"org"},
    69  		BlockRegexps: []string{`.*`},
    70  		Explanation:  "4",
    71  	}
    72  	blockAllOther = plugins.Blockade{
    73  		Repos:        []string{"org2"},
    74  		BlockRegexps: []string{`.*`},
    75  		Explanation:  "5",
    76  	}
    77  	blockConformanceOnReleaseBranch = plugins.Blockade{
    78  		Repos:        []string{"org/repo"},
    79  		BranchRegexp: &releaseBranchRegexp,
    80  		BranchRe:     releaseBranchRe,
    81  		BlockRegexps: []string{`test/conformance/testdata/.*`},
    82  		Explanation:  "6",
    83  	}
    84  	blockBadBranchRe = plugins.Blockade{
    85  		Repos:        []string{"org/repo"},
    86  		BranchRegexp: &releaseBranchRegexp,
    87  		BlockRegexps: []string{`test/conformance/testdata/.*`},
    88  		Explanation:  "6",
    89  	}
    90  )
    91  
    92  // TestCalculateBlocks validates that changes are blocked or allowed correctly.
    93  func TestCalculateBlocks(t *testing.T) {
    94  	tcs := []struct {
    95  		name            string
    96  		branch          string
    97  		changes         []github.PullRequestChange
    98  		config          []plugins.Blockade
    99  		expectedSummary summary
   100  	}{
   101  		{
   102  			name:            "nil BranchRe",
   103  			config:          []plugins.Blockade{blockBadBranchRe},
   104  			changes:         []github.PullRequestChange{},
   105  			expectedSummary: summary{},
   106  		},
   107  		{
   108  			name:    "blocked by 1/1 blockade (no exceptions), extra file",
   109  			config:  []plugins.Blockade{blockDocs},
   110  			changes: []github.PullRequestChange{docFile, docOwners, srcGo},
   111  			expectedSummary: summary{
   112  				"1": []github.PullRequestChange{docFile, docOwners},
   113  			},
   114  		},
   115  		{
   116  			name:    "blocked by 1/1 blockade (1/2 files are exceptions), extra file",
   117  			config:  []plugins.Blockade{blockDocsExceptOwners},
   118  			changes: []github.PullRequestChange{docFile, docOwners, srcGo},
   119  			expectedSummary: summary{
   120  				"2": []github.PullRequestChange{docFile},
   121  			},
   122  		},
   123  		{
   124  			name:            "blocked by 0/1 blockades (2/2 exceptions), extra file",
   125  			config:          []plugins.Blockade{blockDocsExceptOwners},
   126  			changes:         []github.PullRequestChange{docOwners, docOwners2, srcGo},
   127  			expectedSummary: summary{},
   128  		},
   129  		{
   130  			name:            "blocked by 0/1 blockades (no exceptions), extra file",
   131  			config:          []plugins.Blockade{blockDocsExceptOwners},
   132  			changes:         []github.PullRequestChange{srcGo, srcSh},
   133  			expectedSummary: summary{},
   134  		},
   135  		{
   136  			name:    "blocked by 2/2 blockades (no exceptions), extra file",
   137  			config:  []plugins.Blockade{blockDocsExceptOwners, blockShell},
   138  			changes: []github.PullRequestChange{srcGo, srcSh, docFile},
   139  			expectedSummary: summary{
   140  				"2": []github.PullRequestChange{docFile},
   141  				"3": []github.PullRequestChange{srcSh},
   142  			},
   143  		},
   144  		{
   145  			name:    "blocked by 2/2 blockades w/ single file",
   146  			config:  []plugins.Blockade{blockDocsExceptOwners, blockShell},
   147  			changes: []github.PullRequestChange{docSh},
   148  			expectedSummary: summary{
   149  				"2": []github.PullRequestChange{docSh},
   150  				"3": []github.PullRequestChange{docSh},
   151  			},
   152  		},
   153  		{
   154  			name:    "blocked by 2/2 blockades w/ single file (1/2 exceptions)",
   155  			config:  []plugins.Blockade{blockDocsExceptOwners, blockShell},
   156  			changes: []github.PullRequestChange{docSh, docOwners},
   157  			expectedSummary: summary{
   158  				"2": []github.PullRequestChange{docSh},
   159  				"3": []github.PullRequestChange{docSh},
   160  			},
   161  		},
   162  		{
   163  			name:    "blocked by 1/2 blockades (1/2 exceptions), extra file",
   164  			config:  []plugins.Blockade{blockDocsExceptOwners, blockShell},
   165  			changes: []github.PullRequestChange{srcSh, docOwners, srcGo},
   166  			expectedSummary: summary{
   167  				"3": []github.PullRequestChange{srcSh},
   168  			},
   169  		},
   170  		{
   171  			name:            "blocked by 0/2 blockades (1/2 exceptions), extra file",
   172  			config:          []plugins.Blockade{blockDocsExceptOwners, blockShell},
   173  			changes:         []github.PullRequestChange{docOwners, srcGo},
   174  			expectedSummary: summary{},
   175  		},
   176  		{
   177  			name:    "blocked by 1/1 blockade on release branch w/ single file",
   178  			branch:  "release-1.20",
   179  			config:  []plugins.Blockade{blockConformanceOnReleaseBranch},
   180  			changes: []github.PullRequestChange{conformanceYaml},
   181  			expectedSummary: summary{
   182  				"6": []github.PullRequestChange{conformanceYaml},
   183  			},
   184  		},
   185  		{
   186  			name:            "don't block conformance on main branch",
   187  			branch:          "main",
   188  			config:          []plugins.Blockade{blockConformanceOnReleaseBranch},
   189  			changes:         []github.PullRequestChange{conformanceYaml},
   190  			expectedSummary: summary{},
   191  		},
   192  		{
   193  			name:    "blocked by 2/2 blockades on release branch (no exceptions), extra file",
   194  			branch:  "release-1.20",
   195  			config:  []plugins.Blockade{blockConformanceOnReleaseBranch, blockDocsExceptOwners},
   196  			changes: []github.PullRequestChange{conformanceYaml, docFile, srcGo},
   197  			expectedSummary: summary{
   198  				"2": []github.PullRequestChange{docFile},
   199  				"6": []github.PullRequestChange{conformanceYaml},
   200  			},
   201  		},
   202  	}
   203  
   204  	for _, tc := range tcs {
   205  		blockades := compileApplicableBlockades("org", "repo", tc.branch, logrus.WithField("plugin", PluginName), tc.config)
   206  		sum := calculateBlocks(tc.changes, blockades)
   207  		if !reflect.DeepEqual(sum, tc.expectedSummary) {
   208  			t.Errorf("[%s] Expected summary: %#v, actual summary: %#v.", tc.name, tc.expectedSummary, sum)
   209  		}
   210  	}
   211  }
   212  
   213  func TestSummaryString(t *testing.T) {
   214  	// Just one example for now.
   215  	tcs := []struct {
   216  		name             string
   217  		sum              summary
   218  		expectedContents []string
   219  	}{
   220  		{
   221  			name: "Simple example",
   222  			sum: summary{
   223  				"reason A": []github.PullRequestChange{docFile},
   224  				"reason B": []github.PullRequestChange{srcGo, srcSh},
   225  			},
   226  			expectedContents: []string{
   227  				"#### Reasons for blocking this PR:\n",
   228  				"[reason A]\n- [docs/documentation.md](<URL1>)\n\n",
   229  				"[reason B]\n- [src/code.go](<URL4>)\n\n- [src/shell.sh](<URL5>)\n\n",
   230  			},
   231  		},
   232  	}
   233  
   234  	for _, tc := range tcs {
   235  		got := tc.sum.String()
   236  		for _, expected := range tc.expectedContents {
   237  			if !strings.Contains(got, expected) {
   238  				t.Errorf("[%s] Expected summary %#v to contain %q, but got %q.", tc.name, tc.sum, expected, got)
   239  			}
   240  		}
   241  	}
   242  }
   243  
   244  func formatLabel(label string) string {
   245  	return fmt.Sprintf("%s/%s#%d:%s", "org", "repo", 1, label)
   246  }
   247  
   248  type fakePruner struct{}
   249  
   250  func (f *fakePruner) PruneComments(_ func(ic github.IssueComment) bool) {}
   251  
   252  // TestHandle validates that:
   253  // - The correct labels are added/removed.
   254  // - A comment is created when needed.
   255  // - Uninteresting events are ignored.
   256  // - Blockades that don't apply to this repo are ignored.
   257  func TestHandle(t *testing.T) {
   258  	// Don't need to validate the following because they are validated by other tests:
   259  	// - Block calculation. (Whether or not changes justify blocking the PR.)
   260  	// - Comment contents, just existence.
   261  	otherLabel := labels.LGTM
   262  
   263  	tcs := []struct {
   264  		name       string
   265  		action     github.PullRequestEventAction
   266  		config     []plugins.Blockade
   267  		hasLabel   bool
   268  		filesBlock bool // This is ignored if there are no applicable blockades for the repo.
   269  
   270  		labelAdded     string
   271  		labelRemoved   string
   272  		commentCreated bool
   273  	}{
   274  		{
   275  			name:       "Boring action",
   276  			action:     github.PullRequestActionEdited,
   277  			config:     []plugins.Blockade{blockDocsExceptOwners},
   278  			hasLabel:   false,
   279  			filesBlock: true,
   280  		},
   281  		{
   282  			name:       "Basic block",
   283  			action:     github.PullRequestActionOpened,
   284  			config:     []plugins.Blockade{blockDocsExceptOwners},
   285  			hasLabel:   false,
   286  			filesBlock: true,
   287  
   288  			labelAdded:     labels.BlockedPaths,
   289  			commentCreated: true,
   290  		},
   291  		{
   292  			name:       "Basic block, already labeled",
   293  			action:     github.PullRequestActionOpened,
   294  			config:     []plugins.Blockade{blockDocsExceptOwners},
   295  			hasLabel:   true,
   296  			filesBlock: true,
   297  		},
   298  		{
   299  			name:       "Not blocked, not labeled",
   300  			action:     github.PullRequestActionOpened,
   301  			config:     []plugins.Blockade{blockDocsExceptOwners},
   302  			hasLabel:   false,
   303  			filesBlock: false,
   304  		},
   305  		{
   306  			name:       "Not blocked, has label",
   307  			action:     github.PullRequestActionOpened,
   308  			config:     []plugins.Blockade{blockDocsExceptOwners},
   309  			hasLabel:   true,
   310  			filesBlock: false,
   311  
   312  			labelRemoved: labels.BlockedPaths,
   313  		},
   314  		{
   315  			name:       "No blockade, not labeled",
   316  			action:     github.PullRequestActionOpened,
   317  			config:     []plugins.Blockade{},
   318  			hasLabel:   false,
   319  			filesBlock: true,
   320  		},
   321  		{
   322  			name:       "No blockade, has label",
   323  			action:     github.PullRequestActionOpened,
   324  			config:     []plugins.Blockade{},
   325  			hasLabel:   true,
   326  			filesBlock: true,
   327  
   328  			labelRemoved: labels.BlockedPaths,
   329  		},
   330  		{
   331  			name:       "Basic block (org scoped blockade)",
   332  			action:     github.PullRequestActionOpened,
   333  			config:     []plugins.Blockade{blockAllOrg},
   334  			hasLabel:   false,
   335  			filesBlock: true,
   336  
   337  			labelAdded:     labels.BlockedPaths,
   338  			commentCreated: true,
   339  		},
   340  		{
   341  			name:       "Would be blocked, but blockade is not applicable; not labeled",
   342  			action:     github.PullRequestActionOpened,
   343  			config:     []plugins.Blockade{blockAllOther},
   344  			hasLabel:   false,
   345  			filesBlock: true,
   346  		},
   347  	}
   348  
   349  	for _, tc := range tcs {
   350  		var expectAdded []string
   351  		fakeClient := fakegithub.NewFakeClient()
   352  		fakeClient.RepoLabelsExisting = []string{labels.BlockedPaths, otherLabel}
   353  		if tc.hasLabel {
   354  			label := formatLabel(labels.BlockedPaths)
   355  			fakeClient.IssueLabelsAdded = append(fakeClient.IssueLabelsAdded, label)
   356  			expectAdded = append(expectAdded, label)
   357  		}
   358  		calcF := func(_ []github.PullRequestChange, blockades []blockade) summary {
   359  			if !tc.filesBlock {
   360  				return nil
   361  			}
   362  			sum := make(summary)
   363  			for _, b := range blockades {
   364  				// For this test assume 'docFile' is blocked by every blockade that is applicable to the repo.
   365  				sum[b.explanation] = []github.PullRequestChange{docFile}
   366  			}
   367  			return sum
   368  		}
   369  		pre := &github.PullRequestEvent{
   370  			Action: tc.action,
   371  			Repo:   github.Repo{Owner: github.User{Login: "org"}, Name: "repo"},
   372  			Number: 1,
   373  		}
   374  		if err := handle(fakeClient, logrus.WithField("plugin", PluginName), tc.config, &fakePruner{}, calcF, pre); err != nil {
   375  			t.Errorf("[%s] Unexpected error from handle: %v.", tc.name, err)
   376  			continue
   377  		}
   378  
   379  		if tc.labelAdded != "" {
   380  			expectAdded = append(expectAdded, formatLabel(tc.labelAdded))
   381  		}
   382  		sort.Strings(expectAdded)
   383  		sort.Strings(fakeClient.IssueLabelsAdded)
   384  		if !reflect.DeepEqual(expectAdded, fakeClient.IssueLabelsAdded) {
   385  			t.Errorf("[%s]: Expected labels to be added: %q, but got: %q.", tc.name, expectAdded, fakeClient.IssueLabelsAdded)
   386  		}
   387  		var expectRemoved []string
   388  		if tc.labelRemoved != "" {
   389  			expectRemoved = append(expectRemoved, formatLabel(tc.labelRemoved))
   390  		}
   391  		sort.Strings(expectRemoved)
   392  		sort.Strings(fakeClient.IssueLabelsRemoved)
   393  		if !reflect.DeepEqual(expectRemoved, fakeClient.IssueLabelsRemoved) {
   394  			t.Errorf("[%s]: Expected labels to be removed: %q, but got: %q.", tc.name, expectRemoved, fakeClient.IssueLabelsRemoved)
   395  		}
   396  
   397  		if count := len(fakeClient.IssueComments[1]); count > 1 {
   398  			t.Errorf("[%s] More than 1 comment created! (%d created).", tc.name, count)
   399  		} else if (count == 1) != tc.commentCreated {
   400  			t.Errorf("[%s] Expected comment created: %t, but got %t.", tc.name, tc.commentCreated, count == 1)
   401  		}
   402  	}
   403  }
   404  
   405  func TestHelpProvider(t *testing.T) {
   406  	enabledRepos := []config.OrgRepo{
   407  		{Org: "org1", Repo: "repo"},
   408  		{Org: "org2", Repo: "repo"},
   409  	}
   410  	cases := []struct {
   411  		name         string
   412  		config       *plugins.Configuration
   413  		enabledRepos []config.OrgRepo
   414  		err          bool
   415  	}{
   416  		{
   417  			name:         "Empty config",
   418  			config:       &plugins.Configuration{},
   419  			enabledRepos: enabledRepos,
   420  		},
   421  		{
   422  			name: "All configs enabled",
   423  			config: &plugins.Configuration{
   424  				Blockades: []plugins.Blockade{
   425  					{
   426  						Repos:            []string{"org2/repo"},
   427  						BranchRegexp:     &releaseBranchRegexp,
   428  						BlockRegexps:     []string{"no", "nope"},
   429  						ExceptionRegexps: []string{"except", "exceptional"},
   430  						Explanation:      "Because I have decided so.",
   431  					},
   432  				},
   433  			},
   434  			enabledRepos: enabledRepos,
   435  		},
   436  	}
   437  	for _, c := range cases {
   438  		t.Run(c.name, func(t *testing.T) {
   439  			_, err := helpProvider(c.config, c.enabledRepos)
   440  			if err != nil && !c.err {
   441  				t.Fatalf("helpProvider error: %v", err)
   442  			}
   443  		})
   444  	}
   445  }