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