github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/tide/blockers/blockers_test.go (about)

     1  /*
     2  Copyright 2018 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 blockers
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"reflect"
    23  	"strconv"
    24  	"sync"
    25  	"testing"
    26  
    27  	"github.com/google/go-cmp/cmp"
    28  	"github.com/google/go-cmp/cmp/cmpopts"
    29  	githubql "github.com/shurcooL/githubv4"
    30  	"github.com/sirupsen/logrus"
    31  
    32  	"k8s.io/apimachinery/pkg/util/sets"
    33  )
    34  
    35  func TestParseBranches(t *testing.T) {
    36  	tcs := []struct {
    37  		text     string
    38  		expected []string
    39  	}{
    40  		{
    41  			text:     "",
    42  			expected: nil,
    43  		},
    44  		{
    45  			text:     "BAD THINGS (all branches blocked)",
    46  			expected: nil,
    47  		},
    48  		{
    49  			text:     "branch:foo",
    50  			expected: []string{"foo"},
    51  		},
    52  		{
    53  			text:     "branch: foo-bar",
    54  			expected: []string{"foo-bar"},
    55  		},
    56  		{
    57  			text:     "BAD THINGS (BLOCKING BRANCH:foo branch:bar) AHHH",
    58  			expected: []string{"foo", "bar"},
    59  		},
    60  		{
    61  			text:     "branch:\"FOO-bar\"",
    62  			expected: []string{"FOO-bar"},
    63  		},
    64  		{
    65  			text:     "branch: \"foo\" branch: \"bar\"",
    66  			expected: []string{"foo", "bar"},
    67  		},
    68  	}
    69  
    70  	for _, tc := range tcs {
    71  		if got := parseBranches(tc.text); !reflect.DeepEqual(got, tc.expected) {
    72  			t.Errorf("Expected parseBranches(%q)==%q, but got %q.", tc.text, tc.expected, got)
    73  		}
    74  	}
    75  }
    76  
    77  func TestBlockerQuery(t *testing.T) {
    78  	tcs := []struct {
    79  		orgRepoQuery string
    80  		expected     sets.Set[string]
    81  	}{
    82  		{
    83  			orgRepoQuery: "org:\"k8s\"",
    84  			expected: sets.New[string](
    85  				"is:issue",
    86  				"state:open",
    87  				"label:\"blocker\"",
    88  				"org:\"k8s\"",
    89  			),
    90  		},
    91  		{
    92  			orgRepoQuery: "repo:\"k8s/t-i\"",
    93  			expected: sets.New[string](
    94  				"is:issue",
    95  				"state:open",
    96  				"label:\"blocker\"",
    97  				"repo:\"k8s/t-i\"",
    98  			),
    99  		},
   100  		{
   101  			orgRepoQuery: "org:\"k8s\" org:\"kuber\"",
   102  			expected: sets.New[string](
   103  				"is:issue",
   104  				"state:open",
   105  				"label:\"blocker\"",
   106  				"org:\"k8s\"",
   107  				"org:\"kuber\"",
   108  			),
   109  		},
   110  		{
   111  			orgRepoQuery: "repo:\"k8s/t-i\" repo:\"k8s/k8s\"",
   112  			expected: sets.New[string](
   113  				"is:issue",
   114  				"state:open",
   115  				"label:\"blocker\"",
   116  				"repo:\"k8s/t-i\"",
   117  				"repo:\"k8s/k8s\"",
   118  			),
   119  		},
   120  		{
   121  			orgRepoQuery: "org:\"k8s\" org:\"kuber\" repo:\"k8s/t-i\" repo:\"k8s/k8s\"",
   122  			expected: sets.New[string](
   123  				"is:issue",
   124  				"state:open",
   125  				"label:\"blocker\"",
   126  				"repo:\"k8s/t-i\"",
   127  				"repo:\"k8s/k8s\"",
   128  				"org:\"k8s\"",
   129  				"org:\"kuber\"",
   130  			),
   131  		},
   132  	}
   133  
   134  	for _, tc := range tcs {
   135  		got := sets.New[string](blockerQuery("blocker", tc.orgRepoQuery)...)
   136  		if diff := cmp.Diff(got, tc.expected); diff != "" {
   137  			t.Errorf("Actual result differs from expected: %s", diff)
   138  		}
   139  	}
   140  }
   141  
   142  func testIssue(number int, title, org, repo string) Issue {
   143  	return Issue{
   144  		Number: githubql.Int(number),
   145  		Title:  githubql.String(title),
   146  		URL:    githubql.String(strconv.Itoa(number)),
   147  		Repository: struct {
   148  			Name  githubql.String
   149  			Owner struct {
   150  				Login githubql.String
   151  			}
   152  		}{
   153  			Name: githubql.String(repo),
   154  			Owner: struct {
   155  				Login githubql.String
   156  			}{
   157  				Login: githubql.String(org),
   158  			},
   159  		},
   160  	}
   161  }
   162  
   163  func TestBlockers(t *testing.T) {
   164  	type check struct {
   165  		org, repo, branch string
   166  		blockers          sets.Set[int]
   167  	}
   168  
   169  	tcs := []struct {
   170  		name   string
   171  		issues []Issue
   172  		checks []check
   173  	}{
   174  		{
   175  			name:   "No blocker issues",
   176  			issues: []Issue{},
   177  			checks: []check{
   178  				{
   179  					org:      "org",
   180  					repo:     "repo",
   181  					branch:   "branch",
   182  					blockers: sets.New[int](),
   183  				},
   184  			},
   185  		},
   186  		{
   187  			name: "1 repo blocker",
   188  			issues: []Issue{
   189  				testIssue(5, "BLOCK THE WHOLE REPO!", "k", "t-i"),
   190  			},
   191  			checks: []check{
   192  				{
   193  					org:      "k",
   194  					repo:     "t-i",
   195  					branch:   "feature",
   196  					blockers: sets.New[int](5),
   197  				},
   198  				{
   199  					org:      "k",
   200  					repo:     "t-i",
   201  					branch:   "master",
   202  					blockers: sets.New[int](5),
   203  				},
   204  				{
   205  					org:      "k",
   206  					repo:     "k",
   207  					branch:   "master",
   208  					blockers: sets.New[int](),
   209  				},
   210  			},
   211  		},
   212  		{
   213  			name: "1 repo blocker for a branch",
   214  			issues: []Issue{
   215  				testIssue(6, "BLOCK THE release-1.11 BRANCH! branch:release-1.11", "k", "t-i"),
   216  			},
   217  			checks: []check{
   218  				{
   219  					org:      "k",
   220  					repo:     "t-i",
   221  					branch:   "release-1.11",
   222  					blockers: sets.New[int](6),
   223  				},
   224  			},
   225  		},
   226  		{
   227  			name: "1 repo blocker for a branch",
   228  			issues: []Issue{
   229  				testIssue(6, "BLOCK THE slash/in/name BRANCH! branch:slash/in/name", "k", "t-i"),
   230  			},
   231  			checks: []check{
   232  				{
   233  					org:      "k",
   234  					repo:     "t-i",
   235  					branch:   "slash/in/name",
   236  					blockers: sets.New[int](6),
   237  				},
   238  			},
   239  		},
   240  		{
   241  			name: "2 repo blockers for same repo",
   242  			issues: []Issue{
   243  				testIssue(5, "BLOCK THE WHOLE REPO!", "k", "t-i"),
   244  				testIssue(6, "BLOCK THE WHOLE REPO AGAIN!", "k", "t-i"),
   245  			},
   246  			checks: []check{
   247  				{
   248  					org:      "k",
   249  					repo:     "t-i",
   250  					branch:   "feature",
   251  					blockers: sets.New[int](5, 6),
   252  				},
   253  				{
   254  					org:      "k",
   255  					repo:     "t-i",
   256  					branch:   "master",
   257  					blockers: sets.New[int](5, 6),
   258  				},
   259  				{
   260  					org:      "k",
   261  					repo:     "k",
   262  					branch:   "master",
   263  					blockers: sets.New[int](),
   264  				},
   265  			},
   266  		},
   267  		{
   268  			name: "2 repo blockers for different repos",
   269  			issues: []Issue{
   270  				testIssue(5, "BLOCK THE WHOLE REPO!", "k", "t-i"),
   271  				testIssue(6, "BLOCK THE WHOLE (different) REPO!", "k", "community"),
   272  			},
   273  			checks: []check{
   274  				{
   275  					org:      "k",
   276  					repo:     "t-i",
   277  					branch:   "feature",
   278  					blockers: sets.New[int](5),
   279  				},
   280  				{
   281  					org:      "k",
   282  					repo:     "t-i",
   283  					branch:   "master",
   284  					blockers: sets.New[int](5),
   285  				},
   286  				{
   287  					org:      "k",
   288  					repo:     "community",
   289  					branch:   "feature",
   290  					blockers: sets.New[int](6),
   291  				},
   292  				{
   293  					org:      "k",
   294  					repo:     "community",
   295  					branch:   "master",
   296  					blockers: sets.New[int](6),
   297  				},
   298  				{
   299  					org:      "k",
   300  					repo:     "k",
   301  					branch:   "master",
   302  					blockers: sets.New[int](),
   303  				},
   304  			},
   305  		},
   306  		{
   307  			name: "1 repo blocker, 1 branch blocker for different repos",
   308  			issues: []Issue{
   309  				testIssue(5, "BLOCK THE WHOLE REPO!", "k", "t-i"),
   310  				testIssue(6, "BLOCK THE feature BRANCH! branch:feature", "k", "community"),
   311  			},
   312  			checks: []check{
   313  				{
   314  					org:      "k",
   315  					repo:     "t-i",
   316  					branch:   "feature",
   317  					blockers: sets.New[int](5),
   318  				},
   319  				{
   320  					org:      "k",
   321  					repo:     "t-i",
   322  					branch:   "master",
   323  					blockers: sets.New[int](5),
   324  				},
   325  				{
   326  					org:      "k",
   327  					repo:     "community",
   328  					branch:   "feature",
   329  					blockers: sets.New[int](6),
   330  				},
   331  				{
   332  					org:      "k",
   333  					repo:     "community",
   334  					branch:   "master",
   335  					blockers: sets.New[int](),
   336  				},
   337  				{
   338  					org:      "k",
   339  					repo:     "k",
   340  					branch:   "master",
   341  					blockers: sets.New[int](),
   342  				},
   343  			},
   344  		},
   345  		{
   346  			name: "1 repo blocker, 1 branch blocker for same repo",
   347  			issues: []Issue{
   348  				testIssue(5, "BLOCK THE WHOLE REPO!", "k", "t-i"),
   349  				testIssue(6, "BLOCK THE feature BRANCH! branch:feature", "k", "t-i"),
   350  			},
   351  			checks: []check{
   352  				{
   353  					org:      "k",
   354  					repo:     "t-i",
   355  					branch:   "feature",
   356  					blockers: sets.New[int](5, 6),
   357  				},
   358  				{
   359  					org:      "k",
   360  					repo:     "t-i",
   361  					branch:   "master",
   362  					blockers: sets.New[int](5),
   363  				},
   364  				{
   365  					org:      "k",
   366  					repo:     "k",
   367  					branch:   "master",
   368  					blockers: sets.New[int](),
   369  				},
   370  			},
   371  		},
   372  		{
   373  			name: "2 repo blockers, 3 branch blockers (with overlap) for same repo",
   374  			issues: []Issue{
   375  				testIssue(5, "BLOCK THE WHOLE REPO!", "k", "t-i"),
   376  				testIssue(6, "BLOCK THE WHOLE REPO AGAIN!", "k", "t-i"),
   377  				testIssue(7, "BLOCK THE feature BRANCH! branch:feature", "k", "t-i"),
   378  				testIssue(8, "BLOCK THE feature BRANCH! branch:master", "k", "t-i"),
   379  				testIssue(9, "BLOCK THE feature BRANCH! branch:feature branch: master branch:foo", "k", "t-i"),
   380  			},
   381  			checks: []check{
   382  				{
   383  					org:      "k",
   384  					repo:     "t-i",
   385  					branch:   "feature",
   386  					blockers: sets.New[int](5, 6, 7, 9),
   387  				},
   388  				{
   389  					org:      "k",
   390  					repo:     "t-i",
   391  					branch:   "master",
   392  					blockers: sets.New[int](5, 6, 8, 9),
   393  				},
   394  				{
   395  					org:      "k",
   396  					repo:     "t-i",
   397  					branch:   "foo",
   398  					blockers: sets.New[int](5, 6, 9),
   399  				},
   400  				{
   401  					org:      "k",
   402  					repo:     "t-i",
   403  					branch:   "bar",
   404  					blockers: sets.New[int](5, 6),
   405  				},
   406  				{
   407  					org:      "k",
   408  					repo:     "k",
   409  					branch:   "master",
   410  					blockers: sets.New[int](),
   411  				},
   412  			},
   413  		},
   414  	}
   415  
   416  	for _, tc := range tcs {
   417  		t.Logf("Running test case %q.", tc.name)
   418  		b := fromIssues(tc.issues, logrus.WithField("test", tc.name))
   419  		for _, c := range tc.checks {
   420  			actuals := b.GetApplicable(c.org, c.repo, c.branch)
   421  			nums := sets.New[int]()
   422  			for _, actual := range actuals {
   423  				// Check blocker URLs:
   424  				if actual.URL != strconv.Itoa(actual.Number) {
   425  					t.Errorf("blocker %d has URL %q, expected %q", actual.Number, actual.URL, strconv.Itoa(actual.Number))
   426  				}
   427  				nums.Insert(actual.Number)
   428  			}
   429  			// Check that correct blockers were selected:
   430  			if !reflect.DeepEqual(nums, c.blockers) {
   431  				t.Errorf("expected blockers %v, but got %v", c.blockers, nums)
   432  			}
   433  		}
   434  	}
   435  }
   436  
   437  type fakeGitHubClient struct {
   438  	lock    sync.Mutex
   439  	queries map[string][]string
   440  }
   441  
   442  func (fghc *fakeGitHubClient) QueryWithGitHubAppsSupport(ctx context.Context, q interface{}, vars map[string]interface{}, org string) error {
   443  	if query := vars["query"]; query == nil || string(query.(githubql.String)) == "" {
   444  		return fmt.Errorf("query variable was unset, variables: %+v", vars)
   445  	}
   446  
   447  	fghc.lock.Lock()
   448  	defer fghc.lock.Unlock()
   449  
   450  	if fghc.queries == nil {
   451  		fghc.queries = map[string][]string{}
   452  	}
   453  	fghc.queries[org] = append(fghc.queries[org], string(vars["query"].(githubql.String)))
   454  
   455  	return nil
   456  }
   457  
   458  func TestBlockersFindAll(t *testing.T) {
   459  	t.Parallel()
   460  
   461  	orgRepoTokensByOrg := map[string]string{
   462  		"org-a": `org:"org-a" -repo:"org-a/repo-b"`,
   463  		"org-b": `org:"org-b" -repo:"org-b/repo-b"`,
   464  	}
   465  	const blockerLabel = "tide/merge-blocker"
   466  	testCases := []struct {
   467  		name         string
   468  		usesAppsAuth bool
   469  
   470  		expectedQueries map[string][]string
   471  	}{
   472  		{
   473  			name:         "Apps auth, query is split by org",
   474  			usesAppsAuth: true,
   475  			expectedQueries: map[string][]string{
   476  				"org-a": {`-repo:"org-a/repo-b" is:issue label:"tide/merge-blocker" org:"org-a" state:open`},
   477  				"org-b": {`-repo:"org-b/repo-b" is:issue label:"tide/merge-blocker" org:"org-b" state:open`},
   478  			},
   479  		},
   480  		{
   481  			name:         "No apps auth, one query",
   482  			usesAppsAuth: false,
   483  			expectedQueries: map[string][]string{
   484  				"": {`-repo:"org-a/repo-b" -repo:"org-b/repo-b" is:issue label:"tide/merge-blocker" org:"org-a" org:"org-b" state:open`},
   485  			},
   486  		},
   487  	}
   488  
   489  	for _, tc := range testCases {
   490  		t.Run(tc.name, func(t *testing.T) {
   491  			ghc := &fakeGitHubClient{}
   492  
   493  			if _, err := FindAll(ghc, logrus.WithField("tc", tc.name), blockerLabel, orgRepoTokensByOrg, tc.usesAppsAuth); err != nil {
   494  				t.Fatalf("FindAll: %v", err)
   495  			}
   496  
   497  			if diff := cmp.Diff(ghc.queries, tc.expectedQueries, cmpopts.SortSlices(func(a, b string) bool { return a < b })); diff != "" {
   498  				t.Errorf("actual queries differ from expected: %v", diff)
   499  			}
   500  		})
   501  	}
   502  }