github.com/openshift/installer@v1.4.17/pkg/asset/store/store_test.go (about)

     1  package store
     2  
     3  import (
     4  	"context"
     5  	"os"
     6  	"path/filepath"
     7  	"reflect"
     8  	"testing"
     9  
    10  	"github.com/stretchr/testify/assert"
    11  
    12  	"github.com/openshift/installer/pkg/asset"
    13  )
    14  
    15  var (
    16  	// It is unfortunate that these need to be global variables. However, the
    17  	// asset store creates new assets by type, so the tests cannot store behavior
    18  	// state in the assets themselves.
    19  	generationLog []string
    20  	dependencies  map[reflect.Type][]asset.Asset
    21  	onDiskAssets  map[reflect.Type]bool
    22  )
    23  
    24  func clearAssetBehaviors() {
    25  	generationLog = []string{}
    26  	dependencies = map[reflect.Type][]asset.Asset{}
    27  	onDiskAssets = map[reflect.Type]bool{}
    28  }
    29  
    30  func dependenciesTestStoreAsset(a asset.Asset) []asset.Asset {
    31  	return dependencies[reflect.TypeOf(a)]
    32  }
    33  
    34  func generateTestStoreAsset(a asset.Asset) error {
    35  	generationLog = append(generationLog, a.Name())
    36  	return nil
    37  }
    38  
    39  func fileTestStoreAsset(a asset.Asset) []*asset.File {
    40  	return []*asset.File{{Filename: a.Name()}}
    41  }
    42  
    43  func loadTestStoreAsset(a asset.Asset) (bool, error) {
    44  	return onDiskAssets[reflect.TypeOf(a)], nil
    45  }
    46  
    47  type testStoreAssetA struct{}
    48  
    49  func (a *testStoreAssetA) Name() string {
    50  	return "a"
    51  }
    52  
    53  func (a *testStoreAssetA) Dependencies() []asset.Asset {
    54  	return dependenciesTestStoreAsset(a)
    55  }
    56  
    57  func (a *testStoreAssetA) Generate(context.Context, asset.Parents) error {
    58  	return generateTestStoreAsset(a)
    59  }
    60  
    61  func (a *testStoreAssetA) Files() []*asset.File {
    62  	return fileTestStoreAsset(a)
    63  }
    64  
    65  func (a *testStoreAssetA) Load(asset.FileFetcher) (bool, error) {
    66  	return loadTestStoreAsset(a)
    67  }
    68  
    69  type testStoreAssetB struct{}
    70  
    71  func (a *testStoreAssetB) Name() string {
    72  	return "b"
    73  }
    74  
    75  func (a *testStoreAssetB) Dependencies() []asset.Asset {
    76  	return dependenciesTestStoreAsset(a)
    77  }
    78  
    79  func (a *testStoreAssetB) Generate(context.Context, asset.Parents) error {
    80  	return generateTestStoreAsset(a)
    81  }
    82  
    83  func (a *testStoreAssetB) Files() []*asset.File {
    84  	return fileTestStoreAsset(a)
    85  }
    86  
    87  func (a *testStoreAssetB) Load(asset.FileFetcher) (bool, error) {
    88  	return loadTestStoreAsset(a)
    89  }
    90  
    91  type testStoreAssetC struct{}
    92  
    93  func (a *testStoreAssetC) Name() string {
    94  	return "c"
    95  }
    96  
    97  func (a *testStoreAssetC) Dependencies() []asset.Asset {
    98  	return dependenciesTestStoreAsset(a)
    99  }
   100  
   101  func (a *testStoreAssetC) Generate(context.Context, asset.Parents) error {
   102  	return generateTestStoreAsset(a)
   103  }
   104  
   105  func (a *testStoreAssetC) Files() []*asset.File {
   106  	return fileTestStoreAsset(a)
   107  }
   108  
   109  func (a *testStoreAssetC) Load(asset.FileFetcher) (bool, error) {
   110  	return loadTestStoreAsset(a)
   111  }
   112  
   113  type testStoreAssetD struct{}
   114  
   115  func (a *testStoreAssetD) Name() string {
   116  	return "d"
   117  }
   118  
   119  func (a *testStoreAssetD) Dependencies() []asset.Asset {
   120  	return dependenciesTestStoreAsset(a)
   121  }
   122  
   123  func (a *testStoreAssetD) Generate(context.Context, asset.Parents) error {
   124  	return generateTestStoreAsset(a)
   125  }
   126  
   127  func (a *testStoreAssetD) Files() []*asset.File {
   128  	return fileTestStoreAsset(a)
   129  }
   130  
   131  func (a *testStoreAssetD) Load(asset.FileFetcher) (bool, error) {
   132  	return loadTestStoreAsset(a)
   133  }
   134  
   135  func newTestStoreAsset(name string) asset.Asset {
   136  	switch name {
   137  	case "a":
   138  		return &testStoreAssetA{}
   139  	case "b":
   140  		return &testStoreAssetB{}
   141  	case "c":
   142  		return &testStoreAssetC{}
   143  	case "d":
   144  		return &testStoreAssetD{}
   145  	default:
   146  		return nil
   147  	}
   148  }
   149  
   150  // TestStoreFetch tests the Fetch method of StoreImpl.
   151  func TestStoreFetch(t *testing.T) {
   152  	cases := []struct {
   153  		name                  string
   154  		assets                map[string][]string
   155  		existingAssets        []string
   156  		target                string
   157  		expectedGenerationLog []string
   158  	}{
   159  		{
   160  			name: "no dependencies",
   161  			assets: map[string][]string{
   162  				"a": {},
   163  			},
   164  			target:                "a",
   165  			expectedGenerationLog: []string{"a"},
   166  		},
   167  		{
   168  			name: "single dependency",
   169  			assets: map[string][]string{
   170  				"a": {"b"},
   171  				"b": {},
   172  			},
   173  			target:                "a",
   174  			expectedGenerationLog: []string{"b", "a"},
   175  		},
   176  		{
   177  			name: "multiple dependencies",
   178  			assets: map[string][]string{
   179  				"a": {"b", "c"},
   180  				"b": {},
   181  				"c": {},
   182  			},
   183  			target:                "a",
   184  			expectedGenerationLog: []string{"b", "c", "a"},
   185  		},
   186  		{
   187  			name: "grandchild dependency",
   188  			assets: map[string][]string{
   189  				"a": {"b"},
   190  				"b": {"c"},
   191  				"c": {},
   192  			},
   193  			target:                "a",
   194  			expectedGenerationLog: []string{"c", "b", "a"},
   195  		},
   196  		{
   197  			name: "intragenerational shared dependency",
   198  			assets: map[string][]string{
   199  				"a": {"b", "c"},
   200  				"b": {"d"},
   201  				"c": {"d"},
   202  				"d": {},
   203  			},
   204  			target:                "a",
   205  			expectedGenerationLog: []string{"d", "b", "c", "a"},
   206  		},
   207  		{
   208  			name: "intergenerational shared dependency",
   209  			assets: map[string][]string{
   210  				"a": {"b", "c"},
   211  				"b": {"c"},
   212  				"c": {},
   213  			},
   214  			target:                "a",
   215  			expectedGenerationLog: []string{"c", "b", "a"},
   216  		},
   217  		{
   218  			name: "existing asset",
   219  			assets: map[string][]string{
   220  				"a": {},
   221  			},
   222  			existingAssets:        []string{"a"},
   223  			target:                "a",
   224  			expectedGenerationLog: []string{},
   225  		},
   226  		{
   227  			name: "existing child asset",
   228  			assets: map[string][]string{
   229  				"a": {"b"},
   230  				"b": {},
   231  			},
   232  			existingAssets:        []string{"b"},
   233  			target:                "a",
   234  			expectedGenerationLog: []string{"a"},
   235  		},
   236  		{
   237  			name: "absent grandchild asset",
   238  			assets: map[string][]string{
   239  				"a": {"b"},
   240  				"b": {"c"},
   241  				"c": {},
   242  			},
   243  			existingAssets:        []string{"b"},
   244  			target:                "a",
   245  			expectedGenerationLog: []string{"a"},
   246  		},
   247  		{
   248  			name: "absent grandchild with absent parent",
   249  			assets: map[string][]string{
   250  				"a": {"b", "c"},
   251  				"b": {"d"},
   252  				"c": {"d"},
   253  				"d": {},
   254  			},
   255  			existingAssets:        []string{"b"},
   256  			target:                "a",
   257  			expectedGenerationLog: []string{"d", "c", "a"},
   258  		},
   259  	}
   260  	for _, tc := range cases {
   261  		t.Run(tc.name, func(t *testing.T) {
   262  			clearAssetBehaviors()
   263  			store := &storeImpl{
   264  				directory: t.TempDir(),
   265  				assets:    map[reflect.Type]*assetState{},
   266  			}
   267  			assets := make(map[string]asset.Asset, len(tc.assets))
   268  			for name := range tc.assets {
   269  				assets[name] = newTestStoreAsset(name)
   270  			}
   271  			for name, deps := range tc.assets {
   272  				dependenciesOfAsset := make([]asset.Asset, len(deps))
   273  				for i, d := range deps {
   274  					dependenciesOfAsset[i] = assets[d]
   275  				}
   276  				dependencies[reflect.TypeOf(assets[name])] = dependenciesOfAsset
   277  			}
   278  			for _, assetName := range tc.existingAssets {
   279  				asset := assets[assetName]
   280  				store.assets[reflect.TypeOf(asset)] = &assetState{
   281  					asset:  asset,
   282  					source: generatedSource,
   283  				}
   284  			}
   285  			err := store.Fetch(context.Background(), assets[tc.target])
   286  			assert.NoError(t, err, "error fetching asset")
   287  			assert.EqualValues(t, tc.expectedGenerationLog, generationLog)
   288  		})
   289  	}
   290  }
   291  
   292  func TestStoreFetchOnDiskAssets(t *testing.T) {
   293  	cases := []struct {
   294  		name                  string
   295  		assets                map[string][]string
   296  		onDiskAssets          []string
   297  		target                string
   298  		expectedGenerationLog []string
   299  		expectedDirty         bool
   300  	}{
   301  		{
   302  			name: "no on-disk assets",
   303  			assets: map[string][]string{
   304  				"a": {"b"},
   305  				"b": {},
   306  			},
   307  			onDiskAssets:          nil,
   308  			target:                "a",
   309  			expectedGenerationLog: []string{"b", "a"},
   310  			expectedDirty:         false,
   311  		},
   312  		{
   313  			name: "on-disk asset does not need dependent generation",
   314  			assets: map[string][]string{
   315  				"a": {"b"},
   316  				"b": {},
   317  			},
   318  			onDiskAssets:          []string{"a"},
   319  			target:                "a",
   320  			expectedGenerationLog: []string{},
   321  			expectedDirty:         false,
   322  		},
   323  		{
   324  			name: "on-disk dependent asset causes re-generation",
   325  			assets: map[string][]string{
   326  				"a": {"b"},
   327  				"b": {},
   328  			},
   329  			onDiskAssets:          []string{"b"},
   330  			target:                "a",
   331  			expectedGenerationLog: []string{"a"},
   332  			expectedDirty:         true,
   333  		},
   334  		{
   335  			name: "on-disk dependents invalidate all its children",
   336  			assets: map[string][]string{
   337  				"a": {"b", "c"},
   338  				"b": {"d"},
   339  				"c": {"d"},
   340  				"d": {},
   341  			},
   342  			onDiskAssets:          []string{"d"},
   343  			target:                "a",
   344  			expectedGenerationLog: []string{"b", "c", "a"},
   345  			expectedDirty:         true,
   346  		},
   347  		{
   348  			name: "re-generate when both parents and children are on-disk",
   349  			assets: map[string][]string{
   350  				"a": {"b"},
   351  				"b": {},
   352  			},
   353  			onDiskAssets:          []string{"a", "b"},
   354  			target:                "a",
   355  			expectedGenerationLog: []string{"a"},
   356  			expectedDirty:         true,
   357  		},
   358  	}
   359  	for _, tc := range cases {
   360  		t.Run(tc.name, func(t *testing.T) {
   361  			clearAssetBehaviors()
   362  			store := &storeImpl{
   363  				assets: map[reflect.Type]*assetState{},
   364  			}
   365  			assets := make(map[string]asset.Asset, len(tc.assets))
   366  			for name := range tc.assets {
   367  				assets[name] = newTestStoreAsset(name)
   368  			}
   369  			for name, deps := range tc.assets {
   370  				dependenciesOfAsset := make([]asset.Asset, len(deps))
   371  				for i, d := range deps {
   372  					dependenciesOfAsset[i] = assets[d]
   373  				}
   374  				dependencies[reflect.TypeOf(assets[name])] = dependenciesOfAsset
   375  			}
   376  			for _, name := range tc.onDiskAssets {
   377  				onDiskAssets[reflect.TypeOf(assets[name])] = true
   378  			}
   379  			err := store.fetch(context.Background(), assets[tc.target], "")
   380  			assert.NoError(t, err, "unexpected error")
   381  			assert.EqualValues(t, tc.expectedGenerationLog, generationLog)
   382  			assert.Equal(t, tc.expectedDirty, store.assets[reflect.TypeOf(assets[tc.target])].anyParentsDirty)
   383  		})
   384  	}
   385  }
   386  
   387  func TestStoreFetchIdempotency(t *testing.T) {
   388  	clearAssetBehaviors()
   389  
   390  	tempDir := t.TempDir()
   391  
   392  	for i := 0; i < 2; i++ {
   393  		store, err := newStore(tempDir)
   394  		if !assert.NoError(t, err, "(loop %d) unexpected error creating store", i) {
   395  			t.Fatal()
   396  		}
   397  		assets := []asset.WritableAsset{&testStoreAssetA{}, &testStoreAssetB{}}
   398  		for _, a := range assets {
   399  			err = store.Fetch(context.Background(), a, assets...)
   400  			if !assert.NoError(t, err, "(loop %d) unexpected error fetching asset %q", a.Name()) {
   401  				t.Fatal()
   402  			}
   403  			err = asset.PersistToFile(a, tempDir)
   404  			if !assert.NoError(t, err, "(loop %d) unexpected error persisting asset %q", a.Name()) {
   405  				t.Fatal()
   406  			}
   407  			onDiskAssets[reflect.TypeOf(a)] = true
   408  		}
   409  	}
   410  
   411  	expectedFiles := []string{"a", "b"}
   412  	actualFiles := []string{}
   413  	walkFunc := func(path string, fi os.FileInfo, err error) error {
   414  		if fi.IsDir() || fi.Name() == stateFileName {
   415  			return nil
   416  		}
   417  		actualFiles = append(actualFiles, fi.Name())
   418  		return nil
   419  	}
   420  	filepath.Walk(tempDir, walkFunc)
   421  	assert.Equal(t, expectedFiles, actualFiles, "unexpected files on disk")
   422  }
   423  
   424  func TestStoreLoadOnDiskAssets(t *testing.T) {
   425  	cases := []struct {
   426  		name               string
   427  		assets             map[string][]string
   428  		onDiskAssets       []string
   429  		target             string
   430  		expectedFoundValue bool
   431  	}{
   432  		{
   433  			name: "on-disk assets",
   434  			assets: map[string][]string{
   435  				"a": {},
   436  			},
   437  			onDiskAssets:       []string{"a"},
   438  			target:             "a",
   439  			expectedFoundValue: true,
   440  		},
   441  		{
   442  			name: "no on-disk assets",
   443  			assets: map[string][]string{
   444  				"a": {"b"},
   445  				"b": {},
   446  			},
   447  			onDiskAssets:       nil,
   448  			target:             "a",
   449  			expectedFoundValue: false,
   450  		},
   451  	}
   452  	for _, tc := range cases {
   453  		t.Run(tc.name, func(t *testing.T) {
   454  			clearAssetBehaviors()
   455  			store := &storeImpl{
   456  				assets: map[reflect.Type]*assetState{},
   457  			}
   458  			assets := make(map[string]asset.Asset, len(tc.assets))
   459  			for name := range tc.assets {
   460  				assets[name] = newTestStoreAsset(name)
   461  			}
   462  			for name, deps := range tc.assets {
   463  				dependenciesOfAsset := make([]asset.Asset, len(deps))
   464  				for i, d := range deps {
   465  					dependenciesOfAsset[i] = assets[d]
   466  				}
   467  				dependencies[reflect.TypeOf(assets[name])] = dependenciesOfAsset
   468  			}
   469  			for _, name := range tc.onDiskAssets {
   470  				onDiskAssets[reflect.TypeOf(assets[name])] = true
   471  			}
   472  			found, err := store.Load(assets[tc.target])
   473  			assert.NoError(t, err, "unexpected error")
   474  			assert.EqualValues(t, tc.expectedFoundValue, found != nil)
   475  		})
   476  	}
   477  }