github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/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  	"net/http"
    25  	"net/http/httptest"
    26  	"reflect"
    27  	"testing"
    28  
    29  	githubql "github.com/shurcooL/githubv4"
    30  	"github.com/sirupsen/logrus"
    31  
    32  	"k8s.io/apimachinery/pkg/util/sets"
    33  	"k8s.io/test-infra/prow/config"
    34  	"k8s.io/test-infra/prow/git/localgit"
    35  	"k8s.io/test-infra/prow/github"
    36  	"k8s.io/test-infra/prow/kube"
    37  	"k8s.io/test-infra/prow/tide/history"
    38  )
    39  
    40  func testPullsMatchList(t *testing.T, test string, actual []PullRequest, expected []int) {
    41  	if len(actual) != len(expected) {
    42  		t.Errorf("Wrong size for case %s. Got PRs %+v, wanted numbers %v.", test, actual, expected)
    43  		return
    44  	}
    45  	for _, pr := range actual {
    46  		var found bool
    47  		n1 := int(pr.Number)
    48  		for _, n2 := range expected {
    49  			if n1 == n2 {
    50  				found = true
    51  			}
    52  		}
    53  		if !found {
    54  			t.Errorf("For case %s, found PR %d but shouldn't have.", test, n1)
    55  		}
    56  	}
    57  }
    58  
    59  func TestAccumulateBatch(t *testing.T) {
    60  	jobSet := sets.NewString("foo", "bar", "baz")
    61  	type pull struct {
    62  		number int
    63  		sha    string
    64  	}
    65  	type prowjob struct {
    66  		prs   []pull
    67  		job   string
    68  		state kube.ProwJobState
    69  	}
    70  	tests := []struct {
    71  		name       string
    72  		presubmits map[int]sets.String
    73  		pulls      []pull
    74  		prowJobs   []prowjob
    75  
    76  		merges  []int
    77  		pending bool
    78  	}{
    79  		{
    80  			name: "no batches running",
    81  		},
    82  		{
    83  			name:       "batch pending",
    84  			presubmits: map[int]sets.String{1: sets.NewString("foo"), 2: sets.NewString("foo")},
    85  			pulls:      []pull{{1, "a"}, {2, "b"}},
    86  			prowJobs:   []prowjob{{job: "foo", state: kube.PendingState, prs: []pull{{1, "a"}}}},
    87  			pending:    true,
    88  		},
    89  		{
    90  			name:       "pending batch missing presubmits is ignored",
    91  			presubmits: map[int]sets.String{1: jobSet},
    92  			pulls:      []pull{{1, "a"}, {2, "b"}},
    93  			prowJobs:   []prowjob{{job: "foo", state: kube.PendingState, prs: []pull{{1, "a"}}}},
    94  		},
    95  		{
    96  			name:       "batch pending, successful previous run",
    97  			presubmits: map[int]sets.String{1: jobSet, 2: jobSet},
    98  			pulls:      []pull{{1, "a"}, {2, "b"}},
    99  			prowJobs: []prowjob{
   100  				{job: "foo", state: kube.PendingState, prs: []pull{{1, "a"}}},
   101  				{job: "bar", state: kube.SuccessState, prs: []pull{{1, "a"}}},
   102  				{job: "baz", state: kube.SuccessState, prs: []pull{{1, "a"}}},
   103  				{job: "foo", state: kube.SuccessState, prs: []pull{{2, "b"}}},
   104  				{job: "bar", state: kube.SuccessState, prs: []pull{{2, "b"}}},
   105  				{job: "baz", state: kube.SuccessState, prs: []pull{{2, "b"}}},
   106  			},
   107  			pending: true,
   108  			merges:  []int{2},
   109  		},
   110  		{
   111  			name:       "successful run",
   112  			presubmits: map[int]sets.String{1: jobSet, 2: jobSet},
   113  			pulls:      []pull{{1, "a"}, {2, "b"}},
   114  			prowJobs: []prowjob{
   115  				{job: "foo", state: kube.SuccessState, prs: []pull{{2, "b"}}},
   116  				{job: "bar", state: kube.SuccessState, prs: []pull{{2, "b"}}},
   117  				{job: "baz", state: kube.SuccessState, prs: []pull{{2, "b"}}},
   118  			},
   119  			merges: []int{2},
   120  		},
   121  		{
   122  			name:       "successful run, multiple PRs",
   123  			presubmits: map[int]sets.String{1: jobSet, 2: jobSet},
   124  			pulls:      []pull{{1, "a"}, {2, "b"}},
   125  			prowJobs: []prowjob{
   126  				{job: "foo", state: kube.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}},
   127  				{job: "bar", state: kube.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}},
   128  				{job: "baz", state: kube.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}},
   129  			},
   130  			merges: []int{1, 2},
   131  		},
   132  		{
   133  			name:       "successful run, failures in past",
   134  			presubmits: map[int]sets.String{1: jobSet, 2: jobSet},
   135  			pulls:      []pull{{1, "a"}, {2, "b"}},
   136  			prowJobs: []prowjob{
   137  				{job: "foo", state: kube.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}},
   138  				{job: "bar", state: kube.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}},
   139  				{job: "baz", state: kube.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}},
   140  				{job: "foo", state: kube.FailureState, prs: []pull{{1, "a"}, {2, "b"}}},
   141  				{job: "baz", state: kube.FailureState, prs: []pull{{1, "a"}, {2, "b"}}},
   142  				{job: "foo", state: kube.FailureState, prs: []pull{{1, "c"}, {2, "b"}}},
   143  			},
   144  			merges: []int{1, 2},
   145  		},
   146  		{
   147  			name:       "failures",
   148  			presubmits: map[int]sets.String{1: jobSet, 2: jobSet},
   149  			pulls:      []pull{{1, "a"}, {2, "b"}},
   150  			prowJobs: []prowjob{
   151  				{job: "foo", state: kube.FailureState, prs: []pull{{1, "a"}, {2, "b"}}},
   152  				{job: "bar", state: kube.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}},
   153  				{job: "baz", state: kube.FailureState, prs: []pull{{1, "a"}, {2, "b"}}},
   154  				{job: "foo", state: kube.FailureState, prs: []pull{{1, "c"}, {2, "b"}}},
   155  			},
   156  		},
   157  		{
   158  			name:       "missing job required by one PR",
   159  			presubmits: map[int]sets.String{1: jobSet, 2: jobSet.Union(sets.NewString("boo"))},
   160  			pulls:      []pull{{1, "a"}, {2, "b"}},
   161  			prowJobs: []prowjob{
   162  				{job: "foo", state: kube.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}},
   163  				{job: "bar", state: kube.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}},
   164  				{job: "baz", state: kube.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}},
   165  			},
   166  		},
   167  		{
   168  			name:       "successful run with PR that requires additional job",
   169  			presubmits: map[int]sets.String{1: jobSet, 2: jobSet.Union(sets.NewString("boo"))},
   170  			pulls:      []pull{{1, "a"}, {2, "b"}},
   171  			prowJobs: []prowjob{
   172  				{job: "foo", state: kube.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}},
   173  				{job: "bar", state: kube.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}},
   174  				{job: "baz", state: kube.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}},
   175  				{job: "boo", state: kube.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}},
   176  			},
   177  			merges: []int{1, 2},
   178  		},
   179  		{
   180  			name:    "no presubmits",
   181  			pulls:   []pull{{1, "a"}, {2, "b"}},
   182  			pending: false,
   183  		},
   184  		{
   185  			name:       "pending batch with PR that left pool, successful previous run",
   186  			presubmits: map[int]sets.String{2: jobSet},
   187  			pulls:      []pull{{2, "b"}},
   188  			prowJobs: []prowjob{
   189  				{job: "foo", state: kube.PendingState, prs: []pull{{1, "a"}}},
   190  				{job: "foo", state: kube.SuccessState, prs: []pull{{2, "b"}}},
   191  				{job: "bar", state: kube.SuccessState, prs: []pull{{2, "b"}}},
   192  				{job: "baz", state: kube.SuccessState, prs: []pull{{2, "b"}}},
   193  			},
   194  			pending: false,
   195  			merges:  []int{2},
   196  		},
   197  	}
   198  	for _, test := range tests {
   199  		var pulls []PullRequest
   200  		for _, p := range test.pulls {
   201  			pr := PullRequest{
   202  				Number:     githubql.Int(p.number),
   203  				HeadRefOID: githubql.String(p.sha),
   204  			}
   205  			pulls = append(pulls, pr)
   206  		}
   207  		var pjs []kube.ProwJob
   208  		for _, pj := range test.prowJobs {
   209  			npj := kube.ProwJob{
   210  				Spec: kube.ProwJobSpec{
   211  					Job:     pj.job,
   212  					Context: pj.job,
   213  					Type:    kube.BatchJob,
   214  					Refs:    new(kube.Refs),
   215  				},
   216  				Status: kube.ProwJobStatus{State: pj.state},
   217  			}
   218  			for _, pr := range pj.prs {
   219  				npj.Spec.Refs.Pulls = append(npj.Spec.Refs.Pulls, kube.Pull{
   220  					Number: pr.number,
   221  					SHA:    pr.sha,
   222  				})
   223  			}
   224  			pjs = append(pjs, npj)
   225  		}
   226  		merges, pending := accumulateBatch(test.presubmits, pulls, pjs, logrus.NewEntry(logrus.New()))
   227  		if (len(pending) > 0) != test.pending {
   228  			t.Errorf("For case \"%s\", got wrong pending.", test.name)
   229  		}
   230  		testPullsMatchList(t, test.name, merges, test.merges)
   231  	}
   232  }
   233  
   234  func TestAccumulate(t *testing.T) {
   235  	jobSet := sets.NewString("job1", "job2")
   236  	type prowjob struct {
   237  		prNumber int
   238  		job      string
   239  		state    kube.ProwJobState
   240  		sha      string
   241  	}
   242  	tests := []struct {
   243  		presubmits   map[int]sets.String
   244  		pullRequests map[int]string
   245  		prowJobs     []prowjob
   246  
   247  		successes []int
   248  		pendings  []int
   249  		none      []int
   250  	}{
   251  		{
   252  			pullRequests: map[int]string{1: "", 2: "", 3: "", 4: "", 5: "", 6: "", 7: ""},
   253  			presubmits: map[int]sets.String{
   254  				1: jobSet,
   255  				2: jobSet,
   256  				3: jobSet,
   257  				4: jobSet,
   258  				5: jobSet,
   259  				6: jobSet,
   260  				7: jobSet,
   261  			},
   262  			prowJobs: []prowjob{
   263  				{2, "job1", kube.PendingState, ""},
   264  				{3, "job1", kube.PendingState, ""},
   265  				{3, "job2", kube.TriggeredState, ""},
   266  				{4, "job1", kube.FailureState, ""},
   267  				{4, "job2", kube.PendingState, ""},
   268  				{5, "job1", kube.PendingState, ""},
   269  				{5, "job2", kube.FailureState, ""},
   270  				{5, "job2", kube.PendingState, ""},
   271  				{6, "job1", kube.SuccessState, ""},
   272  				{6, "job2", kube.PendingState, ""},
   273  				{7, "job1", kube.SuccessState, ""},
   274  				{7, "job2", kube.SuccessState, ""},
   275  				{7, "job1", kube.FailureState, ""},
   276  			},
   277  
   278  			successes: []int{7},
   279  			pendings:  []int{3, 5, 6},
   280  			none:      []int{1, 2, 4},
   281  		},
   282  		{
   283  			pullRequests: map[int]string{7: ""},
   284  			presubmits:   map[int]sets.String{7: sets.NewString("job1", "job2", "job3", "job4")},
   285  			prowJobs: []prowjob{
   286  				{7, "job1", kube.SuccessState, ""},
   287  				{7, "job2", kube.FailureState, ""},
   288  				{7, "job3", kube.FailureState, ""},
   289  				{7, "job4", kube.FailureState, ""},
   290  				{7, "job3", kube.FailureState, ""},
   291  				{7, "job4", kube.FailureState, ""},
   292  				{7, "job2", kube.SuccessState, ""},
   293  				{7, "job3", kube.SuccessState, ""},
   294  				{7, "job4", kube.FailureState, ""},
   295  			},
   296  
   297  			successes: []int{},
   298  			pendings:  []int{},
   299  			none:      []int{7},
   300  		},
   301  		{
   302  			pullRequests: map[int]string{7: ""},
   303  			presubmits:   map[int]sets.String{7: sets.NewString("job1", "job2", "job3", "job4")},
   304  			prowJobs: []prowjob{
   305  				{7, "job1", kube.FailureState, ""},
   306  				{7, "job2", kube.FailureState, ""},
   307  				{7, "job3", kube.FailureState, ""},
   308  				{7, "job4", kube.FailureState, ""},
   309  				{7, "job3", kube.FailureState, ""},
   310  				{7, "job4", kube.FailureState, ""},
   311  				{7, "job2", kube.FailureState, ""},
   312  				{7, "job3", kube.FailureState, ""},
   313  				{7, "job4", kube.FailureState, ""},
   314  			},
   315  
   316  			successes: []int{},
   317  			pendings:  []int{},
   318  			none:      []int{7},
   319  		},
   320  		{
   321  			pullRequests: map[int]string{7: ""},
   322  			presubmits:   map[int]sets.String{7: sets.NewString("job1", "job2", "job3", "job4")},
   323  			prowJobs: []prowjob{
   324  				{7, "job1", kube.SuccessState, ""},
   325  				{7, "job2", kube.FailureState, ""},
   326  				{7, "job3", kube.FailureState, ""},
   327  				{7, "job4", kube.FailureState, ""},
   328  				{7, "job3", kube.FailureState, ""},
   329  				{7, "job4", kube.FailureState, ""},
   330  				{7, "job2", kube.SuccessState, ""},
   331  				{7, "job3", kube.SuccessState, ""},
   332  				{7, "job4", kube.SuccessState, ""},
   333  				{7, "job1", kube.FailureState, ""},
   334  			},
   335  
   336  			successes: []int{7},
   337  			pendings:  []int{},
   338  			none:      []int{},
   339  		},
   340  		{
   341  			pullRequests: map[int]string{7: ""},
   342  			presubmits:   map[int]sets.String{7: sets.NewString("job1", "job2", "job3", "job4")},
   343  			prowJobs: []prowjob{
   344  				{7, "job1", kube.SuccessState, ""},
   345  				{7, "job2", kube.FailureState, ""},
   346  				{7, "job3", kube.FailureState, ""},
   347  				{7, "job4", kube.FailureState, ""},
   348  				{7, "job3", kube.FailureState, ""},
   349  				{7, "job4", kube.FailureState, ""},
   350  				{7, "job2", kube.SuccessState, ""},
   351  				{7, "job3", kube.SuccessState, ""},
   352  				{7, "job4", kube.PendingState, ""},
   353  				{7, "job1", kube.FailureState, ""},
   354  			},
   355  
   356  			successes: []int{},
   357  			pendings:  []int{7},
   358  			none:      []int{},
   359  		},
   360  		{
   361  			presubmits:   map[int]sets.String{7: sets.NewString("job1")},
   362  			pullRequests: map[int]string{7: "new", 8: "new"},
   363  			prowJobs: []prowjob{
   364  				{7, "job1", kube.SuccessState, "old"},
   365  				{7, "job1", kube.FailureState, "new"},
   366  				{8, "job1", kube.FailureState, "old"},
   367  				{8, "job1", kube.SuccessState, "new"},
   368  			},
   369  
   370  			successes: []int{8},
   371  			pendings:  []int{},
   372  			none:      []int{7},
   373  		},
   374  		{
   375  			pullRequests: map[int]string{7: "new", 8: "new"},
   376  			prowJobs:     []prowjob{},
   377  
   378  			successes: []int{8, 7},
   379  			pendings:  []int{},
   380  			none:      []int{},
   381  		},
   382  	}
   383  
   384  	for i, test := range tests {
   385  		var pulls []PullRequest
   386  		for num, sha := range test.pullRequests {
   387  			pulls = append(
   388  				pulls,
   389  				PullRequest{Number: githubql.Int(num), HeadRefOID: githubql.String(sha)},
   390  			)
   391  		}
   392  		var pjs []kube.ProwJob
   393  		for _, pj := range test.prowJobs {
   394  			pjs = append(pjs, kube.ProwJob{
   395  				Spec: kube.ProwJobSpec{
   396  					Job:     pj.job,
   397  					Context: pj.job,
   398  					Type:    kube.PresubmitJob,
   399  					Refs:    &kube.Refs{Pulls: []kube.Pull{{Number: pj.prNumber, SHA: pj.sha}}},
   400  				},
   401  				Status: kube.ProwJobStatus{State: pj.state},
   402  			})
   403  		}
   404  
   405  		successes, pendings, nones := accumulate(test.presubmits, pulls, pjs, logrus.NewEntry(logrus.New()))
   406  
   407  		t.Logf("test run %d", i)
   408  		testPullsMatchList(t, "successes", successes, test.successes)
   409  		testPullsMatchList(t, "pendings", pendings, test.pendings)
   410  		testPullsMatchList(t, "nones", nones, test.none)
   411  	}
   412  }
   413  
   414  type fgc struct {
   415  	prs       []PullRequest
   416  	refs      map[string]string
   417  	merged    int
   418  	setStatus bool
   419  
   420  	expectedSHA    string
   421  	combinedStatus map[string]string
   422  }
   423  
   424  func (f *fgc) GetRef(o, r, ref string) (string, error) {
   425  	return f.refs[o+"/"+r+" "+ref], nil
   426  }
   427  
   428  func (f *fgc) Query(ctx context.Context, q interface{}, vars map[string]interface{}) error {
   429  	sq, ok := q.(*searchQuery)
   430  	if !ok {
   431  		return errors.New("unexpected query type")
   432  	}
   433  	for _, pr := range f.prs {
   434  		sq.Search.Nodes = append(
   435  			sq.Search.Nodes,
   436  			struct {
   437  				PullRequest PullRequest `graphql:"... on PullRequest"`
   438  			}{PullRequest: pr},
   439  		)
   440  	}
   441  	return nil
   442  }
   443  
   444  func (f *fgc) Merge(org, repo string, number int, details github.MergeDetails) error {
   445  	if details.SHA == "uh oh" {
   446  		return errors.New("invalid sha")
   447  	}
   448  	f.merged++
   449  	return nil
   450  }
   451  
   452  func (f *fgc) CreateStatus(org, repo, ref string, s github.Status) error {
   453  	switch s.State {
   454  	case github.StatusSuccess, github.StatusError, github.StatusPending, github.StatusFailure:
   455  		f.setStatus = true
   456  		return nil
   457  	}
   458  	return fmt.Errorf("invalid 'state' value: %q", s.State)
   459  }
   460  
   461  func (f *fgc) GetCombinedStatus(org, repo, ref string) (*github.CombinedStatus, error) {
   462  	if f.expectedSHA != ref {
   463  		return nil, errors.New("bad combined status request: incorrect sha")
   464  	}
   465  	var statuses []github.Status
   466  	for c, s := range f.combinedStatus {
   467  		statuses = append(statuses, github.Status{Context: c, State: s})
   468  	}
   469  	return &github.CombinedStatus{
   470  			Statuses: statuses,
   471  		},
   472  		nil
   473  }
   474  
   475  func (f *fgc) GetPullRequestChanges(org, repo string, number int) ([]github.PullRequestChange, error) {
   476  	if number != 100 {
   477  		return nil, nil
   478  	}
   479  	return []github.PullRequestChange{
   480  			{
   481  				Filename: "CHANGED",
   482  			},
   483  		},
   484  		nil
   485  }
   486  
   487  // TestDividePool ensures that subpools returned by dividePool satisfy a few
   488  // important invariants.
   489  func TestDividePool(t *testing.T) {
   490  	testPulls := []struct {
   491  		org    string
   492  		repo   string
   493  		number int
   494  		branch string
   495  	}{
   496  		{
   497  			org:    "k",
   498  			repo:   "t-i",
   499  			number: 5,
   500  			branch: "master",
   501  		},
   502  		{
   503  			org:    "k",
   504  			repo:   "t-i",
   505  			number: 6,
   506  			branch: "master",
   507  		},
   508  		{
   509  			org:    "k",
   510  			repo:   "k",
   511  			number: 123,
   512  			branch: "master",
   513  		},
   514  		{
   515  			org:    "k",
   516  			repo:   "k",
   517  			number: 1000,
   518  			branch: "release-1.6",
   519  		},
   520  	}
   521  	testPJs := []struct {
   522  		jobType kube.ProwJobType
   523  		org     string
   524  		repo    string
   525  		baseRef string
   526  		baseSHA string
   527  	}{
   528  		{
   529  			jobType: kube.PresubmitJob,
   530  			org:     "k",
   531  			repo:    "t-i",
   532  			baseRef: "master",
   533  			baseSHA: "123",
   534  		},
   535  		{
   536  			jobType: kube.BatchJob,
   537  			org:     "k",
   538  			repo:    "t-i",
   539  			baseRef: "master",
   540  			baseSHA: "123",
   541  		},
   542  		{
   543  			jobType: kube.PeriodicJob,
   544  		},
   545  		{
   546  			jobType: kube.PresubmitJob,
   547  			org:     "k",
   548  			repo:    "t-i",
   549  			baseRef: "patch",
   550  			baseSHA: "123",
   551  		},
   552  		{
   553  			jobType: kube.PresubmitJob,
   554  			org:     "k",
   555  			repo:    "t-i",
   556  			baseRef: "master",
   557  			baseSHA: "abc",
   558  		},
   559  		{
   560  			jobType: kube.PresubmitJob,
   561  			org:     "o",
   562  			repo:    "t-i",
   563  			baseRef: "master",
   564  			baseSHA: "123",
   565  		},
   566  		{
   567  			jobType: kube.PresubmitJob,
   568  			org:     "k",
   569  			repo:    "other",
   570  			baseRef: "master",
   571  			baseSHA: "123",
   572  		},
   573  	}
   574  	fc := &fgc{
   575  		refs: map[string]string{"k/t-i heads/master": "123"},
   576  	}
   577  	c := &Controller{
   578  		ghc:    fc,
   579  		logger: logrus.WithField("component", "tide"),
   580  	}
   581  	pulls := make(map[string]PullRequest)
   582  	for _, p := range testPulls {
   583  		npr := PullRequest{Number: githubql.Int(p.number)}
   584  		npr.BaseRef.Name = githubql.String(p.branch)
   585  		npr.BaseRef.Prefix = "refs/heads/"
   586  		npr.Repository.Name = githubql.String(p.repo)
   587  		npr.Repository.Owner.Login = githubql.String(p.org)
   588  		pulls[prKey(&npr)] = npr
   589  	}
   590  	var pjs []kube.ProwJob
   591  	for _, pj := range testPJs {
   592  		pjs = append(pjs, kube.ProwJob{
   593  			Spec: kube.ProwJobSpec{
   594  				Type: pj.jobType,
   595  				Refs: &kube.Refs{
   596  					Org:     pj.org,
   597  					Repo:    pj.repo,
   598  					BaseRef: pj.baseRef,
   599  					BaseSHA: pj.baseSHA,
   600  				},
   601  			},
   602  		})
   603  	}
   604  	sps, err := c.dividePool(pulls, pjs)
   605  	if err != nil {
   606  		t.Fatalf("Error dividing pool: %v", err)
   607  	}
   608  	if len(sps) == 0 {
   609  		t.Error("No subpools.")
   610  	}
   611  	for _, sp := range sps {
   612  		name := fmt.Sprintf("%s/%s %s", sp.org, sp.repo, sp.branch)
   613  		sha := fc.refs[sp.org+"/"+sp.repo+" heads/"+sp.branch]
   614  		if sp.sha != sha {
   615  			t.Errorf("For subpool %s, got sha %s, expected %s.", name, sp.sha, sha)
   616  		}
   617  		if len(sp.prs) == 0 {
   618  			t.Errorf("Subpool %s has no PRs.", name)
   619  		}
   620  		for _, pr := range sp.prs {
   621  			if string(pr.Repository.Owner.Login) != sp.org || string(pr.Repository.Name) != sp.repo || string(pr.BaseRef.Name) != sp.branch {
   622  				t.Errorf("PR in wrong subpool. Got PR %+v in subpool %s.", pr, name)
   623  			}
   624  		}
   625  		for _, pj := range sp.pjs {
   626  			if pj.Spec.Type != kube.PresubmitJob && pj.Spec.Type != kube.BatchJob {
   627  				t.Errorf("PJ with bad type in subpool %s: %+v", name, pj)
   628  			}
   629  			if pj.Spec.Refs.Org != sp.org || pj.Spec.Refs.Repo != sp.repo || pj.Spec.Refs.BaseRef != sp.branch || pj.Spec.Refs.BaseSHA != sp.sha {
   630  				t.Errorf("PJ in wrong subpool. Got PJ %+v in subpool %s.", pj, name)
   631  			}
   632  		}
   633  	}
   634  }
   635  
   636  func TestPickBatch(t *testing.T) {
   637  	lg, gc, err := localgit.New()
   638  	if err != nil {
   639  		t.Fatalf("Error making local git: %v", err)
   640  	}
   641  	defer gc.Clean()
   642  	defer lg.Clean()
   643  	if err := lg.MakeFakeRepo("o", "r"); err != nil {
   644  		t.Fatalf("Error making fake repo: %v", err)
   645  	}
   646  	if err := lg.AddCommit("o", "r", map[string][]byte{"foo": []byte("foo")}); err != nil {
   647  		t.Fatalf("Adding initial commit: %v", err)
   648  	}
   649  	testprs := []struct {
   650  		files   map[string][]byte
   651  		success bool
   652  		number  int
   653  
   654  		included bool
   655  	}{
   656  		{
   657  			files:    map[string][]byte{"bar": []byte("ok")},
   658  			success:  true,
   659  			number:   0,
   660  			included: true,
   661  		},
   662  		{
   663  			files:    map[string][]byte{"foo": []byte("ok")},
   664  			success:  true,
   665  			number:   1,
   666  			included: true,
   667  		},
   668  		{
   669  			files:    map[string][]byte{"bar": []byte("conflicts with 0")},
   670  			success:  true,
   671  			number:   2,
   672  			included: false,
   673  		},
   674  		{
   675  			files:    map[string][]byte{"qux": []byte("ok")},
   676  			success:  false,
   677  			number:   6,
   678  			included: false,
   679  		},
   680  		{
   681  			files:    map[string][]byte{"bazel": []byte("ok")},
   682  			success:  true,
   683  			number:   7,
   684  			included: false, // batch of 5 smallest excludes this
   685  		},
   686  		{
   687  			files:    map[string][]byte{"other": []byte("ok")},
   688  			success:  true,
   689  			number:   5,
   690  			included: true,
   691  		},
   692  		{
   693  			files:    map[string][]byte{"changes": []byte("ok")},
   694  			success:  true,
   695  			number:   4,
   696  			included: true,
   697  		},
   698  		{
   699  			files:    map[string][]byte{"something": []byte("ok")},
   700  			success:  true,
   701  			number:   3,
   702  			included: true,
   703  		},
   704  	}
   705  	sp := subpool{
   706  		log:    logrus.WithField("component", "tide"),
   707  		org:    "o",
   708  		repo:   "r",
   709  		branch: "master",
   710  		sha:    "master",
   711  	}
   712  	for _, testpr := range testprs {
   713  		if err := lg.CheckoutNewBranch("o", "r", fmt.Sprintf("pr-%d", testpr.number)); err != nil {
   714  			t.Fatalf("Error checking out new branch: %v", err)
   715  		}
   716  		if err := lg.AddCommit("o", "r", testpr.files); err != nil {
   717  			t.Fatalf("Error adding commit: %v", err)
   718  		}
   719  		if err := lg.Checkout("o", "r", "master"); err != nil {
   720  			t.Fatalf("Error checking out master: %v", err)
   721  		}
   722  		oid := githubql.String(fmt.Sprintf("origin/pr-%d", testpr.number))
   723  		var pr PullRequest
   724  		pr.Number = githubql.Int(testpr.number)
   725  		pr.HeadRefOID = oid
   726  		pr.Commits.Nodes = []struct {
   727  			Commit Commit
   728  		}{{Commit: Commit{OID: oid}}}
   729  		pr.Commits.Nodes[0].Commit.Status.Contexts = append(pr.Commits.Nodes[0].Commit.Status.Contexts, Context{State: githubql.StatusStateSuccess})
   730  		if !testpr.success {
   731  			pr.Commits.Nodes[0].Commit.Status.Contexts[0].State = githubql.StatusStateFailure
   732  		}
   733  		sp.prs = append(sp.prs, pr)
   734  	}
   735  	ca := &config.Agent{}
   736  	ca.Set(&config.Config{})
   737  	c := &Controller{
   738  		logger: logrus.WithField("component", "tide"),
   739  		gc:     gc,
   740  		ca:     ca,
   741  	}
   742  	prs, err := c.pickBatch(sp, &config.TideContextPolicy{})
   743  	if err != nil {
   744  		t.Fatalf("Error from pickBatch: %v", err)
   745  	}
   746  	for _, testpr := range testprs {
   747  		var found bool
   748  		for _, pr := range prs {
   749  			if int(pr.Number) == testpr.number {
   750  				found = true
   751  				break
   752  			}
   753  		}
   754  		if found && !testpr.included {
   755  			t.Errorf("PR %d should not be picked.", testpr.number)
   756  		} else if !found && testpr.included {
   757  			t.Errorf("PR %d should be picked.", testpr.number)
   758  		}
   759  	}
   760  }
   761  
   762  type fkc struct {
   763  	createdJobs []kube.ProwJob
   764  }
   765  
   766  func (c *fkc) ListProwJobs(string) ([]kube.ProwJob, error) {
   767  	return nil, nil
   768  }
   769  
   770  func (c *fkc) CreateProwJob(pj kube.ProwJob) (kube.ProwJob, error) {
   771  	c.createdJobs = append(c.createdJobs, pj)
   772  	return pj, nil
   773  }
   774  
   775  func TestTakeAction(t *testing.T) {
   776  	// PRs 0-9 exist. All are mergable, and all are passing tests.
   777  	testcases := []struct {
   778  		name string
   779  
   780  		batchPending bool
   781  		successes    []int
   782  		pendings     []int
   783  		nones        []int
   784  		batchMerges  []int
   785  		presubmits   map[int]sets.String
   786  
   787  		merged           int
   788  		triggered        int
   789  		triggeredBatches int
   790  		action           Action
   791  	}{
   792  		{
   793  			name: "no prs to test, should do nothing",
   794  
   795  			batchPending: true,
   796  			successes:    []int{},
   797  			pendings:     []int{},
   798  			nones:        []int{},
   799  			batchMerges:  []int{},
   800  			presubmits:   map[int]sets.String{100: sets.NewString("foo", "if-changed")},
   801  
   802  			merged:    0,
   803  			triggered: 0,
   804  			action:    Wait,
   805  		},
   806  		{
   807  			name: "pending batch, pending serial, nothing to do",
   808  
   809  			batchPending: true,
   810  			successes:    []int{},
   811  			pendings:     []int{1},
   812  			nones:        []int{0, 2},
   813  			batchMerges:  []int{},
   814  			presubmits:   map[int]sets.String{100: sets.NewString("foo", "if-changed")},
   815  
   816  			merged:    0,
   817  			triggered: 0,
   818  			action:    Wait,
   819  		},
   820  		{
   821  			name: "pending batch, successful serial, nothing to do",
   822  
   823  			batchPending: true,
   824  			successes:    []int{1},
   825  			pendings:     []int{},
   826  			nones:        []int{0, 2},
   827  			batchMerges:  []int{},
   828  			presubmits:   map[int]sets.String{100: sets.NewString("foo", "if-changed")},
   829  
   830  			merged:    0,
   831  			triggered: 0,
   832  			action:    Wait,
   833  		},
   834  		{
   835  			name: "pending batch, should trigger serial",
   836  
   837  			batchPending: true,
   838  			successes:    []int{},
   839  			pendings:     []int{},
   840  			nones:        []int{0, 1, 2},
   841  			batchMerges:  []int{},
   842  			presubmits:   map[int]sets.String{100: sets.NewString("foo", "if-changed")},
   843  
   844  			merged:    0,
   845  			triggered: 1,
   846  			action:    Trigger,
   847  		},
   848  		{
   849  			name: "no pending batch, should trigger batch",
   850  
   851  			batchPending: false,
   852  			successes:    []int{},
   853  			pendings:     []int{0},
   854  			nones:        []int{1, 2, 3},
   855  			batchMerges:  []int{},
   856  			presubmits:   map[int]sets.String{100: sets.NewString("foo", "if-changed")},
   857  
   858  			merged:           0,
   859  			triggered:        1,
   860  			triggeredBatches: 1,
   861  			action:           TriggerBatch,
   862  		},
   863  		{
   864  			name: "one PR, should not trigger batch",
   865  
   866  			batchPending: false,
   867  			successes:    []int{},
   868  			pendings:     []int{},
   869  			nones:        []int{0},
   870  			batchMerges:  []int{},
   871  			presubmits:   map[int]sets.String{100: sets.NewString("foo", "if-changed")},
   872  
   873  			merged:    0,
   874  			triggered: 1,
   875  			action:    Trigger,
   876  		},
   877  		{
   878  			name: "successful PR, should merge",
   879  
   880  			batchPending: false,
   881  			successes:    []int{0},
   882  			pendings:     []int{},
   883  			nones:        []int{1, 2, 3},
   884  			batchMerges:  []int{},
   885  			presubmits:   map[int]sets.String{100: sets.NewString("foo", "if-changed")},
   886  
   887  			merged:    1,
   888  			triggered: 0,
   889  			action:    Merge,
   890  		},
   891  		{
   892  			name: "successful batch, should merge",
   893  
   894  			batchPending: false,
   895  			successes:    []int{0, 1},
   896  			pendings:     []int{2, 3},
   897  			nones:        []int{4, 5},
   898  			batchMerges:  []int{6, 7, 8},
   899  			presubmits:   map[int]sets.String{100: sets.NewString("foo", "if-changed")},
   900  
   901  			merged:    3,
   902  			triggered: 0,
   903  			action:    MergeBatch,
   904  		},
   905  		{
   906  			name: "one PR that triggers RunIfChangedJob",
   907  
   908  			batchPending: false,
   909  			successes:    []int{},
   910  			pendings:     []int{},
   911  			nones:        []int{100},
   912  			batchMerges:  []int{},
   913  			presubmits:   map[int]sets.String{100: sets.NewString("foo", "if-changed")},
   914  
   915  			merged:    0,
   916  			triggered: 2,
   917  			action:    Trigger,
   918  		},
   919  		{
   920  			name: "no presubmits, merge",
   921  
   922  			batchPending: false,
   923  			successes:    []int{5, 4},
   924  			pendings:     []int{},
   925  			nones:        []int{},
   926  			batchMerges:  []int{},
   927  
   928  			merged:    1,
   929  			triggered: 0,
   930  			action:    Merge,
   931  		},
   932  		{
   933  			name: "no presubmits, wait",
   934  
   935  			batchPending: false,
   936  			successes:    []int{},
   937  			pendings:     []int{},
   938  			nones:        []int{},
   939  			batchMerges:  []int{},
   940  
   941  			merged:    0,
   942  			triggered: 0,
   943  			action:    Wait,
   944  		},
   945  	}
   946  
   947  	for _, tc := range testcases {
   948  		ca := &config.Agent{}
   949  		cfg := &config.Config{}
   950  		if err := cfg.SetPresubmits(
   951  			map[string][]config.Presubmit{
   952  				"o/r": {
   953  					{
   954  						Context:      "foo",
   955  						Trigger:      "/test all",
   956  						RerunCommand: "/test all",
   957  						AlwaysRun:    true,
   958  					},
   959  					{
   960  						Context:      "if-changed",
   961  						Trigger:      "/test if-changed",
   962  						RerunCommand: "/test if-changed",
   963  						RegexpChangeMatcher: config.RegexpChangeMatcher{
   964  							RunIfChanged: "CHANGED",
   965  						},
   966  					},
   967  				},
   968  			},
   969  		); err != nil {
   970  			t.Fatalf("failed to set presubmits: %v", err)
   971  		}
   972  		ca.Set(cfg)
   973  		if len(tc.presubmits) > 0 {
   974  			for i := 0; i <= 8; i++ {
   975  				tc.presubmits[i] = sets.NewString("foo")
   976  			}
   977  		}
   978  		lg, gc, err := localgit.New()
   979  		if err != nil {
   980  			t.Fatalf("Error making local git: %v", err)
   981  		}
   982  		defer gc.Clean()
   983  		defer lg.Clean()
   984  		if err := lg.MakeFakeRepo("o", "r"); err != nil {
   985  			t.Fatalf("Error making fake repo: %v", err)
   986  		}
   987  		if err := lg.AddCommit("o", "r", map[string][]byte{"foo": []byte("foo")}); err != nil {
   988  			t.Fatalf("Adding initial commit: %v", err)
   989  		}
   990  
   991  		sp := subpool{
   992  			log:               logrus.WithField("component", "tide"),
   993  			presubmitContexts: tc.presubmits,
   994  			cc:                &config.TideContextPolicy{},
   995  			org:               "o",
   996  			repo:              "r",
   997  			branch:            "master",
   998  			sha:               "master",
   999  		}
  1000  		genPulls := func(nums []int) []PullRequest {
  1001  			var prs []PullRequest
  1002  			for _, i := range nums {
  1003  				if err := lg.CheckoutNewBranch("o", "r", fmt.Sprintf("pr-%d", i)); err != nil {
  1004  					t.Fatalf("Error checking out new branch: %v", err)
  1005  				}
  1006  				if err := lg.AddCommit("o", "r", map[string][]byte{fmt.Sprintf("%d", i): []byte("WOW")}); err != nil {
  1007  					t.Fatalf("Error adding commit: %v", err)
  1008  				}
  1009  				if err := lg.Checkout("o", "r", "master"); err != nil {
  1010  					t.Fatalf("Error checking out master: %v", err)
  1011  				}
  1012  				oid := githubql.String(fmt.Sprintf("origin/pr-%d", i))
  1013  				var pr PullRequest
  1014  				pr.Number = githubql.Int(i)
  1015  				pr.HeadRefOID = oid
  1016  				pr.Commits.Nodes = []struct {
  1017  					Commit Commit
  1018  				}{{Commit: Commit{OID: oid}}}
  1019  				sp.prs = append(sp.prs, pr)
  1020  				prs = append(prs, pr)
  1021  			}
  1022  			return prs
  1023  		}
  1024  		var fkc fkc
  1025  		var fgc fgc
  1026  		c := &Controller{
  1027  			logger: logrus.WithField("controller", "tide"),
  1028  			gc:     gc,
  1029  			ca:     ca,
  1030  			ghc:    &fgc,
  1031  			kc:     &fkc,
  1032  		}
  1033  		var batchPending []PullRequest
  1034  		if tc.batchPending {
  1035  			batchPending = []PullRequest{{}}
  1036  		}
  1037  		t.Logf("Test case: %s", tc.name)
  1038  		if act, _, err := c.takeAction(sp, batchPending, genPulls(tc.successes), genPulls(tc.pendings), genPulls(tc.nones), genPulls(tc.batchMerges)); err != nil {
  1039  			t.Errorf("Error in takeAction: %v", err)
  1040  			continue
  1041  		} else if act != tc.action {
  1042  			t.Errorf("Wrong action. Got %v, wanted %v.", act, tc.action)
  1043  		}
  1044  		if tc.triggered != len(fkc.createdJobs) {
  1045  			t.Errorf("Wrong number of jobs triggered. Got %d, expected %d.", len(fkc.createdJobs), tc.triggered)
  1046  		}
  1047  		if tc.merged != fgc.merged {
  1048  			t.Errorf("Wrong number of merges. Got %d, expected %d.", fgc.merged, tc.merged)
  1049  		}
  1050  		// Ensure that the correct number of batch jobs were triggered
  1051  		batches := 0
  1052  		for _, job := range fkc.createdJobs {
  1053  			if (len(job.Spec.Refs.Pulls) > 1) != (job.Spec.Type == kube.BatchJob) {
  1054  				t.Error("Found a batch job that doesn't contain multiple pull refs!")
  1055  			}
  1056  			if len(job.Spec.Refs.Pulls) > 1 {
  1057  				batches++
  1058  			}
  1059  		}
  1060  		if tc.triggeredBatches != batches {
  1061  			t.Errorf("Wrong number of batches triggered. Got %d, expected %d.", batches, tc.triggeredBatches)
  1062  		}
  1063  	}
  1064  }
  1065  
  1066  func TestServeHTTP(t *testing.T) {
  1067  	pr1 := PullRequest{}
  1068  	pr1.Commits.Nodes = append(pr1.Commits.Nodes, struct{ Commit Commit }{})
  1069  	pr1.Commits.Nodes[0].Commit.Status.Contexts = []Context{{
  1070  		Context:     githubql.String("coverage/coveralls"),
  1071  		Description: githubql.String("Coverage increased (+0.1%) to 27.599%"),
  1072  	}}
  1073  	c := &Controller{
  1074  		pools: []Pool{
  1075  			{
  1076  				MissingPRs: []PullRequest{pr1},
  1077  				Action:     Merge,
  1078  			},
  1079  		},
  1080  		History: history.New(100),
  1081  	}
  1082  	s := httptest.NewServer(c)
  1083  	defer s.Close()
  1084  	resp, err := http.Get(s.URL)
  1085  	if err != nil {
  1086  		t.Errorf("GET error: %v", err)
  1087  	}
  1088  	defer resp.Body.Close()
  1089  	var pools []Pool
  1090  	if err := json.NewDecoder(resp.Body).Decode(&pools); err != nil {
  1091  		t.Fatalf("JSON decoding error: %v", err)
  1092  	}
  1093  	if !reflect.DeepEqual(c.pools, pools) {
  1094  		t.Errorf("Received pools %v do not match original pools %v.", pools, c.pools)
  1095  	}
  1096  }
  1097  
  1098  func TestHeadContexts(t *testing.T) {
  1099  	type commitContext struct {
  1100  		// one context per commit for testing
  1101  		context string
  1102  		sha     string
  1103  	}
  1104  
  1105  	win := "win"
  1106  	lose := "lose"
  1107  	headSHA := "head"
  1108  	testCases := []struct {
  1109  		name           string
  1110  		commitContexts []commitContext
  1111  		expectAPICall  bool
  1112  	}{
  1113  		{
  1114  			name: "first commit is head",
  1115  			commitContexts: []commitContext{
  1116  				{context: win, sha: headSHA},
  1117  				{context: lose, sha: "other"},
  1118  				{context: lose, sha: "sha"},
  1119  			},
  1120  		},
  1121  		{
  1122  			name: "last commit is head",
  1123  			commitContexts: []commitContext{
  1124  				{context: lose, sha: "shaaa"},
  1125  				{context: lose, sha: "other"},
  1126  				{context: win, sha: headSHA},
  1127  			},
  1128  		},
  1129  		{
  1130  			name: "no commit is head",
  1131  			commitContexts: []commitContext{
  1132  				{context: lose, sha: "shaaa"},
  1133  				{context: lose, sha: "other"},
  1134  				{context: lose, sha: "sha"},
  1135  			},
  1136  			expectAPICall: true,
  1137  		},
  1138  	}
  1139  
  1140  	for _, tc := range testCases {
  1141  		t.Logf("Running test case %q", tc.name)
  1142  		fgc := &fgc{combinedStatus: map[string]string{win: string(githubql.StatusStateSuccess)}}
  1143  		if tc.expectAPICall {
  1144  			fgc.expectedSHA = headSHA
  1145  		}
  1146  		pr := &PullRequest{HeadRefOID: githubql.String(headSHA)}
  1147  		for _, ctx := range tc.commitContexts {
  1148  			commit := Commit{
  1149  				Status: struct{ Contexts []Context }{
  1150  					Contexts: []Context{
  1151  						{
  1152  							Context: githubql.String(ctx.context),
  1153  						},
  1154  					},
  1155  				},
  1156  				OID: githubql.String(ctx.sha),
  1157  			}
  1158  			pr.Commits.Nodes = append(pr.Commits.Nodes, struct{ Commit Commit }{commit})
  1159  		}
  1160  
  1161  		contexts, err := headContexts(logrus.WithField("component", "tide"), fgc, pr)
  1162  		if err != nil {
  1163  			t.Fatalf("Unexpected error from headContexts: %v", err)
  1164  		}
  1165  		if len(contexts) != 1 || string(contexts[0].Context) != win {
  1166  			t.Errorf("Expected exactly 1 %q context, but got: %#v", win, contexts)
  1167  		}
  1168  	}
  1169  }
  1170  
  1171  func testPR(org, repo, branch string, number int, mergeable githubql.MergeableState) PullRequest {
  1172  	pr := PullRequest{
  1173  		Number:     githubql.Int(number),
  1174  		Mergeable:  mergeable,
  1175  		HeadRefOID: githubql.String("SHA"),
  1176  	}
  1177  	pr.Repository.Owner.Login = githubql.String(org)
  1178  	pr.Repository.Name = githubql.String(repo)
  1179  	pr.Repository.NameWithOwner = githubql.String(fmt.Sprintf("%s/%s", org, repo))
  1180  	pr.BaseRef.Name = githubql.String(branch)
  1181  
  1182  	pr.Commits.Nodes = append(pr.Commits.Nodes, struct{ Commit Commit }{
  1183  		Commit{
  1184  			Status: struct{ Contexts []Context }{
  1185  				Contexts: []Context{
  1186  					{
  1187  						Context: githubql.String("context"),
  1188  						State:   githubql.StatusStateSuccess,
  1189  					},
  1190  				},
  1191  			},
  1192  			OID: githubql.String("SHA"),
  1193  		},
  1194  	})
  1195  	return pr
  1196  }
  1197  
  1198  func TestSync(t *testing.T) {
  1199  	mergeableA := testPR("org", "repo", "A", 5, githubql.MergeableStateMergeable)
  1200  	unmergeableA := testPR("org", "repo", "A", 6, githubql.MergeableStateConflicting)
  1201  	unmergeableB := testPR("org", "repo", "B", 7, githubql.MergeableStateConflicting)
  1202  	unknownA := testPR("org", "repo", "A", 8, githubql.MergeableStateUnknown)
  1203  
  1204  	testcases := []struct {
  1205  		name string
  1206  		prs  []PullRequest
  1207  
  1208  		expectedPools []Pool
  1209  	}{
  1210  		{
  1211  			name:          "no PRs",
  1212  			prs:           []PullRequest{},
  1213  			expectedPools: []Pool{},
  1214  		},
  1215  		{
  1216  			name: "1 mergeable PR",
  1217  			prs:  []PullRequest{mergeableA},
  1218  			expectedPools: []Pool{{
  1219  				Org:        "org",
  1220  				Repo:       "repo",
  1221  				Branch:     "A",
  1222  				SuccessPRs: []PullRequest{mergeableA},
  1223  				Action:     Merge,
  1224  				Target:     []PullRequest{mergeableA},
  1225  			}},
  1226  		},
  1227  		{
  1228  			name:          "1 unmergeable PR",
  1229  			prs:           []PullRequest{unmergeableA},
  1230  			expectedPools: []Pool{},
  1231  		},
  1232  		{
  1233  			name: "1 unknown PR",
  1234  			prs:  []PullRequest{unknownA},
  1235  			expectedPools: []Pool{{
  1236  				Org:        "org",
  1237  				Repo:       "repo",
  1238  				Branch:     "A",
  1239  				SuccessPRs: []PullRequest{unknownA},
  1240  				Action:     Merge,
  1241  				Target:     []PullRequest{unknownA},
  1242  			}},
  1243  		},
  1244  		{
  1245  			name: "1 mergeable, 1 unmergeable (different pools)",
  1246  			prs:  []PullRequest{mergeableA, unmergeableB},
  1247  			expectedPools: []Pool{{
  1248  				Org:        "org",
  1249  				Repo:       "repo",
  1250  				Branch:     "A",
  1251  				SuccessPRs: []PullRequest{mergeableA},
  1252  				Action:     Merge,
  1253  				Target:     []PullRequest{mergeableA},
  1254  			}},
  1255  		},
  1256  		{
  1257  			name: "1 mergeable, 1 unmergeable (same pool)",
  1258  			prs:  []PullRequest{mergeableA, unmergeableA},
  1259  			expectedPools: []Pool{{
  1260  				Org:        "org",
  1261  				Repo:       "repo",
  1262  				Branch:     "A",
  1263  				SuccessPRs: []PullRequest{mergeableA},
  1264  				Action:     Merge,
  1265  				Target:     []PullRequest{mergeableA},
  1266  			}},
  1267  		},
  1268  		{
  1269  			name: "1 mergeable PR (satisfies multiple queries)",
  1270  			prs:  []PullRequest{mergeableA, mergeableA},
  1271  			expectedPools: []Pool{{
  1272  				Org:        "org",
  1273  				Repo:       "repo",
  1274  				Branch:     "A",
  1275  				SuccessPRs: []PullRequest{mergeableA},
  1276  				Action:     Merge,
  1277  				Target:     []PullRequest{mergeableA},
  1278  			}},
  1279  		},
  1280  	}
  1281  
  1282  	for _, tc := range testcases {
  1283  		t.Logf("Starting case %q...", tc.name)
  1284  		fgc := &fgc{prs: tc.prs}
  1285  		fkc := &fkc{}
  1286  		ca := &config.Agent{}
  1287  		ca.Set(&config.Config{
  1288  			ProwConfig: config.ProwConfig{
  1289  				Tide: config.Tide{
  1290  					Queries:       []config.TideQuery{{}},
  1291  					MaxGoroutines: 4,
  1292  				},
  1293  			},
  1294  		})
  1295  		sc := &statusController{
  1296  			logger:         logrus.WithField("controller", "status-update"),
  1297  			ghc:            fgc,
  1298  			ca:             ca,
  1299  			newPoolPending: make(chan bool, 1),
  1300  			shutDown:       make(chan bool),
  1301  		}
  1302  		go sc.run()
  1303  		defer sc.shutdown()
  1304  		c := &Controller{
  1305  			ca:     ca,
  1306  			ghc:    fgc,
  1307  			kc:     fkc,
  1308  			logger: logrus.WithField("controller", "sync"),
  1309  			sc:     sc,
  1310  			changedFiles: &changedFilesAgent{
  1311  				ghc:             fgc,
  1312  				nextChangeCache: make(map[changeCacheKey][]string),
  1313  			},
  1314  			History: history.New(100),
  1315  		}
  1316  
  1317  		if err := c.Sync(); err != nil {
  1318  			t.Errorf("Unexpected error from 'Sync()': %v.", err)
  1319  			continue
  1320  		}
  1321  		if len(tc.expectedPools) != len(c.pools) {
  1322  			t.Errorf("Tide pools did not match expected. Got %#v, expected %#v.", c.pools, tc.expectedPools)
  1323  			continue
  1324  		}
  1325  		for _, expected := range tc.expectedPools {
  1326  			var match *Pool
  1327  			for i, actual := range c.pools {
  1328  				if expected.Org == actual.Org && expected.Repo == actual.Repo && expected.Branch == actual.Branch {
  1329  					match = &c.pools[i]
  1330  				}
  1331  			}
  1332  			if match == nil {
  1333  				t.Errorf("Failed to find expected pool %s/%s %s.", expected.Org, expected.Repo, expected.Branch)
  1334  			} else if !reflect.DeepEqual(*match, expected) {
  1335  				t.Errorf("Expected pool %#v does not match actual pool %#v.", expected, *match)
  1336  			}
  1337  		}
  1338  	}
  1339  }
  1340  
  1341  func TestFilterSubpool(t *testing.T) {
  1342  	presubmits := map[int]sets.String{
  1343  		1: sets.NewString("pj-a"),
  1344  		2: sets.NewString("pj-a", "pj-b"),
  1345  	}
  1346  
  1347  	trueVar := true
  1348  	cc := &config.TideContextPolicy{
  1349  		RequiredContexts:    []string{"pj-a", "pj-b", "other-a"},
  1350  		OptionalContexts:    []string{"tide", "pj-c"},
  1351  		SkipUnknownContexts: &trueVar,
  1352  	}
  1353  
  1354  	type pr struct {
  1355  		number    int
  1356  		mergeable bool
  1357  		contexts  []Context
  1358  	}
  1359  	tcs := []struct {
  1360  		name string
  1361  
  1362  		prs         []pr
  1363  		expectedPRs []int // Empty indicates no subpool should be returned.
  1364  	}{
  1365  		{
  1366  			name: "one mergeable passing PR (omitting optional context)",
  1367  			prs: []pr{
  1368  				{
  1369  					number:    1,
  1370  					mergeable: true,
  1371  					contexts: []Context{
  1372  						{
  1373  							Context: githubql.String("pj-a"),
  1374  							State:   githubql.StatusStateSuccess,
  1375  						},
  1376  						{
  1377  							Context: githubql.String("pj-b"),
  1378  							State:   githubql.StatusStateSuccess,
  1379  						},
  1380  						{
  1381  							Context: githubql.String("other-a"),
  1382  							State:   githubql.StatusStateSuccess,
  1383  						},
  1384  					},
  1385  				},
  1386  			},
  1387  			expectedPRs: []int{1},
  1388  		},
  1389  		{
  1390  			name: "one unmergeable passing PR",
  1391  			prs: []pr{
  1392  				{
  1393  					number:    1,
  1394  					mergeable: false,
  1395  					contexts: []Context{
  1396  						{
  1397  							Context: githubql.String("pj-a"),
  1398  							State:   githubql.StatusStateSuccess,
  1399  						},
  1400  						{
  1401  							Context: githubql.String("pj-b"),
  1402  							State:   githubql.StatusStateSuccess,
  1403  						},
  1404  						{
  1405  							Context: githubql.String("other-a"),
  1406  							State:   githubql.StatusStateSuccess,
  1407  						},
  1408  					},
  1409  				},
  1410  			},
  1411  			expectedPRs: []int{},
  1412  		},
  1413  		{
  1414  			name: "one mergeable PR pending non-PJ context (consider failing)",
  1415  			prs: []pr{
  1416  				{
  1417  					number:    2,
  1418  					mergeable: true,
  1419  					contexts: []Context{
  1420  						{
  1421  							Context: githubql.String("pj-a"),
  1422  							State:   githubql.StatusStateSuccess,
  1423  						},
  1424  						{
  1425  							Context: githubql.String("pj-b"),
  1426  							State:   githubql.StatusStateSuccess,
  1427  						},
  1428  						{
  1429  							Context: githubql.String("other-a"),
  1430  							State:   githubql.StatusStatePending,
  1431  						},
  1432  					},
  1433  				},
  1434  			},
  1435  			expectedPRs: []int{},
  1436  		},
  1437  		{
  1438  			name: "one mergeable PR pending PJ context (consider in pool)",
  1439  			prs: []pr{
  1440  				{
  1441  					number:    2,
  1442  					mergeable: true,
  1443  					contexts: []Context{
  1444  						{
  1445  							Context: githubql.String("pj-a"),
  1446  							State:   githubql.StatusStateSuccess,
  1447  						},
  1448  						{
  1449  							Context: githubql.String("pj-b"),
  1450  							State:   githubql.StatusStatePending,
  1451  						},
  1452  						{
  1453  							Context: githubql.String("other-a"),
  1454  							State:   githubql.StatusStateSuccess,
  1455  						},
  1456  					},
  1457  				},
  1458  			},
  1459  			expectedPRs: []int{2},
  1460  		},
  1461  		{
  1462  			name: "one mergeable PR failing PJ context (consider failing)",
  1463  			prs: []pr{
  1464  				{
  1465  					number:    2,
  1466  					mergeable: true,
  1467  					contexts: []Context{
  1468  						{
  1469  							Context: githubql.String("pj-a"),
  1470  							State:   githubql.StatusStateSuccess,
  1471  						},
  1472  						{
  1473  							Context: githubql.String("pj-b"),
  1474  							State:   githubql.StatusStateFailure,
  1475  						},
  1476  						{
  1477  							Context: githubql.String("other-a"),
  1478  							State:   githubql.StatusStateSuccess,
  1479  						},
  1480  					},
  1481  				},
  1482  			},
  1483  			expectedPRs: []int{},
  1484  		},
  1485  		{
  1486  			name: "one mergeable PR missing PJ context (consider failing)",
  1487  			prs: []pr{
  1488  				{
  1489  					number:    2,
  1490  					mergeable: true,
  1491  					contexts: []Context{
  1492  						{
  1493  							Context: githubql.String("pj-b"),
  1494  							State:   githubql.StatusStateSuccess,
  1495  						},
  1496  						{
  1497  							Context: githubql.String("other-a"),
  1498  							State:   githubql.StatusStateSuccess,
  1499  						},
  1500  					},
  1501  				},
  1502  			},
  1503  			expectedPRs: []int{},
  1504  		},
  1505  		{
  1506  			name: "one mergeable PR failing unknown context (consider in pool)",
  1507  			prs: []pr{
  1508  				{
  1509  					number:    2,
  1510  					mergeable: true,
  1511  					contexts: []Context{
  1512  						{
  1513  							Context: githubql.String("pj-a"),
  1514  							State:   githubql.StatusStateSuccess,
  1515  						},
  1516  						{
  1517  							Context: githubql.String("pj-b"),
  1518  							State:   githubql.StatusStateSuccess,
  1519  						},
  1520  						{
  1521  							Context: githubql.String("other-a"),
  1522  							State:   githubql.StatusStateSuccess,
  1523  						},
  1524  						{
  1525  							Context: githubql.String("unknown"),
  1526  							State:   githubql.StatusStateFailure,
  1527  						},
  1528  					},
  1529  				},
  1530  			},
  1531  			expectedPRs: []int{2},
  1532  		},
  1533  		{
  1534  			name: "one PR failing non-PJ required context; one PR successful (should not prune pool)",
  1535  			prs: []pr{
  1536  				{
  1537  					number:    1,
  1538  					mergeable: true,
  1539  					contexts: []Context{
  1540  						{
  1541  							Context: githubql.String("pj-a"),
  1542  							State:   githubql.StatusStateSuccess,
  1543  						},
  1544  						{
  1545  							Context: githubql.String("pj-b"),
  1546  							State:   githubql.StatusStateSuccess,
  1547  						},
  1548  						{
  1549  							Context: githubql.String("other-a"),
  1550  							State:   githubql.StatusStateFailure,
  1551  						},
  1552  					},
  1553  				},
  1554  				{
  1555  					number:    2,
  1556  					mergeable: true,
  1557  					contexts: []Context{
  1558  						{
  1559  							Context: githubql.String("pj-a"),
  1560  							State:   githubql.StatusStateSuccess,
  1561  						},
  1562  						{
  1563  							Context: githubql.String("pj-b"),
  1564  							State:   githubql.StatusStateSuccess,
  1565  						},
  1566  						{
  1567  							Context: githubql.String("other-a"),
  1568  							State:   githubql.StatusStateSuccess,
  1569  						},
  1570  						{
  1571  							Context: githubql.String("unknown"),
  1572  							State:   githubql.StatusStateSuccess,
  1573  						},
  1574  					},
  1575  				},
  1576  			},
  1577  			expectedPRs: []int{2},
  1578  		},
  1579  		{
  1580  			name: "two successful PRs",
  1581  			prs: []pr{
  1582  				{
  1583  					number:    1,
  1584  					mergeable: true,
  1585  					contexts: []Context{
  1586  						{
  1587  							Context: githubql.String("pj-a"),
  1588  							State:   githubql.StatusStateSuccess,
  1589  						},
  1590  						{
  1591  							Context: githubql.String("pj-b"),
  1592  							State:   githubql.StatusStateSuccess,
  1593  						},
  1594  						{
  1595  							Context: githubql.String("other-a"),
  1596  							State:   githubql.StatusStateSuccess,
  1597  						},
  1598  					},
  1599  				},
  1600  				{
  1601  					number:    2,
  1602  					mergeable: true,
  1603  					contexts: []Context{
  1604  						{
  1605  							Context: githubql.String("pj-a"),
  1606  							State:   githubql.StatusStateSuccess,
  1607  						},
  1608  						{
  1609  							Context: githubql.String("pj-b"),
  1610  							State:   githubql.StatusStateSuccess,
  1611  						},
  1612  						{
  1613  							Context: githubql.String("other-a"),
  1614  							State:   githubql.StatusStateSuccess,
  1615  						},
  1616  					},
  1617  				},
  1618  			},
  1619  			expectedPRs: []int{1, 2},
  1620  		},
  1621  	}
  1622  	for _, tc := range tcs {
  1623  		t.Run(tc.name, func(t *testing.T) {
  1624  			sp := &subpool{
  1625  				org:               "org",
  1626  				repo:              "repo",
  1627  				branch:            "branch",
  1628  				presubmitContexts: presubmits,
  1629  				cc:                cc,
  1630  				log:               logrus.WithFields(logrus.Fields{"org": "org", "repo": "repo", "branch": "branch"}),
  1631  			}
  1632  			for _, pull := range tc.prs {
  1633  				pr := PullRequest{
  1634  					Number: githubql.Int(pull.number),
  1635  				}
  1636  				pr.Commits.Nodes = []struct{ Commit Commit }{
  1637  					{
  1638  						Commit{
  1639  							Status: struct{ Contexts []Context }{
  1640  								Contexts: pull.contexts,
  1641  							},
  1642  						},
  1643  					},
  1644  				}
  1645  				if !pull.mergeable {
  1646  					pr.Mergeable = githubql.MergeableStateConflicting
  1647  				}
  1648  				sp.prs = append(sp.prs, pr)
  1649  			}
  1650  
  1651  			filtered := filterSubpool(nil, sp)
  1652  			if len(tc.expectedPRs) == 0 {
  1653  				if filtered != nil {
  1654  					t.Fatalf("Expected subpool to be pruned, but got: %v", filtered)
  1655  				}
  1656  				return
  1657  			}
  1658  			if filtered == nil {
  1659  				t.Fatalf("Expected subpool to have %d prs, but it was pruned.", len(tc.expectedPRs))
  1660  			}
  1661  			if got := prNumbers(filtered.prs); !reflect.DeepEqual(got, tc.expectedPRs) {
  1662  				t.Errorf("Expected filtered pool to have PRs %v, but got %v.", tc.expectedPRs, got)
  1663  			}
  1664  		})
  1665  	}
  1666  }
  1667  
  1668  func TestIsPassing(t *testing.T) {
  1669  	yes := true
  1670  	no := false
  1671  	headSHA := "head"
  1672  	success := string(githubql.StatusStateSuccess)
  1673  	failure := string(githubql.StatusStateFailure)
  1674  	testCases := []struct {
  1675  		name             string
  1676  		passing          bool
  1677  		config           config.TideContextPolicy
  1678  		combinedContexts map[string]string
  1679  	}{
  1680  		{
  1681  			name:             "empty policy - success (trust combined status)",
  1682  			passing:          true,
  1683  			combinedContexts: map[string]string{"c1": success, "c2": success, statusContext: failure},
  1684  		},
  1685  		{
  1686  			name:             "empty policy - failure because of failed context c4 (trust combined status)",
  1687  			passing:          false,
  1688  			combinedContexts: map[string]string{"c1": success, "c2": success, "c3": failure, statusContext: failure},
  1689  		},
  1690  		{
  1691  			name:    "passing (trust combined status)",
  1692  			passing: true,
  1693  			config: config.TideContextPolicy{
  1694  				RequiredContexts:    []string{"c1", "c2", "c3"},
  1695  				SkipUnknownContexts: &no,
  1696  			},
  1697  			combinedContexts: map[string]string{"c1": success, "c2": success, "c3": success, statusContext: failure},
  1698  		},
  1699  		{
  1700  			name:    "failing because of missing required check c3",
  1701  			passing: false,
  1702  			config: config.TideContextPolicy{
  1703  				RequiredContexts: []string{"c1", "c2", "c3"},
  1704  			},
  1705  			combinedContexts: map[string]string{"c1": success, "c2": success, statusContext: failure},
  1706  		},
  1707  		{
  1708  			name:             "failing because of failed context c2",
  1709  			passing:          false,
  1710  			combinedContexts: map[string]string{"c1": success, "c2": failure},
  1711  			config: config.TideContextPolicy{
  1712  				RequiredContexts: []string{"c1", "c2", "c3"},
  1713  				OptionalContexts: []string{"c4"},
  1714  			},
  1715  		},
  1716  		{
  1717  			name:    "passing because of failed context c4 is optional",
  1718  			passing: true,
  1719  
  1720  			combinedContexts: map[string]string{"c1": success, "c2": success, "c3": success, "c4": failure},
  1721  			config: config.TideContextPolicy{
  1722  				RequiredContexts: []string{"c1", "c2", "c3"},
  1723  				OptionalContexts: []string{"c4"},
  1724  			},
  1725  		},
  1726  		{
  1727  			name:    "skipping unknown contexts - failing because of missing required context c3",
  1728  			passing: false,
  1729  			config: config.TideContextPolicy{
  1730  				RequiredContexts:    []string{"c1", "c2", "c3"},
  1731  				SkipUnknownContexts: &yes,
  1732  			},
  1733  			combinedContexts: map[string]string{"c1": success, "c2": success, statusContext: failure},
  1734  		},
  1735  		{
  1736  			name:             "skipping unknown contexts - failing because c2 is failing",
  1737  			passing:          false,
  1738  			combinedContexts: map[string]string{"c1": success, "c2": failure},
  1739  			config: config.TideContextPolicy{
  1740  				RequiredContexts:    []string{"c1", "c2"},
  1741  				OptionalContexts:    []string{"c4"},
  1742  				SkipUnknownContexts: &yes,
  1743  			},
  1744  		},
  1745  		{
  1746  			name:             "skipping unknown contexts - passing because c4 is optional",
  1747  			passing:          true,
  1748  			combinedContexts: map[string]string{"c1": success, "c2": success, "c3": success, "c4": failure},
  1749  			config: config.TideContextPolicy{
  1750  				RequiredContexts:    []string{"c1", "c3"},
  1751  				OptionalContexts:    []string{"c4"},
  1752  				SkipUnknownContexts: &yes,
  1753  			},
  1754  		},
  1755  		{
  1756  			name:    "skipping unknown contexts - passing because c4 is optional and c5 is unknown",
  1757  			passing: true,
  1758  
  1759  			combinedContexts: map[string]string{"c1": success, "c2": success, "c3": success, "c4": failure, "c5": failure},
  1760  			config: config.TideContextPolicy{
  1761  				RequiredContexts:    []string{"c1", "c3"},
  1762  				OptionalContexts:    []string{"c4"},
  1763  				SkipUnknownContexts: &yes,
  1764  			},
  1765  		},
  1766  	}
  1767  
  1768  	for _, tc := range testCases {
  1769  		ghc := &fgc{
  1770  			combinedStatus: tc.combinedContexts,
  1771  			expectedSHA:    headSHA}
  1772  		log := logrus.WithField("component", "tide")
  1773  		_, err := log.String()
  1774  		if err != nil {
  1775  			t.Errorf("Failed to get log output before testing: %v", err)
  1776  			t.FailNow()
  1777  		}
  1778  		pr := PullRequest{HeadRefOID: githubql.String(headSHA)}
  1779  		passing := isPassingTests(log, ghc, pr, &tc.config)
  1780  		if passing != tc.passing {
  1781  			t.Errorf("%s: Expected %t got %t", tc.name, tc.passing, passing)
  1782  		}
  1783  	}
  1784  }
  1785  
  1786  func TestPresubmitsByPull(t *testing.T) {
  1787  	samplePR := PullRequest{
  1788  		Number:     githubql.Int(100),
  1789  		HeadRefOID: githubql.String("sha"),
  1790  	}
  1791  	testcases := []struct {
  1792  		name string
  1793  
  1794  		initialChangeCache map[changeCacheKey][]string
  1795  		presubmits         []config.Presubmit
  1796  
  1797  		expectedPresubmits  map[int]sets.String
  1798  		expectedChangeCache map[changeCacheKey][]string
  1799  	}{
  1800  		{
  1801  			name: "no matching presubmits",
  1802  			presubmits: []config.Presubmit{
  1803  				{
  1804  					Context: "always",
  1805  					RegexpChangeMatcher: config.RegexpChangeMatcher{
  1806  						RunIfChanged: "foo",
  1807  					},
  1808  				},
  1809  				{
  1810  					Context: "never",
  1811  				},
  1812  			},
  1813  			expectedChangeCache: map[changeCacheKey][]string{{number: 100, sha: "sha"}: {"CHANGED"}},
  1814  			expectedPresubmits:  map[int]sets.String{},
  1815  		},
  1816  		{
  1817  			name:               "no presubmits",
  1818  			presubmits:         []config.Presubmit{},
  1819  			expectedPresubmits: map[int]sets.String{},
  1820  		},
  1821  		{
  1822  			name: "no matching presubmits (check cache eviction)",
  1823  			presubmits: []config.Presubmit{
  1824  				{
  1825  					Context: "never",
  1826  				},
  1827  			},
  1828  			initialChangeCache: map[changeCacheKey][]string{{number: 100, sha: "sha"}: {"FILE"}},
  1829  			expectedPresubmits: map[int]sets.String{},
  1830  		},
  1831  		{
  1832  			name: "no matching presubmits (check cache retention)",
  1833  			presubmits: []config.Presubmit{
  1834  				{
  1835  					Context: "always",
  1836  					RegexpChangeMatcher: config.RegexpChangeMatcher{
  1837  						RunIfChanged: "foo",
  1838  					},
  1839  				},
  1840  				{
  1841  					Context: "never",
  1842  				},
  1843  			},
  1844  			initialChangeCache:  map[changeCacheKey][]string{{number: 100, sha: "sha"}: {"FILE"}},
  1845  			expectedChangeCache: map[changeCacheKey][]string{{number: 100, sha: "sha"}: {"FILE"}},
  1846  			expectedPresubmits:  map[int]sets.String{},
  1847  		},
  1848  		{
  1849  			name: "always_run",
  1850  			presubmits: []config.Presubmit{
  1851  				{
  1852  					Context:   "always",
  1853  					AlwaysRun: true,
  1854  				},
  1855  				{
  1856  					Context: "never",
  1857  				},
  1858  			},
  1859  			expectedPresubmits: map[int]sets.String{100: sets.NewString("always")},
  1860  		},
  1861  		{
  1862  			name: "runs against branch",
  1863  			presubmits: []config.Presubmit{
  1864  				{
  1865  					Context:   "presubmit",
  1866  					AlwaysRun: true,
  1867  					Brancher: config.Brancher{
  1868  						Branches: []string{"master", "dev"},
  1869  					},
  1870  				},
  1871  				{
  1872  					Context: "never",
  1873  				},
  1874  			},
  1875  			expectedPresubmits: map[int]sets.String{100: sets.NewString("presubmit")},
  1876  		},
  1877  		{
  1878  			name: "doesn't run against branch",
  1879  			presubmits: []config.Presubmit{
  1880  				{
  1881  					Context:   "presubmit",
  1882  					AlwaysRun: true,
  1883  					Brancher: config.Brancher{
  1884  						Branches: []string{"release", "dev"},
  1885  					},
  1886  				},
  1887  				{
  1888  					Context:   "always",
  1889  					AlwaysRun: true,
  1890  				},
  1891  				{
  1892  					Context: "never",
  1893  				},
  1894  			},
  1895  			expectedPresubmits: map[int]sets.String{100: sets.NewString("always")},
  1896  		},
  1897  		{
  1898  			name: "run_if_changed (uncached)",
  1899  			presubmits: []config.Presubmit{
  1900  				{
  1901  					Context: "presubmit",
  1902  					RegexpChangeMatcher: config.RegexpChangeMatcher{
  1903  						RunIfChanged: "^CHANGE.$",
  1904  					},
  1905  				},
  1906  				{
  1907  					Context:   "always",
  1908  					AlwaysRun: true,
  1909  				},
  1910  				{
  1911  					Context: "never",
  1912  				},
  1913  			},
  1914  			expectedPresubmits:  map[int]sets.String{100: sets.NewString("presubmit", "always")},
  1915  			expectedChangeCache: map[changeCacheKey][]string{{number: 100, sha: "sha"}: {"CHANGED"}},
  1916  		},
  1917  		{
  1918  			name: "run_if_changed (cached)",
  1919  			presubmits: []config.Presubmit{
  1920  				{
  1921  					Context: "presubmit",
  1922  					RegexpChangeMatcher: config.RegexpChangeMatcher{
  1923  						RunIfChanged: "^FIL.$",
  1924  					},
  1925  				},
  1926  				{
  1927  					Context:   "always",
  1928  					AlwaysRun: true,
  1929  				},
  1930  				{
  1931  					Context: "never",
  1932  				},
  1933  			},
  1934  			initialChangeCache:  map[changeCacheKey][]string{{number: 100, sha: "sha"}: {"FILE"}},
  1935  			expectedPresubmits:  map[int]sets.String{100: sets.NewString("presubmit", "always")},
  1936  			expectedChangeCache: map[changeCacheKey][]string{{number: 100, sha: "sha"}: {"FILE"}},
  1937  		},
  1938  		{
  1939  			name: "run_if_changed (cached) (skippable)",
  1940  			presubmits: []config.Presubmit{
  1941  				{
  1942  					Context: "presubmit",
  1943  					RegexpChangeMatcher: config.RegexpChangeMatcher{
  1944  						RunIfChanged: "^CHANGE.$",
  1945  					},
  1946  				},
  1947  				{
  1948  					Context:   "always",
  1949  					AlwaysRun: true,
  1950  				},
  1951  				{
  1952  					Context: "never",
  1953  				},
  1954  			},
  1955  			initialChangeCache:  map[changeCacheKey][]string{{number: 100, sha: "sha"}: {"FILE"}},
  1956  			expectedPresubmits:  map[int]sets.String{100: sets.NewString("always")},
  1957  			expectedChangeCache: map[changeCacheKey][]string{{number: 100, sha: "sha"}: {"FILE"}},
  1958  		},
  1959  	}
  1960  
  1961  	for _, tc := range testcases {
  1962  		t.Logf("Starting test case: %q", tc.name)
  1963  
  1964  		if tc.initialChangeCache == nil {
  1965  			tc.initialChangeCache = map[changeCacheKey][]string{}
  1966  		}
  1967  		if tc.expectedChangeCache == nil {
  1968  			tc.expectedChangeCache = map[changeCacheKey][]string{}
  1969  		}
  1970  
  1971  		cfg := &config.Config{}
  1972  		cfg.SetPresubmits(map[string][]config.Presubmit{
  1973  			"/":       tc.presubmits,
  1974  			"foo/bar": {{Context: "wrong-repo", AlwaysRun: true}},
  1975  		})
  1976  		cfgAgent := &config.Agent{}
  1977  		cfgAgent.Set(cfg)
  1978  		sp := &subpool{
  1979  			branch: "master",
  1980  			prs:    []PullRequest{samplePR},
  1981  		}
  1982  		c := &Controller{
  1983  			ca:  cfgAgent,
  1984  			ghc: &fgc{},
  1985  			changedFiles: &changedFilesAgent{
  1986  				ghc:             &fgc{},
  1987  				changeCache:     tc.initialChangeCache,
  1988  				nextChangeCache: make(map[changeCacheKey][]string),
  1989  			},
  1990  		}
  1991  		presubmits, err := c.presubmitsByPull(sp)
  1992  		if err != nil {
  1993  			t.Fatalf("unexpected error from presubmitsByPull: %v", err)
  1994  		}
  1995  		c.changedFiles.prune()
  1996  		if !reflect.DeepEqual(presubmits, tc.expectedPresubmits) {
  1997  			t.Errorf("expected presubmit mapping: %v,\nbut got %v\n", tc.expectedPresubmits, presubmits)
  1998  		}
  1999  		if got := c.changedFiles.changeCache; !reflect.DeepEqual(got, tc.expectedChangeCache) {
  2000  			t.Errorf("expected file change cache: %v,\nbut got %v\n", tc.expectedChangeCache, got)
  2001  		}
  2002  	}
  2003  }