Files
go-jdenticon/internal/engine/fuzz_test.go
Kevin McIntyre f1544ef49c
Some checks failed
CI / Test (Go 1.24.x, ubuntu-latest) (push) Successful in 1m53s
CI / Code Quality (push) Failing after 26s
CI / Security Scan (push) Failing after 11s
CI / Test Coverage (push) Successful in 1m13s
CI / Benchmarks (push) Failing after 10m22s
CI / Build CLI (push) Failing after 8s
Benchmarks / Run Benchmarks (push) Failing after 10m13s
Release / Test (push) Successful in 55s
Release / Build (amd64, darwin, ) (push) Failing after 12s
Release / Build (amd64, linux, ) (push) Failing after 6s
Release / Build (amd64, windows, .exe) (push) Failing after 12s
Release / Build (arm64, darwin, ) (push) Failing after 12s
Release / Build (arm64, linux, ) (push) Failing after 12s
Release / Release (push) Has been skipped
CI / Test (Go 1.24.x, macos-latest) (push) Has been cancelled
CI / Test (Go 1.24.x, windows-latest) (push) Has been cancelled
chore: update module path to gitea.dockr.co/kev/go-jdenticon
Move hosting from GitHub to private Gitea instance.
2026-02-10 10:07:57 -05:00

413 lines
12 KiB
Go

package engine
import (
"context"
"math"
"testing"
"gitea.dockr.co/kev/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
}