sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/tide/status_test.go (about)

     1  /*
     2  Copyright 2017 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package tide
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"sort"
    24  	"strings"
    25  	"testing"
    26  
    27  	"github.com/go-test/deep"
    28  	"github.com/google/go-cmp/cmp"
    29  	"github.com/google/go-cmp/cmp/cmpopts"
    30  	githubql "github.com/shurcooL/githubv4"
    31  	"github.com/sirupsen/logrus"
    32  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    33  	"k8s.io/apimachinery/pkg/runtime"
    34  	fakectrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
    35  
    36  	"k8s.io/apimachinery/pkg/util/sets"
    37  	prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1"
    38  	"sigs.k8s.io/prow/pkg/config"
    39  	"sigs.k8s.io/prow/pkg/github"
    40  	"sigs.k8s.io/prow/pkg/tide/blockers"
    41  )
    42  
    43  func TestExpectedStatus(t *testing.T) {
    44  	mergeLabel := "tide/merge-method-merge"
    45  	squashLabel := "tide/merge-method-squash"
    46  	neededLabelsWithAlt := []string{"need-1", "need-2", "need-a-very-super-duper-extra-not-short-at-all-label-name,need-3"}
    47  	neededLabels := []string{"need-1", "need-2", "need-a-very-super-duper-extra-not-short-at-all-label-name"}
    48  	forbiddenLabels := []string{"forbidden-1", "forbidden-2"}
    49  	testcases := []struct {
    50  		name string
    51  
    52  		baseref               string
    53  		branchAllowList       []string
    54  		branchDenyList        []string
    55  		sameBranchReqs        bool
    56  		labels                []string
    57  		author                string
    58  		firstQueryAuthor      string
    59  		secondQueryAuthor     string
    60  		milestone             string
    61  		contexts              []Context
    62  		checkRuns             []CheckRun
    63  		inPool                bool
    64  		blocks                []int
    65  		prowJobs              []runtime.Object
    66  		requiredContexts      []string
    67  		mergeConflicts        bool
    68  		displayAllTideQueries bool
    69  		additionalTideQueries []config.TideQuery
    70  		hasApprovingReview    bool
    71  		singleQuery           bool
    72  
    73  		state string
    74  		desc  string
    75  	}{
    76  		{
    77  			name:   "in pool",
    78  			inPool: true,
    79  
    80  			state: github.StatusSuccess,
    81  			desc:  statusInPool,
    82  		},
    83  		{
    84  			name:              "check truncation of label list",
    85  			author:            "batman",
    86  			firstQueryAuthor:  "batman",
    87  			secondQueryAuthor: "batman",
    88  			milestone:         "v1.0",
    89  			inPool:            false,
    90  
    91  			state: github.StatusPending,
    92  			desc:  fmt.Sprintf(statusNotInPool, " Needs need-1, need-2 labels."),
    93  		},
    94  		{
    95  			name:              "check truncation of label list is not excessive",
    96  			labels:            append([]string{}, neededLabels[:2]...),
    97  			author:            "batman",
    98  			firstQueryAuthor:  "batman",
    99  			secondQueryAuthor: "batman",
   100  			milestone:         "v1.0",
   101  			inPool:            false,
   102  
   103  			state: github.StatusPending,
   104  			desc:  fmt.Sprintf(statusNotInPool, " Needs need-a-very-super-duper-extra-not-short-at-all-label-name or need-3 label."),
   105  		},
   106  		{
   107  			name:   "check multiple /tide labels result in conflict message",
   108  			labels: append(append([]string{}, neededLabels...), mergeLabel, squashLabel),
   109  			state:  github.StatusError,
   110  			desc:   fmt.Sprintf(statusNotInPool, " PR has conflicting merge method override labels"),
   111  		},
   112  		{
   113  			name:              "has forbidden labels",
   114  			labels:            append(append([]string{}, neededLabels...), forbiddenLabels...),
   115  			author:            "batman",
   116  			firstQueryAuthor:  "batman",
   117  			secondQueryAuthor: "batman",
   118  			milestone:         "v1.0",
   119  			inPool:            false,
   120  
   121  			state: github.StatusPending,
   122  			desc:  fmt.Sprintf(statusNotInPool, " Should not have forbidden-1, forbidden-2 labels."),
   123  		},
   124  		{
   125  			name:              "has one forbidden label",
   126  			labels:            append(append([]string{}, neededLabels...), forbiddenLabels[0]),
   127  			author:            "batman",
   128  			firstQueryAuthor:  "batman",
   129  			secondQueryAuthor: "batman",
   130  			milestone:         "v1.0",
   131  			inPool:            false,
   132  
   133  			state: github.StatusPending,
   134  			desc:  fmt.Sprintf(statusNotInPool, " Should not have forbidden-1 label."),
   135  		},
   136  		{
   137  			name:              "only mention one requirement class",
   138  			labels:            append(append([]string{}, neededLabels[1:]...), forbiddenLabels[0]),
   139  			author:            "batman",
   140  			firstQueryAuthor:  "batman",
   141  			secondQueryAuthor: "batman",
   142  			milestone:         "v1.0",
   143  			inPool:            false,
   144  
   145  			state: github.StatusPending,
   146  			desc:  fmt.Sprintf(statusNotInPool, " Needs need-1 label."),
   147  		},
   148  		{
   149  			name:                  "mention all possible queries when opted in",
   150  			labels:                append(append([]string{}, neededLabels[1:]...), forbiddenLabels[0]),
   151  			author:                "batman",
   152  			firstQueryAuthor:      "batman",
   153  			secondQueryAuthor:     "batman",
   154  			milestone:             "v1.0",
   155  			inPool:                false,
   156  			displayAllTideQueries: true,
   157  
   158  			state: github.StatusPending,
   159  			desc:  fmt.Sprintf(statusNotInPool, " Needs need-1 label OR Needs 1, 2, 3, 4, 5, 6, 7 labels."),
   160  		},
   161  		{
   162  			name:                  "displayAllTideQueries but only one query",
   163  			labels:                append(append([]string{}, neededLabels[1:]...), forbiddenLabels[0]),
   164  			author:                "batman",
   165  			firstQueryAuthor:      "batman",
   166  			secondQueryAuthor:     "batman",
   167  			milestone:             "v1.0",
   168  			inPool:                false,
   169  			displayAllTideQueries: true,
   170  			singleQuery:           true,
   171  
   172  			state: github.StatusPending,
   173  			desc:  fmt.Sprintf(statusNotInPool, " Needs need-1 label."),
   174  		},
   175  		{
   176  			name:                  "displayAllTideQueries when there is no matching query",
   177  			baseref:               "bad",
   178  			branchDenyList:        []string{"bad"},
   179  			sameBranchReqs:        true,
   180  			labels:                append(append([]string{}, neededLabels[1:]...), forbiddenLabels[0]),
   181  			author:                "batman",
   182  			firstQueryAuthor:      "batman",
   183  			secondQueryAuthor:     "batman",
   184  			milestone:             "v1.0",
   185  			inPool:                false,
   186  			displayAllTideQueries: true,
   187  
   188  			state: github.StatusPending,
   189  			desc:  fmt.Sprintf(statusNotInPool, " No Tide query for branch bad found."),
   190  		},
   191  		{
   192  			name:           "displayAllTideQueries shows only queries matching the branch",
   193  			baseref:        "main",
   194  			branchDenyList: []string{"main"},
   195  			sameBranchReqs: true,
   196  			additionalTideQueries: []config.TideQuery{{
   197  				Orgs:   []string{""},
   198  				Labels: []string{"good-to-go"},
   199  			}},
   200  			labels:                append(append([]string{}, neededLabels[1:]...), forbiddenLabels[0]),
   201  			author:                "batman",
   202  			firstQueryAuthor:      "batman",
   203  			secondQueryAuthor:     "batman",
   204  			milestone:             "v1.0",
   205  			inPool:                false,
   206  			displayAllTideQueries: true,
   207  
   208  			state: github.StatusPending,
   209  			desc:  fmt.Sprintf(statusNotInPool, " Needs good-to-go label."),
   210  		},
   211  		{
   212  			name:           "against excluded branch",
   213  			baseref:        "bad",
   214  			branchDenyList: []string{"bad"},
   215  			sameBranchReqs: true,
   216  			labels:         neededLabels,
   217  			inPool:         false,
   218  
   219  			state: github.StatusPending,
   220  			desc:  fmt.Sprintf(statusNotInPool, " Merging to branch bad is forbidden."),
   221  		},
   222  		{
   223  			name:            "not against included branch",
   224  			baseref:         "bad",
   225  			branchAllowList: []string{"good"},
   226  			sameBranchReqs:  true,
   227  			labels:          neededLabels,
   228  			inPool:          false,
   229  
   230  			state: github.StatusPending,
   231  			desc:  fmt.Sprintf(statusNotInPool, " Merging to branch bad is forbidden."),
   232  		},
   233  		{
   234  			name:              "choose query for correct branch",
   235  			baseref:           "bad",
   236  			branchAllowList:   []string{"good"},
   237  			author:            "batman",
   238  			firstQueryAuthor:  "batman",
   239  			secondQueryAuthor: "batman",
   240  			milestone:         "v1.0",
   241  			labels:            neededLabels,
   242  			inPool:            false,
   243  
   244  			state: github.StatusPending,
   245  			desc:  fmt.Sprintf(statusNotInPool, " Needs 1, 2, 3, 4, 5, 6, 7 labels."),
   246  		},
   247  		{
   248  			name:              "tides own context failed but is ignored",
   249  			labels:            neededLabels,
   250  			author:            "batman",
   251  			firstQueryAuthor:  "batman",
   252  			secondQueryAuthor: "batman",
   253  			milestone:         "v1.0",
   254  			contexts:          []Context{{Context: githubql.String(statusContext), State: githubql.StatusStateError}},
   255  			inPool:            false,
   256  
   257  			state: github.StatusSuccess,
   258  			desc:  statusInPool,
   259  		},
   260  		{
   261  			name:              "single bad context",
   262  			labels:            neededLabels,
   263  			contexts:          []Context{{Context: githubql.String("job-name"), State: githubql.StatusStateError}},
   264  			author:            "batman",
   265  			firstQueryAuthor:  "batman",
   266  			secondQueryAuthor: "batman",
   267  			milestone:         "v1.0",
   268  			inPool:            false,
   269  
   270  			state: github.StatusPending,
   271  			desc:  fmt.Sprintf(statusNotInPool, " Job job-name has not succeeded."),
   272  		},
   273  		{
   274  			name:              "single bad checkrun",
   275  			labels:            neededLabels,
   276  			checkRuns:         []CheckRun{{Name: githubql.String("job-name"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String(githubql.StatusStateFailure)}},
   277  			author:            "batman",
   278  			firstQueryAuthor:  "batman",
   279  			secondQueryAuthor: "batman",
   280  			milestone:         "v1.0",
   281  			inPool:            false,
   282  
   283  			state: github.StatusPending,
   284  			desc:  fmt.Sprintf(statusNotInPool, " Job job-name has not succeeded."),
   285  		},
   286  		{
   287  			name:              "single good checkrun",
   288  			labels:            neededLabels,
   289  			checkRuns:         []CheckRun{{Name: githubql.String("job-name"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String(githubql.StatusStateSuccess)}},
   290  			author:            "batman",
   291  			firstQueryAuthor:  "batman",
   292  			secondQueryAuthor: "batman",
   293  			milestone:         "v1.0",
   294  			inPool:            true,
   295  
   296  			state: github.StatusSuccess,
   297  			desc:  statusInPool,
   298  		},
   299  		{
   300  			name:   "multiple good checkruns",
   301  			labels: neededLabels,
   302  			checkRuns: []CheckRun{
   303  				{Name: githubql.String("job-name"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String(githubql.StatusStateSuccess)},
   304  				{Name: githubql.String("another-job"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String(githubql.StatusStateSuccess)},
   305  			},
   306  			author:            "batman",
   307  			firstQueryAuthor:  "batman",
   308  			secondQueryAuthor: "batman",
   309  			milestone:         "v1.0",
   310  			inPool:            true,
   311  
   312  			state: github.StatusSuccess,
   313  			desc:  statusInPool,
   314  		},
   315  		{
   316  			name:   "mix of good and bad checkruns",
   317  			labels: neededLabels,
   318  			checkRuns: []CheckRun{
   319  				{Name: githubql.String("job-name"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String(githubql.StatusStateSuccess)},
   320  				{Name: githubql.String("another-job"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String(githubql.StatusStateFailure)},
   321  			},
   322  			author:            "batman",
   323  			firstQueryAuthor:  "batman",
   324  			secondQueryAuthor: "batman",
   325  			milestone:         "v1.0",
   326  			inPool:            false,
   327  
   328  			state: github.StatusPending,
   329  			desc:  fmt.Sprintf(statusNotInPool, " Job another-job has not succeeded."),
   330  		},
   331  		{
   332  			name:   "mix of good status contexts and checkruns",
   333  			labels: neededLabels,
   334  			checkRuns: []CheckRun{
   335  				{Name: githubql.String("job-name"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String(githubql.StatusStateSuccess)},
   336  			},
   337  			contexts: []Context{
   338  				{Context: githubql.String("other-job-name"), State: githubql.StatusStateSuccess},
   339  			},
   340  			author:            "batman",
   341  			firstQueryAuthor:  "batman",
   342  			secondQueryAuthor: "batman",
   343  			milestone:         "v1.0",
   344  			inPool:            true,
   345  
   346  			state: github.StatusSuccess,
   347  			desc:  statusInPool,
   348  		},
   349  		{
   350  			name:   "mix of bad status contexts and checkruns",
   351  			labels: neededLabels,
   352  			checkRuns: []CheckRun{
   353  				{Name: githubql.String("job-name"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String(githubql.StatusStateFailure)},
   354  			},
   355  			contexts: []Context{
   356  				{Context: githubql.String("other-job-name"), State: githubql.StatusStateFailure},
   357  			},
   358  			author:            "batman",
   359  			firstQueryAuthor:  "batman",
   360  			secondQueryAuthor: "batman",
   361  			milestone:         "v1.0",
   362  			inPool:            false,
   363  
   364  			state: github.StatusPending,
   365  			desc:  fmt.Sprintf(statusNotInPool, " Jobs job-name, other-job-name have not succeeded."),
   366  		},
   367  		{
   368  			name:   "good context, bad checkrun",
   369  			labels: neededLabels,
   370  			checkRuns: []CheckRun{
   371  				{Name: githubql.String("job-name"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String(githubql.StatusStateFailure)},
   372  			},
   373  			contexts: []Context{
   374  				{Context: githubql.String("other-job-name"), State: githubql.StatusStateSuccess},
   375  			},
   376  			author:            "batman",
   377  			firstQueryAuthor:  "batman",
   378  			secondQueryAuthor: "batman",
   379  			milestone:         "v1.0",
   380  			inPool:            false,
   381  
   382  			state: github.StatusPending,
   383  			desc:  fmt.Sprintf(statusNotInPool, " Job job-name has not succeeded."),
   384  		},
   385  		{
   386  			name:   "bad context, good checkrun",
   387  			labels: neededLabels,
   388  			checkRuns: []CheckRun{
   389  				{Name: githubql.String("job-name"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String(githubql.StatusStateSuccess)},
   390  			},
   391  			contexts: []Context{
   392  				{Context: githubql.String("other-job-name"), State: githubql.StatusStateFailure},
   393  			},
   394  			author:            "batman",
   395  			firstQueryAuthor:  "batman",
   396  			secondQueryAuthor: "batman",
   397  			milestone:         "v1.0",
   398  			inPool:            false,
   399  
   400  			state: github.StatusPending,
   401  			desc:  fmt.Sprintf(statusNotInPool, " Job other-job-name has not succeeded."),
   402  		},
   403  		{
   404  			name:              "multiple bad contexts",
   405  			labels:            neededLabels,
   406  			author:            "batman",
   407  			firstQueryAuthor:  "batman",
   408  			secondQueryAuthor: "batman",
   409  			milestone:         "v1.0",
   410  			contexts: []Context{
   411  				{Context: githubql.String("job-name"), State: githubql.StatusStateError},
   412  				{Context: githubql.String("other-job-name"), State: githubql.StatusStateError},
   413  			},
   414  			inPool: false,
   415  
   416  			state: github.StatusPending,
   417  			desc:  fmt.Sprintf(statusNotInPool, " Jobs job-name, other-job-name have not succeeded."),
   418  		},
   419  		{
   420  			name:              "multiple bad checkruns",
   421  			labels:            neededLabels,
   422  			author:            "batman",
   423  			firstQueryAuthor:  "batman",
   424  			secondQueryAuthor: "batman",
   425  			milestone:         "v1.0",
   426  			checkRuns: []CheckRun{
   427  				{Name: githubql.String("job-name"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String(githubql.StatusStateFailure)},
   428  				{Name: githubql.String("other-job-name"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String(githubql.StatusStateFailure)},
   429  			},
   430  			inPool: false,
   431  
   432  			state: github.StatusPending,
   433  			desc:  fmt.Sprintf(statusNotInPool, " Jobs job-name, other-job-name have not succeeded."),
   434  		},
   435  		{
   436  			name:              "wrong author",
   437  			labels:            neededLabels,
   438  			author:            "robin",
   439  			firstQueryAuthor:  "batman",
   440  			secondQueryAuthor: "batman",
   441  			milestone:         "v1.0",
   442  			contexts:          []Context{{Context: githubql.String("job-name"), State: githubql.StatusStateSuccess}},
   443  			inPool:            false,
   444  
   445  			state: github.StatusPending,
   446  			desc:  fmt.Sprintf(statusNotInPool, " Must be by author batman."),
   447  		},
   448  		{
   449  			name:              "wrong author; use lowest diff",
   450  			labels:            neededLabels,
   451  			author:            "robin",
   452  			firstQueryAuthor:  "penguin",
   453  			secondQueryAuthor: "batman",
   454  			milestone:         "v1.0",
   455  			contexts:          []Context{{Context: githubql.String("job-name"), State: githubql.StatusStateSuccess}},
   456  			inPool:            false,
   457  
   458  			state: github.StatusPending,
   459  			desc:  fmt.Sprintf(statusNotInPool, " Must be by author penguin."),
   460  		},
   461  		{
   462  			name:              "wrong milestone",
   463  			labels:            neededLabels,
   464  			author:            "batman",
   465  			firstQueryAuthor:  "batman",
   466  			secondQueryAuthor: "batman",
   467  			milestone:         "v1.1",
   468  			contexts:          []Context{{Context: githubql.String("job-name"), State: githubql.StatusStateSuccess}},
   469  			inPool:            false,
   470  
   471  			state: github.StatusPending,
   472  			desc:  fmt.Sprintf(statusNotInPool, " Must be in milestone v1.0."),
   473  		},
   474  		{
   475  			name:              "not in pool, but all requirements are met",
   476  			labels:            neededLabels,
   477  			author:            "batman",
   478  			firstQueryAuthor:  "batman",
   479  			secondQueryAuthor: "batman",
   480  			milestone:         "v1.0",
   481  			inPool:            false,
   482  
   483  			state: github.StatusSuccess,
   484  			desc:  statusInPool,
   485  		},
   486  		{
   487  			name:              "not in pool, but all requirements are met, including a successful third-party context",
   488  			labels:            neededLabels,
   489  			author:            "batman",
   490  			firstQueryAuthor:  "batman",
   491  			secondQueryAuthor: "batman",
   492  			milestone:         "v1.0",
   493  			contexts:          []Context{{Context: githubql.String("job-name"), State: githubql.StatusStateSuccess}},
   494  			inPool:            false,
   495  
   496  			state: github.StatusSuccess,
   497  			desc:  statusInPool,
   498  		},
   499  		{
   500  			name:              "check that min diff query is used",
   501  			labels:            []string{"3", "4", "5", "6", "7"},
   502  			author:            "batman",
   503  			firstQueryAuthor:  "batman",
   504  			secondQueryAuthor: "batman",
   505  			milestone:         "v1.0",
   506  			inPool:            false,
   507  
   508  			state: github.StatusPending,
   509  			desc:  fmt.Sprintf(statusNotInPool, " Needs 1, 2 labels."),
   510  		},
   511  		{
   512  			name:              "check that blockers take precedence over other queries",
   513  			labels:            []string{"3", "4", "5", "6", "7"},
   514  			author:            "batman",
   515  			firstQueryAuthor:  "batman",
   516  			secondQueryAuthor: "batman",
   517  			milestone:         "v1.0",
   518  			inPool:            false,
   519  			blocks:            []int{1, 2},
   520  
   521  			state: github.StatusError,
   522  			desc:  fmt.Sprintf(statusNotInPool, " Merging is blocked by issues 1, 2."),
   523  		},
   524  		{
   525  			name:             "missing passing up-to-date context",
   526  			inPool:           true,
   527  			baseref:          "baseref",
   528  			requiredContexts: []string{"foo", "bar"},
   529  			prowJobs: []runtime.Object{
   530  				&prowapi.ProwJob{
   531  					ObjectMeta: metav1.ObjectMeta{Name: "123"},
   532  					Spec: prowapi.ProwJobSpec{
   533  						Context: "foo",
   534  						Refs: &prowapi.Refs{
   535  							BaseSHA: "baseref",
   536  							Pulls:   []prowapi.Pull{{SHA: "head"}},
   537  						},
   538  						Type: prowapi.PresubmitJob,
   539  					},
   540  					Status: prowapi.ProwJobStatus{
   541  						State: prowapi.SuccessState,
   542  					},
   543  				},
   544  				&prowapi.ProwJob{
   545  					ObjectMeta: metav1.ObjectMeta{Name: "1234"},
   546  					Spec: prowapi.ProwJobSpec{
   547  						Context: "bar",
   548  						Refs: &prowapi.Refs{
   549  							BaseSHA: "baseref",
   550  							Pulls:   []prowapi.Pull{{SHA: "head"}},
   551  						},
   552  						Type: prowapi.PresubmitJob,
   553  					},
   554  					Status: prowapi.ProwJobStatus{
   555  						State: prowapi.PendingState,
   556  					},
   557  				},
   558  			},
   559  
   560  			state: github.StatusPending,
   561  			desc:  "Not mergeable. Retesting: bar",
   562  		},
   563  		{
   564  			name:             "missing passing up-to-date contexts",
   565  			inPool:           true,
   566  			baseref:          "baseref",
   567  			requiredContexts: []string{"foo", "bar", "baz"},
   568  			prowJobs: []runtime.Object{
   569  				&prowapi.ProwJob{
   570  					ObjectMeta: metav1.ObjectMeta{Name: "123"},
   571  					Spec: prowapi.ProwJobSpec{
   572  						Context: "foo",
   573  						Refs: &prowapi.Refs{
   574  							BaseSHA: "baseref",
   575  							Pulls:   []prowapi.Pull{{SHA: "head"}},
   576  						},
   577  						Type: prowapi.PresubmitJob,
   578  					},
   579  					Status: prowapi.ProwJobStatus{
   580  						State: prowapi.SuccessState,
   581  					},
   582  				},
   583  				&prowapi.ProwJob{
   584  					ObjectMeta: metav1.ObjectMeta{Name: "1234"},
   585  					Spec: prowapi.ProwJobSpec{
   586  						Context: "bar",
   587  						Refs: &prowapi.Refs{
   588  							BaseSHA: "baseref",
   589  							Pulls:   []prowapi.Pull{{SHA: "head"}},
   590  						},
   591  						Type: prowapi.PresubmitJob,
   592  					},
   593  					Status: prowapi.ProwJobStatus{
   594  						State: prowapi.PendingState,
   595  					},
   596  				},
   597  				&prowapi.ProwJob{
   598  					ObjectMeta: metav1.ObjectMeta{Name: "12345"},
   599  					Spec: prowapi.ProwJobSpec{
   600  						Context: "baz",
   601  						Refs: &prowapi.Refs{
   602  							BaseSHA: "baseref",
   603  							Pulls:   []prowapi.Pull{{SHA: "head"}},
   604  						},
   605  						Type: prowapi.PresubmitJob,
   606  					},
   607  					Status: prowapi.ProwJobStatus{
   608  						State: prowapi.PendingState,
   609  					},
   610  				},
   611  			},
   612  
   613  			state: github.StatusPending,
   614  			desc:  "Not mergeable. Retesting: bar baz",
   615  		},
   616  		{
   617  			name:             "missing passing up-to-date contexts with different ordering",
   618  			inPool:           true,
   619  			baseref:          "baseref",
   620  			requiredContexts: []string{"foo", "bar", "baz"},
   621  			prowJobs: []runtime.Object{
   622  				&prowapi.ProwJob{
   623  					ObjectMeta: metav1.ObjectMeta{Name: "123"},
   624  					Spec: prowapi.ProwJobSpec{
   625  						Context: "foo",
   626  						Refs: &prowapi.Refs{
   627  							BaseSHA: "baseref",
   628  							Pulls:   []prowapi.Pull{{SHA: "head"}},
   629  						},
   630  						Type: prowapi.PresubmitJob,
   631  					},
   632  					Status: prowapi.ProwJobStatus{
   633  						State: prowapi.SuccessState,
   634  					},
   635  				},
   636  				&prowapi.ProwJob{
   637  					ObjectMeta: metav1.ObjectMeta{Name: "1234"},
   638  					Spec: prowapi.ProwJobSpec{
   639  						Context: "baz",
   640  						Refs: &prowapi.Refs{
   641  							BaseSHA: "baseref",
   642  							Pulls:   []prowapi.Pull{{SHA: "head"}},
   643  						},
   644  						Type: prowapi.PresubmitJob,
   645  					},
   646  					Status: prowapi.ProwJobStatus{
   647  						State: prowapi.PendingState,
   648  					},
   649  				},
   650  				&prowapi.ProwJob{
   651  					ObjectMeta: metav1.ObjectMeta{Name: "12345"},
   652  					Spec: prowapi.ProwJobSpec{
   653  						Context: "bar",
   654  						Refs: &prowapi.Refs{
   655  							BaseSHA: "baseref",
   656  							Pulls:   []prowapi.Pull{{SHA: "head"}},
   657  						},
   658  						Type: prowapi.PresubmitJob,
   659  					},
   660  					Status: prowapi.ProwJobStatus{
   661  						State: prowapi.PendingState,
   662  					},
   663  				},
   664  			},
   665  
   666  			state: github.StatusPending,
   667  			desc:  "Not mergeable. Retesting: bar baz",
   668  		},
   669  		{
   670  			name:    "long list of not up-to-date contexts results in shortened message",
   671  			inPool:  true,
   672  			baseref: "baseref",
   673  			requiredContexts: []string{
   674  				strings.Repeat("very-long-context", 8),
   675  				strings.Repeat("also-long-content", 8),
   676  			},
   677  			prowJobs: []runtime.Object{
   678  				&prowapi.ProwJob{
   679  					ObjectMeta: metav1.ObjectMeta{Name: "123"},
   680  					Spec: prowapi.ProwJobSpec{
   681  						Context: strings.Repeat("very-long-context", 8),
   682  						Refs: &prowapi.Refs{
   683  							BaseSHA: "baseref",
   684  							Pulls:   []prowapi.Pull{{SHA: "head"}},
   685  						},
   686  						Type: prowapi.PresubmitJob,
   687  					},
   688  					Status: prowapi.ProwJobStatus{
   689  						State: prowapi.PendingState,
   690  					},
   691  				},
   692  				&prowapi.ProwJob{
   693  					ObjectMeta: metav1.ObjectMeta{Name: "1234"},
   694  					Spec: prowapi.ProwJobSpec{
   695  						Context: strings.Repeat("also-long-content", 8),
   696  						Refs: &prowapi.Refs{
   697  							BaseSHA: "baseref",
   698  							Pulls:   []prowapi.Pull{{SHA: "head"}},
   699  						},
   700  						Type: prowapi.PresubmitJob,
   701  					},
   702  					Status: prowapi.ProwJobStatus{
   703  						State: prowapi.PendingState,
   704  					},
   705  				},
   706  			},
   707  
   708  			state: github.StatusPending,
   709  			desc:  "Not mergeable. Retesting 2 jobs.",
   710  		},
   711  		{
   712  			name:           "mergeconflicts",
   713  			inPool:         true,
   714  			mergeConflicts: true,
   715  			state:          github.StatusError,
   716  			desc:           "Not mergeable. PR has a merge conflict.",
   717  		},
   718  		{
   719  			name:                  "Missing approving review",
   720  			additionalTideQueries: []config.TideQuery{{Orgs: []string{""}, ReviewApprovedRequired: true}},
   721  			inPool:                false,
   722  
   723  			state: github.StatusPending,
   724  			desc:  "Not mergeable. PullRequest is missing sufficient approving GitHub review(s)",
   725  		},
   726  		{
   727  			name:                  "Required approving review is present",
   728  			additionalTideQueries: []config.TideQuery{{Orgs: []string{""}, ReviewApprovedRequired: true}},
   729  			inPool:                false,
   730  			hasApprovingReview:    true,
   731  
   732  			state: github.StatusSuccess,
   733  			desc:  "In merge pool.",
   734  		},
   735  	}
   736  
   737  	for _, tc := range testcases {
   738  		t.Run(tc.name, func(t *testing.T) {
   739  			secondQuery := config.TideQuery{
   740  				Orgs:      []string{""},
   741  				Labels:    []string{"1", "2", "3", "4", "5", "6", "7"}, // lots of requirements
   742  				Author:    tc.secondQueryAuthor,
   743  				Milestone: "v1.0",
   744  			}
   745  			if tc.sameBranchReqs {
   746  				secondQuery.ExcludedBranches = tc.branchDenyList
   747  				secondQuery.IncludedBranches = tc.branchAllowList
   748  			}
   749  			queries := config.TideQueries{
   750  				config.TideQuery{
   751  					Orgs:             []string{""},
   752  					ExcludedBranches: tc.branchDenyList,
   753  					IncludedBranches: tc.branchAllowList,
   754  					Labels:           neededLabelsWithAlt,
   755  					MissingLabels:    forbiddenLabels,
   756  					Author:           tc.firstQueryAuthor,
   757  					Milestone:        "v1.0",
   758  				},
   759  				secondQuery,
   760  			}
   761  			if tc.singleQuery {
   762  				queries = config.TideQueries{queries[0]}
   763  			}
   764  			queries = append(queries, tc.additionalTideQueries...)
   765  			queriesByRepo := queries.QueryMap()
   766  			var pr PullRequest
   767  			pr.BaseRef = struct {
   768  				Name   githubql.String
   769  				Prefix githubql.String
   770  			}{
   771  				Name: githubql.String(tc.baseref),
   772  			}
   773  			for _, label := range tc.labels {
   774  				pr.Labels.Nodes = append(
   775  					pr.Labels.Nodes,
   776  					struct{ Name githubql.String }{Name: githubql.String(label)},
   777  				)
   778  			}
   779  			pr.HeadRefOID = githubql.String("head")
   780  			var checkRunNodes []CheckRunNode
   781  			for _, checkRun := range tc.checkRuns {
   782  				checkRunNodes = append(checkRunNodes, CheckRunNode{CheckRun: checkRun})
   783  			}
   784  			pr.Commits.Nodes = append(
   785  				pr.Commits.Nodes,
   786  				struct{ Commit Commit }{
   787  					Commit: Commit{
   788  						Status: struct{ Contexts []Context }{
   789  							Contexts: tc.contexts,
   790  						},
   791  						OID: githubql.String("head"),
   792  						StatusCheckRollup: StatusCheckRollup{
   793  							Contexts: StatusCheckRollupContext{
   794  								Nodes: checkRunNodes,
   795  							},
   796  						},
   797  					},
   798  				},
   799  			)
   800  			pr.Author = struct {
   801  				Login githubql.String
   802  			}{githubql.String(tc.author)}
   803  			if tc.milestone != "" {
   804  				pr.Milestone = &Milestone{githubql.String(tc.milestone)}
   805  			}
   806  			if tc.mergeConflicts {
   807  				pr.Mergeable = githubql.MergeableStateConflicting
   808  			}
   809  			if tc.hasApprovingReview {
   810  				pr.ReviewDecision = githubql.PullRequestReviewDecisionApproved
   811  			}
   812  			var pool map[string]CodeReviewCommon
   813  			if tc.inPool {
   814  				pool = map[string]CodeReviewCommon{"#0": {}}
   815  			}
   816  			blocks := blockers.Blockers{
   817  				Repo: map[blockers.OrgRepo][]blockers.Blocker{},
   818  			}
   819  			var items []blockers.Blocker
   820  			for _, block := range tc.blocks {
   821  				items = append(items, blockers.Blocker{Number: block})
   822  			}
   823  			blocks.Repo[blockers.OrgRepo{Org: "", Repo: ""}] = items
   824  
   825  			ca := &config.Agent{}
   826  			ca.Set(&config.Config{ProwConfig: config.ProwConfig{Tide: config.Tide{
   827  				TideGitHubConfig: config.TideGitHubConfig{
   828  					DisplayAllQueriesInStatus: tc.displayAllTideQueries,
   829  					MergeLabel:                mergeLabel,
   830  					SquashLabel:               squashLabel,
   831  				}}}})
   832  			mmc := newMergeChecker(ca.Config, &fgc{})
   833  
   834  			sc, err := newStatusController(
   835  				context.Background(),
   836  				logrus.NewEntry(logrus.StandardLogger()),
   837  				nil,
   838  				newFakeManager(tc.prowJobs...),
   839  				nil,
   840  				ca.Config,
   841  				nil,
   842  				"",
   843  				mmc,
   844  				false,
   845  				&statusUpdate{
   846  					dontUpdateStatus: &threadSafePRSet{},
   847  					newPoolPending:   make(chan bool),
   848  				},
   849  			)
   850  			if err != nil {
   851  				t.Fatalf("failed to get statusController: %v", err)
   852  			}
   853  			ccg := func() (contextChecker, error) {
   854  				return &config.TideContextPolicy{RequiredContexts: tc.requiredContexts}, nil
   855  			}
   856  			state, desc, err := sc.expectedStatus(sc.logger, queriesByRepo, CodeReviewCommonFromPullRequest(&pr), pool, ccg, blocks, tc.baseref)
   857  			if err != nil {
   858  				t.Fatalf("error calling expectedStatus(): %v", err)
   859  			}
   860  			if state != tc.state {
   861  				t.Errorf("Expected status state %q, but got %q.", string(tc.state), string(state))
   862  			}
   863  			if desc != tc.desc {
   864  				t.Errorf("Expected status description %q, but got %q.", tc.desc, desc)
   865  			}
   866  		})
   867  	}
   868  }
   869  
   870  func TestSetStatuses(t *testing.T) {
   871  	statusNotInPoolEmpty := fmt.Sprintf(statusNotInPool, "")
   872  	testcases := []struct {
   873  		name string
   874  
   875  		inPool          bool
   876  		hasContext      bool
   877  		inDontSetStatus bool
   878  		state           githubql.StatusState
   879  		desc            string
   880  
   881  		shouldSet bool
   882  	}{
   883  		{
   884  			name: "in pool with proper context",
   885  
   886  			inPool:     true,
   887  			hasContext: true,
   888  			state:      githubql.StatusStateSuccess,
   889  			desc:       statusInPool,
   890  
   891  			shouldSet: false,
   892  		},
   893  		{
   894  			name: "in pool without context",
   895  
   896  			inPool:     true,
   897  			hasContext: false,
   898  
   899  			shouldSet: true,
   900  		},
   901  		{
   902  			name: "in pool with improper context",
   903  
   904  			inPool:     true,
   905  			hasContext: true,
   906  			state:      githubql.StatusStateSuccess,
   907  			desc:       statusNotInPoolEmpty,
   908  
   909  			shouldSet: true,
   910  		},
   911  		{
   912  			name: "in pool with wrong state",
   913  
   914  			inPool:     true,
   915  			hasContext: true,
   916  			state:      githubql.StatusStatePending,
   917  			desc:       statusInPool,
   918  
   919  			shouldSet: true,
   920  		},
   921  		{
   922  			name: "in pool with wrong state but set to not update status",
   923  
   924  			inPool:          true,
   925  			hasContext:      true,
   926  			inDontSetStatus: true,
   927  			state:           githubql.StatusStatePending,
   928  			desc:            statusInPool,
   929  
   930  			shouldSet: false,
   931  		},
   932  		{
   933  			name: "not in pool with proper context",
   934  
   935  			inPool:     false,
   936  			hasContext: true,
   937  			state:      githubql.StatusStatePending,
   938  			desc:       statusNotInPoolEmpty,
   939  
   940  			shouldSet: false,
   941  		},
   942  		{
   943  			name: "not in pool with improper context",
   944  
   945  			inPool:     false,
   946  			hasContext: true,
   947  			state:      githubql.StatusStatePending,
   948  			desc:       statusInPool,
   949  
   950  			shouldSet: true,
   951  		},
   952  		{
   953  			name: "not in pool with no context",
   954  
   955  			inPool:     false,
   956  			hasContext: false,
   957  
   958  			shouldSet: true,
   959  		},
   960  	}
   961  	for _, tc := range testcases {
   962  		var pr PullRequest
   963  		pr.Commits.Nodes = []struct{ Commit Commit }{{}}
   964  		if tc.hasContext {
   965  			pr.Commits.Nodes[0].Commit.Status.Contexts = []Context{
   966  				{
   967  					Context:     githubql.String(statusContext),
   968  					State:       tc.state,
   969  					Description: githubql.String(tc.desc),
   970  				},
   971  			}
   972  		}
   973  		crc := CodeReviewCommonFromPullRequest(&pr)
   974  		pool := make(map[string]CodeReviewCommon)
   975  		if tc.inPool {
   976  			pool[prKey(crc)] = *crc
   977  		}
   978  		fc := &fgc{
   979  			refs: map[string]string{"/ heads/": "SHA"},
   980  		}
   981  		ca := &config.Agent{}
   982  		ca.Set(&config.Config{})
   983  		// setStatuses logs instead of returning errors.
   984  		// Construct a logger to watch for errors to be printed.
   985  		log := logrus.WithField("component", "tide")
   986  		initialLog, err := log.String()
   987  		if err != nil {
   988  			t.Fatalf("Failed to get log output before testing: %v", err)
   989  		}
   990  
   991  		mmc := newMergeChecker(ca.Config, fc)
   992  		sc, err := newStatusController(
   993  			context.Background(),
   994  			log,
   995  			fc,
   996  			newFakeManager(),
   997  			nil,
   998  			ca.Config,
   999  			nil,
  1000  			"",
  1001  			mmc,
  1002  			false,
  1003  			&statusUpdate{
  1004  				dontUpdateStatus: &threadSafePRSet{},
  1005  				newPoolPending:   make(chan bool),
  1006  			},
  1007  		)
  1008  		if err != nil {
  1009  			t.Fatalf("failed to get statusController: %v", err)
  1010  		}
  1011  		if tc.inDontSetStatus {
  1012  			sc.dontUpdateStatus = &threadSafePRSet{data: map[pullRequestIdentifier]struct{}{{}: {}}}
  1013  		}
  1014  		sc.setStatuses([]CodeReviewCommon{*crc}, pool, blockers.Blockers{}, nil, nil)
  1015  		if str, err := log.String(); err != nil {
  1016  			t.Fatalf("For case %s: failed to get log output: %v", tc.name, err)
  1017  		} else if str != initialLog {
  1018  			t.Errorf("For case %s: error setting status: %s", tc.name, str)
  1019  		}
  1020  		if tc.shouldSet && !fc.setStatus {
  1021  			t.Errorf("For case %s: should set but didn't", tc.name)
  1022  		} else if !tc.shouldSet && fc.setStatus {
  1023  			t.Errorf("For case %s: should not set but did", tc.name)
  1024  		}
  1025  	}
  1026  }
  1027  
  1028  func TestTargetUrl(t *testing.T) {
  1029  	testcases := []struct {
  1030  		name   string
  1031  		pr     *PullRequest
  1032  		config config.Tide
  1033  
  1034  		expectedURL string
  1035  	}{
  1036  		{
  1037  			name:        "no config",
  1038  			pr:          &PullRequest{},
  1039  			config:      config.Tide{},
  1040  			expectedURL: "",
  1041  		},
  1042  		{
  1043  			name:        "tide overview config",
  1044  			pr:          &PullRequest{},
  1045  			config:      config.Tide{TideGitHubConfig: config.TideGitHubConfig{TargetURLs: map[string]string{"*": "tide.com"}}},
  1046  			expectedURL: "tide.com",
  1047  		},
  1048  		{
  1049  			name:        "PR dashboard config and overview config",
  1050  			pr:          &PullRequest{},
  1051  			config:      config.Tide{TideGitHubConfig: config.TideGitHubConfig{TargetURLs: map[string]string{"*": "tide.com"}, PRStatusBaseURLs: map[string]string{"*": "pr.status.com"}}},
  1052  			expectedURL: "tide.com",
  1053  		},
  1054  		{
  1055  			name: "PR dashboard config",
  1056  			pr: &PullRequest{
  1057  				Author: struct {
  1058  					Login githubql.String
  1059  				}{Login: githubql.String("author")},
  1060  				Repository: struct {
  1061  					Name          githubql.String
  1062  					NameWithOwner githubql.String
  1063  					Owner         struct {
  1064  						Login githubql.String
  1065  					}
  1066  				}{NameWithOwner: githubql.String("org/repo")},
  1067  				HeadRefName: "head",
  1068  			},
  1069  			config:      config.Tide{TideGitHubConfig: config.TideGitHubConfig{PRStatusBaseURLs: map[string]string{"*": "pr.status.com"}}},
  1070  			expectedURL: "pr.status.com?query=is%3Apr+repo%3Aorg%2Frepo+author%3Aauthor+head%3Ahead",
  1071  		},
  1072  		{
  1073  			name: "generate link by default config",
  1074  			pr: &PullRequest{
  1075  				Author: struct {
  1076  					Login githubql.String
  1077  				}{Login: githubql.String("author")},
  1078  				Repository: struct {
  1079  					Name          githubql.String
  1080  					NameWithOwner githubql.String
  1081  					Owner         struct {
  1082  						Login githubql.String
  1083  					}
  1084  				}{
  1085  					Owner:         struct{ Login githubql.String }{Login: githubql.String("testOrg")},
  1086  					Name:          githubql.String("testRepo"),
  1087  					NameWithOwner: githubql.String("testOrg/testRepo"),
  1088  				},
  1089  				HeadRefName: "head",
  1090  			},
  1091  			config:      config.Tide{TideGitHubConfig: config.TideGitHubConfig{PRStatusBaseURLs: map[string]string{"*": "default.pr.status.com"}}},
  1092  			expectedURL: "default.pr.status.com?query=is%3Apr+repo%3AtestOrg%2FtestRepo+author%3Aauthor+head%3Ahead",
  1093  		},
  1094  		{
  1095  			name: "generate link by org config",
  1096  			pr: &PullRequest{
  1097  				Author: struct {
  1098  					Login githubql.String
  1099  				}{Login: githubql.String("author")},
  1100  				Repository: struct {
  1101  					Name          githubql.String
  1102  					NameWithOwner githubql.String
  1103  					Owner         struct {
  1104  						Login githubql.String
  1105  					}
  1106  				}{
  1107  					Owner:         struct{ Login githubql.String }{Login: githubql.String("testOrg")},
  1108  					Name:          githubql.String("testRepo"),
  1109  					NameWithOwner: githubql.String("testOrg/testRepo"),
  1110  				},
  1111  				HeadRefName: "head",
  1112  			},
  1113  			config: config.Tide{TideGitHubConfig: config.TideGitHubConfig{PRStatusBaseURLs: map[string]string{
  1114  				"*":       "default.pr.status.com",
  1115  				"testOrg": "byorg.pr.status.com"},
  1116  			}},
  1117  			expectedURL: "byorg.pr.status.com?query=is%3Apr+repo%3AtestOrg%2FtestRepo+author%3Aauthor+head%3Ahead",
  1118  		},
  1119  		{
  1120  			name: "generate link by repo config",
  1121  			pr: &PullRequest{
  1122  				Author: struct {
  1123  					Login githubql.String
  1124  				}{Login: githubql.String("author")},
  1125  				Repository: struct {
  1126  					Name          githubql.String
  1127  					NameWithOwner githubql.String
  1128  					Owner         struct {
  1129  						Login githubql.String
  1130  					}
  1131  				}{
  1132  					Owner:         struct{ Login githubql.String }{Login: githubql.String("testOrg")},
  1133  					Name:          githubql.String("testRepo"),
  1134  					NameWithOwner: githubql.String("testOrg/testRepo"),
  1135  				},
  1136  				HeadRefName: "head",
  1137  			},
  1138  			config: config.Tide{TideGitHubConfig: config.TideGitHubConfig{PRStatusBaseURLs: map[string]string{
  1139  				"*":                "default.pr.status.com",
  1140  				"testOrg":          "byorg.pr.status.com",
  1141  				"testOrg/testRepo": "byrepo.pr.status.com"},
  1142  			}},
  1143  			expectedURL: "byrepo.pr.status.com?query=is%3Apr+repo%3AtestOrg%2FtestRepo+author%3Aauthor+head%3Ahead",
  1144  		},
  1145  	}
  1146  
  1147  	for _, tc := range testcases {
  1148  		log := logrus.WithField("controller", "status-update")
  1149  		c := &config.Config{ProwConfig: config.ProwConfig{Tide: tc.config}}
  1150  		if actual, expected := targetURL(c, CodeReviewCommonFromPullRequest(tc.pr), log), tc.expectedURL; actual != expected {
  1151  			t.Errorf("%s: expected target URL %s but got %s", tc.name, expected, actual)
  1152  		}
  1153  	}
  1154  }
  1155  
  1156  func TestOpenPRsQuery(t *testing.T) {
  1157  	orgs := []string{"org", "kuber"}
  1158  	repos := []string{"k8s/k8s", "k8s/t-i"}
  1159  	exceptions := map[string]sets.Set[string]{
  1160  		"org":            sets.New[string]("org/repo1", "org/repo2"),
  1161  		"irrelevant-org": sets.New[string]("irrelevant-org/repo1", "irrelevant-org/repo2"),
  1162  	}
  1163  
  1164  	queriesByOrg := openPRsQueries(orgs, repos, exceptions)
  1165  	expectedQueriesByOrg := map[string]string{
  1166  		"org":   `-repo:"org/repo1" -repo:"org/repo2" archived:false is:pr org:"org" sort:updated-asc state:open`,
  1167  		"kuber": `archived:false is:pr org:"kuber" sort:updated-asc state:open`,
  1168  		"k8s":   ` archived:false is:pr repo:"k8s/k8s" repo:"k8s/t-i" sort:updated-asc state:open`,
  1169  	}
  1170  	for org, query := range queriesByOrg {
  1171  		// This is produced from a map so the result is not deterministic. Work around by using
  1172  		// the fact that the parameters are space split and do a space split, sort, space join.
  1173  		split := strings.Split(query, " ")
  1174  		sort.Strings(split)
  1175  		queriesByOrg[org] = strings.Join(split, " ")
  1176  	}
  1177  
  1178  	if diff := cmp.Diff(queriesByOrg, expectedQueriesByOrg); diff != "" {
  1179  		t.Errorf("actual queries differ from expected: %s", diff)
  1180  	}
  1181  }
  1182  
  1183  func TestIndexFuncPassingJobs(t *testing.T) {
  1184  	testCases := []struct {
  1185  		name     string
  1186  		pj       *prowapi.ProwJob
  1187  		expected []string
  1188  	}{
  1189  		{
  1190  			name: "Jobs that are not presubmit or batch are ignored",
  1191  			pj:   getProwJob(prowapi.PeriodicJob, "org", "", "repo", "baseSHA", prowapi.SuccessState, []prowapi.Pull{{SHA: "head"}}),
  1192  		},
  1193  		{
  1194  			name: "Non-Passing jobs are ignored",
  1195  			pj:   getProwJob(prowapi.PresubmitJob, "org", "repo", "", "baseSHA", prowapi.FailureState, []prowapi.Pull{{SHA: "head"}}),
  1196  		},
  1197  		{
  1198  			name:     "Indexkey is returned for presubmit job",
  1199  			pj:       getProwJob(prowapi.PresubmitJob, "org", "repo", "", "baseSHA", prowapi.SuccessState, []prowapi.Pull{{SHA: "head"}}),
  1200  			expected: []string{"org/repo@baseSHA+head"},
  1201  		},
  1202  		{
  1203  			name:     "Indexkeys are returned for batch job",
  1204  			pj:       getProwJob(prowapi.BatchJob, "org", "repo", "", "baseSHA", prowapi.SuccessState, []prowapi.Pull{{SHA: "head"}, {SHA: "head-2"}}),
  1205  			expected: []string{"org/repo@baseSHA+head", "org/repo@baseSHA+head-2"},
  1206  		},
  1207  	}
  1208  	for _, tc := range testCases {
  1209  		t.Run(tc.name, func(t *testing.T) {
  1210  			var results []string
  1211  			results = append(results, indexFuncPassingJobs(tc.pj)...)
  1212  			if diff := deep.Equal(tc.expected, results); diff != nil {
  1213  				t.Errorf("expected does not match result, diff: %v", diff)
  1214  			}
  1215  		})
  1216  	}
  1217  }
  1218  
  1219  func TestSetStatusRespectsRequiredContexts(t *testing.T) {
  1220  	var pr PullRequest
  1221  	pr.Commits.Nodes = []struct{ Commit Commit }{{}}
  1222  	pr.Repository.NameWithOwner = githubql.String("org/repo")
  1223  	pr.Number = githubql.Int(2)
  1224  	requiredContexts := map[string][]string{"org/repo#2": {"foo", "bar"}}
  1225  
  1226  	fghc := &fgc{
  1227  		refs: map[string]string{"/ heads/": "SHA"},
  1228  	}
  1229  	log := logrus.WithField("component", "tide")
  1230  	initialLog, err := log.String()
  1231  	if err != nil {
  1232  		t.Fatalf("Failed to get log output before testing: %v", err)
  1233  	}
  1234  
  1235  	ca := &config.Agent{}
  1236  	ca.Set(&config.Config{})
  1237  
  1238  	sc := &statusController{
  1239  		logger:   log,
  1240  		ghc:      fghc,
  1241  		config:   ca.Config,
  1242  		pjClient: fakectrlruntimeclient.NewClientBuilder().Build(),
  1243  		ghProvider: &GitHubProvider{
  1244  			ghc:          fghc,
  1245  			mergeChecker: newMergeChecker(ca.Config, fghc),
  1246  		},
  1247  		statusUpdate: &statusUpdate{
  1248  			dontUpdateStatus: &threadSafePRSet{},
  1249  			newPoolPending:   make(chan bool),
  1250  		},
  1251  	}
  1252  	crc := CodeReviewCommonFromPullRequest(&pr)
  1253  	pool := map[string]CodeReviewCommon{prKey(crc): *crc}
  1254  	sc.setStatuses([]CodeReviewCommon{*crc}, pool, blockers.Blockers{}, nil, requiredContexts)
  1255  	if str, err := log.String(); err != nil {
  1256  		t.Fatalf("Failed to get log output: %v", err)
  1257  	} else if str != initialLog {
  1258  		t.Errorf("Error setting status: %s", str)
  1259  	}
  1260  
  1261  	if n := len(fghc.statuses); n != 1 {
  1262  		t.Fatalf("expected exactly one status to be set, got %d", n)
  1263  	}
  1264  
  1265  	expectedDescription := "Not mergeable. Retesting: bar foo"
  1266  	val, exists := fghc.statuses["//"]
  1267  	if !exists {
  1268  		t.Fatal("Status didn't get set")
  1269  	}
  1270  	if val.Description != expectedDescription {
  1271  		t.Errorf("Expected description to be %q, was %q", expectedDescription, val.Description)
  1272  	}
  1273  }
  1274  
  1275  func TestNewBaseSHAGetter(t *testing.T) {
  1276  	org, repo, branch := "org", "repo", "branch"
  1277  	testCases := []struct {
  1278  		name     string
  1279  		baseSHAs map[string]string
  1280  		ghc      githubClient
  1281  
  1282  		expectedSHA string
  1283  		expectErr   bool
  1284  	}{
  1285  		{
  1286  			name:        "Default to content of baseSHAs map",
  1287  			baseSHAs:    map[string]string{"org/repo:branch": "123"},
  1288  			expectedSHA: "123",
  1289  		},
  1290  		{
  1291  			name:        "BaseSHAs map has no entry, ask GitHub",
  1292  			baseSHAs:    map[string]string{},
  1293  			ghc:         &fgc{refs: map[string]string{"org/repo heads/branch": "SHA"}},
  1294  			expectedSHA: "SHA",
  1295  		},
  1296  		{
  1297  			name:      "Error is returned",
  1298  			baseSHAs:  map[string]string{},
  1299  			ghc:       &fgc{err: errors.New("some-failure")},
  1300  			expectErr: true,
  1301  		},
  1302  	}
  1303  
  1304  	for _, tc := range testCases {
  1305  		t.Run(tc.name, func(t *testing.T) {
  1306  			result, err := newBaseSHAGetter(tc.baseSHAs, tc.ghc, org, repo, branch)()
  1307  			if err != nil && !tc.expectErr {
  1308  				t.Fatalf("unexpected error: %v", err)
  1309  			}
  1310  			if tc.expectErr {
  1311  				return
  1312  			}
  1313  			if result != tc.expectedSHA {
  1314  				t.Errorf("expected %q, got %q", tc.expectedSHA, result)
  1315  			}
  1316  			if val := tc.baseSHAs[org+"/"+repo+":"+branch]; val != tc.expectedSHA {
  1317  				t.Errorf("baseSHA in the map (%q) does not match expected(%q)", val, tc.expectedSHA)
  1318  			}
  1319  		})
  1320  	}
  1321  }
  1322  
  1323  func TestStatusControllerSearch(t *testing.T) {
  1324  	t.Parallel()
  1325  	testCases := []struct {
  1326  		name         string
  1327  		prs          map[string][]PullRequest
  1328  		usesAppsAuth bool
  1329  
  1330  		expected []CodeReviewCommon
  1331  	}{
  1332  		{
  1333  			name: "Apps auth: Query gets split by org",
  1334  			prs: map[string][]PullRequest{
  1335  				"org-a": {{Number: githubql.Int(1)}},
  1336  				"org-b": {{Number: githubql.Int(2)}},
  1337  			},
  1338  			usesAppsAuth: true,
  1339  			expected: []CodeReviewCommon{
  1340  				*CodeReviewCommonFromPullRequest(&PullRequest{Number: 1}),
  1341  				*CodeReviewCommonFromPullRequest(&PullRequest{Number: 2}),
  1342  			},
  1343  		},
  1344  		{
  1345  			name: "No apps auth: Query remains unsplit",
  1346  			prs: map[string][]PullRequest{
  1347  				"": {{Number: githubql.Int(1)}, {Number: githubql.Int(2)}},
  1348  			},
  1349  			usesAppsAuth: false,
  1350  			expected: []CodeReviewCommon{
  1351  				*CodeReviewCommonFromPullRequest(&PullRequest{Number: 1}),
  1352  				*CodeReviewCommonFromPullRequest(&PullRequest{Number: 2}),
  1353  			},
  1354  		},
  1355  	}
  1356  
  1357  	for _, tc := range testCases {
  1358  		t.Run(tc.name, func(t *testing.T) {
  1359  			ghc := &fgc{prs: tc.prs}
  1360  			cfg := func() *config.Config {
  1361  				return &config.Config{ProwConfig: config.ProwConfig{Tide: config.Tide{
  1362  					TideGitHubConfig: config.TideGitHubConfig{Queries: config.TideQueries{{Orgs: []string{"org-a", "org-b"}}}}}}}
  1363  			}
  1364  			sc, err := newStatusController(
  1365  				context.Background(),
  1366  				logrus.WithField("tc", tc),
  1367  				ghc,
  1368  				newFakeManager(),
  1369  				nil,
  1370  				cfg,
  1371  				nil,
  1372  				"",
  1373  				nil,
  1374  				tc.usesAppsAuth,
  1375  				&statusUpdate{
  1376  					dontUpdateStatus: &threadSafePRSet{},
  1377  					newPoolPending:   make(chan bool),
  1378  				},
  1379  			)
  1380  			if err != nil {
  1381  				t.Fatalf("failed to construct status controller: %v", err)
  1382  			}
  1383  
  1384  			result := sc.search()
  1385  			if diff := cmp.Diff(result, tc.expected, cmpopts.SortSlices(func(a, b CodeReviewCommon) bool { return a.Number < b.Number })); diff != "" {
  1386  				t.Errorf("result differs from expected: %s", diff)
  1387  			}
  1388  		})
  1389  	}
  1390  }