go/core_test.go

125 lines
3.4 KiB
Go

package core_test
import (
"go/parser"
"go/token"
"os"
"path/filepath"
"strings"
"testing"
)
// TestPublicAPICompleteness dynamically discovers all public services and ensures
// their top-level API packages are in sync with their internal implementations.
func TestPublicAPICompleteness(t *testing.T) {
pkgDir := "pkg"
// 1. Discover all potential service packages in the pkg/ directory.
internalDirs, err := os.ReadDir(pkgDir)
if err != nil {
t.Fatalf("Failed to read pkg directory: %v", err)
}
var allMissingSymbols []string
for _, dir := range internalDirs {
if !dir.IsDir() || dir.Name() == "core" {
continue // Skip files and the core package itself
}
serviceName := dir.Name()
topLevelDir := serviceName
// 2. Check if a corresponding top-level public API directory exists.
if _, err := os.Stat(topLevelDir); os.IsNotExist(err) {
continue // Not a public service, so we skip it.
}
// 3. Define paths for public and internal Go files.
publicFile := filepath.Join(topLevelDir, serviceName+".go")
internalFile := filepath.Join(pkgDir, serviceName, serviceName+".go")
// Ensure both files exist before trying to parse them.
if _, err := os.Stat(publicFile); os.IsNotExist(err) {
t.Logf("Skipping service '%s': public API file not found at %s", serviceName, publicFile)
continue
}
if _, err := os.Stat(internalFile); os.IsNotExist(err) {
t.Logf("Skipping service '%s': internal implementation file not found at %s", serviceName, internalFile)
continue
}
// 4. Compare the exported symbols.
missing, err := compareExports(publicFile, internalFile)
if err != nil {
t.Errorf("Error comparing exports for service '%s': %v", serviceName, err)
continue
}
if len(missing) > 0 {
msg := "- Service: " + serviceName + "\n - Missing: " + strings.Join(missing, ", ")
allMissingSymbols = append(allMissingSymbols, msg)
}
}
// 5. Report all discrepancies at the end.
if len(allMissingSymbols) > 0 {
t.Errorf("Public APIs are out of sync with internal implementations:\n\n%s",
strings.Join(allMissingSymbols, "\n"))
}
}
// compareExports takes two file paths, parses them, and returns a list of
// symbols that are exported in the internal file but not the public one.
func compareExports(publicFile, internalFile string) ([]string, error) {
publicAPI, err := getExportedSymbols(publicFile)
if err != nil {
return nil, err
}
internalImpl, err := getExportedSymbols(internalFile)
if err != nil {
return nil, err
}
publicSymbols := make(map[string]bool)
for _, sym := range publicAPI {
publicSymbols[sym] = true
}
var missingSymbols []string
for _, internalSym := range internalImpl {
// The public API re-exports the interface from core, so we don't expect it here.
if internalSym == "Config" {
continue
}
if !publicSymbols[internalSym] {
missingSymbols = append(missingSymbols, internalSym)
}
}
return missingSymbols, nil
}
// getExportedSymbols parses a Go file and returns a slice of its exported symbol names.
func getExportedSymbols(path string) ([]string, error) {
absPath, err := filepath.Abs(path)
if err != nil {
return nil, err
}
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, absPath, nil, parser.AllErrors)
if err != nil {
return nil, err
}
var symbols []string
for name, obj := range node.Scope.Objects {
if token.IsExported(name) {
symbols = append(symbols, obj.Name)
}
}
return symbols, nil
}