- Core library with SVG and PNG generation - CLI tool with generate and batch commands - Cross-platform path handling for Windows compatibility - Comprehensive test suite with integration tests
450 lines
12 KiB
Go
450 lines
12 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|