github.com/anchore/syft@v1.38.2/syft/pkg/cataloger/generic/cataloger_test.go (about)

     1  package generic
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"strings"
     8  	"testing"
     9  
    10  	"github.com/stretchr/testify/assert"
    11  	"github.com/stretchr/testify/require"
    12  
    13  	"github.com/anchore/syft/internal/unknown"
    14  	"github.com/anchore/syft/syft/artifact"
    15  	"github.com/anchore/syft/syft/file"
    16  	"github.com/anchore/syft/syft/pkg"
    17  )
    18  
    19  func Test_Cataloger(t *testing.T) {
    20  	allParsedPaths := make(map[string]bool)
    21  	parser := func(_ context.Context, resolver file.Resolver, env *Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
    22  		allParsedPaths[reader.Path()] = true
    23  		contents, err := io.ReadAll(reader)
    24  		require.NoError(t, err)
    25  
    26  		if len(contents) == 0 {
    27  			return nil, nil, nil
    28  		}
    29  
    30  		p := pkg.Package{
    31  			Name:      string(contents),
    32  			Locations: file.NewLocationSet(reader.Location),
    33  		}
    34  		r := artifact.Relationship{
    35  			From: p,
    36  			To:   p,
    37  			Type: artifact.ContainsRelationship,
    38  		}
    39  
    40  		return []pkg.Package{p}, []artifact.Relationship{r}, nil
    41  	}
    42  
    43  	upstream := "some-other-cataloger"
    44  
    45  	expectedSelection := []string{"test-fixtures/last/path.txt", "test-fixtures/another-path.txt", "test-fixtures/a-path.txt", "test-fixtures/empty.txt"}
    46  	resolver := file.NewMockResolverForPaths(expectedSelection...)
    47  	cataloger := NewCataloger(upstream).
    48  		WithParserByPath(parser, "test-fixtures/another-path.txt", "test-fixtures/last/path.txt").
    49  		WithParserByGlobs(parser, "**/a-path.txt", "**/empty.txt")
    50  
    51  	actualPkgs, relationships, err := cataloger.Catalog(context.Background(), resolver)
    52  	assert.NoError(t, err)
    53  
    54  	expectedPkgs := make(map[string]pkg.Package)
    55  	for _, path := range expectedSelection {
    56  		require.True(t, allParsedPaths[path])
    57  		if path == "test-fixtures/empty.txt" {
    58  			continue // note: empty.txt won't become a package
    59  		}
    60  		expectedPkgs[path] = pkg.Package{
    61  			FoundBy: upstream,
    62  			Name:    fmt.Sprintf("%s file contents!", path),
    63  		}
    64  	}
    65  
    66  	assert.Len(t, allParsedPaths, len(expectedSelection))
    67  	assert.Len(t, actualPkgs, len(expectedPkgs))
    68  	assert.Len(t, relationships, len(actualPkgs))
    69  
    70  	for _, p := range actualPkgs {
    71  		ls := p.Locations.ToSlice()
    72  		require.NotEmpty(t, ls)
    73  		ref := ls[0]
    74  		exP, ok := expectedPkgs[ref.RealPath]
    75  		if !ok {
    76  			t.Errorf("missing expected pkg: ref=%+v", ref)
    77  			continue
    78  		}
    79  
    80  		// assigned by the generic cataloger
    81  		if p.FoundBy != exP.FoundBy {
    82  			t.Errorf("bad upstream: %s", p.FoundBy)
    83  		}
    84  
    85  		// assigned by the parser
    86  		if exP.Name != p.Name {
    87  			t.Errorf("bad contents mapping: %+v", p.Locations)
    88  		}
    89  	}
    90  }
    91  
    92  type spyReturningFileResolver struct {
    93  	m *file.MockResolver
    94  	s *spyingIoReadCloser
    95  }
    96  
    97  type spyingIoReadCloser struct {
    98  	rc     io.ReadCloser
    99  	closed bool
   100  }
   101  
   102  func newSpyReturningFileResolver(s *spyingIoReadCloser, paths ...string) file.Resolver {
   103  	m := file.NewMockResolverForPaths(paths...)
   104  	return spyReturningFileResolver{
   105  		m: m,
   106  		s: s,
   107  	}
   108  }
   109  
   110  func (s *spyingIoReadCloser) Read(p []byte) (n int, err error) {
   111  	return s.rc.Read(p)
   112  }
   113  
   114  func (s *spyingIoReadCloser) Close() error {
   115  	s.closed = true
   116  	return s.rc.Close()
   117  }
   118  
   119  var _ io.ReadCloser = (*spyingIoReadCloser)(nil)
   120  
   121  func (m spyReturningFileResolver) FileContentsByLocation(location file.Location) (io.ReadCloser, error) {
   122  	return m.s, nil
   123  }
   124  
   125  func (m spyReturningFileResolver) HasPath(path string) bool {
   126  	return m.m.HasPath(path)
   127  }
   128  
   129  func (m spyReturningFileResolver) FilesByPath(paths ...string) ([]file.Location, error) {
   130  	return m.m.FilesByPath(paths...)
   131  }
   132  
   133  func (m spyReturningFileResolver) FilesByGlob(patterns ...string) ([]file.Location, error) {
   134  	return m.m.FilesByGlob(patterns...)
   135  }
   136  
   137  func (m spyReturningFileResolver) FilesByMIMEType(types ...string) ([]file.Location, error) {
   138  	return m.m.FilesByMIMEType(types...)
   139  }
   140  
   141  func (m spyReturningFileResolver) RelativeFileByPath(f file.Location, path string) *file.Location {
   142  	return m.m.RelativeFileByPath(f, path)
   143  }
   144  
   145  func (m spyReturningFileResolver) AllLocations(ctx context.Context) <-chan file.Location {
   146  	return m.m.AllLocations(ctx)
   147  }
   148  
   149  func (m spyReturningFileResolver) FileMetadataByLocation(location file.Location) (file.Metadata, error) {
   150  	return m.m.FileMetadataByLocation(location)
   151  }
   152  
   153  var _ file.Resolver = (*spyReturningFileResolver)(nil)
   154  
   155  func TestClosesFileOnParserPanic(t *testing.T) {
   156  	rc := io.NopCloser(strings.NewReader("some string"))
   157  	spy := spyingIoReadCloser{
   158  		rc: rc,
   159  	}
   160  	resolver := newSpyReturningFileResolver(&spy, "test-fixtures/another-path.txt")
   161  	ctx := context.TODO()
   162  
   163  	processors := []requester{
   164  		func(resolver file.Resolver, env Environment) []request {
   165  			return []request{
   166  				{
   167  					Location: file.Location{
   168  						LocationData: file.LocationData{
   169  							Coordinates: file.Coordinates{},
   170  							AccessPath:  "/some/access/path",
   171  						},
   172  					},
   173  					Parser: func(context.Context, file.Resolver, *Environment, file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
   174  						panic("panic!")
   175  					},
   176  				},
   177  			}
   178  		},
   179  	}
   180  
   181  	c := Cataloger{
   182  		requesters:        processors,
   183  		upstreamCataloger: "unit-test-cataloger",
   184  	}
   185  
   186  	assert.PanicsWithValue(t, "panic!", func() {
   187  		_, _, _ = c.Catalog(ctx, resolver)
   188  	})
   189  	require.True(t, spy.closed)
   190  }
   191  
   192  func Test_genericCatalogerReturnsErrors(t *testing.T) {
   193  	genericErrorReturning := NewCataloger("error returning").WithParserByGlobs(func(ctx context.Context, resolver file.Resolver, environment *Environment, locationReader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
   194  		return []pkg.Package{
   195  			{
   196  				Name: "some-package-" + locationReader.Path(),
   197  			},
   198  		}, nil, unknown.Newf(locationReader, "unable to read")
   199  	}, "**/*")
   200  
   201  	m := file.NewMockResolverForPaths(
   202  		"test-fixtures/a-path.txt",
   203  		"test-fixtures/empty.txt",
   204  	)
   205  
   206  	got, _, errs := genericErrorReturning.Catalog(context.TODO(), m)
   207  
   208  	// require packages and errors
   209  	require.NotEmpty(t, got)
   210  
   211  	unknowns, others := unknown.ExtractCoordinateErrors(errs)
   212  	require.NotEmpty(t, unknowns)
   213  	require.Empty(t, others)
   214  }