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  }