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

     1  /*
     2  Copyright 2021 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 config
    18  
    19  import (
    20  	"fmt"
    21  	"sync"
    22  	"testing"
    23  
    24  	"github.com/google/go-cmp/cmp"
    25  	"github.com/google/go-cmp/cmp/cmpopts"
    26  	"golang.org/x/sync/errgroup"
    27  	v1 "k8s.io/api/core/v1"
    28  	prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1"
    29  	"sigs.k8s.io/prow/pkg/git/v2"
    30  )
    31  
    32  type fakeConfigAgent struct {
    33  	sync.Mutex
    34  	c *Config
    35  }
    36  
    37  func (fca *fakeConfigAgent) Config() *Config {
    38  	fca.Lock()
    39  	defer fca.Unlock()
    40  	return fca.c
    41  }
    42  
    43  func TestNewInRepoConfigCache(t *testing.T) {
    44  	// Invalid size arguments result in a nil cache and non-nil error.
    45  	invalids := []int{-1, 0}
    46  	for _, invalid := range invalids {
    47  
    48  		fca := &fakeConfigAgent{}
    49  		cf := &testClientFactory{}
    50  		cache, err := NewInRepoConfigCache(invalid, fca, cf)
    51  
    52  		if err == nil {
    53  			t.Fatal("Expected non-nil error, got nil")
    54  		}
    55  
    56  		if err.Error() != "Must provide a positive size" {
    57  			t.Errorf("Expected error 'Must provide a positive size', got '%v'", err.Error())
    58  		}
    59  
    60  		if cache != nil {
    61  			t.Errorf("Expected nil cache, got %v", cache)
    62  		}
    63  	}
    64  
    65  	// Valid size arguments.
    66  	valids := []int{1, 5, 1000}
    67  	for _, valid := range valids {
    68  
    69  		fca := &fakeConfigAgent{}
    70  		cf := &testClientFactory{}
    71  		cache, err := NewInRepoConfigCache(valid, fca, cf)
    72  
    73  		if err != nil {
    74  			t.Errorf("Expected error 'nil' got '%v'", err.Error())
    75  		}
    76  
    77  		if cache == nil {
    78  			t.Errorf("Expected non-nil cache, got nil")
    79  		}
    80  	}
    81  }
    82  
    83  func goodSHAGetter(sha string) func() (string, error) {
    84  	return func() (string, error) {
    85  		return sha, nil
    86  	}
    87  }
    88  
    89  func badSHAGetter() (string, error) {
    90  	return "", fmt.Errorf("badSHAGetter")
    91  }
    92  
    93  func TestGetProwYAMLCached(t *testing.T) {
    94  	// fakeProwYAMLGetter mocks prowYAMLGetter(). Instead of using the
    95  	// git.ClientFactory (and other operations), we just use a simple map to get
    96  	// the *ProwYAML value we want. For simplicity we just reuse MakeCacheKey
    97  	// even though we're not using a cache. The point of fakeProwYAMLGetter is to
    98  	// act as a "source of truth" of authoritative *ProwYAML values for purposes
    99  	// of the test cases in this unit test.
   100  	fakeProwYAMLGetter := make(map[CacheKey]*ProwYAML)
   101  
   102  	// goodValConstructor mocks config.getProwYAML.
   103  	// This map pretends to be an expensive computation in order to generate a
   104  	// *ProwYAML value.
   105  	goodValConstructor := func(gc git.ClientFactory, identifier, baseBranch string, baseSHAGetter RefGetter, headSHAGetters ...RefGetter) (*ProwYAML, error) {
   106  
   107  		baseSHA, headSHAs, err := GetAndCheckRefs(baseSHAGetter, headSHAGetters...)
   108  		if err != nil {
   109  			t.Fatal(err)
   110  		}
   111  
   112  		kp := CacheKeyParts{
   113  			Identifier: identifier,
   114  			BaseSHA:    baseSHA,
   115  			HeadSHAs:   headSHAs,
   116  		}
   117  		key, err := kp.CacheKey()
   118  		if err != nil {
   119  			t.Fatal(err)
   120  		}
   121  
   122  		val, ok := fakeProwYAMLGetter[key]
   123  		if ok {
   124  			return val, nil
   125  		}
   126  
   127  		return nil, fmt.Errorf("unable to construct *ProwYAML value")
   128  	}
   129  
   130  	fakeCacheKeyPartsSlice := []CacheKeyParts{
   131  		{
   132  			Identifier: "foo/bar",
   133  			BaseSHA:    "ba5e",
   134  			HeadSHAs:   []string{"abcd", "ef01"},
   135  		},
   136  	}
   137  	// Populate fakeProwYAMLGetter.
   138  	for _, fakeCacheKeyParts := range fakeCacheKeyPartsSlice {
   139  		// To make it easier to compare Presubmit values, we only set the
   140  		// Name field and only compare this field. We also only create a
   141  		// single Presubmit (singleton slice), again for simplicity. Lastly
   142  		// we also set the Name field to the same value as the "key", again
   143  		// for simplicity.
   144  		fakeCacheKey, err := fakeCacheKeyParts.CacheKey()
   145  		if err != nil {
   146  			t.Fatal(err)
   147  		}
   148  		fakeProwYAMLGetter[fakeCacheKey] = &ProwYAML{
   149  			Presubmits: []Presubmit{
   150  				{
   151  					JobBase: JobBase{Name: string(fakeCacheKey)},
   152  				},
   153  			},
   154  		}
   155  	}
   156  
   157  	// goodValConstructorForInitialState is used for warming up the cache for
   158  	// tests that need it.
   159  	goodValConstructorForInitialState := func(val ProwYAML) func() (interface{}, error) {
   160  		return func() (interface{}, error) {
   161  			return &val, nil
   162  		}
   163  	}
   164  
   165  	badValConstructor := func(gc git.ClientFactory, identifier, baseBranch string, baseSHAGetter RefGetter, headSHAGetters ...RefGetter) (*ProwYAML, error) {
   166  		return nil, fmt.Errorf("unable to construct *ProwYAML value")
   167  	}
   168  
   169  	type expected struct {
   170  		prowYAML *ProwYAML
   171  		cacheLen int
   172  		err      string
   173  	}
   174  
   175  	for _, tc := range []struct {
   176  		name           string
   177  		valConstructor func(git.ClientFactory, string, string, RefGetter, ...RefGetter) (*ProwYAML, error)
   178  		// We use a slice of CacheKeysParts for simplicity.
   179  		cacheInitialState   []CacheKeyParts
   180  		cacheCorrupted      bool
   181  		inRepoConfigEnabled bool
   182  		identifier          string
   183  		baseBranch          string
   184  		baseSHAGetter       RefGetter
   185  		headSHAGetters      []RefGetter
   186  		expected            expected
   187  	}{
   188  		{
   189  			name:                "CacheMiss",
   190  			valConstructor:      goodValConstructor,
   191  			cacheInitialState:   nil,
   192  			cacheCorrupted:      false,
   193  			inRepoConfigEnabled: true,
   194  			identifier:          "foo/bar",
   195  			baseBranch:          "main",
   196  			baseSHAGetter:       goodSHAGetter("ba5e"),
   197  			headSHAGetters: []RefGetter{
   198  				goodSHAGetter("abcd"),
   199  				goodSHAGetter("ef01")},
   200  			expected: expected{
   201  				prowYAML: &ProwYAML{
   202  					Presubmits: []Presubmit{
   203  						{
   204  							JobBase: JobBase{Name: `{"identifier":"foo/bar","baseSHA":"ba5e","headSHAs":["abcd","ef01"]}`}},
   205  					},
   206  				},
   207  				cacheLen: 1,
   208  				err:      "",
   209  			},
   210  		},
   211  		{
   212  			// If the InRepoConfig is disabled for this repo, then the returned
   213  			// value should be an empty &ProwYAML{}. Also, the cache miss should
   214  			// not result in adding this entry into the cache (because the value
   215  			// will be a meaninless empty &ProwYAML{}).
   216  			name:                "CacheMiss/InRepoConfigDisabled",
   217  			valConstructor:      goodValConstructor,
   218  			cacheInitialState:   nil,
   219  			cacheCorrupted:      false,
   220  			inRepoConfigEnabled: false,
   221  			identifier:          "foo/bar",
   222  			baseBranch:          "main",
   223  			baseSHAGetter:       goodSHAGetter("ba5e"),
   224  			headSHAGetters: []RefGetter{
   225  				goodSHAGetter("abcd"),
   226  				goodSHAGetter("ef01")},
   227  			expected: expected{
   228  				prowYAML: &ProwYAML{},
   229  				cacheLen: 0,
   230  				err:      "",
   231  			},
   232  		},
   233  		{
   234  			// If we get a cache hit, the value constructor function doesn't
   235  			// matter because it will never be called.
   236  			name:           "CacheHit",
   237  			valConstructor: badValConstructor,
   238  			cacheInitialState: []CacheKeyParts{
   239  				{
   240  					Identifier: "foo/bar",
   241  					BaseSHA:    "ba5e",
   242  					HeadSHAs:   []string{"abcd", "ef01"},
   243  				},
   244  			},
   245  			cacheCorrupted:      false,
   246  			inRepoConfigEnabled: true,
   247  			identifier:          "foo/bar",
   248  			baseBranch:          "main",
   249  			baseSHAGetter:       goodSHAGetter("ba5e"),
   250  			headSHAGetters: []RefGetter{
   251  				goodSHAGetter("abcd"),
   252  				goodSHAGetter("ef01")},
   253  			expected: expected{
   254  				prowYAML: &ProwYAML{
   255  					Presubmits: []Presubmit{
   256  						{
   257  							JobBase: JobBase{Name: `{"identifier":"foo/bar","baseSHA":"ba5e","headSHAs":["abcd","ef01"]}`},
   258  						},
   259  					},
   260  				},
   261  				cacheLen: 1,
   262  				err:      "",
   263  			},
   264  		},
   265  		{
   266  			name:                "BadValConstructorCacheMiss",
   267  			valConstructor:      badValConstructor,
   268  			cacheInitialState:   nil,
   269  			cacheCorrupted:      false,
   270  			inRepoConfigEnabled: true,
   271  			identifier:          "foo/bar",
   272  			baseBranch:          "main",
   273  			baseSHAGetter:       goodSHAGetter("ba5e"),
   274  			headSHAGetters: []RefGetter{
   275  				goodSHAGetter("abcd"),
   276  				goodSHAGetter("ef01")},
   277  			expected: expected{
   278  				prowYAML: nil,
   279  				err:      "unable to construct *ProwYAML value",
   280  			},
   281  		},
   282  		{
   283  			// If we get a cache hit, then it doesn't matter if the state of the
   284  			// world was such that the value could not have been constructed from
   285  			// scratch (because we're solely relying on the cache).
   286  			name:           "BadValConstructorCacheHit",
   287  			valConstructor: badValConstructor,
   288  			cacheInitialState: []CacheKeyParts{
   289  				{
   290  					Identifier: "foo/bar",
   291  					BaseSHA:    "ba5e",
   292  					HeadSHAs:   []string{"abcd", "ef01"},
   293  				},
   294  			},
   295  			cacheCorrupted:      false,
   296  			inRepoConfigEnabled: true,
   297  			identifier:          "foo/bar",
   298  			baseBranch:          "main",
   299  			baseSHAGetter:       goodSHAGetter("ba5e"),
   300  			headSHAGetters: []RefGetter{
   301  				goodSHAGetter("abcd"),
   302  				goodSHAGetter("ef01")},
   303  			expected: expected{
   304  				prowYAML: &ProwYAML{
   305  					Presubmits: []Presubmit{
   306  						{
   307  							JobBase: JobBase{Name: `{"identifier":"foo/bar","baseSHA":"ba5e","headSHAs":["abcd","ef01"]}`}},
   308  					},
   309  				},
   310  				cacheLen: 1,
   311  				err:      "",
   312  			},
   313  		},
   314  		{
   315  			// If the cache is corrupted (it holds values of a type that is not
   316  			// *ProwYAML), then we expect an error.
   317  			name:           "GoodValConstructorCorruptedCacheHit",
   318  			valConstructor: goodValConstructor,
   319  			cacheInitialState: []CacheKeyParts{
   320  				{
   321  					Identifier: "foo/bar",
   322  					BaseSHA:    "ba5e",
   323  					HeadSHAs:   []string{"abcd", "ef01"},
   324  				},
   325  			},
   326  			cacheCorrupted:      true,
   327  			inRepoConfigEnabled: true,
   328  			identifier:          "foo/bar",
   329  			baseBranch:          "main",
   330  			baseSHAGetter:       goodSHAGetter("ba5e"),
   331  			headSHAGetters: []RefGetter{
   332  				goodSHAGetter("abcd"),
   333  				goodSHAGetter("ef01")},
   334  			expected: expected{
   335  				prowYAML: nil,
   336  				err:      "Programmer error: expected value type '*config.ProwYAML', got 'string'",
   337  			},
   338  		},
   339  		{
   340  			// If the cache is corrupted (it holds values of a type that is not
   341  			// *ProwYAML), then we expect an error.
   342  			name:           "BadValConstructorCorruptedCacheHit",
   343  			valConstructor: badValConstructor,
   344  			cacheInitialState: []CacheKeyParts{
   345  				{
   346  					Identifier: "foo/bar",
   347  					BaseSHA:    "ba5e",
   348  					HeadSHAs:   []string{"abcd", "ef01"},
   349  				},
   350  			},
   351  			cacheCorrupted:      true,
   352  			inRepoConfigEnabled: true,
   353  			identifier:          "foo/bar",
   354  			baseBranch:          "main",
   355  			baseSHAGetter:       goodSHAGetter("ba5e"),
   356  			headSHAGetters: []RefGetter{
   357  				goodSHAGetter("abcd"),
   358  				goodSHAGetter("ef01")},
   359  			expected: expected{
   360  				prowYAML: nil,
   361  				err:      "Programmer error: expected value type '*config.ProwYAML', got 'string'",
   362  			},
   363  		},
   364  	} {
   365  		t.Run(tc.name, func(t1 *testing.T) {
   366  			// Reset test state.
   367  			maybeEnabled := make(map[string]*bool)
   368  			maybeEnabled[tc.identifier] = &tc.inRepoConfigEnabled
   369  
   370  			fca := &fakeConfigAgent{
   371  				c: &Config{
   372  					ProwConfig: ProwConfig{
   373  						InRepoConfig: InRepoConfig{
   374  							Enabled: maybeEnabled,
   375  						},
   376  					},
   377  				},
   378  			}
   379  			cf := &testClientFactory{}
   380  			cache, err := NewInRepoConfigCache(1, fca, cf)
   381  			if err != nil {
   382  				t.Fatal("could not initialize cache")
   383  			}
   384  
   385  			for _, kp := range tc.cacheInitialState {
   386  				k, err := kp.CacheKey()
   387  				if err != nil {
   388  					t.Errorf("Expected error 'nil' got '%v'", err.Error())
   389  				}
   390  				_, _, _ = cache.GetOrAdd(k, goodValConstructorForInitialState(ProwYAML{
   391  					Presubmits: []Presubmit{
   392  						{
   393  							JobBase: JobBase{Name: string(k)}},
   394  					},
   395  				}))
   396  			}
   397  
   398  			// Simulate storing a value of the wrong type in the cache (a string
   399  			// instead of a *ProwYAML).
   400  			if tc.cacheCorrupted {
   401  				func() {
   402  					cache.Lock()
   403  					defer cache.Unlock()
   404  					cache.Purge()
   405  				}()
   406  
   407  				for _, kp := range tc.cacheInitialState {
   408  					k, err := kp.CacheKey()
   409  					if err != nil {
   410  						t.Errorf("Expected error 'nil' got '%v'", err.Error())
   411  					}
   412  					_, _, _ = cache.GetOrAdd(k, func() (interface{}, error) { return "<wrong-type>", nil })
   413  				}
   414  			}
   415  
   416  			prowYAML, err := cache.getProwYAML(tc.valConstructor, tc.identifier, tc.baseBranch, tc.baseSHAGetter, tc.headSHAGetters...)
   417  
   418  			if tc.expected.err == "" {
   419  				if err != nil {
   420  					t.Errorf("Expected error 'nil' got '%v'", err.Error())
   421  				}
   422  			} else {
   423  				if err == nil {
   424  					t.Fatal("Expected non-nil error, got nil")
   425  				}
   426  
   427  				if tc.expected.err != err.Error() {
   428  					t.Errorf("Expected error '%v', got '%v'", tc.expected.err, err.Error())
   429  				}
   430  			}
   431  
   432  			if tc.expected.prowYAML == nil && prowYAML != nil {
   433  				t.Fatalf("Expected nil for *ProwYAML, got '%v'", *prowYAML)
   434  			}
   435  
   436  			if tc.expected.prowYAML != nil && prowYAML == nil {
   437  				t.Fatal("Expected non-nil for *ProwYAML, got nil")
   438  			}
   439  
   440  			// If we got what we expected, there's no need to compare these two.
   441  			if tc.expected.prowYAML == nil && prowYAML == nil {
   442  				return
   443  			}
   444  
   445  			// The Presubmit type is not comparable. So instead of checking the
   446  			// overall type for equality, we only check the Name field of it,
   447  			// because it is a simple string type.
   448  			if len(tc.expected.prowYAML.Presubmits) != len(prowYAML.Presubmits) {
   449  				t.Fatalf("Expected prowYAML length '%d', got '%d'", len(tc.expected.prowYAML.Presubmits), len(prowYAML.Presubmits))
   450  			}
   451  			for i := range tc.expected.prowYAML.Presubmits {
   452  				if tc.expected.prowYAML.Presubmits[i].Name != prowYAML.Presubmits[i].Name {
   453  					t.Errorf("Expected presubmits[%d].Name to be '%v', got '%v'", i, tc.expected.prowYAML.Presubmits[i].Name, prowYAML.Presubmits[i].Name)
   454  				}
   455  			}
   456  
   457  			if tc.expected.cacheLen != cache.Len() {
   458  				t.Errorf("Expected '%d' cached elements, got '%d'", tc.expected.cacheLen, cache.Len())
   459  			}
   460  		})
   461  	}
   462  }
   463  
   464  // TestGetProwYAMLCachedAndDefaulted checks that calls to
   465  // cache.GetPresubmits() and cache.GetPostsubmits() return
   466  // defaulted values from the Config, and that changing (reloading) this Config
   467  // and calling it again with the same key (same cached ProwYAML, which has both
   468  // []Presubmit and []Postsubmit jobs) results in returning a __differently__
   469  // defaulted ProwYAML object.
   470  func TestGetProwYAMLCachedAndDefaulted(t *testing.T) {
   471  	identifier := "org/repo"
   472  	baseBranch := "main"
   473  	baseSHAGetter := goodSHAGetter("ba5e")
   474  	headSHAGetters := []RefGetter{
   475  		goodSHAGetter("abcd"),
   476  		goodSHAGetter("ef01"),
   477  	}
   478  
   479  	envBefore := []v1.EnvVar{
   480  		{
   481  			Name:  "ENV_VAR_FOO",
   482  			Value: "VALUE",
   483  		},
   484  	}
   485  	decorationConfigBefore := &prowapi.DecorationConfig{
   486  		GCSConfiguration: &prowapi.GCSConfiguration{
   487  			PathStrategy: prowapi.PathStrategyExplicit,
   488  			DefaultOrg:   "org",
   489  			DefaultRepo:  "repo",
   490  		},
   491  		GCSCredentialsSecret: pStr("service-account-secret"),
   492  		UtilityImages: &prowapi.UtilityImages{
   493  			CloneRefs:  "clonerefs:default-BEFORE",
   494  			InitUpload: "initupload:default-BEFORE",
   495  			Entrypoint: "entrypoint:default-BEFORE",
   496  			Sidecar:    "sidecar:default-BEFORE",
   497  		},
   498  	}
   499  
   500  	envAfter := ([]v1.EnvVar)(nil)
   501  	decorationConfigAfter := &prowapi.DecorationConfig{
   502  		GCSConfiguration: &prowapi.GCSConfiguration{
   503  			PathStrategy: prowapi.PathStrategyExplicit,
   504  			DefaultOrg:   "org",
   505  			DefaultRepo:  "repo",
   506  		},
   507  		GCSCredentialsSecret: pStr("service-account-secret"),
   508  		UtilityImages: &prowapi.UtilityImages{
   509  			CloneRefs:  "clonerefs:default-AFTER",
   510  			InitUpload: "initupload:default-AFTER",
   511  			Entrypoint: "entrypoint:default-AFTER",
   512  			Sidecar:    "sidecar:default-AFTER",
   513  		},
   514  	}
   515  
   516  	type expected struct {
   517  		presubmits  []Presubmit
   518  		postsubmits []Postsubmit
   519  	}
   520  
   521  	true_ := true
   522  
   523  	defaultedPresubmit := func(env []v1.EnvVar, dc *prowapi.DecorationConfig) Presubmit {
   524  		return Presubmit{
   525  			JobBase: JobBase{
   526  				Name:           "presubmitFoo",
   527  				Agent:          "kubernetes",
   528  				Cluster:        "clusterFoo",
   529  				Namespace:      pStr("default"),
   530  				ProwJobDefault: &prowapi.ProwJobDefault{TenantID: "GlobalDefaultID"},
   531  				Spec: &v1.PodSpec{
   532  					Containers: []v1.Container{
   533  						{
   534  							Name:    "hello",
   535  							Image:   "there",
   536  							Command: []string{"earthlings"},
   537  							Env:     env,
   538  						},
   539  					},
   540  				},
   541  				UtilityConfig: UtilityConfig{
   542  					Decorate:         &true_,
   543  					DecorationConfig: dc,
   544  				},
   545  			},
   546  			Trigger:      `(?m)^/test( | .* )presubmitFoo,?($|\s.*)`,
   547  			RerunCommand: "/test presubmitFoo",
   548  			Reporter: Reporter{
   549  				Context:    "presubmitFoo",
   550  				SkipReport: false,
   551  			},
   552  		}
   553  	}
   554  
   555  	defaultedPostsubmit := func(env []v1.EnvVar, dc *prowapi.DecorationConfig) Postsubmit {
   556  		return Postsubmit{
   557  			JobBase: JobBase{
   558  				Name:           "postsubmitFoo",
   559  				Agent:          "kubernetes",
   560  				Cluster:        "clusterFoo",
   561  				Namespace:      pStr("default"),
   562  				ProwJobDefault: &prowapi.ProwJobDefault{TenantID: "GlobalDefaultID"},
   563  				Spec: &v1.PodSpec{
   564  					Containers: []v1.Container{
   565  						{
   566  							Name:    "hello",
   567  							Image:   "there",
   568  							Command: []string{"earthlings"},
   569  							Env:     env,
   570  						},
   571  					},
   572  				},
   573  				UtilityConfig: UtilityConfig{
   574  					Decorate:         &true_,
   575  					DecorationConfig: dc,
   576  				},
   577  			},
   578  			Reporter: Reporter{
   579  				Context:    "postsubmitFoo",
   580  				SkipReport: false,
   581  			},
   582  		}
   583  	}
   584  	inRepoConfigEnabled := make(map[string]*bool)
   585  	inRepoConfigEnabled[identifier] = &true_
   586  
   587  	// fakeProwYAMLGetterFunc mocks prowYAMLGetter(). Instead of using the
   588  	// git.ClientFactory (and other operations), we just use a simple map to get
   589  	// the *ProwYAML value we want. The point of fakeProwYAMLGetterFunc is to
   590  	// act as a "source of truth" of authoritative *ProwYAML values for purposes
   591  	// of the test cases in this unit test.
   592  	fakeProwYAMLGetterFunc := func() ProwYAMLGetter {
   593  		presubmitUndecorated := Presubmit{
   594  			JobBase: JobBase{
   595  				Name:      "presubmitFoo",
   596  				Cluster:   "clusterFoo",
   597  				Namespace: pStr("default"),
   598  				Spec: &v1.PodSpec{
   599  					Containers: []v1.Container{
   600  						{
   601  							Name:    "hello",
   602  							Image:   "there",
   603  							Command: []string{"earthlings"},
   604  						},
   605  					},
   606  				},
   607  			},
   608  		}
   609  		postsubmitUndecorated := Postsubmit{
   610  			JobBase: JobBase{
   611  				Name:      "postsubmitFoo",
   612  				Cluster:   "clusterFoo",
   613  				Namespace: pStr("default"),
   614  				Spec: &v1.PodSpec{
   615  					Containers: []v1.Container{
   616  						{
   617  							Name:    "hello",
   618  							Image:   "there",
   619  							Command: []string{"earthlings"},
   620  						},
   621  					},
   622  				},
   623  			},
   624  		}
   625  		return fakeProwYAMLGetterFactory(
   626  			[]Presubmit{presubmitUndecorated},
   627  			[]Postsubmit{postsubmitUndecorated})
   628  	}
   629  
   630  	makeConfig := func(env []v1.EnvVar, ddc []*DefaultDecorationConfigEntry) *Config {
   631  		return &Config{
   632  			ProwConfig: ProwConfig{
   633  				InRepoConfig: InRepoConfig{
   634  					AllowedClusters: map[string][]string{
   635  						"org/repo": {"clusterFoo"},
   636  					},
   637  					Enabled: inRepoConfigEnabled,
   638  				},
   639  				Plank: Plank{
   640  					DefaultDecorationConfigs: ddc,
   641  				},
   642  				PodNamespace: "default",
   643  			},
   644  			JobConfig: JobConfig{
   645  				DecorateAllJobs: true,
   646  				ProwYAMLGetter:  fakeProwYAMLGetterFunc(),
   647  				Presets: []Preset{
   648  					{
   649  						Env: env,
   650  					},
   651  				},
   652  			},
   653  		}
   654  	}
   655  
   656  	presubmitBefore := defaultedPresubmit(envBefore, decorationConfigBefore)
   657  	postsubmitBefore := defaultedPostsubmit(envBefore, decorationConfigBefore)
   658  
   659  	for _, tc := range []struct {
   660  		name string
   661  		// Initial state of Config with a particular DefaultDecorationConfigEntry.
   662  		configBefore   *Config
   663  		expectedBefore expected
   664  		// Changed state of Config with a possibly __different__ DefaultDecorationConfigEntry.
   665  		configAfter   *Config
   666  		expectedAfter expected
   667  	}{
   668  		{
   669  			// Config has not changed between multiple
   670  			// cache.GetPresubmits() calls.
   671  			name: "ConfigNotChanged",
   672  			configBefore: makeConfig(envBefore, []*DefaultDecorationConfigEntry{
   673  				{
   674  					OrgRepo: "*",
   675  					Cluster: "*",
   676  					Config:  decorationConfigBefore,
   677  				},
   678  			}),
   679  			// These are the expected []Presubmit and []Postsubmit values when
   680  			// defaulted with the "decorationConfigBefore" value. Among other
   681  			// things, the UtilityConfig.DecorationConfig value should reflect
   682  			// the same settings as "decorationConfigBefore".
   683  			expectedBefore: expected{
   684  				presubmits:  []Presubmit{presubmitBefore},
   685  				postsubmits: []Postsubmit{postsubmitBefore},
   686  			},
   687  			// For this test case, we do not change the
   688  			// DefualtDecorationConfigEntry at all, so we don't expect any
   689  			// changes.
   690  			configAfter: makeConfig(envBefore, []*DefaultDecorationConfigEntry{
   691  				{
   692  					OrgRepo: "*",
   693  					Cluster: "*",
   694  					Config:  decorationConfigBefore,
   695  				},
   696  			}),
   697  			expectedAfter: expected{
   698  				presubmits:  []Presubmit{presubmitBefore},
   699  				postsubmits: []Postsubmit{postsubmitBefore},
   700  			},
   701  		},
   702  		{
   703  			// Config has changed between multiple requests to cache.
   704  			name: "ConfigChanged",
   705  			configBefore: makeConfig(envBefore, []*DefaultDecorationConfigEntry{
   706  				{
   707  					OrgRepo: "*",
   708  					Cluster: "*",
   709  					Config:  decorationConfigBefore,
   710  				},
   711  			}),
   712  			// These are the expected []Presubmit and []Postsubmit values when
   713  			// defaulted with the "decorationConfigBefore" value. Among other
   714  			// things, the UtilityConfig.DecorationConfig value should reflect
   715  			// the same settings as "decorationConfigBefore".
   716  			expectedBefore: expected{
   717  				presubmits:  []Presubmit{presubmitBefore},
   718  				postsubmits: []Postsubmit{postsubmitBefore},
   719  			},
   720  			// Change the config to decorationConfigAfter.
   721  			configAfter: makeConfig(envAfter, []*DefaultDecorationConfigEntry{
   722  				{
   723  					OrgRepo: "*",
   724  					Cluster: "*",
   725  					Config:  decorationConfigAfter,
   726  				},
   727  			}),
   728  			// Expect "Env" field to be a nil pointer.
   729  			expectedAfter: expected{
   730  				presubmits: []Presubmit{
   731  					{
   732  						JobBase: JobBase{
   733  							Name:           "presubmitFoo",
   734  							Agent:          "kubernetes",
   735  							Cluster:        "clusterFoo",
   736  							Namespace:      pStr("default"),
   737  							ProwJobDefault: &prowapi.ProwJobDefault{TenantID: "GlobalDefaultID"},
   738  							Spec: &v1.PodSpec{
   739  								Containers: []v1.Container{
   740  									{
   741  										Name:    "hello",
   742  										Image:   "there",
   743  										Command: []string{"earthlings"},
   744  										// Env field is a nil pointer!
   745  										Env: nil,
   746  									},
   747  								},
   748  							},
   749  							UtilityConfig: UtilityConfig{
   750  								Decorate:         &true_,
   751  								DecorationConfig: decorationConfigAfter,
   752  							},
   753  						},
   754  						Trigger:      `(?m)^/test( | .* )presubmitFoo,?($|\s.*)`,
   755  						RerunCommand: "/test presubmitFoo",
   756  						Reporter: Reporter{
   757  							Context:    "presubmitFoo",
   758  							SkipReport: false,
   759  						},
   760  					},
   761  				},
   762  				postsubmits: []Postsubmit{
   763  					{
   764  						JobBase: JobBase{
   765  							Name:           "postsubmitFoo",
   766  							Agent:          "kubernetes",
   767  							Cluster:        "clusterFoo",
   768  							Namespace:      pStr("default"),
   769  							ProwJobDefault: &prowapi.ProwJobDefault{TenantID: "GlobalDefaultID"},
   770  							Spec: &v1.PodSpec{
   771  								Containers: []v1.Container{
   772  									{
   773  										Name:    "hello",
   774  										Image:   "there",
   775  										Command: []string{"earthlings"},
   776  										// Env field is a nil pointer!
   777  										Env: nil,
   778  									},
   779  								},
   780  							},
   781  							UtilityConfig: UtilityConfig{
   782  								Decorate:         &true_,
   783  								DecorationConfig: decorationConfigAfter,
   784  							},
   785  						},
   786  						Reporter: Reporter{
   787  							Context:    "postsubmitFoo",
   788  							SkipReport: false,
   789  						},
   790  					},
   791  				},
   792  			},
   793  		},
   794  	} {
   795  		t.Run(tc.name, func(t1 *testing.T) {
   796  			// Set initial Config.
   797  			fca := &fakeConfigAgent{
   798  				c: tc.configBefore,
   799  			}
   800  			cf := &testClientFactory{}
   801  
   802  			// Initialize cache. Notice that it relies on a snapshot of the Config with configBefore.
   803  			cache, err := NewInRepoConfigCache(10, fca, cf)
   804  			if err != nil {
   805  				t1.Fatal("could not initialize cache")
   806  			}
   807  
   808  			// Get cached values. These cached values should be defaulted by the
   809  			// initial Config.
   810  			// Make sure that this runs concurrently without problem.
   811  			var errGroup errgroup.Group
   812  			for i := 0; i < 1000; i++ {
   813  				errGroup.Go(func() error {
   814  					presubmits, err := cache.GetPresubmits(identifier, baseBranch, baseSHAGetter, headSHAGetters...)
   815  					if err != nil {
   816  						return fmt.Errorf("Expected error 'nil' got '%v'", err.Error())
   817  					}
   818  					if diff := cmp.Diff(tc.expectedBefore.presubmits, presubmits, cmpopts.IgnoreUnexported(Presubmit{}, Brancher{}, RegexpChangeMatcher{})); diff != "" {
   819  						return fmt.Errorf("(before Config reload) presubmits mismatch (-want +got):\n%s", diff)
   820  					}
   821  					return nil
   822  				})
   823  
   824  				errGroup.Go(func() error {
   825  					postsubmits, err := cache.GetPostsubmits(identifier, baseBranch, baseSHAGetter, headSHAGetters...)
   826  					if err != nil {
   827  						return fmt.Errorf("Expected error 'nil' got '%v'", err.Error())
   828  					}
   829  
   830  					if diff := cmp.Diff(tc.expectedBefore.postsubmits, postsubmits, cmpopts.IgnoreUnexported(Postsubmit{}, Brancher{}, RegexpChangeMatcher{})); diff != "" {
   831  						return fmt.Errorf("(before Config reload) postsubmits mismatch (-want +got):\n%s", diff)
   832  					}
   833  					return nil
   834  				})
   835  			}
   836  
   837  			if err := errGroup.Wait(); err != nil {
   838  				t.Fatalf("Failed processing concurrently: %v", err)
   839  			}
   840  
   841  			// Reload Config.
   842  			fca.c = tc.configAfter
   843  
   844  			presubmits, err := cache.GetPresubmits(identifier, baseBranch, baseSHAGetter, headSHAGetters...)
   845  			if err != nil {
   846  				t1.Fatalf("Expected error 'nil' got '%v'", err.Error())
   847  			}
   848  			postsubmits, err := cache.GetPostsubmits(identifier, baseBranch, baseSHAGetter, headSHAGetters...)
   849  			if err != nil {
   850  				t1.Fatalf("Expected error 'nil' got '%v'", err.Error())
   851  			}
   852  
   853  			if diff := cmp.Diff(tc.expectedAfter.presubmits, presubmits, cmpopts.IgnoreUnexported(Presubmit{}, Brancher{}, RegexpChangeMatcher{})); diff != "" {
   854  				t1.Errorf("(after Config reload) presubmits mismatch (-want +got):\n%s", diff)
   855  			}
   856  			if diff := cmp.Diff(tc.expectedAfter.postsubmits, postsubmits, cmpopts.IgnoreUnexported(Postsubmit{}, Brancher{}, RegexpChangeMatcher{})); diff != "" {
   857  				t1.Errorf("(after Config reload) postsubmits mismatch (-want +got):\n%s", diff)
   858  			}
   859  
   860  		})
   861  	}
   862  
   863  }