github.com/anchore/syft@v1.4.2-0.20240516191711-1bec1fc5d397/syft/pkg/cataloger/internal/pkgtest/test_generic_parser.go (about) 1 package pkgtest 2 3 import ( 4 "context" 5 "io" 6 "os" 7 "strings" 8 "testing" 9 10 "github.com/google/go-cmp/cmp" 11 "github.com/google/go-cmp/cmp/cmpopts" 12 "github.com/stretchr/testify/assert" 13 "github.com/stretchr/testify/require" 14 15 "github.com/anchore/stereoscope/pkg/imagetest" 16 "github.com/anchore/syft/internal/cmptest" 17 "github.com/anchore/syft/internal/relationship" 18 "github.com/anchore/syft/syft/artifact" 19 "github.com/anchore/syft/syft/file" 20 "github.com/anchore/syft/syft/linux" 21 "github.com/anchore/syft/syft/pkg" 22 "github.com/anchore/syft/syft/pkg/cataloger/generic" 23 "github.com/anchore/syft/syft/source" 24 "github.com/anchore/syft/syft/source/directorysource" 25 "github.com/anchore/syft/syft/source/stereoscopesource" 26 ) 27 28 type CatalogTester struct { 29 expectedPkgs []pkg.Package 30 expectedRelationships []artifact.Relationship 31 assertResultExpectations bool 32 expectedPathResponses []string // this is a minimum set, the resolver may return more that just this list 33 expectedContentQueries []string // this is a full set, any other queries are unexpected (and will fail the test) 34 ignoreUnfulfilledPathResponses map[string][]string 35 ignoreAnyUnfulfilledPaths []string 36 env *generic.Environment 37 reader file.LocationReadCloser 38 resolver file.Resolver 39 wantErr require.ErrorAssertionFunc 40 compareOptions []cmp.Option 41 locationComparer cmptest.LocationComparer 42 licenseComparer cmptest.LicenseComparer 43 customAssertions []func(t *testing.T, pkgs []pkg.Package, relationships []artifact.Relationship) 44 } 45 46 func NewCatalogTester() *CatalogTester { 47 return &CatalogTester{ 48 wantErr: require.NoError, 49 locationComparer: cmptest.DefaultLocationComparer, 50 licenseComparer: cmptest.DefaultLicenseComparer, 51 ignoreUnfulfilledPathResponses: map[string][]string{ 52 "FilesByPath": { 53 // most catalogers search for a linux release, which will not be fulfilled in testing 54 "/etc/os-release", 55 "/usr/lib/os-release", 56 "/etc/system-release-cpe", 57 "/etc/redhat-release", 58 "/bin/busybox", 59 }, 60 }, 61 } 62 } 63 64 func (p *CatalogTester) FromDirectory(t *testing.T, path string) *CatalogTester { 65 t.Helper() 66 67 s, err := directorysource.NewFromPath(path) 68 require.NoError(t, err) 69 70 resolver, err := s.FileResolver(source.AllLayersScope) 71 require.NoError(t, err) 72 73 p.resolver = resolver 74 return p 75 } 76 77 func (p *CatalogTester) FromFile(t *testing.T, path string) *CatalogTester { 78 t.Helper() 79 80 fixture, err := os.Open(path) 81 require.NoError(t, err) 82 83 p.reader = file.LocationReadCloser{ 84 Location: file.NewLocation(fixture.Name()), 85 ReadCloser: fixture, 86 } 87 return p 88 } 89 90 func (p *CatalogTester) FromString(location, data string) *CatalogTester { 91 p.reader = file.LocationReadCloser{ 92 Location: file.NewLocation(location), 93 ReadCloser: io.NopCloser(strings.NewReader(data)), 94 } 95 return p 96 } 97 98 func (p *CatalogTester) WithLinuxRelease(r linux.Release) *CatalogTester { 99 if p.env == nil { 100 p.env = &generic.Environment{} 101 } 102 p.env.LinuxRelease = &r 103 return p 104 } 105 106 func (p *CatalogTester) WithEnv(env *generic.Environment) *CatalogTester { 107 p.env = env 108 return p 109 } 110 111 func (p *CatalogTester) WithError() *CatalogTester { 112 p.assertResultExpectations = true 113 p.wantErr = require.Error 114 return p 115 } 116 117 func (p *CatalogTester) WithErrorAssertion(a require.ErrorAssertionFunc) *CatalogTester { 118 p.wantErr = a 119 return p 120 } 121 122 func (p *CatalogTester) WithResolver(r file.Resolver) *CatalogTester { 123 p.resolver = r 124 return p 125 } 126 127 func (p *CatalogTester) WithImageResolver(t *testing.T, fixtureName string) *CatalogTester { 128 t.Helper() 129 img := imagetest.GetFixtureImage(t, "docker-archive", fixtureName) 130 131 s := stereoscopesource.New(img, stereoscopesource.ImageConfig{ 132 Reference: fixtureName, 133 }) 134 135 r, err := s.FileResolver(source.SquashedScope) 136 require.NoError(t, err) 137 p.resolver = r 138 return p 139 } 140 141 func (p *CatalogTester) ExpectsAssertion(a func(t *testing.T, pkgs []pkg.Package, relationships []artifact.Relationship)) *CatalogTester { 142 p.customAssertions = append(p.customAssertions, a) 143 return p 144 } 145 146 func (p *CatalogTester) IgnoreLocationLayer() *CatalogTester { 147 p.locationComparer = func(x, y file.Location) bool { 148 return cmp.Equal(x.Coordinates.RealPath, y.Coordinates.RealPath) && cmp.Equal(x.AccessPath, y.AccessPath) 149 } 150 151 // we need to update the license comparer to use the ignored location layer 152 p.licenseComparer = func(x, y pkg.License) bool { 153 return cmp.Equal(x, y, cmp.Comparer(p.locationComparer), cmp.Comparer( 154 func(x, y file.LocationSet) bool { 155 xs := x.ToSlice() 156 ys := y.ToSlice() 157 if len(xs) != len(ys) { 158 return false 159 } 160 for i, xe := range xs { 161 ye := ys[i] 162 if !p.locationComparer(xe, ye) { 163 return false 164 } 165 } 166 167 return true 168 })) 169 } 170 return p 171 } 172 173 func (p *CatalogTester) IgnorePackageFields(fields ...string) *CatalogTester { 174 p.compareOptions = append(p.compareOptions, cmpopts.IgnoreFields(pkg.Package{}, fields...)) 175 return p 176 } 177 178 func (p *CatalogTester) WithCompareOptions(opts ...cmp.Option) *CatalogTester { 179 p.compareOptions = append(p.compareOptions, opts...) 180 return p 181 } 182 183 func (p *CatalogTester) Expects(pkgs []pkg.Package, relationships []artifact.Relationship) *CatalogTester { 184 p.assertResultExpectations = true 185 p.expectedPkgs = pkgs 186 p.expectedRelationships = relationships 187 return p 188 } 189 190 func (p *CatalogTester) ExpectsResolverPathResponses(locations []string) *CatalogTester { 191 p.expectedPathResponses = locations 192 return p 193 } 194 195 func (p *CatalogTester) ExpectsResolverContentQueries(locations []string) *CatalogTester { 196 p.expectedContentQueries = locations 197 return p 198 } 199 200 func (p *CatalogTester) IgnoreUnfulfilledPathResponses(paths ...string) *CatalogTester { 201 p.ignoreAnyUnfulfilledPaths = append(p.ignoreAnyUnfulfilledPaths, paths...) 202 return p 203 } 204 205 func (p *CatalogTester) TestParser(t *testing.T, parser generic.Parser) { 206 t.Helper() 207 pkgs, relationships, err := parser(context.Background(), p.resolver, p.env, p.reader) 208 p.wantErr(t, err) 209 p.assertPkgs(t, pkgs, relationships) 210 } 211 212 func (p *CatalogTester) TestCataloger(t *testing.T, cataloger pkg.Cataloger) { 213 t.Helper() 214 215 resolver := NewObservingResolver(p.resolver) 216 217 pkgs, relationships, err := cataloger.Catalog(context.Background(), resolver) 218 219 // this is a minimum set, the resolver may return more that just this list 220 for _, path := range p.expectedPathResponses { 221 assert.Truef(t, resolver.ObservedPathResponses(path), "expected path query for %q was not observed", path) 222 } 223 224 // this is a full set, any other queries are unexpected (and will fail the test) 225 if len(p.expectedContentQueries) > 0 { 226 assert.ElementsMatchf(t, p.expectedContentQueries, resolver.AllContentQueries(), "unexpected content queries observed: diff %s", cmp.Diff(p.expectedContentQueries, resolver.AllContentQueries())) 227 } 228 229 if p.assertResultExpectations { 230 p.wantErr(t, err) 231 p.assertPkgs(t, pkgs, relationships) 232 } 233 234 for _, a := range p.customAssertions { 235 a(t, pkgs, relationships) 236 } 237 238 if !p.assertResultExpectations && len(p.customAssertions) == 0 { 239 resolver.PruneUnfulfilledPathResponses(p.ignoreUnfulfilledPathResponses, p.ignoreAnyUnfulfilledPaths...) 240 241 // if we aren't testing the results, we should focus on what was searched for (for glob-centric tests) 242 assert.Falsef(t, resolver.HasUnfulfilledPathRequests(), "unfulfilled path requests: \n%v", resolver.PrettyUnfulfilledPathRequests()) 243 } 244 } 245 246 // nolint:funlen 247 func (p *CatalogTester) assertPkgs(t *testing.T, pkgs []pkg.Package, relationships []artifact.Relationship) { 248 t.Helper() 249 250 p.compareOptions = append(p.compareOptions, cmptest.CommonOptions(p.licenseComparer, p.locationComparer)...) 251 252 { 253 r := cmptest.NewDiffReporter() 254 var opts []cmp.Option 255 256 opts = append(opts, p.compareOptions...) 257 opts = append(opts, cmp.Reporter(&r)) 258 259 // order should not matter 260 pkg.Sort(p.expectedPkgs) 261 pkg.Sort(pkgs) 262 263 if diff := cmp.Diff(p.expectedPkgs, pkgs, opts...); diff != "" { 264 t.Log("Specific Differences:\n" + r.String()) 265 t.Errorf("unexpected packages from parsing (-expected +actual)\n%s", diff) 266 } 267 } 268 { 269 r := cmptest.NewDiffReporter() 270 var opts []cmp.Option 271 272 opts = append(opts, p.compareOptions...) 273 opts = append(opts, cmp.Reporter(&r)) 274 275 // order should not matter 276 relationship.Sort(p.expectedRelationships) 277 relationship.Sort(relationships) 278 279 if diff := cmp.Diff(p.expectedRelationships, relationships, opts...); diff != "" { 280 t.Log("Specific Differences:\n" + r.String()) 281 282 t.Errorf("unexpected relationships from parsing (-expected +actual)\n%s", diff) 283 } 284 } 285 } 286 287 func TestFileParser(t *testing.T, fixturePath string, parser generic.Parser, expectedPkgs []pkg.Package, expectedRelationships []artifact.Relationship) { 288 t.Helper() 289 NewCatalogTester().FromFile(t, fixturePath).Expects(expectedPkgs, expectedRelationships).TestParser(t, parser) 290 } 291 292 func TestFileParserWithEnv(t *testing.T, fixturePath string, parser generic.Parser, env *generic.Environment, expectedPkgs []pkg.Package, expectedRelationships []artifact.Relationship) { 293 t.Helper() 294 295 NewCatalogTester().FromFile(t, fixturePath).WithEnv(env).Expects(expectedPkgs, expectedRelationships).TestParser(t, parser) 296 } 297 298 func AssertPackagesEqual(t *testing.T, a, b pkg.Package) { 299 t.Helper() 300 opts := []cmp.Option{ 301 cmpopts.IgnoreFields(pkg.Package{}, "id"), // note: ID is not deterministic for test purposes 302 cmp.Comparer( 303 func(x, y file.LocationSet) bool { 304 xs := x.ToSlice() 305 ys := y.ToSlice() 306 307 if len(xs) != len(ys) { 308 return false 309 } 310 for i, xe := range xs { 311 ye := ys[i] 312 if !cmptest.DefaultLocationComparer(xe, ye) { 313 return false 314 } 315 } 316 317 return true 318 }, 319 ), 320 cmp.Comparer( 321 func(x, y pkg.LicenseSet) bool { 322 xs := x.ToSlice() 323 ys := y.ToSlice() 324 325 if len(xs) != len(ys) { 326 return false 327 } 328 for i, xe := range xs { 329 ye := ys[i] 330 if !cmptest.DefaultLicenseComparer(xe, ye) { 331 return false 332 } 333 } 334 335 return true 336 }, 337 ), 338 cmp.Comparer( 339 cmptest.DefaultLocationComparer, 340 ), 341 cmp.Comparer( 342 cmptest.DefaultLicenseComparer, 343 ), 344 } 345 346 if diff := cmp.Diff(a, b, opts...); diff != "" { 347 t.Errorf("unexpected packages from parsing (-expected +actual)\n%s", diff) 348 } 349 }