syft/syft/pkg/cataloger/generic/cataloger_test.go
Will Murphy 4393654d03
Chore fix sync bump (#4809)
* chore(deps): update anchore dependencies

Signed-off-by: anchore-oss-update-bot <anchore-oss-update-bot@users.noreply.github.com>

* chore: update test to account for sync wrapping panic

Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>

---------

Signed-off-by: anchore-oss-update-bot <anchore-oss-update-bot@users.noreply.github.com>
Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
Co-authored-by: anchore-oss-update-bot <anchore-oss-update-bot@users.noreply.github.com>
2026-04-22 08:48:30 -04:00

271 lines
7.5 KiB
Go

package generic
import (
"context"
"fmt"
"io"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/go-sync"
"github.com/anchore/syft/internal/unknown"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
)
func Test_Cataloger(t *testing.T) {
allParsedPaths := make(map[string]bool)
parser := func(_ context.Context, resolver file.Resolver, env *Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
allParsedPaths[reader.Path()] = true
contents, err := io.ReadAll(reader)
require.NoError(t, err)
if len(contents) == 0 {
return nil, nil, nil
}
p := pkg.Package{
Name: string(contents),
Locations: file.NewLocationSet(reader.Location),
}
r := artifact.Relationship{
From: p,
To: p,
Type: artifact.ContainsRelationship,
}
return []pkg.Package{p}, []artifact.Relationship{r}, nil
}
upstream := "some-other-cataloger"
expectedSelection := []string{"testdata/last/path.txt", "testdata/another-path.txt", "testdata/a-path.txt", "testdata/empty.txt"}
resolver := file.NewMockResolverForPaths(expectedSelection...)
cataloger := NewCataloger(upstream).
WithParserByPath(parser, "testdata/another-path.txt", "testdata/last/path.txt").
WithParserByGlobs(parser, "**/a-path.txt", "**/empty.txt")
actualPkgs, relationships, err := cataloger.Catalog(context.Background(), resolver)
assert.NoError(t, err)
expectedPkgs := make(map[string]pkg.Package)
for _, path := range expectedSelection {
require.True(t, allParsedPaths[path])
if path == "testdata/empty.txt" {
continue // note: empty.txt won't become a package
}
expectedPkgs[path] = pkg.Package{
FoundBy: upstream,
Name: fmt.Sprintf("%s file contents!", path),
}
}
assert.Len(t, allParsedPaths, len(expectedSelection))
assert.Len(t, actualPkgs, len(expectedPkgs))
assert.Len(t, relationships, len(actualPkgs))
for _, p := range actualPkgs {
ls := p.Locations.ToSlice()
require.NotEmpty(t, ls)
ref := ls[0]
exP, ok := expectedPkgs[ref.RealPath]
if !ok {
t.Errorf("missing expected pkg: ref=%+v", ref)
continue
}
// assigned by the generic cataloger
if p.FoundBy != exP.FoundBy {
t.Errorf("bad upstream: %s", p.FoundBy)
}
// assigned by the parser
if exP.Name != p.Name {
t.Errorf("bad contents mapping: %+v", p.Locations)
}
}
}
type spyReturningFileResolver struct {
m *file.MockResolver
s *spyingIoReadCloser
}
type spyingIoReadCloser struct {
rc io.ReadCloser
closed bool
}
func newSpyReturningFileResolver(s *spyingIoReadCloser, paths ...string) file.Resolver {
m := file.NewMockResolverForPaths(paths...)
return spyReturningFileResolver{
m: m,
s: s,
}
}
func (s *spyingIoReadCloser) Read(p []byte) (n int, err error) {
return s.rc.Read(p)
}
func (s *spyingIoReadCloser) Close() error {
s.closed = true
return s.rc.Close()
}
var _ io.ReadCloser = (*spyingIoReadCloser)(nil)
func (m spyReturningFileResolver) FileContentsByLocation(location file.Location) (io.ReadCloser, error) {
return m.s, nil
}
func (m spyReturningFileResolver) HasPath(path string) bool {
return m.m.HasPath(path)
}
func (m spyReturningFileResolver) FilesByPath(paths ...string) ([]file.Location, error) {
return m.m.FilesByPath(paths...)
}
func (m spyReturningFileResolver) FilesByGlob(patterns ...string) ([]file.Location, error) {
return m.m.FilesByGlob(patterns...)
}
func (m spyReturningFileResolver) FilesByMIMEType(types ...string) ([]file.Location, error) {
return m.m.FilesByMIMEType(types...)
}
func (m spyReturningFileResolver) FilesByMediaType(types ...string) ([]file.Location, error) {
return m.m.FilesByMediaType(types...)
}
func (m spyReturningFileResolver) RelativeFileByPath(f file.Location, path string) *file.Location {
return m.m.RelativeFileByPath(f, path)
}
func (m spyReturningFileResolver) AllLocations(ctx context.Context) <-chan file.Location {
return m.m.AllLocations(ctx)
}
func (m spyReturningFileResolver) FileMetadataByLocation(location file.Location) (file.Metadata, error) {
return m.m.FileMetadataByLocation(location)
}
var _ file.Resolver = (*spyReturningFileResolver)(nil)
func TestClosesFileOnParserPanic(t *testing.T) {
rc := io.NopCloser(strings.NewReader("some string"))
spy := spyingIoReadCloser{
rc: rc,
}
resolver := newSpyReturningFileResolver(&spy, "testdata/another-path.txt")
ctx := context.TODO()
processors := []requester{
func(resolver file.Resolver, env Environment) []request {
return []request{
{
Location: file.Location{
LocationData: file.LocationData{
Coordinates: file.Coordinates{},
AccessPath: "/some/access/path",
},
},
Parser: func(context.Context, file.Resolver, *Environment, file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
panic("panic!")
},
},
}
},
}
c := Cataloger{
requesters: processors,
upstreamCataloger: "unit-test-cataloger",
}
_, _, err := c.Catalog(ctx, resolver)
require.Error(t, err)
var panicErr sync.PanicError
require.ErrorAs(t, err, &panicErr)
assert.Equal(t, "panic!", panicErr.Value)
require.True(t, spy.closed)
}
func Test_CatalogerWithParserByMediaType(t *testing.T) {
allParsedPaths := make(map[string]bool)
parser := func(_ context.Context, resolver file.Resolver, env *Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
allParsedPaths[reader.Path()] = true
contents, err := io.ReadAll(reader)
require.NoError(t, err)
if len(contents) == 0 {
return nil, nil, nil
}
p := pkg.Package{
Name: string(contents),
Locations: file.NewLocationSet(reader.Location),
}
return []pkg.Package{p}, nil, nil
}
upstream := "media-type-cataloger"
// Create locations with test fixtures that exist on disk
loc1 := file.NewLocation("testdata/a-path.txt")
loc2 := file.NewLocation("testdata/another-path.txt")
// Create a mock resolver that maps media types to locations
resolver := file.NewMockResolverForMediaTypes(map[string][]file.Location{
"application/vnd.test.model": {loc1, loc2},
})
cataloger := NewCataloger(upstream).
WithParserByMediaType(parser, "application/vnd.test.model")
actualPkgs, _, err := cataloger.Catalog(context.Background(), resolver)
assert.NoError(t, err)
// Verify both files were parsed
assert.True(t, allParsedPaths["testdata/a-path.txt"], "expected a-path.txt to be parsed")
assert.True(t, allParsedPaths["testdata/another-path.txt"], "expected another-path.txt to be parsed")
// Verify packages were created
assert.Len(t, actualPkgs, 2)
// Verify FoundBy is set correctly
for _, p := range actualPkgs {
assert.Equal(t, upstream, p.FoundBy)
}
}
func Test_genericCatalogerReturnsErrors(t *testing.T) {
genericErrorReturning := NewCataloger("error returning").WithParserByGlobs(func(ctx context.Context, resolver file.Resolver, environment *Environment, locationReader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
return []pkg.Package{
{
Name: "some-package-" + locationReader.Path(),
},
}, nil, unknown.Newf(locationReader, "unable to read")
}, "**/*")
m := file.NewMockResolverForPaths(
"testdata/a-path.txt",
"testdata/empty.txt",
)
got, _, errs := genericErrorReturning.Catalog(context.TODO(), m)
// require packages and errors
require.NotEmpty(t, got)
unknowns, others := unknown.ExtractCoordinateErrors(errs)
require.NotEmpty(t, unknowns)
require.Empty(t, others)
}