package main import ( "go/ast" "go/parser" "go/token" "path/filepath" "testing" "github.com/stretchr/testify/require" ) func TestLinkCatalogersToConfigs(t *testing.T) { if testing.Short() { t.Skip("skipping integration test in short mode") } repoRoot, err := RepoRoot() require.NoError(t, err) linkages, err := LinkCatalogersToConfigs(repoRoot) require.NoError(t, err) // verify we discovered multiple catalogers require.NotEmpty(t, linkages, "should discover at least one cataloger linkage") // test cases for known catalogers with configs // NOTE: Some catalogers may not be detected if their Name() method is in a different file // than the constructor function. This is a known limitation. tests := []struct { catalogerName string wantConfig string optional bool // set to true if detection may not work due to cross-file Name() methods }{ { catalogerName: "go-module-binary-cataloger", wantConfig: "golang.CatalogerConfig", }, { catalogerName: "go-module-file-cataloger", wantConfig: "golang.CatalogerConfig", }, { catalogerName: "python-package-cataloger", wantConfig: "python.CatalogerConfig", }, { catalogerName: "java-archive-cataloger", wantConfig: "java.ArchiveCatalogerConfig", }, { catalogerName: "java-pom-cataloger", wantConfig: "java.ArchiveCatalogerConfig", optional: true, // Name() method in different file }, { catalogerName: "dotnet-deps-binary-cataloger", wantConfig: "dotnet.CatalogerConfig", optional: true, // Name() method in different file }, { catalogerName: "javascript-lock-cataloger", wantConfig: "javascript.CatalogerConfig", }, { catalogerName: "linux-kernel-cataloger", wantConfig: "kernel.LinuxKernelCatalogerConfig", }, { catalogerName: "nix-cataloger", wantConfig: "nix.Config", optional: true, // Name() method in different file }, } for _, tt := range tests { t.Run(tt.catalogerName, func(t *testing.T) { config, ok := linkages[tt.catalogerName] if tt.optional && !ok { t.Skipf("cataloger %s not detected (expected due to cross-file Name() method)", tt.catalogerName) return } require.True(t, ok, "should find linkage for cataloger: %s", tt.catalogerName) require.Equal(t, tt.wantConfig, config, "config type should match for cataloger: %s", tt.catalogerName) }) } // test catalogers without configs (should have empty string) catalogersWithoutConfig := []string{ "python-installed-package-cataloger", "java-gradle-lockfile-cataloger", "java-jvm-cataloger", "dotnet-packages-lock-cataloger", "javascript-package-cataloger", } for _, catalogerName := range catalogersWithoutConfig { t.Run(catalogerName+"_no_config", func(t *testing.T) { config, ok := linkages[catalogerName] if ok { require.Empty(t, config, "cataloger %s should have empty config", catalogerName) } }) } // print summary for manual inspection t.Logf("Discovered %d cataloger-to-config linkages:", len(linkages)) // separate into catalogers with and without configs withConfig := make(map[string]string) withoutConfig := make([]string, 0) for name, config := range linkages { if config != "" { withConfig[name] = config } else { withoutConfig = append(withoutConfig, name) } } t.Logf("Catalogers with configs (%d):", len(withConfig)) for name, config := range withConfig { t.Logf(" %s -> %s", name, config) } t.Logf("Catalogers without configs (%d):", len(withoutConfig)) for _, name := range withoutConfig { t.Logf(" %s", name) } // ensure we found at least some catalogers with configs require.GreaterOrEqual(t, len(withConfig), 6, "should find at least 6 catalogers with configs") } func TestLinkCatalogersToConfigsFromPath(t *testing.T) { tests := []struct { name string fixturePath string expectedLinkages map[string]string wantErr require.ErrorAssertionFunc }{ { name: "simple generic cataloger with local config", fixturePath: "simple-generic-cataloger", expectedLinkages: map[string]string{ "go-module-cataloger": "golang.CatalogerConfig", }, }, { name: "cataloger name from constant", fixturePath: "cataloger-with-constant", expectedLinkages: map[string]string{ "python-package-cataloger": "python.CatalogerConfig", }, }, { name: "custom cataloger with Name() in same file", fixturePath: "custom-cataloger-same-file", expectedLinkages: map[string]string{ "java-pom-cataloger": "java.ArchiveCatalogerConfig", }, }, { name: "custom cataloger with Name() in different file - not detected", fixturePath: "custom-cataloger-different-file", expectedLinkages: map[string]string{ // empty - current limitation, cannot detect cross-file Names }, }, { name: "cataloger without config parameter", fixturePath: "no-config-cataloger", expectedLinkages: map[string]string{ "javascript-cataloger": "", // empty string means no config }, }, { name: "imported config type", fixturePath: "imported-config-type", expectedLinkages: map[string]string{ "linux-kernel-cataloger": "kernel.LinuxKernelCatalogerConfig", }, }, { name: "non-config first parameter", fixturePath: "non-config-first-param", expectedLinkages: map[string]string{ "binary-cataloger": "", // Parser not a config type }, }, { name: "conflicting cataloger names", fixturePath: "conflicting-names", wantErr: require.Error, }, { name: "mixed naming patterns", fixturePath: "mixed-naming-patterns", expectedLinkages: map[string]string{ "ruby-cataloger": "ruby.Config", }, }, { name: "selector expression config", fixturePath: "selector-expression-config", expectedLinkages: map[string]string{ "rust-cataloger": "cargo.CatalogerConfig", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.wantErr == nil { tt.wantErr = require.NoError } fixtureDir := filepath.Join("test-fixtures", "config-linking", tt.fixturePath) linkages, err := LinkCatalogersToConfigsFromPath(fixtureDir, fixtureDir) tt.wantErr(t, err) if err != nil { return } require.Equal(t, tt.expectedLinkages, linkages) }) } } func TestExtractConfigTypeName(t *testing.T) { if testing.Short() { t.Skip("skipping integration test in short mode") } tests := []struct { name string catalogerName string expectedConfig string expectedNoConfig bool }{ { name: "golang config", catalogerName: "go-module-binary-cataloger", expectedConfig: "golang.CatalogerConfig", }, { name: "python config", catalogerName: "python-package-cataloger", expectedConfig: "python.CatalogerConfig", }, { name: "java archive config", catalogerName: "java-archive-cataloger", expectedConfig: "java.ArchiveCatalogerConfig", }, { name: "kernel config", catalogerName: "linux-kernel-cataloger", expectedConfig: "kernel.LinuxKernelCatalogerConfig", }, { name: "python installed - no config", catalogerName: "python-installed-package-cataloger", expectedNoConfig: true, }, } repoRoot, err := RepoRoot() require.NoError(t, err) linkages, err := LinkCatalogersToConfigs(repoRoot) require.NoError(t, err) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { config, ok := linkages[tt.catalogerName] if tt.expectedNoConfig { if ok { require.Empty(t, config, "expected no config for %s", tt.catalogerName) } } else { require.True(t, ok, "should find cataloger %s", tt.catalogerName) require.Equal(t, tt.expectedConfig, config) } }) } } func TestLooksLikeConfigType(t *testing.T) { tests := []struct { name string typeName string want bool }{ { name: "golang config", typeName: "golang.CatalogerConfig", want: true, }, { name: "python config", typeName: "python.CatalogerConfig", want: true, }, { name: "java archive config", typeName: "java.ArchiveCatalogerConfig", want: true, }, { name: "kernel config", typeName: "kernel.LinuxKernelCatalogerConfig", want: true, }, { name: "nix config", typeName: "nix.Config", want: true, }, { name: "config prefix", typeName: "package.ConfigOptions", want: true, }, { name: "not a config type", typeName: "package.Parser", want: false, }, { name: "not a config type - resolver", typeName: "file.Resolver", want: false, }, { name: "no package prefix", typeName: "CatalogerConfig", want: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := looksLikeConfigType(tt.typeName) require.Equal(t, tt.want, got) }) } } func TestExtractReceiverTypeName(t *testing.T) { tests := []struct { name string receiver string // receiver code snippet want string }{ { name: "value receiver", receiver: "func (c Cataloger) Name() string { return \"\" }", want: "Cataloger", }, { name: "pointer receiver", receiver: "func (c *Cataloger) Name() string { return \"\" }", want: "Cataloger", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // parse the function to get the receiver type fset := token.NewFileSet() file, err := parser.ParseFile(fset, "", "package test\n"+tt.receiver, 0) require.NoError(t, err) // extract the function declaration require.Len(t, file.Decls, 1) funcDecl, ok := file.Decls[0].(*ast.FuncDecl) require.True(t, ok) // get receiver type var recvType ast.Expr if funcDecl.Recv != nil && len(funcDecl.Recv.List) > 0 { recvType = funcDecl.Recv.List[0].Type } got := extractReceiverTypeName(recvType) require.Equal(t, tt.want, got) }) } } func TestExtractConfigTypeNameHelper(t *testing.T) { tests := []struct { name string funcSig string // function signature with parameter localPackageName string want string }{ { name: "local type", funcSig: "func New(cfg CatalogerConfig) pkg.Cataloger { return nil }", localPackageName: "python", want: "python.CatalogerConfig", }, { name: "imported type", funcSig: "func New(cfg java.ArchiveCatalogerConfig) pkg.Cataloger { return nil }", localPackageName: "python", want: "java.ArchiveCatalogerConfig", }, { name: "imported type - kernel package", funcSig: "func New(cfg kernel.LinuxKernelCatalogerConfig) pkg.Cataloger { return nil }", localPackageName: "other", want: "kernel.LinuxKernelCatalogerConfig", }, { name: "no parameters", funcSig: "func New() pkg.Cataloger { return nil }", localPackageName: "python", want: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // parse the function to get parameter type fset := token.NewFileSet() file, err := parser.ParseFile(fset, "", "package test\n"+tt.funcSig, 0) require.NoError(t, err) // extract the function declaration require.Len(t, file.Decls, 1) funcDecl, ok := file.Decls[0].(*ast.FuncDecl) require.True(t, ok) // get first parameter type var paramType ast.Expr if funcDecl.Type.Params != nil && len(funcDecl.Type.Params.List) > 0 { paramType = funcDecl.Type.Params.List[0].Type } got := extractConfigTypeName(paramType, tt.localPackageName) require.Equal(t, tt.want, got) }) } } func TestExtractReturnTypeName(t *testing.T) { tests := []struct { name string funcDef string // complete function definition want string }{ { name: "pointer to composite literal", funcDef: `func New() pkg.Cataloger { return &javaCataloger{name: "test"} }`, want: "javaCataloger", }, { name: "composite literal", funcDef: `func New() pkg.Cataloger { return pythonCataloger{name: "test"} }`, want: "pythonCataloger", }, { name: "variable return", funcDef: `func New() pkg.Cataloger { c := &Cataloger{} return c }`, want: "", }, { name: "nil return", funcDef: `func New() pkg.Cataloger { return nil }`, want: "", }, { name: "empty function body", funcDef: `func New() pkg.Cataloger {}`, want: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // parse the function fset := token.NewFileSet() file, err := parser.ParseFile(fset, "", "package test\n"+tt.funcDef, 0) require.NoError(t, err) // extract the function declaration require.Len(t, file.Decls, 1) funcDecl, ok := file.Decls[0].(*ast.FuncDecl) require.True(t, ok) got := extractReturnTypeName(funcDecl) require.Equal(t, tt.want, got) }) } }