Initial release: Go Jdenticon library v0.1.0
- 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
This commit is contained in:
412
internal/engine/fuzz_test.go
Normal file
412
internal/engine/fuzz_test.go
Normal file
@@ -0,0 +1,412 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user