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 }