package engine import ( "context" "fmt" "sync" "testing" "time" ) func TestGenerateValidHash(t *testing.T) { generator, err := NewDefaultGenerator() if err != nil { t.Fatalf("NewDefaultGenerator failed: %v", err) } hash := "abcdef123456789" size := 64.0 icon, err := generator.Generate(context.Background(), hash, size) if err != nil { t.Fatalf("Generate failed with error: %v", err) } if icon == nil { t.Fatal("Generate returned nil icon") } if icon.Hash != hash { t.Errorf("Expected hash %s, got %s", hash, icon.Hash) } if icon.Size != size { t.Errorf("Expected size %f, got %f", size, icon.Size) } if len(icon.Shapes) == 0 { t.Error("Generated icon has no shapes") } } func TestGenerateInvalidInputs(t *testing.T) { generator, err := NewDefaultGenerator() if err != nil { t.Fatalf("NewDefaultGenerator failed: %v", err) } tests := []struct { name string hash string size float64 }{ { name: "Empty hash", hash: "", size: 64.0, }, { name: "Zero size", hash: "abcdef1234567890", size: 0.0, }, { name: "Negative size", hash: "abcdef1234567890", size: -10.0, }, { name: "Invalid hash format", hash: "invalid_hash_format", size: 64.0, }, { name: "Hash too short", hash: "abc", size: 64.0, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { _, err := generator.Generate(context.Background(), test.hash, test.size) if err == nil { t.Errorf("Expected error for %s, but got none", test.name) } }) } } func TestGenerateWithoutCache(t *testing.T) { generator, err := NewDefaultGenerator() if err != nil { t.Fatalf("NewDefaultGenerator failed: %v", err) } hash := "abcdef1234567890abcdef1234567890abcdef12" size := 64.0 // Generate without cache icon1, err := generator.GenerateWithoutCache(context.Background(), hash, size) if err != nil { t.Fatalf("GenerateWithoutCache failed: %v", err) } // Generate again without cache - should be different instances icon2, err := generator.GenerateWithoutCache(context.Background(), hash, size) if err != nil { t.Fatalf("Second GenerateWithoutCache failed: %v", err) } // Should be different instances if icon1 == icon2 { t.Error("GenerateWithoutCache returned same instance - should be different") } // But should have same content if icon1.Hash != icon2.Hash { t.Error("Icons have different hashes") } if icon1.Size != icon2.Size { t.Error("Icons have different sizes") } // Cache should remain empty if generator.GetCacheSize() != 0 { t.Errorf("Expected cache size 0 after GenerateWithoutCache, got %d", generator.GetCacheSize()) } } func TestGenerateWithCancellation(t *testing.T) { generator, err := NewDefaultGenerator() if err != nil { t.Fatalf("NewDefaultGenerator failed: %v", err) } hash := "abcdef1234567890abcdef1234567890abcdef12" size := 64.0 // Create canceled context ctx, cancel := context.WithCancel(context.Background()) cancel() // Cancel immediately _, err = generator.Generate(ctx, hash, size) if err == nil { t.Error("Expected error for canceled context, but got none") } if err != context.Canceled { t.Errorf("Expected context.Canceled error, got %v", err) } } func TestGenerateWithTimeout(t *testing.T) { generator, err := NewDefaultGenerator() if err != nil { t.Fatalf("NewDefaultGenerator failed: %v", err) } hash := "abcdef1234567890abcdef1234567890abcdef12" size := 64.0 // Create context with very short timeout ctx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond) defer cancel() // Sleep to ensure timeout time.Sleep(1 * time.Millisecond) _, err = generator.Generate(ctx, hash, size) if err == nil { t.Error("Expected timeout error, but got none") } if err != context.DeadlineExceeded { t.Errorf("Expected context.DeadlineExceeded error, got %v", err) } } func TestConcurrentGenerate(t *testing.T) { generator, err := NewDefaultGenerator() if err != nil { t.Fatalf("NewDefaultGenerator failed: %v", err) } hash := "abcdef1234567890abcdef1234567890abcdef12" size := 64.0 const numGoroutines = 20 icons := make([]*Icon, numGoroutines) errors := make([]error, numGoroutines) var wg sync.WaitGroup wg.Add(numGoroutines) // Start multiple goroutines that generate the same icon concurrently for i := 0; i < numGoroutines; i++ { go func(index int) { defer wg.Done() icon, genErr := generator.Generate(context.Background(), hash, size) icons[index] = icon errors[index] = genErr }(i) } wg.Wait() // Check that all generations succeeded for i, err := range errors { if err != nil { t.Errorf("Goroutine %d failed: %v", i, err) } } // All icons should be identical (same instance due to singleflight) firstIcon := icons[0] for i, icon := range icons[1:] { if icon != firstIcon { t.Errorf("Icon %d is different instance from first icon", i+1) } } // Cache should contain exactly one item if generator.GetCacheSize() != 1 { t.Errorf("Expected cache size 1, got %d", generator.GetCacheSize()) } // Should have exactly one cache miss (the actual generation) // Note: With singleflight, concurrent requests share the result directly from singleflight, // not from the cache. Cache hits only occur for requests that arrive AFTER the initial // generation completes. So we only verify the miss count is 1. _, misses := generator.GetCacheMetrics() if misses != 1 { t.Errorf("Expected exactly 1 cache miss due to singleflight, got %d", misses) } // Verify subsequent requests DO get cache hits _, err = generator.Generate(context.Background(), hash, size) if err != nil { t.Fatalf("Subsequent Generate failed: %v", err) } hits, _ := generator.GetCacheMetrics() if hits == 0 { t.Error("Expected cache hit for subsequent request, got none") } } func TestConcurrentGenerateDifferentHashes(t *testing.T) { generator, err := NewDefaultGenerator() if err != nil { t.Fatalf("NewDefaultGenerator failed: %v", err) } const numGoroutines = 10 size := 64.0 var wg sync.WaitGroup wg.Add(numGoroutines) icons := make([]*Icon, numGoroutines) errors := make([]error, numGoroutines) // Start multiple goroutines that generate different icons concurrently for i := 0; i < numGoroutines; i++ { go func(index int) { defer wg.Done() hash := fmt.Sprintf("%032x", index) icon, err := generator.Generate(context.Background(), hash, size) icons[index] = icon errors[index] = err }(i) } wg.Wait() // Check that all generations succeeded for i, err := range errors { if err != nil { t.Errorf("Goroutine %d failed: %v", i, err) } } // All icons should be different instances for i := 0; i < numGoroutines; i++ { for j := i + 1; j < numGoroutines; j++ { if icons[i] == icons[j] { t.Errorf("Icons %d and %d are the same instance - should be different", i, j) } } } // Cache should contain all generated icons if generator.GetCacheSize() != numGoroutines { t.Errorf("Expected cache size %d, got %d", numGoroutines, generator.GetCacheSize()) } // Should have exactly numGoroutines cache misses and no hits hits, misses := generator.GetCacheMetrics() if misses != int64(numGoroutines) { t.Errorf("Expected %d cache misses, got %d", numGoroutines, misses) } if hits != 0 { t.Errorf("Expected 0 cache hits, got %d", hits) } } func TestSingleflightDeduplication(t *testing.T) { generator, err := NewDefaultGenerator() if err != nil { t.Fatalf("NewDefaultGenerator failed: %v", err) } hash := "abcdef1234567890abcdef1234567890abcdef12" size := 64.0 const numGoroutines = 50 // Use a channel to coordinate goroutine starts start := make(chan struct{}) icons := make([]*Icon, numGoroutines) errors := make([]error, numGoroutines) var wg sync.WaitGroup wg.Add(numGoroutines) // Start all goroutines and have them wait for the signal for i := 0; i < numGoroutines; i++ { go func(index int) { defer wg.Done() <-start // Wait for start signal icon, genErr := generator.Generate(context.Background(), hash, size) icons[index] = icon errors[index] = genErr }(i) } // Signal all goroutines to start at once close(start) wg.Wait() // Check that all generations succeeded for i, err := range errors { if err != nil { t.Errorf("Goroutine %d failed: %v", i, err) } } // All icons should be the exact same instance due to singleflight firstIcon := icons[0] for i, icon := range icons[1:] { if icon != firstIcon { t.Errorf("Icon %d is different instance - singleflight deduplication failed", i+1) } } // Should have exactly one cache miss due to singleflight deduplication // Note: Singleflight shares results directly with waiting goroutines, so they don't // hit the cache. Cache hits only occur for requests that arrive AFTER generation completes. _, misses := generator.GetCacheMetrics() if misses != 1 { t.Errorf("Expected exactly 1 cache miss due to singleflight deduplication, got %d", misses) } // Verify subsequent requests DO get cache hits _, err = generator.Generate(context.Background(), hash, size) if err != nil { t.Fatalf("Subsequent Generate failed: %v", err) } hits, _ := generator.GetCacheMetrics() if hits == 0 { t.Error("Expected cache hit for subsequent request, got none") } } func BenchmarkGenerate(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++ { _, err := generator.Generate(context.Background(), hash, size) if err != nil { b.Fatalf("Generate failed: %v", err) } } } func BenchmarkGenerateWithCache(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) } } }