github.com/anchore/syft@v1.38.2/syft/pkg/collection_test.go (about) 1 package pkg 2 3 import ( 4 "context" 5 "testing" 6 7 "github.com/scylladb/go-set/strset" 8 "github.com/stretchr/testify/assert" 9 "github.com/stretchr/testify/require" 10 11 "github.com/anchore/syft/syft/artifact" 12 "github.com/anchore/syft/syft/cpe" 13 "github.com/anchore/syft/syft/file" 14 ) 15 16 type expectedIndexes struct { 17 byType map[Type]*strset.Set 18 byPath map[string]*strset.Set 19 } 20 21 func TestCatalogMergePackageLicenses(t *testing.T) { 22 ctx := context.TODO() 23 tests := []struct { 24 name string 25 pkgs []Package 26 expectedPkgs []Package 27 }{ 28 { 29 name: "merges licenses of packages with equal ID", 30 pkgs: []Package{ 31 { 32 id: "equal", 33 Licenses: NewLicenseSet( 34 NewLicensesFromValuesWithContext(ctx, "foo", "baq", "quz")..., 35 ), 36 }, 37 { 38 id: "equal", 39 Licenses: NewLicenseSet( 40 NewLicensesFromValuesWithContext(ctx, "bar", "baz", "foo", "qux")..., 41 ), 42 }, 43 }, 44 expectedPkgs: []Package{ 45 { 46 id: "equal", 47 Licenses: NewLicenseSet( 48 NewLicensesFromValuesWithContext(ctx, "foo", "baq", "quz", "qux", "bar", "baz")..., 49 ), 50 }, 51 }, 52 }, 53 } 54 55 for _, test := range tests { 56 t.Run(test.name, func(t *testing.T) { 57 collection := NewCollection(test.pkgs...) 58 for i, p := range collection.Sorted() { 59 assert.Equal(t, test.expectedPkgs[i].Licenses, p.Licenses) 60 } 61 }) 62 } 63 } 64 65 func TestCatalogDeleteRemovesPackages(t *testing.T) { 66 tests := []struct { 67 name string 68 pkgs []Package 69 deleteIDs []artifact.ID 70 expectedIndexes expectedIndexes 71 }{ 72 { 73 name: "delete one package", 74 pkgs: []Package{ 75 { 76 id: "pkg:deb/debian/1", 77 Name: "debian", 78 Version: "1", 79 Type: DebPkg, 80 Locations: file.NewLocationSet( 81 file.NewVirtualLocation("/c/path", "/another/path1"), 82 ), 83 }, 84 { 85 id: "pkg:deb/debian/2", 86 Name: "debian", 87 Version: "2", 88 Type: DebPkg, 89 Locations: file.NewLocationSet( 90 file.NewVirtualLocation("/d/path", "/another/path2"), 91 ), 92 }, 93 }, 94 deleteIDs: []artifact.ID{ 95 artifact.ID("pkg:deb/debian/1"), 96 }, 97 expectedIndexes: expectedIndexes{ 98 byType: map[Type]*strset.Set{ 99 DebPkg: strset.New("pkg:deb/debian/2"), 100 }, 101 byPath: map[string]*strset.Set{ 102 "/d/path": strset.New("pkg:deb/debian/2"), 103 "/another/path2": strset.New("pkg:deb/debian/2"), 104 }, 105 }, 106 }, 107 { 108 name: "delete multiple packages", 109 pkgs: []Package{ 110 { 111 id: "pkg:deb/debian/1", 112 Name: "debian", 113 Version: "1", 114 Type: DebPkg, 115 Locations: file.NewLocationSet( 116 file.NewVirtualLocation("/c/path", "/another/path1"), 117 ), 118 }, 119 { 120 id: "pkg:deb/debian/2", 121 Name: "debian", 122 Version: "2", 123 Type: DebPkg, 124 Locations: file.NewLocationSet( 125 file.NewVirtualLocation("/d/path", "/another/path2"), 126 ), 127 }, 128 { 129 id: "pkg:deb/debian/3", 130 Name: "debian", 131 Version: "3", 132 Type: DebPkg, 133 Locations: file.NewLocationSet( 134 file.NewVirtualLocation("/e/path", "/another/path3"), 135 ), 136 }, 137 }, 138 deleteIDs: []artifact.ID{ 139 artifact.ID("pkg:deb/debian/1"), 140 artifact.ID("pkg:deb/debian/3"), 141 }, 142 expectedIndexes: expectedIndexes{ 143 byType: map[Type]*strset.Set{ 144 DebPkg: strset.New("pkg:deb/debian/2"), 145 }, 146 byPath: map[string]*strset.Set{ 147 "/d/path": strset.New("pkg:deb/debian/2"), 148 "/another/path2": strset.New("pkg:deb/debian/2"), 149 }, 150 }, 151 }, 152 { 153 name: "delete non-existent package", 154 pkgs: []Package{ 155 { 156 id: artifact.ID("pkg:deb/debian/1"), 157 Name: "debian", 158 Version: "1", 159 Type: DebPkg, 160 Locations: file.NewLocationSet( 161 file.NewVirtualLocation("/c/path", "/another/path1"), 162 ), 163 }, 164 { 165 id: artifact.ID("pkg:deb/debian/2"), 166 Name: "debian", 167 Version: "2", 168 Type: DebPkg, 169 Locations: file.NewLocationSet( 170 file.NewVirtualLocation("/d/path", "/another/path2"), 171 ), 172 }, 173 }, 174 deleteIDs: []artifact.ID{ 175 artifact.ID("pkg:deb/debian/3"), 176 }, 177 expectedIndexes: expectedIndexes{ 178 byType: map[Type]*strset.Set{ 179 DebPkg: strset.New("pkg:deb/debian/1", "pkg:deb/debian/2"), 180 }, 181 byPath: map[string]*strset.Set{ 182 "/c/path": strset.New("pkg:deb/debian/1"), 183 "/another/path1": strset.New("pkg:deb/debian/1"), 184 "/d/path": strset.New("pkg:deb/debian/2"), 185 "/another/path2": strset.New("pkg:deb/debian/2"), 186 }, 187 }, 188 }, 189 { 190 name: "delete idsBy key entries when all deleted", 191 pkgs: []Package{ 192 { 193 id: artifact.ID("pkg:deb/debian/1"), 194 Name: "debian", 195 Version: "1", 196 Type: DebPkg, 197 Locations: file.NewLocationSet( 198 file.NewVirtualLocation("/c/path", "/another/path1"), 199 ), 200 }, 201 }, 202 deleteIDs: []artifact.ID{ 203 artifact.ID("pkg:deb/debian/1"), 204 }, 205 expectedIndexes: expectedIndexes{}, 206 }, 207 } 208 209 for _, test := range tests { 210 t.Run(test.name, func(t *testing.T) { 211 c := NewCollection() 212 for _, p := range test.pkgs { 213 c.Add(p) 214 } 215 216 for _, id := range test.deleteIDs { 217 c.Delete(id) 218 } 219 220 assertIndexes(t, c, test.expectedIndexes) 221 }) 222 } 223 } 224 225 func TestCatalogAddPopulatesIndex(t *testing.T) { 226 227 var pkgs = []Package{ 228 { 229 Locations: file.NewLocationSet( 230 file.NewVirtualLocation("/a/path", "/another/path"), 231 file.NewVirtualLocation("/b/path", "/bee/path"), 232 ), 233 Type: RpmPkg, 234 }, 235 { 236 Locations: file.NewLocationSet( 237 file.NewVirtualLocation("/c/path", "/another/path"), 238 file.NewVirtualLocation("/d/path", "/another/path"), 239 ), 240 Type: NpmPkg, 241 }, 242 } 243 244 for i := range pkgs { 245 p := &pkgs[i] 246 p.SetID() 247 } 248 249 fixtureID := func(i int) string { 250 return string(pkgs[i].ID()) 251 } 252 253 tests := []struct { 254 name string 255 expectedIndexes expectedIndexes 256 }{ 257 { 258 name: "vanilla-add", 259 expectedIndexes: expectedIndexes{ 260 byType: map[Type]*strset.Set{ 261 RpmPkg: strset.New(fixtureID(0)), 262 NpmPkg: strset.New(fixtureID(1)), 263 }, 264 byPath: map[string]*strset.Set{ 265 "/another/path": strset.New(fixtureID(0), fixtureID(1)), 266 "/a/path": strset.New(fixtureID(0)), 267 "/b/path": strset.New(fixtureID(0)), 268 "/bee/path": strset.New(fixtureID(0)), 269 "/c/path": strset.New(fixtureID(1)), 270 "/d/path": strset.New(fixtureID(1)), 271 }, 272 }, 273 }, 274 } 275 276 for _, test := range tests { 277 t.Run(test.name, func(t *testing.T) { 278 c := NewCollection(pkgs...) 279 assertIndexes(t, c, test.expectedIndexes) 280 }) 281 } 282 } 283 284 func assertIndexes(t *testing.T, c *Collection, expectedIndexes expectedIndexes) { 285 // assert path index 286 assert.Len(t, c.idsByPath, len(expectedIndexes.byPath), "unexpected path index length") 287 for path, expectedIds := range expectedIndexes.byPath { 288 actualIds := strset.New() 289 for _, p := range c.PackagesByPath(path) { 290 actualIds.Add(string(p.ID())) 291 } 292 293 if !expectedIds.IsEqual(actualIds) { 294 t.Errorf("mismatched IDs for path=%q : %+v", path, strset.SymmetricDifference(actualIds, expectedIds)) 295 } 296 } 297 298 // assert type index 299 assert.Len(t, c.idsByType, len(expectedIndexes.byType), "unexpected type index length") 300 for ty, expectedIds := range expectedIndexes.byType { 301 actualIds := strset.New() 302 for p := range c.Enumerate(ty) { 303 actualIds.Add(string(p.ID())) 304 } 305 306 if !expectedIds.IsEqual(actualIds) { 307 t.Errorf("mismatched IDs for type=%q : %+v", ty, strset.SymmetricDifference(actualIds, expectedIds)) 308 } 309 } 310 } 311 312 func TestCatalog_PathIndexDeduplicatesRealVsVirtualPaths(t *testing.T) { 313 p1 := Package{ 314 Locations: file.NewLocationSet( 315 file.NewVirtualLocation("/b/path", "/another/path"), 316 file.NewVirtualLocation("/b/path", "/b/path"), 317 ), 318 Type: RpmPkg, 319 Name: "Package-1", 320 } 321 322 p2 := Package{ 323 Locations: file.NewLocationSet( 324 file.NewVirtualLocation("/b/path", "/b/path"), 325 ), 326 Type: RpmPkg, 327 Name: "Package-2", 328 } 329 p2Dup := Package{ 330 Locations: file.NewLocationSet( 331 file.NewVirtualLocation("/b/path", "/another/path"), 332 file.NewVirtualLocation("/b/path", "/c/path/b/dup"), 333 ), 334 Type: RpmPkg, 335 Name: "Package-2", 336 } 337 338 tests := []struct { 339 name string 340 pkgs []Package 341 paths []string 342 }{ 343 { 344 name: "multiple locations with shared path", 345 pkgs: []Package{p1}, 346 paths: []string{ 347 "/b/path", 348 "/another/path", 349 }, 350 }, 351 { 352 name: "one location with shared path", 353 pkgs: []Package{p2}, 354 paths: []string{ 355 "/b/path", 356 }, 357 }, 358 { 359 name: "two instances with similar locations", 360 pkgs: []Package{p2, p2Dup}, 361 paths: []string{ 362 "/b/path", 363 "/another/path", 364 "/c/path/b/dup", // this updated the path index on merge 365 }, 366 }, 367 } 368 369 for _, test := range tests { 370 t.Run(test.name, func(t *testing.T) { 371 for _, path := range test.paths { 372 actualPackages := NewCollection(test.pkgs...).PackagesByPath(path) 373 require.Len(t, actualPackages, 1) 374 } 375 }) 376 } 377 378 } 379 380 func TestCatalog_MergeRecords(t *testing.T) { 381 var tests = []struct { 382 name string 383 pkgs []Package 384 expectedLocations []file.Location 385 expectedCPECount int 386 }{ 387 { 388 name: "multiple Locations with shared path", 389 pkgs: []Package{ 390 { 391 CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:package:1:1:*:*:*:*:*:*:*", cpe.GeneratedSource)}, 392 Locations: file.NewLocationSet( 393 file.NewVirtualLocationFromCoordinates( 394 file.Coordinates{ 395 RealPath: "/b/path", 396 FileSystemID: "a", 397 }, 398 "/another/path", 399 ), 400 ), 401 Type: RpmPkg, 402 }, 403 { 404 CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:package:2:2:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource)}, 405 Locations: file.NewLocationSet( 406 file.NewVirtualLocationFromCoordinates( 407 file.Coordinates{ 408 RealPath: "/b/path", 409 FileSystemID: "b", 410 }, 411 "/another/path", 412 ), 413 ), 414 Type: RpmPkg, 415 }, 416 }, 417 expectedLocations: []file.Location{ 418 file.NewVirtualLocationFromCoordinates( 419 file.Coordinates{ 420 RealPath: "/b/path", 421 FileSystemID: "a", 422 }, 423 "/another/path", 424 ), 425 file.NewVirtualLocationFromCoordinates( 426 file.Coordinates{ 427 RealPath: "/b/path", 428 FileSystemID: "b", 429 }, 430 "/another/path", 431 ), 432 }, 433 expectedCPECount: 2, 434 }, 435 } 436 437 for _, tt := range tests { 438 t.Run(tt.name, func(t *testing.T) { 439 actual := NewCollection(tt.pkgs...).PackagesByPath("/b/path") 440 require.Len(t, actual, 1) 441 assert.Equal(t, tt.expectedLocations, actual[0].Locations.ToSlice()) 442 require.Len(t, actual[0].CPEs, tt.expectedCPECount) 443 }) 444 } 445 } 446 447 func TestCatalog_EnumerateNilCatalog(t *testing.T) { 448 var c *Collection 449 assert.Empty(t, c.Enumerate()) 450 } 451 452 func Test_idOrderedSet_add(t *testing.T) { 453 tests := []struct { 454 name string 455 input []artifact.ID 456 expected []artifact.ID 457 }{ 458 { 459 name: "elements deduplicated when added", 460 input: []artifact.ID{ 461 "1", "2", "3", "4", "1", "2", "3", "4", "1", "2", "3", "4", 462 }, 463 expected: []artifact.ID{ 464 "1", "2", "3", "4", 465 }, 466 }, 467 { 468 name: "elements retain ordering when added", 469 input: []artifact.ID{ 470 "4", "3", "2", "1", 471 }, 472 expected: []artifact.ID{ 473 "4", "3", "2", "1", 474 }, 475 }, 476 } 477 for _, tt := range tests { 478 t.Run(tt.name, func(t *testing.T) { 479 var s orderedIDSet 480 s.add(tt.input...) 481 assert.Equal(t, tt.expected, s.slice) 482 }) 483 } 484 }