Enhance test coverage.

This commit is contained in:
2025-09-17 10:31:41 +01:00
parent ff2562f9fe
commit 397efdae0f
12 changed files with 7521 additions and 1323 deletions
+1799
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -67,7 +67,7 @@ func (f *Factory) GetLogger(name string) Logger {
// createLogger creates a new logger instance
func (f *Factory) createLogger(name string) Logger {
if name == "noop" || name == "no-op" || name == "discard" {
return f.GetNoOpLogger()
return GetNoOpLogger()
}
// Create logger with appropriate outputs based on environment
File diff suppressed because it is too large Load Diff
+586
View File
@@ -0,0 +1,586 @@
package pool
import (
"bytes"
"strings"
"sync"
"testing"
)
// TestManager_Singleton tests that Get() returns the same instance
func TestManager_Singleton(t *testing.T) {
manager1 := Get()
manager2 := Get()
if manager1 != manager2 {
t.Error("Get() should return the same instance (singleton)")
}
if manager1 == nil {
t.Error("Get() should not return nil")
}
}
// TestManager_BufferPools tests buffer pool operations
func TestManager_BufferPools(t *testing.T) {
manager := Get()
tests := []struct {
name string
sizeHint int
expected int // expected capacity range
}{
{"small buffer", 512, 1024},
{"medium buffer", 2048, 4096},
{"large buffer", 6144, 8192},
{"xl buffer", 12288, 16384},
{"oversized buffer", 32768, 32768}, // Should create new buffer
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
buf := manager.GetBuffer(test.sizeHint)
if buf == nil {
t.Error("GetBuffer should not return nil")
}
if buf.Cap() < test.sizeHint {
t.Errorf("Buffer capacity %d is less than size hint %d", buf.Cap(), test.sizeHint)
}
// Write some data
buf.WriteString("test data")
if buf.String() != "test data" {
t.Error("Buffer should contain written data")
}
// Return to pool
manager.PutBuffer(buf)
// Buffer should be reset when returned to pool
buf2 := manager.GetBuffer(test.sizeHint)
if buf2.Len() != 0 {
t.Error("Buffer from pool should be reset")
}
})
}
}
// TestManager_PutBuffer_Nil tests putting nil buffer
func TestManager_PutBuffer_Nil(t *testing.T) {
manager := Get()
// Should not panic
manager.PutBuffer(nil)
}
// TestManager_PutBuffer_Oversized tests rejection of oversized buffers
func TestManager_PutBuffer_Oversized(t *testing.T) {
manager := Get()
manager.ResetStats()
// Create oversized buffer
buf := bytes.NewBuffer(make([]byte, 0, 40000))
manager.PutBuffer(buf)
stats := manager.GetStats()
if stats.OversizedRejects == 0 {
t.Error("Oversized buffer should be rejected")
}
}
// TestManager_GzipPools tests gzip writer and reader pools
func TestManager_GzipPools(t *testing.T) {
manager := Get()
// Test gzip writer
writer := manager.GetGzipWriter()
if writer == nil {
t.Error("GetGzipWriter should not return nil")
}
// Test that we can use it
var buf bytes.Buffer
writer.Reset(&buf)
writer.Write([]byte("test data"))
writer.Close()
if buf.Len() == 0 {
t.Error("Gzip writer should have written compressed data")
}
// Return to pool
manager.PutGzipWriter(writer)
// Test gzip reader
reader := manager.GetGzipReader()
// Reader might be nil from pool initially
if reader != nil {
manager.PutGzipReader(reader)
}
}
// TestManager_GzipPools_Nil tests putting nil gzip objects
func TestManager_GzipPools_Nil(t *testing.T) {
manager := Get()
// Should not panic
manager.PutGzipWriter(nil)
manager.PutGzipReader(nil)
}
// TestManager_StringBuilderPool tests string builder pool
func TestManager_StringBuilderPool(t *testing.T) {
manager := Get()
sb := manager.GetStringBuilder()
if sb == nil {
t.Error("GetStringBuilder should not return nil")
}
// Should be reset
if sb.Len() != 0 {
t.Error("String builder from pool should be reset")
}
// Test writing
sb.WriteString("test")
sb.WriteString(" data")
if sb.String() != "test data" {
t.Error("String builder should contain written data")
}
// Return to pool
manager.PutStringBuilder(sb)
// Get another one - should be reset
sb2 := manager.GetStringBuilder()
if sb2.Len() != 0 {
t.Error("String builder from pool should be reset")
}
}
// TestManager_StringBuilderPool_Nil tests putting nil string builder
func TestManager_StringBuilderPool_Nil(t *testing.T) {
manager := Get()
// Should not panic
manager.PutStringBuilder(nil)
}
// TestManager_StringBuilderPool_Oversized tests rejection of oversized string builders
func TestManager_StringBuilderPool_Oversized(t *testing.T) {
manager := Get()
manager.ResetStats()
// Create oversized string builder
sb := &strings.Builder{}
sb.Grow(20000)
sb.WriteString("test")
manager.PutStringBuilder(sb)
stats := manager.GetStats()
if stats.OversizedRejects == 0 {
t.Error("Oversized string builder should be rejected")
}
}
// TestManager_JWTBufferPool tests JWT buffer pool
func TestManager_JWTBufferPool(t *testing.T) {
manager := Get()
jwtBuf := manager.GetJWTBuffer()
if jwtBuf == nil {
t.Error("GetJWTBuffer should not return nil")
return
}
// Check structure
if jwtBuf.Header == nil || jwtBuf.Payload == nil || jwtBuf.Signature == nil {
t.Error("JWT buffer should have all fields initialized")
}
// Should be empty initially
if len(jwtBuf.Header) != 0 || len(jwtBuf.Payload) != 0 || len(jwtBuf.Signature) != 0 {
t.Error("JWT buffer from pool should be reset")
}
// Use the buffer
jwtBuf.Header = append(jwtBuf.Header, []byte("header")...)
jwtBuf.Payload = append(jwtBuf.Payload, []byte("payload")...)
jwtBuf.Signature = append(jwtBuf.Signature, []byte("signature")...)
// Return to pool
manager.PutJWTBuffer(jwtBuf)
// Get another one - should be reset
jwtBuf2 := manager.GetJWTBuffer()
if len(jwtBuf2.Header) != 0 || len(jwtBuf2.Payload) != 0 || len(jwtBuf2.Signature) != 0 {
t.Error("JWT buffer from pool should be reset")
}
}
// TestManager_JWTBufferPool_Nil tests putting nil JWT buffer
func TestManager_JWTBufferPool_Nil(t *testing.T) {
manager := Get()
// Should not panic
manager.PutJWTBuffer(nil)
}
// TestManager_JWTBufferPool_Oversized tests rejection of oversized JWT buffers
func TestManager_JWTBufferPool_Oversized(t *testing.T) {
manager := Get()
manager.ResetStats()
// Create oversized JWT buffer
jwtBuf := &JWTBuffer{
Header: make([]byte, 0, 3000), // Over 2048 limit
Payload: make([]byte, 0, 10000), // Over 8192 limit
Signature: make([]byte, 0, 3000), // Over 2048 limit
}
manager.PutJWTBuffer(jwtBuf)
stats := manager.GetStats()
if stats.OversizedRejects == 0 {
t.Error("Oversized JWT buffer should be rejected")
}
}
// TestManager_HTTPResponsePool tests HTTP response buffer pool
func TestManager_HTTPResponsePool(t *testing.T) {
manager := Get()
buf := manager.GetHTTPResponseBuffer()
if buf == nil {
t.Error("GetHTTPResponseBuffer should not return nil")
}
// Should be empty initially
if len(buf) != 0 {
t.Error("HTTP buffer from pool should be empty")
}
// Use the buffer
buf = append(buf, []byte("HTTP response data")...)
// Return to pool
manager.PutHTTPResponseBuffer(buf)
// Get another one - should be reset
buf2 := manager.GetHTTPResponseBuffer()
if len(buf2) != 0 {
t.Error("HTTP buffer from pool should be reset")
}
}
// TestManager_HTTPResponsePool_Nil tests putting nil HTTP buffer
func TestManager_HTTPResponsePool_Nil(t *testing.T) {
manager := Get()
// Should not panic
manager.PutHTTPResponseBuffer(nil)
}
// TestManager_HTTPResponsePool_Oversized tests rejection of oversized HTTP buffers
func TestManager_HTTPResponsePool_Oversized(t *testing.T) {
manager := Get()
manager.ResetStats()
// Create oversized buffer
buf := make([]byte, 0, 40000)
manager.PutHTTPResponseBuffer(buf)
stats := manager.GetStats()
if stats.OversizedRejects == 0 {
t.Error("Oversized HTTP buffer should be rejected")
}
}
// TestManager_ByteSlicePool tests byte slice pool with dynamic sizing
func TestManager_ByteSlicePool(t *testing.T) {
manager := Get()
tests := []int{256, 512, 1024, 2048, 4096, 8192, 16384}
for _, size := range tests {
t.Run(strings.Join([]string{"size", string(rune(size))}, "_"), func(t *testing.T) {
slice := manager.GetByteSlice(size)
if slice == nil {
t.Error("GetByteSlice should not return nil")
}
if len(slice) != size {
t.Errorf("Byte slice length %d != requested size %d", len(slice), size)
}
if cap(slice) < size {
t.Errorf("Byte slice capacity %d < requested size %d", cap(slice), size)
}
// Use the slice
copy(slice, []byte("test data"))
// Return to pool
manager.PutByteSlice(slice)
})
}
}
// TestManager_ByteSlicePool_CustomSize tests byte slice pool with non-standard sizes
func TestManager_ByteSlicePool_CustomSize(t *testing.T) {
manager := Get()
// Test custom size (should round up to power of 2)
slice := manager.GetByteSlice(300)
if slice == nil {
t.Error("GetByteSlice should not return nil")
}
if len(slice) != 300 {
t.Errorf("Byte slice length %d != requested size 300", len(slice))
}
// Capacity should be >= 300 (likely 512 as next power of 2)
if cap(slice) < 300 {
t.Error("Byte slice capacity should be at least 300")
}
manager.PutByteSlice(slice)
}
// TestManager_ByteSlicePool_Nil tests putting nil byte slice
func TestManager_ByteSlicePool_Nil(t *testing.T) {
manager := Get()
// Should not panic
manager.PutByteSlice(nil)
}
// TestManager_ByteSlicePool_Oversized tests rejection of oversized byte slices
func TestManager_ByteSlicePool_Oversized(t *testing.T) {
manager := Get()
// Create oversized slice
slice := make([]byte, 100000)
// Should not panic and should not be pooled
manager.PutByteSlice(slice)
}
// TestManager_Stats tests statistics tracking
func TestManager_Stats(t *testing.T) {
manager := Get()
manager.ResetStats()
initialStats := manager.GetStats()
if initialStats.BufferGets != 0 || initialStats.BufferPuts != 0 {
t.Error("Stats should be zero after reset")
}
// Perform operations
buf := manager.GetBuffer(1024)
manager.PutBuffer(buf)
writer := manager.GetGzipWriter()
manager.PutGzipWriter(writer)
sb := manager.GetStringBuilder()
manager.PutStringBuilder(sb)
jwtBuf := manager.GetJWTBuffer()
manager.PutJWTBuffer(jwtBuf)
httpBuf := manager.GetHTTPResponseBuffer()
manager.PutHTTPResponseBuffer(httpBuf)
// Check stats
stats := manager.GetStats()
if stats.BufferGets == 0 || stats.BufferPuts == 0 {
t.Error("Buffer stats should be incremented")
}
if stats.GzipGets == 0 || stats.GzipPuts == 0 {
t.Error("Gzip stats should be incremented")
}
if stats.StringGets == 0 || stats.StringPuts == 0 {
t.Error("String stats should be incremented")
}
if stats.JWTGets == 0 || stats.JWTPuts == 0 {
t.Error("JWT stats should be incremented")
}
if stats.HTTPGets == 0 || stats.HTTPPuts == 0 {
t.Error("HTTP stats should be incremented")
}
}
// TestManager_ResetStats tests statistics reset
func TestManager_ResetStats(t *testing.T) {
manager := Get()
// Perform some operations
buf := manager.GetBuffer(1024)
manager.PutBuffer(buf)
// Check that stats are non-zero
stats := manager.GetStats()
if stats.BufferGets == 0 {
t.Error("Stats should be non-zero before reset")
}
// Reset stats
manager.ResetStats()
// Check that stats are zero
resetStats := manager.GetStats()
if resetStats.BufferGets != 0 || resetStats.BufferPuts != 0 {
t.Error("Stats should be zero after reset")
}
}
// TestManager_ConcurrentAccess tests concurrent access to pools
func TestManager_ConcurrentAccess(t *testing.T) {
manager := Get()
manager.ResetStats()
var wg sync.WaitGroup
numGoroutines := 50
operationsPerGoroutine := 10
wg.Add(numGoroutines)
for i := 0; i < numGoroutines; i++ {
go func() {
defer wg.Done()
for j := 0; j < operationsPerGoroutine; j++ {
// Test buffer pool
buf := manager.GetBuffer(1024)
buf.WriteString("test")
manager.PutBuffer(buf)
// Test string builder pool
sb := manager.GetStringBuilder()
sb.WriteString("test")
manager.PutStringBuilder(sb)
// Test JWT buffer pool
jwtBuf := manager.GetJWTBuffer()
jwtBuf.Header = append(jwtBuf.Header, byte(j))
manager.PutJWTBuffer(jwtBuf)
// Test byte slice pool
slice := manager.GetByteSlice(256)
slice[0] = byte(j)
manager.PutByteSlice(slice)
}
}()
}
wg.Wait()
// Check that operations completed without panic
stats := manager.GetStats()
expectedOps := uint64(numGoroutines * operationsPerGoroutine)
if stats.BufferGets < expectedOps || stats.StringGets < expectedOps || stats.JWTGets < expectedOps {
t.Error("Some operations may have failed during concurrent access")
}
}
// TestGlobalConvenienceFunctions tests the global convenience functions
func TestGlobalConvenienceFunctions(t *testing.T) {
// Test buffer functions
buf := Buffer(1024)
if buf == nil {
t.Error("Buffer() should not return nil")
}
buf.WriteString("test")
ReturnBuffer(buf)
// Test gzip functions
writer := GzipWriter()
if writer == nil {
t.Error("GzipWriter() should not return nil")
}
ReturnGzipWriter(writer)
// Test string builder functions
sb := StringBuilder()
if sb == nil {
t.Error("StringBuilder() should not return nil")
}
sb.WriteString("test")
ReturnStringBuilder(sb)
// Test JWT buffer functions
jwtBuf := JWTBuffers()
if jwtBuf == nil {
t.Error("JWTBuffers() should not return nil")
}
ReturnJWTBuffers(jwtBuf)
// Test HTTP buffer functions
httpBuf := HTTPBuffer()
if httpBuf == nil {
t.Error("HTTPBuffer() should not return nil")
}
ReturnHTTPBuffer(httpBuf)
// Test byte slice functions
slice := ByteSlice(256)
if slice == nil {
t.Error("ByteSlice() should not return nil")
}
if len(slice) != 256 {
t.Error("ByteSlice() should return correct size")
}
ReturnByteSlice(slice)
}
// Benchmark tests for performance verification
func BenchmarkManager_GetBuffer(b *testing.B) {
manager := Get()
b.ResetTimer()
for i := 0; i < b.N; i++ {
buf := manager.GetBuffer(1024)
manager.PutBuffer(buf)
}
}
func BenchmarkManager_GetStringBuilder(b *testing.B) {
manager := Get()
b.ResetTimer()
for i := 0; i < b.N; i++ {
sb := manager.GetStringBuilder()
manager.PutStringBuilder(sb)
}
}
func BenchmarkManager_GetJWTBuffer(b *testing.B) {
manager := Get()
b.ResetTimer()
for i := 0; i < b.N; i++ {
jwtBuf := manager.GetJWTBuffer()
manager.PutJWTBuffer(jwtBuf)
}
}
func BenchmarkManager_GetByteSlice(b *testing.B) {
manager := Get()
b.ResetTimer()
for i := 0; i < b.N; i++ {
slice := manager.GetByteSlice(1024)
manager.PutByteSlice(slice)
}
}
func BenchmarkManager_ConcurrentAccess(b *testing.B) {
manager := Get()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
buf := manager.GetBuffer(1024)
buf.WriteString("test")
manager.PutBuffer(buf)
}
})
}
+593
View File
@@ -0,0 +1,593 @@
package pool
import (
"context"
"crypto/tls"
"net/http"
"sync"
"testing"
"time"
)
// TestGetTransportPool_Singleton tests that GetTransportPool returns the same instance
func TestGetTransportPool_Singleton(t *testing.T) {
pool1 := GetTransportPool()
pool2 := GetTransportPool()
if pool1 != pool2 {
t.Error("GetTransportPool() should return the same instance (singleton)")
}
if pool1 == nil {
t.Error("GetTransportPool() should not return nil")
}
}
// TestDefaultTransportConfig tests the default transport configuration
func TestDefaultTransportConfig(t *testing.T) {
config := DefaultTransportConfig()
// Verify security defaults
if config.MinTLSVersion != tls.VersionTLS12 {
t.Errorf("Default MinTLSVersion should be TLS 1.2, got %d", config.MinTLSVersion)
}
if config.InsecureSkipVerify {
t.Error("Default should not skip TLS verification")
}
if !config.ForceHTTP2 {
t.Error("Default should force HTTP/2")
}
// Verify reasonable timeouts
if config.DialTimeout <= 0 {
t.Error("DialTimeout should be positive")
}
if config.TLSHandshakeTimeout <= 0 {
t.Error("TLSHandshakeTimeout should be positive")
}
if config.ResponseHeaderTimeout <= 0 {
t.Error("ResponseHeaderTimeout should be positive")
}
// Verify connection limits
if config.MaxIdleConns <= 0 {
t.Error("MaxIdleConns should be positive")
}
if config.MaxIdleConnsPerHost <= 0 {
t.Error("MaxIdleConnsPerHost should be positive")
}
if config.MaxConnsPerHost <= 0 {
t.Error("MaxConnsPerHost should be positive")
}
}
// TestTransportPool_GetTransport tests transport creation and reuse
func TestTransportPool_GetTransport(t *testing.T) {
pool := &TransportPool{
transports: make(map[string]*sharedTransport),
maxConns: 20,
maxClients: 5,
}
config := DefaultTransportConfig()
// First call should create new transport
transport1 := pool.GetTransport(config)
if transport1 == nil {
t.Error("GetTransport should not return nil")
}
// Second call with same config should return same transport
transport2 := pool.GetTransport(config)
if transport2 == nil {
t.Error("GetTransport should not return nil")
}
if transport1 != transport2 {
t.Error("GetTransport should return same transport for same config")
}
// Verify reference counting
pool.mu.RLock()
key := pool.configKey(config)
shared := pool.transports[key]
refCount := shared.refCount
pool.mu.RUnlock()
if refCount != 2 {
t.Errorf("Reference count should be 2, got %d", refCount)
}
}
// TestTransportPool_GetTransport_DifferentConfigs tests transport creation with different configs
func TestTransportPool_GetTransport_DifferentConfigs(t *testing.T) {
pool := &TransportPool{
transports: make(map[string]*sharedTransport),
maxConns: 20,
maxClients: 5,
}
config1 := DefaultTransportConfig()
config2 := DefaultTransportConfig()
config2.MaxConnsPerHost = 10 // Different from default
transport1 := pool.GetTransport(config1)
transport2 := pool.GetTransport(config2)
if transport1 == transport2 {
t.Error("Different configs should produce different transports")
}
}
// TestTransportPool_GetTransport_ClientLimit tests client limit enforcement
func TestTransportPool_GetTransport_ClientLimit(t *testing.T) {
pool := &TransportPool{
transports: make(map[string]*sharedTransport),
maxConns: 20,
maxClients: 2, // Low limit for testing
clientCount: 2, // Already at limit
}
config := DefaultTransportConfig()
// Should return existing transport when limit reached
transport := pool.GetTransport(config)
// Transport might be nil if no existing transports
if transport != nil && pool.clientCount > pool.maxClients {
t.Error("Should not exceed client limit")
}
}
// TestTransportPool_ReleaseTransport tests transport reference counting
func TestTransportPool_ReleaseTransport(t *testing.T) {
pool := &TransportPool{
transports: make(map[string]*sharedTransport),
maxConns: 20,
maxClients: 5,
}
config := DefaultTransportConfig()
// Get transport
transport := pool.GetTransport(config)
if transport == nil {
t.Error("GetTransport should not return nil")
}
// Release transport
pool.ReleaseTransport(transport)
// Verify reference count decreased
pool.mu.RLock()
key := pool.configKey(config)
shared := pool.transports[key]
refCount := shared.refCount
pool.mu.RUnlock()
if refCount != 0 {
t.Errorf("Reference count should be 0 after release, got %d", refCount)
}
}
// TestTransportPool_ReleaseTransport_Nil tests releasing nil transport
func TestTransportPool_ReleaseTransport_Nil(t *testing.T) {
pool := &TransportPool{
transports: make(map[string]*sharedTransport),
maxConns: 20,
maxClients: 5,
}
// Should not panic
pool.ReleaseTransport(nil)
}
// TestTransportPool_ReleaseTransport_Unknown tests releasing unknown transport
func TestTransportPool_ReleaseTransport_Unknown(t *testing.T) {
pool := &TransportPool{
transports: make(map[string]*sharedTransport),
maxConns: 20,
maxClients: 5,
}
// Create a transport not from the pool
transport := &http.Transport{}
// Should not panic
pool.ReleaseTransport(transport)
}
// TestTransportPool_createTransport tests transport creation with different configs
func TestTransportPool_createTransport(t *testing.T) {
pool := &TransportPool{}
tests := []struct {
name string
config TransportConfig
}{
{
"default config",
DefaultTransportConfig(),
},
{
"custom timeouts",
TransportConfig{
DialTimeout: 10 * time.Second,
TLSHandshakeTimeout: 5 * time.Second,
MinTLSVersion: tls.VersionTLS13,
},
},
{
"insecure config",
TransportConfig{
InsecureSkipVerify: true,
MinTLSVersion: tls.VersionTLS10,
},
},
{
"no HTTP/2",
TransportConfig{
ForceHTTP2: false,
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
transport := pool.createTransport(test.config)
if transport == nil {
t.Error("createTransport should not return nil")
return
}
// Verify TLS config
if transport.TLSClientConfig == nil {
t.Error("Transport should have TLS config")
return
}
// Verify minimum TLS version
expectedMinVersion := test.config.MinTLSVersion
if expectedMinVersion == 0 {
expectedMinVersion = tls.VersionTLS12 // Default
}
if transport.TLSClientConfig.MinVersion != expectedMinVersion {
t.Errorf("TLS MinVersion should be %d, got %d", expectedMinVersion, transport.TLSClientConfig.MinVersion)
}
// Verify max TLS version
if transport.TLSClientConfig.MaxVersion != tls.VersionTLS13 {
t.Errorf("TLS MaxVersion should be %d, got %d", tls.VersionTLS13, transport.TLSClientConfig.MaxVersion)
}
// Verify InsecureSkipVerify
if transport.TLSClientConfig.InsecureSkipVerify != test.config.InsecureSkipVerify {
t.Errorf("InsecureSkipVerify should be %v, got %v", test.config.InsecureSkipVerify, transport.TLSClientConfig.InsecureSkipVerify)
}
// Verify HTTP/2
if transport.ForceAttemptHTTP2 != test.config.ForceHTTP2 {
t.Errorf("ForceAttemptHTTP2 should be %v, got %v", test.config.ForceHTTP2, transport.ForceAttemptHTTP2)
}
// Verify timeouts
if test.config.TLSHandshakeTimeout > 0 && transport.TLSHandshakeTimeout != test.config.TLSHandshakeTimeout {
t.Errorf("TLSHandshakeTimeout should be %v, got %v", test.config.TLSHandshakeTimeout, transport.TLSHandshakeTimeout)
}
})
}
}
// TestTransportPool_configKey tests configuration key generation
func TestTransportPool_configKey(t *testing.T) {
pool := &TransportPool{}
config1 := DefaultTransportConfig()
config2 := DefaultTransportConfig()
key1 := pool.configKey(config1)
key2 := pool.configKey(config2)
if key1 != key2 {
t.Error("Same configs should generate same key")
}
// Different config
config3 := config1
config3.MaxConnsPerHost = 999
key3 := pool.configKey(config3)
if key1 == key3 {
t.Error("Different configs should generate different keys")
}
}
// TestTransportPool_cleanupIdle tests idle transport cleanup
func TestTransportPool_cleanupIdle(t *testing.T) {
pool := &TransportPool{
transports: make(map[string]*sharedTransport),
maxConns: 20,
maxClients: 5,
}
config := DefaultTransportConfig()
transport := pool.createTransport(config)
// Add transport to pool with old timestamp
shared := &sharedTransport{
transport: transport,
refCount: 0,
lastUsed: time.Now().Add(-5 * time.Minute), // Old
config: config,
}
key := pool.configKey(config)
pool.transports[key] = shared
// Run cleanup
pool.cleanupIdle()
// Transport should be removed
if _, exists := pool.transports[key]; exists {
t.Error("Old idle transport should be cleaned up")
}
}
// TestTransportPool_cleanup tests full cleanup
func TestTransportPool_cleanup(t *testing.T) {
pool := &TransportPool{
transports: make(map[string]*sharedTransport),
maxConns: 20,
maxClients: 5,
clientCount: 3,
}
config := DefaultTransportConfig()
transport := pool.createTransport(config)
// Add transport to pool
shared := &sharedTransport{
transport: transport,
refCount: 1,
lastUsed: time.Now(),
config: config,
}
key := pool.configKey(config)
pool.transports[key] = shared
// Run cleanup
pool.cleanup()
// All transports should be removed
if len(pool.transports) != 0 {
t.Error("All transports should be cleaned up")
}
// Client count should be reset
if pool.clientCount != 0 {
t.Error("Client count should be reset")
}
}
// TestTransportPool_Shutdown tests graceful shutdown
func TestTransportPool_Shutdown(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
pool := &TransportPool{
transports: make(map[string]*sharedTransport),
maxConns: 20,
maxClients: 5,
ctx: ctx,
cancel: cancel,
}
// Should not panic
pool.Shutdown()
}
// TestTransportPool_GetStats tests statistics
func TestTransportPool_GetStats(t *testing.T) {
pool := &TransportPool{
transports: make(map[string]*sharedTransport),
maxConns: 20,
maxClients: 5,
clientCount: 3,
}
config := DefaultTransportConfig()
// Add some transports
for i := 0; i < 3; i++ {
transport := pool.createTransport(config)
shared := &sharedTransport{
transport: transport,
refCount: int32(i % 2), // Some active, some idle
lastUsed: time.Now(),
config: config,
}
pool.transports[string(rune(i))] = shared
}
stats := pool.GetStats()
if stats.TotalClients != 3 {
t.Errorf("TotalClients should be 3, got %d", stats.TotalClients)
}
if stats.MaxClients != 5 {
t.Errorf("MaxClients should be 5, got %d", stats.MaxClients)
}
if stats.ActiveTransports < 0 || stats.ActiveTransports > 3 {
t.Errorf("ActiveTransports should be between 0 and 3, got %d", stats.ActiveTransports)
}
}
// TestCreateHTTPClient tests HTTP client creation
func TestCreateHTTPClient(t *testing.T) {
config := DefaultTransportConfig()
timeout := 30 * time.Second
client := CreateHTTPClient(config, timeout)
if client == nil {
t.Error("CreateHTTPClient should not return nil")
return
}
if client.Timeout != timeout {
t.Errorf("Client timeout should be %v, got %v", timeout, client.Timeout)
}
if client.Transport == nil {
t.Error("Client should have transport")
}
if client.CheckRedirect == nil {
t.Error("Client should have redirect policy")
}
// Test redirect policy
req := &http.Request{}
var via []*http.Request
// Should allow up to 9 redirects (10 total requests)
for i := 0; i < 9; i++ {
via = append(via, &http.Request{})
err := client.CheckRedirect(req, via)
if err != nil {
t.Errorf("Should allow %d redirects, got error: %v", i+1, err)
}
}
// Should reject 10th redirect (11th total request)
via = append(via, &http.Request{})
err := client.CheckRedirect(req, via)
if err != http.ErrUseLastResponse {
t.Error("Should reject too many redirects")
}
}
// TestCreateHTTPClient_Fallback tests fallback when pool is exhausted
func TestCreateHTTPClient_Fallback(t *testing.T) {
// Override global pool with limited one
originalPool := globalTransportPool
defer func() {
globalTransportPool = originalPool
}()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
globalTransportPool = &TransportPool{
transports: make(map[string]*sharedTransport),
maxConns: 20,
ctx: ctx,
cancel: cancel,
clientCount: 10,
maxClients: 1, // Very low limit
}
config := DefaultTransportConfig()
timeout := 30 * time.Second
client := CreateHTTPClient(config, timeout)
if client == nil {
t.Error("CreateHTTPClient should not return nil even when pool is exhausted")
return
}
if client.Timeout != timeout {
t.Errorf("Client timeout should be %v, got %v", timeout, client.Timeout)
}
}
// TestTransportPool_ConcurrentAccess tests concurrent access to transport pool
func TestTransportPool_ConcurrentAccess(t *testing.T) {
pool := &TransportPool{
transports: make(map[string]*sharedTransport),
maxConns: 20,
maxClients: 50, // High limit for concurrent test
}
// Use different configs to reduce contention on single transport
baseConfig := DefaultTransportConfig()
configs := make([]TransportConfig, 10)
for i := range configs {
configs[i] = baseConfig
configs[i].MaxConnsPerHost = 5 + i // Make each config unique
}
var wg sync.WaitGroup
numGoroutines := 10
operationsPerGoroutine := 3
wg.Add(numGoroutines)
for i := 0; i < numGoroutines; i++ {
go func(goroutineID int) {
defer wg.Done()
config := configs[goroutineID%len(configs)]
for j := 0; j < operationsPerGoroutine; j++ {
transport := pool.GetTransport(config)
if transport == nil {
continue
}
// Use transport briefly
time.Sleep(time.Millisecond)
pool.ReleaseTransport(transport)
}
}(i)
}
wg.Wait()
// Should not panic and should have reasonable stats
stats := pool.GetStats()
if stats.TotalClients < 0 || stats.TotalClients > int32(numGoroutines) {
t.Errorf("Unexpected client count: %d", stats.TotalClients)
}
}
// Benchmark tests for performance verification
func BenchmarkTransportPool_GetTransport(b *testing.B) {
pool := &TransportPool{
transports: make(map[string]*sharedTransport),
maxConns: 20,
maxClients: 100,
}
config := DefaultTransportConfig()
b.ResetTimer()
for i := 0; i < b.N; i++ {
transport := pool.GetTransport(config)
pool.ReleaseTransport(transport)
}
}
func BenchmarkCreateHTTPClient(b *testing.B) {
config := DefaultTransportConfig()
timeout := 30 * time.Second
b.ResetTimer()
for i := 0; i < b.N; i++ {
CreateHTTPClient(config, timeout)
}
}
func BenchmarkTransportPool_configKey(b *testing.B) {
pool := &TransportPool{}
config := DefaultTransportConfig()
b.ResetTimer()
for i := 0; i < b.N; i++ {
pool.configKey(config)
}
}
+970
View File
@@ -0,0 +1,970 @@
package singleton
import (
"context"
"fmt"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
)
// TestGet_Singleton tests that Get() returns the same instance
func TestGet_Singleton(t *testing.T) {
registry1 := Get()
registry2 := Get()
if registry1 != registry2 {
t.Error("Get() should return the same instance (singleton)")
}
if registry1 == nil {
t.Error("Get() should not return nil")
}
}
// TestRegistry_Register tests singleton registration
func TestRegistry_Register(t *testing.T) {
registry := &Registry{
instances: make(map[string]*Instance),
groups: make(map[string]*Group),
}
initializer := func() interface{} {
return "test-value"
}
finalizer := func(v interface{}) {
// Mock finalizer
}
// Test successful registration
err := registry.Register("test-singleton", initializer, finalizer)
if err != nil {
t.Errorf("Register should succeed, got error: %v", err)
}
// Verify instance was registered
if len(registry.instances) != 1 {
t.Error("Instance should be registered")
}
instance := registry.instances["test-singleton"]
if instance == nil {
t.Error("Instance should not be nil")
return
}
if instance.name != "test-singleton" {
t.Errorf("Instance name should be 'test-singleton', got '%s'", instance.name)
}
if instance.initializer == nil {
t.Error("Instance should have initializer")
}
if instance.finalizer == nil {
t.Error("Instance should have finalizer")
}
}
// TestRegistry_Register_Duplicate tests duplicate registration
func TestRegistry_Register_Duplicate(t *testing.T) {
registry := &Registry{
instances: make(map[string]*Instance),
groups: make(map[string]*Group),
}
initializer := func() interface{} {
return "test-value"
}
// Register first time
err := registry.Register("test-singleton", initializer, nil)
if err != nil {
t.Errorf("First registration should succeed, got error: %v", err)
}
// Register again - should fail
err = registry.Register("test-singleton", initializer, nil)
if err == nil {
t.Error("Duplicate registration should fail")
}
if !strings.Contains(err.Error(), "already registered") {
t.Errorf("Error should mention already registered, got: %v", err)
}
}
// TestRegistry_Register_DuringShutdown tests registration during shutdown
func TestRegistry_Register_DuringShutdown(t *testing.T) {
registry := &Registry{
instances: make(map[string]*Instance),
groups: make(map[string]*Group),
shutdown: 1, // Already shutting down
}
initializer := func() interface{} {
return "test-value"
}
err := registry.Register("test-singleton", initializer, nil)
if err == nil {
t.Error("Registration during shutdown should fail")
}
if !strings.Contains(err.Error(), "shutting down") {
t.Errorf("Error should mention shutting down, got: %v", err)
}
}
// TestRegistry_GetInstance tests singleton retrieval and initialization
func TestRegistry_GetInstance(t *testing.T) {
registry := &Registry{
instances: make(map[string]*Instance),
groups: make(map[string]*Group),
}
callCount := int32(0)
testValue := "test-value"
initializer := func() interface{} {
atomic.AddInt32(&callCount, 1)
return testValue
}
// Register singleton
err := registry.Register("test-singleton", initializer, nil)
if err != nil {
t.Errorf("Register should succeed, got error: %v", err)
}
// First get - should initialize
value1, err := registry.GetInstance("test-singleton")
if err != nil {
t.Errorf("GetInstance should succeed, got error: %v", err)
}
if value1 != testValue {
t.Errorf("Value should be '%s', got '%v'", testValue, value1)
}
if atomic.LoadInt32(&callCount) != 1 {
t.Errorf("Initializer should be called once, called %d times", callCount)
}
// Second get - should return same instance without calling initializer
value2, err := registry.GetInstance("test-singleton")
if err != nil {
t.Errorf("GetInstance should succeed, got error: %v", err)
}
if value2 != testValue {
t.Errorf("Value should be '%s', got '%v'", testValue, value2)
}
if atomic.LoadInt32(&callCount) != 1 {
t.Errorf("Initializer should still be called only once, called %d times", callCount)
}
}
// TestRegistry_GetInstance_NotRegistered tests getting unregistered singleton
func TestRegistry_GetInstance_NotRegistered(t *testing.T) {
registry := &Registry{
instances: make(map[string]*Instance),
groups: make(map[string]*Group),
}
value, err := registry.GetInstance("non-existent")
if err == nil {
t.Error("GetInstance of non-existent singleton should fail")
}
if value != nil {
t.Error("Value should be nil for non-existent singleton")
}
if !strings.Contains(err.Error(), "not registered") {
t.Errorf("Error should mention not registered, got: %v", err)
}
}
// TestRegistry_GetInstance_DuringShutdown tests getting instance during shutdown
func TestRegistry_GetInstance_DuringShutdown(t *testing.T) {
registry := &Registry{
instances: make(map[string]*Instance),
groups: make(map[string]*Group),
shutdown: 1, // Already shutting down
}
value, err := registry.GetInstance("test-singleton")
if err == nil {
t.Error("GetInstance during shutdown should fail")
}
if value != nil {
t.Error("Value should be nil during shutdown")
}
if !strings.Contains(err.Error(), "shutting down") {
t.Errorf("Error should mention shutting down, got: %v", err)
}
}
// TestRegistry_MustGet tests MustGet method
func TestRegistry_MustGet(t *testing.T) {
registry := &Registry{
instances: make(map[string]*Instance),
groups: make(map[string]*Group),
}
testValue := "test-value"
initializer := func() interface{} {
return testValue
}
// Register singleton
err := registry.Register("test-singleton", initializer, nil)
if err != nil {
t.Errorf("Register should succeed, got error: %v", err)
}
// MustGet should succeed
value := registry.MustGet("test-singleton")
if value != testValue {
t.Errorf("Value should be '%s', got '%v'", testValue, value)
}
// MustGet non-existent should panic
defer func() {
if r := recover(); r == nil {
t.Error("MustGet of non-existent singleton should panic")
}
}()
registry.MustGet("non-existent")
}
// TestRegistry_RegisterGroup tests group registration
func TestRegistry_RegisterGroup(t *testing.T) {
registry := &Registry{
instances: make(map[string]*Instance),
groups: make(map[string]*Group),
}
// Test successful group registration
err := registry.RegisterGroup("test-group")
if err != nil {
t.Errorf("RegisterGroup should succeed, got error: %v", err)
}
// Verify group was registered
if len(registry.groups) != 1 {
t.Error("Group should be registered")
}
group := registry.groups["test-group"]
if group == nil {
t.Error("Group should not be nil")
return
}
if group.name != "test-group" {
t.Errorf("Group name should be 'test-group', got '%s'", group.name)
}
// Test duplicate group registration
err = registry.RegisterGroup("test-group")
if err == nil {
t.Error("Duplicate group registration should fail")
}
if !strings.Contains(err.Error(), "already exists") {
t.Errorf("Error should mention already exists, got: %v", err)
}
}
// TestRegistry_AddToGroup tests adding singletons to groups
func TestRegistry_AddToGroup(t *testing.T) {
registry := &Registry{
instances: make(map[string]*Instance),
groups: make(map[string]*Group),
}
// Register a singleton
initializer := func() interface{} {
return "test-value"
}
err := registry.Register("test-singleton", initializer, nil)
if err != nil {
t.Errorf("Register should succeed, got error: %v", err)
}
// Register a group
err = registry.RegisterGroup("test-group")
if err != nil {
t.Errorf("RegisterGroup should succeed, got error: %v", err)
}
// Add singleton to group
err = registry.AddToGroup("test-group", "test-singleton")
if err != nil {
t.Errorf("AddToGroup should succeed, got error: %v", err)
}
// Verify singleton is in group
group := registry.groups["test-group"]
if len(group.instances) != 1 {
t.Error("Group should contain one instance")
}
if group.instances["test-singleton"] == nil {
t.Error("Singleton should be in group")
}
// Test adding to non-existent group
err = registry.AddToGroup("non-existent-group", "test-singleton")
if err == nil {
t.Error("Adding to non-existent group should fail")
}
// Test adding non-existent singleton to group
err = registry.AddToGroup("test-group", "non-existent-singleton")
if err == nil {
t.Error("Adding non-existent singleton should fail")
}
}
// TestRegistry_GetGroup tests retrieving group instances
func TestRegistry_GetGroup(t *testing.T) {
registry := &Registry{
instances: make(map[string]*Instance),
groups: make(map[string]*Group),
}
// Register singletons
err := registry.Register("test-singleton-1", func() interface{} {
return "value-1"
}, nil)
if err != nil {
t.Errorf("Register should succeed, got error: %v", err)
}
err = registry.Register("test-singleton-2", func() interface{} {
return "value-2"
}, nil)
if err != nil {
t.Errorf("Register should succeed, got error: %v", err)
}
// Register group and add singletons
err = registry.RegisterGroup("test-group")
if err != nil {
t.Errorf("RegisterGroup should succeed, got error: %v", err)
}
err = registry.AddToGroup("test-group", "test-singleton-1")
if err != nil {
t.Errorf("AddToGroup should succeed, got error: %v", err)
}
err = registry.AddToGroup("test-group", "test-singleton-2")
if err != nil {
t.Errorf("AddToGroup should succeed, got error: %v", err)
}
// Initialize singletons
_, _ = registry.GetInstance("test-singleton-1")
_, _ = registry.GetInstance("test-singleton-2")
// Get group
groupInstances, err := registry.GetGroup("test-group")
if err != nil {
t.Errorf("GetGroup should succeed, got error: %v", err)
}
if len(groupInstances) != 2 {
t.Errorf("Group should contain 2 instances, got %d", len(groupInstances))
}
if groupInstances["test-singleton-1"] != "value-1" {
t.Error("Group should contain correct instance values")
}
if groupInstances["test-singleton-2"] != "value-2" {
t.Error("Group should contain correct instance values")
}
// Test getting non-existent group
_, err = registry.GetGroup("non-existent-group")
if err == nil {
t.Error("Getting non-existent group should fail")
}
}
// TestRegistry_ReferenceCountingv tests reference counting
func TestRegistry_ReferenceCountingv(t *testing.T) {
registry := &Registry{
instances: make(map[string]*Instance),
groups: make(map[string]*Group),
}
finalizerCalled := int32(0)
finalizer := func(v interface{}) {
atomic.AddInt32(&finalizerCalled, 1)
}
// Register singleton
err := registry.Register("test-singleton", func() interface{} {
return "test-value"
}, finalizer)
if err != nil {
t.Errorf("Register should succeed, got error: %v", err)
}
// Initialize singleton (this adds 1 reference)
_, err = registry.GetInstance("test-singleton")
if err != nil {
t.Errorf("GetInstance should succeed, got error: %v", err)
}
// Check initial reference count
count, err := registry.GetReferenceCount("test-singleton")
if err != nil {
t.Errorf("GetReferenceCount should succeed, got error: %v", err)
}
if count != 1 {
t.Errorf("Reference count should be 1, got %d", count)
}
// Add reference
err = registry.AddReference("test-singleton")
if err != nil {
t.Errorf("AddReference should succeed, got error: %v", err)
}
count, _ = registry.GetReferenceCount("test-singleton")
if count != 2 {
t.Errorf("Reference count should be 2, got %d", count)
}
// Release reference
err = registry.ReleaseReference("test-singleton")
if err != nil {
t.Errorf("ReleaseReference should succeed, got error: %v", err)
}
count, _ = registry.GetReferenceCount("test-singleton")
if count != 1 {
t.Errorf("Reference count should be 1, got %d", count)
}
// Release last reference - should trigger finalizer
err = registry.ReleaseReference("test-singleton")
if err != nil {
t.Errorf("ReleaseReference should succeed, got error: %v", err)
}
count, _ = registry.GetReferenceCount("test-singleton")
if count != 0 {
t.Errorf("Reference count should be 0, got %d", count)
}
// Wait for finalizer to run (it runs in goroutine)
time.Sleep(10 * time.Millisecond)
if atomic.LoadInt32(&finalizerCalled) != 1 {
t.Errorf("Finalizer should be called once, called %d times", finalizerCalled)
}
// Test reference operations on non-existent singleton
err = registry.AddReference("non-existent")
if err == nil {
t.Error("AddReference on non-existent singleton should fail")
}
err = registry.ReleaseReference("non-existent")
if err == nil {
t.Error("ReleaseReference on non-existent singleton should fail")
}
_, err = registry.GetReferenceCount("non-existent")
if err == nil {
t.Error("GetReferenceCount on non-existent singleton should fail")
}
}
// TestRegistry_Shutdown tests graceful shutdown
func TestRegistry_Shutdown(t *testing.T) {
registry := &Registry{
instances: make(map[string]*Instance),
groups: make(map[string]*Group),
}
finalizerCalled := int32(0)
finalizer := func(v interface{}) {
atomic.AddInt32(&finalizerCalled, 1)
}
// Register and initialize singletons
err := registry.Register("test-singleton-1", func() interface{} {
return "value-1"
}, finalizer)
if err != nil {
t.Errorf("Register should succeed, got error: %v", err)
}
err = registry.Register("test-singleton-2", func() interface{} {
return "value-2"
}, finalizer)
if err != nil {
t.Errorf("Register should succeed, got error: %v", err)
}
// Initialize singletons
_, _ = registry.GetInstance("test-singleton-1")
_, _ = registry.GetInstance("test-singleton-2")
// Shutdown
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err = registry.Shutdown(ctx)
if err != nil {
t.Errorf("Shutdown should succeed, got error: %v", err)
}
// Verify finalizers were called
if atomic.LoadInt32(&finalizerCalled) != 2 {
t.Errorf("Finalizers should be called 2 times, called %d times", finalizerCalled)
}
// Verify registry is cleared
if len(registry.instances) != 0 {
t.Error("Instances should be cleared after shutdown")
}
if len(registry.groups) != 0 {
t.Error("Groups should be cleared after shutdown")
}
// Verify shutdown flag is set
if atomic.LoadInt32(&registry.shutdown) != 1 {
t.Error("Shutdown flag should be set")
}
// Test double shutdown
err = registry.Shutdown(ctx)
if err == nil {
t.Error("Double shutdown should fail")
}
}
// TestRegistry_Shutdown_Timeout tests shutdown timeout
func TestRegistry_Shutdown_Timeout(t *testing.T) {
registry := &Registry{
instances: make(map[string]*Instance),
groups: make(map[string]*Group),
}
// Register singleton with slow finalizer
slowFinalizer := func(v interface{}) {
time.Sleep(100 * time.Millisecond)
}
err := registry.Register("slow-singleton", func() interface{} {
return "value"
}, slowFinalizer)
if err != nil {
t.Errorf("Register should succeed, got error: %v", err)
}
// Initialize singleton
_, _ = registry.GetInstance("slow-singleton")
// Shutdown with short timeout
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
defer cancel()
err = registry.Shutdown(ctx)
if err == nil {
t.Error("Shutdown should timeout")
}
if !strings.Contains(err.Error(), "timeout") {
t.Errorf("Error should mention timeout, got: %v", err)
}
}
// TestRegistry_Shutdown_PanicRecovery tests panic recovery during shutdown
func TestRegistry_Shutdown_PanicRecovery(t *testing.T) {
registry := &Registry{
instances: make(map[string]*Instance),
groups: make(map[string]*Group),
}
// Register singleton with panicking finalizer
panicFinalizer := func(v interface{}) {
panic("finalizer panic")
}
err := registry.Register("panic-singleton", func() interface{} {
return "value"
}, panicFinalizer)
if err != nil {
t.Errorf("Register should succeed, got error: %v", err)
}
// Initialize singleton
_, _ = registry.GetInstance("panic-singleton")
// Shutdown should handle panic
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err = registry.Shutdown(ctx)
if err == nil {
t.Error("Shutdown should report finalizer panic")
}
if !strings.Contains(err.Error(), "panicked") {
t.Errorf("Error should mention panic, got: %v", err)
}
}
// TestRegistry_Reset tests registry reset
func TestRegistry_Reset(t *testing.T) {
registry := &Registry{
instances: make(map[string]*Instance),
groups: make(map[string]*Group),
shutdown: 1,
}
// Add some data
registry.instances["test"] = &Instance{}
registry.groups["test"] = &Group{}
// Reset
registry.Reset()
// Verify everything is cleared
if len(registry.instances) != 0 {
t.Error("Instances should be cleared after reset")
}
if len(registry.groups) != 0 {
t.Error("Groups should be cleared after reset")
}
if atomic.LoadInt32(&registry.shutdown) != 0 {
t.Error("Shutdown flag should be cleared after reset")
}
}
// TestRegistry_GetStats tests statistics
func TestRegistry_GetStats(t *testing.T) {
registry := &Registry{
instances: make(map[string]*Instance),
groups: make(map[string]*Group),
}
// Register singletons
err := registry.Register("test-singleton-1", func() interface{} {
return "value-1"
}, nil)
if err != nil {
t.Errorf("Register should succeed, got error: %v", err)
}
err = registry.Register("test-singleton-2", func() interface{} {
return "value-2"
}, nil)
if err != nil {
t.Errorf("Register should succeed, got error: %v", err)
}
// Register group
err = registry.RegisterGroup("test-group")
if err != nil {
t.Errorf("RegisterGroup should succeed, got error: %v", err)
}
// Initialize one singleton
_, _ = registry.GetInstance("test-singleton-1")
// Add reference
_ = registry.AddReference("test-singleton-1")
// Get stats
stats := registry.GetStats()
if stats.TotalRegistered != 2 {
t.Errorf("TotalRegistered should be 2, got %d", stats.TotalRegistered)
}
if stats.TotalInitialized != 1 {
t.Errorf("TotalInitialized should be 1, got %d", stats.TotalInitialized)
}
if stats.TotalGroups != 1 {
t.Errorf("TotalGroups should be 1, got %d", stats.TotalGroups)
}
if stats.TotalReferences != 2 { // 1 from initialization + 1 from AddReference
t.Errorf("TotalReferences should be 2, got %d", stats.TotalReferences)
}
}
// TestBuilder tests the fluent builder interface
func TestBuilder(t *testing.T) {
// Reset global registry for clean test
Get().Reset()
testValue := "builder-test-value"
initializer := func() interface{} {
return testValue
}
finalizer := func(v interface{}) {
// Mock finalizer for builder test
}
// Test builder
err := NewBuilder("builder-singleton").
WithInitializer(initializer).
WithFinalizer(finalizer).
InGroup("builder-group").
Register()
if err != nil {
t.Errorf("Builder registration should succeed, got error: %v", err)
}
// Verify singleton was registered
value, err := Get().GetInstance("builder-singleton")
if err != nil {
t.Errorf("GetInstance should succeed, got error: %v", err)
}
if value != testValue {
t.Errorf("Value should be '%s', got '%v'", testValue, value)
}
// Verify group was created and singleton added
groupInstances, err := Get().GetGroup("builder-group")
if err != nil {
t.Errorf("GetGroup should succeed, got error: %v", err)
}
if len(groupInstances) != 1 {
t.Errorf("Group should contain 1 instance, got %d", len(groupInstances))
}
if groupInstances["builder-singleton"] != testValue {
t.Error("Group should contain correct instance")
}
}
// TestBuilder_WithoutGroup tests builder without group
func TestBuilder_WithoutGroup(t *testing.T) {
registry := &Registry{
instances: make(map[string]*Instance),
groups: make(map[string]*Group),
}
builder := &Builder{
registry: registry,
name: "no-group-singleton",
}
err := builder.WithInitializer(func() interface{} {
return "value"
}).Register()
if err != nil {
t.Errorf("Registration without group should succeed, got error: %v", err)
}
// Verify singleton was registered
if len(registry.instances) != 1 {
t.Error("Singleton should be registered")
}
}
// TestContainsHelper tests the helper string contains function
func TestContainsHelper(t *testing.T) {
tests := []struct {
s string
substr string
expect bool
}{
{"hello world", "world", true},
{"hello world", "hello", true},
{"hello world", "lo wo", true},
{"hello world", "xyz", false},
{"hello", "hello world", false},
{"", "test", false},
{"test", "", true},
{"", "", true},
}
for _, test := range tests {
result := contains(test.s, test.substr)
if result != test.expect {
t.Errorf("contains(%q, %q) = %v, want %v", test.s, test.substr, result, test.expect)
}
}
}
// TestRegistry_ConcurrentAccess tests concurrent access to registry
func TestRegistry_ConcurrentAccess(t *testing.T) {
registry := &Registry{
instances: make(map[string]*Instance),
groups: make(map[string]*Group),
}
callCount := int32(0)
initializer := func() interface{} {
atomic.AddInt32(&callCount, 1)
return "concurrent-value"
}
// Register singleton
err := registry.Register("concurrent-singleton", initializer, nil)
if err != nil {
t.Errorf("Register should succeed, got error: %v", err)
}
var wg sync.WaitGroup
numGoroutines := 50
// Concurrent access
wg.Add(numGoroutines)
for i := 0; i < numGoroutines; i++ {
go func() {
defer wg.Done()
value, err := registry.GetInstance("concurrent-singleton")
if err != nil {
t.Errorf("GetInstance should succeed, got error: %v", err)
return
}
if value != "concurrent-value" {
t.Errorf("Value should be 'concurrent-value', got '%v'", value)
}
}()
}
wg.Wait()
// Initializer should be called only once despite concurrent access
if atomic.LoadInt32(&callCount) != 1 {
t.Errorf("Initializer should be called only once, called %d times", callCount)
}
}
// TestRegistry_ConcurrentReferenceOperations tests concurrent reference operations
func TestRegistry_ConcurrentReferenceOperations(t *testing.T) {
registry := &Registry{
instances: make(map[string]*Instance),
groups: make(map[string]*Group),
}
// Register singleton
err := registry.Register("ref-singleton", func() interface{} {
return "ref-value"
}, nil)
if err != nil {
t.Errorf("Register should succeed, got error: %v", err)
}
// Initialize singleton
_, _ = registry.GetInstance("ref-singleton")
var wg sync.WaitGroup
numGoroutines := 20
// Concurrent reference operations
wg.Add(numGoroutines * 2)
for i := 0; i < numGoroutines; i++ {
go func() {
defer wg.Done()
_ = registry.AddReference("ref-singleton")
}()
go func() {
defer wg.Done()
_ = registry.ReleaseReference("ref-singleton")
}()
}
wg.Wait()
// Reference count should be consistent (initial 1 + net operations)
count, err := registry.GetReferenceCount("ref-singleton")
if err != nil {
t.Errorf("GetReferenceCount should succeed, got error: %v", err)
}
// Count should be >= 0 due to balanced add/release operations
if count < 0 {
t.Errorf("Reference count should not be negative, got %d", count)
}
}
// Benchmark tests for performance verification
func BenchmarkRegistry_GetInstance(b *testing.B) {
registry := &Registry{
instances: make(map[string]*Instance),
groups: make(map[string]*Group),
}
registry.Register("benchmark-singleton", func() interface{} {
return "benchmark-value"
}, nil)
b.ResetTimer()
for i := 0; i < b.N; i++ {
registry.GetInstance("benchmark-singleton")
}
}
func BenchmarkRegistry_ConcurrentGetInstance(b *testing.B) {
registry := &Registry{
instances: make(map[string]*Instance),
groups: make(map[string]*Group),
}
registry.Register("concurrent-benchmark", func() interface{} {
return "concurrent-value"
}, nil)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
registry.GetInstance("concurrent-benchmark")
}
})
}
func BenchmarkBuilder_Register(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
registry := &Registry{
instances: make(map[string]*Instance),
groups: make(map[string]*Group),
}
builder := &Builder{
registry: registry,
name: fmt.Sprintf("benchmark-%d", i),
}
builder.WithInitializer(func() interface{} {
return "value"
}).Register()
}
}