- 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
413 lines
12 KiB
Go
413 lines
12 KiB
Go
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
|
|
}
|