github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/pjutil/filter_test.go (about)

     1  /*
     2  Copyright 2019 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 pjutil
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"reflect"
    23  	"testing"
    24  
    25  	"k8s.io/apimachinery/pkg/util/sets"
    26  	"sigs.k8s.io/prow/pkg/github"
    27  
    28  	"github.com/google/go-cmp/cmp"
    29  	"github.com/sirupsen/logrus"
    30  
    31  	"k8s.io/apimachinery/pkg/util/diff"
    32  	"sigs.k8s.io/prow/pkg/config"
    33  )
    34  
    35  func TestTestAllFilter(t *testing.T) {
    36  	var testCases = []struct {
    37  		name       string
    38  		presubmits []config.Presubmit
    39  		expected   [][]bool
    40  	}{
    41  		{
    42  			name: "test all filter matches jobs which do not require human triggering",
    43  			presubmits: []config.Presubmit{
    44  				{
    45  					JobBase: config.JobBase{
    46  						Name: "always-runs",
    47  					},
    48  					AlwaysRun: true,
    49  				},
    50  				{
    51  					JobBase: config.JobBase{
    52  						Name: "runs-if-changed",
    53  					},
    54  					AlwaysRun: false,
    55  					RegexpChangeMatcher: config.RegexpChangeMatcher{
    56  						RunIfChanged: "sometimes",
    57  					},
    58  				},
    59  				{
    60  					JobBase: config.JobBase{
    61  						Name: "runs-if-changed",
    62  					},
    63  					AlwaysRun: false,
    64  					RegexpChangeMatcher: config.RegexpChangeMatcher{
    65  						SkipIfOnlyChanged: "sometimes",
    66  					},
    67  				},
    68  				{
    69  					JobBase: config.JobBase{
    70  						Name: "runs-if-triggered",
    71  					},
    72  					Reporter: config.Reporter{
    73  						Context: "runs-if-triggered",
    74  					},
    75  					Trigger:      `(?m)^/test (?:.*? )?trigger(?: .*?)?$`,
    76  					RerunCommand: "/test trigger",
    77  				},
    78  				{
    79  					JobBase: config.JobBase{
    80  						Name: "literal-test-all-trigger",
    81  					},
    82  					Reporter: config.Reporter{
    83  						Context: "runs-if-triggered",
    84  					},
    85  					Trigger:      `(?m)^/test (?:.*? )?all(?: .*?)?$`,
    86  					RerunCommand: "/test all",
    87  				},
    88  			},
    89  			expected: [][]bool{{true, false, false}, {true, false, false}, {true, false, false}, {false, false, false}, {false, false, false}},
    90  		},
    91  	}
    92  
    93  	for _, testCase := range testCases {
    94  		t.Run(testCase.name, func(t *testing.T) {
    95  			if len(testCase.presubmits) != len(testCase.expected) {
    96  				t.Fatalf("%s: have %d presubmits but only %d expected filter outputs", testCase.name, len(testCase.presubmits), len(testCase.expected))
    97  			}
    98  			if err := config.SetPresubmitRegexes(testCase.presubmits); err != nil {
    99  				t.Fatalf("%s: could not set presubmit regexes: %v", testCase.name, err)
   100  			}
   101  			filter := NewTestAllFilter()
   102  			for i, presubmit := range testCase.presubmits {
   103  				actualFiltered, actualForced, actualDefault := filter.ShouldRun(presubmit)
   104  				expectedFiltered, expectedForced, expectedDefault := testCase.expected[i][0], testCase.expected[i][1], testCase.expected[i][2]
   105  				if actualFiltered != expectedFiltered {
   106  					t.Errorf("%s: filter did not evaluate correctly, expected %v but got %v for %v", testCase.name, expectedFiltered, actualFiltered, presubmit.Name)
   107  				}
   108  				if actualForced != expectedForced {
   109  					t.Errorf("%s: filter did not determine forced correctly, expected %v but got %v for %v", testCase.name, expectedForced, actualForced, presubmit.Name)
   110  				}
   111  				if actualDefault != expectedDefault {
   112  					t.Errorf("%s: filter did not determine default correctly, expected %v but got %v for %v", testCase.name, expectedDefault, actualDefault, presubmit.Name)
   113  				}
   114  			}
   115  		})
   116  	}
   117  }
   118  
   119  func TestCommandFilter(t *testing.T) {
   120  	var testCases = []struct {
   121  		name         string
   122  		body         string
   123  		half         int
   124  		presubmits   []config.Presubmit
   125  		expected     [][]bool
   126  		expectedName string
   127  	}{
   128  		{
   129  			name: "loose-trigger",
   130  			body: "something/test job-abcdefg",
   131  			presubmits: []config.Presubmit{
   132  				{
   133  					JobBase: config.JobBase{
   134  						Name: "trigger",
   135  					},
   136  					Trigger:      `/test job-a`,
   137  					RerunCommand: "/test job-a", // rerun command has to be set when trigger is set.
   138  				},
   139  			},
   140  			// Loose trigger without `^` and `$` means it would match the text
   141  			// from anywhere in the message. For example a comment like:
   142  			// `something/test job-abcdefg` would match this job.
   143  			expected:     [][]bool{{true, true, true}},
   144  			expectedName: "command-filter: something/test job-abcdefg",
   145  		},
   146  		{
   147  			name: "command filter matches jobs whose triggers match the body",
   148  			body: "/test trigger",
   149  			presubmits: []config.Presubmit{
   150  				{
   151  					JobBase: config.JobBase{
   152  						Name: "trigger",
   153  					},
   154  					Trigger:      `(?m)^/test (?:.*? )?trigger(?: .*?)?$`,
   155  					RerunCommand: "/test trigger",
   156  				},
   157  				{
   158  					JobBase: config.JobBase{
   159  						Name: "other-trigger",
   160  					},
   161  					Trigger:      `(?m)^/test (?:.*? )?other-trigger(?: .*?)?$`,
   162  					RerunCommand: "/test other-trigger",
   163  				},
   164  			},
   165  			expected:     [][]bool{{true, true, true}, {false, false, true}},
   166  			expectedName: "command-filter: /test trigger",
   167  		},
   168  		{
   169  			name: "truncate-name",
   170  			body: `/test trigger
   171  fill in random content so that it exceeds the limit of half*2 chars`,
   172  			half: 10,
   173  			presubmits: []config.Presubmit{
   174  				{
   175  					JobBase: config.JobBase{
   176  						Name: "trigger",
   177  					},
   178  					Trigger:      `(?m)^/test (?:.*? )?trigger(?: .*?)?$`,
   179  					RerunCommand: "/test trigger",
   180  				},
   181  			},
   182  			expected: [][]bool{{true, true, true}},
   183  			expectedName: `command-filter: /test trig
   184  ...
   185  lf*2 chars`,
   186  		},
   187  	}
   188  
   189  	for _, testCase := range testCases {
   190  		t.Run(testCase.name, func(t *testing.T) {
   191  			if len(testCase.presubmits) != len(testCase.expected) {
   192  				t.Fatalf("%s: have %d presubmits but only %d expected filter outputs", testCase.name, len(testCase.presubmits), len(testCase.expected))
   193  			}
   194  			if err := config.SetPresubmitRegexes(testCase.presubmits); err != nil {
   195  				t.Fatalf("%s: could not set presubmit regexes: %v", testCase.name, err)
   196  			}
   197  			filter := NewCommandFilter(testCase.body)
   198  			if testCase.half > 0 {
   199  				filter.half = testCase.half
   200  			}
   201  			for i, presubmit := range testCase.presubmits {
   202  				actualFiltered, actualForced, actualDefault := filter.ShouldRun(presubmit)
   203  				expectedFiltered, expectedForced, expectedDefault := testCase.expected[i][0], testCase.expected[i][1], testCase.expected[i][2]
   204  				if actualFiltered != expectedFiltered {
   205  					t.Errorf("%s: filter did not evaluate correctly, expected %v but got %v for %v", testCase.name, expectedFiltered, actualFiltered, presubmit.Name)
   206  				}
   207  				if actualForced != expectedForced {
   208  					t.Errorf("%s: filter did not determine forced correctly, expected %v but got %v for %v", testCase.name, expectedForced, actualForced, presubmit.Name)
   209  				}
   210  				if actualDefault != expectedDefault {
   211  					t.Errorf("%s: filter did not determine default correctly, expected %v but got %v for %v", testCase.name, expectedDefault, actualDefault, presubmit.Name)
   212  				}
   213  			}
   214  			if diff := cmp.Diff(testCase.expectedName, filter.Name()); diff != "" {
   215  				t.Errorf("Name mismatch. Want(-), got(+):\n%s", diff)
   216  			}
   217  		})
   218  	}
   219  }
   220  
   221  func fakeChangedFilesProvider(shouldError bool) config.ChangedFilesProvider {
   222  	return func() ([]string, error) {
   223  		if shouldError {
   224  			return nil, errors.New("error getting changes")
   225  		}
   226  		return nil, nil
   227  	}
   228  }
   229  
   230  func TestFilterPresubmits(t *testing.T) {
   231  	var testCases = []struct {
   232  		name              string
   233  		filter            Filter
   234  		presubmits        []config.Presubmit
   235  		changesError      bool
   236  		expectedToTrigger []config.Presubmit
   237  		expectErr         bool
   238  	}{
   239  		{
   240  			name: "nothing matches, nothing to run or skip",
   241  			filter: &ArbitraryFilter{
   242  				override: func(p config.Presubmit) (shouldRun bool, forcedToRun bool, defaultBehavior bool) {
   243  					return false, false, false
   244  				},
   245  			},
   246  			presubmits: []config.Presubmit{{
   247  				JobBase:  config.JobBase{Name: "ignored"},
   248  				Reporter: config.Reporter{Context: "first"},
   249  			}, {
   250  				JobBase:  config.JobBase{Name: "ignored"},
   251  				Reporter: config.Reporter{Context: "second"},
   252  			}},
   253  			changesError:      false,
   254  			expectedToTrigger: nil,
   255  			expectErr:         false,
   256  		},
   257  		{
   258  			name: "everything matches and is forced to run, nothing to skip",
   259  			filter: &ArbitraryFilter{
   260  				override: func(p config.Presubmit) (shouldRun bool, forcedToRun bool, defaultBehavior bool) {
   261  					return true, true, true
   262  				},
   263  			},
   264  			presubmits: []config.Presubmit{{
   265  				JobBase:  config.JobBase{Name: "should-trigger"},
   266  				Reporter: config.Reporter{Context: "first"},
   267  			}, {
   268  				JobBase:  config.JobBase{Name: "should-trigger"},
   269  				Reporter: config.Reporter{Context: "second"},
   270  			}},
   271  			changesError: false,
   272  			expectedToTrigger: []config.Presubmit{{
   273  				JobBase:  config.JobBase{Name: "should-trigger"},
   274  				Reporter: config.Reporter{Context: "first"},
   275  			}, {
   276  				JobBase:  config.JobBase{Name: "should-trigger"},
   277  				Reporter: config.Reporter{Context: "second"},
   278  			}},
   279  			expectErr: false,
   280  		},
   281  		{
   282  			name: "error detecting if something should run, nothing to run or skip",
   283  			filter: &ArbitraryFilter{
   284  				override: func(p config.Presubmit) (shouldRun bool, forcedToRun bool, defaultBehavior bool) {
   285  					return true, false, false
   286  				},
   287  			},
   288  			presubmits: []config.Presubmit{{
   289  				JobBase:             config.JobBase{Name: "errors"},
   290  				Reporter:            config.Reporter{Context: "first"},
   291  				RegexpChangeMatcher: config.RegexpChangeMatcher{RunIfChanged: "oopsie"},
   292  			}, {
   293  				JobBase:  config.JobBase{Name: "ignored"},
   294  				Reporter: config.Reporter{Context: "second"},
   295  			}},
   296  			changesError:      true,
   297  			expectedToTrigger: nil,
   298  			expectErr:         true,
   299  		},
   300  		{
   301  			name: "error detecting if something should run, nothing to skip",
   302  			filter: &ArbitraryFilter{
   303  				override: func(p config.Presubmit) (shouldRun bool, forcedToRun bool, defaultBehavior bool) {
   304  					return true, false, false
   305  				},
   306  			},
   307  			presubmits: []config.Presubmit{{
   308  				JobBase:             config.JobBase{Name: "errors"},
   309  				Reporter:            config.Reporter{Context: "first"},
   310  				RegexpChangeMatcher: config.RegexpChangeMatcher{SkipIfOnlyChanged: "oopsie"},
   311  			}, {
   312  				JobBase:  config.JobBase{Name: "ignored"},
   313  				Reporter: config.Reporter{Context: "second"},
   314  			}},
   315  			changesError:      true,
   316  			expectedToTrigger: nil,
   317  			expectErr:         true,
   318  		},
   319  		{
   320  			name: "some things match and are forced to run, nothing to skip",
   321  			filter: &ArbitraryFilter{
   322  				override: func(p config.Presubmit) (shouldRun bool, forcedToRun bool, defaultBehavior bool) {
   323  					return p.Name == "should-trigger", true, true
   324  				},
   325  			},
   326  			presubmits: []config.Presubmit{{
   327  				JobBase:  config.JobBase{Name: "should-trigger"},
   328  				Reporter: config.Reporter{Context: "first"},
   329  			}, {
   330  				JobBase:  config.JobBase{Name: "ignored"},
   331  				Reporter: config.Reporter{Context: "second"},
   332  			}},
   333  			changesError: false,
   334  			expectedToTrigger: []config.Presubmit{{
   335  				JobBase:  config.JobBase{Name: "should-trigger"},
   336  				Reporter: config.Reporter{Context: "first"},
   337  			}},
   338  			expectErr: false,
   339  		},
   340  		{
   341  			name: "everything matches and some things are forced to run, others should be skipped",
   342  			filter: &ArbitraryFilter{
   343  				override: func(p config.Presubmit) (shouldRun bool, forcedToRun bool, defaultBehavior bool) {
   344  					return true, p.Name == "should-trigger", p.Name == "should-trigger"
   345  				},
   346  			},
   347  			presubmits: []config.Presubmit{{
   348  				JobBase:  config.JobBase{Name: "should-trigger"},
   349  				Reporter: config.Reporter{Context: "first"},
   350  			}, {
   351  				JobBase:  config.JobBase{Name: "should-trigger"},
   352  				Reporter: config.Reporter{Context: "second"},
   353  			}, {
   354  				JobBase:  config.JobBase{Name: "should-skip"},
   355  				Reporter: config.Reporter{Context: "third"},
   356  			}, {
   357  				JobBase:  config.JobBase{Name: "should-skip"},
   358  				Reporter: config.Reporter{Context: "fourth"},
   359  			}},
   360  			changesError: false,
   361  			expectedToTrigger: []config.Presubmit{{
   362  				JobBase:  config.JobBase{Name: "should-trigger"},
   363  				Reporter: config.Reporter{Context: "first"},
   364  			}, {
   365  				JobBase:  config.JobBase{Name: "should-trigger"},
   366  				Reporter: config.Reporter{Context: "second"},
   367  			}},
   368  			expectErr: false,
   369  		},
   370  		{
   371  			name: "everything matches and some that are forces to run supercede some that are skipped due to shared contexts",
   372  			filter: &ArbitraryFilter{
   373  				override: func(p config.Presubmit) (shouldRun bool, forcedToRun bool, defaultBehavior bool) {
   374  					return true, p.Name == "should-trigger", p.Name == "should-trigger"
   375  				},
   376  			},
   377  			presubmits: []config.Presubmit{{
   378  				JobBase:  config.JobBase{Name: "should-trigger"},
   379  				Reporter: config.Reporter{Context: "first"},
   380  			}, {
   381  				JobBase:  config.JobBase{Name: "should-trigger"},
   382  				Reporter: config.Reporter{Context: "second"},
   383  			}, {
   384  				JobBase:  config.JobBase{Name: "should-skip"},
   385  				Reporter: config.Reporter{Context: "third"},
   386  			}, {
   387  				JobBase:  config.JobBase{Name: "should-not-skip"},
   388  				Reporter: config.Reporter{Context: "second"},
   389  			}},
   390  			changesError: false,
   391  			expectedToTrigger: []config.Presubmit{{
   392  				JobBase:  config.JobBase{Name: "should-trigger"},
   393  				Reporter: config.Reporter{Context: "first"},
   394  			}, {
   395  				JobBase:  config.JobBase{Name: "should-trigger"},
   396  				Reporter: config.Reporter{Context: "second"},
   397  			}},
   398  			expectErr: false,
   399  		},
   400  	}
   401  
   402  	branch := "foobar"
   403  
   404  	for _, testCase := range testCases {
   405  		t.Run(testCase.name, func(t *testing.T) {
   406  			actualToTrigger, err := FilterPresubmits(testCase.filter, fakeChangedFilesProvider(testCase.changesError), branch, testCase.presubmits, logrus.WithField("test-case", testCase.name))
   407  			if testCase.expectErr && err == nil {
   408  				t.Errorf("%s: expected an error filtering presubmits, but got none", testCase.name)
   409  			}
   410  			if !testCase.expectErr && err != nil {
   411  				t.Errorf("%s: expected no error filtering presubmits, but got one: %v", testCase.name, err)
   412  			}
   413  			if !reflect.DeepEqual(actualToTrigger, testCase.expectedToTrigger) {
   414  				t.Errorf("%s: incorrect set of presubmits to skip: %s", testCase.name, diff.ObjectReflectDiff(actualToTrigger, testCase.expectedToTrigger))
   415  			}
   416  		})
   417  	}
   418  }
   419  
   420  type orgRepoRef struct {
   421  	org, repo, ref string
   422  }
   423  
   424  type fakeContextGetter struct {
   425  	status map[orgRepoRef]*github.CombinedStatus
   426  	errors map[orgRepoRef]error
   427  }
   428  
   429  func (f *fakeContextGetter) getContexts(key orgRepoRef) (sets.Set[string], sets.Set[string], error) {
   430  	allContexts := sets.New[string]()
   431  	failedContexts := sets.New[string]()
   432  	if err, exists := f.errors[key]; exists {
   433  		return failedContexts, allContexts, err
   434  	}
   435  	combinedStatus, exists := f.status[key]
   436  	if !exists {
   437  		return failedContexts, allContexts, fmt.Errorf("failed to find status for %s/%s@%s", key.org, key.repo, key.ref)
   438  	}
   439  	for _, status := range combinedStatus.Statuses {
   440  		allContexts.Insert(status.Context)
   441  		if status.State == github.StatusError || status.State == github.StatusFailure {
   442  			failedContexts.Insert(status.Context)
   443  		}
   444  	}
   445  	return failedContexts, allContexts, nil
   446  }
   447  
   448  func TestPresubmitFilter(t *testing.T) {
   449  	statuses := &github.CombinedStatus{Statuses: []github.Status{
   450  		{
   451  			Context: "existing-successful",
   452  			State:   github.StatusSuccess,
   453  		},
   454  		{
   455  			Context: "existing-pending",
   456  			State:   github.StatusPending,
   457  		},
   458  		{
   459  			Context: "existing-error",
   460  			State:   github.StatusError,
   461  		},
   462  		{
   463  			Context: "existing-failure",
   464  			State:   github.StatusFailure,
   465  		},
   466  	}}
   467  	var testCases = []struct {
   468  		name                 string
   469  		honorOkToTest        bool
   470  		body, org, repo, ref string
   471  		presubmits           []config.Presubmit
   472  		expected             [][]bool
   473  		statusErr, expectErr bool
   474  	}{
   475  		{
   476  			name: "test all comment selects all tests that don't need an explicit trigger",
   477  			body: "/test all",
   478  			org:  "org",
   479  			repo: "repo",
   480  			ref:  "ref",
   481  			presubmits: []config.Presubmit{
   482  				{
   483  					JobBase: config.JobBase{
   484  						Name: "always-runs",
   485  					},
   486  					AlwaysRun: true,
   487  					Reporter: config.Reporter{
   488  						Context: "always-runs",
   489  					},
   490  				},
   491  				{
   492  					JobBase: config.JobBase{
   493  						Name: "runs-if-changed",
   494  					},
   495  					Reporter: config.Reporter{
   496  						Context: "runs-if-changed",
   497  					},
   498  					RegexpChangeMatcher: config.RegexpChangeMatcher{
   499  						RunIfChanged: "sometimes",
   500  					},
   501  				},
   502  				{
   503  					JobBase: config.JobBase{
   504  						Name: "runs-if-changed",
   505  					},
   506  					Reporter: config.Reporter{
   507  						Context: "runs-if-changed",
   508  					},
   509  					RegexpChangeMatcher: config.RegexpChangeMatcher{
   510  						SkipIfOnlyChanged: "sometimes",
   511  					},
   512  				},
   513  				{
   514  					JobBase: config.JobBase{
   515  						Name: "runs-if-triggered",
   516  					},
   517  					Reporter: config.Reporter{
   518  						Context: "runs-if-triggered",
   519  					},
   520  					Trigger:      `(?m)^/test (?:.*? )?trigger(?: .*?)?$`,
   521  					RerunCommand: "/test trigger",
   522  				},
   523  			},
   524  			expected: [][]bool{{true, false, false}, {true, false, false}, {true, false, false}, {false, false, false}},
   525  		},
   526  		{
   527  			name:          "honored ok-to-test comment selects all tests that don't need an explicit trigger",
   528  			body:          "/ok-to-test",
   529  			honorOkToTest: true,
   530  			org:           "org",
   531  			repo:          "repo",
   532  			ref:           "ref",
   533  			presubmits: []config.Presubmit{
   534  				{
   535  					JobBase: config.JobBase{
   536  						Name: "always-runs",
   537  					},
   538  					AlwaysRun: true,
   539  					Reporter: config.Reporter{
   540  						Context: "always-runs",
   541  					},
   542  				},
   543  				{
   544  					JobBase: config.JobBase{
   545  						Name: "runs-if-changed",
   546  					},
   547  					Reporter: config.Reporter{
   548  						Context: "runs-if-changed",
   549  					},
   550  					RegexpChangeMatcher: config.RegexpChangeMatcher{
   551  						RunIfChanged: "sometimes",
   552  					},
   553  				},
   554  				{
   555  					JobBase: config.JobBase{
   556  						Name: "runs-if-changed",
   557  					},
   558  					Reporter: config.Reporter{
   559  						Context: "runs-if-changed",
   560  					},
   561  					RegexpChangeMatcher: config.RegexpChangeMatcher{
   562  						SkipIfOnlyChanged: "sometimes",
   563  					},
   564  				},
   565  				{
   566  					JobBase: config.JobBase{
   567  						Name: "runs-if-triggered",
   568  					},
   569  					Reporter: config.Reporter{
   570  						Context: "runs-if-triggered",
   571  					},
   572  					Trigger:      `(?m)^/test (?:.*? )?trigger(?: .*?)?$`,
   573  					RerunCommand: "/test trigger",
   574  				},
   575  			},
   576  			expected: [][]bool{{true, false, false}, {true, false, false}, {true, false, false}, {false, false, false}},
   577  		},
   578  		{
   579  			name:          "not honored ok-to-test comment selects no tests",
   580  			body:          "/ok-to-test",
   581  			honorOkToTest: false,
   582  			org:           "org",
   583  			repo:          "repo",
   584  			ref:           "ref",
   585  			presubmits: []config.Presubmit{
   586  				{
   587  					JobBase: config.JobBase{
   588  						Name: "always-runs",
   589  					},
   590  					AlwaysRun: true,
   591  					Reporter: config.Reporter{
   592  						Context: "always-runs",
   593  					},
   594  				},
   595  				{
   596  					JobBase: config.JobBase{
   597  						Name: "runs-if-changed",
   598  					},
   599  					Reporter: config.Reporter{
   600  						Context: "runs-if-changed",
   601  					},
   602  					RegexpChangeMatcher: config.RegexpChangeMatcher{
   603  						RunIfChanged: "sometimes",
   604  					},
   605  				},
   606  				{
   607  					JobBase: config.JobBase{
   608  						Name: "runs-if-changed",
   609  					},
   610  					Reporter: config.Reporter{
   611  						Context: "runs-if-changed",
   612  					},
   613  					RegexpChangeMatcher: config.RegexpChangeMatcher{
   614  						SkipIfOnlyChanged: "sometimes",
   615  					},
   616  				},
   617  				{
   618  					JobBase: config.JobBase{
   619  						Name: "runs-if-triggered",
   620  					},
   621  					Reporter: config.Reporter{
   622  						Context: "runs-if-triggered",
   623  					},
   624  					Trigger:      `(?m)^/test (?:.*? )?trigger(?: .*?)?$`,
   625  					RerunCommand: "/test trigger",
   626  				},
   627  			},
   628  			expected: [][]bool{{false, false, false}, {false, false, false}, {false, false, false}, {false, false, false}},
   629  		},
   630  		{
   631  			name:       "statuses are not gathered unless retest is specified (will error but we should not see it)",
   632  			body:       "not a command",
   633  			org:        "org",
   634  			repo:       "repo",
   635  			ref:        "ref",
   636  			presubmits: []config.Presubmit{},
   637  			expected:   [][]bool{},
   638  			statusErr:  true,
   639  			expectErr:  false,
   640  		},
   641  		{
   642  			name:       "statuses are gathered when retest is specified and gather error is propagated",
   643  			body:       "/retest",
   644  			org:        "org",
   645  			repo:       "repo",
   646  			ref:        "ref",
   647  			presubmits: []config.Presubmit{},
   648  			expected:   [][]bool{},
   649  			statusErr:  true,
   650  			expectErr:  true,
   651  		},
   652  		{
   653  			name: "retest command selects for errored or failed contexts and required but missing contexts",
   654  			body: "/retest",
   655  			org:  "org",
   656  			repo: "repo",
   657  			ref:  "ref",
   658  			presubmits: []config.Presubmit{
   659  				{
   660  					JobBase: config.JobBase{
   661  						Name: "successful-job",
   662  					},
   663  					Reporter: config.Reporter{
   664  						Context: "existing-successful",
   665  					},
   666  				},
   667  				{
   668  					JobBase: config.JobBase{
   669  						Name: "pending-job",
   670  					},
   671  					Reporter: config.Reporter{
   672  						Context: "existing-pending",
   673  					},
   674  				},
   675  				{
   676  					JobBase: config.JobBase{
   677  						Name: "failure-job",
   678  					},
   679  					Reporter: config.Reporter{
   680  						Context: "existing-failure",
   681  					},
   682  				},
   683  				{
   684  					JobBase: config.JobBase{
   685  						Name: "error-job",
   686  					},
   687  					Reporter: config.Reporter{
   688  						Context: "existing-error",
   689  					},
   690  				},
   691  				{
   692  					JobBase: config.JobBase{
   693  						Name: "missing-always-runs",
   694  					},
   695  					Reporter: config.Reporter{
   696  						Context: "missing-always-runs",
   697  					},
   698  					AlwaysRun: true,
   699  				},
   700  			},
   701  			expected: [][]bool{{false, false, false}, {false, false, false}, {true, false, true}, {true, false, true}, {true, false, false}},
   702  		},
   703  		{
   704  			name: "retest command selects for errored or failed contexts unless they are optional",
   705  			body: "/retest-required",
   706  			org:  "org",
   707  			repo: "repo",
   708  			ref:  "ref",
   709  			presubmits: []config.Presubmit{
   710  				{
   711  					JobBase: config.JobBase{
   712  						Name: "successful-job",
   713  					},
   714  					Reporter: config.Reporter{
   715  						Context: "existing-successful",
   716  					},
   717  				},
   718  				{
   719  					JobBase: config.JobBase{
   720  						Name: "pending-job",
   721  					},
   722  					Reporter: config.Reporter{
   723  						Context: "existing-pending",
   724  					},
   725  				},
   726  				{
   727  					JobBase: config.JobBase{
   728  						Name: "failure-job",
   729  					},
   730  					Reporter: config.Reporter{
   731  						Context: "existing-failure",
   732  					},
   733  					Optional: true,
   734  				},
   735  				{
   736  					JobBase: config.JobBase{
   737  						Name: "error-job",
   738  					},
   739  					Reporter: config.Reporter{
   740  						Context: "existing-error",
   741  					},
   742  				},
   743  				{
   744  					JobBase: config.JobBase{
   745  						Name: "missing-always-runs",
   746  					},
   747  					Reporter: config.Reporter{
   748  						Context: "missing-always-runs",
   749  					},
   750  					AlwaysRun: true,
   751  				},
   752  			},
   753  			expected: [][]bool{{false, false, false}, {false, false, false}, {false, false, false}, {true, false, true}, {true, false, false}},
   754  		},
   755  		{
   756  			name: "explicit test command filters for jobs that match",
   757  			body: "/test trigger",
   758  			org:  "org",
   759  			repo: "repo",
   760  			ref:  "ref",
   761  			presubmits: []config.Presubmit{
   762  				{
   763  					JobBase: config.JobBase{
   764  						Name: "always-runs",
   765  					},
   766  					AlwaysRun: true,
   767  					Reporter: config.Reporter{
   768  						Context: "always-runs",
   769  					},
   770  					Trigger:      `(?m)^/test (?:.*? )?trigger(?: .*?)?$`,
   771  					RerunCommand: "/test trigger",
   772  				},
   773  				{
   774  					JobBase: config.JobBase{
   775  						Name: "runs-if-changed",
   776  					},
   777  					Reporter: config.Reporter{
   778  						Context: "runs-if-changed",
   779  					},
   780  					RegexpChangeMatcher: config.RegexpChangeMatcher{
   781  						RunIfChanged: "sometimes",
   782  					},
   783  					Trigger:      `(?m)^/test (?:.*? )?trigger(?: .*?)?$`,
   784  					RerunCommand: "/test trigger",
   785  				},
   786  				{
   787  					JobBase: config.JobBase{
   788  						Name: "runs-if-changed",
   789  					},
   790  					Reporter: config.Reporter{
   791  						Context: "runs-if-changed",
   792  					},
   793  					RegexpChangeMatcher: config.RegexpChangeMatcher{
   794  						SkipIfOnlyChanged: "sometimes",
   795  					},
   796  					Trigger:      `(?m)^/test (?:.*? )?trigger(?: .*?)?$`,
   797  					RerunCommand: "/test trigger",
   798  				},
   799  				{
   800  					JobBase: config.JobBase{
   801  						Name: "runs-if-triggered",
   802  					},
   803  					Reporter: config.Reporter{
   804  						Context: "runs-if-triggered",
   805  					},
   806  					Trigger:      `(?m)^/test (?:.*? )?trigger(?: .*?)?$`,
   807  					RerunCommand: "/test trigger",
   808  				},
   809  				{
   810  					JobBase: config.JobBase{
   811  						Name: "always-runs",
   812  					},
   813  					AlwaysRun: true,
   814  					Reporter: config.Reporter{
   815  						Context: "always-runs",
   816  					},
   817  					Trigger:      `(?m)^/test (?:.*? )?other-trigger(?: .*?)?$`,
   818  					RerunCommand: "/test other-trigger",
   819  				},
   820  				{
   821  					JobBase: config.JobBase{
   822  						Name: "runs-if-changed",
   823  					},
   824  					Reporter: config.Reporter{
   825  						Context: "runs-if-changed",
   826  					},
   827  					RegexpChangeMatcher: config.RegexpChangeMatcher{
   828  						RunIfChanged: "sometimes",
   829  					},
   830  					Trigger:      `(?m)^/test (?:.*? )?other-trigger(?: .*?)?$`,
   831  					RerunCommand: "/test other-trigger",
   832  				},
   833  				{
   834  					JobBase: config.JobBase{
   835  						Name: "runs-if-changed",
   836  					},
   837  					Reporter: config.Reporter{
   838  						Context: "runs-if-changed",
   839  					},
   840  					RegexpChangeMatcher: config.RegexpChangeMatcher{
   841  						SkipIfOnlyChanged: "sometimes",
   842  					},
   843  					Trigger:      `(?m)^/test (?:.*? )?other-trigger(?: .*?)?$`,
   844  					RerunCommand: "/test other-trigger",
   845  				},
   846  				{
   847  					JobBase: config.JobBase{
   848  						Name: "runs-if-triggered",
   849  					},
   850  					Reporter: config.Reporter{
   851  						Context: "runs-if-triggered",
   852  					},
   853  					Trigger:      `(?m)^/test (?:.*? )?other-trigger(?: .*?)?$`,
   854  					RerunCommand: "/test other-trigger",
   855  				},
   856  			},
   857  			expected: [][]bool{
   858  				{true, true, true},
   859  				{true, true, true},
   860  				{true, true, true},
   861  				{true, true, true},
   862  				{false, false, false},
   863  				{false, false, false},
   864  				{false, false, false},
   865  				{false, false, false},
   866  			},
   867  		},
   868  		{
   869  			name: "comments matching more than one case will select the union of presubmits",
   870  			body: `/test trigger
   871  /test all
   872  /retest`,
   873  			org:  "org",
   874  			repo: "repo",
   875  			ref:  "ref",
   876  			presubmits: []config.Presubmit{
   877  				{
   878  					JobBase: config.JobBase{
   879  						Name: "always-runs",
   880  					},
   881  					AlwaysRun: true,
   882  					Reporter: config.Reporter{
   883  						Context: "existing-successful",
   884  					},
   885  					Trigger:      `(?m)^/test (?:.*? )?other-trigger(?: .*?)?$`,
   886  					RerunCommand: "/test other-trigger",
   887  				},
   888  				{
   889  					JobBase: config.JobBase{
   890  						Name: "runs-if-changed",
   891  					},
   892  					Reporter: config.Reporter{
   893  						Context: "existing-successful",
   894  					},
   895  					RegexpChangeMatcher: config.RegexpChangeMatcher{
   896  						RunIfChanged: "sometimes",
   897  					},
   898  					Trigger:      `(?m)^/test (?:.*? )?other-trigger(?: .*?)?$`,
   899  					RerunCommand: "/test other-trigger",
   900  				},
   901  				{
   902  					JobBase: config.JobBase{
   903  						Name: "runs-if-changed",
   904  					},
   905  					Reporter: config.Reporter{
   906  						Context: "existing-successful",
   907  					},
   908  					RegexpChangeMatcher: config.RegexpChangeMatcher{
   909  						SkipIfOnlyChanged: "sometimes",
   910  					},
   911  					Trigger:      `(?m)^/test (?:.*? )?other-trigger(?: .*?)?$`,
   912  					RerunCommand: "/test other-trigger",
   913  				},
   914  				{
   915  					JobBase: config.JobBase{
   916  						Name: "runs-if-triggered",
   917  					},
   918  					Reporter: config.Reporter{
   919  						Context: "runs-if-triggered",
   920  					},
   921  					Trigger:      `(?m)^/test (?:.*? )?trigger(?: .*?)?$`,
   922  					RerunCommand: "/test trigger",
   923  				},
   924  				{
   925  					JobBase: config.JobBase{
   926  						Name: "successful-job",
   927  					},
   928  					Reporter: config.Reporter{
   929  						Context: "existing-successful",
   930  					},
   931  				},
   932  				{
   933  					JobBase: config.JobBase{
   934  						Name: "pending-job",
   935  					},
   936  					Reporter: config.Reporter{
   937  						Context: "existing-pending",
   938  					},
   939  				},
   940  				{
   941  					JobBase: config.JobBase{
   942  						Name: "failure-job",
   943  					},
   944  					Reporter: config.Reporter{
   945  						Context: "existing-failure",
   946  					},
   947  				},
   948  				{
   949  					JobBase: config.JobBase{
   950  						Name: "error-job",
   951  					},
   952  					Reporter: config.Reporter{
   953  						Context: "existing-error",
   954  					},
   955  				},
   956  				{
   957  					JobBase: config.JobBase{
   958  						Name: "missing-always-runs",
   959  					},
   960  					AlwaysRun: true,
   961  					Reporter: config.Reporter{
   962  						Context: "missing-always-runs",
   963  					},
   964  				},
   965  			},
   966  			expected: [][]bool{
   967  				{true, false, false},
   968  				{true, false, false},
   969  				{true, false, false},
   970  				{true, true, true},
   971  				{false, false, false},
   972  				{false, false, false},
   973  				{true, false, true},
   974  				{true, false, true},
   975  				{true, false, false},
   976  			},
   977  		},
   978  	}
   979  
   980  	for _, testCase := range testCases {
   981  		t.Run(testCase.name, func(t *testing.T) {
   982  			if len(testCase.presubmits) != len(testCase.expected) {
   983  				t.Fatalf("%s: have %d presubmits but only %d expected filter outputs", testCase.name, len(testCase.presubmits), len(testCase.expected))
   984  			}
   985  			if err := config.SetPresubmitRegexes(testCase.presubmits); err != nil {
   986  				t.Fatalf("%s: could not set presubmit regexes: %v", testCase.name, err)
   987  			}
   988  			fsg := &fakeContextGetter{
   989  				errors: map[orgRepoRef]error{},
   990  				status: map[orgRepoRef]*github.CombinedStatus{},
   991  			}
   992  			key := orgRepoRef{org: testCase.org, repo: testCase.repo, ref: testCase.ref}
   993  			if testCase.statusErr {
   994  				fsg.errors[key] = errors.New("failure")
   995  			} else {
   996  				fsg.status[key] = statuses
   997  			}
   998  
   999  			fakeContextGetter := func() (sets.Set[string], sets.Set[string], error) {
  1000  
  1001  				return fsg.getContexts(key)
  1002  			}
  1003  
  1004  			filter, err := PresubmitFilter(testCase.honorOkToTest, fakeContextGetter, testCase.body, logrus.WithField("test-case", testCase.name))
  1005  
  1006  			if testCase.expectErr && err == nil {
  1007  				t.Errorf("%s: expected an error creating the filter, but got none", testCase.name)
  1008  			}
  1009  			if !testCase.expectErr && err != nil {
  1010  				t.Errorf("%s: expected no error creating the filter, but got one: %v", testCase.name, err)
  1011  			}
  1012  			for i, presubmit := range testCase.presubmits {
  1013  				actualFiltered, actualForced, actualDefault := filter.ShouldRun(presubmit)
  1014  				expectedFiltered, expectedForced, expectedDefault := testCase.expected[i][0], testCase.expected[i][1], testCase.expected[i][2]
  1015  				if actualFiltered != expectedFiltered {
  1016  					t.Errorf("%s: filter did not evaluate correctly, expected %v but got %v for %v", testCase.name, expectedFiltered, actualFiltered, presubmit.Name)
  1017  				}
  1018  				if actualForced != expectedForced {
  1019  					t.Errorf("%s: filter did not determine forced correctly, expected %v but got %v for %v", testCase.name, expectedForced, actualForced, presubmit.Name)
  1020  				}
  1021  				if actualDefault != expectedDefault {
  1022  					t.Errorf("%s: filter did not determine default correctly, expected %v but got %v for %v", testCase.name, expectedDefault, actualDefault, presubmit.Name)
  1023  				}
  1024  			}
  1025  		})
  1026  	}
  1027  }