package engine import ( "context" "math" "testing" "github.com/ungluedlabs/go-jdenticon/internal/util" ) // FuzzGeneratorGenerate tests the internal engine generator with arbitrary inputs func FuzzGeneratorGenerate(f *testing.F) { // Seed with known hash patterns and sizes f.Add("abcdef1234567890", 64.0) f.Add("", 32.0) f.Add("0123456789abcdef", 128.0) f.Add("ffffffffffffffff", 256.0) f.Add("0000000000000000", 1.0) f.Fuzz(func(t *testing.T, hash string, size float64) { // Test invalid sizes for proper error handling if size <= 0 || math.IsNaN(size) || math.IsInf(size, 0) { // Create a generator with default config config := GeneratorConfig{ ColorConfig: DefaultColorConfig(), CacheSize: 100, } generator, err := NewGeneratorWithConfig(config) if err != nil { t.Fatalf("Failed to create generator: %v", err) return } _, err = generator.Generate(context.Background(), hash, size) if err == nil { t.Errorf("Generate with invalid size %f should have returned an error", size) } return // Stop further processing for invalid inputs } if size > 10000 { return // Avoid resource exhaustion } // Create a generator with default config config := GeneratorConfig{ ColorConfig: DefaultColorConfig(), CacheSize: 100, } generator, err := NewGeneratorWithConfig(config) if err != nil { t.Fatalf("Failed to create generator: %v", err) return } // Generate should never panic, regardless of hash input icon, err := generator.Generate(context.Background(), hash, size) // We don't require success for all inputs, but we require no crashes if err != nil { // Check that error is reasonable _ = err return } if icon == nil { t.Errorf("Generate(%q, %f) returned nil icon without error", hash, size) return } // Verify icon has reasonable properties if icon.Size != size { t.Errorf("Generated icon size %f does not match requested size %f", icon.Size, size) } if len(icon.Shapes) == 0 { t.Errorf("Generated icon has no shapes") } }) } // FuzzColorConfigValidation tests color configuration validation func FuzzColorConfigValidation(f *testing.F) { // Seed with various color configuration patterns f.Add(0.5, 0.5, 0.4, 0.8, 0.3, 0.9, 0.08) f.Add(-1.0, 2.0, -0.5, 1.5, -0.1, 1.1, -0.1) f.Add(0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0) f.Fuzz(func(t *testing.T, colorSat, grayscaleSat, colorLightMin, colorLightMax, grayscaleLightMin, grayscaleLightMax, padding float64) { config := ColorConfig{ ColorSaturation: colorSat, GrayscaleSaturation: grayscaleSat, ColorLightness: LightnessRange{ Min: colorLightMin, Max: colorLightMax, }, GrayscaleLightness: LightnessRange{ Min: grayscaleLightMin, Max: grayscaleLightMax, }, IconPadding: padding, } // Validation should never panic err := config.Validate() _ = err // If validation passes, test that we can create a generator if err == nil { genConfig := GeneratorConfig{ ColorConfig: config, CacheSize: 10, } generator, genErr := NewGeneratorWithConfig(genConfig) if genErr == nil && generator != nil { // Try to generate an icon icon, iconErr := generator.Generate(context.Background(), "test-hash", 64.0) if iconErr == nil && icon != nil { // Verify the icon has valid properties if icon.Size != 64.0 { t.Errorf("Icon size mismatch: expected 64.0, got %f", icon.Size) } } } } }) } // FuzzParseHex tests the hex parsing function with arbitrary inputs func FuzzParseHex(f *testing.F) { // Seed with various hex patterns f.Add("abcdef123456", 0, 1) f.Add("0123456789", 5, 2) f.Add("", 0, 1) f.Add("xyz", 0, 1) f.Add("ffffffffff", 10, 5) f.Fuzz(func(t *testing.T, hash string, position, octets int) { // ParseHex should never panic, even with invalid inputs result, err := util.ParseHex(hash, position, octets) // Determine the actual slice being parsed (mimic ParseHex logic) startPosition := position if startPosition < 0 { startPosition = len(hash) + startPosition } // Only check substring if it would be valid to parse if startPosition >= 0 && startPosition < len(hash) { end := len(hash) if octets > 0 { end = startPosition + octets if end > len(hash) { end = len(hash) } } // Extract the substring that ParseHex would actually process if startPosition < end { substr := hash[startPosition:end] // Check if the relevant substring contains invalid hex characters isInvalidHex := containsNonHex(substr) if isInvalidHex && err == nil { t.Errorf("ParseHex should have returned an error for invalid hex substring %q, but didn't", substr) } } } // Check for position out of bounds (after negative position handling) if startPosition >= len(hash) && len(hash) > 0 && err == nil { t.Errorf("ParseHex should return error for position %d >= hash length %d", startPosition, len(hash)) } if err != nil { return // Correctly returned an error } // On success, verify the result is reasonable _ = result // Result could be any valid integer }) } // FuzzColorGeneration tests color generation with arbitrary hue values func FuzzColorGeneration(f *testing.F) { // Seed with various hue values f.Add(0.0, 0.5) f.Add(0.5, 0.7) f.Add(1.0, 0.3) f.Add(-0.1, 0.9) f.Add(1.1, 0.1) f.Fuzz(func(t *testing.T, hue, lightnessValue float64) { // Skip extreme values that might cause issues if math.IsNaN(hue) || math.IsInf(hue, 0) || math.IsNaN(lightnessValue) || math.IsInf(lightnessValue, 0) { return } config := DefaultColorConfig() // Test actual production color generation functions color := GenerateColor(hue, config, lightnessValue) // Verify color has reasonable RGB values (0-255) r, g, b, err := color.ToRGB() if err != nil { t.Errorf("color.ToRGB failed: %v", err) return } // RGB values are uint8, so they're guaranteed to be in 0-255 range _, _, _ = r, g, b // Test grayscale generation as well grayscale := GenerateGrayscale(config, lightnessValue) gr, gg, gb, err := grayscale.ToRGB() if err != nil { t.Errorf("grayscale.ToRGB failed: %v", err) return } // RGB values are uint8, so they're guaranteed to be in 0-255 range _, _, _ = gr, gg, gb // Test color theme generation theme := GenerateColorTheme(hue, config) if len(theme) != 5 { t.Errorf("GenerateColorTheme should return 5 colors, got %d", len(theme)) } for _, themeColor := range theme { tr, tg, tb, err := themeColor.ToRGB() if err != nil { t.Errorf("themeColor.ToRGB failed: %v", err) continue } // RGB values are uint8, so they're guaranteed to be in 0-255 range _, _, _ = tr, tg, tb } }) } // FuzzHexColorParsing tests hex color parsing with arbitrary strings func FuzzHexColorParsing(f *testing.F) { // Seed with various hex color patterns f.Add("#ffffff") f.Add("#000000") f.Add("#fff") f.Add("#12345678") f.Add("invalid") f.Add("") f.Add("#") f.Add("#gggggg") f.Fuzz(func(t *testing.T, colorStr string) { // ValidateHexColor should never panic err := ValidateHexColor(colorStr) _ = err // If validation passes, try parsing if err == nil { color, parseErr := ParseHexColorToEngine(colorStr) if parseErr == nil { // Verify parsed color has valid properties r, g, b, err := color.ToRGB() if err != nil { t.Errorf("color.ToRGB failed: %v", err) return } // RGB and alpha values are uint8, so they're guaranteed to be in 0-255 range _, _, _ = r, g, b _ = color.A } } }) } // FuzzGeneratorCaching tests generator caching behavior with arbitrary inputs func FuzzGeneratorCaching(f *testing.F) { // Seed with various cache scenarios f.Add("hash1", 64.0, "hash2", 128.0) f.Add("same", 64.0, "same", 64.0) f.Add("", 1.0, "different", 1.0) f.Fuzz(func(t *testing.T, hash1 string, size1 float64, hash2 string, size2 float64) { // Skip invalid sizes if size1 <= 0 || size1 > 1000 || size2 <= 0 || size2 > 1000 { return } if math.IsNaN(size1) || math.IsInf(size1, 0) || math.IsNaN(size2) || math.IsInf(size2, 0) { return } config := GeneratorConfig{ ColorConfig: DefaultColorConfig(), CacheSize: 10, } generator, err := NewGeneratorWithConfig(config) if err != nil { return } // Generate first icon icon1, err1 := generator.Generate(context.Background(), hash1, size1) if err1 != nil { return } // Check cache metrics after first generation initialHits, initialMisses := generator.GetCacheMetrics() // Generate second icon (might be cache hit if same) icon2, err2 := generator.Generate(context.Background(), hash2, size2) if err2 != nil { return } // Verify cache behavior finalSize := generator.GetCacheSize() finalHits, finalMisses := generator.GetCacheMetrics() // Cache size should not exceed capacity if finalSize > generator.GetCacheCapacity() { t.Errorf("Cache size %d exceeds capacity %d", finalSize, generator.GetCacheCapacity()) } // Metrics should increase appropriately if finalHits < initialHits || finalMisses < initialMisses { t.Errorf("Cache metrics decreased: hits %d->%d, misses %d->%d", initialHits, finalHits, initialMisses, finalMisses) } // If same hash and size, should be cache hit if hash1 == hash2 && size1 == size2 && icon1 != nil && icon2 != nil { if finalHits <= initialHits { t.Errorf("Expected cache hit for identical inputs, but hits did not increase. Initial: %d, Final: %d", initialHits, finalHits) } } // Clear cache should not panic and should reset metrics generator.ClearCache() clearedSize := generator.GetCacheSize() clearedHits, clearedMisses := generator.GetCacheMetrics() if clearedSize != 0 { t.Errorf("Cache size after clear: expected 0, got %d", clearedSize) } if clearedHits != 0 || clearedMisses != 0 { t.Errorf("Metrics after clear: expected 0,0 got %d,%d", clearedHits, clearedMisses) } }) } // FuzzLightnessRangeOperations tests lightness range calculations func FuzzLightnessRangeOperations(f *testing.F) { // Seed with various lightness range values f.Add(0.0, 1.0, 0.5) f.Add(0.4, 0.8, 0.7) f.Add(-0.1, 1.1, 0.5) f.Add(0.9, 0.1, 0.5) // Invalid range (min > max) f.Fuzz(func(t *testing.T, min, max, value float64) { // Skip NaN and infinite values if math.IsNaN(min) || math.IsInf(min, 0) || math.IsNaN(max) || math.IsInf(max, 0) || math.IsNaN(value) || math.IsInf(value, 0) { return } lightnessRange := LightnessRange{Min: min, Max: max} // Test actual production LightnessRange.GetLightness method result := lightnessRange.GetLightness(value) // GetLightness should never panic and should return a valid result if math.IsNaN(result) || math.IsInf(result, 0) { t.Errorf("GetLightness(%f) with range [%f, %f] returned invalid result: %f", value, min, max, result) } // Result should be clamped to [0, 1] range if result < 0 || result > 1 { t.Errorf("GetLightness(%f) with range [%f, %f] returned out-of-range result: %f", value, min, max, result) } // If input range is valid and value is in [0,1], result should be in range if min >= 0 && max <= 1 && min <= max && value >= 0 && value <= 1 { expectedMin := math.Min(min, max) expectedMax := math.Max(min, max) if result < expectedMin || result > expectedMax { // Allow for floating point precision issues if math.Abs(result-expectedMin) > 1e-10 && math.Abs(result-expectedMax) > 1e-10 { t.Errorf("GetLightness(%f) with valid range [%f, %f] returned result %f outside expected range [%f, %f]", value, min, max, result, expectedMin, expectedMax) } } } }) } // containsNonHex checks if a string contains non-hexadecimal characters // Note: strconv.ParseInt allows negative hex numbers, so '-' is valid at the start func containsNonHex(s string) bool { for i, r := range s { isHexDigit := (r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F') isValidMinus := (r == '-' && i == 0) // Minus only valid at start if !isHexDigit && !isValidMinus { return true } } return false }