sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/tide/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 tide
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"errors"
    23  	"fmt"
    24  	"math/rand"
    25  	"net/http"
    26  	"net/http/httptest"
    27  	"reflect"
    28  	"sort"
    29  	"strconv"
    30  	"strings"
    31  	"sync"
    32  	"testing"
    33  	"text/template"
    34  	"time"
    35  
    36  	"github.com/go-test/deep"
    37  	"github.com/google/go-cmp/cmp"
    38  	"github.com/google/go-cmp/cmp/cmpopts"
    39  	fuzz "github.com/google/gofuzz"
    40  	githubql "github.com/shurcooL/githubv4"
    41  	"github.com/sirupsen/logrus"
    42  	"github.com/sirupsen/logrus/hooks/test"
    43  	"github.com/stretchr/testify/assert"
    44  	apiequality "k8s.io/apimachinery/pkg/api/equality"
    45  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    46  	"k8s.io/apimachinery/pkg/runtime"
    47  	"k8s.io/apimachinery/pkg/util/diff"
    48  	"k8s.io/apimachinery/pkg/util/sets"
    49  	utilpointer "k8s.io/utils/pointer"
    50  	ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client"
    51  	fakectrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
    52  
    53  	prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1"
    54  	"sigs.k8s.io/prow/pkg/config"
    55  	"sigs.k8s.io/prow/pkg/git/localgit"
    56  	"sigs.k8s.io/prow/pkg/git/types"
    57  	"sigs.k8s.io/prow/pkg/git/v2"
    58  	"sigs.k8s.io/prow/pkg/github"
    59  	"sigs.k8s.io/prow/pkg/kube"
    60  	"sigs.k8s.io/prow/pkg/tide/history"
    61  )
    62  
    63  func init() {
    64  	// Debugging tests without this isn't fun
    65  	logrus.SetLevel(logrus.DebugLevel)
    66  }
    67  
    68  var defaultBranch = localgit.DefaultBranch("")
    69  
    70  func testPullsMatchList(t *testing.T, test string, actual []CodeReviewCommon, expected []int) {
    71  	if len(actual) != len(expected) {
    72  		t.Errorf("Wrong size for case %s. Got PRs %+v, wanted numbers %v.", test, actual, expected)
    73  		return
    74  	}
    75  	for _, pr := range actual {
    76  		var found bool
    77  		n1 := int(pr.Number)
    78  		for _, n2 := range expected {
    79  			if n1 == n2 {
    80  				found = true
    81  			}
    82  		}
    83  		if !found {
    84  			t.Errorf("For case %s, found PR %d but shouldn't have.", test, n1)
    85  		}
    86  	}
    87  }
    88  
    89  func TestAccumulateBatch(t *testing.T) {
    90  	jobSet := []config.Presubmit{
    91  		{
    92  			Reporter: config.Reporter{Context: "foo"},
    93  		},
    94  		{
    95  			Reporter: config.Reporter{Context: "bar"},
    96  		},
    97  		{
    98  			Reporter: config.Reporter{Context: "baz"},
    99  		},
   100  	}
   101  	type pull struct {
   102  		number int
   103  		sha    string
   104  	}
   105  	type prowjob struct {
   106  		prs   []pull
   107  		job   string
   108  		state prowapi.ProwJobState
   109  	}
   110  	tests := []struct {
   111  		name           string
   112  		presubmits     []config.Presubmit
   113  		pulls          []pull
   114  		prowJobs       []prowjob
   115  		prowYAMLGetter config.ProwYAMLGetter
   116  
   117  		merges  []int
   118  		pending bool
   119  	}{
   120  		{
   121  			name: "no batches running",
   122  		},
   123  		{
   124  			name: "batch pending",
   125  			presubmits: []config.Presubmit{
   126  				{Reporter: config.Reporter{Context: "foo"}},
   127  			},
   128  			pulls:    []pull{{1, "a"}, {2, "b"}},
   129  			prowJobs: []prowjob{{job: "foo", state: prowapi.PendingState, prs: []pull{{1, "a"}}}},
   130  			pending:  true,
   131  		},
   132  		{
   133  			name:       "pending batch missing presubmits is ignored",
   134  			presubmits: jobSet,
   135  			pulls:      []pull{{1, "a"}, {2, "b"}},
   136  			prowJobs:   []prowjob{{job: "foo", state: prowapi.PendingState, prs: []pull{{1, "a"}}}},
   137  		},
   138  		{
   139  			name:       "batch pending, successful previous run",
   140  			presubmits: jobSet,
   141  			pulls:      []pull{{1, "a"}, {2, "b"}},
   142  			prowJobs: []prowjob{
   143  				{job: "foo", state: prowapi.PendingState, prs: []pull{{1, "a"}}},
   144  				{job: "bar", state: prowapi.SuccessState, prs: []pull{{1, "a"}}},
   145  				{job: "baz", state: prowapi.SuccessState, prs: []pull{{1, "a"}}},
   146  				{job: "foo", state: prowapi.SuccessState, prs: []pull{{2, "b"}}},
   147  				{job: "bar", state: prowapi.SuccessState, prs: []pull{{2, "b"}}},
   148  				{job: "baz", state: prowapi.SuccessState, prs: []pull{{2, "b"}}},
   149  			},
   150  			pending: true,
   151  			merges:  []int{2},
   152  		},
   153  		{
   154  			name:       "successful run",
   155  			presubmits: jobSet,
   156  			pulls:      []pull{{1, "a"}, {2, "b"}},
   157  			prowJobs: []prowjob{
   158  				{job: "foo", state: prowapi.SuccessState, prs: []pull{{2, "b"}}},
   159  				{job: "bar", state: prowapi.SuccessState, prs: []pull{{2, "b"}}},
   160  				{job: "baz", state: prowapi.SuccessState, prs: []pull{{2, "b"}}},
   161  			},
   162  			merges: []int{2},
   163  		},
   164  		{
   165  			name:       "successful run, multiple PRs",
   166  			presubmits: jobSet,
   167  			pulls:      []pull{{1, "a"}, {2, "b"}},
   168  			prowJobs: []prowjob{
   169  				{job: "foo", state: prowapi.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}},
   170  				{job: "bar", state: prowapi.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}},
   171  				{job: "baz", state: prowapi.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}},
   172  			},
   173  			merges: []int{1, 2},
   174  		},
   175  		{
   176  			name:       "successful run, failures in past",
   177  			presubmits: jobSet,
   178  			pulls:      []pull{{1, "a"}, {2, "b"}},
   179  			prowJobs: []prowjob{
   180  				{job: "foo", state: prowapi.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}},
   181  				{job: "bar", state: prowapi.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}},
   182  				{job: "baz", state: prowapi.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}},
   183  				{job: "foo", state: prowapi.FailureState, prs: []pull{{1, "a"}, {2, "b"}}},
   184  				{job: "baz", state: prowapi.FailureState, prs: []pull{{1, "a"}, {2, "b"}}},
   185  				{job: "foo", state: prowapi.FailureState, prs: []pull{{1, "c"}, {2, "b"}}},
   186  			},
   187  			merges: []int{1, 2},
   188  		},
   189  		{
   190  			name:       "failures",
   191  			presubmits: jobSet,
   192  			pulls:      []pull{{1, "a"}, {2, "b"}},
   193  			prowJobs: []prowjob{
   194  				{job: "foo", state: prowapi.FailureState, prs: []pull{{1, "a"}, {2, "b"}}},
   195  				{job: "bar", state: prowapi.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}},
   196  				{job: "baz", state: prowapi.FailureState, prs: []pull{{1, "a"}, {2, "b"}}},
   197  				{job: "foo", state: prowapi.FailureState, prs: []pull{{1, "c"}, {2, "b"}}},
   198  			},
   199  		},
   200  		{
   201  			name:       "missing job required by one PR",
   202  			presubmits: jobSet,
   203  			pulls:      []pull{{1, "a"}, {2, "b"}},
   204  			prowJobs: []prowjob{
   205  				{job: "foo", state: prowapi.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}},
   206  				{job: "bar", state: prowapi.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}},
   207  				{job: "baz", state: prowapi.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}},
   208  			},
   209  			prowYAMLGetter: prowYAMLGetterForHeadRefs([]string{"a", "b"}, []config.Presubmit{{
   210  				AlwaysRun: true,
   211  				Reporter:  config.Reporter{Context: "boo"},
   212  			}}),
   213  		},
   214  		{
   215  			name:       "successful run with PR that requires additional job",
   216  			presubmits: jobSet,
   217  			pulls:      []pull{{1, "a"}, {2, "b"}},
   218  			prowJobs: []prowjob{
   219  				{job: "foo", state: prowapi.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}},
   220  				{job: "bar", state: prowapi.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}},
   221  				{job: "baz", state: prowapi.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}},
   222  				{job: "boo", state: prowapi.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}},
   223  			},
   224  			merges: []int{1, 2},
   225  		},
   226  		{
   227  			name:    "no presubmits",
   228  			pulls:   []pull{{1, "a"}, {2, "b"}},
   229  			pending: false,
   230  		},
   231  		{
   232  			name:       "pending batch with PR that left pool, successful previous run",
   233  			presubmits: jobSet,
   234  			pulls:      []pull{{2, "b"}},
   235  			prowJobs: []prowjob{
   236  				{job: "foo", state: prowapi.PendingState, prs: []pull{{1, "a"}}},
   237  				{job: "foo", state: prowapi.SuccessState, prs: []pull{{2, "b"}}},
   238  				{job: "bar", state: prowapi.SuccessState, prs: []pull{{2, "b"}}},
   239  				{job: "baz", state: prowapi.SuccessState, prs: []pull{{2, "b"}}},
   240  			},
   241  			pending: false,
   242  			merges:  []int{2},
   243  		},
   244  	}
   245  	for _, test := range tests {
   246  		t.Run(test.name, func(t *testing.T) {
   247  
   248  			var pulls []CodeReviewCommon
   249  			for _, p := range test.pulls {
   250  				pr := PullRequest{
   251  					Number:     githubql.Int(p.number),
   252  					HeadRefOID: githubql.String(p.sha),
   253  				}
   254  				pulls = append(pulls, *CodeReviewCommonFromPullRequest(&pr))
   255  			}
   256  			var pjs []prowapi.ProwJob
   257  			for _, pj := range test.prowJobs {
   258  				npj := prowapi.ProwJob{
   259  					Spec: prowapi.ProwJobSpec{
   260  						Job:     pj.job,
   261  						Context: pj.job,
   262  						Type:    prowapi.BatchJob,
   263  						Refs:    new(prowapi.Refs),
   264  					},
   265  					Status: prowapi.ProwJobStatus{State: pj.state},
   266  				}
   267  				for _, pr := range pj.prs {
   268  					npj.Spec.Refs.Pulls = append(npj.Spec.Refs.Pulls, prowapi.Pull{
   269  						Number: pr.number,
   270  						SHA:    pr.sha,
   271  					})
   272  				}
   273  				pjs = append(pjs, npj)
   274  			}
   275  			for idx := range test.presubmits {
   276  				test.presubmits[idx].AlwaysRun = true
   277  			}
   278  
   279  			inrepoconfig := config.InRepoConfig{}
   280  			if test.prowYAMLGetter != nil {
   281  				inrepoconfig.Enabled = map[string]*bool{"*": utilpointer.Bool(true)}
   282  			}
   283  			cfg := func() *config.Config {
   284  				return &config.Config{
   285  					JobConfig: config.JobConfig{
   286  						PresubmitsStatic: map[string][]config.Presubmit{
   287  							"org/repo": test.presubmits,
   288  						},
   289  						ProwYAMLGetterWithDefaults: test.prowYAMLGetter,
   290  					},
   291  					ProwConfig: config.ProwConfig{
   292  						InRepoConfig: inrepoconfig,
   293  					},
   294  				}
   295  			}
   296  			c := &syncController{
   297  				config:       cfg,
   298  				provider:     newGitHubProvider(logrus.WithContext(context.Background()), nil, nil, cfg, nil, false),
   299  				changedFiles: &changedFilesAgent{},
   300  				logger:       logrus.WithField("test", test.name),
   301  			}
   302  			merges, pending := c.accumulateBatch(subpool{org: "org", repo: "repo", prs: pulls, pjs: pjs, log: logrus.WithField("test", test.name)})
   303  			if (len(pending) > 0) != test.pending {
   304  				t.Errorf("For case \"%s\", got wrong pending.", test.name)
   305  			}
   306  			testPullsMatchList(t, test.name, merges, test.merges)
   307  		})
   308  	}
   309  }
   310  
   311  func TestAccumulate(t *testing.T) {
   312  
   313  	const baseSHA = "8d287a3aeae90fd0aef4a70009c715712ff302cd"
   314  	jobSet := []config.Presubmit{
   315  		{
   316  			Reporter: config.Reporter{
   317  				Context: "job1",
   318  			},
   319  		},
   320  		{
   321  			Reporter: config.Reporter{
   322  				Context: "job2",
   323  			},
   324  		},
   325  	}
   326  	type prowjob struct {
   327  		prNumber int
   328  		job      string
   329  		state    prowapi.ProwJobState
   330  		sha      string
   331  	}
   332  	tests := []struct {
   333  		name                string
   334  		presubmits          map[int][]config.Presubmit
   335  		pullRequests        map[int]string
   336  		pullRequestModifier func(*PullRequest)
   337  		prowJobs            []prowjob
   338  
   339  		successes []int
   340  		pendings  []int
   341  		none      []int
   342  	}{
   343  		{
   344  			pullRequests: map[int]string{1: "", 2: "", 3: "", 4: "", 5: "", 6: "", 7: ""},
   345  			presubmits: map[int][]config.Presubmit{
   346  				1: jobSet,
   347  				2: jobSet,
   348  				3: jobSet,
   349  				4: jobSet,
   350  				5: jobSet,
   351  				6: jobSet,
   352  				7: jobSet,
   353  			},
   354  			prowJobs: []prowjob{
   355  				{2, "job1", prowapi.PendingState, ""},
   356  				{3, "job1", prowapi.PendingState, ""},
   357  				{3, "job2", prowapi.TriggeredState, ""},
   358  				{4, "job1", prowapi.FailureState, ""},
   359  				{4, "job2", prowapi.PendingState, ""},
   360  				{5, "job1", prowapi.PendingState, ""},
   361  				{5, "job2", prowapi.FailureState, ""},
   362  				{5, "job2", prowapi.PendingState, ""},
   363  				{6, "job1", prowapi.SuccessState, ""},
   364  				{6, "job2", prowapi.PendingState, ""},
   365  				{7, "job1", prowapi.SuccessState, ""},
   366  				{7, "job2", prowapi.SuccessState, ""},
   367  				{7, "job1", prowapi.FailureState, ""},
   368  			},
   369  
   370  			successes: []int{7},
   371  			pendings:  []int{3, 5, 6},
   372  			none:      []int{1, 2, 4},
   373  		},
   374  		{
   375  			pullRequests: map[int]string{7: ""},
   376  			presubmits: map[int][]config.Presubmit{
   377  				7: {
   378  					{Reporter: config.Reporter{Context: "job1"}},
   379  					{Reporter: config.Reporter{Context: "job2"}},
   380  					{Reporter: config.Reporter{Context: "job3"}},
   381  					{Reporter: config.Reporter{Context: "job4"}},
   382  				},
   383  			},
   384  			prowJobs: []prowjob{
   385  				{7, "job1", prowapi.SuccessState, ""},
   386  				{7, "job2", prowapi.FailureState, ""},
   387  				{7, "job3", prowapi.FailureState, ""},
   388  				{7, "job4", prowapi.FailureState, ""},
   389  				{7, "job3", prowapi.FailureState, ""},
   390  				{7, "job4", prowapi.FailureState, ""},
   391  				{7, "job2", prowapi.SuccessState, ""},
   392  				{7, "job3", prowapi.SuccessState, ""},
   393  				{7, "job4", prowapi.FailureState, ""},
   394  			},
   395  
   396  			successes: []int{},
   397  			pendings:  []int{},
   398  			none:      []int{7},
   399  		},
   400  		{
   401  			pullRequests: map[int]string{7: ""},
   402  			presubmits: map[int][]config.Presubmit{
   403  				7: {
   404  					{Reporter: config.Reporter{Context: "job1"}},
   405  					{Reporter: config.Reporter{Context: "job2"}},
   406  					{Reporter: config.Reporter{Context: "job3"}},
   407  					{Reporter: config.Reporter{Context: "job4"}},
   408  				},
   409  			},
   410  			prowJobs: []prowjob{
   411  				{7, "job1", prowapi.FailureState, ""},
   412  				{7, "job2", prowapi.FailureState, ""},
   413  				{7, "job3", prowapi.FailureState, ""},
   414  				{7, "job4", prowapi.FailureState, ""},
   415  				{7, "job3", prowapi.FailureState, ""},
   416  				{7, "job4", prowapi.FailureState, ""},
   417  				{7, "job2", prowapi.FailureState, ""},
   418  				{7, "job3", prowapi.FailureState, ""},
   419  				{7, "job4", prowapi.FailureState, ""},
   420  			},
   421  
   422  			successes: []int{},
   423  			pendings:  []int{},
   424  			none:      []int{7},
   425  		},
   426  		{
   427  			pullRequests: map[int]string{7: ""},
   428  			presubmits: map[int][]config.Presubmit{
   429  				7: {
   430  					{Reporter: config.Reporter{Context: "job1"}},
   431  					{Reporter: config.Reporter{Context: "job2"}},
   432  					{Reporter: config.Reporter{Context: "job3"}},
   433  					{Reporter: config.Reporter{Context: "job4"}},
   434  				},
   435  			},
   436  			prowJobs: []prowjob{
   437  				{7, "job1", prowapi.SuccessState, ""},
   438  				{7, "job2", prowapi.FailureState, ""},
   439  				{7, "job3", prowapi.FailureState, ""},
   440  				{7, "job4", prowapi.FailureState, ""},
   441  				{7, "job3", prowapi.FailureState, ""},
   442  				{7, "job4", prowapi.FailureState, ""},
   443  				{7, "job2", prowapi.SuccessState, ""},
   444  				{7, "job3", prowapi.SuccessState, ""},
   445  				{7, "job4", prowapi.SuccessState, ""},
   446  				{7, "job1", prowapi.FailureState, ""},
   447  			},
   448  
   449  			successes: []int{7},
   450  			pendings:  []int{},
   451  			none:      []int{},
   452  		},
   453  		{
   454  			pullRequests: map[int]string{7: ""},
   455  			presubmits: map[int][]config.Presubmit{
   456  				7: {
   457  					{Reporter: config.Reporter{Context: "job1"}},
   458  					{Reporter: config.Reporter{Context: "job2"}},
   459  					{Reporter: config.Reporter{Context: "job3"}},
   460  					{Reporter: config.Reporter{Context: "job4"}},
   461  				},
   462  			},
   463  			prowJobs: []prowjob{
   464  				{7, "job1", prowapi.SuccessState, ""},
   465  				{7, "job2", prowapi.FailureState, ""},
   466  				{7, "job3", prowapi.FailureState, ""},
   467  				{7, "job4", prowapi.FailureState, ""},
   468  				{7, "job3", prowapi.FailureState, ""},
   469  				{7, "job4", prowapi.FailureState, ""},
   470  				{7, "job2", prowapi.SuccessState, ""},
   471  				{7, "job3", prowapi.SuccessState, ""},
   472  				{7, "job4", prowapi.PendingState, ""},
   473  				{7, "job1", prowapi.FailureState, ""},
   474  			},
   475  
   476  			successes: []int{},
   477  			pendings:  []int{7},
   478  			none:      []int{},
   479  		},
   480  		{
   481  			presubmits: map[int][]config.Presubmit{
   482  				7: {
   483  					{Reporter: config.Reporter{Context: "job1"}},
   484  				},
   485  			},
   486  			pullRequests: map[int]string{7: "new", 8: "new"},
   487  			prowJobs: []prowjob{
   488  				{7, "job1", prowapi.SuccessState, "old"},
   489  				{7, "job1", prowapi.FailureState, "new"},
   490  				{8, "job1", prowapi.FailureState, "old"},
   491  				{8, "job1", prowapi.SuccessState, "new"},
   492  			},
   493  
   494  			successes: []int{8},
   495  			pendings:  []int{},
   496  			none:      []int{7},
   497  		},
   498  		{
   499  			pullRequests: map[int]string{7: "new", 8: "new"},
   500  			prowJobs:     []prowjob{},
   501  
   502  			successes: []int{8, 7},
   503  			pendings:  []int{},
   504  			none:      []int{},
   505  		},
   506  		{
   507  			name:         "Results from successful status context for which we do not have a prowjob anymore are considered",
   508  			presubmits:   map[int][]config.Presubmit{1: {{Reporter: config.Reporter{Context: "job1"}}}},
   509  			pullRequests: map[int]string{1: "headsha"},
   510  			pullRequestModifier: func(pr *PullRequest) {
   511  				pr.Commits.Nodes = []struct{ Commit Commit }{{
   512  					Commit: Commit{
   513  						OID: githubql.String("headsha"),
   514  						Status: CommitStatus{Contexts: []Context{{
   515  							Context:     githubql.String("job1"),
   516  							Description: githubql.String("Job succeeded. BaseSHA:" + baseSHA),
   517  							State:       githubql.StatusStateSuccess,
   518  						}}}},
   519  				}}
   520  			},
   521  
   522  			successes: []int{1},
   523  		},
   524  		{
   525  			name:         "Results from successful status context for wrong baseSHA is ignored",
   526  			presubmits:   map[int][]config.Presubmit{1: {{Reporter: config.Reporter{Context: "job1"}}}},
   527  			pullRequests: map[int]string{1: "headsha"},
   528  			pullRequestModifier: func(pr *PullRequest) {
   529  				pr.Commits.Nodes = []struct{ Commit Commit }{{
   530  					Commit: Commit{
   531  						OID: githubql.String("headsha"),
   532  						Status: CommitStatus{Contexts: []Context{{
   533  							Context:     githubql.String("job1"),
   534  							Description: githubql.String("Job succeeded. BaseSHA:c22a32add1a36daf3b16af3762b3922e70c9626a"),
   535  							State:       githubql.StatusStateSuccess,
   536  						}}}},
   537  				}}
   538  			},
   539  
   540  			none: []int{1},
   541  		},
   542  		{
   543  			name:         "Results from failed status context for which we do not have a prowjob anymore are irrelevant",
   544  			presubmits:   map[int][]config.Presubmit{1: {{Reporter: config.Reporter{Context: "job1"}}}},
   545  			pullRequests: map[int]string{1: "headsha"},
   546  			pullRequestModifier: func(pr *PullRequest) {
   547  				pr.Commits.Nodes = []struct{ Commit Commit }{{
   548  					Commit: Commit{
   549  						OID: githubql.String("headsha"),
   550  						Status: CommitStatus{Contexts: []Context{{
   551  							Context:     githubql.String("job1"),
   552  							Description: githubql.String("Job succeeded. BaseSHA:" + baseSHA),
   553  							State:       githubql.StatusStateFailure,
   554  						}}}},
   555  				}}
   556  			},
   557  
   558  			none: []int{1},
   559  		},
   560  		{
   561  			name:         "Successful status context and prowjob, success",
   562  			presubmits:   map[int][]config.Presubmit{1: {{Reporter: config.Reporter{Context: "job1"}}}},
   563  			pullRequests: map[int]string{1: "headsha"},
   564  			pullRequestModifier: func(pr *PullRequest) {
   565  				pr.Commits.Nodes = []struct{ Commit Commit }{{
   566  					Commit: Commit{
   567  						OID: githubql.String("headsha"),
   568  						Status: CommitStatus{Contexts: []Context{{
   569  							Context:     githubql.String("job1"),
   570  							Description: githubql.String("Job succeeded. BaseSHA:" + baseSHA),
   571  							State:       githubql.StatusStateSuccess,
   572  						}}}},
   573  				}}
   574  			},
   575  			prowJobs: []prowjob{{1, "job1", prowapi.SuccessState, "headsha"}},
   576  
   577  			successes: []int{1},
   578  		},
   579  		{
   580  			name:         "Successful status context, failed prowjob, success",
   581  			presubmits:   map[int][]config.Presubmit{1: {{Reporter: config.Reporter{Context: "job1"}}}},
   582  			pullRequests: map[int]string{1: "headsha"},
   583  			pullRequestModifier: func(pr *PullRequest) {
   584  				pr.Commits.Nodes = []struct{ Commit Commit }{{
   585  					Commit: Commit{
   586  						OID: githubql.String("headsha"),
   587  						Status: CommitStatus{Contexts: []Context{{
   588  							Context:     githubql.String("job1"),
   589  							Description: githubql.String("Job succeeded. BaseSHA:" + baseSHA),
   590  							State:       githubql.StatusStateSuccess,
   591  						}}}},
   592  				}}
   593  			},
   594  			prowJobs: []prowjob{{1, "job1", prowapi.FailureState, "headsha"}},
   595  
   596  			successes: []int{1},
   597  		},
   598  		{
   599  			name:         "Failed status context, successful prowjob, success",
   600  			presubmits:   map[int][]config.Presubmit{1: {{Reporter: config.Reporter{Context: "job1"}}}},
   601  			pullRequests: map[int]string{1: "headsha"},
   602  			pullRequestModifier: func(pr *PullRequest) {
   603  				pr.Commits.Nodes = []struct{ Commit Commit }{{
   604  					Commit: Commit{
   605  						OID: githubql.String("headsha"),
   606  						Status: CommitStatus{Contexts: []Context{{
   607  							Context:     githubql.String("job1"),
   608  							Description: githubql.String("Job succeeded. BaseSHA:" + baseSHA),
   609  							State:       githubql.StatusStateFailure,
   610  						}}}},
   611  				}}
   612  			},
   613  			prowJobs: []prowjob{{1, "job1", prowapi.SuccessState, "headsha"}},
   614  
   615  			successes: []int{1},
   616  		},
   617  		{
   618  			name:         "Failed status context and prowjob, failure",
   619  			presubmits:   map[int][]config.Presubmit{1: {{Reporter: config.Reporter{Context: "job1"}}}},
   620  			pullRequests: map[int]string{1: "headsha"},
   621  			pullRequestModifier: func(pr *PullRequest) {
   622  				pr.Commits.Nodes = []struct{ Commit Commit }{{
   623  					Commit: Commit{
   624  						OID: githubql.String("headsha"),
   625  						Status: CommitStatus{Contexts: []Context{{
   626  							Context:     githubql.String("job1"),
   627  							Description: githubql.String("Job succeeded. BaseSHA:" + baseSHA),
   628  							State:       githubql.StatusStateFailure,
   629  						}}}},
   630  				}}
   631  			},
   632  			prowJobs: []prowjob{{1, "job1", prowapi.FailureState, "headsha"}},
   633  
   634  			none: []int{1},
   635  		},
   636  		{
   637  			name: "Mixture of results from status context and prowjobs",
   638  			presubmits: map[int][]config.Presubmit{1: {
   639  				{Reporter: config.Reporter{Context: "job1"}},
   640  				{Reporter: config.Reporter{Context: "job2"}},
   641  			}},
   642  			pullRequests: map[int]string{1: "headsha"},
   643  			pullRequestModifier: func(pr *PullRequest) {
   644  				pr.Commits.Nodes = []struct{ Commit Commit }{{
   645  					Commit: Commit{
   646  						OID: githubql.String("headsha"),
   647  						Status: CommitStatus{Contexts: []Context{{
   648  							Context:     githubql.String("job1"),
   649  							Description: githubql.String("Job succeeded. BaseSHA:" + baseSHA),
   650  							State:       githubql.StatusStateSuccess,
   651  						}}}},
   652  				}}
   653  			},
   654  			prowJobs: []prowjob{{1, "job2", prowapi.SuccessState, "headsha"}},
   655  
   656  			successes: []int{1},
   657  		},
   658  	}
   659  
   660  	for i, test := range tests {
   661  		if test.name == "" {
   662  			test.name = strconv.Itoa(i)
   663  		}
   664  		t.Run(test.name, func(t *testing.T) {
   665  			syncCtrl := &syncController{
   666  				provider: &GitHubProvider{ghc: &fgc{}, logger: logrus.NewEntry(logrus.New())},
   667  				logger:   logrus.NewEntry(logrus.New()),
   668  			}
   669  			var pulls []CodeReviewCommon
   670  			for num, sha := range test.pullRequests {
   671  				newPull := PullRequest{Number: githubql.Int(num), HeadRefOID: githubql.String(sha)}
   672  				if test.pullRequestModifier != nil {
   673  					test.pullRequestModifier(&newPull)
   674  				}
   675  				pulls = append(pulls, *CodeReviewCommonFromPullRequest(&newPull))
   676  			}
   677  			var pjs []prowapi.ProwJob
   678  			for _, pj := range test.prowJobs {
   679  				pjs = append(pjs, prowapi.ProwJob{
   680  					Spec: prowapi.ProwJobSpec{
   681  						Job:     pj.job,
   682  						Context: pj.job,
   683  						Type:    prowapi.PresubmitJob,
   684  						Refs:    &prowapi.Refs{Pulls: []prowapi.Pull{{Number: pj.prNumber, SHA: pj.sha}}},
   685  					},
   686  					Status: prowapi.ProwJobStatus{State: pj.state},
   687  				})
   688  			}
   689  
   690  			successes, pendings, nones, _ := syncCtrl.accumulate(test.presubmits, pulls, pjs, baseSHA)
   691  
   692  			t.Logf("test run %d", i)
   693  			testPullsMatchList(t, "successes", successes, test.successes)
   694  			testPullsMatchList(t, "pendings", pendings, test.pendings)
   695  			testPullsMatchList(t, "nones", nones, test.none)
   696  		})
   697  	}
   698  }
   699  
   700  type fgc struct {
   701  	err  error
   702  	lock sync.Mutex
   703  
   704  	prs        map[string][]PullRequest
   705  	refs       map[string]string
   706  	merged     int
   707  	setStatus  bool
   708  	statuses   map[string]github.Status
   709  	mergeErrs  map[int]error
   710  	queryCalls int
   711  
   712  	expectedSHA          string
   713  	skipExpectedShaCheck bool
   714  	combinedStatus       map[string]string
   715  	checkRuns            *github.CheckRunList
   716  }
   717  
   718  func (f *fgc) GetRepo(o, r string) (github.FullRepo, error) {
   719  	repo := github.FullRepo{}
   720  	if strings.Contains(r, "squash") {
   721  		repo.AllowSquashMerge = true
   722  	}
   723  	if strings.Contains(r, "rebase") {
   724  		repo.AllowRebaseMerge = true
   725  	}
   726  	if !strings.Contains(r, "nomerge") {
   727  		repo.AllowMergeCommit = true
   728  	}
   729  	return repo, nil
   730  }
   731  
   732  func (f *fgc) GetRef(o, r, ref string) (string, error) {
   733  	return f.refs[o+"/"+r+" "+ref], f.err
   734  }
   735  
   736  func (f *fgc) QueryWithGitHubAppsSupport(ctx context.Context, q interface{}, vars map[string]interface{}, org string) error {
   737  	sq, ok := q.(*searchQuery)
   738  	if !ok {
   739  		return errors.New("unexpected query type")
   740  	}
   741  
   742  	f.lock.Lock()
   743  	defer f.lock.Unlock()
   744  	f.queryCalls++
   745  
   746  	for _, pr := range f.prs[org] {
   747  		sq.Search.Nodes = append(
   748  			sq.Search.Nodes,
   749  			struct {
   750  				PullRequest PullRequest `graphql:"... on PullRequest"`
   751  			}{PullRequest: pr},
   752  		)
   753  	}
   754  	return nil
   755  }
   756  
   757  func (f *fgc) Merge(org, repo string, number int, details github.MergeDetails) error {
   758  	if err, ok := f.mergeErrs[number]; ok {
   759  		return err
   760  	}
   761  	f.merged++
   762  	return nil
   763  }
   764  
   765  func (f *fgc) CreateStatus(org, repo, ref string, s github.Status) error {
   766  	f.lock.Lock()
   767  	defer f.lock.Unlock()
   768  	switch s.State {
   769  	case github.StatusSuccess, github.StatusError, github.StatusPending, github.StatusFailure:
   770  		if f.statuses == nil {
   771  			f.statuses = map[string]github.Status{}
   772  		}
   773  		f.statuses[org+"/"+repo+"/"+ref] = s
   774  		f.setStatus = true
   775  		return nil
   776  	}
   777  	return fmt.Errorf("invalid 'state' value: %q", s.State)
   778  }
   779  
   780  func (f *fgc) GetCombinedStatus(org, repo, ref string) (*github.CombinedStatus, error) {
   781  	if !f.skipExpectedShaCheck && f.expectedSHA != ref {
   782  		return nil, errors.New("bad combined status request: incorrect sha")
   783  	}
   784  	var statuses []github.Status
   785  	for c, s := range f.combinedStatus {
   786  		statuses = append(statuses, github.Status{Context: c, State: s})
   787  	}
   788  	return &github.CombinedStatus{
   789  			Statuses: statuses,
   790  		},
   791  		nil
   792  }
   793  
   794  func (f *fgc) ListCheckRuns(org, repo, ref string) (*github.CheckRunList, error) {
   795  	if !f.skipExpectedShaCheck && f.expectedSHA != ref {
   796  		return nil, errors.New("bad combined status request: incorrect sha")
   797  	}
   798  	if f.checkRuns != nil {
   799  		return f.checkRuns, nil
   800  	}
   801  	return &github.CheckRunList{}, nil
   802  }
   803  
   804  func (f *fgc) GetPullRequestChanges(org, repo string, number int) ([]github.PullRequestChange, error) {
   805  	if number != 100 {
   806  		return nil, nil
   807  	}
   808  	return []github.PullRequestChange{
   809  			{
   810  				Filename: "CHANGED",
   811  			},
   812  		},
   813  		nil
   814  }
   815  
   816  // TestDividePool ensures that subpools returned by dividePool satisfy a few
   817  // important invariants.
   818  func TestDividePool(t *testing.T) {
   819  	testPulls := []struct {
   820  		org    string
   821  		repo   string
   822  		number int
   823  		branch string
   824  	}{
   825  		{
   826  			org:    "k",
   827  			repo:   "t-i",
   828  			number: 5,
   829  			branch: defaultBranch,
   830  		},
   831  		{
   832  			org:    "k",
   833  			repo:   "t-i",
   834  			number: 6,
   835  			branch: defaultBranch,
   836  		},
   837  		{
   838  			org:    "k",
   839  			repo:   "k",
   840  			number: 123,
   841  			branch: defaultBranch,
   842  		},
   843  		{
   844  			org:    "k",
   845  			repo:   "k",
   846  			number: 1000,
   847  			branch: "release-1.6",
   848  		},
   849  	}
   850  	testPJs := []struct {
   851  		jobType prowapi.ProwJobType
   852  		org     string
   853  		repo    string
   854  		baseRef string
   855  		baseSHA string
   856  	}{
   857  		{
   858  			jobType: prowapi.PresubmitJob,
   859  			org:     "k",
   860  			repo:    "t-i",
   861  			baseRef: defaultBranch,
   862  			baseSHA: "123",
   863  		},
   864  		{
   865  			jobType: prowapi.BatchJob,
   866  			org:     "k",
   867  			repo:    "t-i",
   868  			baseRef: defaultBranch,
   869  			baseSHA: "123",
   870  		},
   871  		{
   872  			jobType: prowapi.PeriodicJob,
   873  		},
   874  		{
   875  			jobType: prowapi.PresubmitJob,
   876  			org:     "k",
   877  			repo:    "t-i",
   878  			baseRef: "patch",
   879  			baseSHA: "123",
   880  		},
   881  		{
   882  			jobType: prowapi.PresubmitJob,
   883  			org:     "k",
   884  			repo:    "t-i",
   885  			baseRef: defaultBranch,
   886  			baseSHA: "abc",
   887  		},
   888  		{
   889  			jobType: prowapi.PresubmitJob,
   890  			org:     "o",
   891  			repo:    "t-i",
   892  			baseRef: defaultBranch,
   893  			baseSHA: "123",
   894  		},
   895  		{
   896  			jobType: prowapi.PresubmitJob,
   897  			org:     "k",
   898  			repo:    "other",
   899  			baseRef: defaultBranch,
   900  			baseSHA: "123",
   901  		},
   902  	}
   903  	fc := &fgc{
   904  		refs: map[string]string{
   905  			"k/t-i heads/master":    "123",
   906  			"k/k heads/master":      "456",
   907  			"k/k heads/release-1.6": "789",
   908  		},
   909  	}
   910  
   911  	configGetter := func() *config.Config {
   912  		return &config.Config{
   913  			ProwConfig: config.ProwConfig{
   914  				ProwJobNamespace: "default",
   915  			},
   916  		}
   917  	}
   918  
   919  	mmc := newMergeChecker(configGetter, fc)
   920  	log := logrus.NewEntry(logrus.StandardLogger())
   921  	ghProvider := newGitHubProvider(log, fc, nil, configGetter, mmc, false)
   922  	mgr := newFakeManager()
   923  	c, err := newSyncController(
   924  		context.Background(),
   925  		log,
   926  		mgr,
   927  		ghProvider,
   928  		configGetter,
   929  		nil,
   930  		nil,
   931  		false,
   932  		&statusUpdate{
   933  			dontUpdateStatus: &threadSafePRSet{},
   934  			newPoolPending:   make(chan bool),
   935  		},
   936  	)
   937  	if err != nil {
   938  		t.Fatalf("failed to construct sync controller: %v", err)
   939  	}
   940  	for idx, pj := range testPJs {
   941  		prowjob := &prowapi.ProwJob{
   942  			ObjectMeta: metav1.ObjectMeta{
   943  				Name:      fmt.Sprintf("pj-%d", idx),
   944  				Namespace: "default",
   945  			},
   946  			Spec: prowapi.ProwJobSpec{
   947  				Type: pj.jobType,
   948  				Refs: &prowapi.Refs{
   949  					Org:     pj.org,
   950  					Repo:    pj.repo,
   951  					BaseRef: pj.baseRef,
   952  					BaseSHA: pj.baseSHA,
   953  				},
   954  			},
   955  		}
   956  		if err := mgr.GetClient().Create(context.Background(), prowjob); err != nil {
   957  			t.Fatalf("failed to create prowjob: %v", err)
   958  		}
   959  	}
   960  	pulls := make(map[string]CodeReviewCommon)
   961  	for _, p := range testPulls {
   962  		npr := PullRequest{Number: githubql.Int(p.number)}
   963  		npr.BaseRef.Name = githubql.String(p.branch)
   964  		npr.BaseRef.Prefix = "refs/heads/"
   965  		npr.Repository.Name = githubql.String(p.repo)
   966  		npr.Repository.Owner.Login = githubql.String(p.org)
   967  		crc := CodeReviewCommonFromPullRequest(&npr)
   968  		pulls[prKey(crc)] = *crc
   969  	}
   970  	sps, err := c.dividePool(pulls)
   971  	if err != nil {
   972  		t.Fatalf("Error dividing pool: %v", err)
   973  	}
   974  	if len(sps) == 0 {
   975  		t.Error("No subpools.")
   976  	}
   977  	for _, sp := range sps {
   978  		name := fmt.Sprintf("%s/%s %s", sp.org, sp.repo, sp.branch)
   979  		sha := fc.refs[sp.org+"/"+sp.repo+" heads/"+sp.branch]
   980  		if sp.sha != sha {
   981  			t.Errorf("For subpool %s, got sha %q, expected %q.", name, sp.sha, sha)
   982  		}
   983  		if len(sp.prs) == 0 {
   984  			t.Errorf("Subpool %s has no PRs.", name)
   985  		}
   986  		for _, pr := range sp.prs {
   987  			if pr.Org != sp.org || pr.Repo != sp.repo || pr.BaseRefName != sp.branch {
   988  				t.Errorf("PR in wrong subpool. Got PR %+v in subpool %s.", pr, name)
   989  			}
   990  		}
   991  		for _, pj := range sp.pjs {
   992  			if pj.Spec.Type != prowapi.PresubmitJob && pj.Spec.Type != prowapi.BatchJob {
   993  				t.Errorf("PJ with bad type in subpool %s: %+v", name, pj)
   994  			}
   995  			referenceRef := &prowapi.Refs{
   996  				Org:     sp.org,
   997  				Repo:    sp.repo,
   998  				BaseRef: sp.branch,
   999  				BaseSHA: sp.sha,
  1000  			}
  1001  			if diff := deep.Equal(pj.Spec.Refs, referenceRef); diff != nil {
  1002  				t.Errorf("Got PJ with wrong refs, diff: %v", diff)
  1003  			}
  1004  		}
  1005  	}
  1006  }
  1007  
  1008  func TestPickBatchV2(t *testing.T) {
  1009  	testPickBatch(localgit.NewV2, t)
  1010  }
  1011  
  1012  func testPickBatch(clients localgit.Clients, t *testing.T) {
  1013  	lg, gc, err := clients()
  1014  	if err != nil {
  1015  		t.Fatalf("Error making local git: %v", err)
  1016  	}
  1017  	defer gc.Clean()
  1018  	defer lg.Clean()
  1019  	if err := lg.MakeFakeRepo("o", "r"); err != nil {
  1020  		t.Fatalf("Error making fake repo: %v", err)
  1021  	}
  1022  	if err := lg.AddCommit("o", "r", map[string][]byte{"foo": []byte("foo")}); err != nil {
  1023  		t.Fatalf("Adding initial commit: %v", err)
  1024  	}
  1025  	testprs := []struct {
  1026  		files   map[string][]byte
  1027  		success bool
  1028  		number  int
  1029  
  1030  		included bool
  1031  	}{
  1032  		{
  1033  			files:    map[string][]byte{"bar": []byte("ok")},
  1034  			success:  true,
  1035  			number:   0,
  1036  			included: true,
  1037  		},
  1038  		{
  1039  			files:    map[string][]byte{"foo": []byte("ok")},
  1040  			success:  true,
  1041  			number:   1,
  1042  			included: true,
  1043  		},
  1044  		{
  1045  			files:    map[string][]byte{"bar": []byte("conflicts with 0")},
  1046  			success:  true,
  1047  			number:   2,
  1048  			included: false,
  1049  		},
  1050  		{
  1051  			files:    map[string][]byte{"something": []byte("ok")},
  1052  			success:  true,
  1053  			number:   3,
  1054  			included: true,
  1055  		},
  1056  		{
  1057  			files:    map[string][]byte{"changes": []byte("ok")},
  1058  			success:  true,
  1059  			number:   4,
  1060  			included: true,
  1061  		},
  1062  		{
  1063  			files:    map[string][]byte{"other": []byte("ok")},
  1064  			success:  true,
  1065  			number:   5,
  1066  			included: false, // excluded by context policy
  1067  		},
  1068  		{
  1069  			files:    map[string][]byte{"qux": []byte("ok")},
  1070  			success:  false,
  1071  			number:   6,
  1072  			included: false,
  1073  		},
  1074  		{
  1075  			files:    map[string][]byte{"bazel": []byte("ok")},
  1076  			success:  true,
  1077  			number:   7,
  1078  			included: true,
  1079  		},
  1080  		{
  1081  			files:    map[string][]byte{"bazel": []byte("ok")},
  1082  			success:  true,
  1083  			number:   8,
  1084  			included: false, // batch of 5 smallest excludes this
  1085  		},
  1086  	}
  1087  	sp := subpool{
  1088  		log:    logrus.WithField("component", "tide"),
  1089  		org:    "o",
  1090  		repo:   "r",
  1091  		branch: defaultBranch,
  1092  		sha:    defaultBranch,
  1093  	}
  1094  	for _, testpr := range testprs {
  1095  		if err := lg.CheckoutNewBranch("o", "r", fmt.Sprintf("pr-%d", testpr.number)); err != nil {
  1096  			t.Fatalf("Error checking out new branch: %v", err)
  1097  		}
  1098  		if err := lg.AddCommit("o", "r", testpr.files); err != nil {
  1099  			t.Fatalf("Error adding commit: %v", err)
  1100  		}
  1101  		if err := lg.Checkout("o", "r", defaultBranch); err != nil {
  1102  			t.Fatalf("Error checking out master: %v", err)
  1103  		}
  1104  		oid := githubql.String(fmt.Sprintf("origin/pr-%d", testpr.number))
  1105  		var pr PullRequest
  1106  		pr.Number = githubql.Int(testpr.number)
  1107  		pr.HeadRefOID = oid
  1108  		pr.Commits.Nodes = []struct {
  1109  			Commit Commit
  1110  		}{{Commit: Commit{OID: oid}}}
  1111  		pr.Commits.Nodes[0].Commit.Status.Contexts = append(pr.Commits.Nodes[0].Commit.Status.Contexts, Context{State: githubql.StatusStateSuccess})
  1112  		if !testpr.success {
  1113  			pr.Commits.Nodes[0].Commit.Status.Contexts[0].State = githubql.StatusStateFailure
  1114  		}
  1115  		sp.prs = append(sp.prs, *CodeReviewCommonFromPullRequest(&pr))
  1116  	}
  1117  	ca := &config.Agent{}
  1118  	ca.Set(&config.Config{
  1119  		ProwConfig: config.ProwConfig{
  1120  			Tide: config.Tide{
  1121  				BatchSizeLimitMap: map[string]int{"*": 5},
  1122  			},
  1123  		},
  1124  		JobConfig: config.JobConfig{
  1125  			PresubmitsStatic: map[string][]config.Presubmit{
  1126  				"o/r": {{
  1127  					AlwaysRun: true,
  1128  					JobBase: config.JobBase{
  1129  						Name: "my-presubmit",
  1130  					},
  1131  				}},
  1132  			},
  1133  		},
  1134  	})
  1135  	logger := logrus.WithField("component", "tide")
  1136  	ghProvider := &GitHubProvider{cfg: ca.Config, gc: gc, mergeChecker: newMergeChecker(ca.Config, &fgc{}), logger: logger}
  1137  	c := &syncController{
  1138  		logger:       logger,
  1139  		provider:     ghProvider,
  1140  		config:       ca.Config,
  1141  		pickNewBatch: pickNewBatch(gc, ca.Config, ghProvider),
  1142  	}
  1143  	prs, presubmits, err := c.pickBatch(sp, map[int]contextChecker{
  1144  		0: &config.TideContextPolicy{},
  1145  		1: &config.TideContextPolicy{},
  1146  		2: &config.TideContextPolicy{},
  1147  		3: &config.TideContextPolicy{},
  1148  		4: &config.TideContextPolicy{},
  1149  		// Test if scoping of ContextPolicy works correctly
  1150  		5: &config.TideContextPolicy{RequiredContexts: []string{"context-from-context-checker"}},
  1151  		6: &config.TideContextPolicy{},
  1152  		7: &config.TideContextPolicy{},
  1153  		8: &config.TideContextPolicy{},
  1154  	}, c.pickNewBatch)
  1155  	if err != nil {
  1156  		t.Fatalf("Error from pickBatch: %v", err)
  1157  	}
  1158  	if !apiequality.Semantic.DeepEqual(presubmits, ca.Config().PresubmitsStatic["o/r"]) {
  1159  		t.Errorf("resolving presubmits failed, diff:\n%v\n", diff.ObjectReflectDiff(presubmits, ca.Config().PresubmitsStatic["o/r"]))
  1160  	}
  1161  	for _, testpr := range testprs {
  1162  		var found bool
  1163  		for _, pr := range prs {
  1164  			if int(pr.Number) == testpr.number {
  1165  				found = true
  1166  				break
  1167  			}
  1168  		}
  1169  		if found && !testpr.included {
  1170  			t.Errorf("PR %d should not be picked.", testpr.number)
  1171  		} else if !found && testpr.included {
  1172  			t.Errorf("PR %d should be picked.", testpr.number)
  1173  		}
  1174  	}
  1175  }
  1176  
  1177  func TestMergeMethodCheckerAndPRMergeMethod(t *testing.T) {
  1178  	squashLabel := "tide/squash"
  1179  	mergeLabel := "tide/merge"
  1180  	rebaseLabel := "tide/rebase"
  1181  
  1182  	tideConfig := config.Tide{
  1183  		TideGitHubConfig: config.TideGitHubConfig{
  1184  			SquashLabel: squashLabel,
  1185  			MergeLabel:  mergeLabel,
  1186  			RebaseLabel: rebaseLabel,
  1187  
  1188  			MergeType: map[string]config.TideOrgMergeType{
  1189  				"o/configured-rebase":              {MergeType: types.MergeRebase}, // GH client allows merge, rebase
  1190  				"o/configured-squash-allow-rebase": {MergeType: types.MergeSquash}, // GH client allows merge, squash, rebase
  1191  				"o/configure-re-base":              {MergeType: types.MergeRebase}, // GH client allows merge
  1192  			},
  1193  		},
  1194  	}
  1195  	cfg := func() *config.Config { return &config.Config{ProwConfig: config.ProwConfig{Tide: tideConfig}} }
  1196  	mmc := newMergeChecker(cfg, &fgc{})
  1197  
  1198  	testcases := []struct {
  1199  		name              string
  1200  		repo              string
  1201  		labels            []string
  1202  		conflict          bool
  1203  		expectedMethod    types.PullRequestMergeType
  1204  		expectErr         bool
  1205  		expectConflictErr bool
  1206  	}{
  1207  		{
  1208  			name:           "default method without PR label override",
  1209  			repo:           "foo",
  1210  			expectedMethod: types.MergeMerge,
  1211  		},
  1212  		{
  1213  			name:           "irrelevant PR labels ignored",
  1214  			repo:           "foo",
  1215  			labels:         []string{"unrelated"},
  1216  			expectedMethod: types.MergeMerge,
  1217  		},
  1218  		{
  1219  			name:           "default method overridden by a PR label",
  1220  			repo:           "allow-squash-nomerge",
  1221  			labels:         []string{"tide/squash"},
  1222  			expectedMethod: types.MergeSquash,
  1223  		},
  1224  		{
  1225  			name:           "use method configured for repo in tide config",
  1226  			repo:           "configured-squash-allow-rebase",
  1227  			labels:         []string{"unrelated"},
  1228  			expectedMethod: types.MergeSquash,
  1229  		},
  1230  		{
  1231  			name:           "tide config method overridden by a PR label",
  1232  			repo:           "configured-squash-allow-rebase",
  1233  			labels:         []string{"unrelated", "tide/rebase"},
  1234  			expectedMethod: types.MergeRebase,
  1235  		},
  1236  		{
  1237  			name:      "multiple merge method PR labels should not merge",
  1238  			repo:      "foo",
  1239  			labels:    []string{"tide/squash", "tide/rebase"},
  1240  			expectErr: true,
  1241  		},
  1242  		{
  1243  			name:              "merge conflict",
  1244  			repo:              "foo",
  1245  			labels:            []string{"unrelated"},
  1246  			conflict:          true,
  1247  			expectedMethod:    types.MergeMerge,
  1248  			expectErr:         false,
  1249  			expectConflictErr: true,
  1250  		},
  1251  		{
  1252  			name:              "squash label conflicts with merge only GH settings",
  1253  			repo:              "foo",
  1254  			labels:            []string{"tide/squash"},
  1255  			expectedMethod:    types.MergeSquash,
  1256  			expectErr:         false,
  1257  			expectConflictErr: true,
  1258  		},
  1259  		{
  1260  			name:              "rebase method tide config conflicts with merge only GH settings",
  1261  			repo:              "configure-re-base",
  1262  			labels:            []string{"unrelated"},
  1263  			expectedMethod:    types.MergeRebase,
  1264  			expectErr:         false,
  1265  			expectConflictErr: true,
  1266  		},
  1267  		{
  1268  			name:              "default method conflicts with squash only GH settings",
  1269  			repo:              "squash-nomerge",
  1270  			labels:            []string{"unrelated"},
  1271  			expectedMethod:    types.MergeMerge,
  1272  			expectErr:         false,
  1273  			expectConflictErr: true,
  1274  		},
  1275  	}
  1276  
  1277  	for _, tc := range testcases {
  1278  		t.Run(tc.name, func(t *testing.T) {
  1279  			pr := &PullRequest{
  1280  				Repository: struct {
  1281  					Name          githubql.String
  1282  					NameWithOwner githubql.String
  1283  					Owner         struct {
  1284  						Login githubql.String
  1285  					}
  1286  				}{
  1287  					Name: githubql.String(tc.repo),
  1288  					Owner: struct {
  1289  						Login githubql.String
  1290  					}{
  1291  						Login: githubql.String("o"),
  1292  					},
  1293  				},
  1294  				Labels: struct {
  1295  					Nodes []struct{ Name githubql.String }
  1296  				}{
  1297  					Nodes: []struct{ Name githubql.String }{},
  1298  				},
  1299  				CanBeRebased: true,
  1300  			}
  1301  			for _, label := range tc.labels {
  1302  				labelNode := struct{ Name githubql.String }{Name: githubql.String(label)}
  1303  				pr.Labels.Nodes = append(pr.Labels.Nodes, labelNode)
  1304  			}
  1305  			if tc.conflict {
  1306  				pr.Mergeable = githubql.MergeableStateConflicting
  1307  			}
  1308  
  1309  			actual := mmc.prMergeMethod(tideConfig, CodeReviewCommonFromPullRequest(pr))
  1310  			if actual == nil {
  1311  				if !tc.expectErr {
  1312  					t.Errorf("multiple merge methods are not allowed")
  1313  				}
  1314  				return
  1315  			} else if tc.expectErr {
  1316  				t.Errorf("missing expected error")
  1317  				return
  1318  			}
  1319  			if tc.expectedMethod != *actual {
  1320  				t.Errorf("wanted: %q, got: %q", tc.expectedMethod, *actual)
  1321  			}
  1322  			reason, err := mmc.isAllowedToMerge(CodeReviewCommonFromPullRequest(pr))
  1323  			if err != nil {
  1324  				t.Errorf("unexpected processing error: %v", err)
  1325  			} else if reason != "" {
  1326  				if !tc.expectConflictErr {
  1327  					t.Errorf("unexpected merge method conflict error: %v", err)
  1328  				}
  1329  				return
  1330  			} else if tc.expectConflictErr {
  1331  				t.Errorf("missing expected merge method conflict error")
  1332  				return
  1333  			}
  1334  		})
  1335  	}
  1336  }
  1337  
  1338  func TestRebaseMergeMethodIsAllowed(t *testing.T) {
  1339  	orgName := "fake-org"
  1340  	repoName := "fake-repo"
  1341  	tideConfig := config.Tide{
  1342  		TideGitHubConfig: config.TideGitHubConfig{
  1343  			MergeType: map[string]config.TideOrgMergeType{
  1344  				fmt.Sprintf("%s/%s", orgName, repoName): {MergeType: types.MergeRebase},
  1345  			},
  1346  		},
  1347  	}
  1348  	cfg := func() *config.Config { return &config.Config{ProwConfig: config.ProwConfig{Tide: tideConfig}} }
  1349  	mmc := newMergeChecker(cfg, &fgc{})
  1350  	mmc.cache = map[config.OrgRepo]map[types.PullRequestMergeType]bool{
  1351  		{Org: orgName, Repo: repoName}: {
  1352  			types.MergeRebase: true,
  1353  		},
  1354  	}
  1355  
  1356  	testCases := []struct {
  1357  		name                string
  1358  		expectedMergeOutput string
  1359  		prCanBeRebased      bool
  1360  	}{
  1361  		{
  1362  			name:                "Merging PR using rebase successfully",
  1363  			expectedMergeOutput: "",
  1364  			prCanBeRebased:      true,
  1365  		},
  1366  		{
  1367  			name:                "Merging PR using rebase but it is not allowed",
  1368  			expectedMergeOutput: "PR can't be rebased",
  1369  			prCanBeRebased:      false,
  1370  		},
  1371  	}
  1372  
  1373  	for _, tc := range testCases {
  1374  		t.Run(tc.name, func(t *testing.T) {
  1375  			pr := &PullRequest{
  1376  				Repository: struct {
  1377  					Name          githubql.String
  1378  					NameWithOwner githubql.String
  1379  					Owner         struct {
  1380  						Login githubql.String
  1381  					}
  1382  				}{
  1383  					Name: githubql.String(repoName),
  1384  					Owner: struct {
  1385  						Login githubql.String
  1386  					}{
  1387  						Login: githubql.String(orgName),
  1388  					},
  1389  				},
  1390  				Labels: struct {
  1391  					Nodes []struct{ Name githubql.String }
  1392  				}{
  1393  					Nodes: []struct{ Name githubql.String }{},
  1394  				},
  1395  				CanBeRebased: githubql.Boolean(tc.prCanBeRebased),
  1396  			}
  1397  
  1398  			mergeOutput, err := mmc.isAllowedToMerge(CodeReviewCommonFromPullRequest(pr))
  1399  			if err != nil {
  1400  				t.Errorf("unexpected error: %v", err)
  1401  			} else {
  1402  				if mergeOutput != tc.expectedMergeOutput {
  1403  					t.Errorf("Expected merge output \"%s\" but got \"%s\"\n", tc.expectedMergeOutput, mergeOutput)
  1404  				}
  1405  			}
  1406  		})
  1407  	}
  1408  }
  1409  
  1410  func TestTakeActionV2(t *testing.T) {
  1411  	testTakeAction(localgit.NewV2, t)
  1412  }
  1413  
  1414  func testTakeAction(clients localgit.Clients, t *testing.T) {
  1415  	sleep = func(time.Duration) {}
  1416  	defer func() { sleep = time.Sleep }()
  1417  
  1418  	// PRs 0-9 exist. All are mergable, and all are passing tests.
  1419  	testcases := []struct {
  1420  		name string
  1421  
  1422  		batchPending     bool
  1423  		successes        []int
  1424  		pendings         []int
  1425  		nones            []int
  1426  		batchMerges      []int
  1427  		presubmits       map[int][]config.Presubmit
  1428  		preExistingJobs  []runtime.Object
  1429  		mergeErrs        map[int]error
  1430  		enableScheduling bool
  1431  
  1432  		merged           int
  1433  		triggered        int
  1434  		triggeredBatches int
  1435  		action           Action
  1436  	}{
  1437  		{
  1438  			name: "no prs to test, should do nothing",
  1439  
  1440  			batchPending: true,
  1441  			successes:    []int{},
  1442  			pendings:     []int{},
  1443  			nones:        []int{},
  1444  			batchMerges:  []int{},
  1445  			presubmits: map[int][]config.Presubmit{
  1446  				100: {
  1447  					{Reporter: config.Reporter{Context: "foo"}},
  1448  					{Reporter: config.Reporter{Context: "if-changed"}},
  1449  				},
  1450  			},
  1451  			merged:    0,
  1452  			triggered: 0,
  1453  			action:    Wait,
  1454  		},
  1455  		{
  1456  			name: "pending batch, pending serial, nothing to do",
  1457  
  1458  			batchPending: true,
  1459  			successes:    []int{},
  1460  			pendings:     []int{1},
  1461  			nones:        []int{0, 2},
  1462  			batchMerges:  []int{},
  1463  			presubmits: map[int][]config.Presubmit{
  1464  				100: {
  1465  					{Reporter: config.Reporter{Context: "foo"}},
  1466  					{Reporter: config.Reporter{Context: "if-changed"}},
  1467  				},
  1468  			},
  1469  			merged:    0,
  1470  			triggered: 0,
  1471  			action:    Wait,
  1472  		},
  1473  		{
  1474  			name: "pending batch, successful serial, nothing to do",
  1475  
  1476  			batchPending: true,
  1477  			successes:    []int{1},
  1478  			pendings:     []int{},
  1479  			nones:        []int{0, 2},
  1480  			batchMerges:  []int{},
  1481  			presubmits: map[int][]config.Presubmit{
  1482  				100: {
  1483  					{Reporter: config.Reporter{Context: "foo"}},
  1484  					{Reporter: config.Reporter{Context: "if-changed"}},
  1485  				},
  1486  			},
  1487  			merged:    0,
  1488  			triggered: 0,
  1489  			action:    Wait,
  1490  		},
  1491  		{
  1492  			name: "pending batch, should trigger serial",
  1493  
  1494  			batchPending: true,
  1495  			successes:    []int{},
  1496  			pendings:     []int{},
  1497  			nones:        []int{0, 1, 2},
  1498  			batchMerges:  []int{},
  1499  			presubmits: map[int][]config.Presubmit{
  1500  				100: {
  1501  					{Reporter: config.Reporter{Context: "foo"}},
  1502  					{Reporter: config.Reporter{Context: "if-changed"}},
  1503  				},
  1504  			},
  1505  			merged:    0,
  1506  			triggered: 1,
  1507  			action:    Trigger,
  1508  		},
  1509  		{
  1510  			name: "no pending batch, should trigger batch",
  1511  
  1512  			batchPending: false,
  1513  			successes:    []int{},
  1514  			pendings:     []int{0},
  1515  			nones:        []int{1, 2, 3},
  1516  			batchMerges:  []int{},
  1517  			presubmits: map[int][]config.Presubmit{
  1518  				100: {
  1519  					{Reporter: config.Reporter{Context: "foo"}},
  1520  					{Reporter: config.Reporter{Context: "if-changed"}},
  1521  				},
  1522  			},
  1523  			merged:           0,
  1524  			triggered:        2,
  1525  			triggeredBatches: 2,
  1526  			action:           TriggerBatch,
  1527  		},
  1528  		{
  1529  			name: "one PR, should not trigger batch",
  1530  
  1531  			batchPending: false,
  1532  			successes:    []int{},
  1533  			pendings:     []int{},
  1534  			nones:        []int{0},
  1535  			batchMerges:  []int{},
  1536  			presubmits: map[int][]config.Presubmit{
  1537  				100: {
  1538  					{Reporter: config.Reporter{Context: "foo"}},
  1539  					{Reporter: config.Reporter{Context: "if-changed"}},
  1540  				},
  1541  			},
  1542  			merged:    0,
  1543  			triggered: 1,
  1544  			action:    Trigger,
  1545  		},
  1546  		{
  1547  			name: "successful PR, should merge",
  1548  
  1549  			batchPending: false,
  1550  			successes:    []int{0},
  1551  			pendings:     []int{},
  1552  			nones:        []int{1, 2, 3},
  1553  			batchMerges:  []int{},
  1554  			presubmits: map[int][]config.Presubmit{
  1555  				100: {
  1556  					{Reporter: config.Reporter{Context: "foo"}},
  1557  					{Reporter: config.Reporter{Context: "if-changed"}},
  1558  				},
  1559  			},
  1560  			merged:    1,
  1561  			triggered: 0,
  1562  			action:    Merge,
  1563  		},
  1564  		{
  1565  			name: "successful batch, should merge",
  1566  
  1567  			batchPending: false,
  1568  			successes:    []int{0, 1},
  1569  			pendings:     []int{2, 3},
  1570  			nones:        []int{4, 5},
  1571  			batchMerges:  []int{6, 7, 8},
  1572  			presubmits: map[int][]config.Presubmit{
  1573  				100: {
  1574  					{Reporter: config.Reporter{Context: "foo"}},
  1575  					{Reporter: config.Reporter{Context: "if-changed"}},
  1576  				},
  1577  			},
  1578  			merged:    3,
  1579  			triggered: 0,
  1580  			action:    MergeBatch,
  1581  		},
  1582  		{
  1583  			name: "one PR that triggers RunIfChangedJob",
  1584  
  1585  			batchPending: false,
  1586  			successes:    []int{},
  1587  			pendings:     []int{},
  1588  			nones:        []int{100},
  1589  			batchMerges:  []int{},
  1590  			presubmits: map[int][]config.Presubmit{
  1591  				100: {
  1592  					{Reporter: config.Reporter{Context: "foo"}},
  1593  					{Reporter: config.Reporter{Context: "if-changed"}},
  1594  				},
  1595  			},
  1596  			merged:    0,
  1597  			triggered: 2,
  1598  			action:    Trigger,
  1599  		},
  1600  		{
  1601  			name: "no presubmits, merge",
  1602  
  1603  			batchPending: false,
  1604  			successes:    []int{5, 4},
  1605  			pendings:     []int{},
  1606  			nones:        []int{},
  1607  			batchMerges:  []int{},
  1608  
  1609  			merged:    1,
  1610  			triggered: 0,
  1611  			action:    Merge,
  1612  		},
  1613  		{
  1614  			name: "no presubmits, wait",
  1615  
  1616  			batchPending: false,
  1617  			successes:    []int{},
  1618  			pendings:     []int{},
  1619  			nones:        []int{},
  1620  			batchMerges:  []int{},
  1621  
  1622  			merged:    0,
  1623  			triggered: 0,
  1624  			action:    Wait,
  1625  		},
  1626  		{
  1627  			name: "no pending serial or batch, should trigger batch",
  1628  
  1629  			batchPending: false,
  1630  			successes:    []int{},
  1631  			pendings:     []int{},
  1632  			nones:        []int{1, 2, 3},
  1633  			batchMerges:  []int{},
  1634  			presubmits: map[int][]config.Presubmit{
  1635  				100: {
  1636  					{Reporter: config.Reporter{Context: "foo"}},
  1637  					{Reporter: config.Reporter{Context: "if-changed"}},
  1638  				},
  1639  			},
  1640  			merged:           0,
  1641  			triggered:        2,
  1642  			triggeredBatches: 2,
  1643  			action:           TriggerBatch,
  1644  		},
  1645  		{
  1646  			name: "no pending serial or batch, should trigger batch and omit pre-existing running job",
  1647  
  1648  			batchPending: false,
  1649  			successes:    []int{},
  1650  			pendings:     []int{},
  1651  			nones:        []int{1, 2, 3},
  1652  			batchMerges:  []int{},
  1653  			presubmits: map[int][]config.Presubmit{
  1654  				100: {
  1655  					{Reporter: config.Reporter{Context: "foo"}},
  1656  					{Reporter: config.Reporter{Context: "if-changed"}},
  1657  				},
  1658  			},
  1659  			preExistingJobs: []runtime.Object{&prowapi.ProwJob{
  1660  				ObjectMeta: metav1.ObjectMeta{Name: "my-job", Namespace: "pj-ns"},
  1661  				Spec: prowapi.ProwJobSpec{
  1662  					Job:  "bar",
  1663  					Type: prowapi.BatchJob,
  1664  					Refs: &prowapi.Refs{
  1665  						Org:     "o",
  1666  						Repo:    "r",
  1667  						BaseRef: defaultBranch,
  1668  						BaseSHA: defaultBranch,
  1669  						Pulls: []prowapi.Pull{
  1670  							{Number: 1, SHA: "origin/pr-1"},
  1671  							{Number: 3, SHA: "origin/pr-3"},
  1672  							{Number: 2, SHA: "origin/pr-2"},
  1673  						},
  1674  					},
  1675  				},
  1676  			}},
  1677  			merged:           0,
  1678  			triggered:        1,
  1679  			triggeredBatches: 1,
  1680  			action:           TriggerBatch,
  1681  		},
  1682  		{
  1683  			name: "no pending serial or batch, should trigger batch and omit pre-existing success job",
  1684  
  1685  			batchPending: false,
  1686  			successes:    []int{},
  1687  			pendings:     []int{},
  1688  			nones:        []int{1, 2, 3},
  1689  			batchMerges:  []int{},
  1690  			presubmits: map[int][]config.Presubmit{
  1691  				100: {
  1692  					{Reporter: config.Reporter{Context: "foo"}},
  1693  					{Reporter: config.Reporter{Context: "if-changed"}},
  1694  				},
  1695  			},
  1696  			preExistingJobs: []runtime.Object{&prowapi.ProwJob{
  1697  				ObjectMeta: metav1.ObjectMeta{Name: "my-job", Namespace: "pj-ns"},
  1698  				Spec: prowapi.ProwJobSpec{
  1699  					Job:  "bar",
  1700  					Type: prowapi.BatchJob,
  1701  					Refs: &prowapi.Refs{
  1702  						Org:     "o",
  1703  						Repo:    "r",
  1704  						BaseRef: defaultBranch,
  1705  						BaseSHA: defaultBranch,
  1706  						Pulls: []prowapi.Pull{
  1707  							{Number: 1, SHA: "origin/pr-1"},
  1708  							{Number: 3, SHA: "origin/pr-3"},
  1709  							{Number: 2, SHA: "origin/pr-2"},
  1710  						},
  1711  					},
  1712  				},
  1713  				Status: prowapi.ProwJobStatus{
  1714  					State:          prowapi.SuccessState,
  1715  					CompletionTime: &metav1.Time{Time: time.Unix(10, 0)},
  1716  				},
  1717  			}},
  1718  			merged:           0,
  1719  			triggered:        1,
  1720  			triggeredBatches: 1,
  1721  			action:           TriggerBatch,
  1722  		},
  1723  		{
  1724  			name: "no pending serial or batch, should trigger batch and ignore pre-existing failure job",
  1725  
  1726  			batchPending: false,
  1727  			successes:    []int{},
  1728  			pendings:     []int{},
  1729  			nones:        []int{1, 2, 3},
  1730  			batchMerges:  []int{},
  1731  			presubmits: map[int][]config.Presubmit{
  1732  				100: {
  1733  					{Reporter: config.Reporter{Context: "foo"}},
  1734  					{Reporter: config.Reporter{Context: "if-changed"}},
  1735  				},
  1736  			},
  1737  			preExistingJobs: []runtime.Object{&prowapi.ProwJob{
  1738  				ObjectMeta: metav1.ObjectMeta{Name: "my-job", Namespace: "pj-ns"},
  1739  				Spec: prowapi.ProwJobSpec{
  1740  					Job:  "bar",
  1741  					Type: prowapi.BatchJob,
  1742  					Refs: &prowapi.Refs{
  1743  						Org:     "o",
  1744  						Repo:    "r",
  1745  						BaseRef: defaultBranch,
  1746  						BaseSHA: defaultBranch,
  1747  						Pulls: []prowapi.Pull{
  1748  							{Number: 1, SHA: "origin/pr-1"},
  1749  							{Number: 3, SHA: "origin/pr-3"},
  1750  							{Number: 2, SHA: "origin/pr-2"},
  1751  						},
  1752  					},
  1753  				},
  1754  				Status: prowapi.ProwJobStatus{
  1755  					State:          prowapi.FailureState,
  1756  					CompletionTime: &metav1.Time{Time: time.Unix(10, 0)},
  1757  				},
  1758  			}},
  1759  			merged:           0,
  1760  			triggered:        2,
  1761  			triggeredBatches: 2,
  1762  			action:           TriggerBatch,
  1763  		},
  1764  		{
  1765  			name: "pending batch, no serial, should trigger serial",
  1766  
  1767  			batchPending: true,
  1768  			successes:    []int{},
  1769  			pendings:     []int{},
  1770  			nones:        []int{1, 2, 3},
  1771  			batchMerges:  []int{},
  1772  			presubmits: map[int][]config.Presubmit{
  1773  				100: {
  1774  					{Reporter: config.Reporter{Context: "foo"}},
  1775  					{Reporter: config.Reporter{Context: "if-changed"}},
  1776  				},
  1777  			},
  1778  			merged:    0,
  1779  			triggered: 1,
  1780  			action:    Trigger,
  1781  		},
  1782  		{
  1783  			name: "batch merge errors but continues if a PR is unmergeable",
  1784  
  1785  			batchMerges: []int{1, 2, 3},
  1786  			mergeErrs:   map[int]error{2: github.UnmergablePRError("test error")},
  1787  			merged:      2,
  1788  			triggered:   0,
  1789  			action:      MergeBatch,
  1790  		},
  1791  		{
  1792  			name: "batch merge errors but continues if a PR has changed",
  1793  
  1794  			batchMerges: []int{1, 2, 3},
  1795  			mergeErrs:   map[int]error{2: github.ModifiedHeadError("test error")},
  1796  			merged:      2,
  1797  			triggered:   0,
  1798  			action:      MergeBatch,
  1799  		},
  1800  		{
  1801  			name: "batch merge errors but continues on unknown error",
  1802  
  1803  			batchMerges: []int{1, 2, 3},
  1804  			mergeErrs:   map[int]error{2: errors.New("test error")},
  1805  			merged:      2,
  1806  			triggered:   0,
  1807  			action:      MergeBatch,
  1808  		},
  1809  		{
  1810  			name: "batch merge stops on auth error",
  1811  
  1812  			batchMerges: []int{1, 2, 3},
  1813  			mergeErrs:   map[int]error{2: github.UnauthorizedToPushError("test error")},
  1814  			merged:      1,
  1815  			triggered:   0,
  1816  			action:      MergeBatch,
  1817  		},
  1818  		{
  1819  			name: "batch merge stops on invalid merge method error",
  1820  
  1821  			batchMerges: []int{1, 2, 3},
  1822  			mergeErrs:   map[int]error{2: github.MergeCommitsForbiddenError("test error")},
  1823  			merged:      1,
  1824  			triggered:   0,
  1825  			action:      MergeBatch,
  1826  		},
  1827  		{
  1828  			name: "pending batch, should trigger serial in scheduling state",
  1829  
  1830  			batchPending: true,
  1831  			successes:    []int{},
  1832  			pendings:     []int{},
  1833  			nones:        []int{0, 1, 2},
  1834  			batchMerges:  []int{},
  1835  			presubmits: map[int][]config.Presubmit{
  1836  				100: {
  1837  					{Reporter: config.Reporter{Context: "foo"}},
  1838  					{Reporter: config.Reporter{Context: "if-changed"}},
  1839  				},
  1840  			},
  1841  			merged:           0,
  1842  			triggered:        1,
  1843  			action:           Trigger,
  1844  			enableScheduling: true,
  1845  		},
  1846  	}
  1847  
  1848  	for _, tc := range testcases {
  1849  		t.Run(tc.name, func(t *testing.T) {
  1850  			ca := &config.Agent{}
  1851  			pjNamespace := "pj-ns"
  1852  			cfg := &config.Config{
  1853  				ProwConfig: config.ProwConfig{
  1854  					ProwJobNamespace: pjNamespace,
  1855  					Scheduler:        config.Scheduler{Enabled: tc.enableScheduling},
  1856  				},
  1857  			}
  1858  			if err := cfg.SetPresubmits(
  1859  				map[string][]config.Presubmit{
  1860  					"o/r": {
  1861  						{
  1862  							Reporter:     config.Reporter{Context: "foo"},
  1863  							Trigger:      "/test all",
  1864  							RerunCommand: "/test all",
  1865  							AlwaysRun:    true,
  1866  						},
  1867  						{
  1868  							JobBase: config.JobBase{
  1869  								Name: "bar",
  1870  							},
  1871  							Reporter:     config.Reporter{Context: "bar"},
  1872  							Trigger:      "/test bar",
  1873  							RerunCommand: "/test bar",
  1874  							AlwaysRun:    true,
  1875  						},
  1876  						{
  1877  							Reporter:     config.Reporter{Context: "if-changed"},
  1878  							Trigger:      "/test if-changed",
  1879  							RerunCommand: "/test if-changed",
  1880  							RegexpChangeMatcher: config.RegexpChangeMatcher{
  1881  								RunIfChanged: "CHANGED",
  1882  							},
  1883  						},
  1884  						{
  1885  							Reporter:     config.Reporter{Context: "if-changed"},
  1886  							Trigger:      "/test if-changed",
  1887  							RerunCommand: "/test if-changed",
  1888  							RegexpChangeMatcher: config.RegexpChangeMatcher{
  1889  								SkipIfOnlyChanged: "CHANGED1",
  1890  							},
  1891  						},
  1892  					},
  1893  				},
  1894  			); err != nil {
  1895  				t.Fatalf("failed to set presubmits: %v", err)
  1896  			}
  1897  			ca.Set(cfg)
  1898  			if len(tc.presubmits) > 0 {
  1899  				for i := 0; i <= 8; i++ {
  1900  					tc.presubmits[i] = []config.Presubmit{{Reporter: config.Reporter{Context: "foo"}}}
  1901  				}
  1902  			}
  1903  			lg, gc, err := clients()
  1904  			if err != nil {
  1905  				t.Fatalf("Error making local git: %v", err)
  1906  			}
  1907  			defer gc.Clean()
  1908  			defer lg.Clean()
  1909  			if err := lg.MakeFakeRepo("o", "r"); err != nil {
  1910  				t.Fatalf("Error making fake repo: %v", err)
  1911  			}
  1912  			if err := lg.AddCommit("o", "r", map[string][]byte{"foo": []byte("foo")}); err != nil {
  1913  				t.Fatalf("Adding initial commit: %v", err)
  1914  			}
  1915  
  1916  			sp := subpool{
  1917  				log:        logrus.WithField("component", "tide"),
  1918  				presubmits: tc.presubmits,
  1919  				cc: map[int]contextChecker{
  1920  					0:   &config.TideContextPolicy{},
  1921  					1:   &config.TideContextPolicy{},
  1922  					2:   &config.TideContextPolicy{},
  1923  					3:   &config.TideContextPolicy{},
  1924  					4:   &config.TideContextPolicy{},
  1925  					5:   &config.TideContextPolicy{},
  1926  					6:   &config.TideContextPolicy{},
  1927  					7:   &config.TideContextPolicy{},
  1928  					8:   &config.TideContextPolicy{},
  1929  					100: &config.TideContextPolicy{},
  1930  				},
  1931  				org:    "o",
  1932  				repo:   "r",
  1933  				branch: defaultBranch,
  1934  				sha:    defaultBranch,
  1935  			}
  1936  			genPulls := func(nums []int) []CodeReviewCommon {
  1937  				var prs []CodeReviewCommon
  1938  				for _, i := range nums {
  1939  					if err := lg.CheckoutNewBranch("o", "r", fmt.Sprintf("pr-%d", i)); err != nil {
  1940  						t.Fatalf("Error checking out new branch: %v", err)
  1941  					}
  1942  					if err := lg.AddCommit("o", "r", map[string][]byte{fmt.Sprintf("%d", i): []byte("WOW")}); err != nil {
  1943  						t.Fatalf("Error adding commit: %v", err)
  1944  					}
  1945  					if err := lg.Checkout("o", "r", defaultBranch); err != nil {
  1946  						t.Fatalf("Error checking out master: %v", err)
  1947  					}
  1948  					oid := githubql.String(fmt.Sprintf("origin/pr-%d", i))
  1949  					var pr PullRequest
  1950  					pr.Number = githubql.Int(i)
  1951  					pr.HeadRefOID = oid
  1952  					pr.Commits.Nodes = []struct {
  1953  						Commit Commit
  1954  					}{{Commit: Commit{OID: oid}}}
  1955  					sp.prs = append(sp.prs, *CodeReviewCommonFromPullRequest(&pr))
  1956  					prs = append(prs, *CodeReviewCommonFromPullRequest(&pr))
  1957  				}
  1958  				return prs
  1959  			}
  1960  			fgc := fgc{mergeErrs: tc.mergeErrs}
  1961  			log := logrus.WithField("controller", "tide")
  1962  			ghProvider := newGitHubProvider(log, &fgc, gc, ca.Config, nil, false)
  1963  			c, err := newSyncController(
  1964  				context.Background(),
  1965  				log,
  1966  				newFakeManager(tc.preExistingJobs...),
  1967  				ghProvider,
  1968  				ca.Config,
  1969  				gc,
  1970  				nil,
  1971  				false,
  1972  				&statusUpdate{
  1973  					dontUpdateStatus: &threadSafePRSet{},
  1974  					newPoolPending:   make(chan bool),
  1975  				},
  1976  			)
  1977  			if err != nil {
  1978  				t.Fatalf("failed to construct sync controller: %v", err)
  1979  			}
  1980  			c.changedFiles = &changedFilesAgent{
  1981  				provider:        ghProvider,
  1982  				nextChangeCache: make(map[changeCacheKey][]string),
  1983  			}
  1984  			var batchPending []CodeReviewCommon
  1985  			if tc.batchPending {
  1986  				batchPending = []CodeReviewCommon{{}}
  1987  			}
  1988  			if act, _, _ := c.takeAction(sp, batchPending, genPulls(tc.successes), genPulls(tc.pendings), genPulls(tc.nones), genPulls(tc.batchMerges), sp.presubmits); act != tc.action {
  1989  				t.Errorf("Wrong action. Got %v, wanted %v.", act, tc.action)
  1990  			}
  1991  
  1992  			prowJobs := &prowapi.ProwJobList{}
  1993  			if err := c.prowJobClient.List(context.Background(), prowJobs); err != nil {
  1994  				t.Fatalf("failed to list ProwJobs: %v", err)
  1995  			}
  1996  			var filteredProwJobs []prowapi.ProwJob
  1997  			// Filter out the ones we passed in
  1998  			for _, job := range prowJobs.Items {
  1999  				var preExists bool
  2000  				for _, preExistingJob := range tc.preExistingJobs {
  2001  					if reflect.DeepEqual(*preExistingJob.(*prowapi.ProwJob), job) {
  2002  						preExists = true
  2003  					}
  2004  				}
  2005  				if !preExists {
  2006  					filteredProwJobs = append(filteredProwJobs, job)
  2007  				}
  2008  
  2009  			}
  2010  			numCreated := len(filteredProwJobs)
  2011  
  2012  			var batchJobs []*prowapi.ProwJob
  2013  			for _, pj := range filteredProwJobs {
  2014  				if pj.Namespace != pjNamespace {
  2015  					t.Errorf("prowjob %q didn't have expected namespace %q but %q", pj.Name, pjNamespace, pj.Namespace)
  2016  				}
  2017  				if pj.Spec.Type == prowapi.BatchJob {
  2018  					pj := pj
  2019  					batchJobs = append(batchJobs, &pj)
  2020  				}
  2021  			}
  2022  
  2023  			if tc.triggered != numCreated {
  2024  				t.Errorf("Wrong number of jobs triggered. Got %d, expected %d.", numCreated, tc.triggered)
  2025  			}
  2026  			if tc.triggered > 0 && tc.enableScheduling {
  2027  				for _, pj := range filteredProwJobs {
  2028  					if pj.Status.State != prowapi.SchedulingState {
  2029  						t.Errorf("Wrong ProwJob state. Got %s, expected %s.", pj.Status.State, prowapi.SchedulingState)
  2030  					}
  2031  				}
  2032  			}
  2033  			if tc.merged != fgc.merged {
  2034  				t.Errorf("Wrong number of merges. Got %d, expected %d.", fgc.merged, tc.merged)
  2035  			}
  2036  			if n := len(c.statusUpdate.dontUpdateStatus.data); n != tc.merged+len(tc.mergeErrs) {
  2037  				t.Errorf("expected %d entries in the dontUpdateStatus map, got %d", tc.merged+len(tc.mergeErrs), n)
  2038  			}
  2039  			// Ensure that the correct number of batch jobs were triggered
  2040  			if tc.triggeredBatches != len(batchJobs) {
  2041  				t.Errorf("Wrong number of batches triggered. Got %d, expected %d.", len(batchJobs), tc.triggeredBatches)
  2042  			}
  2043  			for _, job := range batchJobs {
  2044  				if len(job.Spec.Refs.Pulls) <= 1 {
  2045  					t.Error("Found a batch job that doesn't contain multiple pull refs!")
  2046  				}
  2047  			}
  2048  		})
  2049  	}
  2050  }
  2051  
  2052  func TestServeHTTP(t *testing.T) {
  2053  	pr1 := PullRequest{}
  2054  	pr1.Commits.Nodes = append(pr1.Commits.Nodes, struct{ Commit Commit }{})
  2055  	pr1.Commits.Nodes[0].Commit.Status.Contexts = []Context{{
  2056  		Context:     githubql.String("coverage/coveralls"),
  2057  		Description: githubql.String("Coverage increased (+0.1%) to 27.599%"),
  2058  	}}
  2059  	hist, err := history.New(100, nil, "")
  2060  	if err != nil {
  2061  		t.Fatalf("Failed to create history client: %v", err)
  2062  	}
  2063  	cfg := func() *config.Config { return &config.Config{} }
  2064  	c := &syncController{
  2065  		pools: []Pool{
  2066  			{
  2067  				MissingPRs: []CodeReviewCommon{*CodeReviewCommonFromPullRequest(&pr1)},
  2068  				Action:     Merge,
  2069  			},
  2070  		},
  2071  		provider: &GitHubProvider{
  2072  			mergeChecker: newMergeChecker(cfg, &fgc{}),
  2073  		},
  2074  		History: hist,
  2075  	}
  2076  	s := httptest.NewServer(c)
  2077  	defer s.Close()
  2078  	resp, err := http.Get(s.URL)
  2079  	if err != nil {
  2080  		t.Errorf("GET error: %v", err)
  2081  	}
  2082  	defer resp.Body.Close()
  2083  	var pools []Pool
  2084  	if err := json.NewDecoder(resp.Body).Decode(&pools); err != nil {
  2085  		t.Fatalf("JSON decoding error: %v", err)
  2086  	}
  2087  	if !reflect.DeepEqual(c.pools, pools) {
  2088  		t.Errorf("Received pools %v do not match original pools %v.", pools, c.pools)
  2089  	}
  2090  }
  2091  
  2092  func testPR(org, repo, branch string, number int, mergeable githubql.MergeableState) *PullRequest {
  2093  	pr := PullRequest{
  2094  		Number:     githubql.Int(number),
  2095  		Mergeable:  mergeable,
  2096  		HeadRefOID: githubql.String("SHA"),
  2097  	}
  2098  	pr.Repository.Owner.Login = githubql.String(org)
  2099  	pr.Repository.Name = githubql.String(repo)
  2100  	pr.Repository.NameWithOwner = githubql.String(fmt.Sprintf("%s/%s", org, repo))
  2101  	pr.BaseRef.Name = githubql.String(branch)
  2102  
  2103  	pr.Commits.Nodes = append(pr.Commits.Nodes, struct{ Commit Commit }{
  2104  		Commit{
  2105  			Status: struct{ Contexts []Context }{
  2106  				Contexts: []Context{
  2107  					{
  2108  						Context: githubql.String("context"),
  2109  						State:   githubql.StatusStateSuccess,
  2110  					},
  2111  				},
  2112  			},
  2113  			OID: githubql.String("SHA"),
  2114  		},
  2115  	})
  2116  	return &pr
  2117  }
  2118  
  2119  func testPRWithLabels(org, repo, branch string, number int, mergeable githubql.MergeableState, labels []string) *PullRequest {
  2120  	pr := testPR(org, repo, branch, number, mergeable)
  2121  	for _, label := range labels {
  2122  		labelNode := struct{ Name githubql.String }{Name: githubql.String(label)}
  2123  		pr.Labels.Nodes = append(pr.Labels.Nodes, labelNode)
  2124  	}
  2125  	return pr
  2126  }
  2127  
  2128  func TestSync(t *testing.T) {
  2129  	sleep = func(time.Duration) {}
  2130  	defer func() { sleep = time.Sleep }()
  2131  
  2132  	mergeableA := *testPR("org", "repo", "A", 5, githubql.MergeableStateMergeable)
  2133  	unmergeableA := *testPR("org", "repo", "A", 6, githubql.MergeableStateConflicting)
  2134  	unmergeableB := *testPR("org", "repo", "B", 7, githubql.MergeableStateConflicting)
  2135  	unknownA := *testPR("org", "repo", "A", 8, githubql.MergeableStateUnknown)
  2136  
  2137  	testcases := []struct {
  2138  		name string
  2139  		prs  []PullRequest
  2140  
  2141  		expectedPools []Pool
  2142  	}{
  2143  		{
  2144  			name:          "no PRs",
  2145  			prs:           []PullRequest{},
  2146  			expectedPools: []Pool{},
  2147  		},
  2148  		{
  2149  			name: "1 mergeable PR",
  2150  			prs:  []PullRequest{mergeableA},
  2151  			expectedPools: []Pool{{
  2152  				Org:        "org",
  2153  				Repo:       "repo",
  2154  				Branch:     "A",
  2155  				SuccessPRs: []CodeReviewCommon{*CodeReviewCommonFromPullRequest(&mergeableA)},
  2156  				Action:     Merge,
  2157  				Target:     []CodeReviewCommon{*CodeReviewCommonFromPullRequest(&mergeableA)},
  2158  				TenantIDs:  []string{},
  2159  			}},
  2160  		},
  2161  		{
  2162  			name:          "1 unmergeable PR",
  2163  			prs:           []PullRequest{unmergeableA},
  2164  			expectedPools: []Pool{},
  2165  		},
  2166  		{
  2167  			name: "1 unknown PR",
  2168  			prs:  []PullRequest{unknownA},
  2169  			expectedPools: []Pool{{
  2170  				Org:        "org",
  2171  				Repo:       "repo",
  2172  				Branch:     "A",
  2173  				SuccessPRs: []CodeReviewCommon{*CodeReviewCommonFromPullRequest(&unknownA)},
  2174  				Action:     Merge,
  2175  				Target:     []CodeReviewCommon{*CodeReviewCommonFromPullRequest(&unknownA)},
  2176  				TenantIDs:  []string{},
  2177  			}},
  2178  		},
  2179  		{
  2180  			name: "1 mergeable, 1 unmergeable (different pools)",
  2181  			prs:  []PullRequest{mergeableA, unmergeableB},
  2182  			expectedPools: []Pool{{
  2183  				Org:        "org",
  2184  				Repo:       "repo",
  2185  				Branch:     "A",
  2186  				SuccessPRs: []CodeReviewCommon{*CodeReviewCommonFromPullRequest(&mergeableA)},
  2187  				Action:     Merge,
  2188  				Target:     []CodeReviewCommon{*CodeReviewCommonFromPullRequest(&mergeableA)},
  2189  				TenantIDs:  []string{},
  2190  			}},
  2191  		},
  2192  		{
  2193  			name: "1 mergeable, 1 unmergeable (same pool)",
  2194  			prs:  []PullRequest{mergeableA, unmergeableA},
  2195  			expectedPools: []Pool{{
  2196  				Org:        "org",
  2197  				Repo:       "repo",
  2198  				Branch:     "A",
  2199  				SuccessPRs: []CodeReviewCommon{*CodeReviewCommonFromPullRequest(&mergeableA)},
  2200  				Action:     Merge,
  2201  				Target:     []CodeReviewCommon{*CodeReviewCommonFromPullRequest(&mergeableA)},
  2202  				TenantIDs:  []string{},
  2203  			}},
  2204  		},
  2205  		{
  2206  			name: "1 mergeable PR (satisfies multiple queries)",
  2207  			prs:  []PullRequest{mergeableA, mergeableA},
  2208  			expectedPools: []Pool{{
  2209  				Org:        "org",
  2210  				Repo:       "repo",
  2211  				Branch:     "A",
  2212  				SuccessPRs: []CodeReviewCommon{*CodeReviewCommonFromPullRequest(&mergeableA)},
  2213  				Action:     Merge,
  2214  				Target:     []CodeReviewCommon{*CodeReviewCommonFromPullRequest(&mergeableA)},
  2215  				TenantIDs:  []string{},
  2216  			}},
  2217  		},
  2218  	}
  2219  
  2220  	for _, tc := range testcases {
  2221  		t.Run(tc.name, func(t *testing.T) {
  2222  			fgc := &fgc{
  2223  				prs: map[string][]PullRequest{"": tc.prs},
  2224  				refs: map[string]string{
  2225  					"org/repo heads/A": "SHA",
  2226  					"org/repo A":       "SHA",
  2227  					"org/repo heads/B": "SHA",
  2228  					"org/repo B":       "SHA",
  2229  				},
  2230  			}
  2231  			ca := &config.Agent{}
  2232  			ca.Set(&config.Config{
  2233  				ProwConfig: config.ProwConfig{
  2234  					Tide: config.Tide{
  2235  						MaxGoroutines: 4,
  2236  						TideGitHubConfig: config.TideGitHubConfig{
  2237  							Queries:            []config.TideQuery{{}},
  2238  							StatusUpdatePeriod: &metav1.Duration{Duration: time.Second * 0},
  2239  						},
  2240  					},
  2241  				},
  2242  			})
  2243  			hist, err := history.New(100, nil, "")
  2244  			if err != nil {
  2245  				t.Fatalf("Failed to create history client: %v", err)
  2246  			}
  2247  			mergeChecker := newMergeChecker(ca.Config, fgc)
  2248  			sc := &statusController{
  2249  				pjClient: fakectrlruntimeclient.NewClientBuilder().Build(),
  2250  				logger:   logrus.WithField("controller", "status-update"),
  2251  				ghc:      fgc,
  2252  				gc:       nil,
  2253  				config:   ca.Config,
  2254  				shutDown: make(chan bool),
  2255  				statusUpdate: &statusUpdate{
  2256  					dontUpdateStatus: &threadSafePRSet{},
  2257  					newPoolPending:   make(chan bool),
  2258  				},
  2259  			}
  2260  			go sc.run()
  2261  			defer sc.shutdown()
  2262  			log := logrus.WithField("controller", "sync")
  2263  			ghProvider := newGitHubProvider(log, fgc, nil, ca.Config, mergeChecker, false)
  2264  			c := &syncController{
  2265  				config:        ca.Config,
  2266  				provider:      ghProvider,
  2267  				prowJobClient: fakectrlruntimeclient.NewClientBuilder().Build(),
  2268  				logger:        log,
  2269  				changedFiles: &changedFilesAgent{
  2270  					provider:        ghProvider,
  2271  					nextChangeCache: make(map[changeCacheKey][]string),
  2272  				},
  2273  				History: hist,
  2274  				statusUpdate: &statusUpdate{
  2275  					dontUpdateStatus: &threadSafePRSet{},
  2276  					newPoolPending:   make(chan bool),
  2277  				},
  2278  			}
  2279  
  2280  			if err := c.Sync(); err != nil {
  2281  				t.Fatalf("Unexpected error from 'Sync()': %v.", err)
  2282  			}
  2283  			if len(tc.expectedPools) != len(c.pools) {
  2284  				t.Fatalf("Tide pools did not match expected. Got %#v, expected %#v.", c.pools, tc.expectedPools)
  2285  			}
  2286  			for _, expected := range tc.expectedPools {
  2287  				var match *Pool
  2288  				for i, actual := range c.pools {
  2289  					if expected.Org == actual.Org && expected.Repo == actual.Repo && expected.Branch == actual.Branch {
  2290  						match = &c.pools[i]
  2291  					}
  2292  				}
  2293  				if match == nil {
  2294  					t.Errorf("Failed to find expected pool %s/%s %s.", expected.Org, expected.Repo, expected.Branch)
  2295  				} else if !reflect.DeepEqual(*match, expected) {
  2296  					t.Errorf("Expected pool %#v does not match actual pool %#v.", expected, *match)
  2297  				}
  2298  			}
  2299  		})
  2300  	}
  2301  }
  2302  
  2303  func TestFilterSubpool(t *testing.T) {
  2304  	presubmits := map[int][]config.Presubmit{
  2305  		1: {{Reporter: config.Reporter{Context: "pj-a"}}},
  2306  		2: {{Reporter: config.Reporter{Context: "pj-a"}}, {Reporter: config.Reporter{Context: "pj-b"}}},
  2307  	}
  2308  
  2309  	trueVar := true
  2310  	cc := map[int]contextChecker{
  2311  		1: &config.TideContextPolicy{
  2312  			RequiredContexts:    []string{"pj-a", "pj-b", "other-a"},
  2313  			OptionalContexts:    []string{"tide", "pj-c"},
  2314  			SkipUnknownContexts: &trueVar,
  2315  		},
  2316  		2: &config.TideContextPolicy{
  2317  			RequiredContexts:    []string{"pj-a", "pj-b", "other-a"},
  2318  			OptionalContexts:    []string{"tide", "pj-c"},
  2319  			SkipUnknownContexts: &trueVar,
  2320  		},
  2321  	}
  2322  
  2323  	type pr struct {
  2324  		number    int
  2325  		mergeable bool
  2326  		contexts  []Context
  2327  		checkRuns []CheckRun
  2328  	}
  2329  	tcs := []struct {
  2330  		name string
  2331  
  2332  		prs         []pr
  2333  		expectedPRs []int // Empty indicates no subpool should be returned.
  2334  	}{
  2335  		{
  2336  			name: "one mergeable passing PR (omitting optional context)",
  2337  			prs: []pr{
  2338  				{
  2339  					number:    1,
  2340  					mergeable: true,
  2341  					contexts: []Context{
  2342  						{
  2343  							Context: githubql.String("pj-a"),
  2344  							State:   githubql.StatusStateSuccess,
  2345  						},
  2346  						{
  2347  							Context: githubql.String("pj-b"),
  2348  							State:   githubql.StatusStateSuccess,
  2349  						},
  2350  						{
  2351  							Context: githubql.String("other-a"),
  2352  							State:   githubql.StatusStateSuccess,
  2353  						},
  2354  					},
  2355  				},
  2356  			},
  2357  			expectedPRs: []int{1},
  2358  		},
  2359  		{
  2360  			name: "one mergeable passing PR (omitting optional context), checkrun is considered",
  2361  			prs: []pr{
  2362  				{
  2363  					number:    1,
  2364  					mergeable: true,
  2365  					contexts: []Context{
  2366  						{
  2367  							Context: githubql.String("pj-a"),
  2368  							State:   githubql.StatusStateSuccess,
  2369  						},
  2370  						{
  2371  							Context: githubql.String("pj-b"),
  2372  							State:   githubql.StatusStateSuccess,
  2373  						},
  2374  					},
  2375  					checkRuns: []CheckRun{{
  2376  						Name:       githubql.String("other-a"),
  2377  						Status:     githubql.String(githubql.CheckStatusStateCompleted),
  2378  						Conclusion: githubql.String(githubql.StatusStateSuccess),
  2379  					}},
  2380  				},
  2381  			},
  2382  			expectedPRs: []int{1},
  2383  		},
  2384  		{
  2385  			name: "one mergeable passing PR (omitting optional context), neutral checkrun is considered success",
  2386  			prs: []pr{
  2387  				{
  2388  					number:    1,
  2389  					mergeable: true,
  2390  					contexts: []Context{
  2391  						{
  2392  							Context: githubql.String("pj-a"),
  2393  							State:   githubql.StatusStateSuccess,
  2394  						},
  2395  						{
  2396  							Context: githubql.String("pj-b"),
  2397  							State:   githubql.StatusStateSuccess,
  2398  						},
  2399  					},
  2400  					checkRuns: []CheckRun{{
  2401  						Name:       githubql.String("other-a"),
  2402  						Status:     githubql.String(githubql.CheckStatusStateCompleted),
  2403  						Conclusion: githubql.String(githubql.CheckConclusionStateNeutral),
  2404  					}},
  2405  				},
  2406  			},
  2407  			expectedPRs: []int{1},
  2408  		},
  2409  		{
  2410  			name: "Incomplete checkrun throws the pr out",
  2411  			prs: []pr{
  2412  				{
  2413  					number:    1,
  2414  					mergeable: true,
  2415  					contexts: []Context{
  2416  						{
  2417  							Context: githubql.String("pj-a"),
  2418  							State:   githubql.StatusStateSuccess,
  2419  						},
  2420  						{
  2421  							Context: githubql.String("pj-b"),
  2422  							State:   githubql.StatusStateSuccess,
  2423  						},
  2424  					},
  2425  					checkRuns: []CheckRun{{
  2426  						Name:       githubql.String("other-a"),
  2427  						Conclusion: githubql.String(githubql.StatusStateSuccess),
  2428  					}},
  2429  				},
  2430  			},
  2431  		},
  2432  		{
  2433  			name: "Failing checkrun throws the pr out",
  2434  			prs: []pr{
  2435  				{
  2436  					number:    1,
  2437  					mergeable: true,
  2438  					contexts: []Context{
  2439  						{
  2440  							Context: githubql.String("pj-a"),
  2441  							State:   githubql.StatusStateSuccess,
  2442  						},
  2443  						{
  2444  							Context: githubql.String("pj-b"),
  2445  							State:   githubql.StatusStateSuccess,
  2446  						},
  2447  					},
  2448  					checkRuns: []CheckRun{{
  2449  						Name:       githubql.String("other-a"),
  2450  						Status:     githubql.String(githubql.CheckStatusStateCompleted),
  2451  						Conclusion: githubql.String(githubql.StatusStateFailure),
  2452  					}},
  2453  				},
  2454  			},
  2455  		},
  2456  		{
  2457  			name: "one unmergeable passing PR",
  2458  			prs: []pr{
  2459  				{
  2460  					number:    1,
  2461  					mergeable: false,
  2462  					contexts: []Context{
  2463  						{
  2464  							Context: githubql.String("pj-a"),
  2465  							State:   githubql.StatusStateSuccess,
  2466  						},
  2467  						{
  2468  							Context: githubql.String("pj-b"),
  2469  							State:   githubql.StatusStateSuccess,
  2470  						},
  2471  						{
  2472  							Context: githubql.String("other-a"),
  2473  							State:   githubql.StatusStateSuccess,
  2474  						},
  2475  					},
  2476  				},
  2477  			},
  2478  			expectedPRs: []int{},
  2479  		},
  2480  		{
  2481  			name: "one mergeable PR pending non-PJ context (consider failing)",
  2482  			prs: []pr{
  2483  				{
  2484  					number:    2,
  2485  					mergeable: true,
  2486  					contexts: []Context{
  2487  						{
  2488  							Context: githubql.String("pj-a"),
  2489  							State:   githubql.StatusStateSuccess,
  2490  						},
  2491  						{
  2492  							Context: githubql.String("pj-b"),
  2493  							State:   githubql.StatusStateSuccess,
  2494  						},
  2495  						{
  2496  							Context: githubql.String("other-a"),
  2497  							State:   githubql.StatusStatePending,
  2498  						},
  2499  					},
  2500  				},
  2501  			},
  2502  			expectedPRs: []int{},
  2503  		},
  2504  		{
  2505  			name: "one mergeable PR pending PJ context (consider in pool)",
  2506  			prs: []pr{
  2507  				{
  2508  					number:    2,
  2509  					mergeable: true,
  2510  					contexts: []Context{
  2511  						{
  2512  							Context: githubql.String("pj-a"),
  2513  							State:   githubql.StatusStateSuccess,
  2514  						},
  2515  						{
  2516  							Context: githubql.String("pj-b"),
  2517  							State:   githubql.StatusStatePending,
  2518  						},
  2519  						{
  2520  							Context: githubql.String("other-a"),
  2521  							State:   githubql.StatusStateSuccess,
  2522  						},
  2523  					},
  2524  				},
  2525  			},
  2526  			expectedPRs: []int{2},
  2527  		},
  2528  		{
  2529  			name: "one mergeable PR failing PJ context (consider failing)",
  2530  			prs: []pr{
  2531  				{
  2532  					number:    2,
  2533  					mergeable: true,
  2534  					contexts: []Context{
  2535  						{
  2536  							Context: githubql.String("pj-a"),
  2537  							State:   githubql.StatusStateSuccess,
  2538  						},
  2539  						{
  2540  							Context: githubql.String("pj-b"),
  2541  							State:   githubql.StatusStateFailure,
  2542  						},
  2543  						{
  2544  							Context: githubql.String("other-a"),
  2545  							State:   githubql.StatusStateSuccess,
  2546  						},
  2547  					},
  2548  				},
  2549  			},
  2550  			expectedPRs: []int{},
  2551  		},
  2552  		{
  2553  			name: "one mergeable PR missing PJ context (consider failing)",
  2554  			prs: []pr{
  2555  				{
  2556  					number:    2,
  2557  					mergeable: true,
  2558  					contexts: []Context{
  2559  						{
  2560  							Context: githubql.String("pj-b"),
  2561  							State:   githubql.StatusStateSuccess,
  2562  						},
  2563  						{
  2564  							Context: githubql.String("other-a"),
  2565  							State:   githubql.StatusStateSuccess,
  2566  						},
  2567  					},
  2568  				},
  2569  			},
  2570  			expectedPRs: []int{},
  2571  		},
  2572  		{
  2573  			name: "one mergeable PR failing unknown context (consider in pool)",
  2574  			prs: []pr{
  2575  				{
  2576  					number:    2,
  2577  					mergeable: true,
  2578  					contexts: []Context{
  2579  						{
  2580  							Context: githubql.String("pj-a"),
  2581  							State:   githubql.StatusStateSuccess,
  2582  						},
  2583  						{
  2584  							Context: githubql.String("pj-b"),
  2585  							State:   githubql.StatusStateSuccess,
  2586  						},
  2587  						{
  2588  							Context: githubql.String("other-a"),
  2589  							State:   githubql.StatusStateSuccess,
  2590  						},
  2591  						{
  2592  							Context: githubql.String("unknown"),
  2593  							State:   githubql.StatusStateFailure,
  2594  						},
  2595  					},
  2596  				},
  2597  			},
  2598  			expectedPRs: []int{2},
  2599  		},
  2600  		{
  2601  			name: "one PR failing non-PJ required context; one PR successful (should not prune pool)",
  2602  			prs: []pr{
  2603  				{
  2604  					number:    1,
  2605  					mergeable: true,
  2606  					contexts: []Context{
  2607  						{
  2608  							Context: githubql.String("pj-a"),
  2609  							State:   githubql.StatusStateSuccess,
  2610  						},
  2611  						{
  2612  							Context: githubql.String("pj-b"),
  2613  							State:   githubql.StatusStateSuccess,
  2614  						},
  2615  						{
  2616  							Context: githubql.String("other-a"),
  2617  							State:   githubql.StatusStateFailure,
  2618  						},
  2619  					},
  2620  				},
  2621  				{
  2622  					number:    2,
  2623  					mergeable: true,
  2624  					contexts: []Context{
  2625  						{
  2626  							Context: githubql.String("pj-a"),
  2627  							State:   githubql.StatusStateSuccess,
  2628  						},
  2629  						{
  2630  							Context: githubql.String("pj-b"),
  2631  							State:   githubql.StatusStateSuccess,
  2632  						},
  2633  						{
  2634  							Context: githubql.String("other-a"),
  2635  							State:   githubql.StatusStateSuccess,
  2636  						},
  2637  						{
  2638  							Context: githubql.String("unknown"),
  2639  							State:   githubql.StatusStateSuccess,
  2640  						},
  2641  					},
  2642  				},
  2643  			},
  2644  			expectedPRs: []int{2},
  2645  		},
  2646  		{
  2647  			name: "two successful PRs",
  2648  			prs: []pr{
  2649  				{
  2650  					number:    1,
  2651  					mergeable: true,
  2652  					contexts: []Context{
  2653  						{
  2654  							Context: githubql.String("pj-a"),
  2655  							State:   githubql.StatusStateSuccess,
  2656  						},
  2657  						{
  2658  							Context: githubql.String("pj-b"),
  2659  							State:   githubql.StatusStateSuccess,
  2660  						},
  2661  						{
  2662  							Context: githubql.String("other-a"),
  2663  							State:   githubql.StatusStateSuccess,
  2664  						},
  2665  					},
  2666  				},
  2667  				{
  2668  					number:    2,
  2669  					mergeable: true,
  2670  					contexts: []Context{
  2671  						{
  2672  							Context: githubql.String("pj-a"),
  2673  							State:   githubql.StatusStateSuccess,
  2674  						},
  2675  						{
  2676  							Context: githubql.String("pj-b"),
  2677  							State:   githubql.StatusStateSuccess,
  2678  						},
  2679  						{
  2680  							Context: githubql.String("other-a"),
  2681  							State:   githubql.StatusStateSuccess,
  2682  						},
  2683  					},
  2684  				},
  2685  			},
  2686  			expectedPRs: []int{1, 2},
  2687  		},
  2688  	}
  2689  	for _, tc := range tcs {
  2690  		t.Run(tc.name, func(t *testing.T) {
  2691  			sp := &subpool{
  2692  				org:        "org",
  2693  				repo:       "repo",
  2694  				branch:     "branch",
  2695  				presubmits: presubmits,
  2696  				cc:         cc,
  2697  				log:        logrus.WithFields(logrus.Fields{"org": "org", "repo": "repo", "branch": "branch"}),
  2698  			}
  2699  			for _, pull := range tc.prs {
  2700  				pr := PullRequest{
  2701  					Number: githubql.Int(pull.number),
  2702  				}
  2703  				var checkRunNodes []CheckRunNode
  2704  				for _, checkRun := range pull.checkRuns {
  2705  					checkRunNodes = append(checkRunNodes, CheckRunNode{CheckRun: checkRun})
  2706  				}
  2707  				pr.Commits.Nodes = []struct{ Commit Commit }{
  2708  					{
  2709  						Commit{
  2710  							Status: struct{ Contexts []Context }{
  2711  								Contexts: pull.contexts,
  2712  							},
  2713  							StatusCheckRollup: StatusCheckRollup{
  2714  								Contexts: StatusCheckRollupContext{
  2715  									Nodes: checkRunNodes,
  2716  								},
  2717  							},
  2718  						},
  2719  					},
  2720  				}
  2721  				if !pull.mergeable {
  2722  					pr.Mergeable = githubql.MergeableStateConflicting
  2723  				}
  2724  				sp.prs = append(sp.prs, *CodeReviewCommonFromPullRequest(&pr))
  2725  			}
  2726  
  2727  			configGetter := func() *config.Config { return &config.Config{} }
  2728  			mmc := newMergeChecker(configGetter, &fgc{})
  2729  			provider := &GitHubProvider{
  2730  				cfg:          configGetter,
  2731  				mergeChecker: mmc,
  2732  				logger:       logrus.WithContext(context.Background()),
  2733  			}
  2734  			filtered := filterSubpool(provider, mmc.isAllowedToMerge, sp)
  2735  			if len(tc.expectedPRs) == 0 {
  2736  				if filtered != nil {
  2737  					t.Fatalf("Expected subpool to be pruned, but got: %v", filtered)
  2738  				}
  2739  				return
  2740  			}
  2741  			if filtered == nil {
  2742  				t.Fatalf("Expected subpool to have %d prs, but it was pruned.", len(tc.expectedPRs))
  2743  			}
  2744  			if got := prNumbers(filtered.prs); !reflect.DeepEqual(got, tc.expectedPRs) {
  2745  				t.Errorf("Expected filtered pool to have PRs %v, but got %v.", tc.expectedPRs, got)
  2746  			}
  2747  		})
  2748  	}
  2749  }
  2750  
  2751  func TestIsPassing(t *testing.T) {
  2752  	yes := true
  2753  	no := false
  2754  	headSHA := "head"
  2755  	success := string(githubql.StatusStateSuccess)
  2756  	failure := string(githubql.StatusStateFailure)
  2757  	testCases := []struct {
  2758  		name              string
  2759  		passing           bool
  2760  		config            config.TideContextPolicy
  2761  		combinedContexts  map[string]string
  2762  		availableContexts []string
  2763  		failedContexts    []string
  2764  	}{
  2765  		{
  2766  			name:              "empty policy - success (trust combined status)",
  2767  			passing:           true,
  2768  			combinedContexts:  map[string]string{"c1": success, "c2": success, statusContext: failure},
  2769  			availableContexts: []string{"c1", "c2", statusContext},
  2770  		},
  2771  		{
  2772  			name:              "empty policy - failure because of failed context c4 (trust combined status)",
  2773  			passing:           false,
  2774  			combinedContexts:  map[string]string{"c1": success, "c2": success, "c3": failure, statusContext: failure},
  2775  			availableContexts: []string{"c1", "c2", "c3", statusContext},
  2776  			failedContexts:    []string{"c3"},
  2777  		},
  2778  		{
  2779  			name:    "passing (trust combined status)",
  2780  			passing: true,
  2781  			config: config.TideContextPolicy{
  2782  				RequiredContexts:    []string{"c1", "c2", "c3"},
  2783  				SkipUnknownContexts: &no,
  2784  			},
  2785  			combinedContexts:  map[string]string{"c1": success, "c2": success, "c3": success, statusContext: failure},
  2786  			availableContexts: []string{"c1", "c2", "c3", statusContext},
  2787  		},
  2788  		{
  2789  			name:    "failing because of missing required check c3",
  2790  			passing: false,
  2791  			config: config.TideContextPolicy{
  2792  				RequiredContexts: []string{"c1", "c2", "c3"},
  2793  			},
  2794  			combinedContexts:  map[string]string{"c1": success, "c2": success, statusContext: failure},
  2795  			availableContexts: []string{"c1", "c2", statusContext},
  2796  			failedContexts:    []string{"c3"},
  2797  		},
  2798  		{
  2799  			name:             "failing because of failed context c2",
  2800  			passing:          false,
  2801  			combinedContexts: map[string]string{"c1": success, "c2": failure},
  2802  			config: config.TideContextPolicy{
  2803  				RequiredContexts: []string{"c1", "c2", "c3"},
  2804  				OptionalContexts: []string{"c4"},
  2805  			},
  2806  			availableContexts: []string{"c1", "c2"},
  2807  			failedContexts:    []string{"c2", "c3"},
  2808  		},
  2809  		{
  2810  			name:    "passing because of failed context c4 is optional",
  2811  			passing: true,
  2812  
  2813  			combinedContexts: map[string]string{"c1": success, "c2": success, "c3": success, "c4": failure},
  2814  			config: config.TideContextPolicy{
  2815  				RequiredContexts: []string{"c1", "c2", "c3"},
  2816  				OptionalContexts: []string{"c4"},
  2817  			},
  2818  			availableContexts: []string{"c1", "c2", "c3", "c4"},
  2819  		},
  2820  		{
  2821  			name:    "skipping unknown contexts - failing because of missing required context c3",
  2822  			passing: false,
  2823  			config: config.TideContextPolicy{
  2824  				RequiredContexts:    []string{"c1", "c2", "c3"},
  2825  				SkipUnknownContexts: &yes,
  2826  			},
  2827  			combinedContexts:  map[string]string{"c1": success, "c2": success, statusContext: failure},
  2828  			availableContexts: []string{"c1", "c2", statusContext},
  2829  			failedContexts:    []string{"c3"},
  2830  		},
  2831  		{
  2832  			name:             "skipping unknown contexts - failing because c2 is failing",
  2833  			passing:          false,
  2834  			combinedContexts: map[string]string{"c1": success, "c2": failure},
  2835  			config: config.TideContextPolicy{
  2836  				RequiredContexts:    []string{"c1", "c2"},
  2837  				OptionalContexts:    []string{"c4"},
  2838  				SkipUnknownContexts: &yes,
  2839  			},
  2840  			availableContexts: []string{"c1", "c2"},
  2841  			failedContexts:    []string{"c2"},
  2842  		},
  2843  		{
  2844  			name:             "skipping unknown contexts - passing because c4 is optional",
  2845  			passing:          true,
  2846  			combinedContexts: map[string]string{"c1": success, "c2": success, "c3": success, "c4": failure},
  2847  			config: config.TideContextPolicy{
  2848  				RequiredContexts:    []string{"c1", "c3"},
  2849  				OptionalContexts:    []string{"c4"},
  2850  				SkipUnknownContexts: &yes,
  2851  			},
  2852  			availableContexts: []string{"c1", "c2", "c3", "c4"},
  2853  		},
  2854  		{
  2855  			name:    "skipping unknown contexts - passing because c4 is optional and c5 is unknown",
  2856  			passing: true,
  2857  
  2858  			combinedContexts: map[string]string{"c1": success, "c2": success, "c3": success, "c4": failure, "c5": failure},
  2859  			config: config.TideContextPolicy{
  2860  				RequiredContexts:    []string{"c1", "c3"},
  2861  				OptionalContexts:    []string{"c4"},
  2862  				SkipUnknownContexts: &yes,
  2863  			},
  2864  			availableContexts: []string{"c1", "c2", "c3", "c4", "c5"},
  2865  		},
  2866  	}
  2867  
  2868  	for _, tc := range testCases {
  2869  		t.Run(tc.name, func(t *testing.T) {
  2870  
  2871  			ghc := &fgc{
  2872  				combinedStatus: tc.combinedContexts,
  2873  				expectedSHA:    headSHA}
  2874  			log := logrus.WithField("component", "tide")
  2875  			hook := test.NewGlobal()
  2876  			_, err := log.String()
  2877  			if err != nil {
  2878  				t.Fatalf("Failed to get log output before testing: %v", err)
  2879  			}
  2880  			syncCtl := &syncController{provider: &GitHubProvider{ghc: ghc, logger: log}}
  2881  			pr := PullRequest{HeadRefOID: githubql.String(headSHA)}
  2882  			passing := syncCtl.isPassingTests(log, CodeReviewCommonFromPullRequest(&pr), &tc.config)
  2883  			if passing != tc.passing {
  2884  				t.Errorf("%s: Expected %t got %t", tc.name, tc.passing, passing)
  2885  			}
  2886  
  2887  			// The last entry is used as the hook captures 2 different logs.
  2888  			// The required fields are available in the last entry and are validated.
  2889  			logFields := hook.LastEntry().Data
  2890  			assert.Equal(t, logFields["context_names"], tc.availableContexts)
  2891  			assert.Equal(t, logFields["failed_context_names"], tc.failedContexts)
  2892  			assert.Equal(t, logFields["total_context_count"], len(tc.availableContexts))
  2893  			assert.Equal(t, logFields["failed_context_count"], len(tc.failedContexts))
  2894  			if tc.passing {
  2895  				c := &syncController{
  2896  					provider:      &GitHubProvider{ghc: ghc, logger: log},
  2897  					prowJobClient: fakectrlruntimeclient.NewClientBuilder().Build(),
  2898  					config:        func() *config.Config { return &config.Config{} },
  2899  				}
  2900  				// isRetestEligible is more lenient than isPassingTests, which means we expect it to allow
  2901  				// everything that is allowed by isPassingTests. The reverse might not be true.
  2902  				if !c.isRetestEligible(log, CodeReviewCommonFromPullRequest(&pr), &tc.config) {
  2903  					t.Error("expected pr to be batch testing eligible, wasn't the case")
  2904  				}
  2905  			}
  2906  		})
  2907  	}
  2908  }
  2909  
  2910  func TestPresubmitsByPull(t *testing.T) {
  2911  	samplePR := PullRequest{
  2912  		Number:     githubql.Int(100),
  2913  		HeadRefOID: githubql.String("sha"),
  2914  	}
  2915  	testcases := []struct {
  2916  		name string
  2917  
  2918  		initialChangeCache map[changeCacheKey][]string
  2919  		presubmits         []config.Presubmit
  2920  		prs                []CodeReviewCommon
  2921  		prowYAMLGetter     config.ProwYAMLGetter
  2922  
  2923  		expectedPresubmits           map[int][]config.Presubmit
  2924  		expectedChangeCache          map[changeCacheKey][]string
  2925  		requireManuallyTriggeredJobs bool
  2926  		fromBranchProtection         bool
  2927  	}{
  2928  		{
  2929  			name: "no matching presubmits",
  2930  			presubmits: []config.Presubmit{
  2931  				{
  2932  					Reporter: config.Reporter{Context: "always"},
  2933  					RegexpChangeMatcher: config.RegexpChangeMatcher{
  2934  						RunIfChanged: "foo",
  2935  					},
  2936  				},
  2937  				{
  2938  					Reporter: config.Reporter{Context: "never"},
  2939  				},
  2940  			},
  2941  			expectedChangeCache: map[changeCacheKey][]string{{number: 100, sha: "sha"}: {"CHANGED"}},
  2942  			expectedPresubmits:  map[int][]config.Presubmit{},
  2943  		},
  2944  		{
  2945  			name:               "no presubmits",
  2946  			presubmits:         []config.Presubmit{},
  2947  			expectedPresubmits: map[int][]config.Presubmit{},
  2948  		},
  2949  		{
  2950  			name: "no matching presubmits (check cache eviction)",
  2951  			presubmits: []config.Presubmit{
  2952  				{
  2953  					Reporter: config.Reporter{Context: "never"},
  2954  				},
  2955  			},
  2956  			initialChangeCache: map[changeCacheKey][]string{{number: 100, sha: "sha"}: {"FILE"}},
  2957  			expectedPresubmits: map[int][]config.Presubmit{},
  2958  		},
  2959  		{
  2960  			name: "no matching presubmits (check cache retention)",
  2961  			presubmits: []config.Presubmit{
  2962  				{
  2963  					Reporter: config.Reporter{Context: "always"},
  2964  					RegexpChangeMatcher: config.RegexpChangeMatcher{
  2965  						RunIfChanged: "foo",
  2966  					},
  2967  				},
  2968  				{
  2969  					Reporter: config.Reporter{Context: "never"},
  2970  				},
  2971  			},
  2972  			initialChangeCache:  map[changeCacheKey][]string{{number: 100, sha: "sha"}: {"FILE"}},
  2973  			expectedChangeCache: map[changeCacheKey][]string{{number: 100, sha: "sha"}: {"FILE"}},
  2974  			expectedPresubmits:  map[int][]config.Presubmit{},
  2975  		},
  2976  		{
  2977  			name: "always_run",
  2978  			presubmits: []config.Presubmit{
  2979  				{
  2980  					Reporter:  config.Reporter{Context: "always"},
  2981  					AlwaysRun: true,
  2982  				},
  2983  				{
  2984  					Reporter: config.Reporter{Context: "never"},
  2985  				},
  2986  			},
  2987  			expectedPresubmits: map[int][]config.Presubmit{100: {{
  2988  				Reporter:  config.Reporter{Context: "always"},
  2989  				AlwaysRun: true,
  2990  			}}},
  2991  		},
  2992  		{
  2993  			name: "runs against branch",
  2994  			presubmits: []config.Presubmit{
  2995  				{
  2996  					Reporter:  config.Reporter{Context: "presubmit"},
  2997  					AlwaysRun: true,
  2998  					Brancher: config.Brancher{
  2999  						Branches: []string{defaultBranch, "dev"},
  3000  					},
  3001  				},
  3002  				{
  3003  					Reporter: config.Reporter{Context: "never"},
  3004  				},
  3005  			},
  3006  			expectedPresubmits: map[int][]config.Presubmit{100: {{
  3007  				Reporter:  config.Reporter{Context: "presubmit"},
  3008  				AlwaysRun: true,
  3009  				Brancher: config.Brancher{
  3010  					Branches: []string{defaultBranch, "dev"},
  3011  				},
  3012  			}}},
  3013  		},
  3014  		{
  3015  			name: "doesn't run against branch",
  3016  			presubmits: []config.Presubmit{
  3017  				{
  3018  					Reporter:  config.Reporter{Context: "presubmit"},
  3019  					AlwaysRun: true,
  3020  					Brancher: config.Brancher{
  3021  						Branches: []string{"release", "dev"},
  3022  					},
  3023  				},
  3024  				{
  3025  					Reporter:  config.Reporter{Context: "always"},
  3026  					AlwaysRun: true,
  3027  				},
  3028  				{
  3029  					Reporter: config.Reporter{Context: "never"},
  3030  				},
  3031  			},
  3032  			expectedPresubmits: map[int][]config.Presubmit{100: {{
  3033  				Reporter:  config.Reporter{Context: "always"},
  3034  				AlwaysRun: true,
  3035  			}}},
  3036  		},
  3037  		{
  3038  			name: "no-always-run-no-trigger",
  3039  			presubmits: []config.Presubmit{
  3040  				{
  3041  					Reporter:  config.Reporter{Context: "presubmit"},
  3042  					AlwaysRun: false,
  3043  					Brancher: config.Brancher{
  3044  						Branches: []string{defaultBranch, "dev"},
  3045  					},
  3046  				},
  3047  				{
  3048  					Reporter: config.Reporter{Context: "never"},
  3049  				},
  3050  			},
  3051  			expectedPresubmits: map[int][]config.Presubmit{},
  3052  		},
  3053  		{
  3054  			name: "no-always-run-no-trigger-tide-wants-it",
  3055  			presubmits: []config.Presubmit{
  3056  				{
  3057  					Reporter:  config.Reporter{Context: "presubmit"},
  3058  					AlwaysRun: false,
  3059  					Brancher: config.Brancher{
  3060  						Branches: []string{defaultBranch, "dev"},
  3061  					},
  3062  					RunBeforeMerge: true,
  3063  				},
  3064  				{
  3065  					Reporter: config.Reporter{Context: "never"},
  3066  				},
  3067  			},
  3068  			expectedPresubmits: map[int][]config.Presubmit{100: {{
  3069  				Reporter:       config.Reporter{Context: "presubmit"},
  3070  				AlwaysRun:      false,
  3071  				RunBeforeMerge: true,
  3072  				Brancher: config.Brancher{
  3073  					Branches: []string{defaultBranch, "dev"},
  3074  				},
  3075  			}}},
  3076  		},
  3077  		{
  3078  			name: "runs manual triggered jobs (requireManuallyTriggeredJobs enabled)",
  3079  			presubmits: []config.Presubmit{
  3080  				{
  3081  					Reporter:  config.Reporter{Context: "presubmit"},
  3082  					AlwaysRun: false,
  3083  					Brancher: config.Brancher{
  3084  						Branches: []string{defaultBranch, "dev"},
  3085  					},
  3086  				},
  3087  			},
  3088  			requireManuallyTriggeredJobs: true,
  3089  			fromBranchProtection:         true,
  3090  			expectedPresubmits: map[int][]config.Presubmit{100: {{
  3091  				Reporter:  config.Reporter{Context: "presubmit"},
  3092  				AlwaysRun: false,
  3093  				Brancher: config.Brancher{
  3094  					Branches: []string{defaultBranch, "dev"},
  3095  				},
  3096  			}}},
  3097  		},
  3098  		{
  3099  			name: "doesn't run manual triggered jobs (requireManuallyTriggeredJobs disabled)",
  3100  			presubmits: []config.Presubmit{
  3101  				{
  3102  					Reporter:  config.Reporter{Context: "presubmit"},
  3103  					AlwaysRun: false,
  3104  					Brancher: config.Brancher{
  3105  						Branches: []string{defaultBranch, "dev"},
  3106  					},
  3107  				},
  3108  			},
  3109  			fromBranchProtection: true,
  3110  			expectedPresubmits:   map[int][]config.Presubmit{},
  3111  		},
  3112  		{
  3113  			name: "brancher-not-match-when-tide-wants-it",
  3114  			presubmits: []config.Presubmit{
  3115  				{
  3116  					Reporter:  config.Reporter{Context: "presubmit"},
  3117  					AlwaysRun: false,
  3118  					Brancher: config.Brancher{
  3119  						Branches: []string{"release", "dev"},
  3120  					},
  3121  					RunBeforeMerge: true,
  3122  				},
  3123  				{
  3124  					Reporter: config.Reporter{Context: "never"},
  3125  				},
  3126  			},
  3127  			expectedPresubmits: map[int][]config.Presubmit{},
  3128  		},
  3129  		{
  3130  			name: "run_if_changed (uncached)",
  3131  			presubmits: []config.Presubmit{
  3132  				{
  3133  					Reporter: config.Reporter{Context: "presubmit"},
  3134  					RegexpChangeMatcher: config.RegexpChangeMatcher{
  3135  						RunIfChanged: "^CHANGE.$",
  3136  					},
  3137  				},
  3138  				{
  3139  					Reporter:  config.Reporter{Context: "always"},
  3140  					AlwaysRun: true,
  3141  				},
  3142  				{
  3143  					Reporter: config.Reporter{Context: "never"},
  3144  				},
  3145  			},
  3146  			expectedPresubmits: map[int][]config.Presubmit{100: {{
  3147  				Reporter: config.Reporter{Context: "presubmit"},
  3148  				RegexpChangeMatcher: config.RegexpChangeMatcher{
  3149  					RunIfChanged: "^CHANGE.$",
  3150  				},
  3151  			}, {
  3152  				Reporter:  config.Reporter{Context: "always"},
  3153  				AlwaysRun: true,
  3154  			}}},
  3155  			expectedChangeCache: map[changeCacheKey][]string{{number: 100, sha: "sha"}: {"CHANGED"}},
  3156  		},
  3157  		{
  3158  			name: "run_if_changed (cached)",
  3159  			presubmits: []config.Presubmit{
  3160  				{
  3161  					Reporter: config.Reporter{Context: "presubmit"},
  3162  					RegexpChangeMatcher: config.RegexpChangeMatcher{
  3163  						RunIfChanged: "^FIL.$",
  3164  					},
  3165  				},
  3166  				{
  3167  					Reporter:  config.Reporter{Context: "always"},
  3168  					AlwaysRun: true,
  3169  				},
  3170  				{
  3171  					Reporter: config.Reporter{Context: "never"},
  3172  				},
  3173  			},
  3174  			initialChangeCache: map[changeCacheKey][]string{{number: 100, sha: "sha"}: {"FILE"}},
  3175  			expectedPresubmits: map[int][]config.Presubmit{100: {{
  3176  				Reporter: config.Reporter{Context: "presubmit"},
  3177  				RegexpChangeMatcher: config.RegexpChangeMatcher{
  3178  					RunIfChanged: "^FIL.$",
  3179  				},
  3180  			},
  3181  				{
  3182  					Reporter:  config.Reporter{Context: "always"},
  3183  					AlwaysRun: true,
  3184  				}}},
  3185  			expectedChangeCache: map[changeCacheKey][]string{{number: 100, sha: "sha"}: {"FILE"}},
  3186  		},
  3187  		{
  3188  			name: "run_if_changed (cached) (skippable)",
  3189  			presubmits: []config.Presubmit{
  3190  				{
  3191  					Reporter: config.Reporter{Context: "presubmit"},
  3192  					RegexpChangeMatcher: config.RegexpChangeMatcher{
  3193  						RunIfChanged: "^CHANGE.$",
  3194  					},
  3195  				},
  3196  				{
  3197  					Reporter:  config.Reporter{Context: "always"},
  3198  					AlwaysRun: true,
  3199  				},
  3200  				{
  3201  					Reporter: config.Reporter{Context: "never"},
  3202  				},
  3203  			},
  3204  			initialChangeCache: map[changeCacheKey][]string{{number: 100, sha: "sha"}: {"FILE"}},
  3205  			expectedPresubmits: map[int][]config.Presubmit{100: {{
  3206  				Reporter:  config.Reporter{Context: "always"},
  3207  				AlwaysRun: true,
  3208  			}}},
  3209  			expectedChangeCache: map[changeCacheKey][]string{{number: 100, sha: "sha"}: {"FILE"}},
  3210  		},
  3211  		{
  3212  			name: "inrepoconfig presubmits get only added to the corresponding pull",
  3213  			presubmits: []config.Presubmit{{
  3214  				AlwaysRun: true,
  3215  				Reporter:  config.Reporter{Context: "always"},
  3216  			}},
  3217  			prowYAMLGetter: prowYAMLGetterForHeadRefs([]string{"1"}, []config.Presubmit{{
  3218  				AlwaysRun: true,
  3219  				Reporter:  config.Reporter{Context: "inrepoconfig"},
  3220  			}}),
  3221  			prs: []CodeReviewCommon{
  3222  				{Number: 1, HeadRefOID: "1"},
  3223  			},
  3224  			expectedPresubmits: map[int][]config.Presubmit{
  3225  				1: {
  3226  					{AlwaysRun: true, Reporter: config.Reporter{Context: "always"}},
  3227  					{AlwaysRun: true, Reporter: config.Reporter{Context: "inrepoconfig"}},
  3228  				},
  3229  				100: {
  3230  					{AlwaysRun: true, Reporter: config.Reporter{Context: "always"}},
  3231  				},
  3232  			},
  3233  		},
  3234  		{
  3235  			name: "broken inrepoconfig doesn't break the whole subpool",
  3236  			presubmits: []config.Presubmit{{
  3237  				AlwaysRun: true,
  3238  				Reporter:  config.Reporter{Context: "always"},
  3239  			}},
  3240  			prowYAMLGetter: func(_ *config.Config, _ git.ClientFactory, _, _, _ string, headRefs ...string) (*config.ProwYAML, error) {
  3241  				if len(headRefs) == 1 && headRefs[0] == "1" {
  3242  					return nil, errors.New("you shall not get jobs")
  3243  				}
  3244  				return &config.ProwYAML{}, nil
  3245  			},
  3246  			prs: []CodeReviewCommon{
  3247  				{Number: 1, HeadRefOID: "1"},
  3248  			},
  3249  			expectedPresubmits: map[int][]config.Presubmit{
  3250  				100: {
  3251  					{AlwaysRun: true, Reporter: config.Reporter{Context: "always"}},
  3252  				},
  3253  			},
  3254  		},
  3255  	}
  3256  
  3257  	for _, tc := range testcases {
  3258  		tc := tc
  3259  		t.Run(tc.name, func(t *testing.T) {
  3260  			if tc.initialChangeCache == nil {
  3261  				tc.initialChangeCache = map[changeCacheKey][]string{}
  3262  			}
  3263  			if tc.expectedChangeCache == nil {
  3264  				tc.expectedChangeCache = map[changeCacheKey][]string{}
  3265  			}
  3266  
  3267  			cfg := &config.Config{
  3268  				ProwConfig: config.ProwConfig{
  3269  					BranchProtection: config.BranchProtection{
  3270  						Policy: config.Policy{
  3271  							RequireManuallyTriggeredJobs: &tc.requireManuallyTriggeredJobs,
  3272  						},
  3273  					},
  3274  					Tide: config.Tide{
  3275  						TideGitHubConfig: config.TideGitHubConfig{
  3276  							ContextOptions: config.TideContextPolicyOptions{
  3277  								TideContextPolicy: config.TideContextPolicy{
  3278  									FromBranchProtection: &tc.fromBranchProtection,
  3279  								},
  3280  							},
  3281  						},
  3282  					},
  3283  				},
  3284  			}
  3285  
  3286  			cfg.SetPresubmits(map[string][]config.Presubmit{
  3287  				"/":       tc.presubmits,
  3288  				"foo/bar": {{Reporter: config.Reporter{Context: "wrong-repo"}, AlwaysRun: true}},
  3289  			})
  3290  			if tc.prowYAMLGetter != nil {
  3291  				cfg.InRepoConfig.Enabled = map[string]*bool{"*": utilpointer.Bool(true)}
  3292  				cfg.ProwYAMLGetterWithDefaults = tc.prowYAMLGetter
  3293  			}
  3294  			cfgAgent := &config.Agent{}
  3295  			cfgAgent.Set(cfg)
  3296  			sp := &subpool{
  3297  				branch: defaultBranch,
  3298  				sha:    "master-sha",
  3299  				prs:    append(tc.prs, *CodeReviewCommonFromPullRequest(&samplePR)),
  3300  			}
  3301  			log := logrus.WithField("test", tc.name)
  3302  			ghProvider := newGitHubProvider(log, &fgc{}, nil, cfgAgent.Config, newMergeChecker(cfgAgent.Config, &fgc{}), false)
  3303  			c := &syncController{
  3304  				config:   cfgAgent.Config,
  3305  				provider: ghProvider,
  3306  				changedFiles: &changedFilesAgent{
  3307  					provider:        ghProvider,
  3308  					changeCache:     tc.initialChangeCache,
  3309  					nextChangeCache: make(map[changeCacheKey][]string),
  3310  				},
  3311  				logger: log,
  3312  			}
  3313  			presubmits, err := c.presubmitsByPull(sp)
  3314  			if err != nil {
  3315  				t.Fatalf("unexpected error from presubmitsByPull: %v", err)
  3316  			}
  3317  			c.changedFiles.prune()
  3318  			// for equality we need to clear the compiled regexes
  3319  			for _, jobs := range presubmits {
  3320  				config.ClearCompiledRegexes(jobs)
  3321  			}
  3322  			if !apiequality.Semantic.DeepEqual(presubmits, tc.expectedPresubmits) {
  3323  				t.Errorf("got incorrect presubmit mapping: %v\n", diff.ObjectReflectDiff(tc.expectedPresubmits, presubmits))
  3324  			}
  3325  			if got := c.changedFiles.changeCache; !reflect.DeepEqual(got, tc.expectedChangeCache) {
  3326  				t.Errorf("got incorrect file change cache: %v", diff.ObjectReflectDiff(tc.expectedChangeCache, got))
  3327  			}
  3328  		})
  3329  	}
  3330  }
  3331  
  3332  func getTemplate(name, tplStr string) *template.Template {
  3333  	tpl, _ := template.New(name).Parse(tplStr)
  3334  	return tpl
  3335  }
  3336  
  3337  func TestAccumulateReturnsCorrectMissingTests(t *testing.T) {
  3338  	const baseSHA = "8d287a3aeae90fd0aef4a70009c715712ff302cd"
  3339  
  3340  	testCases := []struct {
  3341  		name               string
  3342  		presubmits         map[int][]config.Presubmit
  3343  		prs                []PullRequest
  3344  		pjs                []prowapi.ProwJob
  3345  		expectedPresubmits map[int][]config.Presubmit
  3346  	}{
  3347  		{
  3348  			name: "All presubmits missing, no changes",
  3349  			prs: []PullRequest{{
  3350  				Number:     1,
  3351  				HeadRefOID: "sha",
  3352  			}},
  3353  			presubmits: map[int][]config.Presubmit{1: {{
  3354  				Reporter: config.Reporter{
  3355  					Context: "my-presubmit",
  3356  				},
  3357  			}}},
  3358  			expectedPresubmits: map[int][]config.Presubmit{
  3359  				1: {{Reporter: config.Reporter{Context: "my-presubmit"}}},
  3360  			},
  3361  		},
  3362  		{
  3363  			name: "All presubmits successful, no retesting needed",
  3364  			prs: []PullRequest{{
  3365  				Number:     1,
  3366  				HeadRefOID: "sha",
  3367  			}},
  3368  			pjs: []prowapi.ProwJob{{
  3369  				Spec: prowapi.ProwJobSpec{
  3370  					Type: prowapi.PresubmitJob,
  3371  					Refs: &prowapi.Refs{
  3372  						Pulls: []prowapi.Pull{{
  3373  							Number: 1,
  3374  							SHA:    "sha",
  3375  						}},
  3376  					},
  3377  					Context: "my-presubmit",
  3378  				},
  3379  				Status: prowapi.ProwJobStatus{State: prowapi.SuccessState},
  3380  			}},
  3381  			presubmits: map[int][]config.Presubmit{
  3382  				1: {{Reporter: config.Reporter{Context: "my-presubmit"}}},
  3383  			},
  3384  		},
  3385  		{
  3386  			name: "All presubmits pending, no retesting needed",
  3387  			prs: []PullRequest{{
  3388  				Number:     1,
  3389  				HeadRefOID: "sha",
  3390  			}},
  3391  			pjs: []prowapi.ProwJob{{
  3392  				Spec: prowapi.ProwJobSpec{
  3393  					Type: prowapi.PresubmitJob,
  3394  					Refs: &prowapi.Refs{
  3395  						Pulls: []prowapi.Pull{{
  3396  							Number: 1,
  3397  							SHA:    "sha",
  3398  						}},
  3399  					},
  3400  					Context: "my-presubmit",
  3401  				},
  3402  				Status: prowapi.ProwJobStatus{State: prowapi.PendingState},
  3403  			}},
  3404  			presubmits: map[int][]config.Presubmit{
  3405  				1: {{Reporter: config.Reporter{Context: "my-presubmit"}}}},
  3406  		},
  3407  		{
  3408  			name: "One successful, one pending, one missing, one failing, only missing and failing remain",
  3409  			prs: []PullRequest{{
  3410  				Number:     1,
  3411  				HeadRefOID: "sha",
  3412  			}},
  3413  			pjs: []prowapi.ProwJob{
  3414  				{
  3415  					Spec: prowapi.ProwJobSpec{
  3416  						Type: prowapi.PresubmitJob,
  3417  						Refs: &prowapi.Refs{
  3418  							Pulls: []prowapi.Pull{{
  3419  								Number: 1,
  3420  								SHA:    "sha",
  3421  							}},
  3422  						},
  3423  						Context: "my-successful-presubmit",
  3424  					},
  3425  					Status: prowapi.ProwJobStatus{State: prowapi.SuccessState},
  3426  				},
  3427  				{
  3428  					Spec: prowapi.ProwJobSpec{
  3429  						Type: prowapi.PresubmitJob,
  3430  						Refs: &prowapi.Refs{
  3431  							Pulls: []prowapi.Pull{{
  3432  								Number: 1,
  3433  								SHA:    "sha",
  3434  							}},
  3435  						},
  3436  						Context: "my-pending-presubmit",
  3437  					},
  3438  					Status: prowapi.ProwJobStatus{State: prowapi.PendingState},
  3439  				},
  3440  				{
  3441  					Spec: prowapi.ProwJobSpec{
  3442  						Type: prowapi.PresubmitJob,
  3443  						Refs: &prowapi.Refs{
  3444  							Pulls: []prowapi.Pull{{
  3445  								Number: 1,
  3446  								SHA:    "sha",
  3447  							}},
  3448  						},
  3449  						Context: "my-failing-presubmit",
  3450  					},
  3451  					Status: prowapi.ProwJobStatus{State: prowapi.FailureState},
  3452  				},
  3453  			},
  3454  			presubmits: map[int][]config.Presubmit{
  3455  				1: {
  3456  					{Reporter: config.Reporter{Context: "my-successful-presubmit"}},
  3457  					{Reporter: config.Reporter{Context: "my-pending-presubmit"}},
  3458  					{Reporter: config.Reporter{Context: "my-failing-presubmit"}},
  3459  					{Reporter: config.Reporter{Context: "my-missing-presubmit"}},
  3460  				}},
  3461  			expectedPresubmits: map[int][]config.Presubmit{
  3462  				1: {
  3463  					{Reporter: config.Reporter{Context: "my-failing-presubmit"}},
  3464  					{Reporter: config.Reporter{Context: "my-missing-presubmit"}},
  3465  				}},
  3466  		},
  3467  		{
  3468  			name: "Two prs, each with one successful, one pending, one missing, one failing, only missing and failing remain",
  3469  			prs: []PullRequest{
  3470  				{
  3471  					Number:     1,
  3472  					HeadRefOID: "sha",
  3473  				},
  3474  				{
  3475  					Number:     2,
  3476  					HeadRefOID: "sha",
  3477  				},
  3478  			},
  3479  			pjs: []prowapi.ProwJob{
  3480  				{
  3481  					Spec: prowapi.ProwJobSpec{
  3482  						Type: prowapi.PresubmitJob,
  3483  						Refs: &prowapi.Refs{
  3484  							Pulls: []prowapi.Pull{{
  3485  								Number: 1,
  3486  								SHA:    "sha",
  3487  							}},
  3488  						},
  3489  						Context: "my-successful-presubmit",
  3490  					},
  3491  					Status: prowapi.ProwJobStatus{State: prowapi.SuccessState},
  3492  				},
  3493  				{
  3494  					Spec: prowapi.ProwJobSpec{
  3495  						Type: prowapi.PresubmitJob,
  3496  						Refs: &prowapi.Refs{
  3497  							Pulls: []prowapi.Pull{{
  3498  								Number: 1,
  3499  								SHA:    "sha",
  3500  							}},
  3501  						},
  3502  						Context: "my-pending-presubmit",
  3503  					},
  3504  					Status: prowapi.ProwJobStatus{State: prowapi.PendingState},
  3505  				},
  3506  				{
  3507  					Spec: prowapi.ProwJobSpec{
  3508  						Type: prowapi.PresubmitJob,
  3509  						Refs: &prowapi.Refs{
  3510  							Pulls: []prowapi.Pull{{
  3511  								Number: 1,
  3512  								SHA:    "sha",
  3513  							}},
  3514  						},
  3515  						Context: "my-failing-presubmit",
  3516  					},
  3517  					Status: prowapi.ProwJobStatus{State: prowapi.FailureState},
  3518  				},
  3519  				{
  3520  					Spec: prowapi.ProwJobSpec{
  3521  						Type: prowapi.PresubmitJob,
  3522  						Refs: &prowapi.Refs{
  3523  							Pulls: []prowapi.Pull{{
  3524  								Number: 2,
  3525  								SHA:    "sha",
  3526  							}},
  3527  						},
  3528  						Context: "my-successful-presubmit",
  3529  					},
  3530  					Status: prowapi.ProwJobStatus{State: prowapi.SuccessState},
  3531  				},
  3532  				{
  3533  					Spec: prowapi.ProwJobSpec{
  3534  						Type: prowapi.PresubmitJob,
  3535  						Refs: &prowapi.Refs{
  3536  							Pulls: []prowapi.Pull{{
  3537  								Number: 2,
  3538  								SHA:    "sha",
  3539  							}},
  3540  						},
  3541  						Context: "my-pending-presubmit",
  3542  					},
  3543  					Status: prowapi.ProwJobStatus{State: prowapi.PendingState},
  3544  				},
  3545  				{
  3546  					Spec: prowapi.ProwJobSpec{
  3547  						Type: prowapi.PresubmitJob,
  3548  						Refs: &prowapi.Refs{
  3549  							Pulls: []prowapi.Pull{{
  3550  								Number: 2,
  3551  								SHA:    "sha",
  3552  							}},
  3553  						},
  3554  						Context: "my-failing-presubmit",
  3555  					},
  3556  					Status: prowapi.ProwJobStatus{State: prowapi.FailureState},
  3557  				},
  3558  			},
  3559  			presubmits: map[int][]config.Presubmit{
  3560  				1: {
  3561  					{Reporter: config.Reporter{Context: "my-successful-presubmit"}},
  3562  					{Reporter: config.Reporter{Context: "my-pending-presubmit"}},
  3563  					{Reporter: config.Reporter{Context: "my-failing-presubmit"}},
  3564  					{Reporter: config.Reporter{Context: "my-missing-presubmit"}},
  3565  				},
  3566  				2: {
  3567  					{Reporter: config.Reporter{Context: "my-successful-presubmit"}},
  3568  					{Reporter: config.Reporter{Context: "my-pending-presubmit"}},
  3569  					{Reporter: config.Reporter{Context: "my-failing-presubmit"}},
  3570  					{Reporter: config.Reporter{Context: "my-missing-presubmit"}},
  3571  				},
  3572  			},
  3573  			expectedPresubmits: map[int][]config.Presubmit{
  3574  				1: {
  3575  					{Reporter: config.Reporter{Context: "my-failing-presubmit"}},
  3576  					{Reporter: config.Reporter{Context: "my-missing-presubmit"}},
  3577  				},
  3578  				2: {
  3579  					{Reporter: config.Reporter{Context: "my-failing-presubmit"}},
  3580  					{Reporter: config.Reporter{Context: "my-missing-presubmit"}},
  3581  				},
  3582  			},
  3583  		},
  3584  		{
  3585  			name:       "Result from successful context gets respected",
  3586  			presubmits: map[int][]config.Presubmit{1: {{Reporter: config.Reporter{Context: "job-1"}}}},
  3587  			prs: []PullRequest{{
  3588  				Number:     1,
  3589  				HeadRefOID: "headsha",
  3590  				Commits: Commits{Nodes: []struct{ Commit Commit }{{Commit: Commit{
  3591  					OID: githubql.String("headsha"),
  3592  					Status: CommitStatus{Contexts: []Context{{
  3593  						Context:     githubql.String("job-1"),
  3594  						Description: githubql.String("Job succeeded. BaseSHA:" + baseSHA),
  3595  						State:       githubql.StatusStateSuccess,
  3596  					}}},
  3597  				}}}}}},
  3598  		},
  3599  		{
  3600  			name:       "Result from successful context gets respected with deprecated baseha delimiter",
  3601  			presubmits: map[int][]config.Presubmit{1: {{Reporter: config.Reporter{Context: "job-1"}}}},
  3602  			prs: []PullRequest{{
  3603  				Number:     1,
  3604  				HeadRefOID: "headsha",
  3605  				Commits: Commits{Nodes: []struct{ Commit Commit }{{Commit: Commit{
  3606  					OID: githubql.String("headsha"),
  3607  					Status: CommitStatus{Contexts: []Context{{
  3608  						Context:     githubql.String("job-1"),
  3609  						Description: githubql.String("Job succeeded. Basesha:" + baseSHA),
  3610  						State:       githubql.StatusStateSuccess,
  3611  					}}},
  3612  				}}}}}},
  3613  		},
  3614  		{
  3615  			name:       "Result from failed context gets ignored",
  3616  			presubmits: map[int][]config.Presubmit{1: {{Reporter: config.Reporter{Context: "job-1"}}}},
  3617  			prs: []PullRequest{{
  3618  				Number:     1,
  3619  				HeadRefOID: "headsha",
  3620  				Commits: Commits{Nodes: []struct{ Commit Commit }{{Commit: Commit{
  3621  					OID: githubql.String("headsha"),
  3622  					Status: CommitStatus{Contexts: []Context{{
  3623  						Context:     githubql.String("job-1"),
  3624  						Description: githubql.String("Job succeeded. BaseSHA:" + baseSHA),
  3625  						State:       githubql.StatusStateFailure,
  3626  					}}},
  3627  				}}}}}},
  3628  			expectedPresubmits: map[int][]config.Presubmit{1: {{Reporter: config.Reporter{Context: "job-1"}}}},
  3629  		},
  3630  	}
  3631  
  3632  	log := logrus.NewEntry(logrus.New())
  3633  	for _, tc := range testCases {
  3634  		t.Run(tc.name, func(t *testing.T) {
  3635  			var crcs []CodeReviewCommon
  3636  			for _, pr := range tc.prs {
  3637  				crc := CodeReviewCommonFromPullRequest(&pr)
  3638  				crcs = append(crcs, *crc)
  3639  			}
  3640  			syncCtrl := &syncController{
  3641  				provider: &GitHubProvider{
  3642  					ghc:    &fgc{},
  3643  					logger: log,
  3644  				},
  3645  				logger: log,
  3646  			}
  3647  			_, _, _, missingSerialTests := syncCtrl.accumulate(tc.presubmits, crcs, tc.pjs, baseSHA)
  3648  			// Apiequality treats nil slices/maps equal to a zero length slice/map, keeping us from
  3649  			// the burden of having to always initialize them
  3650  			if !apiequality.Semantic.DeepEqual(tc.expectedPresubmits, missingSerialTests) {
  3651  				t.Errorf("expected \n%v\n to be \n%v\n", missingSerialTests, tc.expectedPresubmits)
  3652  			}
  3653  		})
  3654  	}
  3655  }
  3656  
  3657  func TestPresubmitsForBatch(t *testing.T) {
  3658  	testCases := []struct {
  3659  		name                         string
  3660  		prs                          []CodeReviewCommon
  3661  		changedFiles                 *changedFilesAgent
  3662  		jobs                         []config.Presubmit
  3663  		prowYAMLGetter               config.ProwYAMLGetter
  3664  		expected                     []config.Presubmit
  3665  		requireManuallyTriggeredJobs bool
  3666  		fromBranchProtection         bool
  3667  	}{
  3668  		{
  3669  			name: "All jobs get picked",
  3670  			prs:  []CodeReviewCommon{*CodeReviewCommonFromPullRequest(getPR("org", "repo", 1))},
  3671  			jobs: []config.Presubmit{{
  3672  				AlwaysRun: true,
  3673  				Reporter:  config.Reporter{Context: "foo"},
  3674  			}},
  3675  			expected: []config.Presubmit{{
  3676  				AlwaysRun: true,
  3677  				Reporter:  config.Reporter{Context: "foo"},
  3678  			}},
  3679  		},
  3680  		{
  3681  			name: "Jobs with branchconfig get picked",
  3682  			prs:  []CodeReviewCommon{*CodeReviewCommonFromPullRequest(getPR("org", "repo", 1))},
  3683  			jobs: []config.Presubmit{{
  3684  				AlwaysRun: true,
  3685  				Reporter:  config.Reporter{Context: "foo"},
  3686  				Brancher:  config.Brancher{Branches: []string{defaultBranch}},
  3687  			}},
  3688  			expected: []config.Presubmit{{
  3689  				AlwaysRun: true,
  3690  				Reporter:  config.Reporter{Context: "foo"},
  3691  				Brancher:  config.Brancher{Branches: []string{defaultBranch}},
  3692  			}},
  3693  		},
  3694  		{
  3695  			name: "Optional jobs are excluded",
  3696  			prs:  []CodeReviewCommon{*CodeReviewCommonFromPullRequest(getPR("org", "repo", 1))},
  3697  			jobs: []config.Presubmit{
  3698  				{
  3699  					AlwaysRun: true,
  3700  					Reporter:  config.Reporter{Context: "foo"},
  3701  				},
  3702  				{
  3703  					Reporter: config.Reporter{Context: "bar"},
  3704  				},
  3705  			},
  3706  			expected: []config.Presubmit{{
  3707  				AlwaysRun: true,
  3708  				Reporter:  config.Reporter{Context: "foo"},
  3709  			}},
  3710  		},
  3711  		{
  3712  			name:                         "jobs that require manual trigger included with branch protection enabled",
  3713  			prs:                          []CodeReviewCommon{*CodeReviewCommonFromPullRequest(getPR("org", "repo", 1))},
  3714  			requireManuallyTriggeredJobs: true,
  3715  			fromBranchProtection:         true,
  3716  			jobs: []config.Presubmit{
  3717  				{
  3718  					AlwaysRun: true,
  3719  					Reporter:  config.Reporter{Context: "foo"},
  3720  				},
  3721  				{
  3722  					Reporter: config.Reporter{Context: "bar"},
  3723  				},
  3724  			},
  3725  			expected: []config.Presubmit{
  3726  				{
  3727  					AlwaysRun: true,
  3728  					Reporter:  config.Reporter{Context: "foo"},
  3729  				},
  3730  				{
  3731  					Reporter: config.Reporter{Context: "bar"},
  3732  				}},
  3733  		},
  3734  		{
  3735  			name: "jobs that require manual trigger excluded with branch protection disabled",
  3736  			prs:  []CodeReviewCommon{*CodeReviewCommonFromPullRequest(getPR("org", "repo", 1))},
  3737  			jobs: []config.Presubmit{
  3738  				{
  3739  					AlwaysRun: true,
  3740  					Reporter:  config.Reporter{Context: "foo"},
  3741  				},
  3742  				{
  3743  					Reporter: config.Reporter{Context: "bar"},
  3744  				},
  3745  			},
  3746  			expected: []config.Presubmit{{
  3747  				AlwaysRun: true,
  3748  				Reporter:  config.Reporter{Context: "foo"},
  3749  			}},
  3750  		},
  3751  		{
  3752  			name: "Jobs that are required by any of the PRs get included",
  3753  			prs: []CodeReviewCommon{
  3754  				*CodeReviewCommonFromPullRequest(getPR("org", "repo", 2)),
  3755  				*CodeReviewCommonFromPullRequest(getPR("org", "repo", 1, func(pr *PullRequest) {
  3756  					pr.HeadRefOID = githubql.String("sha")
  3757  				})),
  3758  			},
  3759  			jobs: []config.Presubmit{{
  3760  				RegexpChangeMatcher: config.RegexpChangeMatcher{
  3761  					RunIfChanged: "/very-important",
  3762  				},
  3763  				Reporter: config.Reporter{Context: "foo"},
  3764  			}},
  3765  			changedFiles: &changedFilesAgent{
  3766  				changeCache: map[changeCacheKey][]string{
  3767  					{org: "org", repo: "repo", number: 1, sha: "sha"}: {"/very-important"},
  3768  					{org: "org", repo: "repo", number: 2}:             {},
  3769  				},
  3770  				nextChangeCache: map[changeCacheKey][]string{},
  3771  			},
  3772  			expected: []config.Presubmit{{
  3773  				RegexpChangeMatcher: config.RegexpChangeMatcher{
  3774  					RunIfChanged: "/very-important",
  3775  				},
  3776  				Reporter: config.Reporter{Context: "foo"},
  3777  			}},
  3778  		},
  3779  		{
  3780  			name: "Inrepoconfig jobs get included if headref matches",
  3781  			prs: []CodeReviewCommon{
  3782  				*CodeReviewCommonFromPullRequest(getPR("org", "repo", 2, func(pr *PullRequest) {
  3783  					pr.HeadRefOID = githubql.String("sha2")
  3784  				})),
  3785  				*CodeReviewCommonFromPullRequest(getPR("org", "repo", 1, func(pr *PullRequest) {
  3786  					pr.HeadRefOID = githubql.String("sha1")
  3787  				})),
  3788  			},
  3789  			jobs: []config.Presubmit{
  3790  				{
  3791  					AlwaysRun: true,
  3792  					Reporter:  config.Reporter{Context: "foo"},
  3793  				},
  3794  			},
  3795  			prowYAMLGetter: prowYAMLGetterForHeadRefs([]string{"sha1", "sha2"}, []config.Presubmit{{
  3796  				AlwaysRun: true,
  3797  				Reporter:  config.Reporter{Context: "bar"},
  3798  			}}),
  3799  			expected: []config.Presubmit{
  3800  				{
  3801  					AlwaysRun: true,
  3802  					Reporter:  config.Reporter{Context: "foo"},
  3803  				},
  3804  				{
  3805  					AlwaysRun: true,
  3806  					Reporter:  config.Reporter{Context: "bar"},
  3807  				},
  3808  			},
  3809  		},
  3810  		{
  3811  			name: "Inrepoconfig jobs do not get included if headref doesnt match",
  3812  			prs: []CodeReviewCommon{
  3813  				*CodeReviewCommonFromPullRequest(getPR("org", "repo", 2, func(pr *PullRequest) {
  3814  					pr.HeadRefOID = githubql.String("sha2")
  3815  				})),
  3816  				*CodeReviewCommonFromPullRequest(getPR("org", "repo", 1, func(pr *PullRequest) {
  3817  					pr.HeadRefOID = githubql.String("sha1")
  3818  				})),
  3819  			},
  3820  			jobs: []config.Presubmit{
  3821  				{
  3822  					AlwaysRun: true,
  3823  					Reporter:  config.Reporter{Context: "foo"},
  3824  				},
  3825  			},
  3826  			prowYAMLGetter: prowYAMLGetterForHeadRefs([]string{"other-sha", "sha2"}, []config.Presubmit{{
  3827  				AlwaysRun: true,
  3828  				Reporter:  config.Reporter{Context: "bar"},
  3829  			}}),
  3830  			expected: []config.Presubmit{
  3831  				{
  3832  					AlwaysRun: true,
  3833  					Reporter:  config.Reporter{Context: "foo"},
  3834  				},
  3835  			},
  3836  		},
  3837  	}
  3838  
  3839  	for _, tc := range testCases {
  3840  		t.Run(tc.name, func(t *testing.T) {
  3841  
  3842  			if tc.changedFiles == nil {
  3843  				tc.changedFiles = &changedFilesAgent{
  3844  					changeCache: map[changeCacheKey][]string{},
  3845  				}
  3846  				for _, pr := range tc.prs {
  3847  					key := changeCacheKey{
  3848  						org:    pr.Org,
  3849  						repo:   pr.Repo,
  3850  						number: int(pr.Number),
  3851  						sha:    string(pr.HeadRefOID),
  3852  					}
  3853  					tc.changedFiles.changeCache[key] = []string{}
  3854  				}
  3855  			}
  3856  
  3857  			if err := config.SetPresubmitRegexes(tc.jobs); err != nil {
  3858  				t.Fatalf("failed to set presubmit regexes: %v", err)
  3859  			}
  3860  
  3861  			inrepoconfig := config.InRepoConfig{}
  3862  			if tc.prowYAMLGetter != nil {
  3863  				inrepoconfig.Enabled = map[string]*bool{"*": utilpointer.Bool(true)}
  3864  			}
  3865  			cfg := func() *config.Config {
  3866  				return &config.Config{
  3867  					JobConfig: config.JobConfig{
  3868  						PresubmitsStatic: map[string][]config.Presubmit{
  3869  							"org/repo": tc.jobs,
  3870  						},
  3871  						ProwYAMLGetterWithDefaults: tc.prowYAMLGetter,
  3872  					},
  3873  					ProwConfig: config.ProwConfig{
  3874  						InRepoConfig: inrepoconfig,
  3875  						BranchProtection: config.BranchProtection{
  3876  							Orgs: map[string]config.Org{
  3877  								"org": {
  3878  									Policy: config.Policy{
  3879  										RequireManuallyTriggeredJobs: &tc.requireManuallyTriggeredJobs,
  3880  									},
  3881  								},
  3882  							},
  3883  						},
  3884  						Tide: config.Tide{
  3885  							TideGitHubConfig: config.TideGitHubConfig{
  3886  								ContextOptions: config.TideContextPolicyOptions{
  3887  									TideContextPolicy: config.TideContextPolicy{
  3888  										FromBranchProtection: &tc.fromBranchProtection,
  3889  									},
  3890  								},
  3891  							},
  3892  						},
  3893  					},
  3894  				}
  3895  			}
  3896  			c := &syncController{
  3897  				provider:     newGitHubProvider(logrus.WithField("test", tc.name), nil, nil, cfg, nil, false),
  3898  				changedFiles: tc.changedFiles,
  3899  				config:       cfg,
  3900  				logger:       logrus.WithField("test", tc.name),
  3901  			}
  3902  
  3903  			presubmits, err := c.presubmitsForBatch(tc.prs, "org", "repo", "baseSHA", defaultBranch)
  3904  			if err != nil {
  3905  				t.Fatalf("failed to get presubmits for batch: %v", err)
  3906  			}
  3907  			// Clear regexes, otherwise DeepEqual comparison wont work
  3908  			config.ClearCompiledRegexes(presubmits)
  3909  			if !apiequality.Semantic.DeepEqual(tc.expected, presubmits) {
  3910  				t.Errorf("returned presubmits do not match expected, diff: %v\n", diff.ObjectReflectDiff(tc.expected, presubmits))
  3911  			}
  3912  		})
  3913  	}
  3914  }
  3915  
  3916  func TestChangedFilesAgentBatchChanges(t *testing.T) {
  3917  	testCases := []struct {
  3918  		name         string
  3919  		prs          []CodeReviewCommon
  3920  		changedFiles *changedFilesAgent
  3921  		expected     []string
  3922  	}{
  3923  		{
  3924  			name: "Single PR",
  3925  			prs: []CodeReviewCommon{
  3926  				*CodeReviewCommonFromPullRequest(getPR("org", "repo", 1)),
  3927  			},
  3928  			changedFiles: &changedFilesAgent{
  3929  				changeCache: map[changeCacheKey][]string{
  3930  					{org: "org", repo: "repo", number: 1}: {"foo"},
  3931  				},
  3932  			},
  3933  			expected: []string{"foo"},
  3934  		},
  3935  		{
  3936  			name: "Multiple PRs, changes are de-duplicated",
  3937  			prs: []CodeReviewCommon{
  3938  				*CodeReviewCommonFromPullRequest(getPR("org", "repo", 1)),
  3939  				*CodeReviewCommonFromPullRequest(getPR("org", "repo", 2)),
  3940  			},
  3941  			changedFiles: &changedFilesAgent{
  3942  				changeCache: map[changeCacheKey][]string{
  3943  					{org: "org", repo: "repo", number: 1}: {"foo"},
  3944  					{org: "org", repo: "repo", number: 2}: {"foo", "bar"},
  3945  				},
  3946  			},
  3947  			expected: []string{"bar", "foo"},
  3948  		},
  3949  	}
  3950  
  3951  	for _, tc := range testCases {
  3952  		t.Run(tc.name, func(t *testing.T) {
  3953  			tc.changedFiles.nextChangeCache = map[changeCacheKey][]string{}
  3954  
  3955  			result, err := tc.changedFiles.batchChanges(tc.prs)()
  3956  			if err != nil {
  3957  				t.Fatalf("fauked to get changed files: %v", err)
  3958  			}
  3959  			if !apiequality.Semantic.DeepEqual(result, tc.expected) {
  3960  				t.Errorf("returned changes do not match expected; diff: %v\n", diff.ObjectReflectDiff(tc.expected, result))
  3961  			}
  3962  		})
  3963  	}
  3964  }
  3965  
  3966  func getPR(org, name string, number int, opts ...func(*PullRequest)) *PullRequest {
  3967  	pr := PullRequest{}
  3968  	pr.Repository.Owner.Login = githubql.String(org)
  3969  	pr.Repository.NameWithOwner = githubql.String(org + "/" + name)
  3970  	pr.Repository.Name = githubql.String(name)
  3971  	pr.Number = githubql.Int(number)
  3972  	for _, opt := range opts {
  3973  		opt(&pr)
  3974  	}
  3975  	return &pr
  3976  }
  3977  
  3978  func TestCacheIndexFuncReturnsDifferentResultsForDifferentInputs(t *testing.T) {
  3979  	type orgRepoBranch struct{ org, repo, branch string }
  3980  
  3981  	results := sets.Set[string]{}
  3982  	inputs := []orgRepoBranch{
  3983  		{"org-a", "repo-a", "branch-a"},
  3984  		{"org-a", "repo-a", "branch-b"},
  3985  		{"org-a", "repo-b", "branch-a"},
  3986  		{"org-b", "repo-a", "branch-a"},
  3987  	}
  3988  	for _, input := range inputs {
  3989  		pj := getProwJob(prowapi.PresubmitJob, input.org, input.repo, input.branch, "123", "", nil)
  3990  		idx := cacheIndexFunc(pj)
  3991  		if n := len(idx); n != 1 {
  3992  			t.Fatalf("expected to get exactly one index back, got %d", n)
  3993  		}
  3994  		if results.Has(idx[0]) {
  3995  			t.Errorf("got duplicate idx %q", idx)
  3996  		}
  3997  		results.Insert(idx[0])
  3998  	}
  3999  }
  4000  
  4001  func TestCacheIndexFunc(t *testing.T) {
  4002  	testCases := []struct {
  4003  		name           string
  4004  		prowjob        *prowapi.ProwJob
  4005  		expectedResult string
  4006  	}{
  4007  		{
  4008  			name:    "Wrong type, no result",
  4009  			prowjob: &prowapi.ProwJob{},
  4010  		},
  4011  		{
  4012  			name:    "No refs, no result",
  4013  			prowjob: getProwJob(prowapi.PresubmitJob, "", "", "", "", "", nil),
  4014  		},
  4015  		{
  4016  			name:           "presubmit job",
  4017  			prowjob:        getProwJob(prowapi.PresubmitJob, "org", "repo", "master", "123", "", nil),
  4018  			expectedResult: "org/repo:master@123",
  4019  		},
  4020  		{
  4021  			name:           "Batch job",
  4022  			prowjob:        getProwJob(prowapi.BatchJob, "org", "repo", "next", "1234", "", nil),
  4023  			expectedResult: "org/repo:next@1234",
  4024  		},
  4025  	}
  4026  
  4027  	for idx := range testCases {
  4028  		tc := testCases[idx]
  4029  		t.Run(tc.name, func(t *testing.T) {
  4030  			result := cacheIndexFunc(tc.prowjob)
  4031  			if n := len(result); n > 1 {
  4032  				t.Errorf("expected at most one result, got %d", n)
  4033  			}
  4034  
  4035  			var resultString string
  4036  			if len(result) == 1 {
  4037  				resultString = result[0]
  4038  			}
  4039  
  4040  			if resultString != tc.expectedResult {
  4041  				t.Errorf("Expected result %q, got result %q", tc.expectedResult, resultString)
  4042  			}
  4043  		})
  4044  	}
  4045  }
  4046  
  4047  func getProwJob(pjtype prowapi.ProwJobType, org, repo, branch, sha string, state prowapi.ProwJobState, pulls []prowapi.Pull) *prowapi.ProwJob {
  4048  	pj := &prowapi.ProwJob{}
  4049  	pj.Spec.Type = pjtype
  4050  	if org != "" || repo != "" || branch != "" || sha != "" {
  4051  		pj.Spec.Refs = &prowapi.Refs{
  4052  			Org:     org,
  4053  			Repo:    repo,
  4054  			BaseRef: branch,
  4055  			BaseSHA: sha,
  4056  			Pulls:   pulls,
  4057  		}
  4058  	}
  4059  	pj.Status.State = state
  4060  	return pj
  4061  }
  4062  
  4063  func newFakeManager(objs ...runtime.Object) *fakeManager {
  4064  	client := &indexingClient{
  4065  		Client:     fakectrlruntimeclient.NewClientBuilder().WithRuntimeObjects(objs...).Build(),
  4066  		indexFuncs: map[string]ctrlruntimeclient.IndexerFunc{},
  4067  	}
  4068  	return &fakeManager{
  4069  		client: client,
  4070  		fakeFieldIndexer: &fakeFieldIndexer{
  4071  			client: client,
  4072  		},
  4073  	}
  4074  }
  4075  
  4076  type fakeManager struct {
  4077  	client *indexingClient
  4078  	*fakeFieldIndexer
  4079  }
  4080  
  4081  type fakeFieldIndexer struct {
  4082  	client *indexingClient
  4083  }
  4084  
  4085  func (fi *fakeFieldIndexer) IndexField(_ context.Context, _ ctrlruntimeclient.Object, field string, extractValue ctrlruntimeclient.IndexerFunc) error {
  4086  	fi.client.indexFuncs[field] = extractValue
  4087  	return nil
  4088  }
  4089  
  4090  func (fm *fakeManager) GetClient() ctrlruntimeclient.Client {
  4091  	return fm.client
  4092  }
  4093  
  4094  func (fm *fakeManager) GetFieldIndexer() ctrlruntimeclient.FieldIndexer {
  4095  	return fm.fakeFieldIndexer
  4096  }
  4097  
  4098  type indexingClient struct {
  4099  	ctrlruntimeclient.Client
  4100  	indexFuncs map[string]ctrlruntimeclient.IndexerFunc
  4101  }
  4102  
  4103  func (c *indexingClient) List(ctx context.Context, list ctrlruntimeclient.ObjectList, opts ...ctrlruntimeclient.ListOption) error {
  4104  	if err := c.Client.List(ctx, list, opts...); err != nil {
  4105  		return err
  4106  	}
  4107  
  4108  	listOpts := &ctrlruntimeclient.ListOptions{}
  4109  	for _, opt := range opts {
  4110  		opt.ApplyToList(listOpts)
  4111  	}
  4112  
  4113  	if listOpts.FieldSelector == nil {
  4114  		return nil
  4115  	}
  4116  
  4117  	if n := len(listOpts.FieldSelector.Requirements()); n == 0 {
  4118  		return nil
  4119  	} else if n > 1 {
  4120  		return fmt.Errorf("the indexing client supports at most one field selector requirement, got %d", n)
  4121  	}
  4122  
  4123  	indexKey := listOpts.FieldSelector.Requirements()[0].Field
  4124  	if indexKey == "" {
  4125  		return nil
  4126  	}
  4127  
  4128  	indexFunc, ok := c.indexFuncs[indexKey]
  4129  	if !ok {
  4130  		return fmt.Errorf("no index with key %q found", indexKey)
  4131  	}
  4132  
  4133  	pjList, ok := list.(*prowapi.ProwJobList)
  4134  	if !ok {
  4135  		return errors.New("indexes are only supported for ProwJobLists")
  4136  	}
  4137  
  4138  	result := prowapi.ProwJobList{}
  4139  	for _, pj := range pjList.Items {
  4140  		for _, indexVal := range indexFunc(&pj) {
  4141  			logrus.Infof("indexVal: %q, requirementVal: %q, match: %t", indexVal, listOpts.FieldSelector.Requirements()[0].Value, indexVal == listOpts.FieldSelector.Requirements()[0].Value)
  4142  			if indexVal == listOpts.FieldSelector.Requirements()[0].Value {
  4143  				result.Items = append(result.Items, pj)
  4144  			}
  4145  		}
  4146  	}
  4147  
  4148  	*pjList = result
  4149  	return nil
  4150  }
  4151  
  4152  func prowYAMLGetterForHeadRefs(headRefsToLookFor []string, ps []config.Presubmit) config.ProwYAMLGetter {
  4153  	return func(_ *config.Config, _ git.ClientFactory, _, _, _ string, headRefs ...string) (*config.ProwYAML, error) {
  4154  		if len(headRefsToLookFor) != len(headRefs) {
  4155  			return nil, fmt.Errorf("expcted %d headrefs, got %d", len(headRefsToLookFor), len(headRefs))
  4156  		}
  4157  		var presubmits []config.Presubmit
  4158  		if sets.New[string](headRefsToLookFor...).Equal(sets.New[string](headRefs...)) {
  4159  			presubmits = ps
  4160  		}
  4161  		return &config.ProwYAML{
  4162  			Presubmits: presubmits,
  4163  		}, nil
  4164  	}
  4165  }
  4166  
  4167  func TestNonFailedBatchByBaseAndPullsIndexFunc(t *testing.T) {
  4168  	successFullBatchJob := func(mods ...func(*prowapi.ProwJob)) *prowapi.ProwJob {
  4169  		pj := &prowapi.ProwJob{
  4170  			Spec: prowapi.ProwJobSpec{
  4171  				Type: prowapi.BatchJob,
  4172  				Job:  "my-job",
  4173  				Refs: &prowapi.Refs{
  4174  					Org:     "org",
  4175  					Repo:    "repo",
  4176  					BaseRef: "master",
  4177  					BaseSHA: "base-sha",
  4178  					Pulls: []prowapi.Pull{
  4179  						{
  4180  							Number: 1,
  4181  							SHA:    "1",
  4182  						},
  4183  						{
  4184  							Number: 2,
  4185  							SHA:    "2",
  4186  						},
  4187  					},
  4188  				},
  4189  			},
  4190  			Status: prowapi.ProwJobStatus{
  4191  				State:          prowapi.SuccessState,
  4192  				CompletionTime: &metav1.Time{},
  4193  			},
  4194  		}
  4195  
  4196  		for _, mod := range mods {
  4197  			mod(pj)
  4198  		}
  4199  
  4200  		return pj
  4201  	}
  4202  	const defaultIndexKey = "my-job|org|repo|master|base-sha|1|1|2|2"
  4203  
  4204  	testCases := []struct {
  4205  		name     string
  4206  		pj       *prowapi.ProwJob
  4207  		expected []string
  4208  	}{
  4209  		{
  4210  			name:     "Basic success",
  4211  			pj:       successFullBatchJob(),
  4212  			expected: []string{defaultIndexKey},
  4213  		},
  4214  		{
  4215  			name: "Pulls reordered, same index",
  4216  			pj: successFullBatchJob(func(pj *prowapi.ProwJob) {
  4217  				pj.Spec.Refs.Pulls = []prowapi.Pull{
  4218  					pj.Spec.Refs.Pulls[1],
  4219  					pj.Spec.Refs.Pulls[0],
  4220  				}
  4221  			}),
  4222  			expected: []string{defaultIndexKey},
  4223  		},
  4224  		{
  4225  			name: "Not completed, state is ignored",
  4226  			pj: successFullBatchJob(func(pj *prowapi.ProwJob) {
  4227  				pj.Status.CompletionTime = nil
  4228  				pj.Status.State = prowapi.TriggeredState
  4229  			}),
  4230  			expected: []string{defaultIndexKey},
  4231  		},
  4232  		{
  4233  			name: "Different name, different index",
  4234  			pj: successFullBatchJob(func(pj *prowapi.ProwJob) {
  4235  				pj.Spec.Job = "my-other-job"
  4236  			}),
  4237  			expected: []string{"my-other-job|org|repo|master|base-sha|1|1|2|2"},
  4238  		},
  4239  		{
  4240  			name: "Not a batch, ignored",
  4241  			pj: successFullBatchJob(func(pj *prowapi.ProwJob) {
  4242  				pj.Spec.Type = prowapi.PresubmitJob
  4243  			}),
  4244  		},
  4245  		{
  4246  			name: "No refs, ignored",
  4247  			pj: successFullBatchJob(func(pj *prowapi.ProwJob) {
  4248  				pj.Spec.Refs = nil
  4249  			}),
  4250  		},
  4251  	}
  4252  
  4253  	for _, tc := range testCases {
  4254  		result := nonFailedBatchByNameBaseAndPullsIndexFunc(tc.pj)
  4255  		if diff := deep.Equal(result, tc.expected); diff != nil {
  4256  			t.Errorf("Result differs from expected, diff: %v", diff)
  4257  		}
  4258  	}
  4259  }
  4260  
  4261  func TestCheckRunNodesToContexts(t *testing.T) {
  4262  	t.Parallel()
  4263  	testCases := []struct {
  4264  		name      string
  4265  		checkRuns []CheckRun
  4266  		expected  []Context
  4267  	}{
  4268  		{
  4269  			name:      "Empty checkrun is ignored",
  4270  			checkRuns: []CheckRun{{}},
  4271  		},
  4272  		{
  4273  			name:      "Incomplete checkrun is considered pending",
  4274  			checkRuns: []CheckRun{{Name: githubql.String("some-job"), Status: githubql.String("queued")}},
  4275  			expected:  []Context{{Context: "some-job", State: githubql.StatusStatePending}},
  4276  		},
  4277  		{
  4278  			name:      "Neutral checkrun is considered success",
  4279  			checkRuns: []CheckRun{{Name: githubql.String("some-job"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String(githubql.CheckConclusionStateNeutral)}},
  4280  			expected:  []Context{{Context: "some-job", State: githubql.StatusStateSuccess}},
  4281  		},
  4282  		{
  4283  			name:      "Successful checkrun is considered success",
  4284  			checkRuns: []CheckRun{{Name: githubql.String("some-job"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String(githubql.StatusStateSuccess)}},
  4285  			expected:  []Context{{Context: "some-job", State: githubql.StatusStateSuccess}},
  4286  		},
  4287  		{
  4288  			name:      "Other checkrun conclusion is considered failure",
  4289  			checkRuns: []CheckRun{{Name: githubql.String("some-job"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: "unclear"}},
  4290  			expected:  []Context{{Context: "some-job", State: githubql.StatusStateFailure}},
  4291  		},
  4292  		{
  4293  			name: "Multiple checkruns are translated correctly",
  4294  			checkRuns: []CheckRun{
  4295  				{Name: githubql.String("some-job"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String(githubql.CheckConclusionStateNeutral)},
  4296  				{Name: githubql.String("another-job"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String(githubql.CheckConclusionStateNeutral)},
  4297  			},
  4298  			expected: []Context{
  4299  				{Context: "another-job", State: githubql.StatusStateSuccess},
  4300  				{Context: "some-job", State: githubql.StatusStateSuccess},
  4301  			},
  4302  		},
  4303  		{
  4304  			name: "De-duplicate checkruns, success > everything",
  4305  			checkRuns: []CheckRun{
  4306  				{Name: githubql.String("some-job"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String("FAILURE")},
  4307  				{Name: githubql.String("some-job"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String("ERROR")},
  4308  				{Name: githubql.String("some-job"), Status: githubql.String(githubql.CheckStatusStateCompleted)},
  4309  				{Name: githubql.String("some-job"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String(githubql.StatusStateSuccess)},
  4310  			},
  4311  			expected: []Context{
  4312  				{Context: "some-job", State: githubql.StatusStateSuccess},
  4313  			},
  4314  		},
  4315  		{
  4316  			name: "De-duplicate checkruns, neutral > everything",
  4317  			checkRuns: []CheckRun{
  4318  				{Name: githubql.String("some-job"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String("FAILURE")},
  4319  				{Name: githubql.String("some-job"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String("ERROR")},
  4320  				{Name: githubql.String("some-job"), Status: githubql.String(githubql.CheckStatusStateCompleted)},
  4321  				{Name: githubql.String("some-job"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String(githubql.CheckConclusionStateNeutral)},
  4322  			},
  4323  			expected: []Context{
  4324  				{Context: "some-job", State: githubql.StatusStateSuccess},
  4325  			},
  4326  		},
  4327  		{
  4328  			name: "De-duplicate checkruns, pending > failure",
  4329  			checkRuns: []CheckRun{
  4330  				{Name: githubql.String("some-job"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String("FAILURE")},
  4331  				{Name: githubql.String("some-job"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String("ERROR")},
  4332  				{Name: githubql.String("some-job"), Status: githubql.String(githubql.CheckStatusStateCompleted)},
  4333  				{Name: githubql.String("some-job")},
  4334  			},
  4335  			expected: []Context{
  4336  				{Context: "some-job", State: githubql.StatusStatePending},
  4337  			},
  4338  		},
  4339  		{
  4340  			name: "De-duplicate checkruns, only failures",
  4341  			checkRuns: []CheckRun{
  4342  				{Name: githubql.String("some-job"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String("FAILURE")},
  4343  				{Name: githubql.String("some-job"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String("ERROR")},
  4344  				{Name: githubql.String("some-job"), Status: githubql.String(githubql.CheckStatusStateCompleted)},
  4345  			},
  4346  			expected: []Context{
  4347  				{Context: "some-job", State: githubql.StatusStateFailure},
  4348  			},
  4349  		},
  4350  	}
  4351  
  4352  	for _, tc := range testCases {
  4353  		t.Run(tc.name, func(t *testing.T) {
  4354  			// Shuffle the checkruns to make sure we don't rely on slice order
  4355  			rand.Shuffle(len(tc.checkRuns), func(i, j int) {
  4356  				tc.checkRuns[i], tc.checkRuns[j] = tc.checkRuns[j], tc.checkRuns[i]
  4357  			})
  4358  
  4359  			var checkRunNodes []CheckRunNode
  4360  			for _, checkRun := range tc.checkRuns {
  4361  				checkRunNodes = append(checkRunNodes, CheckRunNode{CheckRun: checkRun})
  4362  			}
  4363  
  4364  			result := checkRunNodesToContexts(logrus.New().WithField("test", tc.name), checkRunNodes)
  4365  			sort.Slice(result, func(i, j int) bool {
  4366  				return result[i].Context+result[i].Description+githubql.String(result[i].State) < result[j].Context+result[j].Description+githubql.String(result[j].State)
  4367  			})
  4368  
  4369  			if diff := cmp.Diff(result, tc.expected); diff != "" {
  4370  				t.Errorf("actual result differs from expected: %s", diff)
  4371  			}
  4372  		})
  4373  	}
  4374  }
  4375  
  4376  func TestDeduplicateContestsDoesntLoseData(t *testing.T) {
  4377  	seed := time.Now().UnixNano()
  4378  	// Print the seed so failures can easily be reproduced
  4379  	t.Logf("Seed: %d", seed)
  4380  	fuzzer := fuzz.NewWithSeed(seed)
  4381  	for i := 0; i < 100; i++ {
  4382  		t.Run(strconv.Itoa(i), func(t *testing.T) {
  4383  			context := Context{}
  4384  			fuzzer.Fuzz(&context)
  4385  			res := deduplicateContexts([]Context{context})
  4386  			if diff := cmp.Diff(context, res[0]); diff != "" {
  4387  				t.Errorf("deduplicateContexts lost data, new object differs: %s", diff)
  4388  			}
  4389  		})
  4390  	}
  4391  }
  4392  
  4393  func TestPickSmallestPassingNumber(t *testing.T) {
  4394  	priorities := []config.TidePriority{
  4395  		{Labels: []string{"kind/failing-test"}},
  4396  		{Labels: []string{"area/deflake"}},
  4397  		{Labels: []string{"kind/bug", "priority/critical-urgent"}},
  4398  		{Labels: []string{"kind/feature,kind/enhancement,kind/undefined"}},
  4399  	}
  4400  	testCases := []struct {
  4401  		name     string
  4402  		prs      []CodeReviewCommon
  4403  		expected int
  4404  	}{
  4405  		{
  4406  			name: "no label",
  4407  			prs: []CodeReviewCommon{
  4408  				*CodeReviewCommonFromPullRequest(testPR("org", "repo", "A", 5, githubql.MergeableStateMergeable)),
  4409  				*CodeReviewCommonFromPullRequest(testPR("org", "repo", "A", 3, githubql.MergeableStateMergeable)),
  4410  			},
  4411  			expected: 3,
  4412  		},
  4413  		{
  4414  			name: "any of given label alternatives",
  4415  			prs: []CodeReviewCommon{
  4416  				*CodeReviewCommonFromPullRequest(testPRWithLabels("org", "repo", "A", 3, githubql.MergeableStateMergeable, []string{"kind/enhancement", "kind/undefined"})),
  4417  				*CodeReviewCommonFromPullRequest(testPRWithLabels("org", "repo", "A", 1, githubql.MergeableStateMergeable, []string{"kind/enhancement"})),
  4418  			},
  4419  			expected: 1,
  4420  		},
  4421  		{
  4422  			name: "deflake PR",
  4423  			prs: []CodeReviewCommon{
  4424  				*CodeReviewCommonFromPullRequest(testPR("org", "repo", "A", 5, githubql.MergeableStateMergeable)),
  4425  				*CodeReviewCommonFromPullRequest(testPR("org", "repo", "A", 3, githubql.MergeableStateMergeable)),
  4426  				*CodeReviewCommonFromPullRequest(testPRWithLabels("org", "repo", "A", 7, githubql.MergeableStateMergeable, []string{"area/deflake"})),
  4427  			},
  4428  			expected: 7,
  4429  		},
  4430  		{
  4431  			name: "same label",
  4432  			prs: []CodeReviewCommon{
  4433  				*CodeReviewCommonFromPullRequest(testPRWithLabels("org", "repo", "A", 7, githubql.MergeableStateMergeable, []string{"area/deflake"})),
  4434  				*CodeReviewCommonFromPullRequest(testPRWithLabels("org", "repo", "A", 6, githubql.MergeableStateMergeable, []string{"area/deflake"})),
  4435  				*CodeReviewCommonFromPullRequest(testPRWithLabels("org", "repo", "A", 1, githubql.MergeableStateMergeable, []string{"area/deflake"})),
  4436  			},
  4437  			expected: 1,
  4438  		},
  4439  		{
  4440  			name: "missing one label",
  4441  			prs: []CodeReviewCommon{
  4442  				*CodeReviewCommonFromPullRequest(testPR("org", "repo", "A", 5, githubql.MergeableStateMergeable)),
  4443  				*CodeReviewCommonFromPullRequest(testPR("org", "repo", "A", 3, githubql.MergeableStateMergeable)),
  4444  				*CodeReviewCommonFromPullRequest(testPRWithLabels("org", "repo", "A", 6, githubql.MergeableStateMergeable, []string{"kind/bug"})),
  4445  			},
  4446  			expected: 3,
  4447  		},
  4448  		{
  4449  			name: "complete",
  4450  			prs: []CodeReviewCommon{
  4451  				*CodeReviewCommonFromPullRequest(testPR("org", "repo", "A", 5, githubql.MergeableStateMergeable)),
  4452  				*CodeReviewCommonFromPullRequest(testPR("org", "repo", "A", 3, githubql.MergeableStateMergeable)),
  4453  				*CodeReviewCommonFromPullRequest(testPRWithLabels("org", "repo", "A", 6, githubql.MergeableStateMergeable, []string{"kind/bug"})),
  4454  				*CodeReviewCommonFromPullRequest(testPRWithLabels("org", "repo", "A", 7, githubql.MergeableStateMergeable, []string{"area/deflake"})),
  4455  				*CodeReviewCommonFromPullRequest(testPRWithLabels("org", "repo", "A", 8, githubql.MergeableStateMergeable, []string{"kind/bug"})),
  4456  				*CodeReviewCommonFromPullRequest(testPRWithLabels("org", "repo", "A", 9, githubql.MergeableStateMergeable, []string{"kind/failing-test"})),
  4457  				*CodeReviewCommonFromPullRequest(testPRWithLabels("org", "repo", "A", 10, githubql.MergeableStateMergeable, []string{"kind/bug", "priority/critical-urgent"})),
  4458  			},
  4459  			expected: 9,
  4460  		},
  4461  	}
  4462  	alwaysTrue := func(*logrus.Entry, *CodeReviewCommon, contextChecker) bool { return true }
  4463  	for _, tc := range testCases {
  4464  		t.Run(tc.name, func(t *testing.T) {
  4465  			_, got := pickHighestPriorityPR(nil, tc.prs, nil, alwaysTrue, priorities)
  4466  			if int(got.Number) != tc.expected {
  4467  				t.Errorf("got %d, expected %d", int(got.Number), tc.expected)
  4468  			}
  4469  		})
  4470  	}
  4471  }
  4472  
  4473  func TestQueryShardsByOrgWhenAppsAuthIsEnabledOnly(t *testing.T) {
  4474  	t.Parallel()
  4475  
  4476  	testCases := []struct {
  4477  		name                     string
  4478  		usesGitHubAppsAuth       bool
  4479  		prs                      map[string][]PullRequest
  4480  		expectedNumberOfApiCalls int
  4481  	}{
  4482  		{
  4483  			name:               "Apps auth is used, one call per org",
  4484  			usesGitHubAppsAuth: true,
  4485  			prs: map[string][]PullRequest{
  4486  				"org":       {*testPR("org", "repo", "A", 5, githubql.MergeableStateMergeable)},
  4487  				"other-org": {*testPR("other-org", "repo", "A", 5, githubql.MergeableStateMergeable)},
  4488  			},
  4489  			expectedNumberOfApiCalls: 2,
  4490  		},
  4491  		{
  4492  			name:               "Apps auth is unused, one call for all orgs",
  4493  			usesGitHubAppsAuth: false,
  4494  			prs: map[string][]PullRequest{"": {
  4495  				*testPR("org", "repo", "A", 5, githubql.MergeableStateMergeable),
  4496  				*testPR("other-org", "repo", "A", 5, githubql.MergeableStateMergeable),
  4497  			}},
  4498  			expectedNumberOfApiCalls: 1,
  4499  		},
  4500  	}
  4501  
  4502  	for _, tc := range testCases {
  4503  		t.Run(tc.name, func(t *testing.T) {
  4504  			provider := &GitHubProvider{
  4505  				cfg: func() *config.Config {
  4506  					return &config.Config{ProwConfig: config.ProwConfig{Tide: config.Tide{
  4507  						TideGitHubConfig: config.TideGitHubConfig{Queries: []config.TideQuery{{Orgs: []string{"org", "other-org"}}}}}}}
  4508  				},
  4509  				ghc:                &fgc{prs: tc.prs},
  4510  				usesGitHubAppsAuth: tc.usesGitHubAppsAuth,
  4511  				logger:             logrus.WithField("test", tc.name),
  4512  			}
  4513  
  4514  			prs, err := provider.Query()
  4515  			if err != nil {
  4516  				t.Fatalf("query() failed: %v", err)
  4517  			}
  4518  			if n := len(prs); n != 2 {
  4519  				t.Errorf("expected to get two prs back, got %d", n)
  4520  			}
  4521  			if diff := cmp.Diff(tc.expectedNumberOfApiCalls, provider.ghc.(*fgc).queryCalls); diff != "" {
  4522  				t.Errorf("expectedNumberOfApiCallsByOrg differs from actual: %s", diff)
  4523  			}
  4524  		})
  4525  	}
  4526  }
  4527  
  4528  func TestPickBatchPrefersBatchesWithPreexistingJobs(t *testing.T) {
  4529  	t.Parallel()
  4530  	const org, repo = "org", "repo"
  4531  	tests := []struct {
  4532  		name                         string
  4533  		subpool                      func(*subpool)
  4534  		prsFailingContextCheck       sets.Set[int]
  4535  		maxBatchSize                 int
  4536  		prioritizeExistingBatchesMap map[string]bool
  4537  
  4538  		expectedPullRequests []CodeReviewCommon
  4539  	}{
  4540  		{
  4541  			name:    "No pre-existing jobs, new batch is picked",
  4542  			subpool: func(sp *subpool) { sp.pjs = nil },
  4543  			expectedPullRequests: []CodeReviewCommon{
  4544  				*CodeReviewCommonFromPullRequest(&PullRequest{Number: 99, HeadRefOID: "pr-from-new-batch-func"}),
  4545  			},
  4546  		},
  4547  		{
  4548  			name:    "Batch with pre-existing success jobs exists and is picked",
  4549  			subpool: func(sp *subpool) {},
  4550  			expectedPullRequests: []CodeReviewCommon{
  4551  				*CodeReviewCommonFromPullRequest(&PullRequest{
  4552  					Number:     githubql.Int(1),
  4553  					HeadRefOID: githubql.String("1"),
  4554  					Commits: struct{ Nodes []struct{ Commit Commit } }{
  4555  						Nodes: []struct{ Commit Commit }{
  4556  							{Commit: Commit{Status: CommitStatus{Contexts: []Context{}}, OID: "1"}}},
  4557  					},
  4558  				}),
  4559  				*CodeReviewCommonFromPullRequest(&PullRequest{
  4560  					Number:     githubql.Int(2),
  4561  					HeadRefOID: githubql.String("2"),
  4562  					Commits: struct{ Nodes []struct{ Commit Commit } }{
  4563  						Nodes: []struct{ Commit Commit }{
  4564  							{Commit: Commit{Status: CommitStatus{Contexts: []Context{}}, OID: "2"}}},
  4565  					},
  4566  				}),
  4567  				*CodeReviewCommonFromPullRequest(&PullRequest{
  4568  					Number:     githubql.Int(3),
  4569  					HeadRefOID: githubql.String("3"),
  4570  					Commits: struct{ Nodes []struct{ Commit Commit } }{
  4571  						Nodes: []struct{ Commit Commit }{
  4572  							{Commit: Commit{Status: CommitStatus{Contexts: []Context{}}, OID: "3"}}},
  4573  					},
  4574  				}),
  4575  				*CodeReviewCommonFromPullRequest(&PullRequest{
  4576  					Number:     githubql.Int(4),
  4577  					HeadRefOID: githubql.String("4"),
  4578  					Commits: struct{ Nodes []struct{ Commit Commit } }{
  4579  						Nodes: []struct{ Commit Commit }{
  4580  							{Commit: Commit{Status: CommitStatus{Contexts: []Context{}}, OID: "4"}}},
  4581  					},
  4582  				}),
  4583  			},
  4584  		},
  4585  		{
  4586  			name:                         "Batch with pre-existing success jobs exists but PrioritizeExistingBatches is disabled globally, new batch is picked",
  4587  			subpool:                      func(sp *subpool) {},
  4588  			prioritizeExistingBatchesMap: map[string]bool{"*": false},
  4589  			expectedPullRequests: []CodeReviewCommon{
  4590  				*CodeReviewCommonFromPullRequest(&PullRequest{Number: 99, HeadRefOID: "pr-from-new-batch-func"}),
  4591  			},
  4592  		},
  4593  		{
  4594  			name:                         "Batch with pre-existing success jobs exists but PrioritizeExistingBatches is disabled for org, new batch is picked",
  4595  			subpool:                      func(sp *subpool) {},
  4596  			prioritizeExistingBatchesMap: map[string]bool{org: false},
  4597  			expectedPullRequests: []CodeReviewCommon{
  4598  				*CodeReviewCommonFromPullRequest(&PullRequest{Number: 99, HeadRefOID: "pr-from-new-batch-func"}),
  4599  			},
  4600  		},
  4601  		{
  4602  			name:                         "Batch with pre-existing success jobs exists but PrioritizeExistingBatches is disabled for repo, new batch is picked",
  4603  			subpool:                      func(sp *subpool) {},
  4604  			prioritizeExistingBatchesMap: map[string]bool{org + "/" + repo: false},
  4605  			expectedPullRequests: []CodeReviewCommon{
  4606  				*CodeReviewCommonFromPullRequest(&PullRequest{Number: 99, HeadRefOID: "pr-from-new-batch-func"}),
  4607  			},
  4608  		},
  4609  		{
  4610  			name:                   "Batch with pre-existing success job exists but one fails context check, new batch is picked",
  4611  			subpool:                func(sp *subpool) {},
  4612  			prsFailingContextCheck: sets.New[int](1),
  4613  			expectedPullRequests: []CodeReviewCommon{
  4614  				*CodeReviewCommonFromPullRequest(&PullRequest{Number: 99, HeadRefOID: "pr-from-new-batch-func"}),
  4615  			},
  4616  		},
  4617  		{
  4618  			name:         "Batch with pre-existing success job exists but is bigger than maxBatchSize, new batch is picked",
  4619  			subpool:      func(sp *subpool) {},
  4620  			maxBatchSize: 3,
  4621  			expectedPullRequests: []CodeReviewCommon{
  4622  				*CodeReviewCommonFromPullRequest(&PullRequest{Number: 99, HeadRefOID: "pr-from-new-batch-func"}),
  4623  			},
  4624  		},
  4625  		{
  4626  			name:    "Batch with pre-existing success job exists but one PR is outdated, new batch is picked",
  4627  			subpool: func(sp *subpool) { sp.prs[0].HeadRefOID = "new-sha" },
  4628  			expectedPullRequests: []CodeReviewCommon{
  4629  				*CodeReviewCommonFromPullRequest(&PullRequest{Number: 99, HeadRefOID: "pr-from-new-batch-func"}),
  4630  			},
  4631  		},
  4632  		{
  4633  			name:    "Batchjobs exist but is failed, new batch is picked",
  4634  			subpool: func(sp *subpool) { sp.pjs[0].Status.State = prowapi.FailureState },
  4635  			expectedPullRequests: []CodeReviewCommon{
  4636  				*CodeReviewCommonFromPullRequest(&PullRequest{Number: 99, HeadRefOID: "pr-from-new-batch-func"}),
  4637  			},
  4638  		},
  4639  		{
  4640  			name: "Batch with pre-existing success jobs and batch with pre-existing pending jobs exists, batch with success jobs is picked",
  4641  			subpool: func(sp *subpool) {
  4642  				sp.pjs = append(sp.pjs, *sp.pjs[0].DeepCopy())
  4643  				sp.pjs[0].Spec.Refs.Pulls = []prowapi.Pull{{Number: 1, SHA: "1"}, {Number: 2, SHA: "2"}}
  4644  
  4645  				sp.pjs[1].Status.State = prowapi.PendingState
  4646  				sp.pjs[1].Spec.Refs.Pulls = []prowapi.Pull{{Number: 3, SHA: "3"}, {Number: 4, SHA: "4"}}
  4647  			},
  4648  			expectedPullRequests: []CodeReviewCommon{
  4649  				*CodeReviewCommonFromPullRequest(&PullRequest{
  4650  					Number:     githubql.Int(1),
  4651  					HeadRefOID: githubql.String("1"),
  4652  					Commits: struct{ Nodes []struct{ Commit Commit } }{
  4653  						Nodes: []struct{ Commit Commit }{
  4654  							{Commit: Commit{Status: CommitStatus{Contexts: []Context{}}, OID: "1"}}},
  4655  					},
  4656  				}),
  4657  				*CodeReviewCommonFromPullRequest(&PullRequest{
  4658  					Number:     githubql.Int(2),
  4659  					HeadRefOID: githubql.String("2"),
  4660  					Commits: struct{ Nodes []struct{ Commit Commit } }{
  4661  						Nodes: []struct{ Commit Commit }{
  4662  							{Commit: Commit{Status: CommitStatus{Contexts: []Context{}}, OID: "2"}}},
  4663  					},
  4664  				}),
  4665  			},
  4666  		},
  4667  		{
  4668  			name:    "Batch with pre-existing pending jobs exists and is picked",
  4669  			subpool: func(sp *subpool) { sp.pjs[0].Status.State = prowapi.PendingState },
  4670  			expectedPullRequests: []CodeReviewCommon{
  4671  				*CodeReviewCommonFromPullRequest(&PullRequest{
  4672  					Number:     githubql.Int(1),
  4673  					HeadRefOID: githubql.String("1"),
  4674  					Commits: struct{ Nodes []struct{ Commit Commit } }{
  4675  						Nodes: []struct{ Commit Commit }{
  4676  							{Commit: Commit{Status: CommitStatus{Contexts: []Context{}}, OID: "1"}}},
  4677  					},
  4678  				}),
  4679  				*CodeReviewCommonFromPullRequest(&PullRequest{
  4680  					Number:     githubql.Int(2),
  4681  					HeadRefOID: githubql.String("2"),
  4682  					Commits: struct{ Nodes []struct{ Commit Commit } }{
  4683  						Nodes: []struct{ Commit Commit }{
  4684  							{Commit: Commit{Status: CommitStatus{Contexts: []Context{}}, OID: "2"}}},
  4685  					},
  4686  				}),
  4687  				*CodeReviewCommonFromPullRequest(&PullRequest{
  4688  					Number:     githubql.Int(3),
  4689  					HeadRefOID: githubql.String("3"),
  4690  					Commits: struct{ Nodes []struct{ Commit Commit } }{
  4691  						Nodes: []struct{ Commit Commit }{
  4692  							{Commit: Commit{Status: CommitStatus{Contexts: []Context{}}, OID: "3"}}},
  4693  					},
  4694  				}),
  4695  				*CodeReviewCommonFromPullRequest(&PullRequest{
  4696  					Number:     githubql.Int(4),
  4697  					HeadRefOID: githubql.String("4"),
  4698  					Commits: struct{ Nodes []struct{ Commit Commit } }{
  4699  						Nodes: []struct{ Commit Commit }{
  4700  							{Commit: Commit{Status: CommitStatus{Contexts: []Context{}}, OID: "4"}}},
  4701  					},
  4702  				}),
  4703  			},
  4704  		},
  4705  		{
  4706  			name: "Multiple success batches exists, the one with the highest number of tests is picked",
  4707  			subpool: func(sp *subpool) {
  4708  				sp.pjs = append(sp.pjs, *sp.pjs[0].DeepCopy(), *sp.pjs[0].DeepCopy())
  4709  
  4710  				sp.pjs[1].Spec.Refs.Pulls = []prowapi.Pull{{Number: 3, SHA: "3"}, {Number: 4, SHA: "4"}}
  4711  				sp.pjs[2].Spec.Refs.Pulls = []prowapi.Pull{{Number: 3, SHA: "3"}, {Number: 4, SHA: "4"}}
  4712  			},
  4713  			expectedPullRequests: []CodeReviewCommon{
  4714  				*CodeReviewCommonFromPullRequest(&PullRequest{
  4715  					Number:     githubql.Int(3),
  4716  					HeadRefOID: githubql.String("3"),
  4717  					Commits: struct{ Nodes []struct{ Commit Commit } }{
  4718  						Nodes: []struct{ Commit Commit }{
  4719  							{Commit: Commit{Status: CommitStatus{Contexts: []Context{}}, OID: "3"}}},
  4720  					},
  4721  				}),
  4722  				*CodeReviewCommonFromPullRequest(&PullRequest{
  4723  					Number:     githubql.Int(4),
  4724  					HeadRefOID: githubql.String("4"),
  4725  					Commits: struct{ Nodes []struct{ Commit Commit } }{
  4726  						Nodes: []struct{ Commit Commit }{
  4727  							{Commit: Commit{Status: CommitStatus{Contexts: []Context{}}, OID: "4"}}},
  4728  					},
  4729  				}),
  4730  			},
  4731  		},
  4732  		{
  4733  			name: "Multiple pending batches exist, the one with the highest number of tests is picked",
  4734  			subpool: func(sp *subpool) {
  4735  				sp.pjs[0].Status.State = prowapi.PendingState
  4736  				sp.pjs = append(sp.pjs, *sp.pjs[0].DeepCopy(), *sp.pjs[0].DeepCopy())
  4737  
  4738  				sp.pjs[1].Spec.Refs.Pulls = []prowapi.Pull{{Number: 3, SHA: "3"}, {Number: 4, SHA: "4"}}
  4739  				sp.pjs[2].Spec.Refs.Pulls = []prowapi.Pull{{Number: 3, SHA: "3"}, {Number: 4, SHA: "4"}}
  4740  			},
  4741  			expectedPullRequests: []CodeReviewCommon{
  4742  				*CodeReviewCommonFromPullRequest(&PullRequest{
  4743  					Number:     githubql.Int(3),
  4744  					HeadRefOID: githubql.String("3"),
  4745  					Commits: struct{ Nodes []struct{ Commit Commit } }{
  4746  						Nodes: []struct{ Commit Commit }{
  4747  							{Commit: Commit{Status: CommitStatus{Contexts: []Context{}}, OID: "3"}}},
  4748  					},
  4749  				}),
  4750  				*CodeReviewCommonFromPullRequest(&PullRequest{
  4751  					Number:     githubql.Int(4),
  4752  					HeadRefOID: githubql.String("4"),
  4753  					Commits: struct{ Nodes []struct{ Commit Commit } }{
  4754  						Nodes: []struct{ Commit Commit }{
  4755  							{Commit: Commit{Status: CommitStatus{Contexts: []Context{}}, OID: "4"}}},
  4756  					},
  4757  				}),
  4758  			},
  4759  		},
  4760  	}
  4761  
  4762  	for _, tc := range tests {
  4763  		t.Run(tc.name, func(t *testing.T) {
  4764  			sp := subpool{
  4765  				org:  org,
  4766  				repo: repo,
  4767  				log:  logrus.WithField("test", tc.name),
  4768  				prs: []CodeReviewCommon{
  4769  					*CodeReviewCommonFromPullRequest(&PullRequest{Number: 1, HeadRefOID: "1"}),
  4770  					*CodeReviewCommonFromPullRequest(&PullRequest{Number: 2, HeadRefOID: "2"}),
  4771  					*CodeReviewCommonFromPullRequest(&PullRequest{Number: 3, HeadRefOID: "3"}),
  4772  					*CodeReviewCommonFromPullRequest(&PullRequest{Number: 4, HeadRefOID: "4"}),
  4773  					*CodeReviewCommonFromPullRequest(&PullRequest{Number: 5, HeadRefOID: "5"}),
  4774  				},
  4775  				pjs: []prowapi.ProwJob{{
  4776  					Spec: prowapi.ProwJobSpec{
  4777  						Refs: &prowapi.Refs{Pulls: []prowapi.Pull{
  4778  							{Number: 1, SHA: "1"},
  4779  							{Number: 2, SHA: "2"},
  4780  							{Number: 3, SHA: "3"},
  4781  							{Number: 4, SHA: "4"},
  4782  						}},
  4783  						Type: prowapi.BatchJob,
  4784  					},
  4785  					Status: prowapi.ProwJobStatus{
  4786  						State: prowapi.SuccessState,
  4787  					},
  4788  				}},
  4789  			}
  4790  			tc.subpool(&sp)
  4791  
  4792  			contextCheckers := make(map[int]contextChecker, len(sp.prs))
  4793  			for _, pr := range sp.prs {
  4794  				cc := &config.TideContextPolicy{}
  4795  				if tc.prsFailingContextCheck.Has(int(pr.Number)) {
  4796  					cc.RequiredContexts = []string{"guaranteed-absent"}
  4797  				}
  4798  				contextCheckers[int(pr.Number)] = cc
  4799  			}
  4800  
  4801  			newBatchFunc := func(sp subpool, candidates []CodeReviewCommon, maxBatchSize int) ([]CodeReviewCommon, error) {
  4802  				return []CodeReviewCommon{
  4803  					*CodeReviewCommonFromPullRequest(&PullRequest{Number: 99, HeadRefOID: "pr-from-new-batch-func"})}, nil
  4804  			}
  4805  
  4806  			cfg := func() *config.Config {
  4807  				return &config.Config{ProwConfig: config.ProwConfig{
  4808  					Tide: config.Tide{
  4809  						BatchSizeLimitMap:            map[string]int{"*": tc.maxBatchSize},
  4810  						PrioritizeExistingBatchesMap: tc.prioritizeExistingBatchesMap,
  4811  					}},
  4812  				}
  4813  			}
  4814  
  4815  			logger := logrus.WithField("test", tc.name)
  4816  			ghc := &fgc{skipExpectedShaCheck: true}
  4817  			c := &syncController{
  4818  				logger: logrus.WithField("test", tc.name),
  4819  				config: cfg,
  4820  				provider: &GitHubProvider{
  4821  					cfg:    cfg,
  4822  					logger: logger,
  4823  					ghc:    ghc,
  4824  				},
  4825  			}
  4826  			prs, _, err := c.pickBatch(sp, contextCheckers, newBatchFunc)
  4827  			if err != nil {
  4828  				t.Fatalf("pickBatch failed: %v", err)
  4829  			}
  4830  			if diff := cmp.Diff(tc.expectedPullRequests, prs); diff != "" {
  4831  				t.Errorf("expected pull requests differ from actual: %s", diff)
  4832  			}
  4833  		})
  4834  
  4835  	}
  4836  }
  4837  
  4838  func TestTenantIDs(t *testing.T) {
  4839  	tests := []struct {
  4840  		name     string
  4841  		pjs      []prowapi.ProwJob
  4842  		expected []string
  4843  	}{
  4844  		{
  4845  			name:     "no PJs",
  4846  			pjs:      []prowapi.ProwJob{},
  4847  			expected: []string{},
  4848  		},
  4849  		{
  4850  			name: "one PJ",
  4851  			pjs: []prowapi.ProwJob{
  4852  				{
  4853  					Spec: prowapi.ProwJobSpec{
  4854  						ProwJobDefault: &prowapi.ProwJobDefault{
  4855  							TenantID: "test",
  4856  						},
  4857  					},
  4858  				},
  4859  			},
  4860  			expected: []string{"test"},
  4861  		},
  4862  		{
  4863  			name: "multiple PJs with same ID",
  4864  			pjs: []prowapi.ProwJob{
  4865  				{
  4866  					Spec: prowapi.ProwJobSpec{
  4867  						ProwJobDefault: &prowapi.ProwJobDefault{
  4868  							TenantID: "test",
  4869  						},
  4870  					},
  4871  				},
  4872  				{
  4873  					Spec: prowapi.ProwJobSpec{
  4874  						ProwJobDefault: &prowapi.ProwJobDefault{
  4875  							TenantID: "test",
  4876  						},
  4877  					},
  4878  				},
  4879  			},
  4880  			expected: []string{"test"},
  4881  		},
  4882  		{
  4883  			name: "multiple PJs with different ID",
  4884  			pjs: []prowapi.ProwJob{
  4885  				{
  4886  					Spec: prowapi.ProwJobSpec{
  4887  						ProwJobDefault: &prowapi.ProwJobDefault{
  4888  							TenantID: "test",
  4889  						},
  4890  					},
  4891  				},
  4892  				{
  4893  					Spec: prowapi.ProwJobSpec{
  4894  						ProwJobDefault: &prowapi.ProwJobDefault{
  4895  							TenantID: "other",
  4896  						},
  4897  					},
  4898  				},
  4899  			},
  4900  			expected: []string{"test", "other"},
  4901  		},
  4902  		{
  4903  			name: "no tenantID in prowJob",
  4904  			pjs: []prowapi.ProwJob{
  4905  				{
  4906  					Spec: prowapi.ProwJobSpec{
  4907  						ProwJobDefault: &prowapi.ProwJobDefault{
  4908  							TenantID: "test",
  4909  						},
  4910  					},
  4911  				},
  4912  				{
  4913  					Spec: prowapi.ProwJobSpec{
  4914  						ProwJobDefault: &prowapi.ProwJobDefault{},
  4915  					},
  4916  				},
  4917  			},
  4918  			expected: []string{"test", ""},
  4919  		},
  4920  		{
  4921  			name: "no pjDefault in prowJob",
  4922  			pjs: []prowapi.ProwJob{
  4923  				{
  4924  					Spec: prowapi.ProwJobSpec{
  4925  						ProwJobDefault: &prowapi.ProwJobDefault{
  4926  							TenantID: "test",
  4927  						},
  4928  					},
  4929  				},
  4930  				{
  4931  					Spec: prowapi.ProwJobSpec{},
  4932  				},
  4933  			},
  4934  			expected: []string{"test", ""},
  4935  		},
  4936  		{
  4937  			name: "multiple no tenant PJs",
  4938  			pjs: []prowapi.ProwJob{
  4939  				{
  4940  					Spec: prowapi.ProwJobSpec{
  4941  						ProwJobDefault: &prowapi.ProwJobDefault{
  4942  							TenantID: "",
  4943  						},
  4944  					},
  4945  				},
  4946  				{
  4947  					Spec: prowapi.ProwJobSpec{},
  4948  				},
  4949  			},
  4950  			expected: []string{""},
  4951  		},
  4952  	}
  4953  	for _, tc := range tests {
  4954  		t.Run(tc.name, func(t *testing.T) {
  4955  			sp := subpool{pjs: tc.pjs}
  4956  			if diff := cmp.Diff(tc.expected, sp.TenantIDs(), cmpopts.SortSlices(func(x, y string) bool { return strings.Compare(x, y) > 0 })); diff != "" {
  4957  				t.Errorf("expected tenantIDs differ from actual: %s", diff)
  4958  			}
  4959  		})
  4960  	}
  4961  }
  4962  
  4963  func TestSetTideStatusSuccess(t *testing.T) {
  4964  	t.Parallel()
  4965  	testCases := []struct {
  4966  		name string
  4967  		pr   PullRequest
  4968  
  4969  		expectApiCall bool
  4970  	}{
  4971  		{
  4972  			name:          "Status is set",
  4973  			expectApiCall: true,
  4974  		},
  4975  		{
  4976  			name: "PR already has tide status set to success, no api call is made",
  4977  			pr:   PullRequest{Commits: struct{ Nodes []struct{ Commit Commit } }{Nodes: []struct{ Commit Commit }{{Commit: Commit{Status: CommitStatus{Contexts: []Context{{Context: "tide", State: githubql.StatusState("success")}}}}}}}},
  4978  		},
  4979  	}
  4980  
  4981  	for _, tc := range testCases {
  4982  		t.Run(tc.name, func(t *testing.T) {
  4983  			ghc := &fgc{}
  4984  			crc := CodeReviewCommonFromPullRequest(&tc.pr)
  4985  			err := setTideStatusSuccess(*crc, ghc, &config.Config{}, logrus.WithField("test", tc.name))
  4986  			if err != nil {
  4987  				t.Fatalf("failed to set status: %v", err)
  4988  			}
  4989  
  4990  			if ghc.setStatus != tc.expectApiCall {
  4991  				t.Errorf("expected CreateStatusApiCall: %t, got CreateStatusApiCall: %t", tc.expectApiCall, ghc.setStatus)
  4992  			}
  4993  		})
  4994  	}
  4995  }
  4996  
  4997  // TestBatchPickingConsidersPRThatIsCurrentlyBeingSeriallyRetested verifies the following sequence of events:
  4998  // 1. Tide creates a serial retest run for a passing PR
  4999  // 2. The status contexts on the PR get updated to pending
  5000  // 3. A second PR becomes eligible
  5001  // 4. Tide creates a batch of the first and the second PR
  5002  func TestBatchPickingConsidersPRThatIsCurrentlyBeingSeriallyRetested(t *testing.T) {
  5003  	t.Parallel()
  5004  	configGetter := func() *config.Config {
  5005  		return &config.Config{
  5006  			ProwConfig: config.ProwConfig{
  5007  				Tide: config.Tide{
  5008  					MaxGoroutines: 1,
  5009  					TideGitHubConfig: config.TideGitHubConfig{
  5010  						Queries: config.TideQueries{{}},
  5011  					},
  5012  				},
  5013  			},
  5014  			JobConfig: config.JobConfig{PresubmitsStatic: map[string][]config.Presubmit{
  5015  				"/": {{AlwaysRun: true, Reporter: config.Reporter{Context: "mandatory-job"}}},
  5016  			}},
  5017  		}
  5018  	}
  5019  	ghc := &fgc{}
  5020  	mmc := newMergeChecker(configGetter, ghc)
  5021  	mgr := newFakeManager()
  5022  	log := logrus.WithField("test", t.Name())
  5023  	history, err := history.New(1, nil, "")
  5024  	if err != nil {
  5025  		t.Fatalf("failed to construct history: %v", err)
  5026  	}
  5027  	ghProvider := newGitHubProvider(log, ghc, nil, configGetter, mmc, false)
  5028  	c, err := newSyncController(
  5029  		context.Background(),
  5030  		log,
  5031  		mgr,
  5032  		ghProvider,
  5033  		configGetter,
  5034  		nil,
  5035  		history,
  5036  		false,
  5037  		&statusUpdate{
  5038  			dontUpdateStatus: &threadSafePRSet{},
  5039  			newPoolPending:   make(chan bool),
  5040  		},
  5041  	)
  5042  	if err != nil {
  5043  		t.Fatalf("failed to construct sync controller: %v", err)
  5044  	}
  5045  	c.pickNewBatch = func(sp subpool, candidates []CodeReviewCommon, maxBatchSize int) ([]CodeReviewCommon, error) {
  5046  		return candidates, nil
  5047  	}
  5048  
  5049  	// Add a successful PR to github
  5050  	initialPR := PullRequest{}
  5051  	initialPR.Commits.Nodes = append(initialPR.Commits.Nodes, struct{ Commit Commit }{
  5052  		Commit: Commit{Status: CommitStatus{Contexts: []Context{
  5053  			{
  5054  				Context: githubql.String("mandatory-job"),
  5055  				State:   githubql.StatusStateSuccess,
  5056  			},
  5057  			{
  5058  				Context: githubql.String(statusContext),
  5059  				State:   githubql.StatusStatePending,
  5060  			},
  5061  		}}},
  5062  	})
  5063  	ghc.prs = map[string][]PullRequest{"": {initialPR}}
  5064  
  5065  	// sync, this creates a new serial retest prowjob
  5066  	if err := c.Sync(); err != nil {
  5067  		t.Fatalf("sync failed: %v", err)
  5068  	}
  5069  	// Ensure there is actually the retest job
  5070  	var pjs prowapi.ProwJobList
  5071  	if err := c.prowJobClient.List(c.ctx, &pjs); err != nil {
  5072  		t.Fatalf("failed to list prowjobs: %v", err)
  5073  	}
  5074  	if n := len(pjs.Items); n != 1 {
  5075  		t.Fatalf("expected a prowjob to be created, but client had %d items", n)
  5076  	}
  5077  
  5078  	// Update the context on the PR to pending just like crier would
  5079  	for idx, ctx := range initialPR.Commits.Nodes[0].Commit.Status.Contexts {
  5080  		if pjs.Items[0].Spec.Context == string(ctx.Context) {
  5081  			initialPR.Commits.Nodes[0].Commit.Status.Contexts[idx].State = githubql.StatusStatePending
  5082  		}
  5083  	}
  5084  
  5085  	// Add a second PR that also needs retesting to GitHub
  5086  	secondPR := PullRequest{Number: githubql.Int(1)}
  5087  	secondPR.Commits.Nodes = append(secondPR.Commits.Nodes, struct{ Commit Commit }{
  5088  		Commit: Commit{Status: CommitStatus{Contexts: []Context{
  5089  			{
  5090  				Context: githubql.String("mandatory-job"),
  5091  				State:   githubql.StatusStateSuccess,
  5092  			},
  5093  			{
  5094  				Context: githubql.String(statusContext),
  5095  				State:   githubql.StatusStatePending,
  5096  			},
  5097  		}}},
  5098  	})
  5099  	ghc.prs[""] = append(ghc.prs[""], secondPR)
  5100  
  5101  	// sync again
  5102  	if err := c.Sync(); err != nil {
  5103  		t.Fatalf("failed to sync: %v", err)
  5104  	}
  5105  
  5106  	// verify we have a batch prowjob
  5107  	if err := c.prowJobClient.List(c.ctx, &pjs); err != nil {
  5108  		t.Fatalf("failed to list prowjobs: %v", err)
  5109  	}
  5110  	for _, pj := range pjs.Items {
  5111  		if pj.Spec.Type == prowapi.BatchJob {
  5112  			return
  5113  		}
  5114  	}
  5115  
  5116  	t.Errorf("expected to find a batch prwjob, but wasn't the case. ProwJobs: %+v", pjs.Items)
  5117  }
  5118  
  5119  func TestIsBatchCandidateEligible(t *testing.T) {
  5120  	t.Parallel()
  5121  
  5122  	const (
  5123  		requiredContextName = "required-context"
  5124  		optionalContextName = "optional-context"
  5125  	)
  5126  
  5127  	tcs := []struct {
  5128  		name          string
  5129  		pjManipulator func(**prowapi.ProwJob)
  5130  		prManipulator func(*PullRequest)
  5131  
  5132  		expected bool
  5133  	}{
  5134  		{
  5135  			name:     "Is eligible",
  5136  			expected: true,
  5137  		},
  5138  		{
  5139  			name: "Successful context doesn't require prowjob",
  5140  			prManipulator: func(pr *PullRequest) {
  5141  				pr.Commits.Nodes[0].Commit.Status.Contexts[0].State = githubql.StatusStateSuccess
  5142  			},
  5143  			pjManipulator: func(pj **prowapi.ProwJob) { *pj = nil },
  5144  			expected:      true,
  5145  		},
  5146  		{
  5147  			name:     "Optional failed context is ignored",
  5148  			expected: true,
  5149  			prManipulator: func(pr *PullRequest) {
  5150  				pr.Commits.Nodes[0].Commit.Status.Contexts = append(pr.Commits.Nodes[0].Commit.Status.Contexts, Context{
  5151  					Context: githubql.String(optionalContextName),
  5152  					State:   githubql.StatusStateFailure,
  5153  				})
  5154  			},
  5155  		},
  5156  		{
  5157  			name:     "Tides own context is ignored",
  5158  			expected: true,
  5159  			prManipulator: func(pr *PullRequest) {
  5160  				pr.Commits.Nodes[0].Commit.Status.Contexts = append(pr.Commits.Nodes[0].Commit.Status.Contexts, Context{
  5161  					Context: githubql.String(statusContext),
  5162  					State:   githubql.StatusStateFailure,
  5163  				})
  5164  			},
  5165  		},
  5166  		{
  5167  			name:          "Has missing required context, not eligible",
  5168  			prManipulator: func(pr *PullRequest) { pr.Commits.Nodes[0].Commit.Status.Contexts = nil },
  5169  		},
  5170  		{
  5171  			name: "Has failed context, not eligible",
  5172  			prManipulator: func(pr *PullRequest) {
  5173  				pr.Commits.Nodes[0].Commit.Status.Contexts[0].State = githubql.StatusStateFailure
  5174  			},
  5175  		},
  5176  		{
  5177  			name: "Has error context, not eligible",
  5178  			prManipulator: func(pr *PullRequest) {
  5179  				pr.Commits.Nodes[0].Commit.Status.Contexts[0].State = githubql.StatusStateError
  5180  			},
  5181  		},
  5182  		{
  5183  			name:          "No prowjob, not eligible",
  5184  			pjManipulator: func(pj **prowapi.ProwJob) { *pj = nil },
  5185  		},
  5186  		{
  5187  			name:          "Pj doesn't have created by tide label, not eligible",
  5188  			pjManipulator: func(pj **prowapi.ProwJob) { (*pj).Labels["created-by-tide"] = "wrong" },
  5189  		},
  5190  		{
  5191  			name:          "Pj doesn't have presubmit label, not eligible",
  5192  			pjManipulator: func(pj **prowapi.ProwJob) { (*pj).Labels[kube.ProwJobTypeLabel] = "wrong" },
  5193  		},
  5194  		{
  5195  			name:          "PJ doesn't have org label, not eligible",
  5196  			pjManipulator: func(pj **prowapi.ProwJob) { (*pj).Labels[kube.OrgLabel] = "wrong" },
  5197  		},
  5198  		{
  5199  			name:          "PJ doesn't have repo label, not eligible",
  5200  			pjManipulator: func(pj **prowapi.ProwJob) { (*pj).Labels[kube.RepoLabel] = "wrong" },
  5201  		},
  5202  		{
  5203  			name:          "Pj doesn't have baseref label, not eligible",
  5204  			pjManipulator: func(pj **prowapi.ProwJob) { (*pj).Labels[kube.BaseRefLabel] = "wrong" },
  5205  		},
  5206  		{
  5207  			name:          "Pj doesn't have pull label, not eligible",
  5208  			pjManipulator: func(pj **prowapi.ProwJob) { (*pj).Labels[kube.PullLabel] = "wrong" },
  5209  		},
  5210  		{
  5211  			name:          "pj doesn't have context label, not eligible",
  5212  			pjManipulator: func(pj **prowapi.ProwJob) { (*pj).Labels[kube.ContextAnnotation] = "wrong" },
  5213  		},
  5214  		{
  5215  			name:          "Pj is for wrong headref, not eligible",
  5216  			pjManipulator: func(pj **prowapi.ProwJob) { (*pj).Spec.Refs.Pulls[0].SHA = "wrong" },
  5217  		},
  5218  	}
  5219  
  5220  	newPR := func() PullRequest {
  5221  		pr := PullRequest{}
  5222  		pr.Commits.Nodes = append(pr.Commits.Nodes, struct{ Commit Commit }{Commit: Commit{Status: CommitStatus{Contexts: []Context{
  5223  			{Context: githubql.String(requiredContextName), State: githubql.StatusStatePending},
  5224  		}}}})
  5225  		return pr
  5226  	}
  5227  	newProwJob := func() *prowapi.ProwJob {
  5228  		return &prowapi.ProwJob{
  5229  			ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{
  5230  				"created-by-tide":      "true",
  5231  				kube.ProwJobTypeLabel:  "presubmit",
  5232  				kube.OrgLabel:          "",
  5233  				kube.RepoLabel:         "",
  5234  				kube.BaseRefLabel:      "",
  5235  				kube.PullLabel:         "0",
  5236  				kube.ContextAnnotation: requiredContextName,
  5237  			}},
  5238  			Spec: prowapi.ProwJobSpec{Refs: &prowapi.Refs{Pulls: []prowapi.Pull{{}}}},
  5239  		}
  5240  	}
  5241  
  5242  	for _, tc := range tcs {
  5243  		t.Run(tc.name, func(t *testing.T) {
  5244  			pj := newProwJob()
  5245  			pr := newPR()
  5246  
  5247  			if tc.prManipulator != nil {
  5248  				tc.prManipulator(&pr)
  5249  			}
  5250  			if tc.pjManipulator != nil {
  5251  				tc.pjManipulator(&pj)
  5252  			}
  5253  
  5254  			builder := fakectrlruntimeclient.NewClientBuilder()
  5255  			if pj != nil {
  5256  				builder.WithRuntimeObjects(pj)
  5257  			}
  5258  
  5259  			cfg := func() *config.Config { return &config.Config{} }
  5260  			c := &syncController{
  5261  				config:        cfg,
  5262  				provider:      &GitHubProvider{cfg: cfg},
  5263  				ctx:           context.Background(),
  5264  				prowJobClient: builder.Build(),
  5265  			}
  5266  
  5267  			cc := &config.TideContextPolicy{
  5268  				RequiredContexts: []string{requiredContextName},
  5269  				OptionalContexts: []string{optionalContextName},
  5270  			}
  5271  
  5272  			if actual := c.isRetestEligible(logrus.WithField("tc", tc.name), CodeReviewCommonFromPullRequest(&pr), cc); actual != tc.expected {
  5273  				t.Errorf("expected result %t, got %t", tc.expected, actual)
  5274  			}
  5275  		})
  5276  	}
  5277  }
  5278  
  5279  // TestSerialRetestingConsidersPRThatIsCurrentlyBeingSRetested verifies the following sequence of events:
  5280  // 1. Tide creates a serial retest run for a passing PR
  5281  // 2. The status context on the PR gets updated to pending
  5282  // 3. Another PR gets merged and changed the baseSHA, for example because it already had up-to-date tests but was missing labels
  5283  // 4. Tide will again trigger serial retests for the passing PR (The runs from step 1 will be deleted by Plank)
  5284  func TestSerialRetestingConsidersPRThatIsCurrentlyBeingSRetested(t *testing.T) {
  5285  	t.Parallel()
  5286  	configGetter := func() *config.Config {
  5287  		return &config.Config{
  5288  			ProwConfig: config.ProwConfig{
  5289  				Tide: config.Tide{
  5290  					MaxGoroutines: 1,
  5291  					TideGitHubConfig: config.TideGitHubConfig{
  5292  						Queries: config.TideQueries{{}},
  5293  					},
  5294  				},
  5295  			},
  5296  			JobConfig: config.JobConfig{PresubmitsStatic: map[string][]config.Presubmit{
  5297  				"/": {{AlwaysRun: true, Reporter: config.Reporter{Context: "mandatory-job"}}},
  5298  			}},
  5299  		}
  5300  	}
  5301  	ghc := &fgc{}
  5302  	mmc := newMergeChecker(configGetter, ghc)
  5303  	mgr := newFakeManager()
  5304  	log := logrus.WithField("test", t.Name())
  5305  	history, err := history.New(1, nil, "")
  5306  	if err != nil {
  5307  		t.Fatalf("failed to construct history: %v", err)
  5308  	}
  5309  	ghProvider := newGitHubProvider(log, ghc, nil, configGetter, mmc, false)
  5310  	c, err := newSyncController(
  5311  		context.Background(),
  5312  		log,
  5313  		mgr,
  5314  		ghProvider,
  5315  		configGetter,
  5316  		nil,
  5317  		history,
  5318  		false,
  5319  		&statusUpdate{
  5320  			dontUpdateStatus: &threadSafePRSet{},
  5321  			newPoolPending:   make(chan bool),
  5322  		},
  5323  	)
  5324  	if err != nil {
  5325  		t.Fatalf("failed to construct sync controller: %v", err)
  5326  	}
  5327  
  5328  	// Add a successful PR to github
  5329  	initialPR := PullRequest{}
  5330  	initialPR.Commits.Nodes = append(initialPR.Commits.Nodes, struct{ Commit Commit }{
  5331  		Commit: Commit{Status: CommitStatus{Contexts: []Context{
  5332  			{
  5333  				Context: githubql.String("mandatory-job"),
  5334  				State:   githubql.StatusStateSuccess,
  5335  			},
  5336  			{
  5337  				Context: githubql.String(statusContext),
  5338  				State:   githubql.StatusStatePending,
  5339  			},
  5340  		}}},
  5341  	})
  5342  	ghc.prs = map[string][]PullRequest{"": {initialPR}}
  5343  
  5344  	// sync, this creates a new serial retest prowjob
  5345  	if err := c.Sync(); err != nil {
  5346  		t.Fatalf("sync failed: %v", err)
  5347  	}
  5348  	// ensure there is actually the retest job
  5349  	var pjs prowapi.ProwJobList
  5350  	if err := c.prowJobClient.List(c.ctx, &pjs); err != nil {
  5351  		t.Fatalf("failed to list prowjobs: %v", err)
  5352  	}
  5353  	if n := len(pjs.Items); n != 1 {
  5354  		t.Errorf("expected to find exactly one prowjob, got %d from list %+v", n, pjs)
  5355  	}
  5356  
  5357  	// Update the context on the PR to pending just like crier would
  5358  	for idx, ctx := range initialPR.Commits.Nodes[0].Commit.Status.Contexts {
  5359  		if pjs.Items[0].Spec.Context == string(ctx.Context) {
  5360  			initialPR.Commits.Nodes[0].Commit.Status.Contexts[idx].State = githubql.StatusStatePending
  5361  		}
  5362  	}
  5363  
  5364  	// Update the sha of the pool
  5365  	ghc.refs = map[string]string{"/ ": "new-base-sha"}
  5366  
  5367  	// sync, this creates another serial retest prowjob
  5368  	if err := c.Sync(); err != nil {
  5369  		t.Fatalf("sync failed: %v", err)
  5370  	}
  5371  
  5372  	// ensure we have the two retest prowjobs
  5373  	if err := c.prowJobClient.List(c.ctx, &pjs); err != nil {
  5374  		t.Fatalf("failed to list prowjobs: %v", err)
  5375  	}
  5376  	if n := len(pjs.Items); n != 2 {
  5377  		t.Errorf("expected to find exactly two prowjobs, got %d from list %+v", n, pjs)
  5378  	}
  5379  
  5380  }