Create MockResolver and use to improve python cataloger tests

Signed-off-by: Dan Luhring <dan.luhring@anchore.com>
This commit is contained in:
Dan Luhring 2020-12-23 11:23:40 -05:00
parent 13c289eb7e
commit 52e719dcb8
No known key found for this signature in database
GPG key ID: 9CEE23D079426CEF
2 changed files with 147 additions and 160 deletions

View file

@ -1,154 +1,28 @@
package python
import (
"fmt"
"io"
"io/ioutil"
"os"
"strings"
"testing"
"github.com/anchore/syft/internal/file"
"github.com/anchore/syft/syft/source"
"github.com/anchore/syft/syft/pkg"
"github.com/go-test/deep"
)
// TODO: make this generic (based on maps of source.FileData) and make a generic mock to move to the source pkg
type pythonTestResolverMock struct {
metadataReader io.Reader
recordReader io.Reader
topLevelReader io.Reader
metadataRef *source.Location
recordRef *source.Location
topLevelRef *source.Location
contents map[source.Location]string
}
func newTestResolver(metaPath, recordPath, topPath string) *pythonTestResolverMock {
metadataReader, err := os.Open(metaPath)
if err != nil {
panic(fmt.Errorf("failed to open metadata: %+v", err))
}
var recordReader io.Reader
if recordPath != "" {
recordReader, err = os.Open(recordPath)
if err != nil {
panic(fmt.Errorf("failed to open record: %+v", err))
}
}
var topLevelReader io.Reader
if topPath != "" {
topLevelReader, err = os.Open(topPath)
if err != nil {
panic(fmt.Errorf("failed to open top level: %+v", err))
}
}
var recordRef *source.Location
if recordReader != nil {
ref := source.NewLocation("test-fixtures/dist-info/RECORD")
recordRef = &ref
}
var topLevelRef *source.Location
if topLevelReader != nil {
ref := source.NewLocation("test-fixtures/dist-info/top_level.txt")
topLevelRef = &ref
}
metadataRef := source.NewLocation("test-fixtures/dist-info/METADATA")
return &pythonTestResolverMock{
recordReader: recordReader,
metadataReader: metadataReader,
topLevelReader: topLevelReader,
metadataRef: &metadataRef,
recordRef: recordRef,
topLevelRef: topLevelRef,
contents: make(map[source.Location]string),
}
}
func (r *pythonTestResolverMock) FileContentsByLocation(location source.Location) (string, error) {
switch {
case r.topLevelRef != nil && location.Path == r.topLevelRef.Path:
b, err := ioutil.ReadAll(r.topLevelReader)
if err != nil {
return "", err
}
return string(b), nil
case location.Path == r.metadataRef.Path:
b, err := ioutil.ReadAll(r.metadataReader)
if err != nil {
return "", err
}
return string(b), nil
case location.Path == r.recordRef.Path:
b, err := ioutil.ReadAll(r.recordReader)
if err != nil {
return "", err
}
return string(b), nil
}
return "", fmt.Errorf("invalid value given")
}
func (r *pythonTestResolverMock) MultipleFileContentsByLocation(locations []source.Location) (map[source.Location]string, error) {
var results = make(map[source.Location]string)
var err error
for _, l := range locations {
results[l], err = r.FileContentsByLocation(l)
if err != nil {
return nil, err
}
}
return results, nil
}
func (r *pythonTestResolverMock) FilesByPath(_ ...string) ([]source.Location, error) {
return nil, fmt.Errorf("not implemented")
}
func (r *pythonTestResolverMock) FilesByGlob(patterns ...string) ([]source.Location, error) {
var results []source.Location
for _, pattern := range patterns {
for _, l := range []*source.Location{r.topLevelRef, r.metadataRef, r.recordRef} {
if l == nil {
continue
}
if file.GlobMatch(pattern, l.Path) {
results = append(results, *l)
}
}
}
return results, nil
}
func (r *pythonTestResolverMock) RelativeFileByPath(_ source.Location, path string) *source.Location {
switch {
case strings.Contains(path, "RECORD"):
return r.recordRef
case strings.Contains(path, "top_level.txt"):
return r.topLevelRef
default:
panic(fmt.Errorf("invalid RelativeFileByPath value given: %q", path))
}
}
func TestPythonPackageWheelCataloger(t *testing.T) {
tests := []struct {
MetadataFixture string
RecordFixture string
TopLevelFixture string
ExpectedPackage pkg.Package
name string
fixtures []string
expectedPackage pkg.Package
}{
{
MetadataFixture: "test-fixtures/egg-info/PKG-INFO",
RecordFixture: "test-fixtures/egg-info/RECORD",
TopLevelFixture: "test-fixtures/egg-info/top_level.txt",
ExpectedPackage: pkg.Package{
name: "egg-info directory",
fixtures: []string{
"test-fixtures/egg-info/PKG-INFO",
"test-fixtures/egg-info/RECORD",
"test-fixtures/egg-info/top_level.txt",
},
expectedPackage: pkg.Package{
Name: "requests",
Version: "2.22.0",
Type: pkg.PythonPkg,
@ -177,10 +51,13 @@ func TestPythonPackageWheelCataloger(t *testing.T) {
},
},
{
MetadataFixture: "test-fixtures/dist-info/METADATA",
RecordFixture: "test-fixtures/dist-info/RECORD",
TopLevelFixture: "test-fixtures/dist-info/top_level.txt",
ExpectedPackage: pkg.Package{
name: "dist-info directory",
fixtures: []string{
"test-fixtures/dist-info/METADATA",
"test-fixtures/dist-info/RECORD",
"test-fixtures/dist-info/top_level.txt",
},
expectedPackage: pkg.Package{
Name: "Pygments",
Version: "2.6.1",
Type: pkg.PythonPkg,
@ -210,8 +87,9 @@ func TestPythonPackageWheelCataloger(t *testing.T) {
{
// in cases where the metadata file is available and the record is not we should still record there is a package
// additionally empty top_level.txt files should not result in an error
MetadataFixture: "test-fixtures/partial.dist-info/METADATA",
ExpectedPackage: pkg.Package{
name: "partial dist-info directory",
fixtures: []string{"test-fixtures/partial.dist-info/METADATA"},
expectedPackage: pkg.Package{
Name: "Pygments",
Version: "2.6.1",
Type: pkg.PythonPkg,
@ -231,8 +109,9 @@ func TestPythonPackageWheelCataloger(t *testing.T) {
},
},
{
MetadataFixture: "test-fixtures/test.egg-info",
ExpectedPackage: pkg.Package{
name: "egg-info regular file",
fixtures: []string{"test-fixtures/test.egg-info"},
expectedPackage: pkg.Package{
Name: "requests",
Version: "2.22.0",
Type: pkg.PythonPkg,
@ -254,19 +133,15 @@ func TestPythonPackageWheelCataloger(t *testing.T) {
}
for _, test := range tests {
t.Run(test.MetadataFixture, func(t *testing.T) {
resolver := newTestResolver(test.MetadataFixture, test.RecordFixture, test.TopLevelFixture)
t.Run(test.name, func(t *testing.T) {
resolver := source.NewMockResolverForPaths(test.fixtures...)
// note that the source is the record ref created by the resolver mock... attach the expected values
test.ExpectedPackage.Locations = []source.Location{*resolver.metadataRef}
if resolver.recordRef != nil {
test.ExpectedPackage.Locations = append(test.ExpectedPackage.Locations, *resolver.recordRef)
locations, err := resolver.FilesByPath(test.fixtures...)
if err != nil {
t.Fatal(err)
}
if resolver.topLevelRef != nil {
test.ExpectedPackage.Locations = append(test.ExpectedPackage.Locations, *resolver.topLevelRef)
}
// end patching expected values with runtime data...
test.expectedPackage.Locations = locations
actual, err := NewPythonPackageCataloger().Catalog(resolver)
if err != nil {
@ -274,22 +149,20 @@ func TestPythonPackageWheelCataloger(t *testing.T) {
}
if len(actual) != 1 {
t.Fatalf("unexpected length: %d", len(actual))
t.Fatalf("unexpected number of packages: %d", len(actual))
}
for _, d := range deep.Equal(actual[0], test.ExpectedPackage) {
for _, d := range deep.Equal(actual[0], test.expectedPackage) {
t.Errorf("diff: %+v", d)
}
})
}
}
func TestIgnorePackage(t *testing.T) {
tests := []struct {
MetadataFixture string
}{
{
MetadataFixture: "test-fixtures/Python-2.7.egg-info",
},
@ -297,7 +170,7 @@ func TestIgnorePackage(t *testing.T) {
for _, test := range tests {
t.Run(test.MetadataFixture, func(t *testing.T) {
resolver := newTestResolver(test.MetadataFixture, "", "")
resolver := source.NewMockResolverForPaths(test.MetadataFixture)
actual, err := NewPythonPackageCataloger().Catalog(resolver)
if err != nil {
@ -309,5 +182,4 @@ func TestIgnorePackage(t *testing.T) {
}
})
}
}

View file

@ -0,0 +1,115 @@
package source
import (
"fmt"
"github.com/anchore/syft/internal/file"
"io/ioutil"
"os"
)
var _ Resolver = (*MockResolver)(nil)
// MockResolver implements the Resolver interface and is intended for use *only in test code*.
// It provides an implementation that can resolve local filesystem paths using only a provided discrete list of file
// paths, which are typically paths to test fixtures.
type MockResolver struct {
Locations []Location
}
// NewMockResolverForPaths creates a new MockResolver, where the only resolvable
// files are those specified by the supplied paths.
func NewMockResolverForPaths(paths ...string) *MockResolver {
var locations []Location
for _, p := range paths {
locations = append(locations, NewLocation(p))
}
return &MockResolver{Locations: locations}
}
// String returns the string representation of the MockResolver.
func (r MockResolver) String() string {
return fmt.Sprintf("mock:(%s,...)", r.Locations[0].Path)
}
// FileContentsByLocation fetches file contents for a single location. If the
// path does not exist, an error is returned.
func (r MockResolver) FileContentsByLocation(location Location) (string, error) {
for _, l := range r.Locations {
if l == location {
return stringContent(location.Path)
}
}
return "", fmt.Errorf("no file for location: %v", location)
}
// MultipleFileContentsByLocation returns the file contents for all specified Locations.
func (r MockResolver) MultipleFileContentsByLocation(locations []Location) (map[Location]string, error) {
results := make(map[Location]string)
for _, l := range locations {
contents, err := r.FileContentsByLocation(l)
if err != nil {
return nil, err
}
results[l] = contents
}
return results, nil
}
// FilesByPath returns all Locations that match the given paths.
func (r MockResolver) FilesByPath(paths ...string) ([]Location, error) {
var results []Location
for _, p := range paths {
for _, location := range r.Locations {
if p == location.Path {
results = append(results, NewLocation(p))
}
}
}
return results, nil
}
// FilesByGlob returns all Locations that match the given path glob pattern.
func (r MockResolver) FilesByGlob(patterns ...string) ([]Location, error) {
var results []Location
for _, pattern := range patterns {
for _, location := range r.Locations {
if file.GlobMatch(pattern, location.Path) {
results = append(results, location)
}
}
}
return results, nil
}
// RelativeFileByPath returns a single Location for the given path.
func (r MockResolver) RelativeFileByPath(_ Location, path string) *Location {
paths, err := r.FilesByPath(path)
if err != nil {
return nil
}
if len(paths) < 1 {
return nil
}
return &paths[0]
}
func stringContent(path string) (string, error) {
reader, err := os.Open(path)
if err != nil {
return "", err
}
b, err := ioutil.ReadAll(reader)
if err != nil {
return "", err
}
return string(b), nil
}