sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/config/tide_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 config
    18  
    19  import (
    20  	"fmt"
    21  	"reflect"
    22  	"regexp"
    23  	"strings"
    24  	"testing"
    25  
    26  	"github.com/google/go-cmp/cmp"
    27  	"k8s.io/apimachinery/pkg/util/diff"
    28  	"k8s.io/apimachinery/pkg/util/sets"
    29  	utilpointer "k8s.io/utils/pointer"
    30  	"sigs.k8s.io/yaml"
    31  
    32  	"sigs.k8s.io/prow/pkg/git/types"
    33  	"sigs.k8s.io/prow/pkg/git/v2"
    34  	"sigs.k8s.io/prow/pkg/labels"
    35  )
    36  
    37  var testQuery = TideQuery{
    38  	Orgs:                   []string{"org"},
    39  	Repos:                  []string{"k/k", "k/t-i"},
    40  	ExcludedRepos:          []string{"org/repo"},
    41  	Labels:                 []string{labels.LGTM, labels.Approved, "this,or,that"},
    42  	MissingLabels:          []string{"foo"},
    43  	Author:                 "batman",
    44  	Milestone:              "milestone",
    45  	ReviewApprovedRequired: true,
    46  }
    47  
    48  var expectedQueryComponents = []string{
    49  	"is:pr",
    50  	"state:open",
    51  	"archived:false",
    52  	"label:\"lgtm\"",
    53  	"label:\"approved\"",
    54  	"label:\"this\",\"or\",\"that\"",
    55  	"-label:\"foo\"",
    56  	"author:\"batman\"",
    57  	"milestone:\"milestone\"",
    58  	"review:approved",
    59  }
    60  
    61  func TestMarshalMergeMethod(t *testing.T) {
    62  	testCases := []struct {
    63  		testName   string
    64  		tideConfig TideGitHubConfig
    65  		wantYaml   string
    66  	}{
    67  		{
    68  			testName: "Org-wide",
    69  			tideConfig: TideGitHubConfig{
    70  				MergeType: map[string]TideOrgMergeType{
    71  					"org1": {
    72  						MergeType: "squash",
    73  					},
    74  				},
    75  			},
    76  			wantYaml: `context_options: {}
    77  merge_method:
    78    org1: squash
    79  `,
    80  		},
    81  		{
    82  			testName: "Empty org",
    83  			tideConfig: TideGitHubConfig{
    84  				MergeType: map[string]TideOrgMergeType{
    85  					"org1": {},
    86  				},
    87  			},
    88  			wantYaml: `context_options: {}
    89  merge_method:
    90    org1: ""
    91  `,
    92  		},
    93  		{
    94  			testName: "Repo-wide",
    95  			tideConfig: TideGitHubConfig{
    96  				MergeType: map[string]TideOrgMergeType{
    97  					"org1": {
    98  						Repos: map[string]TideRepoMergeType{
    99  							"repo1": {
   100  								MergeType: "rebase",
   101  							},
   102  						},
   103  					},
   104  				},
   105  			},
   106  			wantYaml: `context_options: {}
   107  merge_method:
   108    org1:
   109      repo1: rebase
   110  `,
   111  		},
   112  		{
   113  			testName: "Empty repo",
   114  			tideConfig: TideGitHubConfig{
   115  				MergeType: map[string]TideOrgMergeType{
   116  					"org1": {
   117  						Repos: map[string]TideRepoMergeType{
   118  							"repo1": {},
   119  						},
   120  					},
   121  				},
   122  			},
   123  			wantYaml: `context_options: {}
   124  merge_method:
   125    org1:
   126      repo1: ""
   127  `,
   128  		},
   129  		{
   130  			testName: "Multiple branches",
   131  			tideConfig: TideGitHubConfig{
   132  				MergeType: map[string]TideOrgMergeType{
   133  					"org1": {
   134  						Repos: map[string]TideRepoMergeType{
   135  							"repo1": {
   136  								Branches: map[string]TideBranchMergeType{
   137  									"branch1": {
   138  										Regexpr:   regexp.MustCompile("branch1"),
   139  										MergeType: types.MergeMerge,
   140  									},
   141  									"branch2": {
   142  										Regexpr:   regexp.MustCompile("branch2"),
   143  										MergeType: types.MergeSquash,
   144  									},
   145  								},
   146  							},
   147  						},
   148  					},
   149  				},
   150  			},
   151  			wantYaml: `context_options: {}
   152  merge_method:
   153    org1:
   154      repo1:
   155        branch1: merge
   156        branch2: squash
   157  `,
   158  		},
   159  		{
   160  			testName: "Complex",
   161  			tideConfig: TideGitHubConfig{
   162  				MergeType: map[string]TideOrgMergeType{
   163  					"org1": {
   164  						Repos: map[string]TideRepoMergeType{
   165  							"repo1": {
   166  								Branches: map[string]TideBranchMergeType{
   167  									"branch1": {
   168  										Regexpr:   regexp.MustCompile("branch1"),
   169  										MergeType: types.MergeMerge,
   170  									},
   171  									"branch2": {
   172  										Regexpr:   regexp.MustCompile("branch2"),
   173  										MergeType: types.MergeSquash,
   174  									},
   175  								},
   176  							},
   177  						},
   178  					},
   179  					"org2/repo1@master": {
   180  						MergeType: "squash",
   181  					},
   182  					"org2": {
   183  						Repos: map[string]TideRepoMergeType{
   184  							"repo2": {
   185  								MergeType: "merge",
   186  							},
   187  						},
   188  					},
   189  					"org3": {
   190  						MergeType: "rebase",
   191  					},
   192  				},
   193  			},
   194  			wantYaml: `context_options: {}
   195  merge_method:
   196    org1:
   197      repo1:
   198        branch1: merge
   199        branch2: squash
   200    org2:
   201      repo2: merge
   202    org2/repo1@master: squash
   203    org3: rebase
   204  `,
   205  		},
   206  	}
   207  	for _, testCase := range testCases {
   208  		t.Run(testCase.testName, func(t *testing.T) {
   209  			yaml, err := yaml.Marshal(testCase.tideConfig)
   210  			if err != nil {
   211  				t.Errorf("unmarshal error: %v", err)
   212  			}
   213  			if diff := cmp.Diff(testCase.wantYaml, string(yaml)); diff != "" {
   214  				t.Errorf("unexpected yaml: %s", diff)
   215  			}
   216  		})
   217  	}
   218  }
   219  
   220  func TestUnmarshalMergeMethod(t *testing.T) {
   221  	testCases := []struct {
   222  		testName   string
   223  		yamlConfig string
   224  		wantConfig Config
   225  	}{
   226  		{
   227  			testName:   "No merge config",
   228  			yamlConfig: `tide:`,
   229  			wantConfig: Config{
   230  				ProwConfig: ProwConfig{
   231  					Tide: Tide{},
   232  				},
   233  			},
   234  		},
   235  		{
   236  			testName: "Mix OrgRepo and branches config",
   237  			yamlConfig: `
   238  tide:
   239    merge_method:
   240      org1:
   241        repo1:
   242          branch1: merge
   243      org2/repo1: rebase`,
   244  			wantConfig: Config{
   245  				ProwConfig: ProwConfig{
   246  					Tide: Tide{
   247  						TideGitHubConfig: TideGitHubConfig{
   248  							MergeType: map[string]TideOrgMergeType{
   249  								"org1": {
   250  									Repos: map[string]TideRepoMergeType{
   251  										"repo1": {
   252  											Branches: map[string]TideBranchMergeType{
   253  												"branch1": {
   254  													MergeType: types.MergeMerge,
   255  												},
   256  											},
   257  										},
   258  									},
   259  								},
   260  								"org2/repo1": {
   261  									MergeType: types.MergeRebase,
   262  								},
   263  							},
   264  						},
   265  					},
   266  				},
   267  			},
   268  		},
   269  		{
   270  			testName: "The same repo-wide and per branch config",
   271  			yamlConfig: `
   272  tide:
   273    merge_method:
   274      org1:
   275        repo1:
   276          branch1: merge
   277      org1/repo1: rebase`,
   278  			wantConfig: Config{
   279  				ProwConfig: ProwConfig{
   280  					Tide: Tide{
   281  						TideGitHubConfig: TideGitHubConfig{
   282  							MergeType: map[string]TideOrgMergeType{
   283  								"org1": {
   284  									Repos: map[string]TideRepoMergeType{
   285  										"repo1": {
   286  											Branches: map[string]TideBranchMergeType{
   287  												"branch1": {
   288  													MergeType: types.MergeMerge,
   289  												},
   290  											},
   291  										},
   292  									},
   293  								},
   294  								"org1/repo1": {
   295  									MergeType: types.MergeRebase,
   296  								},
   297  							},
   298  						},
   299  					},
   300  				},
   301  			},
   302  		},
   303  		{
   304  			testName: "Legacy repo only config",
   305  			yamlConfig: `
   306  tide:
   307    merge_method:
   308      org1/repo1: merge`,
   309  			wantConfig: Config{
   310  				ProwConfig: ProwConfig{
   311  					Tide: Tide{
   312  						TideGitHubConfig: TideGitHubConfig{
   313  							MergeType: map[string]TideOrgMergeType{
   314  								"org1/repo1": {
   315  									MergeType: types.MergeMerge,
   316  								},
   317  							},
   318  						},
   319  					},
   320  				},
   321  			},
   322  		},
   323  		{
   324  			testName: "Repo only config",
   325  			yamlConfig: `
   326  tide:
   327    merge_method:
   328      org1:
   329        repo1: merge`,
   330  			wantConfig: Config{
   331  				ProwConfig: ProwConfig{
   332  					Tide: Tide{
   333  						TideGitHubConfig: TideGitHubConfig{
   334  							MergeType: map[string]TideOrgMergeType{
   335  								"org1": {
   336  									Repos: map[string]TideRepoMergeType{
   337  										"repo1": {
   338  											MergeType: types.MergeMerge,
   339  										},
   340  									},
   341  								},
   342  							},
   343  						},
   344  					},
   345  				},
   346  			},
   347  		},
   348  		{
   349  			testName: "Org only config",
   350  			yamlConfig: `
   351  tide:
   352    merge_method:
   353      org1: rebase`,
   354  			wantConfig: Config{
   355  				ProwConfig: ProwConfig{
   356  					Tide: Tide{
   357  						TideGitHubConfig: TideGitHubConfig{
   358  							MergeType: map[string]TideOrgMergeType{
   359  								"org1": {
   360  									MergeType: types.MergeRebase,
   361  								},
   362  							},
   363  						},
   364  					},
   365  				},
   366  			},
   367  		},
   368  		{
   369  			testName: "Branches only config",
   370  			yamlConfig: `
   371  tide:
   372    merge_method:
   373      org1:
   374        repo1:
   375          branch1: merge`,
   376  			wantConfig: Config{
   377  				ProwConfig: ProwConfig{
   378  					Tide: Tide{
   379  						TideGitHubConfig: TideGitHubConfig{
   380  							MergeType: map[string]TideOrgMergeType{
   381  								"org1": {
   382  									Repos: map[string]TideRepoMergeType{
   383  										"repo1": {
   384  											Branches: map[string]TideBranchMergeType{
   385  												"branch1": {
   386  													MergeType: types.MergeMerge,
   387  												},
   388  											},
   389  										},
   390  									},
   391  								},
   392  							},
   393  						},
   394  					},
   395  				},
   396  			},
   397  		},
   398  		{
   399  			testName: "Branches config: wildcard on branch and repo",
   400  			yamlConfig: `
   401  tide:
   402    merge_method:
   403      org1:
   404        ".*":
   405          ".+": merge`,
   406  			wantConfig: Config{
   407  				ProwConfig: ProwConfig{
   408  					Tide: Tide{
   409  						TideGitHubConfig: TideGitHubConfig{
   410  							MergeType: map[string]TideOrgMergeType{
   411  								"org1": {
   412  									Repos: map[string]TideRepoMergeType{
   413  										".*": {
   414  											Branches: map[string]TideBranchMergeType{
   415  												".+": {
   416  													MergeType: types.MergeMerge,
   417  												},
   418  											},
   419  										},
   420  									},
   421  								},
   422  							},
   423  						},
   424  					},
   425  				},
   426  			},
   427  		},
   428  		{
   429  			testName: "Branches config: no repos at all",
   430  			yamlConfig: `
   431  tide:
   432    merge_method:
   433      ".*":`,
   434  			wantConfig: Config{
   435  				ProwConfig: ProwConfig{
   436  					Tide: Tide{
   437  						TideGitHubConfig: TideGitHubConfig{
   438  							MergeType: map[string]TideOrgMergeType{
   439  								".*": {},
   440  							},
   441  						},
   442  					},
   443  				},
   444  			},
   445  		},
   446  	}
   447  	for _, testCase := range testCases {
   448  		t.Run(testCase.testName, func(t *testing.T) {
   449  			var config Config
   450  			if err := yaml.Unmarshal([]byte(testCase.yamlConfig), &config); err != nil {
   451  				t.Fatalf("unmarshal error: %v", err)
   452  			}
   453  			if diff := cmp.Diff(&testCase.wantConfig, &config); diff != "" {
   454  				t.Errorf("merge method configurations differ: %s", diff)
   455  			}
   456  		})
   457  	}
   458  }
   459  
   460  func TestTideQuery(t *testing.T) {
   461  	q := " " + testQuery.Query() + " "
   462  	checkTok := checkTok(t, q)
   463  
   464  	checkTok("org:\"org\"")
   465  	checkTok("repo:\"k/k\"")
   466  	checkTok("repo:\"k/t-i\"")
   467  	checkTok("-repo:\"org/repo\"")
   468  	for _, expectedComponent := range expectedQueryComponents {
   469  		checkTok(expectedComponent)
   470  	}
   471  
   472  	elements := strings.Fields(q)
   473  	alreadySeen := sets.Set[string]{}
   474  	for _, element := range elements {
   475  		if alreadySeen.Has(element) {
   476  			t.Errorf("element %q was multiple times in the query string", element)
   477  		}
   478  		alreadySeen.Insert(element)
   479  	}
   480  }
   481  
   482  func checkTok(t *testing.T, q string) func(tok string) {
   483  	return func(tok string) {
   484  		t.Run("Query string contains "+tok, func(t *testing.T) {
   485  			if !strings.Contains(q, " "+tok+" ") {
   486  				t.Errorf("Expected query to contain \"%s\", got \"%s\"", tok, q)
   487  			}
   488  		})
   489  	}
   490  }
   491  
   492  func TestOrgQueries(t *testing.T) {
   493  	queries := testQuery.OrgQueries()
   494  	if n := len(queries); n != 2 {
   495  		t.Errorf("expected exactly two queries, got %d", n)
   496  	}
   497  	if queries["org"] == "" {
   498  		t.Error("no query for org org found")
   499  	}
   500  	if queries["k"] == "" {
   501  		t.Error("no query for org k found")
   502  	}
   503  
   504  	for org, query := range queries {
   505  		t.Run(org, func(t *testing.T) {
   506  			checkTok := checkTok(t, " "+query+" ")
   507  			t.Logf("query: %s", query)
   508  
   509  			for _, expectedComponent := range expectedQueryComponents {
   510  				checkTok(expectedComponent)
   511  			}
   512  
   513  			elements := strings.Fields(query)
   514  			alreadySeen := sets.Set[string]{}
   515  			for _, element := range elements {
   516  				if alreadySeen.Has(element) {
   517  					t.Errorf("element %q was multiple times in the query string", element)
   518  				}
   519  				alreadySeen.Insert(element)
   520  			}
   521  
   522  			if org == "org" {
   523  				checkTok(`org:"org"`)
   524  				checkTok(`-repo:"org/repo"`)
   525  			}
   526  
   527  			if org == "k" {
   528  				for _, repo := range testQuery.Repos {
   529  					checkTok(fmt.Sprintf(`repo:"%s"`, repo))
   530  				}
   531  			}
   532  		})
   533  	}
   534  }
   535  
   536  func TestOrgExceptionsAndRepos(t *testing.T) {
   537  	queries := TideQueries{
   538  		{
   539  			Orgs:          []string{"k8s"},
   540  			ExcludedRepos: []string{"k8s/k8s"},
   541  		},
   542  		{
   543  			Orgs:          []string{"kuber"},
   544  			Repos:         []string{"foo/bar", "baz/bar"},
   545  			ExcludedRepos: []string{"kuber/netes"},
   546  		},
   547  		{
   548  			Orgs:          []string{"k8s"},
   549  			ExcludedRepos: []string{"k8s/k8s", "k8s/t-i"},
   550  		},
   551  		{
   552  			Orgs:          []string{"org", "org2"},
   553  			ExcludedRepos: []string{"org2/repo", "org2/repo2", "org2/repo3"},
   554  		},
   555  		{
   556  			Orgs:  []string{"foo"},
   557  			Repos: []string{"org2/repo3"},
   558  		},
   559  	}
   560  
   561  	expectedOrgs := map[string]sets.Set[string]{
   562  		"foo":   sets.New[string](),
   563  		"k8s":   sets.New[string]("k8s/k8s"),
   564  		"kuber": sets.New[string]("kuber/netes"),
   565  		"org":   sets.New[string](),
   566  		"org2":  sets.New[string]("org2/repo", "org2/repo2"),
   567  	}
   568  	expectedRepos := sets.New[string]("foo/bar", "baz/bar", "org2/repo3")
   569  
   570  	orgs, repos := queries.OrgExceptionsAndRepos()
   571  	if !reflect.DeepEqual(orgs, expectedOrgs) {
   572  		t.Errorf("Expected org map %v, but got %v.", expectedOrgs, orgs)
   573  	}
   574  	if !repos.Equal(expectedRepos) {
   575  		t.Errorf("Expected repo set %v, but got %v.", expectedRepos, repos)
   576  	}
   577  }
   578  
   579  func TestMergeMethod(t *testing.T) {
   580  	ti := &Tide{
   581  		TideGitHubConfig: TideGitHubConfig{
   582  			MergeType: map[string]TideOrgMergeType{
   583  				"kubernetes/kops":             {MergeType: types.MergeRebase},
   584  				"kubernetes-helm":             {MergeType: types.MergeSquash},
   585  				"kubernetes-helm/chartmuseum": {MergeType: types.MergeMerge},
   586  			},
   587  		},
   588  	}
   589  
   590  	var testcases = []struct {
   591  		org      string
   592  		repo     string
   593  		expected types.PullRequestMergeType
   594  	}{
   595  		{
   596  			"kubernetes",
   597  			"kubernetes",
   598  			types.MergeMerge,
   599  		},
   600  		{
   601  			"kubernetes",
   602  			"kops",
   603  			types.MergeRebase,
   604  		},
   605  		{
   606  			"kubernetes-helm",
   607  			"monocular",
   608  			types.MergeSquash,
   609  		},
   610  		{
   611  			"kubernetes-helm",
   612  			"chartmuseum",
   613  			types.MergeMerge,
   614  		},
   615  	}
   616  
   617  	for _, test := range testcases {
   618  		actual := ti.MergeMethod(OrgRepo{Org: test.org, Repo: test.repo})
   619  		if actual != test.expected {
   620  			t.Errorf("Expected merge method %q but got %q for %s/%s", test.expected, actual, test.org, test.repo)
   621  		}
   622  	}
   623  }
   624  
   625  func TestOrgRepoMatchMergeMethod(t *testing.T) {
   626  	var testCases = []struct {
   627  		name     string
   628  		config   Tide
   629  		org      string
   630  		repo     string
   631  		branch   string
   632  		expected types.PullRequestMergeType
   633  	}{
   634  		// Edge cases
   635  		{
   636  			name: "No input at all",
   637  			config: Tide{
   638  				TideGitHubConfig: TideGitHubConfig{
   639  					MergeType: map[string]TideOrgMergeType{
   640  						"kubernetes": {
   641  							Repos: map[string]TideRepoMergeType{
   642  								"test-infra": {
   643  									Branches: map[string]TideBranchMergeType{
   644  										"master": {
   645  											Regexpr:   regexp.MustCompile("master"),
   646  											MergeType: types.MergeRebase,
   647  										},
   648  									},
   649  								},
   650  							},
   651  						},
   652  					},
   653  				},
   654  			},
   655  			expected: types.MergeMerge,
   656  		},
   657  		{
   658  			name:     "Empty tide config",
   659  			config:   Tide{},
   660  			org:      "kubernetes",
   661  			repo:     "test-infra",
   662  			branch:   "master",
   663  			expected: types.MergeMerge,
   664  		},
   665  		{
   666  			name:     "Empty tide config and no input",
   667  			config:   Tide{},
   668  			expected: types.MergeMerge,
   669  		},
   670  		// Shorthands
   671  		{
   672  			name: "org shorthand: match",
   673  			config: Tide{
   674  				TideGitHubConfig: TideGitHubConfig{
   675  					MergeType: map[string]TideOrgMergeType{
   676  						"kubernetes-helm": {MergeType: types.MergeSquash},
   677  					},
   678  				},
   679  			},
   680  			org:      "kubernetes-helm",
   681  			expected: types.MergeSquash,
   682  		},
   683  		{
   684  			name: "org shorthand: no match",
   685  			config: Tide{
   686  				TideGitHubConfig: TideGitHubConfig{
   687  					MergeType: map[string]TideOrgMergeType{
   688  						"kubernetes-helm": {MergeType: types.MergeSquash},
   689  					},
   690  				},
   691  			},
   692  			org:      "kubernetes",
   693  			expected: types.MergeMerge,
   694  		},
   695  		{
   696  			name: "org shorthand: no org provided",
   697  			config: Tide{
   698  				TideGitHubConfig: TideGitHubConfig{
   699  					MergeType: map[string]TideOrgMergeType{
   700  						"kubernetes": {MergeType: types.MergeRebase},
   701  					},
   702  				},
   703  			},
   704  			expected: types.MergeMerge,
   705  		},
   706  		{
   707  			name: "org shorthand: neither repo nor branch matches",
   708  			config: Tide{
   709  				TideGitHubConfig: TideGitHubConfig{
   710  					MergeType: map[string]TideOrgMergeType{
   711  						"kubernetes": {MergeType: types.MergeRebase},
   712  					},
   713  				},
   714  			},
   715  			org:      "kubernetes",
   716  			repo:     "test-infra",
   717  			branch:   "dev",
   718  			expected: types.MergeRebase,
   719  		},
   720  		{
   721  			name: "org/repo shorthand: match",
   722  			config: Tide{
   723  				TideGitHubConfig: TideGitHubConfig{
   724  					MergeType: map[string]TideOrgMergeType{
   725  						"kubernetes-helm/chartmuseum": {MergeType: types.MergeSquash},
   726  					},
   727  				},
   728  			},
   729  			org:      "kubernetes-helm",
   730  			repo:     "chartmuseum",
   731  			expected: types.MergeSquash,
   732  		},
   733  		{
   734  			name: "org/repo shorthand: no match",
   735  			config: Tide{
   736  				TideGitHubConfig: TideGitHubConfig{
   737  					MergeType: map[string]TideOrgMergeType{
   738  						"kubernetes-helm/chartmuseum": {MergeType: types.MergeSquash},
   739  					},
   740  				},
   741  			},
   742  			org:      "kubernetes-helm",
   743  			repo:     "test-infra",
   744  			expected: types.MergeMerge,
   745  		},
   746  		{
   747  			name: "org/repo shorthand: no repo provided",
   748  			config: Tide{
   749  				TideGitHubConfig: TideGitHubConfig{
   750  					MergeType: map[string]TideOrgMergeType{
   751  						"kubernetes-helm/chartmuseum": {MergeType: types.MergeSquash},
   752  					},
   753  				},
   754  			},
   755  			org:      "kubernetes-helm",
   756  			expected: types.MergeMerge,
   757  		},
   758  		{
   759  			name: "org/repo shorthand: org only match",
   760  			config: Tide{
   761  				TideGitHubConfig: TideGitHubConfig{
   762  					MergeType: map[string]TideOrgMergeType{
   763  						"kubernetes-helm": {MergeType: types.MergeSquash},
   764  					},
   765  				},
   766  			},
   767  			org:      "kubernetes-helm",
   768  			repo:     "chartmuseum",
   769  			expected: types.MergeSquash,
   770  		},
   771  		{
   772  			name: "org/repo shorthand: fallback to org/repo when branch doesn't match",
   773  			config: Tide{
   774  				TideGitHubConfig: TideGitHubConfig{
   775  					MergeType: map[string]TideOrgMergeType{
   776  						"kubernetes-helm/chartmuseum": {MergeType: types.MergeSquash},
   777  					},
   778  				},
   779  			},
   780  			org:      "kubernetes-helm",
   781  			repo:     "chartmuseum",
   782  			branch:   "master",
   783  			expected: types.MergeSquash,
   784  		},
   785  		{
   786  			name: "org/repo@branch shorthand: match",
   787  			config: Tide{
   788  				TideGitHubConfig: TideGitHubConfig{
   789  					MergeType: map[string]TideOrgMergeType{
   790  						"kubernetes/kops@main": {MergeType: types.MergeRebase},
   791  					},
   792  				},
   793  			},
   794  			org:      "kubernetes",
   795  			repo:     "kops",
   796  			branch:   "main",
   797  			expected: types.MergeRebase,
   798  		},
   799  		{
   800  			name: "org/repo@branch shorthand: no match",
   801  			config: Tide{
   802  				TideGitHubConfig: TideGitHubConfig{
   803  					MergeType: map[string]TideOrgMergeType{
   804  						"kubernetes/kops@main": {MergeType: types.MergeRebase},
   805  					},
   806  				},
   807  			},
   808  			org:      "kubernetes",
   809  			repo:     "kops",
   810  			branch:   "master",
   811  			expected: types.MergeMerge,
   812  		},
   813  		{
   814  			name: "org/repo@branch shorthand: no branch provided",
   815  			config: Tide{
   816  				TideGitHubConfig: TideGitHubConfig{
   817  					MergeType: map[string]TideOrgMergeType{
   818  						"kubernetes/kops@main": {MergeType: types.MergeRebase},
   819  					},
   820  				},
   821  			},
   822  			org:      "kubernetes",
   823  			repo:     "kops",
   824  			expected: types.MergeMerge,
   825  		},
   826  		// Repo-wide config
   827  		{
   828  			name: "Repo-wide config: match",
   829  			config: Tide{
   830  				TideGitHubConfig: TideGitHubConfig{
   831  					MergeType: map[string]TideOrgMergeType{
   832  						"kubernetes": {
   833  							Repos: map[string]TideRepoMergeType{
   834  								"kubernetes": {MergeType: types.MergeIfNecessary},
   835  							},
   836  						},
   837  					},
   838  				},
   839  			},
   840  			org:      "kubernetes",
   841  			repo:     "kubernetes",
   842  			expected: types.MergeIfNecessary,
   843  		},
   844  		{
   845  			name: "Repo-wide config: no match",
   846  			config: Tide{
   847  				TideGitHubConfig: TideGitHubConfig{
   848  					MergeType: map[string]TideOrgMergeType{
   849  						"kubernetes": {
   850  							Repos: map[string]TideRepoMergeType{
   851  								"kubernetes": {MergeType: types.MergeIfNecessary},
   852  							},
   853  						},
   854  					},
   855  				},
   856  			},
   857  			org:      "kubernetes",
   858  			repo:     "test-infra",
   859  			expected: types.MergeMerge,
   860  		},
   861  		{
   862  			name: "Repo-wide config: match using '*'",
   863  			config: Tide{
   864  				TideGitHubConfig: TideGitHubConfig{
   865  					MergeType: map[string]TideOrgMergeType{
   866  						"kubernetes": {
   867  							Repos: map[string]TideRepoMergeType{
   868  								"*":          {MergeType: types.MergeIfNecessary},
   869  								"kubernetes": {MergeType: types.MergeSquash},
   870  							},
   871  						},
   872  					},
   873  				},
   874  			},
   875  			org:      "kubernetes",
   876  			repo:     "test-infra",
   877  			expected: types.MergeIfNecessary,
   878  		},
   879  		// Branch level config
   880  		{
   881  			name: "Branch level config: no match",
   882  			config: Tide{
   883  				TideGitHubConfig: TideGitHubConfig{
   884  					MergeType: map[string]TideOrgMergeType{
   885  						"kubernetes": {
   886  							Repos: map[string]TideRepoMergeType{
   887  								"test-infra": {
   888  									Branches: map[string]TideBranchMergeType{
   889  										"master": {
   890  											Regexpr:   regexp.MustCompile("master"),
   891  											MergeType: types.MergeRebase,
   892  										},
   893  									},
   894  								},
   895  							},
   896  						},
   897  					},
   898  				},
   899  			},
   900  			org:      "kubernetes",
   901  			repo:     "test-infra",
   902  			branch:   "main",
   903  			expected: types.MergeMerge,
   904  		},
   905  		{
   906  			name: "Branch level config: match no regex",
   907  			config: Tide{
   908  				TideGitHubConfig: TideGitHubConfig{
   909  					MergeType: map[string]TideOrgMergeType{
   910  						"kubernetes": {
   911  							Repos: map[string]TideRepoMergeType{
   912  								"test-infra": {
   913  									Branches: map[string]TideBranchMergeType{
   914  										"master": {
   915  											Regexpr:   regexp.MustCompile("master"),
   916  											MergeType: types.MergeRebase,
   917  										},
   918  									},
   919  								},
   920  							},
   921  						},
   922  					},
   923  				},
   924  			},
   925  			org:      "kubernetes",
   926  			repo:     "test-infra",
   927  			branch:   "master",
   928  			expected: types.MergeRebase,
   929  		},
   930  		{
   931  			name: "Branch level config: match regex",
   932  			config: Tide{
   933  				TideGitHubConfig: TideGitHubConfig{
   934  					MergeType: map[string]TideOrgMergeType{
   935  						"kubernetes": {
   936  							Repos: map[string]TideRepoMergeType{
   937  								"test-infra": {
   938  									Branches: map[string]TideBranchMergeType{
   939  										`release-\d+(.\d+)?`: {
   940  											Regexpr:   regexp.MustCompile(`release-\d+(.\d+)?`),
   941  											MergeType: types.MergeSquash,
   942  										},
   943  									},
   944  								},
   945  							},
   946  						},
   947  					},
   948  				},
   949  			},
   950  			org:      "kubernetes",
   951  			repo:     "test-infra",
   952  			branch:   "release-0.2",
   953  			expected: types.MergeSquash,
   954  		},
   955  		{
   956  			name: "Branch level config: multiple regex matches, pick the first one",
   957  			config: Tide{
   958  				TideGitHubConfig: TideGitHubConfig{
   959  					MergeType: map[string]TideOrgMergeType{
   960  						"kubernetes": {
   961  							Repos: map[string]TideRepoMergeType{
   962  								"test-infra": {
   963  									Branches: map[string]TideBranchMergeType{
   964  										`ma.*`: {
   965  											Regexpr:   regexp.MustCompile(`ma.*`),
   966  											MergeType: types.MergeSquash,
   967  										},
   968  										`mast.*`: {
   969  											Regexpr:   regexp.MustCompile(`ma.*`),
   970  											MergeType: types.MergeIfNecessary,
   971  										},
   972  									},
   973  								},
   974  							},
   975  						},
   976  					},
   977  				},
   978  			},
   979  			org:      "kubernetes",
   980  			repo:     "test-infra",
   981  			branch:   "master",
   982  			expected: types.MergeSquash,
   983  		},
   984  		{
   985  			name: "Branch level config: match '*' wildcard at repository level",
   986  			config: Tide{
   987  				TideGitHubConfig: TideGitHubConfig{
   988  					MergeType: map[string]TideOrgMergeType{
   989  						"golang": {
   990  							Repos: map[string]TideRepoMergeType{
   991  								"*": {
   992  									Branches: map[string]TideBranchMergeType{
   993  										"main": {
   994  											Regexpr:   regexp.MustCompile("main"),
   995  											MergeType: types.MergeIfNecessary,
   996  										},
   997  									},
   998  								},
   999  							},
  1000  						},
  1001  					},
  1002  				},
  1003  			},
  1004  			org:      "golang",
  1005  			repo:     "docs",
  1006  			branch:   "main",
  1007  			expected: types.MergeIfNecessary,
  1008  		},
  1009  		// Precedences
  1010  		{
  1011  			name: "Precedence: org/repo@branch shorthand over branch level config",
  1012  			config: Tide{
  1013  				TideGitHubConfig: TideGitHubConfig{
  1014  					MergeType: map[string]TideOrgMergeType{
  1015  						"kubernetes/kops@main": {MergeType: types.MergeSquash},
  1016  						"kubernetes": {
  1017  							Repos: map[string]TideRepoMergeType{
  1018  								"kops": {
  1019  									Branches: map[string]TideBranchMergeType{
  1020  										"main": {
  1021  											Regexpr:   regexp.MustCompile("main"),
  1022  											MergeType: types.MergeIfNecessary,
  1023  										},
  1024  									},
  1025  								},
  1026  							},
  1027  						},
  1028  					},
  1029  				},
  1030  			},
  1031  			org:      "kubernetes",
  1032  			repo:     "kops",
  1033  			branch:   "main",
  1034  			expected: types.MergeSquash,
  1035  		},
  1036  		{
  1037  			name: "Precedence: branch level config over org/repo shorthand",
  1038  			config: Tide{
  1039  				TideGitHubConfig: TideGitHubConfig{
  1040  					MergeType: map[string]TideOrgMergeType{
  1041  						"kubernetes/kops": {MergeType: types.MergeSquash},
  1042  						"kubernetes": {
  1043  							Repos: map[string]TideRepoMergeType{
  1044  								"kops": {
  1045  									Branches: map[string]TideBranchMergeType{
  1046  										"main": {
  1047  											Regexpr:   regexp.MustCompile("main"),
  1048  											MergeType: types.MergeIfNecessary,
  1049  										},
  1050  									},
  1051  								},
  1052  							},
  1053  						},
  1054  					},
  1055  				},
  1056  			},
  1057  			org:      "kubernetes",
  1058  			repo:     "kops",
  1059  			branch:   "main",
  1060  			expected: types.MergeIfNecessary,
  1061  		},
  1062  		{
  1063  			name: "Precedence: org/repo shorthand over repo-wide config",
  1064  			config: Tide{
  1065  				TideGitHubConfig: TideGitHubConfig{
  1066  					MergeType: map[string]TideOrgMergeType{
  1067  						"kubernetes/kops": {MergeType: types.MergeSquash},
  1068  						"kubernetes": {
  1069  							Repos: map[string]TideRepoMergeType{
  1070  								"kops": {MergeType: types.MergeRebase},
  1071  							},
  1072  						},
  1073  					},
  1074  				},
  1075  			},
  1076  			org:      "kubernetes",
  1077  			repo:     "kops",
  1078  			expected: types.MergeSquash,
  1079  		},
  1080  		{
  1081  			name: "Precedence: org/repo shorthand over org config",
  1082  			config: Tide{
  1083  				TideGitHubConfig: TideGitHubConfig{
  1084  					MergeType: map[string]TideOrgMergeType{
  1085  						"kubernetes/kops": {MergeType: types.MergeSquash},
  1086  						"kubernetes":      {MergeType: types.MergeIfNecessary},
  1087  					},
  1088  				},
  1089  			},
  1090  			org:      "kubernetes",
  1091  			repo:     "kops",
  1092  			expected: types.MergeSquash,
  1093  		},
  1094  	}
  1095  	for _, test := range testCases {
  1096  		test := test
  1097  		t.Run(test.name, func(t *testing.T) {
  1098  			actual := test.config.OrgRepoBranchMergeMethod(OrgRepo{Org: test.org, Repo: test.repo}, test.branch)
  1099  			if actual != test.expected {
  1100  				t.Errorf("Expected merge method %q but got %q for org: %q, repo: %q, branch: %q",
  1101  					test.expected, actual, test.org, test.repo, test.branch)
  1102  			}
  1103  		})
  1104  	}
  1105  }
  1106  func TestMergeTemplate(t *testing.T) {
  1107  	ti := &Tide{
  1108  		TideGitHubConfig: TideGitHubConfig{
  1109  			MergeTemplate: map[string]TideMergeCommitTemplate{
  1110  				"kubernetes/kops": {
  1111  					TitleTemplate: "",
  1112  					BodyTemplate:  "",
  1113  				},
  1114  				"kubernetes-helm": {
  1115  					TitleTemplate: "{{ .Title }}",
  1116  					BodyTemplate:  "{{ .Body }}",
  1117  				},
  1118  			},
  1119  		},
  1120  	}
  1121  
  1122  	var testcases = []struct {
  1123  		org      string
  1124  		repo     string
  1125  		expected TideMergeCommitTemplate
  1126  	}{
  1127  		{
  1128  			org:      "kubernetes",
  1129  			repo:     "kubernetes",
  1130  			expected: TideMergeCommitTemplate{},
  1131  		},
  1132  		{
  1133  			org:  "kubernetes",
  1134  			repo: "kops",
  1135  			expected: TideMergeCommitTemplate{
  1136  				TitleTemplate: "",
  1137  				BodyTemplate:  "",
  1138  			},
  1139  		},
  1140  		{
  1141  			org:  "kubernetes-helm",
  1142  			repo: "monocular",
  1143  			expected: TideMergeCommitTemplate{
  1144  				TitleTemplate: "{{ .Title }}",
  1145  				BodyTemplate:  "{{ .Body }}",
  1146  			},
  1147  		},
  1148  	}
  1149  
  1150  	for _, test := range testcases {
  1151  		actual := ti.MergeCommitTemplate(OrgRepo{Org: test.org, Repo: test.repo})
  1152  
  1153  		if actual.TitleTemplate != test.expected.TitleTemplate || actual.BodyTemplate != test.expected.BodyTemplate {
  1154  			t.Errorf("Expected title \"%v\", body \"%v\", but got title \"%v\", body \"%v\" for %v/%v", test.expected.TitleTemplate, test.expected.BodyTemplate, actual.TitleTemplate, actual.BodyTemplate, test.org, test.repo)
  1155  		}
  1156  	}
  1157  }
  1158  
  1159  func TestParseTideContextPolicyOptions(t *testing.T) {
  1160  	yes := true
  1161  	no := false
  1162  	org, repo, branch := "org", "repo", "branch"
  1163  	testCases := []struct {
  1164  		name     string
  1165  		config   TideContextPolicyOptions
  1166  		expected TideContextPolicy
  1167  	}{
  1168  		{
  1169  			name: "empty",
  1170  		},
  1171  		{
  1172  			name: "global config",
  1173  			config: TideContextPolicyOptions{
  1174  				TideContextPolicy: TideContextPolicy{
  1175  					FromBranchProtection: &yes,
  1176  					SkipUnknownContexts:  &yes,
  1177  					RequiredContexts:     []string{"r1"},
  1178  					OptionalContexts:     []string{"o1"},
  1179  				},
  1180  			},
  1181  			expected: TideContextPolicy{
  1182  				SkipUnknownContexts:  &yes,
  1183  				RequiredContexts:     []string{"r1"},
  1184  				OptionalContexts:     []string{"o1"},
  1185  				FromBranchProtection: &yes,
  1186  			},
  1187  		},
  1188  		{
  1189  			name: "org config",
  1190  			config: TideContextPolicyOptions{
  1191  				TideContextPolicy: TideContextPolicy{
  1192  					RequiredContexts:     []string{"r1"},
  1193  					OptionalContexts:     []string{"o1"},
  1194  					FromBranchProtection: &no,
  1195  				},
  1196  				Orgs: map[string]TideOrgContextPolicy{
  1197  					"org": {
  1198  						TideContextPolicy: TideContextPolicy{
  1199  							SkipUnknownContexts:  &yes,
  1200  							RequiredContexts:     []string{"r2"},
  1201  							OptionalContexts:     []string{"o2"},
  1202  							FromBranchProtection: &yes,
  1203  						},
  1204  					},
  1205  				},
  1206  			},
  1207  			expected: TideContextPolicy{
  1208  				SkipUnknownContexts:  &yes,
  1209  				RequiredContexts:     []string{"r1", "r2"},
  1210  				OptionalContexts:     []string{"o1", "o2"},
  1211  				FromBranchProtection: &yes,
  1212  			},
  1213  		},
  1214  		{
  1215  			name: "repo config",
  1216  			config: TideContextPolicyOptions{
  1217  				TideContextPolicy: TideContextPolicy{
  1218  					RequiredContexts:     []string{"r1"},
  1219  					OptionalContexts:     []string{"o1"},
  1220  					FromBranchProtection: &no,
  1221  				},
  1222  				Orgs: map[string]TideOrgContextPolicy{
  1223  					"org": {
  1224  						TideContextPolicy: TideContextPolicy{
  1225  							SkipUnknownContexts:  &no,
  1226  							RequiredContexts:     []string{"r2"},
  1227  							OptionalContexts:     []string{"o2"},
  1228  							FromBranchProtection: &no,
  1229  						},
  1230  						Repos: map[string]TideRepoContextPolicy{
  1231  							"repo": {
  1232  								TideContextPolicy: TideContextPolicy{
  1233  									SkipUnknownContexts:  &yes,
  1234  									RequiredContexts:     []string{"r3"},
  1235  									OptionalContexts:     []string{"o3"},
  1236  									FromBranchProtection: &yes,
  1237  								},
  1238  							},
  1239  						},
  1240  					},
  1241  				},
  1242  			},
  1243  			expected: TideContextPolicy{
  1244  				SkipUnknownContexts:  &yes,
  1245  				RequiredContexts:     []string{"r1", "r2", "r3"},
  1246  				OptionalContexts:     []string{"o1", "o2", "o3"},
  1247  				FromBranchProtection: &yes,
  1248  			},
  1249  		},
  1250  		{
  1251  			name: "branch config",
  1252  			config: TideContextPolicyOptions{
  1253  				TideContextPolicy: TideContextPolicy{
  1254  					RequiredContexts: []string{"r1"},
  1255  					OptionalContexts: []string{"o1"},
  1256  				},
  1257  				Orgs: map[string]TideOrgContextPolicy{
  1258  					"org": {
  1259  						TideContextPolicy: TideContextPolicy{
  1260  							RequiredContexts: []string{"r2"},
  1261  							OptionalContexts: []string{"o2"},
  1262  						},
  1263  						Repos: map[string]TideRepoContextPolicy{
  1264  							"repo": {
  1265  								TideContextPolicy: TideContextPolicy{
  1266  									RequiredContexts: []string{"r3"},
  1267  									OptionalContexts: []string{"o3"},
  1268  								},
  1269  								Branches: map[string]TideContextPolicy{
  1270  									"branch": {
  1271  										RequiredContexts: []string{"r4"},
  1272  										OptionalContexts: []string{"o4"},
  1273  									},
  1274  								},
  1275  							},
  1276  						},
  1277  					},
  1278  				},
  1279  			},
  1280  			expected: TideContextPolicy{
  1281  				RequiredContexts: []string{"r1", "r2", "r3", "r4"},
  1282  				OptionalContexts: []string{"o1", "o2", "o3", "o4"},
  1283  			},
  1284  		},
  1285  	}
  1286  	for _, tc := range testCases {
  1287  		policy := ParseTideContextPolicyOptions(org, repo, branch, tc.config)
  1288  		if !reflect.DeepEqual(policy, tc.expected) {
  1289  			t.Errorf("%s - did not get expected policy: %s", tc.name, diff.ObjectReflectDiff(tc.expected, policy))
  1290  		}
  1291  	}
  1292  }
  1293  
  1294  func TestConfigGetTideContextPolicy(t *testing.T) {
  1295  	yes := true
  1296  	no := false
  1297  	org, repo, branch := "org", "repo", "branch"
  1298  	testCases := []struct {
  1299  		name     string
  1300  		config   Config
  1301  		expected TideContextPolicy
  1302  		error    string
  1303  	}{
  1304  		{
  1305  			name: "no policy - use prow jobs",
  1306  			config: Config{
  1307  				ProwConfig: ProwConfig{
  1308  					BranchProtection: BranchProtection{
  1309  						Policy: Policy{
  1310  							Protect: &yes,
  1311  							RequiredStatusChecks: &ContextPolicy{
  1312  								Contexts: []string{"r1", "r2"},
  1313  							},
  1314  						},
  1315  					},
  1316  				},
  1317  				JobConfig: JobConfig{
  1318  					PresubmitsStatic: map[string][]Presubmit{
  1319  						"org/repo": {
  1320  							Presubmit{
  1321  								Reporter: Reporter{
  1322  									Context: "pr1",
  1323  								},
  1324  								AlwaysRun: true,
  1325  							},
  1326  							Presubmit{
  1327  								Reporter: Reporter{
  1328  									Context: "po1",
  1329  								},
  1330  								AlwaysRun: true,
  1331  								Optional:  true,
  1332  							},
  1333  						},
  1334  					},
  1335  				},
  1336  			},
  1337  			expected: TideContextPolicy{
  1338  				RequiredContexts:          []string{"pr1"},
  1339  				RequiredIfPresentContexts: []string{},
  1340  				OptionalContexts:          []string{"po1"},
  1341  			},
  1342  		},
  1343  		{
  1344  			name: "no policy no prow jobs defined - empty",
  1345  			config: Config{
  1346  				ProwConfig: ProwConfig{
  1347  					BranchProtection: BranchProtection{
  1348  						Policy: Policy{
  1349  							Protect: &yes,
  1350  							RequiredStatusChecks: &ContextPolicy{
  1351  								Contexts: []string{"r1", "r2"},
  1352  							},
  1353  						},
  1354  					},
  1355  				},
  1356  			},
  1357  			expected: TideContextPolicy{
  1358  				RequiredContexts:          []string{},
  1359  				RequiredIfPresentContexts: []string{},
  1360  				OptionalContexts:          []string{},
  1361  			},
  1362  		},
  1363  		{
  1364  			name: "no branch protection",
  1365  			config: Config{
  1366  				ProwConfig: ProwConfig{
  1367  					Tide: Tide{
  1368  						TideGitHubConfig: TideGitHubConfig{
  1369  							ContextOptions: TideContextPolicyOptions{
  1370  								TideContextPolicy: TideContextPolicy{
  1371  									FromBranchProtection: &yes,
  1372  								},
  1373  							},
  1374  						},
  1375  					},
  1376  				},
  1377  			},
  1378  			expected: TideContextPolicy{
  1379  				RequiredContexts:          []string{},
  1380  				RequiredIfPresentContexts: []string{},
  1381  				OptionalContexts:          []string{},
  1382  			},
  1383  		},
  1384  		{
  1385  			name: "invalid branch protection",
  1386  			config: Config{
  1387  				ProwConfig: ProwConfig{
  1388  					BranchProtection: BranchProtection{
  1389  						Orgs: map[string]Org{
  1390  							"org": {
  1391  								Policy: Policy{
  1392  									Protect: &no,
  1393  								},
  1394  							},
  1395  						},
  1396  					},
  1397  					Tide: Tide{
  1398  						TideGitHubConfig: TideGitHubConfig{
  1399  							ContextOptions: TideContextPolicyOptions{
  1400  								TideContextPolicy: TideContextPolicy{
  1401  									FromBranchProtection: &yes,
  1402  								},
  1403  							},
  1404  						},
  1405  					},
  1406  				},
  1407  			},
  1408  			expected: TideContextPolicy{
  1409  				RequiredContexts:          []string{},
  1410  				RequiredIfPresentContexts: []string{},
  1411  				OptionalContexts:          []string{},
  1412  			},
  1413  		},
  1414  		{
  1415  			name: "branch protection with manually required triggered jobs",
  1416  			config: Config{
  1417  				ProwConfig: ProwConfig{
  1418  					BranchProtection: BranchProtection{
  1419  						Orgs: map[string]Org{
  1420  							"org": {
  1421  								Policy: Policy{
  1422  									RequireManuallyTriggeredJobs: &yes,
  1423  								},
  1424  							},
  1425  						},
  1426  					},
  1427  					Tide: Tide{
  1428  						TideGitHubConfig: TideGitHubConfig{
  1429  							ContextOptions: TideContextPolicyOptions{
  1430  								TideContextPolicy: TideContextPolicy{
  1431  									FromBranchProtection: &yes,
  1432  								},
  1433  							},
  1434  						},
  1435  					},
  1436  				},
  1437  				JobConfig: JobConfig{
  1438  					PresubmitsStatic: map[string][]Presubmit{
  1439  						"org/repo": {
  1440  							Presubmit{
  1441  								Reporter: Reporter{
  1442  									Context: "pr1",
  1443  								},
  1444  								AlwaysRun: false,
  1445  								Optional:  false,
  1446  							},
  1447  							Presubmit{
  1448  								Reporter: Reporter{
  1449  									Context: "pr2",
  1450  								},
  1451  								AlwaysRun: true,
  1452  							},
  1453  						},
  1454  					},
  1455  				},
  1456  			},
  1457  			expected: TideContextPolicy{
  1458  				RequiredContexts:          []string{"pr1", "pr2"},
  1459  				RequiredIfPresentContexts: []string{},
  1460  				OptionalContexts:          []string{},
  1461  			},
  1462  		},
  1463  		{
  1464  			name: "manually defined policy",
  1465  			config: Config{
  1466  				ProwConfig: ProwConfig{
  1467  					Tide: Tide{
  1468  						TideGitHubConfig: TideGitHubConfig{
  1469  							ContextOptions: TideContextPolicyOptions{
  1470  								TideContextPolicy: TideContextPolicy{
  1471  									RequiredContexts:          []string{"r1"},
  1472  									RequiredIfPresentContexts: []string{},
  1473  									OptionalContexts:          []string{"o1"},
  1474  									SkipUnknownContexts:       &yes,
  1475  								},
  1476  							},
  1477  						},
  1478  					},
  1479  				},
  1480  			},
  1481  			expected: TideContextPolicy{
  1482  				RequiredContexts:          []string{"r1"},
  1483  				RequiredIfPresentContexts: []string{},
  1484  				OptionalContexts:          []string{"o1"},
  1485  				SkipUnknownContexts:       &yes,
  1486  			},
  1487  		},
  1488  		{
  1489  			name: "jobs from inrepoconfig are considered",
  1490  			config: Config{
  1491  				JobConfig: JobConfig{
  1492  					ProwYAMLGetterWithDefaults: fakeProwYAMLGetterFactory(
  1493  						[]Presubmit{
  1494  							{
  1495  								AlwaysRun: true,
  1496  								Reporter:  Reporter{Context: "ir0"},
  1497  							},
  1498  							{
  1499  								AlwaysRun: true,
  1500  								Optional:  true,
  1501  								Reporter:  Reporter{Context: "ir1"},
  1502  							},
  1503  						},
  1504  						nil,
  1505  					),
  1506  				},
  1507  				ProwConfig: ProwConfig{
  1508  					InRepoConfig: InRepoConfig{
  1509  						Enabled: map[string]*bool{"*": utilpointer.Bool(true)},
  1510  					},
  1511  				},
  1512  			},
  1513  			expected: TideContextPolicy{
  1514  				RequiredContexts:          []string{"ir0"},
  1515  				RequiredIfPresentContexts: []string{},
  1516  				OptionalContexts:          []string{"ir1"},
  1517  			},
  1518  		},
  1519  		{
  1520  			name: "both static and inrepoconfig jobs are consired",
  1521  			config: Config{
  1522  				JobConfig: JobConfig{
  1523  					PresubmitsStatic: map[string][]Presubmit{
  1524  						"org/repo": {
  1525  							Presubmit{
  1526  								Reporter: Reporter{
  1527  									Context: "pr1",
  1528  								},
  1529  								AlwaysRun: true,
  1530  							},
  1531  							Presubmit{
  1532  								Reporter: Reporter{
  1533  									Context: "po1",
  1534  								},
  1535  								AlwaysRun: true,
  1536  								Optional:  true,
  1537  							},
  1538  						},
  1539  					},
  1540  					ProwYAMLGetterWithDefaults: fakeProwYAMLGetterFactory(
  1541  						[]Presubmit{
  1542  							{
  1543  								AlwaysRun: true,
  1544  								Reporter:  Reporter{Context: "ir0"},
  1545  							},
  1546  							{
  1547  								AlwaysRun: true,
  1548  								Optional:  true,
  1549  								Reporter:  Reporter{Context: "ir1"},
  1550  							},
  1551  						},
  1552  						nil,
  1553  					),
  1554  				},
  1555  				ProwConfig: ProwConfig{
  1556  					InRepoConfig: InRepoConfig{
  1557  						Enabled: map[string]*bool{"*": utilpointer.Bool(true)},
  1558  					},
  1559  				},
  1560  			},
  1561  			expected: TideContextPolicy{
  1562  				RequiredContexts:          []string{"ir0", "pr1"},
  1563  				RequiredIfPresentContexts: []string{},
  1564  				OptionalContexts:          []string{"ir1", "po1"},
  1565  			},
  1566  		},
  1567  	}
  1568  
  1569  	for _, tc := range testCases {
  1570  		t.Run(tc.name, func(t *testing.T) {
  1571  
  1572  			baseSHAGetter := func() (string, error) {
  1573  				return "baseSHA", nil
  1574  			}
  1575  			p, err := tc.config.GetTideContextPolicy(nil, org, repo, branch, baseSHAGetter, "some-sha")
  1576  			if !reflect.DeepEqual(p, &tc.expected) {
  1577  				t.Errorf("%s - did not get expected policy: %s", tc.name, diff.ObjectReflectDiff(&tc.expected, p))
  1578  			}
  1579  			if err != nil {
  1580  				if err.Error() != tc.error {
  1581  					t.Errorf("%s - expected error %v got %v", tc.name, tc.error, err.Error())
  1582  				}
  1583  			} else if tc.error != "" {
  1584  				t.Errorf("%s - expected error %v got nil", tc.name, tc.error)
  1585  			}
  1586  		})
  1587  	}
  1588  }
  1589  
  1590  func TestMergeTideContextPolicyConfig(t *testing.T) {
  1591  	yes := true
  1592  	no := false
  1593  	testCases := []struct {
  1594  		name    string
  1595  		a, b, c TideContextPolicy
  1596  	}{
  1597  		{
  1598  			name: "all empty",
  1599  		},
  1600  		{
  1601  			name: "empty a",
  1602  			b: TideContextPolicy{
  1603  				SkipUnknownContexts:  &yes,
  1604  				FromBranchProtection: &no,
  1605  				RequiredContexts:     []string{"r1"},
  1606  				OptionalContexts:     []string{"o1"},
  1607  			},
  1608  			c: TideContextPolicy{
  1609  				SkipUnknownContexts:  &yes,
  1610  				FromBranchProtection: &no,
  1611  				RequiredContexts:     []string{"r1"},
  1612  				OptionalContexts:     []string{"o1"},
  1613  			},
  1614  		},
  1615  		{
  1616  			name: "empty b",
  1617  			a: TideContextPolicy{
  1618  				SkipUnknownContexts:  &yes,
  1619  				FromBranchProtection: &no,
  1620  				RequiredContexts:     []string{"r1"},
  1621  				OptionalContexts:     []string{"o1"},
  1622  			},
  1623  			c: TideContextPolicy{
  1624  				SkipUnknownContexts:  &yes,
  1625  				FromBranchProtection: &no,
  1626  				RequiredContexts:     []string{"r1"},
  1627  				OptionalContexts:     []string{"o1"},
  1628  			},
  1629  		},
  1630  		{
  1631  			name: "merging unset boolean",
  1632  			a: TideContextPolicy{
  1633  				FromBranchProtection: &no,
  1634  				RequiredContexts:     []string{"r1"},
  1635  				OptionalContexts:     []string{"o1"},
  1636  			},
  1637  			b: TideContextPolicy{
  1638  				SkipUnknownContexts: &yes,
  1639  				RequiredContexts:    []string{"r2"},
  1640  				OptionalContexts:    []string{"o2"},
  1641  			},
  1642  			c: TideContextPolicy{
  1643  				SkipUnknownContexts:  &yes,
  1644  				FromBranchProtection: &no,
  1645  				RequiredContexts:     []string{"r1", "r2"},
  1646  				OptionalContexts:     []string{"o1", "o2"},
  1647  			},
  1648  		},
  1649  		{
  1650  			name: "merging unset contexts in a",
  1651  			a: TideContextPolicy{
  1652  				FromBranchProtection: &no,
  1653  				SkipUnknownContexts:  &yes,
  1654  			},
  1655  			b: TideContextPolicy{
  1656  				FromBranchProtection: &yes,
  1657  				SkipUnknownContexts:  &no,
  1658  				RequiredContexts:     []string{"r1"},
  1659  				OptionalContexts:     []string{"o1"},
  1660  			},
  1661  			c: TideContextPolicy{
  1662  				FromBranchProtection: &yes,
  1663  				SkipUnknownContexts:  &no,
  1664  				RequiredContexts:     []string{"r1"},
  1665  				OptionalContexts:     []string{"o1"},
  1666  			},
  1667  		},
  1668  		{
  1669  			name: "merging unset contexts in b",
  1670  			a: TideContextPolicy{
  1671  				FromBranchProtection: &yes,
  1672  				SkipUnknownContexts:  &no,
  1673  				RequiredContexts:     []string{"r1"},
  1674  				OptionalContexts:     []string{"o1"},
  1675  			},
  1676  			b: TideContextPolicy{
  1677  				FromBranchProtection: &no,
  1678  				SkipUnknownContexts:  &yes,
  1679  			},
  1680  			c: TideContextPolicy{
  1681  				FromBranchProtection: &no,
  1682  				SkipUnknownContexts:  &yes,
  1683  				RequiredContexts:     []string{"r1"},
  1684  				OptionalContexts:     []string{"o1"},
  1685  			},
  1686  		},
  1687  	}
  1688  
  1689  	for _, tc := range testCases {
  1690  		c := mergeTideContextPolicy(tc.a, tc.b)
  1691  		if !reflect.DeepEqual(c, tc.c) {
  1692  			t.Errorf("%s - expected %v got %v", tc.name, tc.c, c)
  1693  		}
  1694  	}
  1695  }
  1696  
  1697  func TestTideQuery_Validate(t *testing.T) {
  1698  	testCases := []struct {
  1699  		name        string
  1700  		query       TideQuery
  1701  		expectError bool
  1702  	}{
  1703  		{
  1704  			name: "good query",
  1705  			query: TideQuery{
  1706  				Orgs:                   []string{"kuber"},
  1707  				Repos:                  []string{"foo/bar", "baz/bar"},
  1708  				ExcludedRepos:          []string{"kuber/netes"},
  1709  				IncludedBranches:       []string{"master"},
  1710  				Milestone:              "backlog-forever",
  1711  				Labels:                 []string{labels.LGTM, labels.Approved},
  1712  				MissingLabels:          []string{"do-not-merge/evil-code"},
  1713  				ReviewApprovedRequired: true,
  1714  			},
  1715  			expectError: false,
  1716  		},
  1717  		{
  1718  			name: "simple org query is valid",
  1719  			query: TideQuery{
  1720  				Orgs: []string{"kuber"},
  1721  			},
  1722  			expectError: false,
  1723  		},
  1724  		{
  1725  			name: "org with slash is invalid",
  1726  			query: TideQuery{
  1727  				Orgs: []string{"kube/r"},
  1728  			},
  1729  			expectError: true,
  1730  		},
  1731  		{
  1732  			name: "empty org is invalid",
  1733  			query: TideQuery{
  1734  				Orgs: []string{""},
  1735  			},
  1736  			expectError: true,
  1737  		},
  1738  		{
  1739  			name: "duplicate org is invalid",
  1740  			query: TideQuery{
  1741  				Orgs: []string{"kuber", "kuber"},
  1742  			},
  1743  			expectError: true,
  1744  		},
  1745  		{
  1746  			name: "simple repo query is valid",
  1747  			query: TideQuery{
  1748  				Repos: []string{"kuber/netes"},
  1749  			},
  1750  			expectError: false,
  1751  		},
  1752  		{
  1753  			name: "repo without slash is invalid",
  1754  			query: TideQuery{
  1755  				Repos: []string{"foobar", "baz/bar"},
  1756  			},
  1757  			expectError: true,
  1758  		},
  1759  		{
  1760  			name: "repo included with parent org is invalid",
  1761  			query: TideQuery{
  1762  				Orgs:  []string{"kuber"},
  1763  				Repos: []string{"foo/bar", "kuber/netes"},
  1764  			},
  1765  			expectError: true,
  1766  		},
  1767  		{
  1768  			name: "duplicate repo is invalid",
  1769  			query: TideQuery{
  1770  				Repos: []string{"baz/bar", "foo/bar", "baz/bar"},
  1771  			},
  1772  			expectError: true,
  1773  		},
  1774  		{
  1775  			name: "empty orgs and repos is invalid",
  1776  			query: TideQuery{
  1777  				IncludedBranches:       []string{"master"},
  1778  				Milestone:              "backlog-forever",
  1779  				Labels:                 []string{labels.LGTM, labels.Approved},
  1780  				MissingLabels:          []string{"do-not-merge/evil-code"},
  1781  				ReviewApprovedRequired: true,
  1782  			},
  1783  			expectError: true,
  1784  		},
  1785  		{
  1786  			name: "simple excluded repo query is valid",
  1787  			query: TideQuery{
  1788  				Orgs:          []string{"kuber"},
  1789  				ExcludedRepos: []string{"kuber/netes"},
  1790  			},
  1791  			expectError: false,
  1792  		},
  1793  		{
  1794  			name: "excluded repo without slash is invalid",
  1795  			query: TideQuery{
  1796  				Orgs:          []string{"kuber"},
  1797  				ExcludedRepos: []string{"kubernetes"},
  1798  			},
  1799  			expectError: true,
  1800  		},
  1801  		{
  1802  			name: "excluded repo included without parent org is invalid",
  1803  			query: TideQuery{
  1804  				Repos:         []string{"foo/bar", "baz/bar"},
  1805  				ExcludedRepos: []string{"kuber/netes"},
  1806  			},
  1807  			expectError: true,
  1808  		},
  1809  		{
  1810  			name: "duplicate excluded repo is invalid",
  1811  			query: TideQuery{
  1812  				Orgs:                   []string{"kuber"},
  1813  				ExcludedRepos:          []string{"kuber/netes", "kuber/netes"},
  1814  				ReviewApprovedRequired: true,
  1815  			},
  1816  			expectError: true,
  1817  		},
  1818  		{
  1819  			name: "label cannot be required and forbidden",
  1820  			query: TideQuery{
  1821  				Orgs:          []string{"kuber"},
  1822  				Labels:        []string{labels.LGTM, labels.Approved},
  1823  				MissingLabels: []string{"do-not-merge/evil-code", labels.LGTM},
  1824  			},
  1825  			expectError: true,
  1826  		},
  1827  		{
  1828  			name: "simple excluded branches query is valid",
  1829  			query: TideQuery{
  1830  				Orgs:             []string{"kuber"},
  1831  				ExcludedBranches: []string{"dev"},
  1832  			},
  1833  			expectError: false,
  1834  		},
  1835  		{
  1836  			name: "specifying both included and excluded branches is invalid",
  1837  			query: TideQuery{
  1838  				Orgs:             []string{"kuber"},
  1839  				IncludedBranches: []string{"master"},
  1840  				ExcludedBranches: []string{"dev"},
  1841  			},
  1842  			expectError: true,
  1843  		},
  1844  	}
  1845  	for _, tc := range testCases {
  1846  		t.Run(tc.name, func(t *testing.T) {
  1847  			err := tc.query.Validate()
  1848  			if err != nil && !tc.expectError {
  1849  				t.Errorf("Unexpected error: %v.", err)
  1850  			} else if err == nil && tc.expectError {
  1851  				t.Error("Expected a validation error, but didn't get one.")
  1852  			}
  1853  		})
  1854  
  1855  	}
  1856  }
  1857  
  1858  func TestTideContextPolicy_Validate(t *testing.T) {
  1859  	testCases := []struct {
  1860  		name   string
  1861  		t      TideContextPolicy
  1862  		failed bool
  1863  	}{
  1864  		{
  1865  			name: "good policy",
  1866  			t: TideContextPolicy{
  1867  				OptionalContexts: []string{"o1"},
  1868  				RequiredContexts: []string{"r1"},
  1869  			},
  1870  		},
  1871  		{
  1872  			name: "optional contexts must differ from required contexts",
  1873  			t: TideContextPolicy{
  1874  				OptionalContexts: []string{"c1"},
  1875  				RequiredContexts: []string{"c1"},
  1876  			},
  1877  			failed: true,
  1878  		},
  1879  		{
  1880  			name: "individual contexts cannot be both optional and required",
  1881  			t: TideContextPolicy{
  1882  				OptionalContexts: []string{"c1", "c2", "c3", "c4"},
  1883  				RequiredContexts: []string{"c1", "c4"},
  1884  			},
  1885  			failed: true,
  1886  		},
  1887  	}
  1888  	for _, tc := range testCases {
  1889  		err := tc.t.Validate()
  1890  		failed := err != nil
  1891  		if failed != tc.failed {
  1892  			t.Errorf("%s - expected %v got %v", tc.name, tc.failed, err)
  1893  		}
  1894  	}
  1895  }
  1896  
  1897  func TestTideContextPolicy_IsOptional(t *testing.T) {
  1898  	testCases := []struct {
  1899  		name                string
  1900  		skipUnknownContexts bool
  1901  		required, optional  []string
  1902  		contexts            []string
  1903  		results             []bool
  1904  	}{
  1905  		{
  1906  			name:     "only optional contexts registered - skipUnknownContexts false",
  1907  			contexts: []string{"c1", "o1", "o2"},
  1908  			optional: []string{"o1", "o2"},
  1909  			results:  []bool{false, true, true},
  1910  		},
  1911  		{
  1912  			name:     "no contexts registered - skipUnknownContexts false",
  1913  			contexts: []string{"t2"},
  1914  			results:  []bool{false},
  1915  		},
  1916  		{
  1917  			name:     "only required contexts registered - skipUnknownContexts false",
  1918  			required: []string{"c1", "c2", "c3"},
  1919  			contexts: []string{"c1", "c2", "c3", "t1"},
  1920  			results:  []bool{false, false, false, false},
  1921  		},
  1922  		{
  1923  			name:     "optional and required contexts registered - skipUnknownContexts false",
  1924  			optional: []string{"o1", "o2"},
  1925  			required: []string{"c1", "c2", "c3"},
  1926  			contexts: []string{"o1", "o2", "c1", "c2", "c3", "t1"},
  1927  			results:  []bool{true, true, false, false, false, false},
  1928  		},
  1929  		{
  1930  			name:                "only optional contexts registered - skipUnknownContexts true",
  1931  			contexts:            []string{"c1", "o1", "o2"},
  1932  			optional:            []string{"o1", "o2"},
  1933  			skipUnknownContexts: true,
  1934  			results:             []bool{true, true, true},
  1935  		},
  1936  		{
  1937  			name:                "no contexts registered - skipUnknownContexts true",
  1938  			contexts:            []string{"t2"},
  1939  			skipUnknownContexts: true,
  1940  			results:             []bool{true},
  1941  		},
  1942  		{
  1943  			name:                "only required contexts registered - skipUnknownContexts true",
  1944  			required:            []string{"c1", "c2", "c3"},
  1945  			contexts:            []string{"c1", "c2", "c3", "t1"},
  1946  			skipUnknownContexts: true,
  1947  			results:             []bool{false, false, false, true},
  1948  		},
  1949  		{
  1950  			name:                "optional and required contexts registered - skipUnknownContexts true",
  1951  			optional:            []string{"o1", "o2"},
  1952  			required:            []string{"c1", "c2", "c3"},
  1953  			contexts:            []string{"o1", "o2", "c1", "c2", "c3", "t1"},
  1954  			skipUnknownContexts: true,
  1955  			results:             []bool{true, true, false, false, false, true},
  1956  		},
  1957  	}
  1958  
  1959  	for _, tc := range testCases {
  1960  		cp := TideContextPolicy{
  1961  			SkipUnknownContexts: &tc.skipUnknownContexts,
  1962  			RequiredContexts:    tc.required,
  1963  			OptionalContexts:    tc.optional,
  1964  		}
  1965  		for i, c := range tc.contexts {
  1966  			if cp.IsOptional(c) != tc.results[i] {
  1967  				t.Errorf("%s - IsOptional for %s should return %t", tc.name, c, tc.results[i])
  1968  			}
  1969  		}
  1970  	}
  1971  }
  1972  
  1973  func TestTideContextPolicy_MissingRequiredContexts(t *testing.T) {
  1974  	testCases := []struct {
  1975  		name                               string
  1976  		skipUnknownContexts                bool
  1977  		required, optional                 []string
  1978  		existingContexts, expectedContexts []string
  1979  	}{
  1980  		{
  1981  			name:             "no contexts registered",
  1982  			existingContexts: []string{"c1", "c2"},
  1983  		},
  1984  		{
  1985  			name:             "optional contexts registered / no missing contexts",
  1986  			optional:         []string{"o1", "o2", "o3"},
  1987  			existingContexts: []string{"c1", "c2"},
  1988  		},
  1989  		{
  1990  			name:             "required  contexts registered / missing contexts",
  1991  			required:         []string{"c1", "c2", "c3"},
  1992  			existingContexts: []string{"c1", "c2"},
  1993  			expectedContexts: []string{"c3"},
  1994  		},
  1995  		{
  1996  			name:             "required contexts registered / no missing contexts",
  1997  			required:         []string{"c1", "c2", "c3"},
  1998  			existingContexts: []string{"c1", "c2", "c3"},
  1999  		},
  2000  		{
  2001  			name:             "optional and required contexts registered / missing contexts",
  2002  			optional:         []string{"o1", "o2", "o3"},
  2003  			required:         []string{"c1", "c2", "c3"},
  2004  			existingContexts: []string{"c1", "c2"},
  2005  			expectedContexts: []string{"c3"},
  2006  		},
  2007  		{
  2008  			name:             "optional and required contexts registered / no missing contexts",
  2009  			optional:         []string{"o1", "o2", "o3"},
  2010  			required:         []string{"c1", "c2"},
  2011  			existingContexts: []string{"c1", "c2", "c4"},
  2012  		},
  2013  	}
  2014  
  2015  	for _, tc := range testCases {
  2016  		cp := TideContextPolicy{
  2017  			SkipUnknownContexts: &tc.skipUnknownContexts,
  2018  			RequiredContexts:    tc.required,
  2019  			OptionalContexts:    tc.optional,
  2020  		}
  2021  		missingContexts := cp.MissingRequiredContexts(tc.existingContexts)
  2022  		if !sets.New[string](missingContexts...).Equal(sets.New[string](tc.expectedContexts...)) {
  2023  			t.Errorf("%s - expected %v got %v", tc.name, tc.expectedContexts, missingContexts)
  2024  		}
  2025  	}
  2026  }
  2027  
  2028  func fakeProwYAMLGetterFactory(presubmits []Presubmit, postsubmits []Postsubmit) ProwYAMLGetter {
  2029  	return func(_ *Config, _ git.ClientFactory, _, _, _ string, _ ...string) (*ProwYAML, error) {
  2030  		return &ProwYAML{
  2031  			Presubmits:  presubmits,
  2032  			Postsubmits: postsubmits,
  2033  		}, nil
  2034  	}
  2035  }