Alex Goodman 3472b48177
Add relationships for python poetry packages (#2906)
* [wip] add initial poetry.lock relationship support

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* provide generic set for basic types

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* dependency resolver should allow for conditional deps

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* add tests for poetry lock relationship additions

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* update schema with python poetry dependency refs

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* dep specification data structure should not be recursive in nature

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2024-06-04 20:00:05 +00:00

246 lines
5.7 KiB
Go

package dependency
import (
"errors"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/pkg"
)
func Test_resolve(t *testing.T) {
a := pkg.Package{
Name: "a",
}
a.SetID()
b := pkg.Package{
Name: "b",
}
b.SetID()
c := pkg.Package{
Name: "c",
}
c.SetID()
subjects := []pkg.Package{a, b, c}
tests := []struct {
name string
s Specifier
want map[string][]string
}{
{
name: "find relationships between packages",
s: newSpecifierBuilder().
WithProvides(a /* provides */, "a-resource").
WithRequires(b /* requires */, "a-resource").
Specifier(),
want: map[string][]string{
"b": /* depends on */ {"a"},
},
},
{
name: "find relationships between packages with variants",
s: newSpecifierBuilder().
WithProvides(a /* provides */, "a-resource").
WithRequires(b /* requires */, "a[variant]").
WithProvides(c /* provides */, "c-resource").
WithVariant(a /* provides */, "variant" /* which requires */, "c-resource").
Specifier(),
want: map[string][]string{
"b":/* depends on */ {"a"},
"a":/* depends on */ {"c"},
},
},
{
name: "deduplicates provider keys",
s: newSpecifierBuilder().
WithProvides(a /* provides */, "a-resource", "a-resource", "a-resource").
WithRequires(b /* requires */, "a-resource", "a-resource", "a-resource").
Specifier(),
want: map[string][]string{
"b": /* depends on */ {"a"},
// note: we're NOT seeing:
// "b": /* depends on */ {"a", "a", "a"},
},
},
{
name: "deduplicates crafted relationships",
s: newSpecifierBuilder().
WithProvides(a /* provides */, "a1-resource", "a2-resource", "a3-resource").
WithRequires(b /* requires */, "a1-resource", "a2-resource").
Specifier(),
want: map[string][]string{
"b": /* depends on */ {"a"},
// note: we're NOT seeing:
// "b": /* depends on */ {"a", "a"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
relationships := Resolve(tt.s, subjects)
if d := cmp.Diff(tt.want, abstractRelationships(t, relationships)); d != "" {
t.Errorf("unexpected relationships (-want +got):\n%s", d)
}
})
}
}
type specifierBuilder struct {
provides map[string][]string
requires map[string][]string
variants map[string]map[string][]string
}
func newSpecifierBuilder() *specifierBuilder {
return &specifierBuilder{
provides: make(map[string][]string),
requires: make(map[string][]string),
variants: make(map[string]map[string][]string),
}
}
func (m *specifierBuilder) WithProvides(p pkg.Package, provides ...string) *specifierBuilder {
m.provides[p.Name] = append(m.provides[p.Name], provides...)
return m
}
func (m *specifierBuilder) WithRequires(p pkg.Package, requires ...string) *specifierBuilder {
m.requires[p.Name] = append(m.requires[p.Name], requires...)
return m
}
func (m *specifierBuilder) WithVariant(p pkg.Package, variantName string, requires ...string) *specifierBuilder {
if _, ok := m.variants[p.Name]; !ok {
m.variants[p.Name] = make(map[string][]string)
}
m.variants[p.Name][variantName] = append(m.variants[p.Name][variantName], requires...)
return m
}
func (m specifierBuilder) Specifier() Specifier {
return func(p pkg.Package) Specification {
var prs []ProvidesRequires
for variantName, requires := range m.variants[p.Name] {
prs = append(prs, ProvidesRequires{
Provides: []string{p.Name + "[" + variantName + "]"},
Requires: requires,
})
}
return Specification{
ProvidesRequires: ProvidesRequires{
Provides: m.provides[p.Name],
Requires: m.requires[p.Name],
},
Variants: prs,
}
}
}
func abstractRelationships(t testing.TB, relationships []artifact.Relationship) map[string][]string {
t.Helper()
abstracted := make(map[string][]string)
for _, relationship := range relationships {
fromPkg, ok := relationship.From.(pkg.Package)
if !ok {
continue
}
toPkg, ok := relationship.To.(pkg.Package)
if !ok {
continue
}
// we build this backwards since we use DependencyOfRelationship instead of DependsOn
abstracted[toPkg.Name] = append(abstracted[toPkg.Name], fromPkg.Name)
}
return abstracted
}
func Test_Processor(t *testing.T) {
a := pkg.Package{
Name: "a",
}
a.SetID()
b := pkg.Package{
Name: "b",
}
b.SetID()
c := pkg.Package{
Name: "c",
}
c.SetID()
tests := []struct {
name string
sp Specifier
pkgs []pkg.Package
rels []artifact.Relationship
err error
wantPkgCount int
wantRelCount int
wantErr assert.ErrorAssertionFunc
}{
{
name: "happy path preserves decorated values",
sp: newSpecifierBuilder().
WithProvides(b, "b-resource").
WithRequires(c, "b-resource").
Specifier(),
pkgs: []pkg.Package{a, b, c},
rels: []artifact.Relationship{
{
From: a,
To: b,
Type: artifact.DependencyOfRelationship,
},
},
wantPkgCount: 3,
wantRelCount: 2, // original + new
},
{
name: "error from cataloger is propagated",
sp: newSpecifierBuilder().
WithProvides(b, "b-resource").
WithRequires(c, "b-resource").
Specifier(),
err: errors.New("surprise!"),
pkgs: []pkg.Package{a, b, c},
rels: []artifact.Relationship{
{
From: a,
To: b,
Type: artifact.DependencyOfRelationship,
},
},
wantPkgCount: 3,
wantRelCount: 2, // original + new
wantErr: assert.Error,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.wantErr == nil {
tt.wantErr = assert.NoError
}
gotPkgs, gotRels, err := Processor(tt.sp)(tt.pkgs, tt.rels, tt.err)
tt.wantErr(t, err)
assert.Len(t, gotPkgs, tt.wantPkgCount)
assert.Len(t, gotRels, tt.wantRelCount)
})
}
}