Add options type to Config service and improve type assignment error handling; add unit tests for Config functionality
This commit is contained in:
parent
536ce7b6cd
commit
bb94bdf061
4 changed files with 191 additions and 1 deletions
|
|
@ -13,6 +13,9 @@ import (
|
|||
// to the underlying implementation, making it transparent to the user.
|
||||
type Service = impl.Service
|
||||
|
||||
// Options is the public type for the Config service options.
|
||||
type Options = impl.Options
|
||||
|
||||
// New is the public constructor for the Config service. It is a variable
|
||||
// that points to the real constructor in the implementation package.
|
||||
var New = impl.New
|
||||
|
|
|
|||
62
config/config_test.go
Normal file
62
config/config_test.go
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
package config_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/Snider/Core/config"
|
||||
"github.com/Snider/Core/pkg/core"
|
||||
)
|
||||
|
||||
func TestInterfaceCompliance(t *testing.T) {
|
||||
var _ config.Config = (*config.Service)(nil)
|
||||
}
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
if config.Register == nil {
|
||||
t.Fatal("config.Register factory is nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGet_NonExistentKey validates that getting a non-existent key returns an error.
|
||||
func TestGet_NonExistentKey(t *testing.T) {
|
||||
coreImpl, err := core.New(core.WithService(config.Register))
|
||||
if err != nil {
|
||||
t.Fatalf("core.New() failed: %v", err)
|
||||
}
|
||||
|
||||
var value string
|
||||
err = coreImpl.Config().Get("nonexistent.key", &value)
|
||||
if err == nil {
|
||||
t.Fatal("expected an error when getting a nonexistent key, but got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSetAndGet verifies that a value can be set and then retrieved correctly.
|
||||
func TestSetAndGet(t *testing.T) {
|
||||
coreImpl, err := core.New(core.WithService(config.Register))
|
||||
if err != nil {
|
||||
t.Fatalf("core.New() failed: %v", err)
|
||||
}
|
||||
|
||||
cfg := coreImpl.Config()
|
||||
|
||||
// 1. Set a value for an existing key
|
||||
key := "language"
|
||||
expectedValue := "fr"
|
||||
err = cfg.Set(key, expectedValue)
|
||||
if err != nil {
|
||||
t.Fatalf("Set(%q, %q) failed: %v", key, expectedValue, err)
|
||||
}
|
||||
|
||||
// 2. Get the value back
|
||||
var actualValue string
|
||||
err = cfg.Get(key, &actualValue)
|
||||
if err != nil {
|
||||
t.Fatalf("Get(%q) failed: %v", key, err)
|
||||
}
|
||||
|
||||
// 3. Compare the values
|
||||
if actualValue != expectedValue {
|
||||
t.Errorf("Get(%q) returned %q, want %q", key, actualValue, expectedValue)
|
||||
}
|
||||
}
|
||||
125
core_test.go
Normal file
125
core_test.go
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
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
|
||||
}
|
||||
|
|
@ -147,7 +147,7 @@ func (s *Service) Get(key string, out any) error {
|
|||
targetVal := outVal.Elem()
|
||||
srcVal := val.Field(i)
|
||||
|
||||
if !targetVal.Type().AssignableTo(srcVal.Type()) {
|
||||
if !srcVal.Type().AssignableTo(targetVal.Type()) {
|
||||
return fmt.Errorf("cannot assign config value of type %s to output of type %s", srcVal.Type(), targetVal.Type())
|
||||
}
|
||||
targetVal.Set(srcVal)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue