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

     1  /*
     2  Copyright 2018 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 statusreconciler
    18  
    19  import (
    20  	"errors"
    21  	"testing"
    22  
    23  	"github.com/google/go-cmp/cmp"
    24  	"github.com/google/go-cmp/cmp/cmpopts"
    25  	"github.com/sirupsen/logrus"
    26  	"k8s.io/apimachinery/pkg/util/sets"
    27  	"sigs.k8s.io/yaml"
    28  
    29  	"sigs.k8s.io/prow/pkg/config"
    30  	"sigs.k8s.io/prow/pkg/github"
    31  )
    32  
    33  var ignoreUnexported = cmpopts.IgnoreUnexported(config.Presubmit{}, config.RegexpChangeMatcher{}, config.Brancher{})
    34  
    35  func TestAddedBlockingPresubmits(t *testing.T) {
    36  	var testCases = []struct {
    37  		name     string
    38  		old, new string
    39  		expected map[string][]config.Presubmit
    40  	}{
    41  		{
    42  			name: "no change in blocking presubmits means no added blocking jobs",
    43  			old: `"org/repo":
    44  - name: old-job
    45    context: old-context
    46    always_run: true`,
    47  			new: `"org/repo":
    48  - name: old-job
    49    context: old-context
    50    always_run: true`,
    51  			expected: map[string][]config.Presubmit{
    52  				"org/repo": {},
    53  			},
    54  		},
    55  		{
    56  			name: "added optional presubmit means no added blocking jobs",
    57  			old: `"org/repo":
    58  - name: old-job
    59    context: old-context
    60    always_run: true`,
    61  			new: `"org/repo":
    62  - name: old-job
    63    context: old-context
    64    always_run: true
    65  - name: new-job
    66    context: new-context
    67    always_run: true
    68    optional: true`,
    69  			expected: map[string][]config.Presubmit{
    70  				"org/repo": {},
    71  			},
    72  		},
    73  		{
    74  			name: "added non-reporting presubmit means no added blocking jobs",
    75  			old: `"org/repo":
    76  - name: old-job
    77    context: old-context
    78    always_run: true`,
    79  			new: `"org/repo":
    80  - name: old-job
    81    context: old-context
    82    always_run: true
    83  - name: new-job
    84    context: new-context
    85    always_run: true
    86    skip_report: true`,
    87  			expected: map[string][]config.Presubmit{
    88  				"org/repo": {},
    89  			},
    90  		},
    91  		{
    92  			name: "added presubmit that needs a manual trigger means no added blocking jobs",
    93  			old: `"org/repo":
    94  - name: old-job
    95    context: old-context
    96    always_run: true`,
    97  			new: `"org/repo":
    98  - name: old-job
    99    context: old-context
   100    always_run: true
   101  - name: new-job
   102    context: new-context
   103    always_run: false`,
   104  			expected: map[string][]config.Presubmit{
   105  				"org/repo": {},
   106  			},
   107  		},
   108  		{
   109  			name: "added required presubmit means added blocking jobs",
   110  			old: `"org/repo":
   111  - name: old-job
   112    context: old-context
   113    always_run: true`,
   114  			new: `"org/repo":
   115  - name: old-job
   116    context: old-context
   117    always_run: true
   118  - name: new-job
   119    context: new-context
   120    always_run: true`,
   121  			expected: map[string][]config.Presubmit{
   122  				"org/repo": {{
   123  					JobBase: config.JobBase{Name: "new-job"},
   124  					Reporter: config.Reporter{
   125  						Context:    "new-context",
   126  						SkipReport: false,
   127  					},
   128  					AlwaysRun: true,
   129  					Optional:  false,
   130  				}},
   131  			},
   132  		},
   133  		{
   134  			name: "optional presubmit transitioning to required means no added blocking jobs",
   135  			old: `"org/repo":
   136  - name: old-job
   137    context: old-context
   138    always_run: true
   139    optional: true`,
   140  			new: `"org/repo":
   141  - name: old-job
   142    context: old-context
   143    always_run: true`,
   144  			expected: map[string][]config.Presubmit{
   145  				"org/repo": {},
   146  			},
   147  		},
   148  		{
   149  			name: "non-reporting presubmit transitioning to required means added blocking jobs",
   150  			old: `"org/repo":
   151  - name: old-job
   152    context: old-context
   153    always_run: true
   154    skip_report: true`,
   155  			new: `"org/repo":
   156  - name: old-job
   157    context: old-context
   158    always_run: true`,
   159  			expected: map[string][]config.Presubmit{
   160  				"org/repo": {{
   161  					JobBase:   config.JobBase{Name: "old-job"},
   162  					Reporter:  config.Reporter{Context: "old-context"},
   163  					AlwaysRun: true,
   164  				}},
   165  			},
   166  		},
   167  		{
   168  			name: "required presubmit transitioning run_if_changed means added blocking jobs",
   169  			old: `"org/repo":
   170  - name: old-job
   171    context: old-context
   172    run_if_changed: old-changes`,
   173  			new: `"org/repo":
   174  - name: old-job
   175    context: old-context
   176    run_if_changed: new-changes`,
   177  			expected: map[string][]config.Presubmit{
   178  				"org/repo": {{
   179  					JobBase:             config.JobBase{Name: "old-job"},
   180  					Reporter:            config.Reporter{Context: "old-context"},
   181  					RegexpChangeMatcher: config.RegexpChangeMatcher{RunIfChanged: "new-changes"},
   182  				}},
   183  			},
   184  		},
   185  		{
   186  			name: "optional presubmit transitioning run_if_changed means no added blocking jobs",
   187  			old: `"org/repo":
   188  - name: old-job
   189    context: old-context
   190    run_if_changed: old-changes
   191    optional: true`,
   192  			new: `"org/repo":
   193  - name: old-job
   194    context: old-context
   195    run_if_changed: new-changes
   196    optional: true`,
   197  			expected: map[string][]config.Presubmit{
   198  				"org/repo": {},
   199  			},
   200  		},
   201  		{
   202  			name: "optional presubmit transitioning to required run_if_changed means added blocking jobs",
   203  			old: `"org/repo":
   204  - name: old-job
   205    context: old-context
   206    always_run: true
   207    optional: true`,
   208  			new: `"org/repo":
   209  - name: old-job
   210    context: old-context
   211    run_if_changed: changes`,
   212  			expected: map[string][]config.Presubmit{
   213  				"org/repo": {{
   214  					JobBase:             config.JobBase{Name: "old-job"},
   215  					Reporter:            config.Reporter{Context: "old-context"},
   216  					RegexpChangeMatcher: config.RegexpChangeMatcher{RunIfChanged: "changes"},
   217  				}},
   218  			},
   219  		},
   220  		{
   221  			name: "required presubmit transitioning to new context means no added blocking jobs",
   222  			old: `"org/repo":
   223  - name: old-job
   224    context: old-context
   225    always_run: true`,
   226  			new: `"org/repo":
   227  - name: old-job
   228    context: new-context
   229    always_run: true`,
   230  			expected: map[string][]config.Presubmit{
   231  				"org/repo": {},
   232  			},
   233  		},
   234  	}
   235  
   236  	for _, testCase := range testCases {
   237  		t.Run(testCase.name, func(t *testing.T) {
   238  			var oldConfig, newConfig map[string][]config.Presubmit
   239  			if err := yaml.Unmarshal([]byte(testCase.old), &oldConfig); err != nil {
   240  				t.Fatalf("%s: could not unmarshal old config: %v", testCase.name, err)
   241  			}
   242  			if err := yaml.Unmarshal([]byte(testCase.new), &newConfig); err != nil {
   243  				t.Fatalf("%s: could not unmarshal new config: %v", testCase.name, err)
   244  			}
   245  			actual, _ := addedBlockingPresubmits(oldConfig, newConfig, logrusEntry())
   246  			if diff := cmp.Diff(actual, testCase.expected, ignoreUnexported); diff != "" {
   247  				t.Errorf("%s: did not get correct added presubmits: %v", testCase.name, diff)
   248  			}
   249  		})
   250  	}
   251  }
   252  
   253  func TestRemovedPresubmits(t *testing.T) {
   254  	var testCases = []struct {
   255  		name     string
   256  		old, new string
   257  		expected map[string][]config.Presubmit
   258  	}{
   259  		{
   260  			name: "no change in blocking presubmits means no removed jobs",
   261  			old: `"org/repo":
   262  - name: old-job
   263    context: old-context`,
   264  			new: `"org/repo":
   265  - name: old-job
   266    context: old-context`,
   267  			expected: map[string][]config.Presubmit{
   268  				"org/repo": {},
   269  			},
   270  		},
   271  		{
   272  			name: "removed optional presubmit means removed job",
   273  			old: `"org/repo":
   274  - name: old-job
   275    context: old-context
   276    optional: true`,
   277  			new: `"org/repo": []`,
   278  			expected: map[string][]config.Presubmit{
   279  				"org/repo": {{
   280  					JobBase:  config.JobBase{Name: "old-job"},
   281  					Reporter: config.Reporter{Context: "old-context"},
   282  					Optional: true,
   283  				}},
   284  			},
   285  		},
   286  		{
   287  			name: "removed non-reporting presubmit means removed job",
   288  			old: `"org/repo":
   289  - name: old-job
   290    context: old-context
   291    skip_report: true`,
   292  			new: `"org/repo": []`,
   293  			expected: map[string][]config.Presubmit{
   294  				"org/repo": {{
   295  					JobBase:  config.JobBase{Name: "old-job"},
   296  					Reporter: config.Reporter{Context: "old-context", SkipReport: true},
   297  				}},
   298  			},
   299  		},
   300  		{
   301  			name: "removed required presubmit means removed jobs",
   302  			old: `"org/repo":
   303  - name: old-job
   304    context: old-context`,
   305  			new: `"org/repo": []`,
   306  			expected: map[string][]config.Presubmit{
   307  				"org/repo": {{
   308  					JobBase:  config.JobBase{Name: "old-job"},
   309  					Reporter: config.Reporter{Context: "old-context"},
   310  				}},
   311  			},
   312  		},
   313  		{
   314  			name: "required presubmit transitioning to optional means no removed jobs",
   315  			old: `"org/repo":
   316  - name: old-job
   317    context: old-context`,
   318  			new: `"org/repo":
   319  - name: old-job
   320    context: old-context
   321    optional: true`,
   322  			expected: map[string][]config.Presubmit{
   323  				"org/repo": {},
   324  			},
   325  		},
   326  		{
   327  			name: "reporting presubmit transitioning to non-reporting means no removed jobs",
   328  			old: `"org/repo":
   329  - name: old-job
   330    context: old-context`,
   331  			new: `"org/repo":
   332  - name: old-job
   333    context: old-context
   334    skip_report: true`,
   335  			expected: map[string][]config.Presubmit{
   336  				"org/repo": {},
   337  			},
   338  		},
   339  		{
   340  			name: "all presubmits removed means removed jobs",
   341  			old: `"org/repo":
   342  - name: old-job
   343    context: old-context`,
   344  			new: `{}`,
   345  			expected: map[string][]config.Presubmit{
   346  				"org/repo": {{
   347  					JobBase:  config.JobBase{Name: "old-job"},
   348  					Reporter: config.Reporter{Context: "old-context"},
   349  				}},
   350  			},
   351  		},
   352  		{
   353  			name: "required presubmit transitioning to new context means no removed jobs",
   354  			old: `"org/repo":
   355  - name: old-job
   356    context: old-context`,
   357  			new: `"org/repo":
   358  - name: old-job
   359    context: new-context`,
   360  			expected: map[string][]config.Presubmit{
   361  				"org/repo": {},
   362  			},
   363  		},
   364  		{
   365  			name: "required presubmit transitioning run_if_changed means no removed jobs",
   366  			old: `"org/repo":
   367  - name: old-job
   368    context: old-context
   369    run_if_changed: old-changes`,
   370  			new: `"org/repo":
   371  - name: old-job
   372    context: old-context
   373    run_if_changed: new-changes`,
   374  			expected: map[string][]config.Presubmit{
   375  				"org/repo": {},
   376  			},
   377  		},
   378  		{
   379  			name: "optional presubmit transitioning to required run_if_changed means no removed jobs",
   380  			old: `"org/repo":
   381  - name: old-job
   382    context: old-context
   383    optional: true`,
   384  			new: `"org/repo":
   385  - name: old-job
   386    context: old-context
   387    run_if_changed: changes`,
   388  			expected: map[string][]config.Presubmit{
   389  				"org/repo": {},
   390  			},
   391  		},
   392  	}
   393  
   394  	for _, testCase := range testCases {
   395  		t.Run(testCase.name, func(t *testing.T) {
   396  			var oldConfig, newConfig map[string][]config.Presubmit
   397  			if err := yaml.Unmarshal([]byte(testCase.old), &oldConfig); err != nil {
   398  				t.Fatalf("%s: could not unmarshal old config: %v", testCase.name, err)
   399  			}
   400  			if err := yaml.Unmarshal([]byte(testCase.new), &newConfig); err != nil {
   401  				t.Fatalf("%s: could not unmarshal new config: %v", testCase.name, err)
   402  			}
   403  			actual, _ := removedPresubmits(oldConfig, newConfig, logrusEntry())
   404  			if diff := cmp.Diff(actual, testCase.expected, ignoreUnexported); diff != "" {
   405  				t.Errorf("%s: did not get correct removed presubmits: %v", testCase.name, diff)
   406  			}
   407  		})
   408  	}
   409  }
   410  
   411  func TestMigratedBlockingPresubmits(t *testing.T) {
   412  	var testCases = []struct {
   413  		name     string
   414  		old, new string
   415  		expected map[string][]presubmitMigration
   416  	}{
   417  		{
   418  			name: "no change in blocking presubmits means no migrated blocking jobs",
   419  			old: `"org/repo":
   420  - name: old-job
   421    context: old-context`,
   422  			new: `"org/repo":
   423  - name: old-job
   424    context: old-context`,
   425  			expected: map[string][]presubmitMigration{
   426  				"org/repo": {},
   427  			},
   428  		},
   429  		{
   430  			name: "removed optional presubmit means no migrated blocking jobs",
   431  			old: `"org/repo":
   432  - name: old-job
   433    context: old-context
   434    optional: true`,
   435  			new: `"org/repo": []`,
   436  			expected: map[string][]presubmitMigration{
   437  				"org/repo": {},
   438  			},
   439  		},
   440  		{
   441  			name: "removed non-reporting presubmit means no migrated blocking jobs",
   442  			old: `"org/repo":
   443  - name: old-job
   444    context: old-context
   445    skip_report: true`,
   446  			new: `"org/repo": []`,
   447  			expected: map[string][]presubmitMigration{
   448  				"org/repo": {},
   449  			},
   450  		},
   451  		{
   452  			name: "removed required presubmit means no migrated blocking jobs",
   453  			old: `"org/repo":
   454  - name: old-job
   455    context: old-context`,
   456  			new: `"org/repo": []`,
   457  			expected: map[string][]presubmitMigration{
   458  				"org/repo": {},
   459  			},
   460  		},
   461  		{
   462  			name: "required presubmit transitioning to optional means no migrated blocking jobs",
   463  			old: `"org/repo":
   464  - name: old-job
   465    context: old-context`,
   466  			new: `"org/repo":
   467  - name: old-job
   468    context: old-context
   469    optional: true`,
   470  			expected: map[string][]presubmitMigration{
   471  				"org/repo": {},
   472  			},
   473  		},
   474  		{
   475  			name: "reporting presubmit transitioning to non-reporting means no migrated blocking jobs",
   476  			old: `"org/repo":
   477  - name: old-job
   478    context: old-context`,
   479  			new: `"org/repo":
   480  - name: old-job
   481    context: old-context
   482    skip_report: true`,
   483  			expected: map[string][]presubmitMigration{
   484  				"org/repo": {},
   485  			},
   486  		},
   487  		{
   488  			name: "all presubmits removed means no migrated blocking jobs",
   489  			old: `"org/repo":
   490  - name: old-job
   491    context: old-context`,
   492  			new: `{}`,
   493  			expected: map[string][]presubmitMigration{
   494  				"org/repo": {},
   495  			},
   496  		},
   497  		{
   498  			name: "required presubmit transitioning to new context means migrated blocking jobs",
   499  			old: `"org/repo":
   500  - name: old-job
   501    context: old-context`,
   502  			new: `"org/repo":
   503  - name: old-job
   504    context: new-context`,
   505  			expected: map[string][]presubmitMigration{
   506  				"org/repo": {{
   507  					from: config.Presubmit{
   508  						JobBase:  config.JobBase{Name: "old-job"},
   509  						Reporter: config.Reporter{Context: "old-context"},
   510  					},
   511  					to: config.Presubmit{
   512  						JobBase:  config.JobBase{Name: "old-job"},
   513  						Reporter: config.Reporter{Context: "new-context"},
   514  					},
   515  				}},
   516  			},
   517  		},
   518  		{
   519  			name: "required presubmit transitioning run_if_changed means no removed blocking jobs",
   520  			old: `"org/repo":
   521  - name: old-job
   522    context: old-context
   523    run_if_changed: old-changes`,
   524  			new: `"org/repo":
   525  - name: old-job
   526    context: old-context
   527    run_if_changed: new-changes`,
   528  			expected: map[string][]presubmitMigration{
   529  				"org/repo": {},
   530  			},
   531  		},
   532  		{
   533  			name: "optional presubmit transitioning to required run_if_changed means no removed blocking jobs",
   534  			old: `"org/repo":
   535  - name: old-job
   536    context: old-context
   537    optional: true`,
   538  			new: `"org/repo":
   539  - name: old-job
   540    context: old-context
   541    run_if_changed: changes`,
   542  			expected: map[string][]presubmitMigration{
   543  				"org/repo": {},
   544  			},
   545  		},
   546  	}
   547  
   548  	for _, testCase := range testCases {
   549  		t.Run(testCase.name, func(t *testing.T) {
   550  			var oldConfig, newConfig map[string][]config.Presubmit
   551  			if err := yaml.Unmarshal([]byte(testCase.old), &oldConfig); err != nil {
   552  				t.Fatalf("%s: could not unmarshal old config: %v", testCase.name, err)
   553  			}
   554  			if err := yaml.Unmarshal([]byte(testCase.new), &newConfig); err != nil {
   555  				t.Fatalf("%s: could not unmarshal new config: %v", testCase.name, err)
   556  			}
   557  			actual, _ := migratedBlockingPresubmits(oldConfig, newConfig, logrusEntry())
   558  			if diff := cmp.Diff(actual, testCase.expected, ignoreUnexported, cmp.AllowUnexported(presubmitMigration{})); diff != "" {
   559  				t.Errorf("%s: did not get correct removed presubmits: %v", testCase.name, diff)
   560  			}
   561  		})
   562  	}
   563  }
   564  
   565  type orgRepo struct {
   566  	org, repo string
   567  }
   568  
   569  type orgRepoSet map[orgRepo]interface{}
   570  
   571  func (s orgRepoSet) has(item orgRepo) bool {
   572  	_, contained := s[item]
   573  	return contained
   574  }
   575  
   576  type migration struct {
   577  	from, to string
   578  }
   579  
   580  type migrationSet map[migration]interface{}
   581  
   582  func (s migrationSet) insert(items ...migration) {
   583  	for _, item := range items {
   584  		s[item] = nil
   585  	}
   586  }
   587  
   588  func (s migrationSet) has(item migration) bool {
   589  	_, contained := s[item]
   590  	return contained
   591  }
   592  
   593  func newFakeMigrator(key orgRepo) fakeMigrator {
   594  	return fakeMigrator{
   595  		retireErrors:  map[orgRepo]sets.Set[string]{key: sets.New[string]()},
   596  		migrateErrors: map[orgRepo]migrationSet{key: {}},
   597  		retired:       map[orgRepo]sets.Set[string]{key: sets.New[string]()},
   598  		migrated:      map[orgRepo]migrationSet{key: {}},
   599  	}
   600  }
   601  
   602  type fakeMigrator struct {
   603  	retireErrors  map[orgRepo]sets.Set[string]
   604  	migrateErrors map[orgRepo]migrationSet
   605  
   606  	retired  map[orgRepo]sets.Set[string]
   607  	migrated map[orgRepo]migrationSet
   608  }
   609  
   610  func (m *fakeMigrator) retire(org, repo, context string, _ func(string) bool) error {
   611  	key := orgRepo{org: org, repo: repo}
   612  	if contexts, exist := m.retireErrors[key]; exist && contexts.Has(context) {
   613  		return errors.New("failed to retire context")
   614  	}
   615  	if _, exist := m.retired[key]; exist {
   616  		m.retired[key].Insert(context)
   617  	} else {
   618  		m.retired[key] = sets.New[string](context)
   619  	}
   620  	return nil
   621  }
   622  
   623  func (m *fakeMigrator) migrate(org, repo, from, to string, _ func(string) bool) error {
   624  	key := orgRepo{org: org, repo: repo}
   625  	item := migration{from: from, to: to}
   626  	if contexts, exist := m.migrateErrors[key]; exist && contexts.has(item) {
   627  		return errors.New("failed to migrate context")
   628  	}
   629  	if _, exist := m.migrated[key]; exist {
   630  		m.migrated[key].insert(item)
   631  	} else {
   632  		newSet := migrationSet{}
   633  		newSet.insert(item)
   634  		m.migrated[key] = newSet
   635  	}
   636  	return nil
   637  }
   638  
   639  func newfakeProwJobTriggerer() fakeProwJobTriggerer {
   640  	return fakeProwJobTriggerer{
   641  		errors:  map[prKey]sets.Set[string]{},
   642  		created: map[prKey]sets.Set[string]{},
   643  	}
   644  }
   645  
   646  type prKey struct {
   647  	org, repo string
   648  	num       int
   649  }
   650  
   651  type fakeProwJobTriggerer struct {
   652  	errors  map[prKey]sets.Set[string]
   653  	created map[prKey]sets.Set[string]
   654  }
   655  
   656  func (c *fakeProwJobTriggerer) runAndSkip(pr *github.PullRequest, requestedJobs []config.Presubmit) error {
   657  	actions := []struct {
   658  		jobs    []config.Presubmit
   659  		records map[prKey]sets.Set[string]
   660  	}{
   661  		{
   662  			jobs:    requestedJobs,
   663  			records: c.created,
   664  		},
   665  	}
   666  	for _, action := range actions {
   667  		names := sets.New[string]()
   668  		key := prKey{org: pr.Base.Repo.Owner.Login, repo: pr.Base.Repo.Name, num: pr.Number}
   669  		for _, job := range action.jobs {
   670  			if jobErrors, exists := c.errors[key]; exists && jobErrors.Has(job.Name) {
   671  				return errors.New("failed to trigger prow job")
   672  			}
   673  			names.Insert(job.Name)
   674  		}
   675  		if current, exists := action.records[key]; exists {
   676  			action.records[key] = current.Union(names)
   677  		} else {
   678  			action.records[key] = names
   679  		}
   680  	}
   681  	return nil
   682  }
   683  
   684  func newFakeGitHubClient(key orgRepo) fakeGitHubClient {
   685  	return fakeGitHubClient{
   686  		prErrors:  orgRepoSet{},
   687  		refErrors: map[orgRepo]sets.Set[string]{key: sets.New[string]()},
   688  		prs:       map[orgRepo][]github.PullRequest{key: {}},
   689  		refs:      map[orgRepo]map[string]string{key: {}},
   690  	}
   691  }
   692  
   693  type fakeGitHubClient struct {
   694  	prErrors     orgRepoSet
   695  	refErrors    map[orgRepo]sets.Set[string]
   696  	changeErrors map[orgRepo]sets.Set[int]
   697  
   698  	prs     map[orgRepo][]github.PullRequest
   699  	refs    map[orgRepo]map[string]string
   700  	changes map[orgRepo]map[int][]github.PullRequestChange
   701  }
   702  
   703  func (c *fakeGitHubClient) GetPullRequests(org, repo string) ([]github.PullRequest, error) {
   704  	key := orgRepo{org: org, repo: repo}
   705  	if c.prErrors.has(key) {
   706  		return nil, errors.New("failed to get PRs")
   707  	}
   708  	return c.prs[key], nil
   709  }
   710  
   711  func (c *fakeGitHubClient) GetPullRequestChanges(org, repo string, number int) ([]github.PullRequestChange, error) {
   712  	key := orgRepo{org: org, repo: repo}
   713  	if changes, exist := c.changeErrors[key]; exist && changes.Has(number) {
   714  		return nil, errors.New("failed to get changes")
   715  	}
   716  	return c.changes[key][number], nil
   717  }
   718  
   719  func (c *fakeGitHubClient) GetRef(org, repo, ref string) (string, error) {
   720  	key := orgRepo{org: org, repo: repo}
   721  	if refs, exist := c.refErrors[key]; exist && refs.Has(ref) {
   722  		return "", errors.New("failed to get ref")
   723  	}
   724  	return c.refs[key][ref], nil
   725  }
   726  
   727  type prAuthor struct {
   728  	pr     int
   729  	author string
   730  }
   731  
   732  type prAuthorSet map[prAuthor]interface{}
   733  
   734  func (s prAuthorSet) has(item prAuthor) bool {
   735  	_, contained := s[item]
   736  	return contained
   737  }
   738  
   739  func newFakeTrustedChecker(key orgRepo) fakeTrustedChecker {
   740  	return fakeTrustedChecker{
   741  		errors:  map[orgRepo]prAuthorSet{key: {}},
   742  		trusted: map[orgRepo]map[prAuthor]bool{key: {}},
   743  	}
   744  }
   745  
   746  type fakeTrustedChecker struct {
   747  	errors map[orgRepo]prAuthorSet
   748  
   749  	trusted map[orgRepo]map[prAuthor]bool
   750  }
   751  
   752  func (c *fakeTrustedChecker) trustedPullRequest(author, org, repo string, num int) (bool, error) {
   753  	key := orgRepo{org: org, repo: repo}
   754  	item := prAuthor{pr: num, author: author}
   755  	if errs, exist := c.errors[key]; exist && errs.has(item) {
   756  		return false, errors.New("failed to check trusted")
   757  	}
   758  	return c.trusted[key][item], nil
   759  }
   760  
   761  func TestControllerReconcile(t *testing.T) {
   762  	// the diff from these configs causes:
   763  	//  - deletion (required-job),
   764  	//  - creation (new-required-job)
   765  	//  - migration (other-required-job)
   766  	oldConfigData := `presubmits:
   767    "org/repo":
   768    - name: required-job
   769      context: required-job
   770      always_run: true
   771    - name: other-required-job
   772      context: other-required-job
   773      always_run: true`
   774  	newConfigData := `presubmits:
   775    "org/repo":
   776    - name: other-required-job
   777      context: new-context
   778      always_run: true
   779    - name: new-required-job
   780      context: new-required-context
   781      always_run: true
   782      branches:
   783      - base`
   784  
   785  	var oldConfig, newConfig config.Config
   786  	if err := yaml.Unmarshal([]byte(oldConfigData), &oldConfig); err != nil {
   787  		t.Fatalf("could not unmarshal old config: %v", err)
   788  	}
   789  	for _, presubmits := range oldConfig.PresubmitsStatic {
   790  		if err := config.SetPresubmitRegexes(presubmits); err != nil {
   791  			t.Fatalf("could not set presubmit regexes for old config: %v", err)
   792  		}
   793  	}
   794  	if err := yaml.Unmarshal([]byte(newConfigData), &newConfig); err != nil {
   795  		t.Fatalf("could not unmarshal new config: %v", err)
   796  	}
   797  	for _, presubmits := range newConfig.PresubmitsStatic {
   798  		if err := config.SetPresubmitRegexes(presubmits); err != nil {
   799  			t.Fatalf("could not set presubmit regexes for new config: %v", err)
   800  		}
   801  	}
   802  	delta := config.Delta{Before: oldConfig, After: newConfig}
   803  	migrate := migration{from: "other-required-job", to: "new-context"}
   804  	org, repo := "org", "repo"
   805  	orgRepoKey := orgRepo{org: org, repo: repo}
   806  	prNumber := 1
   807  	secondPrNumber := 2
   808  	thirdPrNumber := 3
   809  	author := "user"
   810  	prAuthorKey := prAuthor{author: author, pr: prNumber}
   811  	secondPrAuthorKey := prAuthor{author: author, pr: secondPrNumber}
   812  	thirdPrAuthorKey := prAuthor{author: author, pr: thirdPrNumber}
   813  	prOrgRepoKey := prKey{org: org, repo: repo, num: prNumber}
   814  	thirdPrOrgRepoKey := prKey{org: org, repo: repo, num: thirdPrNumber}
   815  	baseRef := "base"
   816  	otherBaseRef := "other"
   817  	baseSha := "abc"
   818  	notMergable := false
   819  	pr := github.PullRequest{
   820  		User: github.User{
   821  			Login: author,
   822  		},
   823  		Number: prNumber,
   824  		Base: github.PullRequestBranch{
   825  			Repo: github.Repo{
   826  				Owner: github.User{
   827  					Login: org,
   828  				},
   829  				Name: repo,
   830  			},
   831  			Ref: baseRef,
   832  		},
   833  		Head: github.PullRequestBranch{
   834  			SHA: "prsha",
   835  		},
   836  	}
   837  	secondPr := github.PullRequest{
   838  		User: github.User{
   839  			Login: author,
   840  		},
   841  		Number: secondPrNumber,
   842  		Base: github.PullRequestBranch{
   843  			Repo: github.Repo{
   844  				Owner: github.User{
   845  					Login: org,
   846  				},
   847  				Name: repo,
   848  			},
   849  			Ref: baseRef,
   850  		},
   851  		Head: github.PullRequestBranch{
   852  			SHA: "prsha2",
   853  		},
   854  		Mergable: &notMergable,
   855  	}
   856  	thirdPr := github.PullRequest{
   857  		User: github.User{
   858  			Login: author,
   859  		},
   860  		Number: thirdPrNumber,
   861  		Base: github.PullRequestBranch{
   862  			Repo: github.Repo{
   863  				Owner: github.User{
   864  					Login: org,
   865  				},
   866  				Name: repo,
   867  			},
   868  			Ref: otherBaseRef,
   869  		},
   870  		Head: github.PullRequestBranch{
   871  			SHA: "prsha3",
   872  		},
   873  	}
   874  	var testCases = []struct {
   875  		name string
   876  		// generator creates the controller and a func that checks
   877  		// the internal state of the fakes in the controller
   878  		generator func() (Controller, func(*testing.T))
   879  		expectErr bool
   880  	}{
   881  		{
   882  			name: "ignored org skips creation, retire and migrate",
   883  			generator: func() (Controller, func(*testing.T)) {
   884  				fpjt := newfakeProwJobTriggerer()
   885  				fghc := newFakeGitHubClient(orgRepoKey)
   886  				fghc.prs[orgRepoKey] = []github.PullRequest{pr}
   887  				fghc.refs[orgRepoKey]["heads/"+pr.Base.Ref] = baseSha
   888  				fsm := newFakeMigrator(orgRepoKey)
   889  				ftc := newFakeTrustedChecker(orgRepoKey)
   890  				ftc.trusted[orgRepoKey][prAuthorKey] = true
   891  				controller := Controller{
   892  					continueOnError:        true,
   893  					addedPresubmitDenylist: sets.New[string]("org"),
   894  					prowJobTriggerer:       &fpjt,
   895  					githubClient:           &fghc,
   896  					statusMigrator:         &fsm,
   897  					trustedChecker:         &ftc,
   898  				}
   899  				checker := func(t *testing.T) {
   900  					checkTriggerer(t, fpjt, map[prKey]sets.Set[string]{})
   901  					checkMigrator(t, fsm, map[orgRepo]sets.Set[string]{orgRepoKey: sets.New[string]("required-job")}, map[orgRepo]migrationSet{orgRepoKey: {migrate: nil}})
   902  				}
   903  				return controller, checker
   904  			},
   905  		},
   906  		{
   907  			name: "ignored org/repo skips creation, retire and migrate",
   908  			generator: func() (Controller, func(*testing.T)) {
   909  				fpjt := newfakeProwJobTriggerer()
   910  				fghc := newFakeGitHubClient(orgRepoKey)
   911  				fghc.prs[orgRepoKey] = []github.PullRequest{pr}
   912  				fghc.refs[orgRepoKey]["heads/"+pr.Base.Ref] = baseSha
   913  				fsm := newFakeMigrator(orgRepoKey)
   914  				ftc := newFakeTrustedChecker(orgRepoKey)
   915  				ftc.trusted[orgRepoKey][prAuthorKey] = true
   916  				controller := Controller{
   917  					continueOnError:        true,
   918  					addedPresubmitDenylist: sets.New[string]("org/repo"),
   919  					prowJobTriggerer:       &fpjt,
   920  					githubClient:           &fghc,
   921  					statusMigrator:         &fsm,
   922  					trustedChecker:         &ftc,
   923  				}
   924  				checker := func(t *testing.T) {
   925  					checkTriggerer(t, fpjt, map[prKey]sets.Set[string]{})
   926  					checkMigrator(t, fsm, map[orgRepo]sets.Set[string]{orgRepoKey: sets.New[string]("required-job")}, map[orgRepo]migrationSet{orgRepoKey: {migrate: nil}})
   927  				}
   928  				return controller, checker
   929  			},
   930  		},
   931  		{
   932  			name: "ignored all org skips creation, retire and migrate",
   933  			generator: func() (Controller, func(*testing.T)) {
   934  				fpjt := newfakeProwJobTriggerer()
   935  				fghc := newFakeGitHubClient(orgRepoKey)
   936  				fghc.prs[orgRepoKey] = []github.PullRequest{pr}
   937  				fghc.refs[orgRepoKey]["heads/"+pr.Base.Ref] = baseSha
   938  				fsm := newFakeMigrator(orgRepoKey)
   939  				ftc := newFakeTrustedChecker(orgRepoKey)
   940  				ftc.trusted[orgRepoKey][prAuthorKey] = true
   941  				controller := Controller{
   942  					continueOnError:           true,
   943  					addedPresubmitDenylistAll: sets.New[string]("org"),
   944  					prowJobTriggerer:          &fpjt,
   945  					githubClient:              &fghc,
   946  					statusMigrator:            &fsm,
   947  					trustedChecker:            &ftc,
   948  				}
   949  				checker := func(t *testing.T) {
   950  					checkTriggerer(t, fpjt, map[prKey]sets.Set[string]{})
   951  					checkMigrator(t, fsm, map[orgRepo]sets.Set[string]{orgRepoKey: sets.New[string]()}, map[orgRepo]migrationSet{orgRepoKey: {}})
   952  				}
   953  				return controller, checker
   954  			},
   955  		},
   956  		{
   957  			name: "ignored all org/repo skips creation, retire and migrate",
   958  			generator: func() (Controller, func(*testing.T)) {
   959  				fpjt := newfakeProwJobTriggerer()
   960  				fghc := newFakeGitHubClient(orgRepoKey)
   961  				fghc.prs[orgRepoKey] = []github.PullRequest{pr}
   962  				fghc.refs[orgRepoKey]["heads/"+pr.Base.Ref] = baseSha
   963  				fsm := newFakeMigrator(orgRepoKey)
   964  				ftc := newFakeTrustedChecker(orgRepoKey)
   965  				ftc.trusted[orgRepoKey][prAuthorKey] = true
   966  				controller := Controller{
   967  					continueOnError:           true,
   968  					addedPresubmitDenylistAll: sets.New[string]("org/repo"),
   969  					prowJobTriggerer:          &fpjt,
   970  					githubClient:              &fghc,
   971  					statusMigrator:            &fsm,
   972  					trustedChecker:            &ftc,
   973  				}
   974  				checker := func(t *testing.T) {
   975  					checkTriggerer(t, fpjt, map[prKey]sets.Set[string]{})
   976  					checkMigrator(t, fsm, map[orgRepo]sets.Set[string]{orgRepoKey: sets.New[string]()}, map[orgRepo]migrationSet{orgRepoKey: {}})
   977  				}
   978  				return controller, checker
   979  			},
   980  		},
   981  		{
   982  			name: "no errors and trusted PR means we should see a trigger, retire and migrate",
   983  			generator: func() (Controller, func(*testing.T)) {
   984  				fpjt := newfakeProwJobTriggerer()
   985  				fghc := newFakeGitHubClient(orgRepoKey)
   986  				fghc.prs[orgRepoKey] = []github.PullRequest{pr}
   987  				fghc.refs[orgRepoKey]["heads/"+pr.Base.Ref] = baseSha
   988  				fsm := newFakeMigrator(orgRepoKey)
   989  				ftc := newFakeTrustedChecker(orgRepoKey)
   990  				ftc.trusted[orgRepoKey][prAuthorKey] = true
   991  				controller := Controller{
   992  					continueOnError:        true,
   993  					addedPresubmitDenylist: sets.New[string](),
   994  					prowJobTriggerer:       &fpjt,
   995  					githubClient:           &fghc,
   996  					statusMigrator:         &fsm,
   997  					trustedChecker:         &ftc,
   998  				}
   999  				checker := func(t *testing.T) {
  1000  					expectedProwJob := map[prKey]sets.Set[string]{prOrgRepoKey: sets.New[string]("new-required-job")}
  1001  					checkTriggerer(t, fpjt, expectedProwJob)
  1002  					checkMigrator(t, fsm, map[orgRepo]sets.Set[string]{orgRepoKey: sets.New[string]("required-job")}, map[orgRepo]migrationSet{orgRepoKey: {migrate: nil}})
  1003  				}
  1004  				return controller, checker
  1005  			},
  1006  		},
  1007  		{
  1008  			name: "no errors and untrusted PR means we should see no trigger, a retire and a migrate",
  1009  			generator: func() (Controller, func(*testing.T)) {
  1010  				fpjt := newfakeProwJobTriggerer()
  1011  				fghc := newFakeGitHubClient(orgRepoKey)
  1012  				fghc.prs[orgRepoKey] = []github.PullRequest{pr}
  1013  				fghc.refs[orgRepoKey]["heads/"+pr.Base.Ref] = baseSha
  1014  				fsm := newFakeMigrator(orgRepoKey)
  1015  				ftc := newFakeTrustedChecker(orgRepoKey)
  1016  				ftc.trusted[orgRepoKey][prAuthorKey] = false
  1017  				controller := Controller{
  1018  					continueOnError:        true,
  1019  					addedPresubmitDenylist: sets.New[string](),
  1020  					prowJobTriggerer:       &fpjt,
  1021  					githubClient:           &fghc,
  1022  					statusMigrator:         &fsm,
  1023  					trustedChecker:         &ftc,
  1024  				}
  1025  				checker := func(t *testing.T) {
  1026  					checkTriggerer(t, fpjt, map[prKey]sets.Set[string]{})
  1027  					checkMigrator(t, fsm, map[orgRepo]sets.Set[string]{orgRepoKey: sets.New[string]("required-job")}, map[orgRepo]migrationSet{orgRepoKey: {migrate: nil}})
  1028  				}
  1029  				return controller, checker
  1030  			},
  1031  		},
  1032  		{
  1033  			name: "no errors and unmergable PR means we should see no trigger, a retire and a migrate",
  1034  			generator: func() (Controller, func(*testing.T)) {
  1035  				fpjt := newfakeProwJobTriggerer()
  1036  				fghc := newFakeGitHubClient(orgRepoKey)
  1037  				fghc.prs[orgRepoKey] = []github.PullRequest{secondPr}
  1038  				fghc.refs[orgRepoKey]["heads/"+secondPr.Base.Ref] = baseSha
  1039  				fsm := newFakeMigrator(orgRepoKey)
  1040  				ftc := newFakeTrustedChecker(orgRepoKey)
  1041  				ftc.trusted[orgRepoKey][secondPrAuthorKey] = true
  1042  				controller := Controller{
  1043  					continueOnError:        true,
  1044  					addedPresubmitDenylist: sets.New[string](),
  1045  					prowJobTriggerer:       &fpjt,
  1046  					githubClient:           &fghc,
  1047  					statusMigrator:         &fsm,
  1048  					trustedChecker:         &ftc,
  1049  				}
  1050  				checker := func(t *testing.T) {
  1051  					checkTriggerer(t, fpjt, map[prKey]sets.Set[string]{})
  1052  					checkMigrator(t, fsm, map[orgRepo]sets.Set[string]{orgRepoKey: sets.New[string]("required-job")}, map[orgRepo]migrationSet{orgRepoKey: {migrate: nil}})
  1053  				}
  1054  				return controller, checker
  1055  			},
  1056  		},
  1057  		{
  1058  			name: "no errors and PR that doesn't match the added job means we should see no trigger, a retire and a migrate",
  1059  			generator: func() (Controller, func(*testing.T)) {
  1060  				fpjt := newfakeProwJobTriggerer()
  1061  				fghc := newFakeGitHubClient(orgRepoKey)
  1062  				fghc.prs[orgRepoKey] = []github.PullRequest{thirdPr}
  1063  				fghc.refs[orgRepoKey]["heads/"+thirdPr.Base.Ref] = baseSha
  1064  				fsm := newFakeMigrator(orgRepoKey)
  1065  				ftc := newFakeTrustedChecker(orgRepoKey)
  1066  				ftc.trusted[orgRepoKey][thirdPrAuthorKey] = true
  1067  				controller := Controller{
  1068  					continueOnError:        true,
  1069  					addedPresubmitDenylist: sets.New[string](),
  1070  					prowJobTriggerer:       &fpjt,
  1071  					githubClient:           &fghc,
  1072  					statusMigrator:         &fsm,
  1073  					trustedChecker:         &ftc,
  1074  				}
  1075  				checker := func(t *testing.T) {
  1076  					checkTriggerer(t, fpjt, map[prKey]sets.Set[string]{thirdPrOrgRepoKey: sets.New[string]()})
  1077  					checkMigrator(t, fsm, map[orgRepo]sets.Set[string]{orgRepoKey: sets.New[string]("required-job")}, map[orgRepo]migrationSet{orgRepoKey: {migrate: nil}})
  1078  				}
  1079  				return controller, checker
  1080  			},
  1081  		},
  1082  		{
  1083  			name: "trust check error means we should see no trigger, a retire and a migrate",
  1084  			generator: func() (Controller, func(*testing.T)) {
  1085  				fpjt := newfakeProwJobTriggerer()
  1086  				fghc := newFakeGitHubClient(orgRepoKey)
  1087  				fghc.prs[orgRepoKey] = []github.PullRequest{pr}
  1088  				fghc.refs[orgRepoKey]["heads/"+pr.Base.Ref] = baseSha
  1089  				fsm := newFakeMigrator(orgRepoKey)
  1090  				ftc := newFakeTrustedChecker(orgRepoKey)
  1091  				ftc.errors = map[orgRepo]prAuthorSet{orgRepoKey: {prAuthorKey: nil}}
  1092  				controller := Controller{
  1093  					continueOnError:        true,
  1094  					addedPresubmitDenylist: sets.New[string](),
  1095  					prowJobTriggerer:       &fpjt,
  1096  					githubClient:           &fghc,
  1097  					statusMigrator:         &fsm,
  1098  					trustedChecker:         &ftc,
  1099  				}
  1100  				checker := func(t *testing.T) {
  1101  					checkTriggerer(t, fpjt, map[prKey]sets.Set[string]{})
  1102  					checkMigrator(t, fsm, map[orgRepo]sets.Set[string]{orgRepoKey: sets.New[string]("required-job")}, map[orgRepo]migrationSet{orgRepoKey: {migrate: nil}})
  1103  				}
  1104  				return controller, checker
  1105  			},
  1106  			expectErr: true,
  1107  		},
  1108  		{
  1109  			name: "trigger error means we should see no trigger, a retire and a migrate",
  1110  			generator: func() (Controller, func(*testing.T)) {
  1111  				fpjt := newfakeProwJobTriggerer()
  1112  				fpjt.errors[prOrgRepoKey] = sets.New[string]("new-required-job")
  1113  				fghc := newFakeGitHubClient(orgRepoKey)
  1114  				fghc.prs[orgRepoKey] = []github.PullRequest{pr}
  1115  				fghc.refs[orgRepoKey]["heads/"+pr.Base.Ref] = baseSha
  1116  				fsm := newFakeMigrator(orgRepoKey)
  1117  				ftc := newFakeTrustedChecker(orgRepoKey)
  1118  				ftc.errors = map[orgRepo]prAuthorSet{orgRepoKey: {prAuthorKey: nil}}
  1119  				controller := Controller{
  1120  					continueOnError:        true,
  1121  					addedPresubmitDenylist: sets.New[string](),
  1122  					prowJobTriggerer:       &fpjt,
  1123  					githubClient:           &fghc,
  1124  					statusMigrator:         &fsm,
  1125  					trustedChecker:         &ftc,
  1126  				}
  1127  				checker := func(t *testing.T) {
  1128  					checkTriggerer(t, fpjt, map[prKey]sets.Set[string]{})
  1129  					checkMigrator(t, fsm, map[orgRepo]sets.Set[string]{orgRepoKey: sets.New[string]("required-job")}, map[orgRepo]migrationSet{orgRepoKey: {migrate: nil}})
  1130  				}
  1131  				return controller, checker
  1132  			},
  1133  			expectErr: true,
  1134  		},
  1135  		{
  1136  			name: "retire errors and trusted PR means we should see a trigger and migrate",
  1137  			generator: func() (Controller, func(*testing.T)) {
  1138  				fpjt := newfakeProwJobTriggerer()
  1139  				fghc := newFakeGitHubClient(orgRepoKey)
  1140  				fghc.prs[orgRepoKey] = []github.PullRequest{pr}
  1141  				fghc.refs[orgRepoKey]["heads/"+pr.Base.Ref] = baseSha
  1142  				fsm := newFakeMigrator(orgRepoKey)
  1143  				fsm.retireErrors = map[orgRepo]sets.Set[string]{orgRepoKey: sets.New[string]("required-job")}
  1144  				ftc := newFakeTrustedChecker(orgRepoKey)
  1145  				ftc.trusted[orgRepoKey][prAuthorKey] = true
  1146  				controller := Controller{
  1147  					continueOnError:        true,
  1148  					addedPresubmitDenylist: sets.New[string](),
  1149  					prowJobTriggerer:       &fpjt,
  1150  					githubClient:           &fghc,
  1151  					statusMigrator:         &fsm,
  1152  					trustedChecker:         &ftc,
  1153  				}
  1154  				checker := func(t *testing.T) {
  1155  					expectedProwJob := map[prKey]sets.Set[string]{prOrgRepoKey: sets.New[string]("new-required-job")}
  1156  					checkTriggerer(t, fpjt, expectedProwJob)
  1157  					checkMigrator(t, fsm, map[orgRepo]sets.Set[string]{orgRepoKey: sets.New[string]()}, map[orgRepo]migrationSet{orgRepoKey: {migrate: nil}})
  1158  				}
  1159  				return controller, checker
  1160  			},
  1161  			expectErr: true,
  1162  		},
  1163  		{
  1164  			name: "migrate errors and trusted PR means we should see a trigger and retire",
  1165  			generator: func() (Controller, func(*testing.T)) {
  1166  				fpjt := newfakeProwJobTriggerer()
  1167  				fghc := newFakeGitHubClient(orgRepoKey)
  1168  				fghc.prs[orgRepoKey] = []github.PullRequest{pr}
  1169  				fghc.refs[orgRepoKey]["heads/"+pr.Base.Ref] = baseSha
  1170  				fsm := newFakeMigrator(orgRepoKey)
  1171  				fsm.migrateErrors = map[orgRepo]migrationSet{orgRepoKey: {migrate: nil}}
  1172  				ftc := newFakeTrustedChecker(orgRepoKey)
  1173  				ftc.trusted[orgRepoKey][prAuthorKey] = true
  1174  				controller := Controller{
  1175  					continueOnError:        true,
  1176  					addedPresubmitDenylist: sets.New[string](),
  1177  					prowJobTriggerer:       &fpjt,
  1178  					githubClient:           &fghc,
  1179  					statusMigrator:         &fsm,
  1180  					trustedChecker:         &ftc,
  1181  				}
  1182  				checker := func(t *testing.T) {
  1183  					expectedProwJob := map[prKey]sets.Set[string]{prOrgRepoKey: sets.New[string]("new-required-job")}
  1184  					checkTriggerer(t, fpjt, expectedProwJob)
  1185  					checkMigrator(t, fsm, map[orgRepo]sets.Set[string]{orgRepoKey: sets.New[string]("required-job")}, map[orgRepo]migrationSet{orgRepoKey: {}})
  1186  				}
  1187  				return controller, checker
  1188  			},
  1189  			expectErr: true,
  1190  		},
  1191  	}
  1192  
  1193  	for _, testCase := range testCases {
  1194  		t.Run(testCase.name, func(t *testing.T) {
  1195  			controller, check := testCase.generator()
  1196  			err := controller.reconcile(delta, logrusEntry())
  1197  			if err == nil && testCase.expectErr {
  1198  				t.Errorf("expected an error, but got none")
  1199  			}
  1200  			if err != nil && !testCase.expectErr {
  1201  				t.Errorf("expected no error, but got one: %v", err)
  1202  			}
  1203  			check(t)
  1204  		})
  1205  	}
  1206  }
  1207  
  1208  func logrusEntry() *logrus.Entry {
  1209  	return logrus.NewEntry(logrus.StandardLogger())
  1210  }
  1211  
  1212  func checkTriggerer(t *testing.T, triggerer fakeProwJobTriggerer, expectedCreatedJobs map[prKey]sets.Set[string]) {
  1213  	actual, expected := triggerer.created, expectedCreatedJobs
  1214  	if diff := cmp.Diff(actual, expected, ignoreUnexported); diff != "" {
  1215  		t.Errorf("did not create expected ProwJob: %s", diff)
  1216  	}
  1217  }
  1218  
  1219  func checkMigrator(t *testing.T, migrator fakeMigrator, expectedRetiredStatuses map[orgRepo]sets.Set[string], expectedMigratedStatuses map[orgRepo]migrationSet) {
  1220  	if diff := cmp.Diff(migrator.retired, expectedRetiredStatuses, ignoreUnexported); diff != "" {
  1221  		t.Errorf("did not retire correct statuses: %s", diff)
  1222  	}
  1223  	if diff := cmp.Diff(migrator.migrated, expectedMigratedStatuses, ignoreUnexported); diff != "" {
  1224  		t.Errorf("did not migrate correct statuses: %s", diff)
  1225  	}
  1226  }