package engine import ( "context" "fmt" "sync" "testing" ) func TestGenerateCaching(t *testing.T) { generator, err := NewDefaultGenerator() if err != nil { t.Fatalf("NewDefaultGenerator failed: %v", err) } hash := "abcdef123456789" size := 64.0 // Generate icon first time icon1, err := generator.Generate(context.Background(), hash, size) if err != nil { t.Fatalf("First generate failed: %v", err) } // Check cache size if generator.GetCacheSize() != 1 { t.Errorf("Expected cache size 1, got %d", generator.GetCacheSize()) } // Generate same icon again icon2, err := generator.Generate(context.Background(), hash, size) if err != nil { t.Fatalf("Second generate failed: %v", err) } // Should be the same instance from cache if icon1 != icon2 { t.Error("Second generate did not return cached instance") } // Cache size should still be 1 if generator.GetCacheSize() != 1 { t.Errorf("Expected cache size 1 after second generate, got %d", generator.GetCacheSize()) } } func TestClearCache(t *testing.T) { generator, err := NewDefaultGenerator() if err != nil { t.Fatalf("NewDefaultGenerator failed: %v", err) } hash := "abcdef123456789" size := 64.0 // Generate an icon to populate cache _, err = generator.Generate(context.Background(), hash, size) if err != nil { t.Fatalf("Generate failed: %v", err) } // Verify cache has content if generator.GetCacheSize() == 0 { t.Error("Cache should not be empty after generate") } // Clear cache generator.ClearCache() // Verify cache is empty if generator.GetCacheSize() != 0 { t.Errorf("Expected cache size 0 after clear, got %d", generator.GetCacheSize()) } } func TestSetConfig(t *testing.T) { generator, err := NewDefaultGenerator() if err != nil { t.Fatalf("NewDefaultGenerator failed: %v", err) } hash := "abcdef123456789" size := 64.0 // Generate an icon to populate cache _, err = generator.Generate(context.Background(), hash, size) if err != nil { t.Fatalf("Generate failed: %v", err) } // Verify cache has content if generator.GetCacheSize() == 0 { t.Error("Cache should not be empty after generate") } // Set new config newConfig := DefaultColorConfig() newConfig.IconPadding = 0.1 generator.SetConfig(newConfig) // Verify config was updated if generator.GetConfig().IconPadding != 0.1 { t.Errorf("Expected icon padding 0.1, got %f", generator.GetConfig().IconPadding) } // Verify cache was cleared if generator.GetCacheSize() != 0 { t.Errorf("Expected cache size 0 after config change, got %d", generator.GetCacheSize()) } } func TestLRUCacheEviction(t *testing.T) { // Create generator with small cache for testing eviction config := GeneratorConfig{ ColorConfig: DefaultColorConfig(), CacheSize: 2, // Small cache to test eviction } generator, err := NewGeneratorWithConfig(config) if err != nil { t.Fatalf("NewGeneratorWithConfig failed: %v", err) } // Generate 3 different icons to trigger eviction hashes := []string{ "abcdef1234567890abcdef1234567890abcdef12", "123456789abcdef0123456789abcdef0123456789", "fedcba0987654321fedcba0987654321fedcba09", } size := 64.0 icons := make([]*Icon, len(hashes)) for i, hash := range hashes { var icon *Icon icon, err = generator.Generate(context.Background(), hash, size) if err != nil { t.Fatalf("Generate failed for hash %s: %v", hash, err) } icons[i] = icon } // Cache should only contain 2 items (the last 2) if generator.GetCacheSize() != 2 { t.Errorf("Expected cache size 2 after eviction, got %d", generator.GetCacheSize()) } // The first icon should have been evicted, so generating it again should create a new instance icon1Again, err := generator.Generate(context.Background(), hashes[0], size) if err != nil { t.Fatalf("Generate failed for evicted hash: %v", err) } // This should be a different instance since the first was evicted if icons[0] == icon1Again { t.Error("First icon was not evicted from cache as expected") } // The last icon should still be cached icon3Again, err := generator.Generate(context.Background(), hashes[2], size) if err != nil { t.Fatalf("Generate failed for cached hash: %v", err) } // This should be the same instance if icons[2] != icon3Again { t.Error("Last icon was evicted from cache unexpectedly") } } func TestCacheMetrics(t *testing.T) { generator, err := NewDefaultGenerator() if err != nil { t.Fatalf("NewDefaultGenerator failed: %v", err) } // Initially, metrics should be zero hits, misses := generator.GetCacheMetrics() if hits != 0 || misses != 0 { t.Errorf("Expected initial metrics (0, 0), got (%d, %d)", hits, misses) } hash := "abcdef1234567890abcdef1234567890abcdef12" size := 64.0 // First generation should be a cache miss _, err = generator.Generate(context.Background(), hash, size) if err != nil { t.Fatalf("Generate failed: %v", err) } hits, misses = generator.GetCacheMetrics() if hits != 0 || misses != 1 { t.Errorf("Expected metrics (0, 1) after first generate, got (%d, %d)", hits, misses) } // Second generation should be a cache hit _, err = generator.Generate(context.Background(), hash, size) if err != nil { t.Fatalf("Second generate failed: %v", err) } hits, misses = generator.GetCacheMetrics() if hits != 1 || misses != 1 { t.Errorf("Expected metrics (1, 1) after cache hit, got (%d, %d)", hits, misses) } // Generate different icon should be another miss _, err = generator.Generate(context.Background(), "1234567890abcdef1234567890abcdef12345678", size) if err != nil { t.Fatalf("Generate with different hash failed: %v", err) } hits, misses = generator.GetCacheMetrics() if hits != 1 || misses != 2 { t.Errorf("Expected metrics (1, 2) after different hash, got (%d, %d)", hits, misses) } // Clear cache should reset metrics generator.ClearCache() hits, misses = generator.GetCacheMetrics() if hits != 0 || misses != 0 { t.Errorf("Expected metrics (0, 0) after cache clear, got (%d, %d)", hits, misses) } } func TestSetGeneratorConfig(t *testing.T) { generator, err := NewDefaultGenerator() if err != nil { t.Fatalf("NewDefaultGenerator failed: %v", err) } // Generate an icon to populate cache hash := "abcdef1234567890abcdef1234567890abcdef12" _, err = generator.Generate(context.Background(), hash, 64.0) if err != nil { t.Fatalf("Generate failed: %v", err) } // Verify cache has content if generator.GetCacheSize() == 0 { t.Error("Cache should not be empty after generate") } // Update configuration with different cache size newConfig := GeneratorConfig{ ColorConfig: DefaultColorConfig(), CacheSize: 500, } newConfig.ColorConfig.IconPadding = 0.15 err = generator.SetGeneratorConfig(newConfig) if err != nil { t.Fatalf("SetGeneratorConfig failed: %v", err) } // Verify configuration was updated currentConfig := generator.GetGeneratorConfig() if currentConfig.CacheSize != 500 { t.Errorf("Expected cache size 500, got %d", currentConfig.CacheSize) } if currentConfig.ColorConfig.IconPadding != 0.15 { t.Errorf("Expected icon padding 0.15, got %f", currentConfig.ColorConfig.IconPadding) } // Verify cache was cleared due to config change if generator.GetCacheSize() != 0 { t.Errorf("Expected cache size 0 after config change, got %d", generator.GetCacheSize()) } // Verify cache capacity is updated if generator.GetCacheCapacity() != 500 { t.Errorf("Expected cache capacity 500, got %d", generator.GetCacheCapacity()) } } func TestSetGeneratorConfigSameSize(t *testing.T) { config := GeneratorConfig{ ColorConfig: DefaultColorConfig(), CacheSize: 1000, // Same as default } generator, err := NewGeneratorWithConfig(config) if err != nil { t.Fatalf("NewGeneratorWithConfig failed: %v", err) } // Generate an icon to populate cache hash := "abcdef1234567890abcdef1234567890abcdef12" _, err = generator.Generate(context.Background(), hash, 64.0) if err != nil { t.Fatalf("Generate failed: %v", err) } // Update configuration with same cache size but different color config newConfig := config newConfig.ColorConfig.IconPadding = 0.2 err = generator.SetGeneratorConfig(newConfig) if err != nil { t.Fatalf("SetGeneratorConfig failed: %v", err) } // Cache should be cleared even with same cache size if generator.GetCacheSize() != 0 { t.Errorf("Expected cache size 0 after config change, got %d", generator.GetCacheSize()) } } func TestSetGeneratorConfigInvalidCacheSize(t *testing.T) { generator, err := NewDefaultGenerator() if err != nil { t.Fatalf("NewDefaultGenerator failed: %v", err) } tests := []struct { name string cacheSize int }{ {"Zero cache size", 0}, {"Negative cache size", -1}, {"Very negative cache size", -100}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { config := GeneratorConfig{ ColorConfig: DefaultColorConfig(), CacheSize: test.cacheSize, } err := generator.SetGeneratorConfig(config) if err == nil { t.Errorf("Expected error for cache size %d, but got none", test.cacheSize) } }) } } func TestConcurrentCacheAccess(t *testing.T) { generator, err := NewDefaultGenerator() if err != nil { t.Fatalf("NewDefaultGenerator failed: %v", err) } const numGoroutines = 10 const numGenerations = 5 hash := "abcdef1234567890abcdef1234567890abcdef12" size := 64.0 var wg sync.WaitGroup wg.Add(numGoroutines) // Launch multiple goroutines that generate the same icon concurrently for i := 0; i < numGoroutines; i++ { go func() { defer wg.Done() for j := 0; j < numGenerations; j++ { _, err := generator.Generate(context.Background(), hash, size) if err != nil { t.Errorf("Concurrent generate failed: %v", err) return } } }() } wg.Wait() // Cache should only contain one item since all goroutines generated the same icon if generator.GetCacheSize() != 1 { t.Errorf("Expected cache size 1 after concurrent access, got %d", generator.GetCacheSize()) } // Total cache operations should be recorded correctly (allow tolerance for singleflight deduplication) hits, misses := generator.GetCacheMetrics() totalOperations := hits + misses expectedOperations := int64(numGoroutines * numGenerations) // Singleflight can significantly reduce counted operations when many goroutines // request the same key simultaneously - they share the result from one generation. // Allow for up to 50% reduction due to deduplication in highly concurrent scenarios. minExpected := expectedOperations / 2 if totalOperations < minExpected || totalOperations > expectedOperations { t.Errorf("Expected %d-%d total cache operations, got %d (hits: %d, misses: %d)", minExpected, expectedOperations, totalOperations, hits, misses) } // There should be at least one miss (the first generation) if misses < 1 { t.Errorf("Expected at least 1 cache miss, got %d", misses) } } func BenchmarkCacheKey(b *testing.B) { generator, err := NewDefaultGenerator() if err != nil { b.Fatalf("NewDefaultGenerator failed: %v", err) } hash := "abcdef1234567890abcdef1234567890abcdef12" size := 64.0 b.ResetTimer() for i := 0; i < b.N; i++ { _ = generator.cacheKey(hash, size) } } func BenchmarkLRUCacheHit(b *testing.B) { generator, err := NewDefaultGenerator() if err != nil { b.Fatalf("NewDefaultGenerator failed: %v", err) } hash := "abcdef1234567890abcdef1234567890abcdef12" size := 64.0 // Pre-populate cache _, err = generator.Generate(context.Background(), hash, size) if err != nil { b.Fatalf("Pre-populate failed: %v", err) } b.ResetTimer() for i := 0; i < b.N; i++ { _, err := generator.Generate(context.Background(), hash, size) if err != nil { b.Fatalf("Generate failed: %v", err) } } } func BenchmarkLRUCacheMiss(b *testing.B) { generator, err := NewDefaultGenerator() if err != nil { b.Fatalf("NewDefaultGenerator failed: %v", err) } size := 64.0 b.ResetTimer() for i := 0; i < b.N; i++ { // Use different hash each time to ensure cache miss hash := fmt.Sprintf("%040x", i) _, err := generator.GenerateWithoutCache(context.Background(), hash, size) if err != nil { b.Fatalf("Generate failed: %v", err) } } }