mirror of
https://github.com/anchore/syft.git
synced 2025-11-17 08:23:15 +01:00
456 lines
12 KiB
Go
456 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"go/ast"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// expected config structs that should be discovered with app-config annotations
|
|
var expectedCatalogConfigs = []string{
|
|
"golang.CatalogerConfig",
|
|
"golang.MainModuleVersionConfig",
|
|
"java.ArchiveCatalogerConfig",
|
|
"python.CatalogerConfig",
|
|
"dotnet.CatalogerConfig",
|
|
"kernel.LinuxKernelCatalogerConfig",
|
|
"javascript.CatalogerConfig",
|
|
"nix.Config",
|
|
}
|
|
|
|
func TestDiscoverConfigs(t *testing.T) {
|
|
repoRoot, err := RepoRoot()
|
|
require.NoError(t, err)
|
|
|
|
configs, err := DiscoverConfigs(repoRoot)
|
|
require.NoError(t, err)
|
|
|
|
// verify we discovered multiple config structs
|
|
require.NotEmpty(t, configs, "should discover at least one config struct")
|
|
|
|
// check for known config structs that have app-config annotations
|
|
for _, expected := range expectedCatalogConfigs {
|
|
config, ok := configs[expected]
|
|
require.True(t, ok, "should discover config: %s", expected)
|
|
require.NotEmpty(t, config.Fields, "config %s should have fields", expected)
|
|
require.Equal(t, expected, config.PackageName+"."+config.StructName)
|
|
}
|
|
|
|
// verify golang.CatalogerConfig structure
|
|
golangConfig := configs["golang.CatalogerConfig"]
|
|
wantGolangConfig := ConfigInfo{
|
|
PackageName: "golang",
|
|
StructName: "CatalogerConfig",
|
|
}
|
|
if diff := cmp.Diff(wantGolangConfig.PackageName, golangConfig.PackageName); diff != "" {
|
|
t.Errorf("golang.CatalogerConfig.PackageName mismatch (-want +got):\n%s", diff)
|
|
}
|
|
if diff := cmp.Diff(wantGolangConfig.StructName, golangConfig.StructName); diff != "" {
|
|
t.Errorf("golang.CatalogerConfig.StructName mismatch (-want +got):\n%s", diff)
|
|
}
|
|
require.NotEmpty(t, golangConfig.Fields)
|
|
|
|
// check for specific field
|
|
var foundSearchLocalModCache bool
|
|
for _, field := range golangConfig.Fields {
|
|
if field.Name == "SearchLocalModCacheLicenses" {
|
|
foundSearchLocalModCache = true
|
|
wantField := ConfigField{
|
|
Name: "SearchLocalModCacheLicenses",
|
|
Type: "bool",
|
|
AppKey: "golang.search-local-mod-cache-licenses",
|
|
}
|
|
if diff := cmp.Diff(wantField.Name, field.Name); diff != "" {
|
|
t.Errorf("SearchLocalModCacheLicenses field Name mismatch (-want +got):\n%s", diff)
|
|
}
|
|
if diff := cmp.Diff(wantField.Type, field.Type); diff != "" {
|
|
t.Errorf("SearchLocalModCacheLicenses field Type mismatch (-want +got):\n%s", diff)
|
|
}
|
|
if diff := cmp.Diff(wantField.AppKey, field.AppKey); diff != "" {
|
|
t.Errorf("SearchLocalModCacheLicenses field AppKey mismatch (-want +got):\n%s", diff)
|
|
}
|
|
require.NotEmpty(t, field.Description)
|
|
require.Contains(t, field.Description, "searching for go package licenses")
|
|
}
|
|
}
|
|
require.True(t, foundSearchLocalModCache, "should find SearchLocalModCacheLicenses field")
|
|
|
|
// verify nested config struct
|
|
golangMainModuleConfig := configs["golang.MainModuleVersionConfig"]
|
|
wantMainModuleConfig := ConfigInfo{
|
|
PackageName: "golang",
|
|
StructName: "MainModuleVersionConfig",
|
|
}
|
|
if diff := cmp.Diff(wantMainModuleConfig.PackageName, golangMainModuleConfig.PackageName); diff != "" {
|
|
t.Errorf("golang.MainModuleVersionConfig.PackageName mismatch (-want +got):\n%s", diff)
|
|
}
|
|
if diff := cmp.Diff(wantMainModuleConfig.StructName, golangMainModuleConfig.StructName); diff != "" {
|
|
t.Errorf("golang.MainModuleVersionConfig.StructName mismatch (-want +got):\n%s", diff)
|
|
}
|
|
require.NotEmpty(t, golangMainModuleConfig.Fields)
|
|
|
|
// check for specific nested field
|
|
var foundFromLDFlags bool
|
|
for _, field := range golangMainModuleConfig.Fields {
|
|
if field.Name == "FromLDFlags" {
|
|
foundFromLDFlags = true
|
|
wantField := ConfigField{
|
|
Name: "FromLDFlags",
|
|
Type: "bool",
|
|
AppKey: "golang.main-module-version.from-ld-flags",
|
|
}
|
|
if diff := cmp.Diff(wantField.Name, field.Name); diff != "" {
|
|
t.Errorf("FromLDFlags field Name mismatch (-want +got):\n%s", diff)
|
|
}
|
|
if diff := cmp.Diff(wantField.Type, field.Type); diff != "" {
|
|
t.Errorf("FromLDFlags field Type mismatch (-want +got):\n%s", diff)
|
|
}
|
|
if diff := cmp.Diff(wantField.AppKey, field.AppKey); diff != "" {
|
|
t.Errorf("FromLDFlags field AppKey mismatch (-want +got):\n%s", diff)
|
|
}
|
|
require.NotEmpty(t, field.Description)
|
|
}
|
|
}
|
|
require.True(t, foundFromLDFlags, "should find FromLDFlags field in MainModuleVersionConfig")
|
|
|
|
// print summary for manual inspection
|
|
t.Logf("Discovered %d config structs:", len(configs))
|
|
for key, config := range configs {
|
|
t.Logf(" %s: %d fields", key, len(config.Fields))
|
|
for _, field := range config.Fields {
|
|
t.Logf(" - %s (%s): %s", field.Name, field.Type, field.AppKey)
|
|
if diff := cmp.Diff("", field.Description); diff == "" {
|
|
t.Logf(" WARNING: field %s has no description", field.Name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestExtractPackageNameFromPath(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
filePath string
|
|
want string
|
|
}{
|
|
{
|
|
name: "golang package",
|
|
filePath: "syft/pkg/cataloger/golang/config.go",
|
|
want: "golang",
|
|
},
|
|
{
|
|
name: "java package",
|
|
filePath: "syft/pkg/cataloger/java/config.go",
|
|
want: "java",
|
|
},
|
|
{
|
|
name: "python cataloger",
|
|
filePath: "syft/pkg/cataloger/python/cataloger.go",
|
|
want: "python",
|
|
},
|
|
{
|
|
name: "kernel cataloger",
|
|
filePath: "syft/pkg/cataloger/kernel/cataloger.go",
|
|
want: "kernel",
|
|
},
|
|
{
|
|
name: "binary classifier",
|
|
filePath: "syft/pkg/cataloger/binary/classifier_cataloger.go",
|
|
want: "binary",
|
|
},
|
|
{
|
|
name: "not a cataloger path",
|
|
filePath: "syft/pkg/other/file.go",
|
|
want: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := extractPackageNameFromPath(tt.filePath)
|
|
require.Equal(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFormatFieldType(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
expr ast.Expr
|
|
want string
|
|
}{
|
|
{
|
|
name: "basic identifier - string",
|
|
expr: &ast.Ident{Name: "string"},
|
|
want: "string",
|
|
},
|
|
{
|
|
name: "basic identifier - bool",
|
|
expr: &ast.Ident{Name: "bool"},
|
|
want: "bool",
|
|
},
|
|
{
|
|
name: "basic identifier - int",
|
|
expr: &ast.Ident{Name: "int"},
|
|
want: "int",
|
|
},
|
|
{
|
|
name: "selector expression - package.Type",
|
|
expr: &ast.SelectorExpr{
|
|
X: &ast.Ident{Name: "time"},
|
|
Sel: &ast.Ident{Name: "Time"},
|
|
},
|
|
want: "time.Time",
|
|
},
|
|
{
|
|
name: "selector expression - cataloging.Config",
|
|
expr: &ast.SelectorExpr{
|
|
X: &ast.Ident{Name: "cataloging"},
|
|
Sel: &ast.Ident{Name: "ArchiveSearchConfig"},
|
|
},
|
|
want: "cataloging.ArchiveSearchConfig",
|
|
},
|
|
{
|
|
name: "array of strings",
|
|
expr: &ast.ArrayType{
|
|
Elt: &ast.Ident{Name: "string"},
|
|
},
|
|
want: "[]string",
|
|
},
|
|
{
|
|
name: "array of ints",
|
|
expr: &ast.ArrayType{
|
|
Elt: &ast.Ident{Name: "int"},
|
|
},
|
|
want: "[]int",
|
|
},
|
|
{
|
|
name: "map[string]bool",
|
|
expr: &ast.MapType{
|
|
Key: &ast.Ident{Name: "string"},
|
|
Value: &ast.Ident{Name: "bool"},
|
|
},
|
|
want: "map[string]bool",
|
|
},
|
|
{
|
|
name: "map[string]int",
|
|
expr: &ast.MapType{
|
|
Key: &ast.Ident{Name: "string"},
|
|
Value: &ast.Ident{Name: "int"},
|
|
},
|
|
want: "map[string]int",
|
|
},
|
|
{
|
|
name: "pointer to type",
|
|
expr: &ast.StarExpr{
|
|
X: &ast.Ident{Name: "Config"},
|
|
},
|
|
want: "*Config",
|
|
},
|
|
{
|
|
name: "pointer to selector",
|
|
expr: &ast.StarExpr{
|
|
X: &ast.SelectorExpr{
|
|
X: &ast.Ident{Name: "time"},
|
|
Sel: &ast.Ident{Name: "Time"},
|
|
},
|
|
},
|
|
want: "*time.Time",
|
|
},
|
|
{
|
|
name: "interface{}",
|
|
expr: &ast.InterfaceType{
|
|
Methods: &ast.FieldList{},
|
|
},
|
|
want: "interface{}",
|
|
},
|
|
{
|
|
name: "nested array of arrays",
|
|
expr: &ast.ArrayType{
|
|
Elt: &ast.ArrayType{
|
|
Elt: &ast.Ident{Name: "string"},
|
|
},
|
|
},
|
|
want: "[][]string",
|
|
},
|
|
{
|
|
name: "map with array value",
|
|
expr: &ast.MapType{
|
|
Key: &ast.Ident{Name: "string"},
|
|
Value: &ast.ArrayType{
|
|
Elt: &ast.Ident{Name: "int"},
|
|
},
|
|
},
|
|
want: "map[string][]int",
|
|
},
|
|
{
|
|
name: "pointer to array",
|
|
expr: &ast.StarExpr{
|
|
X: &ast.ArrayType{
|
|
Elt: &ast.Ident{Name: "string"},
|
|
},
|
|
},
|
|
want: "*[]string",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := formatFieldType(tt.expr)
|
|
require.Equal(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExtractFieldComments(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
commentGroup *ast.CommentGroup
|
|
wantDescription string
|
|
wantAppKey string
|
|
}{
|
|
{
|
|
name: "nil comment group",
|
|
commentGroup: nil,
|
|
wantDescription: "",
|
|
wantAppKey: "",
|
|
},
|
|
{
|
|
name: "empty comment group",
|
|
commentGroup: &ast.CommentGroup{
|
|
List: []*ast.Comment{},
|
|
},
|
|
wantDescription: "",
|
|
wantAppKey: "",
|
|
},
|
|
{
|
|
name: "app-config annotation only",
|
|
commentGroup: &ast.CommentGroup{
|
|
List: []*ast.Comment{
|
|
{Text: "// app-config: golang.search-local-mod-cache-licenses"},
|
|
},
|
|
},
|
|
wantDescription: "",
|
|
wantAppKey: "golang.search-local-mod-cache-licenses",
|
|
},
|
|
{
|
|
name: "description only",
|
|
commentGroup: &ast.CommentGroup{
|
|
List: []*ast.Comment{
|
|
{Text: "// enable searching for go package licenses in the local mod cache"},
|
|
},
|
|
},
|
|
wantDescription: "enable searching for go package licenses in the local mod cache",
|
|
wantAppKey: "",
|
|
},
|
|
{
|
|
name: "description and app-config",
|
|
commentGroup: &ast.CommentGroup{
|
|
List: []*ast.Comment{
|
|
{Text: "// enable searching for go package licenses in the local mod cache"},
|
|
{Text: "// app-config: golang.search-local-mod-cache-licenses"},
|
|
},
|
|
},
|
|
wantDescription: "enable searching for go package licenses in the local mod cache",
|
|
wantAppKey: "golang.search-local-mod-cache-licenses",
|
|
},
|
|
{
|
|
name: "app-config before description",
|
|
commentGroup: &ast.CommentGroup{
|
|
List: []*ast.Comment{
|
|
{Text: "// app-config: golang.search-local-mod-cache-licenses"},
|
|
{Text: "// enable searching for go package licenses in the local mod cache"},
|
|
},
|
|
},
|
|
wantDescription: "enable searching for go package licenses in the local mod cache",
|
|
wantAppKey: "golang.search-local-mod-cache-licenses",
|
|
},
|
|
{
|
|
name: "multi-line description",
|
|
commentGroup: &ast.CommentGroup{
|
|
List: []*ast.Comment{
|
|
{Text: "// this is the first line of the description."},
|
|
{Text: "// this is the second line of the description."},
|
|
{Text: "// app-config: test.multi-line"},
|
|
},
|
|
},
|
|
wantDescription: "this is the first line of the description. this is the second line of the description.",
|
|
wantAppKey: "test.multi-line",
|
|
},
|
|
{
|
|
name: "app-config with extra whitespace",
|
|
commentGroup: &ast.CommentGroup{
|
|
List: []*ast.Comment{
|
|
{Text: "// app-config: golang.test-key "},
|
|
},
|
|
},
|
|
wantDescription: "",
|
|
wantAppKey: "golang.test-key",
|
|
},
|
|
{
|
|
name: "description with special characters",
|
|
commentGroup: &ast.CommentGroup{
|
|
List: []*ast.Comment{
|
|
{Text: "// enable searching for Go's package licenses (*.mod files)"},
|
|
{Text: "// app-config: golang.search"},
|
|
},
|
|
},
|
|
wantDescription: "enable searching for Go's package licenses (*.mod files)",
|
|
wantAppKey: "golang.search",
|
|
},
|
|
{
|
|
name: "comment with empty lines",
|
|
commentGroup: &ast.CommentGroup{
|
|
List: []*ast.Comment{
|
|
{Text: "// first line"},
|
|
{Text: "//"},
|
|
{Text: "// second line"},
|
|
{Text: "// app-config: test.key"},
|
|
},
|
|
},
|
|
wantDescription: "first line second line",
|
|
wantAppKey: "test.key",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
gotDescription, gotAppKey := extractFieldComments(tt.commentGroup)
|
|
require.Equal(t, tt.wantDescription, gotDescription)
|
|
require.Equal(t, tt.wantAppKey, gotAppKey)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDiscoverAllowedConfigStructs(t *testing.T) {
|
|
repoRoot, err := RepoRoot()
|
|
require.NoError(t, err)
|
|
|
|
allowedConfigs, err := DiscoverAllowedConfigStructs(repoRoot)
|
|
require.NoError(t, err)
|
|
|
|
// verify we found multiple config types
|
|
require.NotEmpty(t, allowedConfigs, "should discover at least one allowed config type")
|
|
|
|
// verify specific config types that should be in pkgcataloging.Config
|
|
expectedConfigs := []string{
|
|
"golang.CatalogerConfig",
|
|
"java.ArchiveCatalogerConfig",
|
|
"python.CatalogerConfig",
|
|
"dotnet.CatalogerConfig",
|
|
"kernel.LinuxKernelCatalogerConfig",
|
|
"javascript.CatalogerConfig",
|
|
}
|
|
|
|
for _, expected := range expectedConfigs {
|
|
require.True(t, allowedConfigs[expected], "should find %s in allowed configs", expected)
|
|
}
|
|
|
|
// log all discovered configs for manual inspection
|
|
t.Logf("Discovered %d allowed config types:", len(allowedConfigs))
|
|
for configType := range allowedConfigs {
|
|
t.Logf(" - %s", configType)
|
|
}
|
|
}
|