sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/jenkins/controller_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 jenkins
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"net/http"
    24  	"net/http/httptest"
    25  	"reflect"
    26  	"sync"
    27  	"testing"
    28  	"text/template"
    29  	"time"
    30  
    31  	"github.com/sirupsen/logrus"
    32  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    33  	"k8s.io/apimachinery/pkg/runtime"
    34  	"k8s.io/utils/clock"
    35  	clocktesting "k8s.io/utils/clock/testing"
    36  	"sigs.k8s.io/prow/pkg/client/clientset/versioned/fake"
    37  
    38  	prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1"
    39  	"sigs.k8s.io/prow/pkg/config"
    40  	"sigs.k8s.io/prow/pkg/github"
    41  	"sigs.k8s.io/prow/pkg/pjutil"
    42  )
    43  
    44  type fca struct {
    45  	sync.Mutex
    46  	c *config.Config
    47  }
    48  
    49  func newFakeConfigAgent(t *testing.T, maxConcurrency int, operators []config.JenkinsOperator) *fca {
    50  	presubmits := []config.Presubmit{
    51  		{
    52  			JobBase: config.JobBase{
    53  				Name: "test-bazel-build",
    54  			},
    55  		},
    56  		{
    57  			JobBase: config.JobBase{
    58  				Name: "test-e2e",
    59  			},
    60  		},
    61  		{
    62  			AlwaysRun: true,
    63  			JobBase: config.JobBase{
    64  				Name: "test-bazel-test",
    65  			},
    66  		},
    67  	}
    68  	if err := config.SetPresubmitRegexes(presubmits); err != nil {
    69  		t.Fatal(err)
    70  	}
    71  	presubmitMap := map[string][]config.Presubmit{
    72  		"kubernetes/kubernetes": presubmits,
    73  	}
    74  
    75  	ca := &fca{
    76  		c: &config.Config{
    77  			ProwConfig: config.ProwConfig{
    78  				ProwJobNamespace: "prowjobs",
    79  				JenkinsOperators: []config.JenkinsOperator{
    80  					{
    81  						Controller: config.Controller{
    82  							JobURLTemplate: template.Must(template.New("test").Parse("{{.Status.PodName}}/{{.Status.State}}")),
    83  							MaxConcurrency: maxConcurrency,
    84  							MaxGoroutines:  20,
    85  						},
    86  					},
    87  				},
    88  				StatusErrorLink: "https://github.com/kubernetes/test-infra/issues",
    89  			},
    90  			JobConfig: config.JobConfig{
    91  				PresubmitsStatic: presubmitMap,
    92  			},
    93  		},
    94  	}
    95  	if len(operators) > 0 {
    96  		ca.c.JenkinsOperators = operators
    97  	}
    98  	return ca
    99  }
   100  
   101  func (f *fca) Config() *config.Config {
   102  	f.Lock()
   103  	defer f.Unlock()
   104  	return f.c
   105  }
   106  
   107  type fjc struct {
   108  	sync.Mutex
   109  	built       bool
   110  	pjs         []prowapi.ProwJob
   111  	err         error
   112  	builds      map[string]Build
   113  	didAbort    bool
   114  	abortErrors bool
   115  }
   116  
   117  func (f *fjc) Build(pj *prowapi.ProwJob, buildID string) error {
   118  	f.Lock()
   119  	defer f.Unlock()
   120  	if f.err != nil {
   121  		return f.err
   122  	}
   123  	f.built = true
   124  	f.pjs = append(f.pjs, *pj)
   125  	return nil
   126  }
   127  
   128  func (f *fjc) ListBuilds(jobs []BuildQueryParams) (map[string]Build, error) {
   129  	f.Lock()
   130  	defer f.Unlock()
   131  	if f.err != nil {
   132  		return nil, f.err
   133  	}
   134  	return f.builds, nil
   135  }
   136  
   137  func (f *fjc) Abort(job string, build *Build) error {
   138  	f.Lock()
   139  	defer f.Unlock()
   140  	if f.abortErrors {
   141  		return errors.New("erroring on abort as requested")
   142  	}
   143  	f.didAbort = true
   144  	return nil
   145  }
   146  
   147  type fghc struct {
   148  	sync.Mutex
   149  	changes []github.PullRequestChange
   150  	err     error
   151  }
   152  
   153  func (f *fghc) GetPullRequestChanges(org, repo string, number int) ([]github.PullRequestChange, error) {
   154  	f.Lock()
   155  	defer f.Unlock()
   156  	return f.changes, f.err
   157  }
   158  
   159  func (f *fghc) BotUserCheckerWithContext(context.Context) (func(string) bool, error) {
   160  	return func(candidate string) bool {
   161  		return candidate == "bot"
   162  	}, nil
   163  }
   164  func (f *fghc) CreateStatusWithContext(_ context.Context, org, repo, ref string, s github.Status) error {
   165  	f.Lock()
   166  	defer f.Unlock()
   167  	return nil
   168  }
   169  func (f *fghc) ListIssueCommentsWithContext(_ context.Context, org, repo string, number int) ([]github.IssueComment, error) {
   170  	f.Lock()
   171  	defer f.Unlock()
   172  	return nil, nil
   173  }
   174  func (f *fghc) CreateCommentWithContext(_ context.Context, org, repo string, number int, comment string) error {
   175  	f.Lock()
   176  	defer f.Unlock()
   177  	return nil
   178  }
   179  func (f *fghc) DeleteCommentWithContext(_ context.Context, org, repo string, ID int) error {
   180  	f.Lock()
   181  	defer f.Unlock()
   182  	return nil
   183  }
   184  func (f *fghc) EditCommentWithContext(_ context.Context, org, repo string, ID int, comment string) error {
   185  	f.Lock()
   186  	defer f.Unlock()
   187  	return nil
   188  }
   189  
   190  func TestSyncTriggeredJobs(t *testing.T) {
   191  	fakeClock := clocktesting.NewFakeClock(time.Now().Truncate(1 * time.Second))
   192  	pendingTime := metav1.NewTime(fakeClock.Now())
   193  
   194  	var testcases = []struct {
   195  		name           string
   196  		pj             prowapi.ProwJob
   197  		pendingJobs    map[string]int
   198  		maxConcurrency int
   199  		builds         map[string]Build
   200  		err            error
   201  
   202  		expectedState       prowapi.ProwJobState
   203  		expectedBuild       bool
   204  		expectedComplete    bool
   205  		expectedReport      bool
   206  		expectedEnqueued    bool
   207  		expectedError       bool
   208  		expectedPendingTime *metav1.Time
   209  	}{
   210  		{
   211  			name: "start new job",
   212  			pj: prowapi.ProwJob{
   213  				ObjectMeta: metav1.ObjectMeta{
   214  					Name:      "test",
   215  					Namespace: "prowjobs",
   216  				},
   217  				Spec: prowapi.ProwJobSpec{
   218  					Type: prowapi.PostsubmitJob,
   219  				},
   220  				Status: prowapi.ProwJobStatus{
   221  					State: prowapi.TriggeredState,
   222  				},
   223  			},
   224  			expectedBuild:       true,
   225  			expectedReport:      true,
   226  			expectedState:       prowapi.PendingState,
   227  			expectedEnqueued:    true,
   228  			expectedPendingTime: &pendingTime,
   229  		},
   230  		{
   231  			name: "start new job, error",
   232  			pj: prowapi.ProwJob{
   233  				ObjectMeta: metav1.ObjectMeta{
   234  					Name:      "test",
   235  					Namespace: "prowjobs",
   236  				},
   237  				Spec: prowapi.ProwJobSpec{
   238  					Type: prowapi.PresubmitJob,
   239  					Refs: &prowapi.Refs{
   240  						Pulls: []prowapi.Pull{{
   241  							Number: 1,
   242  							SHA:    "fake-sha",
   243  						}},
   244  					},
   245  				},
   246  				Status: prowapi.ProwJobStatus{
   247  					State: prowapi.TriggeredState,
   248  				},
   249  			},
   250  			err:              errors.New("oh no"),
   251  			expectedReport:   true,
   252  			expectedState:    prowapi.ErrorState,
   253  			expectedComplete: true,
   254  			expectedError:    true,
   255  		},
   256  		{
   257  			name: "block running new job",
   258  			pj: prowapi.ProwJob{
   259  				ObjectMeta: metav1.ObjectMeta{
   260  					Name:      "test",
   261  					Namespace: "prowjobs",
   262  				},
   263  				Spec: prowapi.ProwJobSpec{
   264  					Type: prowapi.PostsubmitJob,
   265  				},
   266  				Status: prowapi.ProwJobStatus{
   267  					State: prowapi.TriggeredState,
   268  				},
   269  			},
   270  			pendingJobs:      map[string]int{"motherearth": 10, "allagash": 8, "krusovice": 2},
   271  			maxConcurrency:   20,
   272  			expectedBuild:    false,
   273  			expectedReport:   false,
   274  			expectedState:    prowapi.TriggeredState,
   275  			expectedEnqueued: false,
   276  		},
   277  		{
   278  			name: "allow running new job",
   279  			pj: prowapi.ProwJob{
   280  				ObjectMeta: metav1.ObjectMeta{
   281  					Name:      "test",
   282  					Namespace: "prowjobs",
   283  				},
   284  				Spec: prowapi.ProwJobSpec{
   285  					Type: prowapi.PostsubmitJob,
   286  				},
   287  				Status: prowapi.ProwJobStatus{
   288  					State: prowapi.TriggeredState,
   289  				},
   290  			},
   291  			pendingJobs:         map[string]int{"motherearth": 10, "allagash": 8, "krusovice": 2},
   292  			maxConcurrency:      21,
   293  			expectedBuild:       true,
   294  			expectedReport:      true,
   295  			expectedState:       prowapi.PendingState,
   296  			expectedEnqueued:    true,
   297  			expectedPendingTime: &pendingTime,
   298  		},
   299  	}
   300  	for _, tc := range testcases {
   301  		totServ := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   302  			fmt.Fprint(w, "42")
   303  		}))
   304  		defer totServ.Close()
   305  		t.Logf("scenario %q", tc.name)
   306  		fjc := &fjc{
   307  			err: tc.err,
   308  		}
   309  		fakeProwJobClient := fake.NewSimpleClientset(&tc.pj)
   310  
   311  		c := Controller{
   312  			prowJobClient: fakeProwJobClient.ProwV1().ProwJobs("prowjobs"),
   313  			jc:            fjc,
   314  			log:           logrus.NewEntry(logrus.StandardLogger()),
   315  			cfg:           newFakeConfigAgent(t, tc.maxConcurrency, nil).Config,
   316  			totURL:        totServ.URL,
   317  			lock:          sync.RWMutex{},
   318  			pendingJobs:   make(map[string]int),
   319  			clock:         fakeClock,
   320  		}
   321  		if tc.pendingJobs != nil {
   322  			c.pendingJobs = tc.pendingJobs
   323  		}
   324  
   325  		reports := make(chan prowapi.ProwJob, 100)
   326  		if err := c.syncTriggeredJob(tc.pj, reports, tc.builds); err != nil {
   327  			t.Errorf("unexpected error: %v", err)
   328  			continue
   329  		}
   330  		close(reports)
   331  
   332  		actualProwJobs, err := fakeProwJobClient.ProwV1().ProwJobs("prowjobs").List(context.Background(), metav1.ListOptions{})
   333  		if err != nil {
   334  			t.Fatalf("failed to list prowjobs from client %v", err)
   335  		}
   336  		if len(actualProwJobs.Items) != 1 {
   337  			t.Fatalf("Didn't create just one ProwJob, but %d", len(actualProwJobs.Items))
   338  		}
   339  		actual := actualProwJobs.Items[0]
   340  		if tc.expectedError && actual.Status.Description != "Error starting Jenkins job." {
   341  			t.Errorf("expected description %q, got %q", "Error starting Jenkins job.", actual.Status.Description)
   342  			continue
   343  		}
   344  		if actual.Status.State != tc.expectedState {
   345  			t.Errorf("expected state %q, got %q", tc.expectedState, actual.Status.State)
   346  			continue
   347  		}
   348  		if actual.Complete() != tc.expectedComplete {
   349  			t.Errorf("expected complete prowjob, got %v", actual)
   350  			continue
   351  		}
   352  		if tc.expectedReport && len(reports) != 1 {
   353  			t.Errorf("wanted one report but got %d", len(reports))
   354  			continue
   355  		}
   356  		if !tc.expectedReport && len(reports) != 0 {
   357  			t.Errorf("did not wany any reports but got %d", len(reports))
   358  			continue
   359  		}
   360  		if fjc.built != tc.expectedBuild {
   361  			t.Errorf("expected build: %t, got: %t", tc.expectedBuild, fjc.built)
   362  			continue
   363  		}
   364  		if tc.expectedEnqueued && actual.Status.Description != "Jenkins job enqueued." {
   365  			t.Errorf("expected enqueued prowjob, got %v", actual)
   366  		}
   367  		if !reflect.DeepEqual(actual.Status.PendingTime, tc.expectedPendingTime) {
   368  			t.Errorf("for case %q got pending time %v, expected %v", tc.name, actual.Status.PendingTime, tc.expectedPendingTime)
   369  		}
   370  	}
   371  }
   372  
   373  func TestSyncPendingJobs(t *testing.T) {
   374  	var testcases = []struct {
   375  		name        string
   376  		pj          prowapi.ProwJob
   377  		pendingJobs map[string]int
   378  		builds      map[string]Build
   379  		err         error
   380  
   381  		// TODO: Change to pass a ProwJobStatus
   382  		expectedState    prowapi.ProwJobState
   383  		expectedBuild    bool
   384  		expectedURL      string
   385  		expectedComplete bool
   386  		expectedReport   bool
   387  		expectedEnqueued bool
   388  		expectedError    bool
   389  	}{
   390  		{
   391  			name: "enqueued",
   392  			pj: prowapi.ProwJob{
   393  				ObjectMeta: metav1.ObjectMeta{
   394  					Name:      "foofoo",
   395  					Namespace: "prowjobs",
   396  				},
   397  				Spec: prowapi.ProwJobSpec{
   398  					Job: "test-job",
   399  				},
   400  				Status: prowapi.ProwJobStatus{
   401  					State:       prowapi.PendingState,
   402  					Description: "Jenkins job enqueued.",
   403  				},
   404  			},
   405  			builds: map[string]Build{
   406  				"foofoo": {enqueued: true, Number: 10},
   407  			},
   408  			expectedState:    prowapi.PendingState,
   409  			expectedEnqueued: true,
   410  		},
   411  		{
   412  			name: "finished queue",
   413  			pj: prowapi.ProwJob{
   414  				ObjectMeta: metav1.ObjectMeta{
   415  					Name:      "boing",
   416  					Namespace: "prowjobs",
   417  				},
   418  				Spec: prowapi.ProwJobSpec{
   419  					Job: "test-job",
   420  				},
   421  				Status: prowapi.ProwJobStatus{
   422  					State:       prowapi.PendingState,
   423  					Description: "Jenkins job enqueued.",
   424  				},
   425  			},
   426  			builds: map[string]Build{
   427  				"boing": {enqueued: false, Number: 10},
   428  			},
   429  			expectedURL:      "boing/pending",
   430  			expectedState:    prowapi.PendingState,
   431  			expectedEnqueued: false,
   432  			expectedReport:   true,
   433  		},
   434  		{
   435  			name: "building",
   436  			pj: prowapi.ProwJob{
   437  				ObjectMeta: metav1.ObjectMeta{
   438  					Name:      "firstoutthetrenches",
   439  					Namespace: "prowjobs",
   440  				},
   441  				Spec: prowapi.ProwJobSpec{
   442  					Job: "test-job",
   443  				},
   444  				Status: prowapi.ProwJobStatus{
   445  					State: prowapi.PendingState,
   446  				},
   447  			},
   448  			builds: map[string]Build{
   449  				"firstoutthetrenches": {enqueued: false, Number: 10},
   450  			},
   451  			expectedURL:    "firstoutthetrenches/pending",
   452  			expectedState:  prowapi.PendingState,
   453  			expectedReport: true,
   454  		},
   455  		{
   456  			name: "missing build",
   457  			pj: prowapi.ProwJob{
   458  				ObjectMeta: metav1.ObjectMeta{
   459  					Name:      "blabla",
   460  					Namespace: "prowjobs",
   461  				},
   462  				Spec: prowapi.ProwJobSpec{
   463  					Type: prowapi.PresubmitJob,
   464  					Job:  "test-job",
   465  					Refs: &prowapi.Refs{
   466  						Pulls: []prowapi.Pull{{
   467  							Number: 1,
   468  							SHA:    "fake-sha",
   469  						}},
   470  					},
   471  				},
   472  				Status: prowapi.ProwJobStatus{
   473  					State: prowapi.PendingState,
   474  				},
   475  			},
   476  			// missing build
   477  			builds: map[string]Build{
   478  				"other": {enqueued: false, Number: 10},
   479  			},
   480  			expectedURL:      "https://github.com/kubernetes/test-infra/issues",
   481  			expectedState:    prowapi.ErrorState,
   482  			expectedError:    true,
   483  			expectedComplete: true,
   484  			expectedReport:   true,
   485  		},
   486  		{
   487  			name: "finished, success",
   488  			pj: prowapi.ProwJob{
   489  				ObjectMeta: metav1.ObjectMeta{
   490  					Name:      "winwin",
   491  					Namespace: "prowjobs",
   492  				},
   493  				Spec: prowapi.ProwJobSpec{
   494  					Job: "test-job",
   495  				},
   496  				Status: prowapi.ProwJobStatus{
   497  					State: prowapi.PendingState,
   498  				},
   499  			},
   500  			builds: map[string]Build{
   501  				"winwin": {Result: pState(success), Number: 11},
   502  			},
   503  			expectedURL:      "winwin/success",
   504  			expectedState:    prowapi.SuccessState,
   505  			expectedComplete: true,
   506  			expectedReport:   true,
   507  		},
   508  		{
   509  			name: "finished, failed",
   510  			pj: prowapi.ProwJob{
   511  				ObjectMeta: metav1.ObjectMeta{
   512  					Name:      "whatapity",
   513  					Namespace: "prowjobs",
   514  				},
   515  				Spec: prowapi.ProwJobSpec{
   516  					Job: "test-job",
   517  				},
   518  				Status: prowapi.ProwJobStatus{
   519  					State: prowapi.PendingState,
   520  				},
   521  			},
   522  			builds: map[string]Build{
   523  				"whatapity": {Result: pState(failure), Number: 12},
   524  			},
   525  			expectedURL:      "whatapity/failure",
   526  			expectedState:    prowapi.FailureState,
   527  			expectedComplete: true,
   528  			expectedReport:   true,
   529  		},
   530  	}
   531  	for _, tc := range testcases {
   532  		t.Logf("scenario %q", tc.name)
   533  		totServ := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   534  			fmt.Fprint(w, "42")
   535  		}))
   536  		defer totServ.Close()
   537  		fjc := &fjc{
   538  			err: tc.err,
   539  		}
   540  		fakeProwJobClient := fake.NewSimpleClientset(&tc.pj)
   541  
   542  		c := Controller{
   543  			prowJobClient: fakeProwJobClient.ProwV1().ProwJobs("prowjobs"),
   544  			jc:            fjc,
   545  			log:           logrus.NewEntry(logrus.StandardLogger()),
   546  			cfg:           newFakeConfigAgent(t, 0, nil).Config,
   547  			totURL:        totServ.URL,
   548  			lock:          sync.RWMutex{},
   549  			pendingJobs:   make(map[string]int),
   550  			clock:         clock.RealClock{},
   551  		}
   552  
   553  		reports := make(chan prowapi.ProwJob, 100)
   554  		if err := c.syncPendingJob(tc.pj, reports, tc.builds); err != nil {
   555  			t.Errorf("unexpected error: %v", err)
   556  			continue
   557  		}
   558  		close(reports)
   559  
   560  		actualProwJobs, err := fakeProwJobClient.ProwV1().ProwJobs("prowjobs").List(context.Background(), metav1.ListOptions{})
   561  		if err != nil {
   562  			t.Fatalf("failed to list prowjobs from client %v", err)
   563  		}
   564  		if len(actualProwJobs.Items) != 1 {
   565  			t.Fatalf("Didn't create just one ProwJob, but %d", len(actualProwJobs.Items))
   566  		}
   567  		actual := actualProwJobs.Items[0]
   568  		if tc.expectedError && actual.Status.Description != "Error finding Jenkins job." {
   569  			t.Errorf("expected description %q, got %q", "Error finding Jenkins job.", actual.Status.Description)
   570  			continue
   571  		}
   572  		if actual.Status.State != tc.expectedState {
   573  			t.Errorf("expected state %q, got %q", tc.expectedState, actual.Status.State)
   574  			continue
   575  		}
   576  		if actual.Complete() != tc.expectedComplete {
   577  			t.Errorf("expected complete prowjob, got %v", actual)
   578  			continue
   579  		}
   580  		if tc.expectedReport && len(reports) != 1 {
   581  			t.Errorf("wanted one report but got %d", len(reports))
   582  			continue
   583  		}
   584  		if !tc.expectedReport && len(reports) != 0 {
   585  			t.Errorf("did not wany any reports but got %d", len(reports))
   586  			continue
   587  		}
   588  		if fjc.built != tc.expectedBuild {
   589  			t.Errorf("expected build: %t, got: %t", tc.expectedBuild, fjc.built)
   590  			continue
   591  		}
   592  		if tc.expectedEnqueued && actual.Status.Description != "Jenkins job enqueued." {
   593  			t.Errorf("expected enqueued prowjob, got %v", actual)
   594  		}
   595  		if tc.expectedURL != actual.Status.URL {
   596  			t.Errorf("expected status URL: %s, got: %s", tc.expectedURL, actual.Status.URL)
   597  		}
   598  	}
   599  }
   600  
   601  func pState(state string) *string {
   602  	s := state
   603  	return &s
   604  }
   605  
   606  // TestBatch walks through the happy path of a batch job on Jenkins.
   607  func TestBatch(t *testing.T) {
   608  	pre := config.Presubmit{
   609  		JobBase: config.JobBase{
   610  			Name:  "pr-some-job",
   611  			Agent: "jenkins",
   612  		},
   613  		Reporter: config.Reporter{
   614  			Context: "Some Job Context",
   615  		},
   616  	}
   617  	pj := pjutil.NewProwJob(pjutil.BatchSpec(pre, prowapi.Refs{
   618  		Org:     "o",
   619  		Repo:    "r",
   620  		BaseRef: "master",
   621  		BaseSHA: "123",
   622  		Pulls: []prowapi.Pull{
   623  			{
   624  				Number: 1,
   625  				SHA:    "abc",
   626  			},
   627  			{
   628  				Number: 2,
   629  				SHA:    "qwe",
   630  			},
   631  		},
   632  	}), nil, nil)
   633  	pj.ObjectMeta.Name = "known_name"
   634  	pj.ObjectMeta.Namespace = "prowjobs"
   635  	fakeProwJobClient := fake.NewSimpleClientset(&pj)
   636  	jc := &fjc{
   637  		builds: map[string]Build{
   638  			"known_name": { /* Running */ },
   639  		},
   640  	}
   641  	totServ := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   642  		fmt.Fprint(w, "42")
   643  	}))
   644  	defer totServ.Close()
   645  	c := Controller{
   646  		prowJobClient: fakeProwJobClient.ProwV1().ProwJobs("prowjobs"),
   647  		ghc:           &fghc{},
   648  		jc:            jc,
   649  		log:           logrus.NewEntry(logrus.StandardLogger()),
   650  		cfg:           newFakeConfigAgent(t, 0, nil).Config,
   651  		totURL:        totServ.URL,
   652  		pendingJobs:   make(map[string]int),
   653  		lock:          sync.RWMutex{},
   654  		clock:         clock.RealClock{},
   655  	}
   656  
   657  	if err := c.Sync(); err != nil {
   658  		t.Fatalf("Error on first sync: %v", err)
   659  	}
   660  	afterFirstSync, err := fakeProwJobClient.ProwV1().ProwJobs("prowjobs").Get(context.Background(), "known_name", metav1.GetOptions{})
   661  	if err != nil {
   662  		t.Fatalf("failed to get prowjob from client: %v", err)
   663  	}
   664  	if afterFirstSync.Status.State != prowapi.PendingState {
   665  		t.Fatalf("Wrong state: %v", afterFirstSync.Status.State)
   666  	}
   667  	if afterFirstSync.Status.Description != "Jenkins job enqueued." {
   668  		t.Fatalf("Expected description %q, got %q.", "Jenkins job enqueued.", afterFirstSync.Status.Description)
   669  	}
   670  	jc.builds["known_name"] = Build{Number: 42}
   671  	if err := c.Sync(); err != nil {
   672  		t.Fatalf("Error on second sync: %v", err)
   673  	}
   674  	afterSecondSync, err := fakeProwJobClient.ProwV1().ProwJobs("prowjobs").Get(context.Background(), "known_name", metav1.GetOptions{})
   675  	if err != nil {
   676  		t.Fatalf("failed to get prowjob from client: %v", err)
   677  	}
   678  	if afterSecondSync.Status.Description != "Jenkins job running." {
   679  		t.Fatalf("Expected description %q, got %q.", "Jenkins job running.", afterSecondSync.Status.Description)
   680  	}
   681  	if afterSecondSync.Status.PodName != "known_name" {
   682  		t.Fatalf("Wrong PodName: %s", afterSecondSync.Status.PodName)
   683  	}
   684  	jc.builds["known_name"] = Build{Result: pState(success)}
   685  	if err := c.Sync(); err != nil {
   686  		t.Fatalf("Error on third sync: %v", err)
   687  	}
   688  	afterThirdSync, err := fakeProwJobClient.ProwV1().ProwJobs("prowjobs").Get(context.Background(), "known_name", metav1.GetOptions{})
   689  	if err != nil {
   690  		t.Fatalf("failed to get prowjob from client: %v", err)
   691  	}
   692  	if afterThirdSync.Status.Description != "Jenkins job succeeded." {
   693  		t.Fatalf("Expected description %q, got %q.", "Jenkins job succeeded.", afterThirdSync.Status.Description)
   694  	}
   695  	if afterThirdSync.Status.State != prowapi.SuccessState {
   696  		t.Fatalf("Wrong state: %v", afterThirdSync.Status.State)
   697  	}
   698  	// This is what the SQ reads.
   699  	if afterThirdSync.Spec.Context != "Some Job Context" {
   700  		t.Fatalf("Wrong context: %v", afterThirdSync.Spec.Context)
   701  	}
   702  }
   703  
   704  func TestMaxConcurrencyWithNewlyTriggeredJobs(t *testing.T) {
   705  	tests := []struct {
   706  		name           string
   707  		pjs            []prowapi.ProwJob
   708  		pendingJobs    map[string]int
   709  		expectedBuilds int
   710  	}{
   711  		{
   712  			name: "avoid starting a triggered job",
   713  			pjs: []prowapi.ProwJob{
   714  				{
   715  					ObjectMeta: metav1.ObjectMeta{
   716  						Name:      "first",
   717  						Namespace: "prowjobs",
   718  					},
   719  					Spec: prowapi.ProwJobSpec{
   720  						Job:            "test-bazel-build",
   721  						Type:           prowapi.PostsubmitJob,
   722  						MaxConcurrency: 1,
   723  					},
   724  					Status: prowapi.ProwJobStatus{
   725  						State: prowapi.TriggeredState,
   726  					},
   727  				},
   728  				{
   729  					ObjectMeta: metav1.ObjectMeta{
   730  						Name:      "second",
   731  						Namespace: "prowjobs",
   732  					},
   733  					Spec: prowapi.ProwJobSpec{
   734  						Job:            "test-bazel-build",
   735  						Type:           prowapi.PostsubmitJob,
   736  						MaxConcurrency: 1,
   737  					},
   738  					Status: prowapi.ProwJobStatus{
   739  						State: prowapi.TriggeredState,
   740  					},
   741  				},
   742  			},
   743  			pendingJobs:    make(map[string]int),
   744  			expectedBuilds: 1,
   745  		},
   746  		{
   747  			name: "both triggered jobs can start",
   748  			pjs: []prowapi.ProwJob{
   749  				{
   750  					ObjectMeta: metav1.ObjectMeta{
   751  						Name:      "first",
   752  						Namespace: "prowjobs",
   753  					},
   754  					Spec: prowapi.ProwJobSpec{
   755  						Job:            "test-bazel-build",
   756  						Type:           prowapi.PostsubmitJob,
   757  						MaxConcurrency: 2,
   758  					},
   759  					Status: prowapi.ProwJobStatus{
   760  						State: prowapi.TriggeredState,
   761  					},
   762  				},
   763  				{
   764  					ObjectMeta: metav1.ObjectMeta{
   765  						Name:      "second",
   766  						Namespace: "prowjobs",
   767  					},
   768  					Spec: prowapi.ProwJobSpec{
   769  						Job:            "test-bazel-build",
   770  						Type:           prowapi.PostsubmitJob,
   771  						MaxConcurrency: 2,
   772  					},
   773  					Status: prowapi.ProwJobStatus{
   774  						State: prowapi.TriggeredState,
   775  					},
   776  				},
   777  			},
   778  			pendingJobs:    make(map[string]int),
   779  			expectedBuilds: 2,
   780  		},
   781  		{
   782  			name: "no triggered job can start",
   783  			pjs: []prowapi.ProwJob{
   784  				{
   785  					ObjectMeta: metav1.ObjectMeta{
   786  						Name:      "first",
   787  						Namespace: "prowjobs",
   788  					},
   789  					Spec: prowapi.ProwJobSpec{
   790  						Job:            "test-bazel-build",
   791  						Type:           prowapi.PostsubmitJob,
   792  						MaxConcurrency: 5,
   793  					},
   794  					Status: prowapi.ProwJobStatus{
   795  						State: prowapi.TriggeredState,
   796  					},
   797  				},
   798  				{
   799  					ObjectMeta: metav1.ObjectMeta{
   800  						Name:      "second",
   801  						Namespace: "prowjobs",
   802  					},
   803  					Spec: prowapi.ProwJobSpec{
   804  						Job:            "test-bazel-build",
   805  						Type:           prowapi.PostsubmitJob,
   806  						MaxConcurrency: 5,
   807  					},
   808  					Status: prowapi.ProwJobStatus{
   809  						State: prowapi.TriggeredState,
   810  					},
   811  				},
   812  			},
   813  			pendingJobs:    map[string]int{"test-bazel-build": 5},
   814  			expectedBuilds: 0,
   815  		},
   816  	}
   817  
   818  	for _, test := range tests {
   819  		totServ := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   820  			fmt.Fprint(w, "42")
   821  		}))
   822  		defer totServ.Close()
   823  		jobs := make(chan prowapi.ProwJob, len(test.pjs))
   824  		for _, pj := range test.pjs {
   825  			jobs <- pj
   826  		}
   827  		close(jobs)
   828  
   829  		var prowJobs []runtime.Object
   830  		for i := range test.pjs {
   831  			prowJobs = append(prowJobs, &test.pjs[i])
   832  		}
   833  		fakeProwJobClient := fake.NewSimpleClientset(prowJobs...)
   834  		fjc := &fjc{}
   835  		c := Controller{
   836  			prowJobClient: fakeProwJobClient.ProwV1().ProwJobs("prowjobs"),
   837  			jc:            fjc,
   838  			log:           logrus.NewEntry(logrus.StandardLogger()),
   839  			cfg:           newFakeConfigAgent(t, 0, nil).Config,
   840  			totURL:        totServ.URL,
   841  			pendingJobs:   test.pendingJobs,
   842  			clock:         clock.RealClock{},
   843  		}
   844  
   845  		reports := make(chan<- prowapi.ProwJob, len(test.pjs))
   846  		errors := make(chan<- error, len(test.pjs))
   847  
   848  		syncProwJobs(c.log, c.syncTriggeredJob, 20, jobs, reports, errors, nil)
   849  		if len(fjc.pjs) != test.expectedBuilds {
   850  			t.Errorf("expected builds: %d, got: %d", test.expectedBuilds, len(fjc.pjs))
   851  		}
   852  	}
   853  }
   854  
   855  func TestGetJenkinsJobs(t *testing.T) {
   856  	now := func() *metav1.Time {
   857  		n := metav1.Now()
   858  		return &n
   859  	}
   860  	tests := []struct {
   861  		name     string
   862  		pjs      []prowapi.ProwJob
   863  		expected []string
   864  	}{
   865  		{
   866  			name: "both complete and running",
   867  			pjs: []prowapi.ProwJob{
   868  				{
   869  					Spec: prowapi.ProwJobSpec{
   870  						Job: "coolio",
   871  					},
   872  					Status: prowapi.ProwJobStatus{
   873  						CompletionTime: now(),
   874  					},
   875  				},
   876  				{
   877  					Spec: prowapi.ProwJobSpec{
   878  						Job: "maradona",
   879  					},
   880  					Status: prowapi.ProwJobStatus{},
   881  				},
   882  			},
   883  			expected: []string{"maradona"},
   884  		},
   885  		{
   886  			name: "only complete",
   887  			pjs: []prowapi.ProwJob{
   888  				{
   889  					Spec: prowapi.ProwJobSpec{
   890  						Job: "coolio",
   891  					},
   892  					Status: prowapi.ProwJobStatus{
   893  						CompletionTime: now(),
   894  					},
   895  				},
   896  				{
   897  					Spec: prowapi.ProwJobSpec{
   898  						Job: "maradona",
   899  					},
   900  					Status: prowapi.ProwJobStatus{
   901  						CompletionTime: now(),
   902  					},
   903  				},
   904  			},
   905  			expected: nil,
   906  		},
   907  		{
   908  			name: "only running",
   909  			pjs: []prowapi.ProwJob{
   910  				{
   911  					Spec: prowapi.ProwJobSpec{
   912  						Job: "coolio",
   913  					},
   914  					Status: prowapi.ProwJobStatus{},
   915  				},
   916  				{
   917  					Spec: prowapi.ProwJobSpec{
   918  						Job: "maradona",
   919  					},
   920  					Status: prowapi.ProwJobStatus{},
   921  				},
   922  			},
   923  			expected: []string{"maradona", "coolio"},
   924  		},
   925  		{
   926  			name: "running jenkins jobs",
   927  			pjs: []prowapi.ProwJob{
   928  				{
   929  					Spec: prowapi.ProwJobSpec{
   930  						Job:   "coolio",
   931  						Agent: "jenkins",
   932  						JenkinsSpec: &prowapi.JenkinsSpec{
   933  							GitHubBranchSourceJob: true,
   934  						},
   935  						Refs: &prowapi.Refs{
   936  							BaseRef: "master",
   937  							Pulls: []prowapi.Pull{{
   938  								Number: 12,
   939  							}},
   940  						},
   941  					},
   942  					Status: prowapi.ProwJobStatus{},
   943  				},
   944  				{
   945  					Spec: prowapi.ProwJobSpec{
   946  						Job:   "maradona",
   947  						Agent: "jenkins",
   948  						JenkinsSpec: &prowapi.JenkinsSpec{
   949  							GitHubBranchSourceJob: true,
   950  						},
   951  						Refs: &prowapi.Refs{
   952  							BaseRef: "master",
   953  						},
   954  					},
   955  					Status: prowapi.ProwJobStatus{},
   956  				},
   957  			},
   958  			expected: []string{"maradona/job/master", "coolio/view/change-requests/job/PR-12"},
   959  		},
   960  	}
   961  
   962  	for _, test := range tests {
   963  		t.Logf("scenario %q", test.name)
   964  		got := getJenkinsJobs(test.pjs)
   965  		if len(got) != len(test.expected) {
   966  			t.Errorf("unexpected job amount: %d (%v), expected: %d (%v)",
   967  				len(got), got, len(test.expected), test.expected)
   968  		}
   969  		for _, ej := range test.expected {
   970  			var found bool
   971  			for _, gj := range got {
   972  				if ej == gj.JobName {
   973  					found = true
   974  					break
   975  				}
   976  			}
   977  			if !found {
   978  				t.Errorf("expected jobs: %v\ngot: %v", test.expected, got)
   979  			}
   980  		}
   981  	}
   982  }
   983  
   984  func TestOperatorConfig(t *testing.T) {
   985  	tests := []struct {
   986  		name string
   987  
   988  		operators     []config.JenkinsOperator
   989  		labelSelector string
   990  
   991  		expected config.Controller
   992  	}{
   993  		{
   994  			name: "single operator config",
   995  
   996  			operators:     nil, // defaults to a single operator
   997  			labelSelector: "",
   998  
   999  			expected: config.Controller{
  1000  				JobURLTemplate: template.Must(template.New("test").Parse("{{.Status.PodName}}/{{.Status.State}}")),
  1001  				MaxConcurrency: 10,
  1002  				MaxGoroutines:  20,
  1003  			},
  1004  		},
  1005  		{
  1006  			name: "single operator config, --label-selector used",
  1007  
  1008  			operators:     nil, // defaults to a single operator
  1009  			labelSelector: "master=ci.jenkins.org",
  1010  
  1011  			expected: config.Controller{
  1012  				JobURLTemplate: template.Must(template.New("test").Parse("{{.Status.PodName}}/{{.Status.State}}")),
  1013  				MaxConcurrency: 10,
  1014  				MaxGoroutines:  20,
  1015  			},
  1016  		},
  1017  		{
  1018  			name: "multiple operator config",
  1019  
  1020  			operators: []config.JenkinsOperator{
  1021  				{
  1022  					Controller: config.Controller{
  1023  						JobURLTemplate: template.Must(template.New("test").Parse("{{.Status.PodName}}/{{.Status.State}}")),
  1024  						MaxConcurrency: 5,
  1025  						MaxGoroutines:  10,
  1026  					},
  1027  					LabelSelectorString: "master=ci.openshift.org",
  1028  				},
  1029  				{
  1030  					Controller: config.Controller{
  1031  						MaxConcurrency: 100,
  1032  						MaxGoroutines:  100,
  1033  					},
  1034  					LabelSelectorString: "master=ci.jenkins.org",
  1035  				},
  1036  			},
  1037  			labelSelector: "master=ci.jenkins.org",
  1038  
  1039  			expected: config.Controller{
  1040  				MaxConcurrency: 100,
  1041  				MaxGoroutines:  100,
  1042  			},
  1043  		},
  1044  	}
  1045  
  1046  	for _, test := range tests {
  1047  		t.Logf("scenario %q", test.name)
  1048  
  1049  		c := Controller{
  1050  			cfg:      newFakeConfigAgent(t, 10, test.operators).Config,
  1051  			selector: test.labelSelector,
  1052  			clock:    clock.RealClock{},
  1053  		}
  1054  
  1055  		got := c.config()
  1056  		if !reflect.DeepEqual(got, test.expected) {
  1057  			t.Errorf("expected controller:\n%#v\ngot controller:\n%#v\n", test.expected, got)
  1058  		}
  1059  	}
  1060  }
  1061  
  1062  func TestSyncAbortedJob(t *testing.T) {
  1063  	testCases := []struct {
  1064  		name           string
  1065  		hasBuild       bool
  1066  		abortErrors    bool
  1067  		expectAbort    bool
  1068  		expectComplete bool
  1069  	}{
  1070  		{
  1071  			name:           "Build is aborted",
  1072  			hasBuild:       true,
  1073  			expectAbort:    true,
  1074  			expectComplete: true,
  1075  		},
  1076  		{
  1077  			name:           "No build, no abort",
  1078  			hasBuild:       false,
  1079  			expectAbort:    false,
  1080  			expectComplete: true,
  1081  		},
  1082  		{
  1083  			name:           "Abort errors, job is not marked completed",
  1084  			hasBuild:       true,
  1085  			abortErrors:    true,
  1086  			expectComplete: false,
  1087  		},
  1088  	}
  1089  
  1090  	for _, tc := range testCases {
  1091  		t.Run(tc.name, func(t *testing.T) {
  1092  
  1093  			pj := &prowapi.ProwJob{
  1094  				ObjectMeta: metav1.ObjectMeta{
  1095  					Name: "my-pj",
  1096  				},
  1097  				Status: prowapi.ProwJobStatus{
  1098  					State: prowapi.AbortedState,
  1099  				},
  1100  			}
  1101  
  1102  			var buildMap map[string]Build
  1103  			if tc.hasBuild {
  1104  				buildMap = map[string]Build{pj.Name: {}}
  1105  			}
  1106  			pjClient := fake.NewSimpleClientset(pj)
  1107  			jobClient := &fjc{abortErrors: tc.abortErrors}
  1108  			c := &Controller{
  1109  				log:           logrus.NewEntry(logrus.New()),
  1110  				prowJobClient: pjClient.ProwV1().ProwJobs(""),
  1111  				jc:            jobClient,
  1112  			}
  1113  
  1114  			if err := c.syncAbortedJob(*pj, nil, buildMap); (err != nil) != tc.abortErrors {
  1115  				t.Fatalf("syncAbortedJob failed: %v", err)
  1116  			}
  1117  
  1118  			pj, err := pjClient.ProwV1().ProwJobs("").Get(context.Background(), pj.Name, metav1.GetOptions{})
  1119  			if err != nil {
  1120  				t.Fatalf("failed to get prowjob: %v", err)
  1121  			}
  1122  
  1123  			if pj.Complete() != tc.expectComplete {
  1124  				t.Errorf("expected completed job: %t, got completed job: %t", tc.expectComplete, pj.Complete())
  1125  			}
  1126  
  1127  			if jobClient.didAbort != tc.expectAbort {
  1128  				t.Errorf("expected abort: %t, did abort: %t", tc.expectAbort, jobClient.didAbort)
  1129  			}
  1130  		})
  1131  	}
  1132  }