Files
go-jdenticon/internal/engine/generator_core_test.go
Kevin McIntyre d9e84812ff 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
2026-01-03 23:41:48 -05:00

636 lines
15 KiB
Go

package engine
import (
"context"
"fmt"
"testing"
"github.com/ungluedlabs/go-jdenticon/internal/util"
)
func TestNewGenerator(t *testing.T) {
config := DefaultColorConfig()
generator, err := NewGenerator(config)
if err != nil {
t.Fatalf("NewGenerator failed: %v", err)
}
if generator == nil {
t.Fatal("NewGenerator returned nil")
}
if generator.config.ColorConfig.IconPadding != config.IconPadding {
t.Errorf("Expected icon padding %f, got %f", config.IconPadding, generator.config.ColorConfig.IconPadding)
}
if generator.cache == nil {
t.Error("Generator cache was not initialized")
}
}
func TestNewDefaultGenerator(t *testing.T) {
generator, err := NewDefaultGenerator()
if err != nil {
t.Fatalf("NewDefaultGenerator failed: %v", err)
}
if generator == nil {
t.Fatal("NewDefaultGenerator returned nil")
}
expectedConfig := DefaultColorConfig()
if generator.config.ColorConfig.IconPadding != expectedConfig.IconPadding {
t.Errorf("Expected icon padding %f, got %f", expectedConfig.IconPadding, generator.config.ColorConfig.IconPadding)
}
}
func TestNewGeneratorWithConfig(t *testing.T) {
config := GeneratorConfig{
ColorConfig: DefaultColorConfig(),
CacheSize: 500,
}
generator, err := NewGeneratorWithConfig(config)
if err != nil {
t.Fatalf("NewGeneratorWithConfig failed: %v", err)
}
if generator == nil {
t.Fatal("NewGeneratorWithConfig returned nil")
}
if generator.config.CacheSize != 500 {
t.Errorf("Expected cache size 500, got %d", generator.config.CacheSize)
}
}
func TestDefaultGeneratorConfig(t *testing.T) {
config := DefaultGeneratorConfig()
if config.CacheSize != 1000 {
t.Errorf("Expected default cache size 1000, got %d", config.CacheSize)
}
if config.MaxComplexity != 0 {
t.Errorf("Expected default max complexity 0, got %d", config.MaxComplexity)
}
}
func TestExtractHue(t *testing.T) {
generator, err := NewDefaultGenerator()
if err != nil {
t.Fatalf("NewDefaultGenerator failed: %v", err)
}
tests := []struct {
name string
hash string
expectedHue float64
expectsError bool
}{
{
name: "Valid 40-character hash",
hash: "abcdef1234567890abcdef1234567890abcdef12",
expectedHue: float64(0xbcdef12) / float64(0xfffffff),
expectsError: false,
},
{
name: "Valid hash with different values",
hash: "1234567890abcdef1234567890abcdef12345678",
expectedHue: float64(0x2345678) / float64(0xfffffff),
expectsError: false,
},
{
name: "Hash too short",
hash: "abc",
expectedHue: 0,
expectsError: true,
},
{
name: "Invalid hex characters",
hash: "abcdef1234567890abcdef1234567890abcdefgh",
expectedHue: 0,
expectsError: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
hue, err := generator.extractHue(test.hash)
if test.expectsError {
if err == nil {
t.Errorf("Expected error for hash %s, but got none", test.hash)
}
return
}
if err != nil {
t.Errorf("Unexpected error for hash %s: %v", test.hash, err)
return
}
if fmt.Sprintf("%.6f", hue) != fmt.Sprintf("%.6f", test.expectedHue) {
t.Errorf("Expected hue %.6f, got %.6f", test.expectedHue, hue)
}
})
}
}
func TestSelectColors(t *testing.T) {
generator, err := NewDefaultGenerator()
if err != nil {
t.Fatalf("NewDefaultGenerator failed: %v", err)
}
availableColors := []Color{
{H: 0.0, S: 1.0, L: 0.5, A: 255}, // Red
{H: 0.33, S: 1.0, L: 0.5, A: 255}, // Green
{H: 0.67, S: 1.0, L: 0.5, A: 255}, // Blue
{H: 0.17, S: 1.0, L: 0.5, A: 255}, // Yellow
{H: 0.0, S: 0.0, L: 0.5, A: 255}, // Gray
}
hash := "abcdef1234567890abcdef1234567890abcdef12"
selectedIndexes, err := generator.selectColors(hash, availableColors)
if err != nil {
t.Fatalf("selectColors failed: %v", err)
}
if len(selectedIndexes) != 3 {
t.Errorf("Expected 3 selected color indexes, got %d", len(selectedIndexes))
}
for i, index := range selectedIndexes {
if index < 0 || index >= len(availableColors) {
t.Errorf("Selected index %d at position %d is out of range [0, %d)", index, i, len(availableColors))
}
}
}
func TestSelectColorsEmptyPalette(t *testing.T) {
generator, err := NewDefaultGenerator()
if err != nil {
t.Fatalf("NewDefaultGenerator failed: %v", err)
}
hash := "abcdef1234567890abcdef1234567890abcdef12"
_, err = generator.selectColors(hash, []Color{})
if err == nil {
t.Error("Expected error for empty color palette, but got none")
}
}
func TestConsistentGeneration(t *testing.T) {
generator, err := NewDefaultGenerator()
if err != nil {
t.Fatalf("NewDefaultGenerator failed: %v", err)
}
hash := "abcdef1234567890abcdef1234567890abcdef12"
size := 64.0
icon1, err := generator.GenerateWithoutCache(context.Background(), hash, size)
if err != nil {
t.Fatalf("First generation failed: %v", err)
}
icon2, err := generator.GenerateWithoutCache(context.Background(), hash, size)
if err != nil {
t.Fatalf("Second generation failed: %v", err)
}
if icon1.Hash != icon2.Hash {
t.Error("Icons have different hashes")
}
if icon1.Size != icon2.Size {
t.Error("Icons have different sizes")
}
if len(icon1.Shapes) != len(icon2.Shapes) {
t.Errorf("Icons have different number of shape groups: %d vs %d", len(icon1.Shapes), len(icon2.Shapes))
}
for i, group1 := range icon1.Shapes {
group2 := icon2.Shapes[i]
if len(group1.Shapes) != len(group2.Shapes) {
t.Errorf("Shape group %d has different number of shapes: %d vs %d", i, len(group1.Shapes), len(group2.Shapes))
}
}
}
func TestIsColorInForbiddenSet(t *testing.T) {
tests := []struct {
name string
index int
forbidden []int
expected bool
}{
{
name: "Index in forbidden set",
index: 2,
forbidden: []int{0, 2, 4},
expected: true,
},
{
name: "Index not in forbidden set",
index: 1,
forbidden: []int{0, 2, 4},
expected: false,
},
{
name: "Empty forbidden set",
index: 1,
forbidden: []int{},
expected: false,
},
{
name: "Single element forbidden set - match",
index: 5,
forbidden: []int{5},
expected: true,
},
{
name: "Single element forbidden set - no match",
index: 3,
forbidden: []int{5},
expected: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := isColorInForbiddenSet(test.index, test.forbidden)
if result != test.expected {
t.Errorf("Expected %v, got %v", test.expected, result)
}
})
}
}
func TestHasSelectedColorInForbiddenSet(t *testing.T) {
tests := []struct {
name string
selected []int
forbidden []int
expected bool
}{
{
name: "No overlap",
selected: []int{1, 3, 5},
forbidden: []int{0, 2, 4},
expected: false,
},
{
name: "Partial overlap",
selected: []int{1, 2, 5},
forbidden: []int{0, 2, 4},
expected: true,
},
{
name: "Complete overlap",
selected: []int{0, 2, 4},
forbidden: []int{0, 2, 4},
expected: true,
},
{
name: "Empty selected",
selected: []int{},
forbidden: []int{0, 2, 4},
expected: false,
},
{
name: "Empty forbidden",
selected: []int{1, 3, 5},
forbidden: []int{},
expected: false,
},
{
name: "Both empty",
selected: []int{},
forbidden: []int{},
expected: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := hasSelectedColorInForbiddenSet(test.selected, test.forbidden)
if result != test.expected {
t.Errorf("Expected %v, got %v", test.expected, result)
}
})
}
}
func TestIsDuplicateColorRefactored(t *testing.T) {
generator, err := NewDefaultGenerator()
if err != nil {
t.Fatalf("NewDefaultGenerator failed: %v", err)
}
tests := []struct {
name string
index int
selected []int
forbidden []int
expected bool
}{
{
name: "Index not in forbidden set",
index: 1,
selected: []int{0, 4},
forbidden: []int{0, 4},
expected: false,
},
{
name: "Index in forbidden set, no selected colors in forbidden set",
index: 0,
selected: []int{1, 3},
forbidden: []int{0, 4},
expected: false,
},
{
name: "Index in forbidden set, has selected colors in forbidden set",
index: 0,
selected: []int{1, 4},
forbidden: []int{0, 4},
expected: true,
},
{
name: "Dark gray and dark main conflict",
index: colorDarkGray,
selected: []int{colorDarkMain},
forbidden: []int{colorDarkGray, colorDarkMain},
expected: true,
},
{
name: "Light gray and light main conflict",
index: colorLightGray,
selected: []int{colorLightMain},
forbidden: []int{colorLightGray, colorLightMain},
expected: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := generator.isDuplicateColor(test.index, test.selected, test.forbidden)
if result != test.expected {
t.Errorf("Expected %v, got %v", test.expected, result)
}
})
}
}
func TestShapeCollector(t *testing.T) {
collector := &shapeCollector{}
// Test initial state
if len(collector.shapes) != 0 {
t.Error("Expected empty shapes slice initially")
}
// Test AddPolygon
points := []Point{{X: 0, Y: 0}, {X: 10, Y: 0}, {X: 5, Y: 10}}
collector.AddPolygon(points)
if len(collector.shapes) != 1 {
t.Errorf("Expected 1 shape after AddPolygon, got %d", len(collector.shapes))
}
shape := collector.shapes[0]
if shape.Type != "polygon" {
t.Errorf("Expected shape type 'polygon', got '%s'", shape.Type)
}
if len(shape.Points) != 3 {
t.Errorf("Expected 3 points, got %d", len(shape.Points))
}
// Test AddCircle
collector.AddCircle(Point{X: 5, Y: 5}, 20, false)
if len(collector.shapes) != 2 {
t.Errorf("Expected 2 shapes after AddCircle, got %d", len(collector.shapes))
}
circleShape := collector.shapes[1]
if circleShape.Type != "circle" {
t.Errorf("Expected shape type 'circle', got '%s'", circleShape.Type)
}
if circleShape.CircleX != 5 {
t.Errorf("Expected CircleX 5, got %f", circleShape.CircleX)
}
if circleShape.CircleY != 5 {
t.Errorf("Expected CircleY 5, got %f", circleShape.CircleY)
}
if circleShape.CircleSize != 20 {
t.Errorf("Expected CircleSize 20, got %f", circleShape.CircleSize)
}
if circleShape.Invert != false {
t.Errorf("Expected Invert false, got %v", circleShape.Invert)
}
// Test Reset
collector.Reset()
if len(collector.shapes) != 0 {
t.Errorf("Expected empty shapes slice after Reset, got %d", len(collector.shapes))
}
// Test that we can add shapes again after reset
collector.AddPolygon([]Point{{X: 1, Y: 1}})
if len(collector.shapes) != 1 {
t.Errorf("Expected 1 shape after Reset and AddPolygon, got %d", len(collector.shapes))
}
}
func TestIsValidHash(t *testing.T) {
tests := []struct {
name string
hash string
expected bool
}{
{
name: "Valid 40-character hex hash",
hash: "abcdef1234567890abcdef1234567890abcdef12",
expected: true,
},
{
name: "Valid 32-character hex hash",
hash: "abcdef1234567890abcdef1234567890",
expected: true,
},
{
name: "Empty hash",
hash: "",
expected: false,
},
{
name: "Hash too short",
hash: "abc",
expected: false,
},
{
name: "Hash with invalid characters",
hash: "abcdef1234567890abcdef1234567890abcdefgh",
expected: false,
},
{
name: "Hash with uppercase letters",
hash: "ABCDEF1234567890ABCDEF1234567890ABCDEF12",
expected: true,
},
{
name: "Mixed case hash",
hash: "AbCdEf1234567890aBcDeF1234567890AbCdEf12",
expected: true,
},
{
name: "Hash with spaces",
hash: "abcdef12 34567890abcdef1234567890abcdef12",
expected: false,
},
{
name: "All zeros",
hash: "0000000000000000000000000000000000000000",
expected: true,
},
{
name: "All f's",
hash: "ffffffffffffffffffffffffffffffffffffffff",
expected: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := util.IsValidHash(test.hash)
if result != test.expected {
t.Errorf("Expected %v for hash '%s', got %v", test.expected, test.hash, result)
}
})
}
}
func TestParseHex(t *testing.T) {
tests := []struct {
name string
hash string
position int
octets int
expected int
expectsError bool
}{
{
name: "Valid single octet",
hash: "abcdef1234567890",
position: 0,
octets: 1,
expected: 0xa,
expectsError: false,
},
{
name: "Valid two octets",
hash: "abcdef1234567890",
position: 1,
octets: 2,
expected: 0xbc,
expectsError: false,
},
{
name: "Position at end of hash",
hash: "abcdef12",
position: 7,
octets: 1,
expected: 0x2,
expectsError: false,
},
{
name: "Position beyond hash length",
hash: "abc",
position: 5,
octets: 1,
expected: 0,
expectsError: true,
},
{
name: "Octets extend beyond hash",
hash: "abcdef12",
position: 6,
octets: 3,
expected: 0x12, // Should read to end of hash
expectsError: false,
},
{
name: "Zero octets",
hash: "abcdef12",
position: 0,
octets: 0,
expected: 0xabcdef12, // Should read to end when octets is 0
expectsError: false,
},
{
name: "Negative position",
hash: "abcdef12",
position: -1,
octets: 1,
expected: 0x2, // Should read from end
expectsError: false,
},
{
name: "Empty hash",
hash: "",
position: 0,
octets: 1,
expected: 0,
expectsError: true,
},
{
name: "All f's",
hash: "ffffffff",
position: 0,
octets: 4,
expected: 0xffff,
expectsError: false,
},
{
name: "Mixed case",
hash: "AbCdEf12",
position: 2,
octets: 2,
expected: 0xcd,
expectsError: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := util.ParseHex(test.hash, test.position, test.octets)
if test.expectsError {
if err == nil {
t.Errorf("Expected error for ParseHex(%s, %d, %d), but got none", test.hash, test.position, test.octets)
}
return
}
if err != nil {
t.Errorf("Unexpected error for ParseHex(%s, %d, %d): %v", test.hash, test.position, test.octets, err)
return
}
if result != test.expected {
t.Errorf("Expected %d (0x%x), got %d (0x%x)", test.expected, test.expected, result, result)
}
})
}
}